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
20#[derive(Clone)]
21pub struct SchemaSnapshot {
22 tables: HashMap<String, TableSchema>,
23 views: HashMap<String, ViewDef>,
24 generation: u64,
25}
26
27impl SchemaManager {
28 pub fn load(db: &Database) -> Result<Self> {
30 let mut tables = HashMap::new();
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 = HashMap::new();
57 let mut rtx2 = db.begin_read();
58 let mut view_err: Option<crate::error::SqlError> = None;
59 let view_scan = rtx2.table_for_each(VIEWS_TABLE, |_key, value| {
60 match ViewDef::deserialize(value) {
61 Ok(vd) => {
62 views.insert(vd.name.clone(), vd);
63 }
64 Err(e) => {
65 view_err = Some(e);
66 }
67 }
68 Ok(())
69 });
70
71 match view_scan {
72 Ok(()) => {}
73 Err(citadel_core::Error::TableNotFound(_)) => {}
74 Err(e) => return Err(e.into()),
75 }
76 if let Some(e) = view_err {
77 return Err(e);
78 }
79
80 Ok(Self {
81 tables,
82 views,
83 generation: 0,
84 })
85 }
86
87 pub fn get(&self, name: &str) -> Option<&TableSchema> {
88 if let Some(s) = self.tables.get(name) {
89 return Some(s);
90 }
91 if name.bytes().any(|b| b.is_ascii_uppercase()) {
92 self.tables.get(&name.to_ascii_lowercase())
93 } else {
94 None
95 }
96 }
97
98 pub fn contains(&self, name: &str) -> bool {
99 if self.tables.contains_key(name) {
100 return true;
101 }
102 if name.bytes().any(|b| b.is_ascii_uppercase()) {
103 self.tables.contains_key(&name.to_ascii_lowercase())
104 } else {
105 false
106 }
107 }
108
109 pub fn generation(&self) -> u64 {
110 self.generation
111 }
112
113 pub fn register(&mut self, schema: TableSchema) {
114 let lower = schema.name.to_ascii_lowercase();
115 self.tables.insert(lower, schema);
116 self.generation += 1;
117 }
118
119 pub fn remove(&mut self, name: &str) -> Option<TableSchema> {
120 let lower = name.to_ascii_lowercase();
121 let result = self.tables.remove(&lower);
122 if result.is_some() {
123 self.generation += 1;
124 }
125 result
126 }
127
128 pub fn table_names(&self) -> Vec<&str> {
129 self.tables.keys().map(|s| s.as_str()).collect()
130 }
131
132 pub fn all_schemas(&self) -> impl Iterator<Item = &TableSchema> {
134 self.tables.values()
135 }
136
137 pub fn get_view(&self, name: &str) -> Option<&ViewDef> {
140 if let Some(v) = self.views.get(name) {
141 return Some(v);
142 }
143 if name.bytes().any(|b| b.is_ascii_uppercase()) {
144 self.views.get(&name.to_ascii_lowercase())
145 } else {
146 None
147 }
148 }
149
150 pub fn register_view(&mut self, view: ViewDef) {
151 let lower = view.name.to_ascii_lowercase();
152 self.views.insert(lower, view);
153 self.generation += 1;
154 }
155
156 pub fn remove_view(&mut self, name: &str) -> Option<ViewDef> {
157 let lower = name.to_ascii_lowercase();
158 let result = self.views.remove(&lower);
159 if result.is_some() {
160 self.generation += 1;
161 }
162 result
163 }
164
165 pub fn view_names(&self) -> Vec<&str> {
166 self.views.keys().map(|s| s.as_str()).collect()
167 }
168
169 pub fn save_view(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, view: &ViewDef) -> Result<()> {
170 let lower = view.name.to_ascii_lowercase();
171 let data = view.serialize();
172 wtx.table_insert(VIEWS_TABLE, lower.as_bytes(), &data)?;
173 Ok(())
174 }
175
176 pub fn delete_view(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
177 let lower = name.to_ascii_lowercase();
178 wtx.table_delete(VIEWS_TABLE, lower.as_bytes())
179 .map_err(|e| match e {
180 citadel_core::Error::TableNotFound(_) => SqlError::ViewNotFound(name.into()),
181 other => SqlError::Storage(other),
182 })?;
183 Ok(())
184 }
185
186 pub fn ensure_views_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
187 match wtx.create_table(VIEWS_TABLE) {
188 Ok(()) => Ok(()),
189 Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
190 Err(e) => Err(e.into()),
191 }
192 }
193
194 pub fn child_fks_for(&self, parent: &str) -> Vec<(&str, &ForeignKeySchemaEntry)> {
196 self.tables
197 .iter()
198 .flat_map(|(name, schema)| {
199 schema
200 .foreign_keys
201 .iter()
202 .filter(|fk| fk.foreign_table == parent)
203 .map(move |fk| (name.as_str(), fk))
204 })
205 .collect()
206 }
207
208 pub fn save_schema(
210 wtx: &mut citadel_txn::write_txn::WriteTxn<'_>,
211 schema: &TableSchema,
212 ) -> Result<()> {
213 let lower = schema.name.to_ascii_lowercase();
214 let data = schema.serialize();
215 wtx.table_insert(SCHEMA_TABLE, lower.as_bytes(), &data)?;
216 Ok(())
217 }
218
219 pub fn delete_schema(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>, name: &str) -> Result<()> {
221 let lower = name.to_ascii_lowercase();
222 wtx.table_delete(SCHEMA_TABLE, lower.as_bytes())
223 .map_err(|e| match e {
224 citadel_core::Error::TableNotFound(_) => SqlError::TableNotFound(name.into()),
225 other => SqlError::Storage(other),
226 })?;
227 Ok(())
228 }
229
230 pub fn ensure_schema_table(wtx: &mut citadel_txn::write_txn::WriteTxn<'_>) -> Result<()> {
232 match wtx.create_table(SCHEMA_TABLE) {
234 Ok(()) => Ok(()),
235 Err(citadel_core::Error::TableAlreadyExists(_)) => Ok(()),
236 Err(e) => Err(e.into()),
237 }
238 }
239
240 pub fn save_snapshot(&self) -> SchemaSnapshot {
241 SchemaSnapshot {
242 tables: self.tables.clone(),
243 views: self.views.clone(),
244 generation: self.generation,
245 }
246 }
247
248 pub fn restore_snapshot(&mut self, snap: SchemaSnapshot) {
249 self.tables = snap.tables;
250 self.views = snap.views;
251 self.generation = snap.generation;
252 }
253}