Skip to main content

uqa_sql/schema/
domains.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Domain declaration validation, inherited defaults, and constraint binding.
8
9use super::SchemaBindingContext;
10use crate::assignment::domain::domain_error;
11use crate::plpgsql::{bind_expr, ResolvedVariable, VariableResolver};
12use crate::{
13    ast::{ColumnType, CreateDomain, DomainCheck, DomainNotNull, Expr},
14    catalog::domain::DomainCatalog,
15    RowSchema, SQLError,
16};
17use std::collections::BTreeSet;
18use uqa_core::Value;
19
20pub fn prepare_domain_definition(
21    context: &SchemaBindingContext<'_, '_>,
22    domains: &dyn DomainCatalog,
23    definition: &mut CreateDomain,
24    schema_names: &BTreeSet<String>,
25) -> Result<(), SQLError> {
26    definition.base =
27        crate::type_resolution::resolve_declared_column_type(context.catalog, &definition.base)?;
28    if definition.default.is_none() {
29        definition.default =
30            crate::catalog::domain::domain_default_expression(domains, &definition.base);
31    }
32    if matches!(
33        definition.base,
34        ColumnType::Void | ColumnType::Record | ColumnType::AnyArray
35    ) {
36        return Err(domain_error(
37            "42804",
38            format!(
39                "\"{}\" is not a valid base type for a domain",
40                definition.base.sql_name()
41            ),
42        ));
43    }
44    // DefineDomain requires USAGE on the base type once it is known to be valid for a domain.
45    context.catalog.require_type_usage(&definition.base)?;
46    if definition.collation.is_some() {
47        return Err(SQLError::Unsupported(
48            "domain collation binding is not implemented".into(),
49        ));
50    }
51    if let Some(default) = &mut definition.default {
52        // `DefineDomain` cooks the default against the base type and names the domain as the column.
53        let domain = definition
54            .name
55            .rsplit('.')
56            .next()
57            .unwrap_or(definition.name.as_str());
58        if !super::defaults::validate_default_expression(
59            context,
60            default,
61            &definition.base,
62            domain,
63        )? {
64            definition.default = None;
65        }
66    }
67    constraints::assign_names(definition, schema_names)?;
68    for check in &mut definition.checks {
69        bind_domain_check(context, &definition.base, &mut check.expression)?;
70    }
71    definition
72        .checks
73        .sort_by(|left, right| left.name.cmp(&right.name));
74    Ok(())
75}
76
77/// Name and bind only a newly added CHECK. Existing defaults and constraints already carry their catalog identities and must not be analyzed again in the ALTER statement's namespace.
78pub fn prepare_added_check(
79    context: &SchemaBindingContext<'_, '_>,
80    definition: &CreateDomain,
81    check: DomainCheck,
82    schema_names: &BTreeSet<String>,
83) -> Result<DomainCheck, SQLError> {
84    let mut named = definition.clone();
85    named.checks.push(check);
86    constraints::assign_names(&mut named, schema_names)?;
87    let mut check = named.checks.pop().expect("new domain CHECK");
88    bind_domain_check(context, &definition.base, &mut check.expression)?;
89    Ok(check)
90}
91
92/// Name a new NOT NULL constraint after the executor has resolved and locked its domain. An existing NOT NULL constraint is retained unchanged, as ALTER DOMAIN does even when the requested name differs.
93pub fn prepare_added_not_null(
94    definition: &CreateDomain,
95    constraint: DomainNotNull,
96    schema_names: &BTreeSet<String>,
97) -> Result<DomainNotNull, SQLError> {
98    if let Some(existing) = &definition.not_null {
99        return Ok(existing.clone());
100    }
101    let mut named = definition.clone();
102    named.not_null = Some(constraint);
103    constraints::assign_names(&mut named, schema_names)?;
104    Ok(named.not_null.expect("new domain NOT NULL"))
105}
106
107/// Bind only predecessor composite constructors; retain every other stored default, constraint and catalog identity.
108pub fn restore_composite_constructors(
109    context: &SchemaBindingContext<'_, '_>,
110    definition: &mut CreateDomain,
111) -> Result<bool, SQLError> {
112    let mut changed = false;
113    if let Some(default) = &mut definition.default {
114        if crate::type_resolution::composite_rows::expression_requires_binding(
115            default,
116            context.catalog,
117        )? {
118            changed |=
119                super::defaults::bind_stored_schema_expression(context, default, default.clone())?;
120        }
121    }
122    for check in &mut definition.checks {
123        if crate::type_resolution::composite_rows::expression_requires_binding(
124            &check.expression,
125            context.catalog,
126        )? {
127            bind_domain_check(context, &definition.base, &mut check.expression)?;
128            changed = true;
129        }
130    }
131    Ok(changed)
132}
133
134struct DomainValueResolver<'a>(&'a ColumnType);
135
136impl VariableResolver for DomainValueResolver<'_> {
137    fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
138        if name != "value" {
139            return Err(SQLError::UnknownColumn(name.into()));
140        }
141        Ok(Some(ResolvedVariable {
142            value: Value::Null,
143            declared_type: Some(self.0.catalog_name()),
144        }))
145    }
146
147    fn resolve_qualified(
148        &mut self,
149        qualifier: &str,
150        _column: &str,
151    ) -> Result<Option<ResolvedVariable>, SQLError> {
152        Err(SQLError::UnknownTable(qualifier.into()))
153    }
154
155    fn resolve_param(&mut self, index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
156        Err(domain_error(
157            "42P02",
158            format!("there is no parameter ${index}"),
159        ))
160    }
161}
162
163fn bind_domain_check(
164    context: &SchemaBindingContext<'_, '_>,
165    base: &ColumnType,
166    expression: &mut Expr,
167) -> Result<(), SQLError> {
168    let original = crate::plan::ExpressionPlan::lower(expression.clone());
169    let mut plan = original.clone();
170    let input = RowSchema::with_types(vec!["value".into()], vec![Some(base.clone())]);
171    let ty =
172        crate::binding::analyze_domain_check(context.catalog, &mut plan, &input, context.binding)?;
173    let sites = crate::binding::syntax_sites::expression_syntax_sites(&original, &plan)?;
174    crate::catalog::stored_ast::bind_stored_expression_sites(expression, &sites)?;
175    if let Some(mut ty) = ty.as_ref() {
176        while let ColumnType::Domain { base, .. } = ty {
177            ty = base;
178        }
179        if *ty != ColumnType::Boolean {
180            return Err(domain_error(
181                "42804",
182                format!(
183                    "argument of CHECK must be type boolean, not type {}",
184                    ty.regtype_name()
185                ),
186            ));
187        }
188    } else if matches!(expression, Expr::Literal(Value::Null | Value::Str(_))) {
189        crate::catalog::stored_ast::read_unknown_stored_literal(
190            crate::FunctionTypeResolver::enum_labels(context.catalog),
191            crate::FunctionTypeResolver::catalog_input_functions(context.catalog),
192            expression,
193            &ColumnType::Boolean,
194            false,
195        )?;
196    } else {
197        *expression = Expr::Cast {
198            implicit: true,
199            expr: Box::new(expression.clone()),
200            ty: "boolean".into(),
201        };
202    }
203    // The stored syntax includes the boolean cast, so its typed copy does too.
204    let typed = bind_expr(expression, &mut DomainValueResolver(base))?;
205    super::defaults::bind_stored_schema_expression(context, expression, typed)?;
206    Ok(())
207}
208
209pub mod constraints;
210pub mod dependencies;
211pub mod removal;
212
213#[cfg(test)]
214mod tests;