Skip to main content

uqa_sql/schema/columns/
alteration.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Analyze altered column expressions and types against ordered catalog snapshots.
8use crate::{
9    assignment::columns::{AssignmentColumnCatalog, ColumnCatalogError},
10    ast::{ColumnDef, ColumnType, Expr, GeneratedColumn, GeneratedColumnKind},
11    schema::{
12        columns::addition::AddedColumnKeys, constraint_changes::ConstraintTypeContext,
13        dependencies::registration::SchemaDependencyBindingContext,
14    },
15    SQLError,
16};
17/// Read only the declared type, existence, or loaded column generation requested by an ALTER action.
18pub trait ColumnChangeCatalog {
19    fn has_column(&self, table: &str, column: &str) -> Result<bool, ColumnCatalogError>;
20    fn column_type(
21        &self,
22        table: &str,
23        column: &str,
24    ) -> Result<Option<ColumnType>, ColumnCatalogError>;
25    fn stored_columns(&self, table: &str) -> Result<Vec<ColumnDef>, ColumnCatalogError>;
26}
27pub struct ColumnAlterAnalysisContext<'a> {
28    pub columns: &'a dyn AssignmentColumnCatalog,
29    pub keys: &'a dyn AddedColumnKeys,
30    pub state: &'a dyn ColumnChangeCatalog,
31    pub bindings: SchemaDependencyBindingContext<'a>,
32    pub constraint_types: ConstraintTypeContext<'a>,
33}
34fn ddl_storage_error(action: &str, error: ColumnCatalogError) -> SQLError {
35    crate::catalog::errors::storage_error(action, error.as_ref())
36}
37pub fn generated_columns_referencing_column(columns: &[ColumnDef], column: &str) -> Vec<String> {
38    columns
39        .iter()
40        .filter(|candidate| candidate.name != column)
41        .filter(|candidate| {
42            candidate.generated.as_ref().is_some_and(|generated| {
43                crate::schema::dependencies::schema_expr_references_column(
44                    &generated.expression,
45                    column,
46                )
47            })
48        })
49        .map(|candidate| candidate.name.clone())
50        .collect()
51}
52pub fn validate_column_default(
53    context: &ColumnAlterAnalysisContext<'_>,
54    table: &str,
55    column: &str,
56    default: &mut Expr,
57) -> Result<(), SQLError> {
58    super::reject_default_change_on_generated_column(context.columns, table, column)?;
59    let target = context
60        .state
61        .column_type(table, column)
62        .map_err(|error| ddl_storage_error("ALTER COLUMN SET DEFAULT", error))?
63        .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{column}")))?;
64    let binding = context.bindings.bindings.binding_scope()?;
65    crate::schema::defaults::validate_default_expression(
66        &crate::schema::SchemaBindingContext {
67            catalog: context.bindings.schema,
68            binding: &binding.context(),
69        },
70        default,
71        &target,
72    )
73}
74pub fn analyze_generated_expression(
75    context: &ColumnAlterAnalysisContext<'_>,
76    table: &str,
77    qualifier: &str,
78    name: &str,
79    expression: Expr,
80) -> Result<(GeneratedColumn, GeneratedColumnKind), SQLError> {
81    let mut columns = context
82        .columns
83        .try_describe_table(table)
84        .map_err(|error| ddl_storage_error("ALTER COLUMN SET EXPRESSION", error))?
85        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
86    let column = columns
87        .iter_mut()
88        .find(|column| column.name == name)
89        .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{name}")))?;
90    let Some(current) = column.generated.as_ref() else {
91        return Err(SQLError::TypeMismatch(format!(
92            "column `{name}` of relation `{table}` is not a generated column"
93        )));
94    };
95    let kind = current.kind;
96    column.generated = Some(GeneratedColumn {
97        kind,
98        expression: Box::new(expression),
99        function_dependencies: Vec::new(),
100    });
101    let key_constraints = context
102        .keys
103        .try_key_constraints(table)
104        .map_err(|error| ddl_storage_error("ALTER COLUMN SET EXPRESSION", error))?;
105    let foreign_keys = context
106        .keys
107        .try_foreign_keys(table)
108        .map_err(|error| ddl_storage_error("ALTER COLUMN SET EXPRESSION", error))?;
109    crate::schema::generated::prepare_generated_columns(
110        context.bindings.schema,
111        qualifier,
112        &mut columns,
113        &key_constraints,
114        &foreign_keys,
115    )?;
116    let generated = columns
117        .iter()
118        .find(|column| column.name == name)
119        .and_then(|column| column.generated.clone())
120        .ok_or_else(|| {
121            SQLError::Internal(format!(
122                "generated column `{name}` disappeared during validation"
123            ))
124        })?;
125    Ok((generated, kind))
126}
127pub fn validate_drop_expression(
128    context: &ColumnAlterAnalysisContext<'_>,
129    table: &str,
130    name: &str,
131) -> Result<(), SQLError> {
132    let columns = context
133        .columns
134        .try_describe_table(table)
135        .map_err(|error| ddl_storage_error("ALTER COLUMN DROP EXPRESSION", error))?
136        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
137    let column = columns
138        .iter()
139        .find(|column| column.name == name)
140        .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{name}")))?;
141    let Some(generated) = column.generated.as_ref() else {
142        return Err(SQLError::TypeMismatch(format!(
143            "column `{name}` of relation `{table}` is not a generated column"
144        )));
145    };
146    if generated.kind == GeneratedColumnKind::Virtual {
147        return Err(SQLError::Unsupported(format!(
148            "ALTER TABLE / DROP EXPRESSION is not supported for virtual generated column `{name}`"
149        )));
150    }
151    Ok(())
152}
153pub fn analyze_column_type(
154    context: &ColumnAlterAnalysisContext<'_>,
155    table: &str,
156    qualifier: &str,
157    name: &str,
158    ty: &ColumnType,
159) -> Result<Option<GeneratedColumnKind>, SQLError> {
160    if !context
161        .state
162        .has_column(table, name)
163        .map_err(|error| ddl_storage_error("ALTER COLUMN", error))?
164    {
165        return Err(SQLError::Unsupported(format!(
166            "ALTER TABLE ALTER COLUMN: column `{name}` does not exist"
167        )));
168    }
169    super::validate_postgres_relation_column_type(name, ty)?;
170    let mut candidate_columns = context
171        .columns
172        .try_describe_table(table)
173        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
174        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
175    let candidate = candidate_columns
176        .iter_mut()
177        .find(|column| column.name == name)
178        .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{name}")))?;
179    candidate.ty.clone_from(ty);
180    let target_generated_kind = candidate.generated.as_ref().map(|generated| generated.kind);
181    if target_generated_kind.is_none() {
182        let columns = context
183            .state
184            .stored_columns(table)
185            .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?;
186        let dependents = generated_columns_referencing_column(&columns, name);
187        if !dependents.is_empty() {
188            return Err(SQLError::TypeMismatch(format!("cannot alter type of column `{name}` because generated column(s) `{}` depend on it", dependents.join("`, `"))));
189        }
190    }
191    let key_constraints = context
192        .keys
193        .try_key_constraints(table)
194        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?;
195    let foreign_keys = context
196        .keys
197        .try_foreign_keys(table)
198        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?;
199    crate::schema::constraint_changes::validate_altered_constraint_column_types(
200        &context.constraint_types,
201        table,
202        &candidate_columns,
203        &key_constraints,
204        &foreign_keys,
205    )?;
206    crate::schema::generated::prepare_generated_columns(
207        context.bindings.schema,
208        qualifier,
209        &mut candidate_columns,
210        &key_constraints,
211        &foreign_keys,
212    )?;
213    Ok(target_generated_kind)
214}