1use crate::error::{MongrelError, Result};
38use crate::external_table::ExternalTableEntry;
39use crate::procedure::ProcedureEntry;
40use crate::schema::Schema;
41use crate::trigger::TriggerEntry;
42use serde::{Deserialize, Serialize};
43use sha2::{Digest, Sha256};
44use std::io::Read;
45use std::path::Path;
46
47pub const CATALOG_FILENAME: &str = "CATALOG";
48const MAGIC: &[u8; 8] = b"MONGRCAT";
49const CATALOG_FORMAT_VERSION: u16 = 1;
50pub const META_DEK_LEN: usize = 32;
52
53pub(crate) fn inject_hook(name: &'static str) -> Result<()> {
58 mongreldb_fault::inject(name).map_err(|fault| MongrelError::Other(fault.to_string()))
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63pub enum TableState {
64 Live,
66 Dropped { at_epoch: u64 },
69 Building {
72 intended_name: String,
73 query_id: String,
74 created_at_unix_nanos: u64,
75 #[serde(default)]
76 replaces_table_id: Option<u64>,
77 },
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct CatalogEntry {
84 pub table_id: u64,
85 pub name: String,
86 pub schema: Schema,
87 pub state: TableState,
88 pub created_epoch: u64,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(deny_unknown_fields)]
94pub struct MaterializedViewEntry {
95 pub name: String,
96 pub query: String,
97 pub last_refresh_epoch: u64,
98 #[serde(default)]
99 pub incremental: Option<IncrementalAggregateView>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103#[serde(deny_unknown_fields)]
104pub struct IncrementalAggregateView {
105 pub source_table: String,
106 pub source_table_id: u64,
107 pub group_column: u16,
108 pub group_output_column: u16,
109 pub outputs: Vec<IncrementalAggregateOutput>,
110 pub count_output_column: u16,
111 pub checkpoint_event_id: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115#[serde(deny_unknown_fields)]
116pub struct IncrementalAggregateOutput {
117 pub output_column: u16,
118 pub kind: IncrementalAggregateKind,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122pub enum IncrementalAggregateKind {
123 Count,
124 Sum { source_column: u16 },
125}
126
127#[derive(Debug, Clone, Serialize, Default)]
134pub struct Catalog {
135 pub db_epoch: u64,
137 pub next_table_id: u64,
139 pub next_segment_no: u64,
141 pub tables: Vec<CatalogEntry>,
142 #[serde(default)]
143 pub procedures: Vec<ProcedureEntry>,
144 #[serde(default)]
145 pub triggers: Vec<TriggerEntry>,
146 #[serde(default)]
147 pub external_tables: Vec<ExternalTableEntry>,
148 #[serde(default)]
149 pub materialized_views: Vec<MaterializedViewEntry>,
150 #[serde(default)]
151 pub security: crate::security::SecurityCatalog,
152 #[serde(default)]
154 pub security_version: u64,
155 #[serde(default)]
157 pub users: Vec<crate::auth::UserEntry>,
158 #[serde(default)]
160 pub roles: Vec<crate::auth::RoleEntry>,
161 #[serde(default)]
163 pub next_user_id: u64,
164 #[serde(default)]
169 pub require_auth: bool,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub user_version: Option<i64>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub application_id: Option<i64>,
175 #[serde(default, skip_serializing_if = "is_zero_u64")]
179 pub catalog_version: u64,
180 #[serde(default, skip_serializing_if = "Vec::is_empty")]
185 pub command_log: Vec<crate::catalog_cmds::CatalogCommandRecord>,
186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
189 pub resource_groups: Vec<crate::catalog_cmds::ResourceGroupDef>,
190 #[serde(default, skip_serializing_if = "Vec::is_empty")]
193 pub job_definitions: Vec<crate::catalog_cmds::JobDefinition>,
194}
195
196fn is_zero_u64(value: &u64) -> bool {
197 *value == 0
198}
199
200#[derive(Deserialize)]
201#[serde(deny_unknown_fields)]
202struct CatalogWire {
203 db_epoch: u64,
204 next_table_id: u64,
205 next_segment_no: u64,
206 tables: Vec<CatalogEntry>,
207 #[serde(default)]
208 procedures: Vec<ProcedureEntry>,
209 #[serde(default)]
210 triggers: Vec<TriggerEntry>,
211 #[serde(default)]
212 external_tables: Vec<ExternalTableEntry>,
213 #[serde(default)]
214 materialized_views: Vec<MaterializedViewEntry>,
215 #[serde(default)]
216 security: crate::security::SecurityCatalog,
217 #[serde(default)]
218 security_version: u64,
219 #[serde(default)]
220 users: Vec<crate::auth::UserEntry>,
221 #[serde(default)]
222 roles: Vec<crate::auth::RoleEntry>,
223 #[serde(default)]
224 next_user_id: u64,
225 #[serde(default)]
226 require_auth: bool,
227 #[serde(default)]
228 user_version: Option<i64>,
229 #[serde(default)]
230 application_id: Option<i64>,
231 #[serde(default)]
232 catalog_version: u64,
233 #[serde(default)]
234 command_log: Vec<crate::catalog_cmds::CatalogCommandRecord>,
235 #[serde(default)]
236 resource_groups: Vec<crate::catalog_cmds::ResourceGroupDef>,
237 #[serde(default)]
238 job_definitions: Vec<crate::catalog_cmds::JobDefinition>,
239 #[serde(default)]
242 open_generation: Option<u64>,
243}
244
245impl<'de> Deserialize<'de> for Catalog {
246 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
247 where
248 D: serde::Deserializer<'de>,
249 {
250 let wire = CatalogWire::deserialize(deserializer)?;
251 let _ = wire.open_generation;
252 Ok(Self {
253 db_epoch: wire.db_epoch,
254 next_table_id: wire.next_table_id,
255 next_segment_no: wire.next_segment_no,
256 tables: wire.tables,
257 procedures: wire.procedures,
258 triggers: wire.triggers,
259 external_tables: wire.external_tables,
260 materialized_views: wire.materialized_views,
261 security: wire.security,
262 security_version: wire.security_version,
263 users: wire.users,
264 roles: wire.roles,
265 next_user_id: wire.next_user_id,
266 require_auth: wire.require_auth,
267 user_version: wire.user_version,
268 application_id: wire.application_id,
269 catalog_version: wire.catalog_version,
270 command_log: wire.command_log,
271 resource_groups: wire.resource_groups,
272 job_definitions: wire.job_definitions,
273 })
274 }
275}
276
277#[derive(Serialize, Deserialize)]
278#[serde(deny_unknown_fields)]
279struct CatalogEnvelope {
280 format_version: u16,
281 catalog: Catalog,
282}
283
284pub const GENERATION_FILENAME: &str = "generation";
291
292pub fn read_generation(root: &crate::durable_file::DurableRoot) -> Result<Option<u64>> {
296 let relative = Path::new("_meta").join(GENERATION_FILENAME);
297 match root.entry_exists(&relative) {
298 Ok(true) => {}
299 Ok(false) => return Ok(None),
300 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
301 Err(error) => return Err(error.into()),
302 }
303 let mut file = root.open_regular(&relative)?;
304 let length = file.metadata()?.len();
305 if length != 8 {
306 return Err(MongrelError::Other(format!(
307 "invalid open-generation length: got {length}, expected 8"
308 )));
309 }
310 let mut bytes = [0_u8; 8];
311 std::io::Read::read_exact(&mut file, &mut bytes)?;
312 Ok(Some(u64::from_le_bytes(bytes)))
313}
314
315pub fn write_generation(root: &crate::durable_file::DurableRoot, generation: u64) -> Result<()> {
320 root.create_directory_all("_meta")?;
321 root.write_atomic(
322 Path::new("_meta").join(GENERATION_FILENAME),
323 &generation.to_le_bytes(),
324 )?;
325 Ok(())
326}
327
328impl Catalog {
329 pub fn empty() -> Self {
331 Catalog::default()
332 }
333
334 pub fn live(&self, name: &str) -> Option<&CatalogEntry> {
336 self.tables
337 .iter()
338 .find(|t| t.name == name && matches!(t.state, TableState::Live))
339 }
340
341 pub(crate) fn building(&self, name: &str) -> Option<&CatalogEntry> {
342 self.tables
343 .iter()
344 .find(|table| table.name == name && matches!(table.state, TableState::Building { .. }))
345 }
346
347 pub(crate) fn building_for(&self, intended_name: &str) -> Option<&CatalogEntry> {
348 self.tables.iter().find(|table| {
349 matches!(
350 &table.state,
351 TableState::Building {
352 intended_name: candidate,
353 ..
354 } if candidate == intended_name
355 )
356 })
357 }
358
359 pub fn catalog_version(&self) -> u64 {
362 self.catalog_version
363 }
364
365 pub fn commands_since(&self, version: u64) -> Vec<crate::catalog_cmds::CatalogCommandRecord> {
371 self.command_log
372 .iter()
373 .filter(|record| record.catalog_version > version)
374 .cloned()
375 .collect()
376 }
377
378 pub fn apply_command(
396 &mut self,
397 record: &crate::catalog_cmds::CatalogCommandRecord,
398 ) -> Result<crate::catalog_cmds::CatalogDelta> {
399 use crate::catalog_cmds::{
400 apply, encode_command, CatalogDelta, CATALOG_COMMAND_FORMAT_VERSION,
401 COMMAND_HISTORY_LIMIT,
402 };
403
404 if record.version != CATALOG_COMMAND_FORMAT_VERSION {
405 return Err(MongrelError::UnsupportedStorageVersion {
406 component: "catalog command",
407 found: record.version,
408 supported: CATALOG_COMMAND_FORMAT_VERSION,
409 });
410 }
411 if record.catalog_version <= self.catalog_version {
412 return match self
413 .command_log
414 .iter()
415 .find(|existing| existing.catalog_version == record.catalog_version)
416 {
417 Some(existing) if encode_command(existing)? == encode_command(record)? => {
418 Ok(CatalogDelta::NoOp)
419 }
420 Some(_) => Err(MongrelError::Conflict(format!(
421 "a different catalog command is already recorded at version {}",
422 record.catalog_version
423 ))),
424 None => Ok(CatalogDelta::NoOp),
426 };
427 }
428 let expected = self
429 .catalog_version
430 .checked_add(1)
431 .ok_or_else(|| MongrelError::Full("catalog version space exhausted".into()))?;
432 if record.catalog_version != expected {
433 return Err(MongrelError::Conflict(format!(
434 "catalog command version gap: got {}, expected {expected}",
435 record.catalog_version
436 )));
437 }
438 let delta = apply(self, &record.command)?;
439 delta.apply_to(self)?;
440 self.catalog_version = record.catalog_version;
441 self.command_log.push(record.clone());
442 if self.command_log.len() > COMMAND_HISTORY_LIMIT {
443 let overflow = self.command_log.len() - COMMAND_HISTORY_LIMIT;
444 self.command_log.drain(..overflow);
445 }
446 Ok(delta)
447 }
448
449 pub fn apply_command_and_checkpoint(
454 &mut self,
455 dir: &Path,
456 meta_dek: Option<&[u8; META_DEK_LEN]>,
457 record: &crate::catalog_cmds::CatalogCommandRecord,
458 ) -> Result<crate::catalog_cmds::CatalogDelta> {
459 let delta = self.apply_command(record)?;
460 write_atomic(dir, self, meta_dek)?;
461 Ok(delta)
462 }
463}
464
465#[cfg(feature = "encryption")]
466fn seal(body: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
467 match meta_dek {
468 Some(dek) => crate::encryption::encrypt_blob(dek, body),
469 None => Ok(plaintext_frame(body)),
470 }
471}
472
473#[cfg(not(feature = "encryption"))]
474fn seal(body: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Vec<u8>> {
475 Ok(plaintext_frame(body))
476}
477
478fn plaintext_frame(body: &[u8]) -> Vec<u8> {
479 let hash = Sha256::digest(body);
480 let mut out = Vec::with_capacity(body.len() + 8 + 32);
481 out.extend_from_slice(MAGIC);
482 out.extend_from_slice(&hash);
483 out.extend_from_slice(body);
484 out
485}
486
487pub fn write_atomic(
492 dir: &Path,
493 cat: &Catalog,
494 meta_dek: Option<&[u8; META_DEK_LEN]>,
495) -> Result<()> {
496 write_atomic_controlled(dir, cat, meta_dek, || Ok(()))
497}
498
499pub fn write_atomic_controlled<F>(
502 dir: &Path,
503 cat: &Catalog,
504 meta_dek: Option<&[u8; META_DEK_LEN]>,
505 before_publish: F,
506) -> Result<()>
507where
508 F: FnOnce() -> Result<()>,
509{
510 write_atomic_controlled_with_after(dir, cat, meta_dek, before_publish, || {})
511}
512
513pub(crate) fn write_atomic_controlled_with_after<F, A>(
518 dir: &Path,
519 cat: &Catalog,
520 meta_dek: Option<&[u8; META_DEK_LEN]>,
521 before_publish: F,
522 after_publish: A,
523) -> Result<()>
524where
525 F: FnOnce() -> Result<()>,
526 A: FnOnce(),
527{
528 let body = encode(cat)?;
529 let payload = seal(&body, meta_dek)?;
530
531 inject_hook("catalog.publish.before")?;
533 let root = crate::durable_file::DurableRoot::open(dir)?;
534 root.write_atomic_controlled_with_after(
535 CATALOG_FILENAME,
536 &payload,
537 before_publish,
538 after_publish,
539 )?;
540 inject_hook("catalog.publish.after")?;
543 Ok(())
544}
545
546#[cfg(feature = "encryption")]
547fn open_payload(bytes: &[u8], meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
548 match meta_dek {
549 Some(dek) => match crate::encryption::decrypt_blob(dek, bytes) {
550 Ok(body) => deserialize(&body),
551 Err(_) => Ok(None),
552 },
553 None => parse_plaintext(bytes),
554 }
555}
556
557#[cfg(not(feature = "encryption"))]
558fn open_payload(bytes: &[u8], _meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
559 parse_plaintext(bytes)
560}
561
562pub(crate) fn encode(catalog: &Catalog) -> Result<Vec<u8>> {
563 serde_json::to_vec(&CatalogEnvelope {
564 format_version: CATALOG_FORMAT_VERSION,
565 catalog: catalog.clone(),
566 })
567 .map_err(|error| MongrelError::Other(format!("catalog serialize: {error}")))
568}
569
570pub(crate) fn write_durable(
571 root: &crate::durable_file::DurableRoot,
572 catalog: &Catalog,
573 meta_dek: Option<&[u8; META_DEK_LEN]>,
574) -> Result<()> {
575 let body = encode(catalog)?;
576 let payload = seal(&body, meta_dek)?;
577 inject_hook("catalog.publish.before")?;
578 root.write_atomic(CATALOG_FILENAME, &payload)?;
579 inject_hook("catalog.publish.after")?;
580 Ok(())
581}
582
583pub(crate) fn decode(body: &[u8]) -> Result<Catalog> {
584 let value: serde_json::Value = serde_json::from_slice(body)
585 .map_err(|error| MongrelError::Other(format!("catalog deserialize: {error}")))?;
586 let is_envelope = value.as_object().is_some_and(|object| {
587 object.contains_key("format_version") || object.contains_key("catalog")
588 });
589 if !is_envelope {
590 return serde_json::from_value(value)
593 .map_err(|error| MongrelError::Other(format!("catalog deserialize: {error}")));
594 }
595 let envelope: CatalogEnvelope = serde_json::from_value(value)
596 .map_err(|error| MongrelError::Other(format!("catalog deserialize: {error}")))?;
597 if envelope.format_version != CATALOG_FORMAT_VERSION {
598 return Err(MongrelError::Other(format!(
599 "unsupported catalog format version {}",
600 envelope.format_version
601 )));
602 }
603 Ok(envelope.catalog)
604}
605
606fn deserialize(body: &[u8]) -> Result<Option<Catalog>> {
607 decode(body).map(Some)
608}
609
610pub fn read(dir: &Path, meta_dek: Option<&[u8; META_DEK_LEN]>) -> Result<Option<Catalog>> {
613 let p = dir.join(CATALOG_FILENAME);
614 let file = match crate::durable_file::open_regular_nofollow(&p) {
615 Ok(file) => file,
616 Err(MongrelError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
617 Err(e) => return Err(e),
618 };
619 read_file(file, meta_dek)
620}
621
622pub(crate) fn read_durable(
623 root: &crate::durable_file::DurableRoot,
624 meta_dek: Option<&[u8; META_DEK_LEN]>,
625) -> Result<Option<Catalog>> {
626 let file = match root.open_regular(CATALOG_FILENAME) {
627 Ok(file) => file,
628 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
629 Err(error) => return Err(error.into()),
630 };
631 read_file(file, meta_dek)
632}
633
634fn read_file(
635 file: std::fs::File,
636 meta_dek: Option<&[u8; META_DEK_LEN]>,
637) -> Result<Option<Catalog>> {
638 const MAX_CATALOG_BYTES: u64 = 64 * 1024 * 1024;
639 let length = file.metadata()?.len();
640 if length > MAX_CATALOG_BYTES {
641 return Err(MongrelError::ResourceLimitExceeded {
642 resource: "catalog bytes",
643 requested: usize::try_from(length).unwrap_or(usize::MAX),
644 limit: MAX_CATALOG_BYTES as usize,
645 });
646 }
647 let mut bytes = Vec::with_capacity(length as usize);
648 file.take(MAX_CATALOG_BYTES + 1).read_to_end(&mut bytes)?;
649 if bytes.len() as u64 != length {
650 return Err(MongrelError::Other(
651 "catalog length changed while reading".into(),
652 ));
653 }
654 open_payload(&bytes, meta_dek)
655}
656
657fn parse_plaintext(bytes: &[u8]) -> Result<Option<Catalog>> {
658 if bytes.len() < 8 + 32 || &bytes[..8] != MAGIC {
659 return Ok(None);
660 }
661 let (tag, body) = bytes[8..].split_at(32);
662 let calc = Sha256::digest(body);
663 if tag != calc.as_slice() {
664 return Ok(None);
666 }
667 deserialize(body)
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn empty_catalog_default() {
676 let c = Catalog::empty();
677 assert_eq!(c.db_epoch, 0);
678 assert_eq!(c.next_table_id, 0);
679 assert!(c.tables.is_empty());
680 }
681}