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