Skip to main content

sim_relation_schema/
validation.rs

1use crate::{Column, Constraint, Schema, Table, View};
2use sim_kernel::Datum;
3use sim_relation_core::{
4    ColumnName, ConstraintName, DomainCatalog, DomainId, IndexName, SchemaName, TableName, ViewName,
5};
6use std::{
7    collections::{BTreeMap, BTreeSet},
8    fmt,
9};
10
11/// Injected runtime boundary for checking a Datum through a domain Shape.
12pub trait ValueShapeValidator {
13    /// Returns whether `value` satisfies the Shape registered for `domain`.
14    fn accepts(&self, domain: &DomainId, value: &Datum) -> bool;
15}
16/// Validator useful when expression values have already been Shape-checked upstream.
17pub struct AcceptAllValues;
18impl ValueShapeValidator for AcceptAllValues {
19    fn accepts(&self, _: &DomainId, _: &Datum) -> bool {
20        true
21    }
22}
23
24/// A schema refusal, reported before codec/provider dispatch.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum SchemaError {
27    /// A table name repeats.
28    DuplicateTable(TableName),
29    /// A view name repeats.
30    DuplicateView(ViewName),
31    /// A column repeats in one table.
32    DuplicateColumn {
33        /// Containing table.
34        table: TableName,
35        /// Repeated column.
36        column: ColumnName,
37    },
38    /// A constraint repeats in one table.
39    DuplicateConstraint {
40        /// Containing table.
41        table: TableName,
42        /// Repeated constraint.
43        constraint: ConstraintName,
44    },
45    /// An index repeats in one table.
46    DuplicateIndex {
47        /// Containing table.
48        table: TableName,
49        /// Repeated index.
50        index: IndexName,
51    },
52    /// A domain id is absent from the catalog.
53    DanglingDomain(DomainId),
54    /// A column reference is absent or crosses constraint scope.
55    DanglingColumn {
56        /// Referencing table.
57        table: TableName,
58        /// Missing column.
59        column: ColumnName,
60    },
61    /// A table reference is absent.
62    DanglingTable(TableName),
63    /// A view reference is absent.
64    DanglingView(ViewName),
65    /// A key has no columns.
66    EmptyKey(ConstraintName),
67    /// Primary-key columns cannot be nullable.
68    NullablePrimaryKey(ColumnName),
69    /// A column declares both default and generation.
70    DefaultAndGenerated(ColumnName),
71    /// A generated column directly depends on itself.
72    GeneratedCycle(ColumnName),
73    /// A default is rejected by its domain Shape.
74    InvalidDefault(ColumnName),
75    /// A generated expression is rejected by its domain Shape.
76    InvalidGenerated(ColumnName),
77    /// Foreign key sides have different arity.
78    ForeignKeyArity(ConstraintName),
79    /// Foreign key domains differ at an offset.
80    ForeignKeyDomain {
81        /// Foreign-key constraint.
82        constraint: ConstraintName,
83        /// Mismatching mapping offset.
84        index: usize,
85    },
86    /// View dependencies contain a cycle.
87    ViewCycle(ViewName),
88}
89impl fmt::Display for SchemaError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "{self:?}")
92    }
93}
94impl std::error::Error for SchemaError {}
95
96pub(crate) fn validate(
97    name: SchemaName,
98    mut tables: Vec<Table>,
99    mut views: Vec<View>,
100    domains: &DomainCatalog,
101    validator: &impl ValueShapeValidator,
102) -> Result<Schema, SchemaError> {
103    unique(&tables, |v| v.name.clone(), SchemaError::DuplicateTable)?;
104    unique(&views, |v| v.name.clone(), SchemaError::DuplicateView)?;
105    let table_map: BTreeMap<_, _> = tables.iter().map(|v| (v.name.clone(), v)).collect();
106    for table in &tables {
107        validate_table(table, &table_map, domains, validator)?;
108    }
109    let view_names: BTreeSet<_> = views.iter().map(|v| v.name.clone()).collect();
110    for view in &views {
111        for table in &view.table_dependencies {
112            if !table_map.contains_key(table) {
113                return Err(SchemaError::DanglingTable(table.clone()));
114            }
115        }
116        for dep in &view.view_dependencies {
117            if !view_names.contains(dep) {
118                return Err(SchemaError::DanglingView(dep.clone()));
119            }
120        }
121    }
122    for view in &views {
123        visit_view(
124            &view.name,
125            &views,
126            &mut BTreeSet::new(),
127            &mut BTreeSet::new(),
128        )?;
129    }
130    tables.sort_by(|a, b| a.name.cmp(&b.name));
131    views.sort_by(|a, b| a.name.cmp(&b.name));
132    for table in &mut tables {
133        table.constraints.sort_by(|a, b| a.name().cmp(b.name()));
134        table.indexes.sort_by(|a, b| a.name.cmp(&b.name));
135    }
136    Ok(Schema {
137        name,
138        tables,
139        views,
140    })
141}
142fn unique<T, K: Ord + Clone>(
143    values: &[T],
144    key: impl Fn(&T) -> K,
145    error: impl Fn(K) -> SchemaError,
146) -> Result<(), SchemaError> {
147    let mut seen = BTreeSet::new();
148    for v in values {
149        let k = key(v);
150        if !seen.insert(k.clone()) {
151            return Err(error(k));
152        }
153    }
154    Ok(())
155}
156fn columns(table: &Table) -> BTreeMap<ColumnName, &Column> {
157    table.columns.iter().map(|v| (v.name.clone(), v)).collect()
158}
159fn validate_table(
160    table: &Table,
161    tables: &BTreeMap<TableName, &Table>,
162    domains: &DomainCatalog,
163    validator: &impl ValueShapeValidator,
164) -> Result<(), SchemaError> {
165    unique(
166        &table.columns,
167        |v| v.name.clone(),
168        |column| SchemaError::DuplicateColumn {
169            table: table.name.clone(),
170            column,
171        },
172    )?;
173    unique(
174        &table.constraints,
175        |v| v.name().clone(),
176        |constraint| SchemaError::DuplicateConstraint {
177            table: table.name.clone(),
178            constraint,
179        },
180    )?;
181    unique(
182        &table.indexes,
183        |v| v.name.clone(),
184        |index| SchemaError::DuplicateIndex {
185            table: table.name.clone(),
186            index,
187        },
188    )?;
189    let cols = columns(table);
190    for c in &table.columns {
191        if domains.get(&c.domain).is_none() {
192            return Err(SchemaError::DanglingDomain(c.domain.clone()));
193        }
194        if c.default.is_some() && c.generated.is_some() {
195            return Err(SchemaError::DefaultAndGenerated(c.name.clone()));
196        }
197        if let Some(v) = &c.default
198            && !validator.accepts(&c.domain, &v.0)
199        {
200            return Err(SchemaError::InvalidDefault(c.name.clone()));
201        }
202        if let Some(v) = &c.generated {
203            for dep in &v.depends_on {
204                require_column(table, &cols, dep)?;
205                if dep == &c.name {
206                    return Err(SchemaError::GeneratedCycle(c.name.clone()));
207                }
208            }
209            if !validator.accepts(&c.domain, &v.expression) {
210                return Err(SchemaError::InvalidGenerated(c.name.clone()));
211            }
212        }
213    }
214    for constraint in &table.constraints {
215        match constraint {
216            Constraint::Primary(v) => {
217                key(table, &cols, &v.name, &v.columns)?;
218                for n in &v.columns {
219                    if cols[n].nullable {
220                        return Err(SchemaError::NullablePrimaryKey(n.clone()));
221                    }
222                }
223            }
224            Constraint::Unique(v) => key(table, &cols, &v.name, &v.columns)?,
225            Constraint::Check(v) => {
226                for n in &v.columns {
227                    require_column(table, &cols, n)?
228                }
229            }
230            Constraint::Foreign(v) => {
231                key(table, &cols, &v.name, &v.columns)?;
232                if v.columns.len() != v.target_columns.len() {
233                    return Err(SchemaError::ForeignKeyArity(v.name.clone()));
234                }
235                let target = tables
236                    .get(&v.target_table)
237                    .ok_or_else(|| SchemaError::DanglingTable(v.target_table.clone()))?;
238                let target_cols = columns(target);
239                for (i, (left, right)) in v.columns.iter().zip(&v.target_columns).enumerate() {
240                    require_column(target, &target_cols, right)?;
241                    if cols[left].domain != target_cols[right].domain {
242                        return Err(SchemaError::ForeignKeyDomain {
243                            constraint: v.name.clone(),
244                            index: i,
245                        });
246                    }
247                }
248            }
249        }
250    }
251    for index in &table.indexes {
252        if index.columns.is_empty() {
253            return Err(SchemaError::DanglingColumn {
254                table: table.name.clone(),
255                column: ColumnName::new(sim_kernel::Symbol::new("<empty-index>")).expect("valid"),
256            });
257        }
258        for n in &index.columns {
259            require_column(table, &cols, n)?
260        }
261    }
262    Ok(())
263}
264fn require_column(
265    table: &Table,
266    cols: &BTreeMap<ColumnName, &Column>,
267    name: &ColumnName,
268) -> Result<(), SchemaError> {
269    if cols.contains_key(name) {
270        Ok(())
271    } else {
272        Err(SchemaError::DanglingColumn {
273            table: table.name.clone(),
274            column: name.clone(),
275        })
276    }
277}
278fn key(
279    table: &Table,
280    cols: &BTreeMap<ColumnName, &Column>,
281    name: &ConstraintName,
282    names: &[ColumnName],
283) -> Result<(), SchemaError> {
284    if names.is_empty() {
285        return Err(SchemaError::EmptyKey(name.clone()));
286    }
287    for n in names {
288        require_column(table, cols, n)?
289    }
290    Ok(())
291}
292fn visit_view(
293    name: &ViewName,
294    views: &[View],
295    visiting: &mut BTreeSet<ViewName>,
296    done: &mut BTreeSet<ViewName>,
297) -> Result<(), SchemaError> {
298    if done.contains(name) {
299        return Ok(());
300    }
301    if !visiting.insert(name.clone()) {
302        return Err(SchemaError::ViewCycle(name.clone()));
303    }
304    let view = views
305        .iter()
306        .find(|v| &v.name == name)
307        .expect("existence checked");
308    for dep in &view.view_dependencies {
309        visit_view(dep, views, visiting, done)?;
310    }
311    visiting.remove(name);
312    done.insert(name.clone());
313    Ok(())
314}