1use std::sync::Arc;
4
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use citadel::{Database, SqlCacheHandle};
8use parking_lot::Mutex;
9
10use crate::error::{Result, SqlError};
11use crate::system_tables::{self, VirtualTable};
12use crate::types::{ForeignKeySchemaEntry, TableSchema, ViewDef};
13
14const SCHEMA_TABLE: &[u8] = b"_schema";
15const VIEWS_TABLE: &[u8] = b"_views";
16const TRIGGERS_TABLE: &[u8] = b"_triggers";
17const MATVIEWS_TABLE: &[u8] = b"_matviews";
18
19thread_local! {
20 static TRANSITION_TABLES: std::cell::RefCell<Vec<FxHashMap<String, String>>> =
23 const { std::cell::RefCell::new(Vec::new()) };
24}
25
26fn transition_table_lookup(name_lower: &str) -> Option<String> {
27 TRANSITION_TABLES.with(|cell| {
28 let stack = cell.borrow();
29 for frame in stack.iter().rev() {
30 if let Some(storage) = frame.get(name_lower) {
31 return Some(storage.clone());
32 }
33 }
34 None
35 })
36}
37
38pub(crate) fn push_transition_tables(aliases: FxHashMap<String, String>) -> TransitionGuard {
39 TRANSITION_TABLES.with(|cell| cell.borrow_mut().push(aliases));
40 TransitionGuard
41}
42
43pub(crate) struct TransitionGuard;
44impl Drop for TransitionGuard {
45 fn drop(&mut self) {
46 TRANSITION_TABLES.with(|cell| {
47 cell.borrow_mut().pop();
48 });
49 }
50}
51
52pub struct SchemaManager {
54 tables: FxHashMap<String, TableSchema>,
55 views: FxHashMap<String, ViewDef>,
56 virtual_tables: FxHashMap<String, Arc<dyn VirtualTable>>,
57 triggers: FxHashMap<String, Vec<crate::types::TriggerDef>>,
59 matviews: FxHashMap<String, crate::types::MatviewDef>,
62 temp_aliases: FxHashMap<String, String>,
64 transition_schemas: std::cell::RefCell<FxHashMap<String, &'static TableSchema>>,
67 generation: u64,
68 pub sql_caches: SqlCacheHandle,
72 dml_dirty_tables: std::cell::RefCell<FxHashSet<String>>,
75 dml_append_tables: std::cell::RefCell<FxHashMap<String, i64>>,
78}
79
80pub struct DmlDirty {
82 pub mutating: Vec<String>,
83 pub appends: Vec<(String, i64)>,
84}
85
86#[derive(Clone)]
87pub struct SchemaSnapshot {
88 tables: FxHashMap<String, TableSchema>,
89 views: FxHashMap<String, ViewDef>,
90 generation: u64,
91}
92
93impl SchemaManager {
94 pub fn empty() -> Self {
95 Self {
96 tables: FxHashMap::default(),
97 views: FxHashMap::default(),
98 virtual_tables: FxHashMap::default(),
99 triggers: FxHashMap::default(),
100 matviews: FxHashMap::default(),
101 temp_aliases: FxHashMap::default(),
102 transition_schemas: std::cell::RefCell::new(FxHashMap::default()),
103 generation: 0,
104 sql_caches: Arc::new(Mutex::new(FxHashMap::default())),
105 dml_dirty_tables: std::cell::RefCell::new(FxHashSet::default()),
106 dml_append_tables: std::cell::RefCell::new(FxHashMap::default()),
107 }
108 }
109
110 pub fn mark_dml(&self, table_name: &str) {
112 let lower = table_name.to_ascii_lowercase();
113 self.dml_append_tables.borrow_mut().remove(&lower);
114 self.dml_dirty_tables.borrow_mut().insert(lower);
115 }
116
117 pub fn mark_dml_append(&self, table_name: &str, min_pk: i64) {
119 let lower = table_name.to_ascii_lowercase();
120 if self.dml_dirty_tables.borrow().contains(&lower) {
121 return;
122 }
123 self.dml_append_tables
124 .borrow_mut()
125 .entry(lower)
126 .and_modify(|m| *m = (*m).min(min_pk))
127 .or_insert(min_pk);
128 }
129
130 pub fn drain_dml_dirty(&self) -> DmlDirty {
132 DmlDirty {
133 mutating: self.dml_dirty_tables.borrow_mut().drain().collect(),
134 appends: self.dml_append_tables.borrow_mut().drain().collect(),
135 }
136 }
137
138 pub fn clear_dml_dirty(&self) {
141 self.dml_dirty_tables.borrow_mut().clear();
142 self.dml_append_tables.borrow_mut().clear();
143 }
144
145 pub fn register_temp_alias(&mut self, user_name: &str, prefixed_name: String) {
146 self.temp_aliases
147 .insert(user_name.to_ascii_lowercase(), prefixed_name);
148 self.generation += 1;
149 }
150
151 pub fn unregister_temp_alias(&mut self, user_name: &str) -> Option<String> {
152 let lower = user_name.to_ascii_lowercase();
153 let removed = self.temp_aliases.remove(&lower);
154 if removed.is_some() {
155 self.generation += 1;
156 }
157 removed
158 }
159
160 pub fn temp_alias_iter(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
161 self.temp_aliases
162 .iter()
163 .map(|(k, v)| (k.as_str(), v.as_str()))
164 }
165
166 pub fn resolve_temp(&self, name: &str) -> String {
167 let lower = name.to_ascii_lowercase();
168 if let Some(prefixed) = self.temp_aliases.get(&lower) {
169 return prefixed.clone();
170 }
171 name.to_string()
172 }
173
174 pub fn load(db: &Database) -> Result<Self> {
175 let mut tables = FxHashMap::default();
176
177 let mut rtx = db.begin_read();
178 let mut parse_err: Option<crate::error::SqlError> = None;
179 let scan_result = rtx.table_for_each(SCHEMA_TABLE, |_key, value| {
180 match TableSchema::deserialize(value) {
181 Ok(schema) => {
182 tables.insert(schema.name.clone(), schema);
183 }
184 Err(e) => {
185 parse_err = Some(e);
186 }
187 }
188 Ok(())
189 });
190
191 match scan_result {
192 Ok(()) => {}
193 Err(citadel_core::Error::TableNotFound(_)) => {}
194 Err(e) => return Err(e.into()),
195 }
196 if let Some(e) = parse_err {
197 return Err(e);
198 }
199
200 let mut views = FxHashMap::default();
201 let mut rtx2 = db.begin_read();
202 let mut view_err: Option<crate::error::SqlError> = None;
203 let view_scan = rtx2.table_for_each(VIEWS_TABLE, |_key, value| {
204 match ViewDef::deserialize(value) {
205 Ok(vd) => {
206 views.insert(vd.name.clone(), vd);
207 }
208 Err(e) => {
209 view_err = Some(e);
210 }
211 }
212 Ok(())
213 });
214
215 match view_scan {
216 Ok(()) => {}
217 Err(citadel_core::Error::TableNotFound(_)) => {}
218 Err(e) => return Err(e.into()),
219 }
220 if let Some(e) = view_err {
221 return Err(e);
222 }
223
224 let mut triggers: FxHashMap<String, Vec<crate::types::TriggerDef>> = FxHashMap::default();
225 let mut rtx3 = db.begin_read();
226 let mut trig_err: Option<crate::error::SqlError> = None;
227 let trig_scan = rtx3.table_for_each(TRIGGERS_TABLE, |_key, value| {
228 match crate::types::TriggerDef::deserialize(value) {
229 Ok(td) => {
230 triggers
231 .entry(td.target.to_ascii_lowercase())
232 .or_default()
233 .push(td);
234 }
235 Err(e) => {
236 trig_err = Some(e);
237 }
238 }
239 Ok(())
240 });
241 match trig_scan {
242 Ok(()) => {}
243 Err(citadel_core::Error::TableNotFound(_)) => {}
244 Err(e) => return Err(e.into()),
245 }
246 if let Some(e) = trig_err {
247 return Err(e);
248 }
249 for v in triggers.values_mut() {
251 v.sort_by(|a, b| a.name.cmp(&b.name));
252 }
253
254 let mut matviews: FxHashMap<String, crate::types::MatviewDef> = FxHashMap::default();
255 let mut rtx4 = db.begin_read();
256 let mut mv_err: Option<crate::error::SqlError> = None;
257 let mv_scan = rtx4.table_for_each(MATVIEWS_TABLE, |_key, value| {
258 match crate::types::MatviewDef::deserialize(value) {
259 Ok(mv) => {
260 matviews.insert(mv.name.to_ascii_lowercase(), mv);
261 }
262 Err(e) => {
263 mv_err = Some(e);
264 }
265 }
266 Ok(())
267 });
268 match mv_scan {
269 Ok(()) => {}
270 Err(citadel_core::Error::TableNotFound(_)) => {}
271 Err(e) => return Err(e.into()),
272 }
273 if let Some(e) = mv_err {
274 return Err(e);
275 }
276
277 let mut mgr = Self {
278 tables,
279 views,
280 virtual_tables: FxHashMap::default(),
281 triggers,
282 matviews,
283 temp_aliases: FxHashMap::default(),
284 transition_schemas: std::cell::RefCell::new(FxHashMap::default()),
285 generation: 0,
286 sql_caches: db.sql_cache_handle(),
287 dml_dirty_tables: std::cell::RefCell::new(FxHashSet::default()),
288 dml_append_tables: std::cell::RefCell::new(FxHashMap::default()),
289 };
290 system_tables::register_builtins(&mut mgr);
291 Ok(mgr)
292 }
293
294 pub fn get_virtual(&self, name: &str) -> Option<&Arc<dyn VirtualTable>> {
295 self.virtual_tables.get(name)
296 }
297
298 pub fn register_virtual(&mut self, vt: Arc<dyn VirtualTable>) {
299 let name = vt.name().to_ascii_lowercase();
300 self.virtual_tables.insert(name, vt);
301 }
302
303 pub fn get(&self, name: &str) -> Option<&TableSchema> {
304 let lower = name.to_ascii_lowercase();
305 if let Some(prefixed) = transition_table_lookup(&lower) {
306 if let Some(s) = self.tables.get(&prefixed) {
307 return Some(s);
308 }
309 if let Some(&leaked) = self.transition_schemas.borrow().get(&prefixed) {
310 return Some(leaked);
311 }
312 }
313 if let Some(mv) = self.matviews.get(&lower) {
314 return self.tables.get(&mv.backing_table);
315 }
316 if let Some(prefixed) = self.temp_aliases.get(&lower) {
317 return self.tables.get(prefixed);
318 }
319 if let Some(s) = self.tables.get(name) {
320 return Some(s);
321 }
322 if name.bytes().any(|b| b.is_ascii_uppercase()) {
323 self.tables.get(&lower)
324 } else {
325 None
326 }
327 }
328
329 pub fn register_transition_schema(&self, storage_name: String, schema: TableSchema) {
330 let leaked: &'static TableSchema = Box::leak(Box::new(schema));
331 self.transition_schemas
332 .borrow_mut()
333 .insert(storage_name, leaked);
334 }
335
336 pub fn unregister_transition_schema(&self, storage_name: &str) {
337 self.transition_schemas.borrow_mut().remove(storage_name);
338 }
339
340 pub fn contains(&self, name: &str) -> bool {
341 let lower = name.to_ascii_lowercase();
342 if transition_table_lookup(&lower).is_some() {
343 return true;
344 }
345 if self.matviews.contains_key(&lower) {
346 return true;
347 }
348 if self.temp_aliases.contains_key(&lower) {
349 return true;
350 }
351 if self.tables.contains_key(name) {
352 return true;
353 }
354 if name.bytes().any(|b| b.is_ascii_uppercase()) {
355 self.tables.contains_key(&lower)
356 } else {
357 false
358 }
359 }
360
361 pub fn generation(&self) -> u64 {
362 self.generation
363 }
364
365 pub fn register(&mut self, schema: TableSchema) {
366 let lower = schema.name.to_ascii_lowercase();
367 self.tables.insert(lower, schema);
368 self.generation += 1;
369 }
370
371 pub fn remove(&mut self, name: &str) -> Option<TableSchema> {
372 let lower = name.to_ascii_lowercase();
373 let result = self.tables.remove(&lower);
374 if result.is_some() {
375 self.generation += 1;
376 }
377 result
378 }
379
380 pub fn table_names(&self) -> Vec<&str> {
381 self.tables.keys().map(|s| s.as_str()).collect()
382 }
383
384 pub fn all_schemas(&self) -> impl Iterator<Item = &TableSchema> {
385 self.tables.values()
386 }
387
388 pub fn get_view(&self, name: &str) -> Option<&ViewDef> {
389 if let Some(v) = self.views.get(name) {
390 return Some(v);
391 }
392 if name.bytes().any(|b| b.is_ascii_uppercase()) {
393 self.views.get(&name.to_ascii_lowercase())
394 } else {
395 None
396 }
397 }
398
399 pub fn register_view(&mut self, view: ViewDef) {
400 let lower = view.name.to_ascii_lowercase();
401 self.views.insert(lower, view);
402 self.generation += 1;
403 }
404
405 pub fn remove_view(&mut self, name: &str) -> Option<ViewDef> {
406 let lower = name.to_ascii_lowercase();
407 let result = self.views.remove(&lower);
408 if result.is_some() {
409 self.generation += 1;
410 }
411 result
412 }
413
414 pub fn view_names(&self) -> Vec<&str> {
415 self.views.keys().map(|s| s.as_str()).collect()
416 }
417
418 pub fn triggers_for(&self, target: &str) -> &[crate::types::TriggerDef] {
419 let key = target.to_ascii_lowercase();
420 self.triggers.get(&key).map(|v| v.as_slice()).unwrap_or(&[])
421 }
422
423 pub fn all_triggers(&self) -> impl Iterator<Item = &crate::types::TriggerDef> + '_ {
424 self.triggers.values().flatten()
425 }
426
427 pub fn register_trigger(&mut self, trig: crate::types::TriggerDef) {
428 let target = trig.target.to_ascii_lowercase();
429 let bucket = self.triggers.entry(target).or_default();
430 bucket.push(trig);
431 bucket.sort_by(|a, b| a.name.cmp(&b.name));
432 self.generation += 1;
433 }
434
435 pub fn remove_trigger(&mut self, name: &str) -> Option<crate::types::TriggerDef> {
436 let lower = name.to_ascii_lowercase();
437 let mut result = None;
438 for bucket in self.triggers.values_mut() {
439 if let Some(pos) = bucket
440 .iter()
441 .position(|t| t.name.eq_ignore_ascii_case(&lower))
442 {
443 result = Some(bucket.remove(pos));
444 break;
445 }
446 }
447 if result.is_some() {
448 self.generation += 1;
449 }
450 result
451 }
452
453 pub fn remove_triggers_for(&mut self, target: &str) -> Vec<crate::types::TriggerDef> {
455 let key = target.to_ascii_lowercase();
456 let removed = self.triggers.remove(&key).unwrap_or_default();
457 if !removed.is_empty() {
458 self.generation += 1;
459 }
460 removed
461 }
462
463 pub fn find_trigger(&self, name: &str) -> Option<(&str, &crate::types::TriggerDef)> {
464 let lower = name.to_ascii_lowercase();
465 for (target, bucket) in &self.triggers {
466 if let Some(t) = bucket.iter().find(|t| t.name.eq_ignore_ascii_case(&lower)) {
467 return Some((target.as_str(), t));
468 }
469 }
470 None
471 }
472
473 pub fn set_trigger_enabled(&mut self, name: &str, enabled: bool) -> bool {
474 let lower = name.to_ascii_lowercase();
475 for bucket in self.triggers.values_mut() {
476 if let Some(t) = bucket
477 .iter_mut()
478 .find(|t| t.name.eq_ignore_ascii_case(&lower))
479 {
480 t.enabled = enabled;
481 self.generation += 1;
482 return true;
483 }
484 }
485 false
486 }
487
488 pub fn set_all_triggers_enabled(&mut self, target: &str, enabled: bool) -> usize {
489 let key = target.to_ascii_lowercase();
490 let bucket = match self.triggers.get_mut(&key) {
491 Some(b) => b,
492 None => return 0,
493 };
494 let count = bucket.len();
495 for t in bucket {
496 t.enabled = enabled;
497 }
498 if count > 0 {
499 self.generation += 1;
500 }
501 count
502 }
503
504 pub fn ensure_triggers_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
505 match wtx.create_table(TRIGGERS_TABLE) {
506 Ok(()) => Ok(()),
507 Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
508 Err(e) => Err(e.into()),
509 }
510 }
511
512 pub fn save_trigger(
513 wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
514 trig: &crate::types::TriggerDef,
515 ) -> Result<()> {
516 Self::ensure_triggers_table(wtx)?;
517 let data = trig.serialize();
518 let lower = trig.name.to_ascii_lowercase();
519 wtx.table_insert(TRIGGERS_TABLE, lower.as_bytes(), &data)
520 .map_err(crate::error::SqlError::from)?;
521 Ok(())
522 }
523
524 pub fn delete_trigger(
525 wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
526 name: &str,
527 ) -> Result<()> {
528 Self::ensure_triggers_table(wtx)?;
529 let lower = name.to_ascii_lowercase();
530 wtx.table_delete(TRIGGERS_TABLE, lower.as_bytes())
531 .map_err(crate::error::SqlError::from)?;
532 Ok(())
533 }
534
535 pub fn save_view(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, view: &ViewDef) -> Result<()> {
536 let lower = view.name.to_ascii_lowercase();
537 let data = view.serialize();
538 wtx.table_insert(VIEWS_TABLE, lower.as_bytes(), &data)?;
539 Ok(())
540 }
541
542 pub fn delete_view(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
543 let lower = name.to_ascii_lowercase();
544 wtx.table_delete(VIEWS_TABLE, lower.as_bytes())
545 .map_err(|e| match e {
546 citadel_core::Error::TableNotFound(_) => SqlError::ViewNotFound(name.into()),
547 other => SqlError::Storage(other),
548 })?;
549 Ok(())
550 }
551
552 pub fn ensure_views_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
553 match wtx.create_table(VIEWS_TABLE) {
554 Ok(()) => Ok(()),
555 Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
556 Err(e) => Err(e.into()),
557 }
558 }
559
560 pub fn get_matview(&self, name: &str) -> Option<&crate::types::MatviewDef> {
561 let lower = name.to_ascii_lowercase();
562 self.matviews.get(&lower)
563 }
564
565 pub fn matview_names(&self) -> Vec<&str> {
566 self.matviews.keys().map(|s| s.as_str()).collect()
567 }
568
569 pub fn all_matviews(&self) -> impl Iterator<Item = &crate::types::MatviewDef> + '_ {
570 self.matviews.values()
571 }
572
573 pub fn register_matview(&mut self, mv: crate::types::MatviewDef) {
574 let lower = mv.name.to_ascii_lowercase();
575 self.matviews.insert(lower, mv);
576 self.generation += 1;
577 }
578
579 pub fn remove_matview(&mut self, name: &str) -> Option<crate::types::MatviewDef> {
580 let lower = name.to_ascii_lowercase();
581 let removed = self.matviews.remove(&lower);
582 if removed.is_some() {
583 self.generation += 1;
584 }
585 removed
586 }
587
588 pub fn ensure_matviews_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
589 match wtx.create_table(MATVIEWS_TABLE) {
590 Ok(()) => Ok(()),
591 Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
592 Err(e) => Err(e.into()),
593 }
594 }
595
596 pub fn save_matview(
597 wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
598 mv: &crate::types::MatviewDef,
599 ) -> Result<()> {
600 Self::ensure_matviews_table(wtx)?;
601 let lower = mv.name.to_ascii_lowercase();
602 let data = mv.serialize();
603 wtx.table_insert(MATVIEWS_TABLE, lower.as_bytes(), &data)?;
604 Ok(())
605 }
606
607 pub fn delete_matview(
608 wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
609 name: &str,
610 ) -> Result<()> {
611 Self::ensure_matviews_table(wtx)?;
612 let lower = name.to_ascii_lowercase();
613 wtx.table_delete(MATVIEWS_TABLE, lower.as_bytes())
614 .map_err(crate::error::SqlError::from)?;
615 Ok(())
616 }
617
618 pub fn child_fks_for(&self, parent: &str) -> Vec<(&str, &ForeignKeySchemaEntry)> {
619 self.tables
620 .iter()
621 .flat_map(|(name, schema)| {
622 schema
623 .foreign_keys
624 .iter()
625 .filter(|fk| fk.foreign_table == parent)
626 .map(move |fk| (name.as_str(), fk))
627 })
628 .collect()
629 }
630
631 pub fn save_schema(
632 wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
633 schema: &TableSchema,
634 ) -> Result<()> {
635 let lower = schema.name.to_ascii_lowercase();
636 let data = schema.serialize();
637 wtx.table_insert(SCHEMA_TABLE, lower.as_bytes(), &data)?;
638 Ok(())
639 }
640
641 pub fn delete_schema(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
642 let lower = name.to_ascii_lowercase();
643 wtx.table_delete(SCHEMA_TABLE, lower.as_bytes())
644 .map_err(|e| match e {
645 citadel_core::Error::TableNotFound(_) => SqlError::TableNotFound(name.into()),
646 other => SqlError::Storage(other),
647 })?;
648 Ok(())
649 }
650
651 pub fn ensure_schema_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
652 match wtx.create_table(SCHEMA_TABLE) {
653 Ok(()) => Ok(()),
654 Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
655 Err(e) => Err(e.into()),
656 }
657 }
658
659 pub fn save_snapshot(&self) -> SchemaSnapshot {
660 SchemaSnapshot {
661 tables: self.tables.clone(),
662 views: self.views.clone(),
663 generation: self.generation,
664 }
665 }
666
667 pub fn restore_snapshot(&mut self, snap: SchemaSnapshot) {
668 self.tables = snap.tables;
669 self.views = snap.views;
670 self.generation = snap.generation;
671 }
672}
673
674#[cfg(test)]
675#[path = "schema_tests.rs"]
676mod tests;