Skip to main content

uqa_sql/schema/
generated.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` 18 generated-column validation and row computation.
8
9use super::SchemaExpressionCatalog;
10use crate::ast::ForeignKey;
11use crate::ast::{ColumnDef, Expr, GeneratedColumnKind, TableKeyConstraint};
12use crate::{
13    assignment::conversion::convert_value_to_column_type, semantics::aggregates, ColumnType,
14    SQLError,
15};
16
17pub(crate) mod eligibility;
18pub(super) mod typing;
19
20pub fn prepare_generated_columns(
21    engine: &dyn SchemaExpressionCatalog,
22    qualifier: &str,
23    columns: &mut [ColumnDef],
24    key_constraints: &[TableKeyConstraint],
25    foreign_keys: &[ForeignKey],
26) -> Result<(), SQLError> {
27    let snapshot = columns.to_vec();
28    for (index, column) in snapshot.iter().enumerate() {
29        let Some(generated) = column.generated.as_ref() else {
30            continue;
31        };
32        if column.default.is_some() {
33            return Err(SQLError::TypeMismatch(format!(
34                "both default and generation expression specified for column `{}`",
35                column.name
36            )));
37        }
38        if column.auto_increment.is_some() {
39            return Err(SQLError::TypeMismatch(format!(
40                "both identity and generation expression specified for column `{}`",
41                column.name
42            )));
43        }
44        if generated.kind == GeneratedColumnKind::Virtual {
45            validate_virtual_column_envelope(column, key_constraints, foreign_keys)?;
46        }
47        let plan = crate::plan::ExpressionPlan::lower((*generated.expression).clone());
48        if !plan.subqueries.is_empty() {
49            return Err(SQLError::TypeMismatch(
50                "cannot use subquery in column generation expression".into(),
51            ));
52        }
53        if aggregates::contains_aggregate(engine, &plan.scalar) {
54            return Err(SQLError::TypeMismatch(
55                "aggregate functions are not allowed in column generation expressions".into(),
56            ));
57        }
58        validate_generation_expression(
59            engine,
60            qualifier,
61            &snapshot,
62            &generated.expression,
63            generated.kind,
64        )?;
65        let prepared = columns[index]
66            .generated
67            .as_mut()
68            .ok_or_else(|| SQLError::Internal("generated column disappeared".into()))?;
69        bind_schema_column_references(&mut prepared.expression, qualifier);
70        let (expression_type, function_dependencies) =
71            typing::infer_generation_expression(engine, &snapshot, &mut prepared.expression)?;
72        crate::catalog::regrole_dependencies::reject_stored_regrole_constants(
73            engine,
74            &prepared.expression,
75            Some(&column.ty),
76        )?;
77        if let typing::GenerationType::UnknownLiteral(value) = &expression_type {
78            convert_value_to_column_type(uqa_core::Value::Str(value.clone()), &column.ty)?;
79        } else if !typing::generation_type_assignable_to(&expression_type, &column.ty) {
80            return Err(SQLError::TypeMismatch(format!(
81                "column `{}` has type {} but generation expression has type {}",
82                column.name,
83                crate::catalog::type_metadata::column_type_name(&column.ty),
84                typing::generation_type_name(&expression_type)
85            )));
86        }
87        prepared.function_dependencies = function_dependencies;
88    }
89    Ok(())
90}
91
92fn validate_virtual_column_envelope(
93    column: &ColumnDef,
94    key_constraints: &[TableKeyConstraint],
95    foreign_keys: &[ForeignKey],
96) -> Result<(), SQLError> {
97    if contains_engine_defined_type(&column.ty) {
98        return Err(SQLError::TypeMismatch(format!(
99            "virtual generated column `{}` cannot use a user-defined type",
100            column.name
101        )));
102    }
103    if column.primary_key
104        || key_constraints.iter().any(|constraint| {
105            constraint.kind == crate::ast::TableKeyConstraintKind::PrimaryKey
106                && constraint.columns.iter().any(|name| name == &column.name)
107        })
108    {
109        return Err(SQLError::TypeMismatch(
110            "primary keys on virtual generated columns are not supported".into(),
111        ));
112    }
113    if column.unique
114        || key_constraints.iter().any(|constraint| {
115            constraint.kind == crate::ast::TableKeyConstraintKind::Unique
116                && constraint.columns.iter().any(|name| name == &column.name)
117        })
118    {
119        return Err(SQLError::TypeMismatch(
120            "unique constraints on virtual generated columns are not supported".into(),
121        ));
122    }
123    if column.references.is_some()
124        || foreign_keys.iter().any(|foreign_key| {
125            foreign_key
126                .local_columns
127                .iter()
128                .any(|name| name == &column.name)
129        })
130    {
131        return Err(SQLError::TypeMismatch(
132            "foreign key constraints on virtual generated columns are not supported".into(),
133        ));
134    }
135    Ok(())
136}
137
138fn contains_engine_defined_type(ty: &ColumnType) -> bool {
139    match ty {
140        ColumnType::Vector(_) | ColumnType::Tensor(_) => true,
141        ColumnType::Array(element) => contains_engine_defined_type(element),
142        _ => false,
143    }
144}
145
146#[expect(
147    clippy::too_many_lines,
148    reason = "preserves generated coercion diagnostics"
149)]
150fn validate_generation_expression(
151    engine: &dyn SchemaExpressionCatalog,
152    qualifier: &str,
153    columns: &[ColumnDef],
154    expression: &Expr,
155    kind: GeneratedColumnKind,
156) -> Result<(), SQLError> {
157    match expression {
158        Expr::Column(name) => validate_generation_column_reference(columns, name),
159        Expr::QualifiedColumn {
160            qualifier: expression_qualifier,
161            column,
162            ..
163        } => {
164            if expression_qualifier != qualifier {
165                return Err(SQLError::UnknownTable(expression_qualifier.clone()));
166            }
167            validate_generation_column_reference(columns, column)
168        }
169        Expr::Func {
170            name,
171            binding,
172            args,
173            distinct,
174            order_by,
175            filter,
176            ..
177        } => {
178            if *distinct || !order_by.is_empty() || filter.is_some() {
179                return Err(SQLError::TypeMismatch(
180                    "aggregate syntax is not allowed in column generation expressions".into(),
181                ));
182            }
183            if kind == GeneratedColumnKind::Virtual
184                && binding
185                    .as_ref()
186                    .and_then(|binding| binding.dispatch)
187                    .is_none()
188                && (engine
189                    .registered_runtime_function_volatility(name)
190                    .is_some()
191                    || engine.lookup_visible_sql_functions(name)?.is_some())
192            {
193                return Err(SQLError::Diagnostic {
194                    sqlstate: "0A000".into(),
195                    message: "generation expression uses user-defined function".into(),
196                    detail: Some("Virtual generated columns that make use of user-defined functions are not yet supported.".into()),
197                    hint: None,
198                });
199            }
200            for argument in args {
201                validate_generation_expression(engine, qualifier, columns, argument, kind)?;
202            }
203            Ok(())
204        }
205        Expr::Array(items) | Expr::Row(items) | Expr::And(items) | Expr::Or(items) => {
206            for item in items {
207                validate_generation_expression(engine, qualifier, columns, item, kind)?;
208            }
209            Ok(())
210        }
211        Expr::Binary { lhs, rhs, .. } => {
212            validate_generation_expression(engine, qualifier, columns, lhs, kind)?;
213            validate_generation_expression(engine, qualifier, columns, rhs, kind)
214        }
215        Expr::Not(inner)
216        | Expr::UnaryMinus(inner)
217        | Expr::IsNull { expr: inner, .. }
218        | Expr::Cast { expr: inner, .. } => {
219            validate_generation_expression(engine, qualifier, columns, inner, kind)
220        }
221        Expr::Between { expr, low, high } => {
222            validate_generation_expression(engine, qualifier, columns, expr, kind)?;
223            validate_generation_expression(engine, qualifier, columns, low, kind)?;
224            validate_generation_expression(engine, qualifier, columns, high, kind)
225        }
226        Expr::InList { expr, list, .. } => {
227            validate_generation_expression(engine, qualifier, columns, expr, kind)?;
228            for item in list {
229                validate_generation_expression(engine, qualifier, columns, item, kind)?;
230            }
231            Ok(())
232        }
233        Expr::Case {
234            base,
235            when,
236            else_branch,
237        } => {
238            if let Some(base) = base {
239                validate_generation_expression(engine, qualifier, columns, base, kind)?;
240            }
241            for (condition, result) in when {
242                validate_generation_expression(engine, qualifier, columns, condition, kind)?;
243                validate_generation_expression(engine, qualifier, columns, result, kind)?;
244            }
245            if let Some(else_branch) = else_branch {
246                validate_generation_expression(engine, qualifier, columns, else_branch, kind)?;
247            }
248            Ok(())
249        }
250        Expr::Default | Expr::Param(_) => Err(SQLError::TypeMismatch(
251            "parameters and DEFAULT are not allowed in column generation expressions".into(),
252        )),
253        Expr::Star | Expr::QualifiedStar(_) => Err(SQLError::TypeMismatch(
254            "whole-row references are not allowed in column generation expressions".into(),
255        )),
256        Expr::InternalColumn(_) => Err(SQLError::Internal(
257            "executor-only column reached generation expression validation".into(),
258        )),
259        Expr::WindowCall { .. } => Err(SQLError::TypeMismatch(
260            "window functions are not allowed in column generation expressions".into(),
261        )),
262        Expr::ScalarSubquery(_) | Expr::Exists { .. } | Expr::InSubquery { .. } => Err(
263            SQLError::TypeMismatch("cannot use subquery in column generation expression".into()),
264        ),
265        Expr::Literal(_) | Expr::TypedLiteral { .. } => Ok(()),
266    }
267}
268
269pub fn bind_schema_column_references(expression: &mut Expr, qualifier: &str) {
270    if let Expr::QualifiedColumn {
271        qualifier: expression_qualifier,
272        column,
273    } = expression
274    {
275        if expression_qualifier == qualifier {
276            *expression = Expr::Column(column.clone());
277        }
278        return;
279    }
280    match expression {
281        Expr::Func {
282            args,
283            order_by,
284            filter,
285            ..
286        } => {
287            for argument in args {
288                bind_schema_column_references(argument, qualifier);
289            }
290            for order in order_by {
291                bind_schema_column_references(&mut order.expr, qualifier);
292            }
293            if let Some(filter) = filter {
294                bind_schema_column_references(filter, qualifier);
295            }
296        }
297        Expr::Array(items) | Expr::Row(items) | Expr::And(items) | Expr::Or(items) => {
298            for item in items {
299                bind_schema_column_references(item, qualifier);
300            }
301        }
302        Expr::Binary { lhs, rhs, .. } => {
303            bind_schema_column_references(lhs, qualifier);
304            bind_schema_column_references(rhs, qualifier);
305        }
306        Expr::Not(inner)
307        | Expr::UnaryMinus(inner)
308        | Expr::IsNull { expr: inner, .. }
309        | Expr::Cast { expr: inner, .. } => {
310            bind_schema_column_references(inner, qualifier);
311        }
312        Expr::Between { expr, low, high } => {
313            bind_schema_column_references(expr, qualifier);
314            bind_schema_column_references(low, qualifier);
315            bind_schema_column_references(high, qualifier);
316        }
317        Expr::InList { expr, list, .. } => {
318            bind_schema_column_references(expr, qualifier);
319            for item in list {
320                bind_schema_column_references(item, qualifier);
321            }
322        }
323        Expr::Case {
324            base,
325            when,
326            else_branch,
327        } => {
328            if let Some(base) = base {
329                bind_schema_column_references(base, qualifier);
330            }
331            for (condition, result) in when {
332                bind_schema_column_references(condition, qualifier);
333                bind_schema_column_references(result, qualifier);
334            }
335            if let Some(else_branch) = else_branch {
336                bind_schema_column_references(else_branch, qualifier);
337            }
338        }
339        Expr::Star
340        | Expr::QualifiedStar(_)
341        | Expr::Default
342        | Expr::Column(_)
343        | Expr::QualifiedColumn { .. }
344        | Expr::InternalColumn(_)
345        | Expr::Literal(_)
346        | Expr::TypedLiteral { .. }
347        | Expr::Param(_)
348        | Expr::WindowCall { .. }
349        | Expr::ScalarSubquery(_)
350        | Expr::Exists { .. }
351        | Expr::InSubquery { .. } => {}
352    }
353}
354
355fn validate_generation_column_reference(columns: &[ColumnDef], name: &str) -> Result<(), SQLError> {
356    let Some(column) = columns.iter().find(|column| column.name == name) else {
357        return Err(SQLError::UnknownColumn(name.to_string()));
358    };
359    if column.generated.is_some() {
360        return Err(SQLError::TypeMismatch(format!(
361            "cannot use generated column `{name}` in column generation expression"
362        )));
363    }
364    Ok(())
365}