1use std::collections::HashMap;
4
5use citadel::Database;
6
7use crate::error::{Result, SqlError};
8use crate::types::{ForeignKeySchemaEntry, TableSchema};
9
10const SCHEMA_TABLE: &[u8] = b"_schema";
11
12pub struct SchemaManager {
14 tables: HashMap<String, TableSchema>,
15 generation: u64,
16}
17
18impl SchemaManager {
19 pub fn load(db: &Database) -> Result<Self> {
21 let mut tables = HashMap::new();
22
23 let mut rtx = db.begin_read();
24 let mut parse_err: Option<crate::error::SqlError> = None;
25 let scan_result = rtx.table_for_each(SCHEMA_TABLE, |_key, value| {
26 match TableSchema::deserialize(value) {
27 Ok(schema) => {
28 tables.insert(schema.name.clone(), schema);
29 }
30 Err(e) => {
31 parse_err = Some(e);
32 }
33 }
34 Ok(())
35 });
36
37 match scan_result {
38 Ok(()) => {}
39 Err(citadel_core::Error::TableNotFound(_)) => {}
40 Err(e) => return Err(e.into()),
41 }
42 if let Some(e) = parse_err {
43 return Err(e);
44 }
45
46 Ok(Self {
47 tables,
48 generation: 0,
49 })
50 }
51
52 pub fn get(&self, name: &str) -> Option<&TableSchema> {
53 if let Some(s) = self.tables.get(name) {
54 return Some(s);
55 }
56 if name.bytes().any(|b| b.is_ascii_uppercase()) {
57 self.tables.get(&name.to_ascii_lowercase())
58 } else {
59 None
60 }
61 }
62
63 pub fn contains(&self, name: &str) -> bool {
64 if self.tables.contains_key(name) {
65 return true;
66 }
67 if name.bytes().any(|b| b.is_ascii_uppercase()) {
68 self.tables.contains_key(&name.to_ascii_lowercase())
69 } else {
70 false
71 }
72 }
73
74 pub fn generation(&self) -> u64 {
75 self.generation
76 }
77
78 pub fn register(&mut self, schema: TableSchema) {
79 let lower = schema.name.to_ascii_lowercase();
80 self.tables.insert(lower, schema);
81 self.generation += 1;
82 }
83
84 pub fn remove(&mut self, name: &str) -> Option<TableSchema> {
85 let lower = name.to_ascii_lowercase();
86 let result = self.tables.remove(&lower);
87 if result.is_some() {
88 self.generation += 1;
89 }
90 result
91 }
92
93 pub fn table_names(&self) -> Vec<&str> {
94 self.tables.keys().map(|s| s.as_str()).collect()
95 }
96
97 pub fn all_schemas(&self) -> impl Iterator<Item = &TableSchema> {
99 self.tables.values()
100 }
101
102 pub fn child_fks_for(&self, parent: &str) -> Vec<(&str, &ForeignKeySchemaEntry)> {
104 self.tables
105 .iter()
106 .flat_map(|(name, schema)| {
107 schema
108 .foreign_keys
109 .iter()
110 .filter(|fk| fk.foreign_table == parent)
111 .map(move |fk| (name.as_str(), fk))
112 })
113 .collect()
114 }
115
116 pub fn save_schema(
118 wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
119 schema: &TableSchema,
120 ) -> Result<()> {
121 let lower = schema.name.to_ascii_lowercase();
122 let data = schema.serialize();
123 wtx.table_insert(SCHEMA_TABLE, lower.as_bytes(), &data)?;
124 Ok(())
125 }
126
127 pub fn delete_schema(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
129 let lower = name.to_ascii_lowercase();
130 wtx.table_delete(SCHEMA_TABLE, lower.as_bytes())
131 .map_err(|e| match e {
132 citadel_core::Error::TableNotFound(_) => SqlError::TableNotFound(name.into()),
133 other => SqlError::Storage(other),
134 })?;
135 Ok(())
136 }
137
138 pub fn ensure_schema_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
140 match wtx.create_table(SCHEMA_TABLE) {
142 Ok(()) => Ok(()),
143 Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
144 Err(e) => Err(e.into()),
145 }
146 }
147}