1use 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
13pub 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 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 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 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 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 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 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}