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