Skip to main content

uqa_sql/schema/table_creation/
checks.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The CHECK constraints of a new table, which `DefineRelation` adds once the table's partitioning is set up. `AddRelationNewConstraints` takes them in written order: it transforms each expression, then rejects a name an earlier CHECK took, merges a named CHECK with the inherited constraint of its name or rejects a name another constraint of the table holds, or else chooses a name, and `StoreRelCheck` rejects a NO INHERIT constraint on a partitioned table before it creates the constraint row with its OID; a merged CHECK keeps the inherited row.
8
9use super::declaration::{validate_check_expression, CreateTableAnalysisContext};
10use crate::ast::{CreateTable, DeclaredCheck, TableCheck};
11use crate::schema::check_inheritance::{duplicate_check, validate_check_merge};
12use crate::schema::constraint_changes::{restore_column_check, take_column_check};
13use crate::schema::constraint_metadata::{
14    assign_check_name, materialize_check_identity, CatalogIdentityAllocator,
15    ConstraintMetadataError,
16};
17use crate::{SQLError, SQLNotice};
18use std::collections::BTreeSet;
19
20/// Add the CHECK constraints `table` declares, in written order. `held` names the constraints the table holds before its CHECKs besides the ones it inherits: the keys and foreign keys a partition clones from its parent.
21pub fn define_create_table_checks(
22    context: &CreateTableAnalysisContext<'_>,
23    table: &mut CreateTable,
24    held: &BTreeSet<String>,
25    notices: &mut Vec<SQLNotice>,
26    allocate: &mut CatalogIdentityAllocator<'_>,
27) -> Result<(), SQLError> {
28    let relation =
29        uqa_core::RelationIdentity::from_legacy_name(&table.name).map_err(SQLError::Internal)?;
30    let columns = table.columns.clone();
31    let partition = table.hierarchy.is_partition();
32    let (mut inherited, local): (Vec<TableCheck>, Vec<TableCheck>) =
33        std::mem::take(&mut table.checks)
34            .into_iter()
35            .partition(|check| !check.is_local);
36    let mut local = local.into_iter().map(Some).collect::<Vec<_>>();
37    // The inherited CHECKs, which the parents already transformed, are bound to the new table.
38    for check in &mut inherited {
39        bind_check(context, table, &columns, check)?;
40    }
41    // `ChooseConstraintName` avoids every constraint of the schema, which by now include the ones the table inherits and clones.
42    let mut used = context
43        .index_names
44        .automatic_constraint_names(&table.name)?;
45    used.extend(held.iter().cloned());
46    used.extend(inherited.iter().filter_map(|check| check.name.clone()));
47    let mut chosen = BTreeSet::new();
48    let mut stored = Vec::new();
49    for declared in std::mem::take(&mut table.check_order) {
50        let (mut check, column) = match declared {
51            DeclaredCheck::Column(name) => {
52                let index = table
53                    .columns
54                    .iter()
55                    .position(|column| column.name == name)
56                    .ok_or_else(|| lost_check(&name))?;
57                let check = take_column_check(&mut table.columns[index])
58                    .ok_or_else(|| lost_check(&name))?;
59                (check, Some(index))
60            }
61            DeclaredCheck::Table(position) => (
62                local
63                    .get_mut(position)
64                    .and_then(Option::take)
65                    .ok_or_else(|| lost_check(&position.to_string()))?,
66                None,
67            ),
68        };
69        bind_check(context, table, &columns, &mut check)?;
70        let merged = if let Some(name) = check.name.clone() {
71            if !chosen.insert(name.clone()) {
72                return Err(SQLError::Routine {
73                    sqlstate: "42710".into(),
74                    message: format!("check constraint \"{name}\" already exists"),
75                });
76            }
77            used.insert(name);
78            merge_with_existing(&relation.name, &inherited, held, &check, &columns)?
79        } else {
80            assign_check_name(&relation.name, &check.expr, &mut check.name, &mut used)
81                .map_err(|error| SQLError::Internal(error.to_string()))?;
82            chosen.extend(check.name.clone());
83            None
84        };
85        if let Some(name) = merged.and(check.name.as_deref()) {
86            notices.push(SQLNotice::notice(format!(
87                "merging constraint \"{name}\" with inherited definition"
88            )));
89        } else if check.no_inherit && table.hierarchy.partition_spec.is_some() {
90            return Err(SQLError::Routine {
91                sqlstate: "42P16".into(),
92                message: format!(
93                    "cannot add NO INHERIT constraint to partitioned table \"{}\"",
94                    relation.name
95                ),
96            });
97        }
98        // A local CHECK merged with the inherited one stands for it and keeps its row; a partition's merged constraints stay inherited.
99        if merged.is_some() {
100            check.is_local = !partition;
101        } else {
102            materialize_check_identity(&mut check.object_id, &mut check.catalog_oid, allocate)
103                .map_err(ConstraintMetadataError::into_sql_error)?;
104        }
105        match (merged, column) {
106            (Some(position), Some(index)) => {
107                let existing = inherited.remove(position);
108                check.object_id = existing.object_id;
109                check.catalog_oid = existing.catalog_oid;
110                restore_column_check(&mut table.columns[index], check);
111            }
112            (Some(position), None) => {
113                check.object_id = inherited[position].object_id;
114                check.catalog_oid = inherited[position].catalog_oid;
115                inherited[position] = check;
116            }
117            (None, Some(index)) => restore_column_check(&mut table.columns[index], check),
118            (None, None) => stored.push(check),
119        }
120    }
121    if local.iter().any(Option::is_some) {
122        return Err(lost_check("a table CHECK"));
123    }
124    inherited.append(&mut stored);
125    table.checks = inherited;
126    Ok(())
127}
128
129/// Transform a CHECK expression against the new table's columns, as `cookConstraint` does.
130fn bind_check(
131    context: &CreateTableAnalysisContext<'_>,
132    table: &CreateTable,
133    columns: &[crate::ast::ColumnDef],
134    check: &mut TableCheck,
135) -> Result<(), SQLError> {
136    validate_check_expression(
137        context,
138        &table.name,
139        &table.qualifier,
140        columns,
141        &mut check.expr,
142    )?;
143    crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
144        context.schema,
145        &check.expr,
146        None,
147    )
148}
149
150/// `MergeWithExistingConstraint`: the position of the inherited CHECK a named local CHECK merges with, or none when the table holds no constraint of its name; a name that another kind of constraint holds is taken.
151fn merge_with_existing(
152    relation: &str,
153    inherited: &[TableCheck],
154    held: &BTreeSet<String>,
155    check: &TableCheck,
156    columns: &[crate::ast::ColumnDef],
157) -> Result<Option<usize>, SQLError> {
158    let name = check.name.as_deref().unwrap_or_default();
159    if let Some(position) = inherited
160        .iter()
161        .position(|existing| existing.name.as_deref() == Some(name))
162    {
163        validate_check_merge(relation, &inherited[position], check, columns)?;
164        return Ok(Some(position));
165    }
166    if held.contains(name) {
167        return Err(duplicate_check(relation, name));
168    }
169    Ok(None)
170}
171
172fn lost_check(name: &str) -> SQLError {
173    SQLError::Internal(format!(
174        "the written order of CREATE TABLE CHECKs lost {name}"
175    ))
176}