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, Expr},
14    catalog::domain::DomainCatalog,
15    RowSchema, SQLError,
16};
17use std::collections::BTreeSet;
18use uqa_core::{RelationIdentity, Value};
19
20/// Namespace and collision reads used before binding a domain declaration.
21pub trait DomainCreationCatalog {
22    fn domain_type_exists(&self, name: &str) -> bool;
23    fn domain_table_exists(&self, name: &str) -> Result<bool, SQLError>;
24}
25
26pub fn bind_domain_creation_target(
27    catalog: &dyn DomainCreationCatalog,
28    definition: &mut CreateDomain,
29) -> Result<RelationIdentity, SQLError> {
30    let identity =
31        RelationIdentity::from_legacy_name(&definition.name).map_err(SQLError::Internal)?;
32    if catalog.domain_type_exists(&definition.name)
33        || catalog.domain_table_exists(&definition.name)?
34    {
35        return Err(domain_error(
36            "42710",
37            format!("type \"{}\" already exists", identity.name),
38        ));
39    }
40    Ok(identity)
41}
42
43pub fn prepare_domain_definition(
44    context: &SchemaBindingContext<'_, '_>,
45    domains: &dyn DomainCatalog,
46    definition: &mut CreateDomain,
47) -> Result<(), SQLError> {
48    let identity =
49        RelationIdentity::from_legacy_name(&definition.name).map_err(SQLError::Internal)?;
50    definition.base =
51        crate::type_resolution::resolve_declared_column_type(context.catalog, &definition.base)?;
52    if definition.default.is_none() {
53        definition.default =
54            crate::catalog::domain::domain_default_expression(domains, &definition.base);
55    }
56    if matches!(
57        definition.base,
58        ColumnType::Void | ColumnType::Record | ColumnType::AnyArray
59    ) {
60        return Err(domain_error(
61            "42809",
62            format!(
63                "\"{}\" is not a valid base type for a domain",
64                definition.base.sql_name()
65            ),
66        ));
67    }
68    if definition.collation.is_some() {
69        return Err(SQLError::Unsupported(
70            "domain collation binding is not implemented".into(),
71        ));
72    }
73    if let Some(default) = &mut definition.default {
74        super::defaults::validate_default_expression(context, default, &definition.base)?;
75        if let Expr::Literal(Value::Str(value)) = default {
76            let mut base = &definition.base;
77            while let ColumnType::Domain { base: parent, .. } = base {
78                base = parent;
79            }
80            crate::expr::cast_value_with_type_resolution(
81                &Value::Str(value.clone()),
82                None,
83                &base.without_type_modifiers().sql_name(),
84                Some(context.catalog),
85            )?;
86        }
87    }
88    let mut names = BTreeSet::new();
89    if let Some(not_null) = &mut definition.not_null {
90        let name = not_null
91            .name
92            .get_or_insert_with(|| format!("{}_not_null", identity.name));
93        names.insert(name.clone());
94    }
95    for check in &mut definition.checks {
96        if let Some(name) = &check.name {
97            if !names.insert(name.clone()) {
98                return Err(domain_error(
99                    "42710",
100                    format!(
101                        "constraint \"{name}\" for domain \"{}\" already exists",
102                        identity.name
103                    ),
104                ));
105            }
106        } else {
107            let base = format!("{}_check", identity.name);
108            let mut name = base.clone();
109            let mut suffix = 1;
110            while !names.insert(name.clone()) {
111                name = format!("{base}{suffix}");
112                suffix += 1;
113            }
114            check.name = Some(name);
115        }
116        bind_domain_check(context, &definition.base, &mut check.expression)?;
117    }
118    definition
119        .checks
120        .sort_by(|left, right| left.name.cmp(&right.name));
121    Ok(())
122}
123
124struct DomainValueResolver<'a>(&'a ColumnType);
125
126impl VariableResolver for DomainValueResolver<'_> {
127    fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
128        if name != "value" {
129            return Err(SQLError::UnknownColumn(name.into()));
130        }
131        Ok(Some(ResolvedVariable {
132            value: Value::Null,
133            declared_type: Some(self.0.sql_name()),
134        }))
135    }
136
137    fn resolve_qualified(
138        &mut self,
139        qualifier: &str,
140        _column: &str,
141    ) -> Result<Option<ResolvedVariable>, SQLError> {
142        Err(SQLError::UnknownTable(qualifier.into()))
143    }
144
145    fn resolve_param(&mut self, index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
146        Err(domain_error(
147            "42P02",
148            format!("there is no parameter ${index}"),
149        ))
150    }
151}
152
153fn bind_domain_check(
154    context: &SchemaBindingContext<'_, '_>,
155    base: &ColumnType,
156    expression: &mut Expr,
157) -> Result<(), SQLError> {
158    let typed = bind_expr(expression, &mut DomainValueResolver(base))?;
159    let plan = crate::plan::ExpressionPlan::lower(typed.clone());
160    if !plan.subqueries.is_empty() {
161        return Err(domain_error(
162            "0A000",
163            "cannot use subquery in check constraint",
164        ));
165    }
166    if crate::semantics::windows::expr_has_window(&plan.scalar) {
167        return Err(domain_error(
168            "42P20",
169            "window functions are not allowed in check constraints",
170        ));
171    }
172    if crate::semantics::aggregates::contains_aggregate(context.catalog, &plan.scalar) {
173        return Err(domain_error(
174            "42803",
175            "aggregate functions are not allowed in check constraints",
176        ));
177    }
178    let ty = crate::type_resolution::common_context_expression_type(
179        &plan.scalar,
180        &RowSchema::default(),
181        &[],
182        Some(context.catalog),
183    )?;
184    if let Some(ty) = ty {
185        if crate::expr::coercion_type_name(&ty) != "boolean" {
186            return Err(domain_error(
187                "42804",
188                format!(
189                    "argument of CHECK must be type boolean, not type {}",
190                    ty.sql_name()
191                ),
192            ));
193        }
194    } else {
195        *expression = Expr::Cast {
196            expr: Box::new(expression.clone()),
197            ty: "boolean".into(),
198        };
199    }
200    super::defaults::bind_stored_schema_expression_routines(context, expression, typed)?;
201    Ok(())
202}
203
204pub mod dependencies;
205pub mod removal;