Skip to main content

ormdantic_schema/
registry.rs

1use 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}