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