Skip to main content

citadel_sql/
schema.rs

1//! Schema manager: in-memory cache of table schemas.
2
3use rustc_hash::FxHashMap;
4
5use citadel::Database;
6
7use crate::error::{Result, SqlError};
8use crate::types::{ForeignKeySchemaEntry, TableSchema, ViewDef};
9
10const SCHEMA_TABLE: &[u8] = b"_schema";
11const VIEWS_TABLE: &[u8] = b"_views";
12
13/// Manages table schemas in memory, backed by the `_schema` table.
14pub struct SchemaManager {
15    tables: FxHashMap<String, TableSchema>,
16    views: FxHashMap<String, ViewDef>,
17    generation: u64,
18}
19
20#[derive(Clone)]
21pub struct SchemaSnapshot {
22    tables: FxHashMap<String, TableSchema>,
23    views: FxHashMap<String, ViewDef>,
24    generation: u64,
25}
26
27impl SchemaManager {
28    /// Load all schemas from the database's `_schema` table.
29    pub fn load(db: &Database) -> Result<Self> {
30        let mut tables = FxHashMap::default();
31
32        let mut rtx = db.begin_read();
33        let mut parse_err: Option<crate::error::SqlError> = None;
34        let scan_result = rtx.table_for_each(SCHEMA_TABLE, |_key, value| {
35            match TableSchema::deserialize(value) {
36                Ok(schema) => {
37                    tables.insert(schema.name.clone(), schema);
38                }
39                Err(e) => {
40                    parse_err = Some(e);
41                }
42            }
43            Ok(())
44        });
45
46        match scan_result {
47            Ok(()) => {}
48            Err(citadel_core::Error::TableNotFound(_)) => {}
49            Err(e) => return Err(e.into()),
50        }
51        if let Some(e) = parse_err {
52            return Err(e);
53        }
54
55        let mut views = FxHashMap::default();
56        let mut rtx2 = db.begin_read();
57        let mut view_err: Option<crate::error::SqlError> = None;
58        let view_scan = rtx2.table_for_each(VIEWS_TABLE, |_key, value| {
59            match ViewDef::deserialize(value) {
60                Ok(vd) => {
61                    views.insert(vd.name.clone(), vd);
62                }
63                Err(e) => {
64                    view_err = Some(e);
65                }
66            }
67            Ok(())
68        });
69
70        match view_scan {
71            Ok(()) => {}
72            Err(citadel_core::Error::TableNotFound(_)) => {}
73            Err(e) => return Err(e.into()),
74        }
75        if let Some(e) = view_err {
76            return Err(e);
77        }
78
79        Ok(Self {
80            tables,
81            views,
82            generation: 0,
83        })
84    }
85
86    pub fn get(&self, name: &str) -> Option<&TableSchema> {
87        if let Some(s) = self.tables.get(name) {
88            return Some(s);
89        }
90        if name.bytes().any(|b| b.is_ascii_uppercase()) {
91            self.tables.get(&name.to_ascii_lowercase())
92        } else {
93            None
94        }
95    }
96
97    pub fn contains(&self, name: &str) -> bool {
98        if self.tables.contains_key(name) {
99            return true;
100        }
101        if name.bytes().any(|b| b.is_ascii_uppercase()) {
102            self.tables.contains_key(&name.to_ascii_lowercase())
103        } else {
104            false
105        }
106    }
107
108    pub fn generation(&self) -> u64 {
109        self.generation
110    }
111
112    pub fn register(&mut self, schema: TableSchema) {
113        let lower = schema.name.to_ascii_lowercase();
114        self.tables.insert(lower, schema);
115        self.generation += 1;
116    }
117
118    pub fn remove(&mut self, name: &str) -> Option<TableSchema> {
119        let lower = name.to_ascii_lowercase();
120        let result = self.tables.remove(&lower);
121        if result.is_some() {
122            self.generation += 1;
123        }
124        result
125    }
126
127    pub fn table_names(&self) -> Vec<&str> {
128        self.tables.keys().map(|s| s.as_str()).collect()
129    }
130
131    /// Returns all table schemas.
132    pub fn all_schemas(&self) -> impl Iterator<Item = &TableSchema> {
133        self.tables.values()
134    }
135
136    pub fn get_view(&self, name: &str) -> Option<&ViewDef> {
137        if let Some(v) = self.views.get(name) {
138            return Some(v);
139        }
140        if name.bytes().any(|b| b.is_ascii_uppercase()) {
141            self.views.get(&name.to_ascii_lowercase())
142        } else {
143            None
144        }
145    }
146
147    pub fn register_view(&mut self, view: ViewDef) {
148        let lower = view.name.to_ascii_lowercase();
149        self.views.insert(lower, view);
150        self.generation += 1;
151    }
152
153    pub fn remove_view(&mut self, name: &str) -> Option<ViewDef> {
154        let lower = name.to_ascii_lowercase();
155        let result = self.views.remove(&lower);
156        if result.is_some() {
157            self.generation += 1;
158        }
159        result
160    }
161
162    pub fn view_names(&self) -> Vec<&str> {
163        self.views.keys().map(|s| s.as_str()).collect()
164    }
165
166    pub fn save_view(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, view: &ViewDef) -> Result<()> {
167        let lower = view.name.to_ascii_lowercase();
168        let data = view.serialize();
169        wtx.table_insert(VIEWS_TABLE, lower.as_bytes(), &data)?;
170        Ok(())
171    }
172
173    pub fn delete_view(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
174        let lower = name.to_ascii_lowercase();
175        wtx.table_delete(VIEWS_TABLE, lower.as_bytes())
176            .map_err(|e| match e {
177                citadel_core::Error::TableNotFound(_) => SqlError::ViewNotFound(name.into()),
178                other => SqlError::Storage(other),
179            })?;
180        Ok(())
181    }
182
183    pub fn ensure_views_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
184        match wtx.create_table(VIEWS_TABLE) {
185            Ok(()) => Ok(()),
186            Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
187            Err(e) => Err(e.into()),
188        }
189    }
190
191    /// Find all FKs in other tables that reference `parent` table.
192    pub fn child_fks_for(&self, parent: &str) -> Vec<(&str, &ForeignKeySchemaEntry)> {
193        self.tables
194            .iter()
195            .flat_map(|(name, schema)| {
196                schema
197                    .foreign_keys
198                    .iter()
199                    .filter(|fk| fk.foreign_table == parent)
200                    .map(move |fk| (name.as_str(), fk))
201            })
202            .collect()
203    }
204
205    /// Persist a schema to the _schema table (called within a write txn).
206    pub fn save_schema(
207        wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
208        schema: &TableSchema,
209    ) -> Result<()> {
210        let lower = schema.name.to_ascii_lowercase();
211        let data = schema.serialize();
212        wtx.table_insert(SCHEMA_TABLE, lower.as_bytes(), &data)?;
213        Ok(())
214    }
215
216    /// Remove a schema from the _schema table (called within a write txn).
217    pub fn delete_schema(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
218        let lower = name.to_ascii_lowercase();
219        wtx.table_delete(SCHEMA_TABLE, lower.as_bytes())
220            .map_err(|e| match e {
221                citadel_core::Error::TableNotFound(_) => SqlError::TableNotFound(name.into()),
222                other => SqlError::Storage(other),
223            })?;
224        Ok(())
225    }
226
227    /// Ensure the _schema table exists (called once per write).
228    pub fn ensure_schema_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
229        match wtx.create_table(SCHEMA_TABLE) {
230            Ok(()) => Ok(()),
231            Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
232            Err(e) => Err(e.into()),
233        }
234    }
235
236    pub fn save_snapshot(&self) -> SchemaSnapshot {
237        SchemaSnapshot {
238            tables: self.tables.clone(),
239            views: self.views.clone(),
240            generation: self.generation,
241        }
242    }
243
244    pub fn restore_snapshot(&mut self, snap: SchemaSnapshot) {
245        self.tables = snap.tables;
246        self.views = snap.views;
247        self.generation = snap.generation;
248    }
249}