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}
52fn described_columns(
53    context: &ColumnAlterAnalysisContext<'_>,
54    table: &str,
55    action: &str,
56) -> Result<Vec<ColumnDef>, SQLError> {
57    context
58        .columns
59        .try_describe_table(table)
60        .map_err(|error| ddl_storage_error(action, error))?
61        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))
62}
63/// `ATExecColumnDefault` for `SET DEFAULT`: the column is checked before the default is analyzed against its type. Returns whether a default remains; `SET DEFAULT NULL` leaves none.
64pub fn validate_column_default(
65    context: &ColumnAlterAnalysisContext<'_>,
66    table: &str,
67    column: &str,
68    default: &mut Expr,
69) -> Result<bool, SQLError> {
70    let columns = described_columns(context, table, "ALTER COLUMN SET DEFAULT")?;
71    super::validate_default_change(table, super::altered_column(table, &columns, column)?, true)?;
72    let target = context
73        .state
74        .column_type(table, column)
75        .map_err(|error| ddl_storage_error("ALTER COLUMN SET DEFAULT", error))?
76        .ok_or_else(|| super::undefined_relation_column(table, column))?;
77    let binding = context.bindings.bindings.binding_scope()?;
78    crate::schema::defaults::validate_default_expression(
79        &crate::schema::SchemaBindingContext {
80            catalog: context.bindings.schema,
81            binding: &binding.context(),
82        },
83        default,
84        &target,
85        column,
86    )
87}
88/// `ATExecColumnDefault` for `DROP DEFAULT`.
89pub fn validate_default_removal(
90    context: &ColumnAlterAnalysisContext<'_>,
91    table: &str,
92    column: &str,
93) -> Result<(), SQLError> {
94    let columns = described_columns(context, table, "ALTER COLUMN DROP DEFAULT")?;
95    super::validate_default_change(
96        table,
97        super::altered_column(table, &columns, column)?,
98        false,
99    )
100}
101fn not_generated(table: &str, column: &str) -> Result<String, SQLError> {
102    let relation = uqa_core::RelationIdentity::from_legacy_name(table).map_err(|error| {
103        SQLError::Internal(format!("resolve ALTER TABLE target `{table}`: {error}"))
104    })?;
105    Ok(format!(
106        "column \"{column}\" of relation \"{}\" is not a generated column",
107        relation.name
108    ))
109}
110pub fn analyze_generated_expression(
111    context: &ColumnAlterAnalysisContext<'_>,
112    table: &str,
113    qualifier: &str,
114    name: &str,
115    expression: Expr,
116) -> Result<(GeneratedColumn, GeneratedColumnKind), SQLError> {
117    let mut columns = context
118        .columns
119        .try_describe_table(table)
120        .map_err(|error| ddl_storage_error("ALTER COLUMN SET EXPRESSION", error))?
121        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
122    let column = columns
123        .iter_mut()
124        .find(|column| column.name == name)
125        .ok_or_else(|| super::missing_altered_column(table, name))?;
126    let Some(current) = column.generated.as_ref() else {
127        return Err(SQLError::Routine {
128            sqlstate: "55000".into(),
129            message: not_generated(table, name)?,
130        });
131    };
132    let kind = current.kind;
133    column.generated = Some(GeneratedColumn {
134        kind,
135        expression: Box::new(expression),
136        function_dependencies: Vec::new(),
137    });
138    let foreign_keys = context
139        .keys
140        .try_foreign_keys(table)
141        .map_err(|error| ddl_storage_error("ALTER COLUMN SET EXPRESSION", error))?;
142    let binding = context.bindings.bindings.binding_scope()?;
143    crate::schema::generated::prepare_generated_columns(
144        &crate::schema::SchemaBindingContext {
145            catalog: context.bindings.schema,
146            binding: &binding.context(),
147        },
148        qualifier,
149        &mut columns,
150        &foreign_keys,
151    )?;
152    let generated = columns
153        .iter()
154        .find(|column| column.name == name)
155        .and_then(|column| column.generated.clone())
156        .ok_or_else(|| {
157            SQLError::Internal(format!(
158                "generated column `{name}` disappeared during validation"
159            ))
160        })?;
161    Ok((generated, kind))
162}
163/// `ATExecDropExpression`'s checks: the notice that skips a column that is not generated under `IF EXISTS`, or `None` to drop the expression of a stored generated column.
164pub fn validate_drop_expression(
165    context: &ColumnAlterAnalysisContext<'_>,
166    table: &str,
167    name: &str,
168    if_exists: bool,
169) -> Result<Option<crate::SQLNotice>, SQLError> {
170    let columns = described_columns(context, table, "ALTER COLUMN DROP EXPRESSION")?;
171    let column = super::altered_column(table, &columns, name)?;
172    let Some(generated) = column.generated.as_ref() else {
173        let message = not_generated(table, name)?;
174        if if_exists {
175            return Ok(Some(crate::SQLNotice::notice(format!(
176                "{message}, skipping"
177            ))));
178        }
179        return Err(SQLError::Routine {
180            sqlstate: "55000".into(),
181            message,
182        });
183    };
184    if generated.kind == GeneratedColumnKind::Virtual {
185        let relation = uqa_core::RelationIdentity::from_legacy_name(table).map_err(|error| {
186            SQLError::Internal(format!("resolve ALTER TABLE target `{table}`: {error}"))
187        })?;
188        return Err(SQLError::Diagnostic {
189            sqlstate: "0A000".into(),
190            message: "ALTER TABLE / DROP EXPRESSION is not supported for virtual generated columns"
191                .into(),
192            detail: Some(format!(
193                "Column \"{name}\" of relation \"{}\" is a virtual generated column.",
194                relation.name
195            )),
196            hint: None,
197        });
198    }
199    Ok(None)
200}
201pub fn analyze_column_type(
202    context: &ColumnAlterAnalysisContext<'_>,
203    table: &str,
204    _qualifier: &str,
205    name: &str,
206    ty: &ColumnType,
207) -> Result<Option<GeneratedColumnKind>, SQLError> {
208    if !context
209        .state
210        .has_column(table, name)
211        .map_err(|error| ddl_storage_error("ALTER COLUMN", error))?
212    {
213        return Err(super::missing_altered_column(table, name));
214    }
215    // ATPrepAlterColumnType requires USAGE on the new type before CheckAttributeType rejects a pseudo-type.
216    context.bindings.schema.require_type_usage(ty)?;
217    super::validate_postgres_relation_column_type(name, ty)?;
218    let mut candidate_columns = context
219        .columns
220        .try_describe_table(table)
221        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
222        .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
223    let candidate = candidate_columns
224        .iter_mut()
225        .find(|column| column.name == name)
226        .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{name}")))?;
227    candidate.ty.clone_from(ty);
228    let target_generated_kind = candidate.generated.as_ref().map(|generated| generated.kind);
229    if target_generated_kind.is_none() {
230        let columns = context
231            .state
232            .stored_columns(table)
233            .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?;
234        let dependents = generated_columns_referencing_column(&columns, name);
235        if !dependents.is_empty() {
236            return Err(SQLError::TypeMismatch(format!("cannot alter type of column `{name}` because generated column(s) `{}` depend on it", dependents.join("`, `"))));
237        }
238    }
239    Ok(target_generated_kind)
240}
241
242/// Rebind the dependent declarations after every column type has changed. A foreign key can refer to another column changed by the same ALTER statement.
243pub fn validate_column_type_constraints(
244    context: &ColumnAlterAnalysisContext<'_>,
245    table: &str,
246    qualifier: &str,
247) -> Result<(), SQLError> {
248    let mut candidate_columns = described_columns(context, table, "ALTER COLUMN TYPE")?;
249    let key_constraints = context
250        .keys
251        .try_key_constraints(table)
252        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?;
253    let foreign_keys = context
254        .keys
255        .try_foreign_keys(table)
256        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?;
257    crate::schema::constraint_changes::validate_altered_constraint_column_types(
258        &context.constraint_types,
259        table,
260        &candidate_columns,
261        &key_constraints,
262        &foreign_keys,
263    )?;
264    let binding = context.bindings.bindings.binding_scope()?;
265    crate::schema::generated::prepare_generated_columns(
266        &crate::schema::SchemaBindingContext {
267            catalog: context.bindings.schema,
268            binding: &binding.context(),
269        },
270        qualifier,
271        &mut candidate_columns,
272        &foreign_keys,
273    )?;
274    Ok(())
275}