Skip to main content

uqa_sql/schema/constraint_changes/
validation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Select constraint validation dependencies and match inherited NOT NULL constraints by column.
8
9use super::{constraint_error, find_constraint, ConstraintLocation};
10use crate::{
11    ast::{ColumnDef, TableConstraintSet},
12    SQLError,
13};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum ConstraintValidationKind<'a> {
17    Check {
18        no_inherit: bool,
19    },
20    NotNull {
21        column: &'a str,
22        no_inherit: bool,
23    },
24    ForeignKey {
25        referenced_table: &'a str,
26    },
27    /// A constraint a foreign key derives on a referenced partition, by the partition's object identity.
28    ReferencedPartition {
29        partition: [u8; 16],
30    },
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct ConstraintValidation<'a> {
35    pub kind: ConstraintValidationKind<'a>,
36    pub validated: bool,
37}
38
39impl ConstraintValidation<'_> {
40    pub fn requires_descendants(self) -> bool {
41        !self.validated
42            && matches!(
43                self.kind,
44                ConstraintValidationKind::Check { no_inherit: false }
45                    | ConstraintValidationKind::NotNull {
46                        no_inherit: false,
47                        ..
48                    }
49            )
50    }
51
52    pub fn child_constraint_name<'a>(
53        self,
54        original: &'a str,
55        child: &str,
56        columns: &'a [ColumnDef],
57    ) -> Result<&'a str, SQLError> {
58        let ConstraintValidationKind::NotNull { column, .. } = self.kind else {
59            return Ok(original);
60        };
61        super::inheritance::not_null_constraint(columns, column)
62            .and_then(|candidate| candidate.not_null_name.as_deref())
63            .ok_or_else(|| constraint_error("XX000", format!("cache lookup failed for not-null constraint on column \"{column}\" of relation \"{child}\"")))
64    }
65}
66
67pub fn constraint_validation<'a>(
68    table: &str,
69    name: &str,
70    columns: &'a [ColumnDef],
71    constraints: &'a TableConstraintSet,
72) -> Result<ConstraintValidation<'a>, SQLError> {
73    let location = find_constraint(columns, constraints, name).ok_or_else(|| {
74        constraint_error(
75            "42704",
76            format!("constraint \"{name}\" of relation \"{table}\" does not exist"),
77        )
78    })?;
79    let (kind, validated, enforced) = match location {
80        ConstraintLocation::NotNull(index) => {
81            let column = &columns[index];
82            (ConstraintValidationKind::NotNull { column: &column.name, no_inherit: column.not_null_no_inherit }, column.not_null_validated, true)
83        }
84        ConstraintLocation::ColumnCheck(index) => {
85            let column = &columns[index];
86            (ConstraintValidationKind::Check { no_inherit: column.check_no_inherit }, column.check_validated, column.check_enforced)
87        }
88        ConstraintLocation::TableCheck(index) => {
89            let check = &constraints.checks[index];
90            (ConstraintValidationKind::Check { no_inherit: check.no_inherit }, check.validated, check.enforced)
91        }
92        ConstraintLocation::ColumnForeignKey(index) => {
93            let reference = columns[index].references.as_ref()
94                .ok_or_else(|| SQLError::Internal("column FOREIGN KEY disappeared".into()))?;
95            (ConstraintValidationKind::ForeignKey { referenced_table: &reference.table }, reference.validated, reference.enforced)
96        }
97        ConstraintLocation::TableForeignKey(index) => {
98            let reference = &constraints.foreign_keys[index];
99            (ConstraintValidationKind::ForeignKey { referenced_table: &reference.ref_table }, reference.validated, reference.enforced)
100        }
101        ConstraintLocation::Key(_) => return Err(constraint_error("42809", format!("constraint \"{name}\" of relation \"{table}\" is not a foreign key, check, or not-null constraint"))),
102        ConstraintLocation::ReferencedPartition(foreign_key, index) => {
103            let derived = foreign_key
104                .derived(columns, constraints)
105                .and_then(|(_, derived)| derived.get(index))
106                .ok_or_else(|| SQLError::Internal("derived constraint disappeared".into()))?;
107            let enforced = match foreign_key {
108                super::ForeignKeyLocation::Column(position) => columns[position]
109                    .references
110                    .as_ref()
111                    .is_some_and(|reference| reference.enforced),
112                super::ForeignKeyLocation::Table(position) => constraints.foreign_keys[position].enforced,
113            };
114            (ConstraintValidationKind::ReferencedPartition { partition: derived.partition }, derived.validated, enforced)
115        }
116    };
117    if !enforced {
118        return Err(constraint_error(
119            "55000",
120            "cannot validate NOT ENFORCED constraint",
121        ));
122    }
123    Ok(ConstraintValidation { kind, validated })
124}
125
126pub fn ensure_validation_recurses(recurse: bool, has_children: bool) -> Result<(), SQLError> {
127    if !recurse && has_children {
128        return Err(constraint_error(
129            "42P16",
130            "constraint must be validated on child tables too",
131        ));
132    }
133    Ok(())
134}
135
136#[cfg(test)]
137mod tests;