ormdantic_schema/
registry.rs1use std::collections::{HashMap, HashSet};
2
3use ormdantic_core::{OrmdanticError, OrmdanticResult, TableId};
4
5use crate::TableDef;
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct SchemaRegistry {
9 tables: Vec<TableDef>,
10 table_ids: HashMap<String, TableId>,
11}
12
13impl SchemaRegistry {
14 pub fn new() -> Self {
15 Self::default()
16 }
17
18 pub fn register_table(&mut self, mut table: TableDef) -> OrmdanticResult<TableId> {
19 if self.table_ids.contains_key(table.name()) {
20 return Err(OrmdanticError::DuplicateTable {
21 tablename: table.name().to_string(),
22 });
23 }
24
25 validate_columns(&table)?;
26 validate_primary_key(&table)?;
27 validate_indexes(&table)?;
28 validate_unique_constraints(&table)?;
29 validate_foreign_keys(&table)?;
30 validate_exclusion_constraints(&table)?;
31
32 let table_id = TableId(self.tables.len());
33 table.set_id(table_id);
34 self.table_ids.insert(table.name().to_string(), table_id);
35 self.tables.push(table);
36 Ok(table_id)
37 }
38
39 pub fn validate_relationships(&self) -> OrmdanticResult<()> {
40 for table in &self.tables {
41 for relationship in table.relationships() {
42 let Some(target_table) = self.get_table(relationship.target_table()) else {
43 return Err(OrmdanticError::InvalidRelationship {
44 table: table.name().to_string(),
45 field: relationship.field().to_string(),
46 target_table: relationship.target_table().to_string(),
47 });
48 };
49 if !target_table
50 .column_names()
51 .any(|column| column == relationship.target_field())
52 {
53 return Err(OrmdanticError::InvalidRelationship {
54 table: table.name().to_string(),
55 field: relationship.field().to_string(),
56 target_table: relationship.target_table().to_string(),
57 });
58 }
59 }
60 }
61 Ok(())
62 }
63
64 pub fn get_table(&self, tablename: &str) -> Option<&TableDef> {
65 self.table_ids
66 .get(tablename)
67 .and_then(|table_id| self.tables.get(table_id.0))
68 }
69
70 pub fn tables(&self) -> &[TableDef] {
71 &self.tables
72 }
73}
74
75fn validate_columns(table: &TableDef) -> OrmdanticResult<()> {
76 let mut seen = HashSet::new();
77 for column in table.columns() {
78 if !seen.insert(column.name()) {
79 return Err(OrmdanticError::DuplicateColumn {
80 tablename: table.name().to_string(),
81 column: column.name().to_string(),
82 });
83 }
84 }
85 Ok(())
86}
87
88fn validate_primary_key(table: &TableDef) -> OrmdanticResult<()> {
89 if table
90 .columns()
91 .iter()
92 .any(|column| column.name() == table.primary_key())
93 {
94 return Ok(());
95 }
96
97 Err(OrmdanticError::MissingPrimaryKey {
98 tablename: table.name().to_string(),
99 primary_key: table.primary_key().to_string(),
100 })
101}
102
103fn validate_indexes(table: &TableDef) -> OrmdanticResult<()> {
104 for index in table.indexes() {
105 if index.columns().is_empty() && index.expressions_ref().is_empty() {
106 return Err(OrmdanticError::SqlCompile {
107 message: format!(
108 "index '{}' on table '{}' must reference at least one column or expression",
109 index.name(),
110 table.name()
111 ),
112 });
113 }
114 for column in index.columns() {
115 validate_column_reference(table, column, "index", index.name())?;
116 }
117 for column in index.include_columns_ref() {
118 validate_column_reference(table, column, "index", index.name())?;
119 }
120 }
121 Ok(())
122}
123
124fn validate_unique_constraints(table: &TableDef) -> OrmdanticResult<()> {
125 for constraint in table.unique_constraints() {
126 for column in constraint.columns() {
127 validate_column_reference(table, column, "unique constraint", constraint.name())?;
128 }
129 }
130 Ok(())
131}
132
133fn validate_foreign_keys(table: &TableDef) -> OrmdanticResult<()> {
134 for constraint in table.foreign_keys() {
135 let owner_name = constraint.name().unwrap_or("foreign_key");
136 for column in constraint.local_columns() {
137 validate_column_reference(table, column, "foreign key", owner_name)?;
138 }
139 }
140 Ok(())
141}
142
143fn validate_exclusion_constraints(table: &TableDef) -> OrmdanticResult<()> {
144 for constraint in table.exclusion_constraints() {
145 for element in constraint.elements() {
146 if element.is_quoted() {
147 validate_column_reference(
148 table,
149 element.value(),
150 "exclusion constraint",
151 constraint.name(),
152 )?;
153 }
154 }
155 }
156 Ok(())
157}
158
159fn validate_column_reference(
160 table: &TableDef,
161 column: &str,
162 owner_kind: &str,
163 owner_name: &str,
164) -> OrmdanticResult<()> {
165 if table.column_names().any(|known| known == column) {
166 return Ok(());
167 }
168 Err(OrmdanticError::SqlCompile {
169 message: format!(
170 "{owner_kind} '{owner_name}' on table '{}' references unknown column '{column}'",
171 table.name()
172 ),
173 })
174}