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