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::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            "42809",
38            format!(
39                "\"{}\" is not a valid base type for a domain",
40                definition.base.sql_name()
41            ),
42        ));
43    }
44    if definition.collation.is_some() {
45        return Err(SQLError::Unsupported(
46            "domain collation binding is not implemented".into(),
47        ));
48    }
49    if let Some(default) = &mut definition.default {
50        super::defaults::validate_default_expression(context, default, &definition.base)?;
51        if let Expr::Literal(Value::Str(value)) = default {
52            let mut base = &definition.base;
53            while let ColumnType::Domain { base: parent, .. } = base {
54                base = parent;
55            }
56            crate::expr::cast_value_with_type_resolution(
57                &Value::Str(value.clone()),
58                None,
59                &base.without_type_modifiers().sql_name(),
60                Some(context.catalog),
61            )?;
62        }
63    }
64    constraints::assign_names(definition, schema_names)?;
65    for check in &mut definition.checks {
66        bind_domain_check(context, &definition.base, &mut check.expression)?;
67    }
68    definition
69        .checks
70        .sort_by(|left, right| left.name.cmp(&right.name));
71    Ok(())
72}
73
74struct DomainValueResolver<'a>(&'a ColumnType);
75
76impl VariableResolver for DomainValueResolver<'_> {
77    fn resolve_name(&mut self, name: &str) -> Result<Option<ResolvedVariable>, SQLError> {
78        if name != "value" {
79            return Err(SQLError::UnknownColumn(name.into()));
80        }
81        Ok(Some(ResolvedVariable {
82            value: Value::Null,
83            declared_type: Some(self.0.sql_name()),
84        }))
85    }
86
87    fn resolve_qualified(
88        &mut self,
89        qualifier: &str,
90        _column: &str,
91    ) -> Result<Option<ResolvedVariable>, SQLError> {
92        Err(SQLError::UnknownTable(qualifier.into()))
93    }
94
95    fn resolve_param(&mut self, index: usize) -> Result<Option<ResolvedVariable>, SQLError> {
96        Err(domain_error(
97            "42P02",
98            format!("there is no parameter ${index}"),
99        ))
100    }
101}
102
103fn bind_domain_check(
104    context: &SchemaBindingContext<'_, '_>,
105    base: &ColumnType,
106    expression: &mut Expr,
107) -> Result<(), SQLError> {
108    let typed = bind_expr(expression, &mut DomainValueResolver(base))?;
109    let plan = crate::plan::ExpressionPlan::lower(typed.clone());
110    if !plan.subqueries.is_empty() {
111        return Err(domain_error(
112            "0A000",
113            "cannot use subquery in check constraint",
114        ));
115    }
116    if crate::semantics::windows::expr_has_window(&plan.scalar) {
117        return Err(domain_error(
118            "42P20",
119            "window functions are not allowed in check constraints",
120        ));
121    }
122    if crate::semantics::aggregates::contains_aggregate(context.catalog, &plan.scalar) {
123        return Err(domain_error(
124            "42803",
125            "aggregate functions are not allowed in check constraints",
126        ));
127    }
128    let ty = crate::type_resolution::common_context_expression_type(
129        &plan.scalar,
130        &RowSchema::default(),
131        &[],
132        Some(context.catalog),
133    )?;
134    if let Some(ty) = ty {
135        if crate::expr::coercion_type_name(&ty) != "boolean" {
136            return Err(domain_error(
137                "42804",
138                format!(
139                    "argument of CHECK must be type boolean, not type {}",
140                    ty.sql_name()
141                ),
142            ));
143        }
144    } else {
145        *expression = Expr::Cast {
146            expr: Box::new(expression.clone()),
147            ty: "boolean".into(),
148        };
149    }
150    super::defaults::bind_stored_schema_expression_routines(context, expression, typed)?;
151    Ok(())
152}
153
154pub mod constraints;
155pub mod dependencies;
156pub mod removal;