1use 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
13pub struct SchemaManager {
15 tables: HashMap<String, TableSchema>,
16 views: HashMap<String, ViewDef>,
17 generation: u64,
18}
19
20impl SchemaManager {
21 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 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 pub fn all_schemas(&self) -> impl Iterator<Item = &TableSchema> {
127 self.tables.values()
128 }
129
130 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 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 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 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 pub fn ensure_schema_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
225 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}