Skip to main content

uqa_sql/schema/columns/
removal_metadata.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Validate column dependencies against retained relation metadata and remove local declarations.
8use crate::ast::{ColumnDef, ForeignKey, TableCheck, TableKeyConstraint};
9use crate::schema::dependencies::{
10    rewrites::stored_relation_reference_matches, schema_expr_depends_on_column,
11    schema_expr_references_column,
12};
13use std::ops::Deref;
14use uqa_core::RelationIdentity;
15pub type ColumnsRead<'a> = Box<dyn Deref<Target = Vec<ColumnDef>> + 'a>;
16pub type ForeignKeysRead<'a> = Box<dyn Deref<Target = Vec<ForeignKey>> + 'a>;
17pub type KeyConstraintsRead<'a> = Box<dyn Deref<Target = Vec<TableKeyConstraint>> + 'a>;
18pub trait ColumnDependencyState {
19    fn columns(&self) -> ColumnsRead<'_>;
20    fn foreign_keys(&self) -> ForeignKeysRead<'_>;
21    fn key_constraints(&self) -> KeyConstraintsRead<'_>;
22}
23pub type ColumnDependencyEntries<'a> = Vec<(String, Box<dyn ColumnDependencyState + 'a>)>;
24pub fn validate_column_dependencies(
25    target: &RelationIdentity,
26    table_name: &str,
27    column: &str,
28    entries: &[(String, Box<dyn ColumnDependencyState + '_>)],
29) -> Result<(), String> {
30    let target_state = entries
31        .iter()
32        .find(|(name, _)| name == table_name)
33        .map(|(_, state)| state)
34        .ok_or_else(|| format!("table `{table_name}` does not exist"))?;
35    for candidate in target_state.columns().iter() {
36        if candidate.name == column {
37            continue;
38        }
39        if candidate
40            .default
41            .as_ref()
42            .is_some_and(|expr| schema_expr_references_column(expr, column))
43            || candidate.generated.as_ref().is_some_and(|generated| {
44                schema_expr_references_column(&generated.expression, column)
45            })
46        {
47            return Err(format!(
48                    "ALTER TABLE DROP COLUMN `{table_name}`.`{column}` rejected: column `{}` has a dependent DEFAULT/generation expression",
49                    candidate.name
50                ));
51        }
52    }
53
54    // Dropping a column that a key constraint includes drops the constraint, so a foreign key that references the constraint's key depends on the column too.
55    let included_keys = target_state
56        .key_constraints()
57        .iter()
58        .filter(|constraint| {
59            constraint
60                .included_columns
61                .iter()
62                .any(|name| name == column)
63        })
64        .map(|constraint| (constraint.kind, constraint.columns.clone()))
65        .collect::<Vec<_>>();
66    let references_included_key = |referenced: Option<&[String]>| {
67        included_keys.iter().any(|(kind, key)| match referenced {
68            Some(referenced) => key.as_slice() == referenced,
69            // A reference without columns is to the primary key.
70            None => *kind == crate::ast::TableKeyConstraintKind::PrimaryKey,
71        })
72    };
73    let mut inbound = Vec::new();
74    for (candidate_name, table) in entries {
75        for foreign_key in table.foreign_keys().iter() {
76            let local_dependency = candidate_name == table_name
77                && (foreign_key.local_columns.iter().any(|name| name == column)
78                    || foreign_key
79                        .on_delete_set_columns
80                        .iter()
81                        .any(|name| name == column));
82            let referenced_dependency =
83                stored_relation_reference_matches(&foreign_key.ref_table, target)
84                    && (foreign_key.ref_columns.iter().any(|name| name == column)
85                        || references_included_key(
86                            (!foreign_key.ref_columns.is_empty())
87                                .then_some(foreign_key.ref_columns.as_slice()),
88                        ));
89            if referenced_dependency && !local_dependency {
90                inbound.push(candidate_name.clone());
91            }
92        }
93        for candidate in table.columns().iter() {
94            if candidate_name == table_name && candidate.name == column {
95                continue;
96            }
97            if candidate.references.as_ref().is_some_and(|reference| {
98                stored_relation_reference_matches(&reference.table, target)
99                    && (reference.column.as_deref() == Some(column)
100                        || references_included_key(
101                            reference.column.as_ref().map(std::slice::from_ref),
102                        ))
103            }) {
104                inbound.push(candidate_name.clone());
105            }
106        }
107    }
108    inbound.sort_unstable();
109    inbound.dedup();
110    if !inbound.is_empty() {
111        return Err(format!(
112                "ALTER TABLE DROP COLUMN `{table_name}`.`{column}` rejected: referenced by foreign key(s) on `{}`",
113                inbound.join("`, `")
114            ));
115    }
116    Ok(())
117}
118/// A column's written CHECK can depend only on the relation. Move that surviving declaration to the table without changing its identity or enforcement flags.
119pub fn remove_column_declarations(columns: &mut Vec<ColumnDef>, column: &str) -> Vec<TableCheck> {
120    let mut retained = Vec::new();
121    for definition in columns.iter_mut() {
122        if definition
123            .check
124            .as_ref()
125            .is_some_and(|expression| schema_expr_depends_on_column(expression, column))
126        {
127            let _ = crate::schema::constraint_changes::take_column_check(definition);
128        } else if definition.name == column {
129            retained.extend(crate::schema::constraint_changes::take_column_check(
130                definition,
131            ));
132        }
133    }
134    columns.retain(|c| c.name != column);
135    retained
136}
137pub fn remove_column_checks(checks: &mut Vec<TableCheck>, column: &str) {
138    checks.retain(|constraint| !schema_expr_depends_on_column(&constraint.expr, column));
139}
140/// A key constraint goes with any column of its supporting index: a key column or an included one. A single-column constraint that goes with an included column also gives up the key flag of its surviving key column, unless another constraint of its kind still keys that column alone.
141pub fn remove_column_keys(
142    columns: &mut [ColumnDef],
143    keys: &mut Vec<TableKeyConstraint>,
144    column: &str,
145) {
146    let mut removed = Vec::new();
147    keys.retain(|constraint| {
148        let goes = constraint
149            .columns
150            .iter()
151            .chain(&constraint.included_columns)
152            .any(|name| name == column);
153        if goes {
154            removed.push(constraint.clone());
155        }
156        !goes
157    });
158    for constraint in removed {
159        let [key] = constraint.columns.as_slice() else {
160            continue;
161        };
162        if keys
163            .iter()
164            .any(|kept| kept.kind == constraint.kind && kept.columns.as_slice() == [key.as_str()])
165        {
166            continue;
167        }
168        if let Some(definition) = columns
169            .iter_mut()
170            .find(|definition| definition.name == *key)
171        {
172            match constraint.kind {
173                crate::ast::TableKeyConstraintKind::PrimaryKey => definition.primary_key = false,
174                crate::ast::TableKeyConstraintKind::Unique => definition.unique = false,
175            }
176        }
177    }
178}
179pub fn remove_column_foreign_keys(keys: &mut Vec<ForeignKey>, column: &str) {
180    keys.retain(|foreign_key| {
181        !foreign_key.local_columns.iter().any(|name| name == column)
182            && !foreign_key
183                .on_delete_set_columns
184                .iter()
185                .any(|name| name == column)
186    });
187}
188
189#[cfg(test)]
190mod tests;