Skip to main content

uqa_sql/schema/
check_inheritance.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! CHECK definition merging at CREATE and ALTER inheritance boundaries.
8
9use crate::ast::{ColumnDef, ColumnType, Expr, TableCheck};
10use crate::ScalarExpr;
11use crate::{ast::CreateTable, SQLError};
12use uqa_core::Value;
13
14pub fn bind_parent_check_columns(parent: &str, expr: &mut Expr) -> Result<(), SQLError> {
15    let relation =
16        uqa_core::RelationIdentity::from_legacy_name(parent).map_err(SQLError::Internal)?;
17    crate::schema::generated::bind_schema_column_references(expr, parent);
18    crate::schema::generated::bind_schema_column_references(expr, &relation.name);
19    Ok(())
20}
21
22pub fn same_check_expression(
23    left: &Expr,
24    right: &Expr,
25    columns: &[ColumnDef],
26) -> Result<bool, SQLError> {
27    fn canonical(expression: &Expr, columns: &[ColumnDef]) -> Result<ScalarExpr, SQLError> {
28        let mut scalar = crate::plan::ExpressionPlan::lower(expression.clone()).scalar;
29        let mut failure = None;
30        crate::plan::rewrite_scalar_expression(&mut scalar, &mut |node| {
31            let ScalarExpr::Cast { expr, ty } = node else {
32                return;
33            };
34            let Ok(target) = ColumnType::from_sql_name(ty) else {
35                return;
36            };
37            if let ScalarExpr::Column(name) = expr.as_ref() {
38                if columns
39                    .iter()
40                    .any(|column| column.name == *name && column.ty == target)
41                {
42                    *node = *expr.clone();
43                }
44            } else if let ScalarExpr::Literal(value @ Value::Str(_)) = expr.as_ref() {
45                // PostgreSQL resolves an unknown string to an integer constant during analysis. Keep wider and narrower integer coercions distinct from the ordinary int4 literal.
46                if target == ColumnType::Integer {
47                    match crate::expr::cast_value(value, ty) {
48                        Ok(value) => *node = ScalarExpr::Literal(value),
49                        Err(error) => failure = Some(error),
50                    }
51                }
52            }
53        });
54        if let Some(error) = failure {
55            return Err(error);
56        }
57        Ok(scalar)
58    }
59    Ok(canonical(left, columns)? == canonical(right, columns)?)
60}
61
62pub fn duplicate_check(table: &str, name: &str) -> SQLError {
63    error(
64        "42710",
65        format!("constraint \"{name}\" for relation \"{table}\" already exists"),
66    )
67}
68
69fn error(sqlstate: &str, message: String) -> SQLError {
70    SQLError::Routine {
71        sqlstate: sqlstate.into(),
72        message,
73    }
74}
75
76/// The caller decides whether a local or inherited duplicate is eligible to merge. Existing validation and enforcement states follow `PostgreSQL`'s directional merge rules.
77pub fn validate_check_merge(
78    table: &str,
79    existing: &TableCheck,
80    incoming: &TableCheck,
81    columns: &[ColumnDef],
82) -> Result<(), SQLError> {
83    let name = incoming.name.as_deref().unwrap_or("<unnamed>");
84    if !same_check_expression(&existing.expr, &incoming.expr, columns)? {
85        return Err(duplicate_check(table, name));
86    }
87    let conflict = if existing.no_inherit {
88        Some("non-inherited")
89    } else if incoming.no_inherit {
90        Some("inherited")
91    } else if incoming.validated && existing.enforced && !existing.validated {
92        Some("NOT VALID")
93    } else if (!incoming.is_local && incoming.enforced && !existing.enforced)
94        || (incoming.is_local && !incoming.enforced && existing.enforced)
95    {
96        Some("NOT ENFORCED")
97    } else {
98        None
99    };
100    if let Some(conflict) = conflict {
101        return Err(error("42P17", format!("constraint \"{name}\" conflicts with {conflict} constraint on relation \"{table}\"")));
102    }
103    Ok(())
104}
105
106/// Merge bound CHECK expressions after the complete CREATE row type has been validated. Anonymous local constraints remain independent and receive names at publication.
107pub fn merge_create_checks(table: &mut CreateTable) -> Result<(), SQLError> {
108    let relation =
109        uqa_core::RelationIdentity::from_legacy_name(&table.name).map_err(SQLError::Internal)?;
110    let mut local_names = std::collections::BTreeSet::new();
111    for name in table
112        .columns
113        .iter()
114        .filter_map(|column| column.check_name.as_ref())
115        .chain(
116            table
117                .checks
118                .iter()
119                .filter(|check| check.is_local)
120                .filter_map(|check| check.name.as_ref()),
121        )
122    {
123        if !local_names.insert(name) {
124            return Err(duplicate_check(&relation.name, name));
125        }
126    }
127    if table.hierarchy.parents.is_empty() {
128        return Ok(());
129    }
130    let check_columns = table.columns.clone();
131    let mut inherited: Vec<TableCheck> = Vec::new();
132    let mut local = Vec::new();
133    for check in std::mem::take(&mut table.checks) {
134        if check.is_local {
135            local.push(check);
136        } else if let Some(existing) = inherited
137            .iter_mut()
138            .find(|existing| existing.name == check.name)
139        {
140            if !same_check_expression(&existing.expr, &check.expr, &check_columns)? {
141                return Err(error("42710", format!("check constraint name \"{}\" appears multiple times but with different expressions", check.name.as_deref().unwrap_or("<unnamed>"))));
142            }
143            existing.enforced |= check.enforced;
144            existing.validated = existing.enforced;
145        } else {
146            inherited.push(check);
147        }
148    }
149    for column in &mut table.columns {
150        let Some((expr, name)) = column.check.as_ref().zip(column.check_name.as_ref()) else {
151            continue;
152        };
153        let Some(index) = inherited
154            .iter()
155            .position(|check| check.name.as_ref() == Some(name))
156        else {
157            continue;
158        };
159        let incoming = TableCheck {
160            name: Some(name.clone()),
161            object_id: column.check_object_id,
162            is_local: true,
163            expr: expr.clone(),
164            enforced: column.check_enforced,
165            validated: column.check_validated,
166            no_inherit: column.check_no_inherit,
167            partition_constraint: None,
168        };
169        validate_check_merge(&relation.name, &inherited[index], &incoming, &check_columns)?;
170        inherited.remove(index);
171        column.check_is_local = !table.hierarchy.is_partition();
172    }
173    for mut check in local {
174        if let Some(index) = inherited
175            .iter()
176            .position(|existing| existing.name.is_some() && existing.name == check.name)
177        {
178            let existing = &inherited[index];
179            if existing.is_local {
180                return Err(duplicate_check(
181                    &relation.name,
182                    check.name.as_deref().unwrap_or("<unnamed>"),
183                ));
184            }
185            validate_check_merge(&relation.name, existing, &check, &check_columns)?;
186            check.is_local = !table.hierarchy.is_partition();
187            inherited[index] = check;
188        } else {
189            inherited.push(check);
190        }
191    }
192    table.checks = inherited;
193    Ok(())
194}