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_references_column,
11};
12use std::ops::Deref;
13use uqa_core::RelationIdentity;
14pub type ColumnsRead<'a> = Box<dyn Deref<Target = Vec<ColumnDef>> + 'a>;
15pub type ForeignKeysRead<'a> = Box<dyn Deref<Target = Vec<ForeignKey>> + 'a>;
16pub type KeyConstraintsRead<'a> = Box<dyn Deref<Target = Vec<TableKeyConstraint>> + 'a>;
17pub trait ColumnDependencyState {
18    fn columns(&self) -> ColumnsRead<'_>;
19    fn foreign_keys(&self) -> ForeignKeysRead<'_>;
20    fn key_constraints(&self) -> KeyConstraintsRead<'_>;
21}
22pub type ColumnDependencyEntries<'a> = Vec<(String, Box<dyn ColumnDependencyState + 'a>)>;
23pub fn validate_column_dependencies(
24    target: &RelationIdentity,
25    table_name: &str,
26    column: &str,
27    entries: &[(String, Box<dyn ColumnDependencyState + '_>)],
28) -> Result<(), String> {
29    let target_state = entries
30        .iter()
31        .find(|(name, _)| name == table_name)
32        .map(|(_, state)| state)
33        .ok_or_else(|| format!("table `{table_name}` does not exist"))?;
34    for candidate in target_state.columns().iter() {
35        if candidate.name == column {
36            continue;
37        }
38        if candidate
39            .default
40            .as_ref()
41            .is_some_and(|expr| schema_expr_references_column(expr, column))
42            || candidate.generated.as_ref().is_some_and(|generated| {
43                schema_expr_references_column(&generated.expression, column)
44            })
45        {
46            return Err(format!(
47                    "ALTER TABLE DROP COLUMN `{table_name}`.`{column}` rejected: column `{}` has a dependent DEFAULT/generation expression",
48                    candidate.name
49                ));
50        }
51    }
52
53    // 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.
54    let included_keys = target_state
55        .key_constraints()
56        .iter()
57        .filter(|constraint| {
58            constraint
59                .included_columns
60                .iter()
61                .any(|name| name == column)
62        })
63        .map(|constraint| (constraint.kind, constraint.columns.clone()))
64        .collect::<Vec<_>>();
65    let references_included_key = |referenced: Option<&[String]>| {
66        included_keys.iter().any(|(kind, key)| match referenced {
67            Some(referenced) => key.as_slice() == referenced,
68            // A reference without columns is to the primary key.
69            None => *kind == crate::ast::TableKeyConstraintKind::PrimaryKey,
70        })
71    };
72    let mut inbound = Vec::new();
73    for (candidate_name, table) in entries {
74        for foreign_key in table.foreign_keys().iter() {
75            let local_dependency = candidate_name == table_name
76                && (foreign_key.local_columns.iter().any(|name| name == column)
77                    || foreign_key
78                        .on_delete_set_columns
79                        .iter()
80                        .any(|name| name == column));
81            let referenced_dependency =
82                stored_relation_reference_matches(&foreign_key.ref_table, target)
83                    && (foreign_key.ref_columns.iter().any(|name| name == column)
84                        || references_included_key(
85                            (!foreign_key.ref_columns.is_empty())
86                                .then_some(foreign_key.ref_columns.as_slice()),
87                        ));
88            if referenced_dependency && !local_dependency {
89                inbound.push(candidate_name.clone());
90            }
91        }
92        for candidate in table.columns().iter() {
93            if candidate_name == table_name && candidate.name == column {
94                continue;
95            }
96            if candidate.references.as_ref().is_some_and(|reference| {
97                stored_relation_reference_matches(&reference.table, target)
98                    && (reference.column.as_deref() == Some(column)
99                        || references_included_key(
100                            reference.column.as_ref().map(std::slice::from_ref),
101                        ))
102            }) {
103                inbound.push(candidate_name.clone());
104            }
105        }
106    }
107    inbound.sort_unstable();
108    inbound.dedup();
109    if !inbound.is_empty() {
110        return Err(format!(
111                "ALTER TABLE DROP COLUMN `{table_name}`.`{column}` rejected: referenced by foreign key(s) on `{}`",
112                inbound.join("`, `")
113            ));
114    }
115    Ok(())
116}
117pub fn remove_column_declarations(columns: &mut Vec<ColumnDef>, column: &str) {
118    for definition in columns.iter_mut() {
119        if definition
120            .check
121            .as_ref()
122            .is_some_and(|expression| schema_expr_references_column(expression, column))
123        {
124            definition.check = None;
125            definition.check_name = None;
126            definition.check_object_id = None;
127            definition.check_catalog_oid = None;
128            definition.check_is_local = true;
129            definition.check_enforced = true;
130            definition.check_validated = true;
131            definition.check_no_inherit = false;
132        }
133    }
134    columns.retain(|c| c.name != column);
135}
136pub fn remove_column_checks(checks: &mut Vec<TableCheck>, column: &str) {
137    checks.retain(|constraint| !schema_expr_references_column(&constraint.expr, column));
138}
139/// 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.
140pub fn remove_column_keys(
141    columns: &mut [ColumnDef],
142    keys: &mut Vec<TableKeyConstraint>,
143    column: &str,
144) {
145    let mut removed = Vec::new();
146    keys.retain(|constraint| {
147        let goes = constraint
148            .columns
149            .iter()
150            .chain(&constraint.included_columns)
151            .any(|name| name == column);
152        if goes {
153            removed.push(constraint.clone());
154        }
155        !goes
156    });
157    for constraint in removed {
158        let [key] = constraint.columns.as_slice() else {
159            continue;
160        };
161        if keys
162            .iter()
163            .any(|kept| kept.kind == constraint.kind && kept.columns.as_slice() == [key.as_str()])
164        {
165            continue;
166        }
167        if let Some(definition) = columns
168            .iter_mut()
169            .find(|definition| definition.name == *key)
170        {
171            match constraint.kind {
172                crate::ast::TableKeyConstraintKind::PrimaryKey => definition.primary_key = false,
173                crate::ast::TableKeyConstraintKind::Unique => definition.unique = false,
174            }
175        }
176    }
177}
178pub fn remove_column_foreign_keys(keys: &mut Vec<ForeignKey>, column: &str) {
179    keys.retain(|foreign_key| {
180        !foreign_key.local_columns.iter().any(|name| name == column)
181            && !foreign_key
182                .on_delete_set_columns
183                .iter()
184                .any(|name| name == column)
185    });
186}