1use crate::error::{MongrelError, Result};
15use crate::external_table::ExternalTableEntry;
16use crate::procedure::ProcedureEntry;
17use crate::schema::Schema;
18use crate::trigger::TriggerEntry;
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21use std::io::Write;
22use std::path::Path;
23
24pub const CATALOG_FILENAME: &str = "CATALOG";
25const MAGIC: &[u8; 8] = b"MONGRCAT";
26pub const META_DEK_LEN: usize = 32;
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub enum TableState {
32 Live,
34 Dropped { at_epoch: u64 },
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CatalogEntry {
42 pub table_id: u64,
43 pub name: String,
44 pub schema: Schema,
45 pub state: TableState,
46 pub created_epoch: u64,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51pub struct MaterializedViewEntry {
52 pub name: String,
53 pub query: String,
54 pub last_refresh_epoch: u64,
55 #[serde(default)]
56 pub incremental: Option<IncrementalAggregateView>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct IncrementalAggregateView {
61 pub source_table: String,
62 pub source_table_id: u64,
63 pub group_column: u16,
64 pub group_output_column: u16,
65 pub outputs: Vec<IncrementalAggregateOutput>,
66 pub count_output_column: u16,
67 pub checkpoint_event_id: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
71pub struct IncrementalAggregateOutput {
72 pub output_column: u16,
73 pub kind: IncrementalAggregateKind,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
77pub enum IncrementalAggregateKind {
78 Count,
79 Sum { source_column: u16 },
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, Default)]
89pub struct Catalog {
90 pub db_epoch: u64,
92 pub next_table_id: u64,
94 pub next_segment_no: u64,
96 pub tables: Vec<CatalogEntry>,
97 #[serde(default)]
98 pub procedures: Vec<ProcedureEntry>,
99 #[serde(default)]
100 pub triggers: Vec<TriggerEntry>,
101 #[serde(default)]
102 pub external_tables: Vec<ExternalTableEntry>,
103 #[serde(default)]
104 pub materialized_views: Vec<MaterializedViewEntry>,
105 #[serde(default)]
106 pub security: crate::security::SecurityCatalog,
107 #[serde(default)]
109 pub security_version: u64,
110 #[serde(default)]
112 pub users: Vec<crate::auth::UserEntry>,
113 #[serde(default)]
115 pub roles: Vec<crate::auth::RoleEntry>,
116 #[serde(default)]
118 pub next_user_id: u64,
119 #[serde(default)]
124 pub require_auth: bool,
125}
126
127pub const GENERATION_FILENAME: &str = "generation";
134
135pub fn read_generation(meta_dir: &Path) -> u64 {
141 let p = meta_dir.join(GENERATION_FILENAME);
142 match std::fs::read(&p) {
143 Ok(bytes) => {
144 if bytes.len() >= 8 {
145 let mut arr = [0u8; 8];
146 arr.copy_from_slice(&bytes[..8]);
147 u64::from_le_bytes(arr)
148 } else {
149 0
150 }
151 }
152 Err(_) => 0,
153 }
154}
155
156pub fn write_generation(meta_dir: &Path, generation: u64) -> Result<()> {
161 std::fs::create_dir_all(meta_dir)?;
162 let tmp = meta_dir.join(format!(".{GENERATION_FILENAME}.tmp"));
163 {
164 let mut f = std::fs::File::create(&tmp)?;
165 f.write_all(&generation.to_le_bytes())?;
166 f.sync_all()?;
167 }
168 let dest = meta_dir.join(GENERATION_FILENAME);
169 std::fs::rename(&tmp, &dest)?;
170 if let Ok(d) = std::fs::File::open(meta_dir) {
171 let _ = d.sync_all();
172 }
173 Ok(())
174}
175
176impl Catalog {
177 pub fn empty() -> Self {
179 Catalog::default()
180 }
181
182 pub fn live(&self, name: &str) -> Option<&CatalogEntry> {
184 self.tables
185 .iter()
186 .find(|t| t.name == name && matches!(t.state, TableState::Live))
187 }
188}
189
190#[cfg(feature = "encryption")]
191fn seal(body: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
192 match meta_dek {
193 Some(dek) => crate::encryption::encrypt_blob(dek, body),
194 None => Ok(plaintext_frame(body)),
195 }
196}
197
198#[cfg(not(feature = "encryption"))]
199fn seal(body: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
200 Ok(plaintext_frame(body))
201}
202
203fn plaintext_frame(body: &[u8]) -> Vec<u8> {
204 let hash = Sha256::digest(body);
205 let mut out = Vec::with_capacity(body.len() + 8 + 32);
206 out.extend_from_slice(MAGIC);
207 out.extend_from_slice(&hash);
208 out.extend_from_slice(body);
209 out
210}
211
212pub fn write_atomic(
217 dir: &Path,
218 cat: &Catalog,
219 meta_dek: Option<&[u8; META_DEK_LEN]>,
220) -> Result<()> {
221 let body = serde_json::to_vec(cat)
222 .map_err(|e| MongrelError::Other(format!("catalog serialize: {e}")))?;
223 let payload = seal(&body, meta_dek)?;
224
225 let tmp = dir.join(format!(".{CATALOG_FILENAME}.tmp"));
226 {
227 let mut f = std::fs::File::create(&tmp)?;
228 f.write_all(&payload)?;
229 f.sync_all()?;
230 }
231 let dest = dir.join(CATALOG_FILENAME);
232 std::fs::rename(&tmp, &dest)?;
233 if let Ok(d) = std::fs::File::open(dir) {
235 let _ = d.sync_all();
236 }
237 Ok(())
238}
239
240#[cfg(feature = "encryption")]
241fn open_payload(bytes: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
242 match meta_dek {
243 Some(dek) => match crate::encryption::decrypt_blob(dek, bytes) {
244 Ok(body) => deserialize(&body),
245 Err(_) => Ok(None),
246 },
247 None => parse_plaintext(bytes),
248 }
249}
250
251#[cfg(not(feature = "encryption"))]
252fn open_payload(bytes: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
253 parse_plaintext(bytes)
254}
255
256fn deserialize(body: &[u8]) -> Result<Option<Catalog>> {
257 serde_json::from_slice(body)
258 .map(Some)
259 .map_err(|e| MongrelError::Other(format!("catalog deserialize: {e}")))
260}
261
262pub fn read(dir: &Path, meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
265 let p = dir.join(CATALOG_FILENAME);
266 let bytes = match std::fs::read(&p) {
267 Ok(b) => b,
268 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
269 Err(e) => return Err(e.into()),
270 };
271 open_payload(&bytes, meta_dek)
272}
273
274fn parse_plaintext(bytes: &[u8]) -> Result<Option<Catalog>> {
275 if bytes.len() < 8 + 32 || &bytes[..8] != MAGIC {
276 return Ok(None);
277 }
278 let (tag, body) = bytes[8..].split_at(32);
279 let calc = Sha256::digest(body);
280 if tag != calc.as_slice() {
281 return Ok(None);
283 }
284 deserialize(body)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn empty_catalog_default() {
293 let c = Catalog::empty();
294 assert_eq!(c.db_epoch, 0);
295 assert_eq!(c.next_table_id, 0);
296 assert!(c.tables.is_empty());
297 }
298}