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    definition.collation_oid = Some(crate::catalog::collations::declarations::select(
47        context.catalog,
48        &definition.base,
49        definition.collation.as_deref(),
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/// Restore predecessor range syntax and composite constructors while retaining stored catalog identities.
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        changed |= default.upgrade_legacy_serialized_dispatches();
115        let pattern = crate::binding::legacy_patterns::restore_expression(default, context.catalog)
116            .map_err(SQLError::Internal)?;
117        if pattern
118            || crate::type_resolution::composite_rows::expression_requires_binding(
119                default,
120                context.catalog,
121            )?
122        {
123            super::defaults::bind_stored_schema_expression(context, default, default.clone())?;
124            changed = true;
125        }
126    }
127    for check in &mut definition.checks {
128        changed |= check.expression.upgrade_legacy_serialized_dispatches();
129        let pattern = crate::binding::legacy_patterns::restore_expression(
130            &mut check.expression,
131            context.catalog,
132        )
133        .map_err(SQLError::Internal)?;
134        if pattern
135            || crate::type_resolution::composite_rows::expression_requires_binding(
136                &check.expression,
137                context.catalog,
138            )?
139        {
140            bind_domain_check(context, &definition.base, &mut check.expression)?;
141            changed = true;
142        }
143    }
144    Ok(changed)
145}
146
147struct DomainValueResolver<'a>(&'a ColumnType);
148
149impl VariableResolver for DomainValueResolver<'_> {
150    fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
151        if name != "value" {
152            return Err(SQLError::UnknownColumn(name.into()));
153        }
154        Ok(Some(ResolvedVariable {
155            collation_oid: None,
156            value: Value::Null,
157            declared_type: Some(self.0.catalog_name()),
158        }))
159    }
160
161    fn resolve_qualified(
162        &mut self,
163        qualifier: &str,
164        _column: &str,
165    ) -> Result<Option<ResolvedVariable>, SQLError> {
166        Err(SQLError::UnknownTable(qualifier.into()))
167    }
168
169    fn resolve_param(&mut self, index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
170        Err(domain_error(
171            "42P02",
172            format!("there is no parameter ${index}"),
173        ))
174    }
175}
176
177fn bind_domain_check(
178    context: &SchemaBindingContext<'_, '_>,
179    base: &ColumnType,
180    expression: &mut Expr,
181) -> Result<(), SQLError> {
182    let original = crate::plan::ExpressionPlan::lower(expression.clone());
183    let mut plan = original.clone();
184    let input = RowSchema::with_types(vec!["value".into()], vec![Some(base.clone())]);
185    let ty =
186        crate::binding::analyze_domain_check(context.catalog, &mut plan, &input, context.binding)?;
187    let sites = crate::binding::syntax_sites::expression_syntax_sites(&original, &plan)?;
188    crate::catalog::stored_ast::bind_stored_expression_sites(expression, &sites)?;
189    if let Some(mut ty) = ty.as_ref() {
190        while let ColumnType::Domain { base, .. } = ty {
191            ty = base;
192        }
193        if *ty != ColumnType::Boolean {
194            return Err(domain_error(
195                "42804",
196                format!(
197                    "argument of CHECK must be type boolean, not type {}",
198                    ty.regtype_name()
199                ),
200            ));
201        }
202    } else if matches!(expression, Expr::Literal(Value::Null | Value::Str(_))) {
203        crate::catalog::stored_ast::read_unknown_stored_literal(
204            crate::FunctionTypeResolver::enum_labels(context.catalog),
205            crate::FunctionTypeResolver::catalog_input_functions(context.catalog),
206            expression,
207            &ColumnType::Boolean,
208            false,
209        )?;
210    } else {
211        *expression = Expr::Cast {
212            implicit: true,
213            expr: Box::new(expression.clone()),
214            ty: "boolean".into(),
215        };
216    }
217    // The stored syntax includes the boolean cast, so its typed copy does too.
218    let typed = bind_expr(expression, &mut DomainValueResolver(base))?;
219    super::defaults::bind_stored_schema_expression(context, expression, typed)?;
220    Ok(())
221}
222
223pub mod constraints;
224pub mod dependencies;
225pub mod removal;
226
227#[cfg(test)]
228mod tests;