Skip to main content

citadel_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::{ForeignKeySchemaEntry, 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) => {
28                    tables.insert(schema.name.clone(), schema);
29                }
30                Err(e) => {
31                    parse_err = Some(e);
32                }
33            }
34            Ok(())
35        });
36
37        match scan_result {
38            Ok(()) => {}
39            Err(citadel_core::Error::TableNotFound(_)) => {}
40            Err(e) => return Err(e.into()),
41        }
42        if let Some(e) = parse_err {
43            return Err(e);
44        }
45
46        Ok(Self {
47            tables,
48            generation: 0,
49        })
50    }
51
52    pub fn get(&self, name: &str) -> Option<&TableSchema> {
53        if let Some(s) = self.tables.get(name) {
54            return Some(s);
55        }
56        if name.bytes().any(|b| b.is_ascii_uppercase()) {
57            self.tables.get(&name.to_ascii_lowercase())
58        } else {
59            None
60        }
61    }
62
63    pub fn contains(&self, name: &str) -> bool {
64        if self.tables.contains_key(name) {
65            return true;
66        }
67        if name.bytes().any(|b| b.is_ascii_uppercase()) {
68            self.tables.contains_key(&name.to_ascii_lowercase())
69        } else {
70            false
71        }
72    }
73
74    pub fn generation(&self) -> u64 {
75        self.generation
76    }
77
78    pub fn register(&mut self, schema: TableSchema) {
79        let lower = schema.name.to_ascii_lowercase();
80        self.tables.insert(lower, schema);
81        self.generation += 1;
82    }
83
84    pub fn remove(&mut self, name: &str) -> Option<TableSchema> {
85        let lower = name.to_ascii_lowercase();
86        let result = self.tables.remove(&lower);
87        if result.is_some() {
88            self.generation += 1;
89        }
90        result
91    }
92
93    pub fn table_names(&self) -> Vec<&str> {
94        self.tables.keys().map(|s| s.as_str()).collect()
95    }
96
97    /// Returns all table schemas.
98    pub fn all_schemas(&self) -> impl Iterator<Item = &TableSchema> {
99        self.tables.values()
100    }
101
102    /// Find all FKs in other tables that reference `parent` table.
103    pub fn child_fks_for(&self, parent: &str) -> Vec<(&str, &ForeignKeySchemaEntry)> {
104        self.tables
105            .iter()
106            .flat_map(|(name, schema)| {
107                schema
108                    .foreign_keys
109                    .iter()
110                    .filter(|fk| fk.foreign_table == parent)
111                    .map(move |fk| (name.as_str(), fk))
112            })
113            .collect()
114    }
115
116    /// Persist a schema to the _schema table (called within a write txn).
117    pub fn save_schema(
118        wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
119        schema: &TableSchema,
120    ) -> Result<()> {
121        let lower = schema.name.to_ascii_lowercase();
122        let data = schema.serialize();
123        wtx.table_insert(SCHEMA_TABLE, lower.as_bytes(), &data)?;
124        Ok(())
125    }
126
127    /// Remove a schema from the _schema table (called within a write txn).
128    pub fn delete_schema(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
129        let lower = name.to_ascii_lowercase();
130        wtx.table_delete(SCHEMA_TABLE, lower.as_bytes())
131            .map_err(|e| match e {
132                citadel_core::Error::TableNotFound(_) => SqlError::TableNotFound(name.into()),
133                other => SqlError::Storage(other),
134            })?;
135        Ok(())
136    }
137
138    /// Ensure the _schema table exists (called once per write).
139    pub fn ensure_schema_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
140        // Try to create; ignore if already exists
141        match wtx.create_table(SCHEMA_TABLE) {
142            Ok(()) => Ok(()),
143            Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
144            Err(e) => Err(e.into()),
145        }
146    }
147}