Skip to main content

uqa_sql/schema/constraints/
checks.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! CHECK analysis keeps the declaring relation's actual row namespace and type.
8
9use crate::{
10    ast::{ColumnDef, Expr},
11    plpgsql::{bind_expr, ResolvedVariable, VariableResolver},
12    schema::{ColumnIdentity, SchemaBindingContext},
13    ColumnType, RowSchema, SQLError,
14};
15use uqa_core::Value;
16
17/// A CHECK sees user columns, its named whole row and the relation OID; system attributes are not ordinary declaration columns.
18pub fn check_row_schema(
19    qualifier: &str,
20    columns: &[ColumnDef],
21    row_type: &ColumnType,
22) -> Result<RowSchema, SQLError> {
23    let schema = RowSchema::with_column_definitions(Some(qualifier), columns)
24        .with_relation_row_type(qualifier, row_type.clone())?;
25    Ok(RowSchema::with_typed_virtual_identities(
26        &schema,
27        &[
28            (
29                ColumnIdentity::unqualified("tableoid"),
30                Some(ColumnType::Oid),
31            ),
32            (
33                ColumnIdentity::qualified(qualifier, "tableoid"),
34                Some(ColumnType::Oid),
35            ),
36        ],
37    ))
38}
39
40pub fn validate_check_expression(
41    context: &SchemaBindingContext<'_, '_>,
42    table: &str,
43    qualifier: &str,
44    columns: &[ColumnDef],
45    expression: &mut Expr,
46) -> Result<(), SQLError> {
47    let row_type = crate::FunctionTypeResolver::resolve_type_name(context.catalog, table)?
48        .ok_or_else(|| SQLError::UnknownTable(table.into()))?;
49    validate_check_expression_with_type(context, table, qualifier, columns, &row_type, expression)
50}
51
52/// Rebind retained calls in the same named row scope used for CHECK analysis, including tableoid and the actual composite identity.
53pub fn bind_stored_check_expression(
54    context: &SchemaBindingContext<'_, '_>,
55    table: &str,
56    qualifier: &str,
57    columns: &[ColumnDef],
58    expression: &mut Expr,
59) -> Result<bool, SQLError> {
60    let row_type = crate::FunctionTypeResolver::resolve_type_name(context.catalog, table)?
61        .ok_or_else(|| SQLError::UnknownTable(table.into()))?;
62    let relation = crate::RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
63    let schema = check_row_schema(&relation.name, columns, &row_type)?;
64    let typed = bind_expr(
65        expression,
66        &mut RelationReferences {
67            table,
68            qualifier,
69            local: &relation.name,
70            columns,
71        },
72    )?;
73    crate::schema::defaults::bind_stored_schema_expression_with_schema(
74        context, expression, typed, &schema,
75    )
76}
77
78/// Analyze CHECKs against the actual declared row type, using the same SQL boundary for CREATE and ALTER.
79pub fn validate_check_expression_with_type(
80    context: &SchemaBindingContext<'_, '_>,
81    table: &str,
82    qualifier: &str,
83    columns: &[ColumnDef],
84    row_type: &ColumnType,
85    expression: &mut Expr,
86) -> Result<(), SQLError> {
87    let relation = crate::RelationIdentity::from_legacy_name(table).map_err(SQLError::Internal)?;
88    let schema = check_row_schema(&relation.name, columns, row_type)?;
89    *expression = bind_expr(
90        expression,
91        &mut RelationReferences {
92            table,
93            qualifier,
94            local: &relation.name,
95            columns,
96        },
97    )?;
98    bind_whole_row_fields(expression, &schema)?;
99    let original = crate::plan::ExpressionPlan::lower(expression.clone());
100    let mut plan = original.clone();
101    let ty =
102        crate::binding::analyze_table_check(context.catalog, &mut plan, &schema, context.binding)?;
103    let sites = crate::binding::syntax_sites::expression_syntax_sites(&original, &plan)?;
104    crate::catalog::stored_ast::bind_stored_expression_sites(expression, &sites)?;
105    require_boolean(context, expression, ty.as_ref())?;
106    crate::schema::defaults::bind_stored_schema_expression_with_schema(
107        context,
108        expression,
109        expression.clone(),
110        &schema,
111    )?;
112    crate::schema::generated::bind_schema_column_references(expression, &relation.name);
113    crate::schema::check_inheritance::remove_identity_casts(expression, columns)
114}
115
116/// Selecting a field of the declaring row denotes that column, not a dependency on every field of a composite value.
117fn bind_whole_row_fields(expression: &mut Expr, schema: &RowSchema) -> Result<(), SQLError> {
118    crate::catalog::stored_ast::visit_stored_expression(expression, &mut |node| {
119        let Expr::Func {
120            binding: Some(binding),
121            args,
122            ..
123        } = node
124        else {
125            return Ok(());
126        };
127        if binding.dispatch != Some(crate::ast::FunctionDispatch::FieldSelect) {
128            return Ok(());
129        }
130        if let [Expr::QualifiedStar(qualifier), Expr::Literal(Value::Str(column))] = args.as_slice()
131        {
132            if schema.has_qualifier(qualifier) {
133                *node = Expr::QualifiedColumn {
134                    qualifier: qualifier.clone(),
135                    column: column.clone(),
136                };
137            }
138        }
139        Ok(())
140    })
141}
142
143fn require_boolean(
144    context: &SchemaBindingContext<'_, '_>,
145    expression: &mut Expr,
146    ty: Option<&ColumnType>,
147) -> Result<(), SQLError> {
148    if let Some(mut ty) = ty {
149        while let ColumnType::Domain { base, .. } = ty {
150            ty = base;
151        }
152        if *ty != ColumnType::Boolean {
153            return Err(SQLError::Routine {
154                sqlstate: "42804".into(),
155                message: format!(
156                    "argument of CHECK must be type boolean, not type {}",
157                    ty.regtype_name()
158                ),
159            });
160        }
161    } else if matches!(expression, Expr::Literal(Value::Null | Value::Str(_))) {
162        crate::catalog::stored_ast::read_unknown_stored_literal(
163            crate::FunctionTypeResolver::enum_labels(context.catalog),
164            crate::FunctionTypeResolver::catalog_input_functions(context.catalog),
165            expression,
166            &ColumnType::Boolean,
167            false,
168        )?;
169    } else {
170        *expression = Expr::Cast {
171            implicit: true,
172            expr: Box::new(expression.clone()),
173            ty: "boolean".into(),
174        };
175    }
176    Ok(())
177}
178
179/// Normalize known relation spellings without evaluating values or changing analysis error order. Unknown names remain for the ordered analyzer to reject.
180struct RelationReferences<'a> {
181    table: &'a str,
182    qualifier: &'a str,
183    local: &'a str,
184    columns: &'a [ColumnDef],
185}
186
187impl RelationReferences<'_> {
188    fn matches(&self, qualifier: &str) -> bool {
189        [self.table, self.qualifier, self.local].contains(&qualifier)
190    }
191}
192
193impl VariableResolver for RelationReferences<'_> {
194    fn resolve_name(&mut self, _: &str) -> Result<Option<ResolvedVariable>, SQLError> {
195        Ok(None)
196    }
197
198    fn resolve_qualified(
199        &mut self,
200        _: &str,
201        _: &str,
202    ) -> Result<Option<ResolvedVariable>, SQLError> {
203        Ok(None)
204    }
205
206    fn resolve_param(&mut self, _: usize) -> Result<Option<ResolvedVariable>, SQLError> {
207        Ok(None)
208    }
209
210    fn rewrite_name(&mut self, name: &str) -> Result<Option<Expr>, SQLError> {
211        Ok(
212            (self.matches(name) && !self.columns.iter().any(|column| column.name == name))
213                .then(|| Expr::QualifiedStar(self.local.into())),
214        )
215    }
216
217    fn rewrite_qualified(
218        &mut self,
219        qualifier: &str,
220        column: &str,
221    ) -> Result<Option<Expr>, SQLError> {
222        Ok(self.matches(qualifier).then(|| Expr::QualifiedColumn {
223            qualifier: self.local.into(),
224            column: column.into(),
225        }))
226    }
227
228    fn rewrite_qualified_whole_row(&mut self, qualifier: &str) -> Result<Option<Expr>, SQLError> {
229        Ok(self
230            .matches(qualifier)
231            .then(|| Expr::QualifiedStar(self.local.into())))
232    }
233}
234
235#[cfg(test)]
236mod tests;