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::Read;
22use std::path::Path;
23
24pub const CATALOG_FILENAME: &str = "CATALOG";
25const MAGIC: &[u8; 8] = b"MONGRCAT";
26const CATALOG_FORMAT_VERSION: u16 = 1;
27pub const META_DEK_LEN: usize = 32;
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub enum TableState {
33 Live,
35 Dropped { at_epoch: u64 },
38 Building {
41 intended_name: String,
42 query_id: String,
43 created_at_unix_nanos: u64,
44 #[serde(default)]
45 replaces_table_id: Option<u64>,
46 },
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct CatalogEntry {
53 pub table_id: u64,
54 pub name: String,
55 pub schema: Schema,
56 pub state: TableState,
57 pub created_epoch: u64,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(deny_unknown_fields)]
63pub struct MaterializedViewEntry {
64 pub name: String,
65 pub query: String,
66 pub last_refresh_epoch: u64,
67 #[serde(default)]
68 pub incremental: Option<IncrementalAggregateView>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(deny_unknown_fields)]
73pub struct IncrementalAggregateView {
74 pub source_table: String,
75 pub source_table_id: u64,
76 pub group_column: u16,
77 pub group_output_column: u16,
78 pub outputs: Vec<IncrementalAggregateOutput>,
79 pub count_output_column: u16,
80 pub checkpoint_event_id: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(deny_unknown_fields)]
85pub struct IncrementalAggregateOutput {
86 pub output_column: u16,
87 pub kind: IncrementalAggregateKind,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub enum IncrementalAggregateKind {
92 Count,
93 Sum { source_column: u16 },
94}
95
96#[derive(Debug, Clone, Serialize, Default)]
103pub struct Catalog {
104 pub db_epoch: u64,
106 pub next_table_id: u64,
108 pub next_segment_no: u64,
110 pub tables: Vec<CatalogEntry>,
111 #[serde(default)]
112 pub procedures: Vec<ProcedureEntry>,
113 #[serde(default)]
114 pub triggers: Vec<TriggerEntry>,
115 #[serde(default)]
116 pub external_tables: Vec<ExternalTableEntry>,
117 #[serde(default)]
118 pub materialized_views: Vec<MaterializedViewEntry>,
119 #[serde(default)]
120 pub security: crate::security::SecurityCatalog,
121 #[serde(default)]
123 pub security_version: u64,
124 #[serde(default)]
126 pub users: Vec<crate::auth::UserEntry>,
127 #[serde(default)]
129 pub roles: Vec<crate::auth::RoleEntry>,
130 #[serde(default)]
132 pub next_user_id: u64,
133 #[serde(default)]
138 pub require_auth: bool,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub user_version: Option<i64>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub application_id: Option<i64>,
144}
145
146#[derive(Deserialize)]
147#[serde(deny_unknown_fields)]
148struct CatalogWire {
149 db_epoch: u64,
150 next_table_id: u64,
151 next_segment_no: u64,
152 tables: Vec<CatalogEntry>,
153 #[serde(default)]
154 procedures: Vec<ProcedureEntry>,
155 #[serde(default)]
156 triggers: Vec<TriggerEntry>,
157 #[serde(default)]
158 external_tables: Vec<ExternalTableEntry>,
159 #[serde(default)]
160 materialized_views: Vec<MaterializedViewEntry>,
161 #[serde(default)]
162 security: crate::security::SecurityCatalog,
163 #[serde(default)]
164 security_version: u64,
165 #[serde(default)]
166 users: Vec<crate::auth::UserEntry>,
167 #[serde(default)]
168 roles: Vec<crate::auth::RoleEntry>,
169 #[serde(default)]
170 next_user_id: u64,
171 #[serde(default)]
172 require_auth: bool,
173 #[serde(default)]
174 user_version: Option<i64>,
175 #[serde(default)]
176 application_id: Option<i64>,
177 #[serde(default)]
180 open_generation: Option<u64>,
181}
182
183impl<'de> Deserialize<'de> for Catalog {
184 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
185 where
186 D: serde::Deserializer<'de>,
187 {
188 let wire = CatalogWire::deserialize(deserializer)?;
189 let _ = wire.open_generation;
190 Ok(Self {
191 db_epoch: wire.db_epoch,
192 next_table_id: wire.next_table_id,
193 next_segment_no: wire.next_segment_no,
194 tables: wire.tables,
195 procedures: wire.procedures,
196 triggers: wire.triggers,
197 external_tables: wire.external_tables,
198 materialized_views: wire.materialized_views,
199 security: wire.security,
200 security_version: wire.security_version,
201 users: wire.users,
202 roles: wire.roles,
203 next_user_id: wire.next_user_id,
204 require_auth: wire.require_auth,
205 user_version: wire.user_version,
206 application_id: wire.application_id,
207 })
208 }
209}
210
211#[derive(Serialize, Deserialize)]
212#[serde(deny_unknown_fields)]
213struct CatalogEnvelope {
214 format_version: u16,
215 catalog: Catalog,
216}
217
218pub const GENERATION_FILENAME: &str = "generation";
225
226pub fn read_generation(root: &crate::durable_file::DurableRoot) -> Result<Option<u64>> {
230 let relative = Path::new("_meta").join(GENERATION_FILENAME);
231 match root.entry_exists(&relative) {
232 Ok(true) => {}
233 Ok(false) => return Ok(None),
234 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
235 Err(error) => return Err(error.into()),
236 }
237 let mut file = root.open_regular(&relative)?;
238 let length = file.metadata()?.len();
239 if length != 8 {
240 return Err(MongrelError::Other(format!(
241 "invalid open-generation length: got {length}, expected 8"
242 )));
243 }
244 let mut bytes = [0_u8; 8];
245 std::io::Read::read_exact(&mut file, &mut bytes)?;
246 Ok(Some(u64::from_le_bytes(bytes)))
247}
248
249pub fn write_generation(root: &crate::durable_file::DurableRoot, generation: u64) -> Result<()> {
254 root.create_directory_all("_meta")?;
255 root.write_atomic(
256 Path::new("_meta").join(GENERATION_FILENAME),
257 &generation.to_le_bytes(),
258 )?;
259 Ok(())
260}
261
262impl Catalog {
263 pub fn empty() -> Self {
265 Catalog::default()
266 }
267
268 pub fn live(&self, name: &str) -> Option<&CatalogEntry> {
270 self.tables
271 .iter()
272 .find(|t| t.name == name && matches!(t.state, TableState::Live))
273 }
274
275 pub(crate) fn building(&self, name: &str) -> Option<&CatalogEntry> {
276 self.tables
277 .iter()
278 .find(|table| table.name == name && matches!(table.state, TableState::Building { .. }))
279 }
280
281 pub(crate) fn building_for(&self, intended_name: &str) -> Option<&CatalogEntry> {
282 self.tables.iter().find(|table| {
283 matches!(
284 &table.state,
285 TableState::Building {
286 intended_name: candidate,
287 ..
288 } if candidate == intended_name
289 )
290 })
291 }
292}
293
294#[cfg(feature = "encryption")]
295fn seal(body: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
296 match meta_dek {
297 Some(dek) => crate::encryption::encrypt_blob(dek, body),
298 None => Ok(plaintext_frame(body)),
299 }
300}
301
302#[cfg(not(feature = "encryption"))]
303fn seal(body: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
304 Ok(plaintext_frame(body))
305}
306
307fn plaintext_frame(body: &[u8]) -> Vec<u8> {
308 let hash = Sha256::digest(body);
309 let mut out = Vec::with_capacity(body.len() + 8 + 32);
310 out.extend_from_slice(MAGIC);
311 out.extend_from_slice(&hash);
312 out.extend_from_slice(body);
313 out
314}
315
316pub fn write_atomic(
321 dir: &Path,
322 cat: &Catalog,
323 meta_dek: Option<&[u8; META_DEK_LEN]>,
324) -> Result<()> {
325 write_atomic_controlled(dir, cat, meta_dek, || Ok(()))
326}
327
328pub fn write_atomic_controlled<F>(
331 dir: &Path,
332 cat: &Catalog,
333 meta_dek: Option<&[u8; META_DEK_LEN]>,
334 before_publish: F,
335) -> Result<()>
336where
337 F: FnOnce() -> Result<()>,
338{
339 write_atomic_controlled_with_after(dir, cat, meta_dek, before_publish, || {})
340}
341
342pub(crate) fn write_atomic_controlled_with_after<F, A>(
347 dir: &Path,
348 cat: &Catalog,
349 meta_dek: Option<&[u8; META_DEK_LEN]>,
350 before_publish: F,
351 after_publish: A,
352) -> Result<()>
353where
354 F: FnOnce() -> Result<()>,
355 A: FnOnce(),
356{
357 let body = encode(cat)?;
358 let payload = seal(&body, meta_dek)?;
359
360 let root = crate::durable_file::DurableRoot::open(dir)?;
361 root.write_atomic_controlled_with_after(
362 CATALOG_FILENAME,
363 &payload,
364 before_publish,
365 after_publish,
366 )?;
367 Ok(())
368}
369
370#[cfg(feature = "encryption")]
371fn open_payload(bytes: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
372 match meta_dek {
373 Some(dek) => match crate::encryption::decrypt_blob(dek, bytes) {
374 Ok(body) => deserialize(&body),
375 Err(_) => Ok(None),
376 },
377 None => parse_plaintext(bytes),
378 }
379}
380
381#[cfg(not(feature = "encryption"))]
382fn open_payload(bytes: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
383 parse_plaintext(bytes)
384}
385
386pub(crate) fn encode(catalog: &Catalog) -> Result<Vec<u8>> {
387 serde_json::to_vec(&CatalogEnvelope {
388 format_version: CATALOG_FORMAT_VERSION,
389 catalog: catalog.clone(),
390 })
391 .map_err(|error| MongrelError::Other(format!("catalog serialize: {error}")))
392}
393
394pub(crate) fn write_durable(
395 root: &crate::durable_file::DurableRoot,
396 catalog: &Catalog,
397 meta_dek: Option<&[u8; META_DEK_LEN]>,
398) -> Result<()> {
399 let body = encode(catalog)?;
400 let payload = seal(&body, meta_dek)?;
401 root.write_atomic(CATALOG_FILENAME, &payload)?;
402 Ok(())
403}
404
405pub(crate) fn decode(body: &[u8]) -> Result<Catalog> {
406 let value: serde_json::Value = serde_json::from_slice(body)
407 .map_err(|error| MongrelError::Other(format!("catalog deserialize: {error}")))?;
408 let is_envelope = value.as_object().is_some_and(|object| {
409 object.contains_key("format_version") || object.contains_key("catalog")
410 });
411 if !is_envelope {
412 return serde_json::from_value(value)
415 .map_err(|error| MongrelError::Other(format!("catalog deserialize: {error}")));
416 }
417 let envelope: CatalogEnvelope = serde_json::from_value(value)
418 .map_err(|error| MongrelError::Other(format!("catalog deserialize: {error}")))?;
419 if envelope.format_version != CATALOG_FORMAT_VERSION {
420 return Err(MongrelError::Other(format!(
421 "unsupported catalog format version {}",
422 envelope.format_version
423 )));
424 }
425 Ok(envelope.catalog)
426}
427
428fn deserialize(body: &[u8]) -> Result<Option<Catalog>> {
429 decode(body).map(Some)
430}
431
432pub fn read(dir: &Path, meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
435 let p = dir.join(CATALOG_FILENAME);
436 let file = match crate::durable_file::open_regular_nofollow(&p) {
437 Ok(file) => file,
438 Err(MongrelError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
439 Err(e) => return Err(e),
440 };
441 read_file(file, meta_dek)
442}
443
444pub(crate) fn read_durable(
445 root: &crate::durable_file::DurableRoot,
446 meta_dek: Option<&[u8; META_DEK_LEN]>,
447) -> Result<Option<Catalog>> {
448 let file = match root.open_regular(CATALOG_FILENAME) {
449 Ok(file) => file,
450 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
451 Err(error) => return Err(error.into()),
452 };
453 read_file(file, meta_dek)
454}
455
456fn read_file(
457 file: std::fs::File,
458 meta_dek: Option<&[u8; META_DEK_LEN]>,
459) -> Result<Option<Catalog>> {
460 const MAX_CATALOG_BYTES: u64 = 64 * 1024 * 1024;
461 let length = file.metadata()?.len();
462 if length > MAX_CATALOG_BYTES {
463 return Err(MongrelError::ResourceLimitExceeded {
464 resource: "catalog bytes",
465 requested: usize::try_from(length).unwrap_or(usize::MAX),
466 limit: MAX_CATALOG_BYTES as usize,
467 });
468 }
469 let mut bytes = Vec::with_capacity(length as usize);
470 file.take(MAX_CATALOG_BYTES + 1).read_to_end(&mut bytes)?;
471 if bytes.len() as u64 != length {
472 return Err(MongrelError::Other(
473 "catalog length changed while reading".into(),
474 ));
475 }
476 open_payload(&bytes, meta_dek)
477}
478
479fn parse_plaintext(bytes: &[u8]) -> Result<Option<Catalog>> {
480 if bytes.len() < 8 + 32 || &bytes[..8] != MAGIC {
481 return Ok(None);
482 }
483 let (tag, body) = bytes[8..].split_at(32);
484 let calc = Sha256::digest(body);
485 if tag != calc.as_slice() {
486 return Ok(None);
488 }
489 deserialize(body)
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 #[test]
497 fn empty_catalog_default() {
498 let c = Catalog::empty();
499 assert_eq!(c.db_epoch, 0);
500 assert_eq!(c.next_table_id, 0);
501 assert!(c.tables.is_empty());
502 }
503}