Skip to main content

citadeldb_sql/
schema.rs

1//! Schema manager: in-memory cache of table schemas.
2
3use std::collections::HashMap;
4
5use citadel::Database;
6
7use crate::error::{Result, SqlError};
8use crate::types::TableSchema;
9
10const SCHEMA_TABLE: &[u8] = b"_schema";
11
12/// Manages table schemas in memory, backed by the `_schema` table.
13pub struct SchemaManager {
14    tables: HashMap<String, TableSchema>,
15    generation: u64,
16}
17
18impl SchemaManager {
19    /// Load all schemas from the database's `_schema` table.
20    pub fn load(db: &Database) -> Result<Self> {
21        let mut tables = HashMap::new();
22
23        let mut rtx = db.begin_read();
24        let mut parse_err: Option<crate::error::SqlError> = None;
25        let scan_result = rtx.table_for_each(SCHEMA_TABLE, |_key, value| {
26            match TableSchema::deserialize(value) {
27                Ok(schema) => { tables.insert(schema.name.clone(), schema); }
28                Err(e) => { parse_err = Some(e); }
29            }
30            Ok(())
31        });
32
33        match scan_result {
34            Ok(()) => {}
35            Err(citadel_core::Error::TableNotFound(_)) => {}
36            Err(e) => return Err(e.into()),
37        }
38        if let Some(e) = parse_err {
39            return Err(e);
40        }
41
42        Ok(Self { tables, generation: 0 })
43    }
44
45    pub fn get(&self, name: &str) -> Option<&TableSchema> {
46        let lower = name.to_ascii_lowercase();
47        self.tables.get(&lower)
48    }
49
50    pub fn contains(&self, name: &str) -> bool {
51        let lower = name.to_ascii_lowercase();
52        self.tables.contains_key(&lower)
53    }
54
55    pub fn generation(&self) -> u64 {
56        self.generation
57    }
58
59    pub fn register(&mut self, schema: TableSchema) {
60        let lower = schema.name.to_ascii_lowercase();
61        self.tables.insert(lower, schema);
62        self.generation += 1;
63    }
64
65    pub fn remove(&mut self, name: &str) -> Option<TableSchema> {
66        let lower = name.to_ascii_lowercase();
67        let result = self.tables.remove(&lower);
68        if result.is_some() {
69            self.generation += 1;
70        }
71        result
72    }
73
74    pub fn table_names(&self) -> Vec<&str> {
75        self.tables.keys().map(|s| s.as_str()).collect()
76    }
77
78    /// Persist a schema to the _schema table (called within a write txn).
79    pub fn save_schema(
80        wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
81        schema: &TableSchema,
82    ) -> Result<()> {
83        let lower = schema.name.to_ascii_lowercase();
84        let data = schema.serialize();
85        wtx.table_insert(SCHEMA_TABLE, lower.as_bytes(), &data)?;
86        Ok(())
87    }
88
89    /// Remove a schema from the _schema table (called within a write txn).
90    pub fn delete_schema(
91        wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
92        name: &str,
93    ) -> Result<()> {
94        let lower = name.to_ascii_lowercase();
95        wtx.table_delete(SCHEMA_TABLE, lower.as_bytes())
96            .map_err(|e| match e {
97                citadel_core::Error::TableNotFound(_) => {
98                    SqlError::TableNotFound(name.into())
99                }
100                other => SqlError::Storage(other),
101            })?;
102        Ok(())
103    }
104
105    /// Ensure the _schema table exists (called once per write).
106    pub fn ensure_schema_table(
107        wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
108    ) -> Result<()> {
109        // Try to create; ignore if already exists
110        match wtx.create_table(SCHEMA_TABLE) {
111            Ok(()) => Ok(()),
112            Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
113            Err(e) => Err(e.into()),
114        }
115    }
116}