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