Skip to main content

uqa_sql/schema/
defaults.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Column default validation and binding, as `cookDefault` analyzes a default: the expression is checked for what a default cannot contain, reduced to a constant when it is one, and coerced to the column's type in assignment context.
8
9use crate::ast::{ColumnType, Expr};
10use crate::{RowSchema, SQLError};
11use uqa_core::Value;
12
13use super::SchemaBindingContext;
14
15/// Analyze a default as `cookDefault` does and bind it for storage. `column` names the column, or the domain, the default belongs to. Returns whether the default remains: a default that cooks to a NULL constant is dropped, as `AddRelationNewConstraints` and `DefineDomain` store no default for it.
16pub fn validate_default_expression(
17    context: &SchemaBindingContext<'_, '_>,
18    expression: &mut Expr,
19    target: &ColumnType,
20    column: &str,
21) -> Result<bool, SQLError> {
22    let plan = crate::plan::ExpressionPlan::lower(expression.clone());
23    if !plan.subqueries.is_empty() {
24        return Err(default_error(
25            "0A000",
26            "cannot use subquery in DEFAULT expression",
27        ));
28    }
29    if crate::semantics::windows::expr_has_window(&plan.scalar) {
30        return Err(default_error(
31            "42P20",
32            "window functions are not allowed in DEFAULT expressions",
33        ));
34    }
35    if crate::semantics::aggregates::contains_aggregate(context.catalog, &plan.scalar) {
36        return Err(default_error(
37            "42803",
38            "aggregate functions are not allowed in DEFAULT expressions",
39        ));
40    }
41    if crate::semantics::aggregates::expr_references_columns(&plan.scalar) {
42        return Err(default_error(
43            "0A000",
44            "cannot use column reference in DEFAULT expression",
45        ));
46    }
47    if crate::semantics::sets::validation::expression_may_return_set(
48        context.catalog,
49        context.catalog,
50        &plan.scalar,
51        &RowSchema::default(),
52        &[],
53    )? {
54        return Err(default_error(
55            "0A000",
56            "set-returning functions are not allowed in DEFAULT expressions",
57        ));
58    }
59    crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
60        context.catalog,
61        expression,
62        Some(target),
63    )?;
64    if !cook_constant(context, expression, target)? {
65        return Ok(false);
66    }
67    // Coercion follows the selected function signature: an unknown regclass input
68    // freezes an OID, while an explicitly typed text expression stays late-bound.
69    let original = crate::plan::ExpressionPlan::lower_with(expression.clone(), &|name: &str| {
70        context.catalog.has_registered_aggregate_function(name)
71    });
72    let mut bound = original.clone();
73    let source = crate::binding::analyze_default_inputs(
74        context.catalog,
75        context.catalog,
76        context.catalog,
77        &mut bound,
78        context.binding,
79    )?;
80    if let Some(source) = source {
81        check_assignable(&source, target, column, "default expression")?;
82    }
83    crate::binding::bind_expression_plan_routines_for_storage(
84        context.catalog,
85        &mut bound,
86        &[],
87        context.binding,
88        &RowSchema::default(),
89    )?;
90    let sites = crate::binding::syntax_sites::expression_syntax_sites(&original, &bound)?;
91    crate::catalog::stored_ast::bind_stored_expression_sites(expression, &sites)?;
92    Ok(true)
93}
94
95/// `coerce_to_target_type` in assignment context: the expression's type must have an assignment cast to the column's type.
96pub fn check_assignable(
97    source: &ColumnType,
98    target: &ColumnType,
99    column: &str,
100    expression: &str,
101) -> Result<(), SQLError> {
102    if crate::type_resolution::assignment_type_compatible(source, target) {
103        return Ok(());
104    }
105    Err(SQLError::Diagnostic {
106        sqlstate: "42804".into(),
107        message: format!(
108            "column \"{column}\" is of type {} but {expression} is of type {}",
109            target.regtype_name(),
110            source.regtype_name()
111        ),
112        detail: None,
113        hint: Some("You will need to rewrite or cast the expression.".into()),
114    })
115}
116
117/// Reduce a default that is a constant as parse analysis reduces it: an `unknown` literal is read by the column type's input function and stored as a constant of that type, a cast of a literal is read by the cast's type, and a NULL constant leaves no default. Returns whether a default remains.
118pub fn cook_constant(
119    context: &SchemaBindingContext<'_, '_>,
120    expression: &mut Expr,
121    target: &ColumnType,
122) -> Result<bool, SQLError> {
123    match expression {
124        Expr::Literal(Value::Null) => return Ok(false),
125        Expr::TypedLiteral {
126            value: Value::Null, ..
127        } => return Ok(false),
128        Expr::Literal(Value::Str(_)) => {
129            crate::catalog::stored_ast::read_unknown_stored_literal(
130                crate::FunctionTypeResolver::enum_labels(context.catalog),
131                crate::FunctionTypeResolver::catalog_input_functions(context.catalog),
132                expression,
133                target,
134                false,
135            )?;
136        }
137        Expr::Cast { expr, ty, .. }
138            if matches!(expr.as_ref(), Expr::Literal(Value::Str(_) | Value::Null)) =>
139        {
140            // A cast of a literal is read by the cast's type and keeps the modifier the cast writes.
141            let cast_type = crate::expr::EngineHook::resolve_type_name(context.catalog, ty)
142                .map_err(SQLError::Internal)?
143                .map_or_else(|| ColumnType::from_sql_name(ty), Ok)?;
144            let domain_cast = matches!(cast_type, ColumnType::Domain { .. });
145            if matches!(expr.as_ref(), Expr::Literal(Value::Null)) && !domain_cast {
146                return Ok(false);
147            }
148            if crate::type_resolution::catalog_input_type(&cast_type) {
149                // The cast is already the form the catalog binding of a stored expression resolves.
150                return Ok(true);
151            }
152            crate::catalog::stored_ast::read_unknown_stored_literal(
153                crate::FunctionTypeResolver::enum_labels(context.catalog),
154                crate::FunctionTypeResolver::catalog_input_functions(context.catalog),
155                expr,
156                &cast_type,
157                true,
158            )?;
159            if domain_cast {
160                // The base input is frozen, but the outer domain still checks
161                // its constraints when the default is evaluated, including NULL.
162                return Ok(true);
163            }
164            let cooked = std::mem::replace(expr.as_mut(), Expr::Literal(Value::Null));
165            *expression = cooked;
166        }
167        _ => {}
168    }
169    Ok(true)
170}
171
172/// Bind a copy of stored schema expression syntax and carry its exact routine identities, user-defined type identities and enum constants back into the syntax. `typed_expression` is the syntax with its column references replaced by typed placeholders.
173pub fn bind_stored_schema_expression(
174    context: &SchemaBindingContext<'_, '_>,
175    expression: &mut Expr,
176    typed_expression: Expr,
177) -> Result<bool, SQLError> {
178    let lowered = crate::plan::ExpressionPlan::lower_with(typed_expression, &|name: &str| {
179        context.catalog.has_registered_aggregate_function(name)
180    });
181    let mut plan = lowered.clone();
182    crate::binding::analyze_stored_expression_inputs(
183        context.catalog,
184        &mut plan,
185        context.binding,
186        &RowSchema::default(),
187    )?;
188    crate::binding::bind_expression_plan_routines_for_storage(
189        context.catalog,
190        &mut plan,
191        &[],
192        context.binding,
193        &RowSchema::default(),
194    )?;
195    let sites = crate::binding::syntax_sites::expression_syntax_sites(&lowered, &plan)?;
196    let changed = crate::catalog::stored_ast::bind_stored_expression_sites(expression, &sites)?;
197    // Function binding can introduce regclass casts in CHECK expressions too.
198    // Read those inputs before publication so refresh never repairs a new schema.
199    let before_inputs = expression.clone();
200    crate::schema::dependencies::oid_alias::read_oid_alias_constants(context.catalog, expression)?;
201    Ok(changed || *expression != before_inputs)
202}
203
204fn default_error(sqlstate: &str, message: &str) -> SQLError {
205    SQLError::Routine {
206        sqlstate: sqlstate.into(),
207        message: message.into(),
208    }
209}