Skip to main content

uqa_sql/schema/
constraints.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Shared CREATE/ALTER validation for table constraints.
8
9use crate::ast::{ColumnDef, ForeignKey, TableKeyConstraint};
10use crate::{ColumnType, SQLError};
11
12mod checks;
13mod references;
14pub use checks::{
15    bind_stored_check_expression, check_row_schema, validate_check_expression,
16    validate_check_expression_with_type,
17};
18pub use references::{check_references, CheckReference};
19
20pub fn validate_foreign_key_definition(
21    local_table: &str,
22    local_columns: &[ColumnDef],
23    parent_table: &str,
24    parent_columns: &[ColumnDef],
25    parent_keys: &[TableKeyConstraint],
26    foreign_key: &ForeignKey,
27) -> Result<(), SQLError> {
28    if foreign_key.local_columns.is_empty()
29        || foreign_key.local_columns.len() != foreign_key.ref_columns.len()
30    {
31        return Err(invalid_foreign_key(format!(
32            "foreign key on relation \"{local_table}\" has mismatched local and referenced columns"
33        )));
34    }
35
36    let local_types = foreign_key
37        .local_columns
38        .iter()
39        .map(|name| {
40            local_columns
41                .iter()
42                .find(|column| column.name == *name)
43                .map(|column| &column.ty)
44                .ok_or_else(|| SQLError::UnknownColumn(format!("{local_table}.{name}")))
45        })
46        .collect::<Result<Vec<_>, _>>()?;
47    let parent_types = foreign_key
48        .ref_columns
49        .iter()
50        .map(|name| {
51            parent_columns
52                .iter()
53                .find(|column| column.name == *name)
54                .map(|column| &column.ty)
55                .ok_or_else(|| SQLError::UnknownColumn(format!("{parent_table}.{name}")))
56        })
57        .collect::<Result<Vec<_>, _>>()?;
58
59    let target_key = parent_keys.iter().find(|constraint| {
60        constraint.columns == foreign_key.ref_columns && constraint.without_overlaps
61    });
62    if foreign_key.period && target_key.is_none() {
63        return Err(invalid_foreign_key(format!(
64            "there is no primary key or unique constraint declared WITH WITHOUT OVERLAPS matching the referenced columns for table \"{parent_table}\""
65        )));
66    }
67
68    if foreign_key.period {
69        if foreign_key.local_columns.len() < 2 {
70            return Err(invalid_foreign_key(
71                "PERIOD foreign key must contain at least one ordinary column and one period column",
72            ));
73        }
74        let local_period = local_types.last().expect("non-empty foreign key");
75        let parent_period = parent_types.last().expect("non-empty foreign key");
76        if !matches!(
77            local_period,
78            ColumnType::Range(_) | ColumnType::Multirange(_)
79        ) || local_period != parent_period
80        {
81            return Err(SQLError::Routine {
82                sqlstate: "42804".into(),
83                message: format!(
84                    "PERIOD columns \"{}\" and \"{}\" have incompatible types {} and {}",
85                    foreign_key
86                        .local_columns
87                        .last()
88                        .expect("non-empty foreign key"),
89                    foreign_key
90                        .ref_columns
91                        .last()
92                        .expect("non-empty foreign key"),
93                    local_period.sql_name(),
94                    parent_period.sql_name()
95                ),
96            });
97        }
98    }
99
100    Ok(())
101}
102
103fn invalid_foreign_key(message: impl Into<String>) -> SQLError {
104    SQLError::Routine {
105        sqlstate: "42830".into(),
106        message: message.into(),
107    }
108}