Skip to main content

uqa_sql/schema/
constraint_changes.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Locate durable constraints and analyze changes to their type, identity, and enforcement metadata.
8pub mod foreign_key_target;
9pub mod inheritance;
10pub mod names;
11pub mod not_null_removal;
12pub mod renaming;
13pub mod validation;
14
15use crate::schema::foreign_keys::column_foreign_key;
16use crate::{
17    ast::{ColumnType, ForeignKey, TableCheck},
18    SQLError,
19};
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum ConstraintLocation {
22    NotNull(usize),
23    ColumnCheck(usize),
24    ColumnForeignKey(usize),
25    TableCheck(usize),
26    TableForeignKey(usize),
27    Key(usize),
28}
29
30pub fn constraint_error(sqlstate: &str, message: impl Into<String>) -> SQLError {
31    SQLError::Routine {
32        sqlstate: sqlstate.into(),
33        message: message.into(),
34    }
35}
36
37pub fn find_constraint(
38    columns: &[crate::ast::ColumnDef],
39    constraints: &crate::ast::TableConstraintSet,
40    name: &str,
41) -> Option<ConstraintLocation> {
42    names::ConstraintNames::from_definition(columns, constraints)
43        .entries()
44        .find(|constraint| constraint.name == name)
45        .map(|constraint| constraint.location)
46}
47
48pub fn ensure_constraint_name_available(
49    columns: &[crate::ast::ColumnDef],
50    constraints: &crate::ast::TableConstraintSet,
51    name: Option<&str>,
52    table: &str,
53) -> Result<(), SQLError> {
54    if let Some(name) = name.filter(|name| find_constraint(columns, constraints, name).is_some()) {
55        return Err(constraint_error(
56            "42710",
57            format!("constraint \"{name}\" for relation \"{table}\" already exists"),
58        ));
59    }
60    Ok(())
61}
62
63pub fn ensure_not_null_inheritable(
64    table: &str,
65    column: &crate::ast::ColumnDef,
66    sqlstate: &str,
67) -> Result<(), SQLError> {
68    if column.not_null_no_inherit {
69        let relation = uqa_core::RelationIdentity::from_legacy_name(table)
70            .map_err(|error| SQLError::Internal(format!("resolve NOT NULL relation: {error}")))?;
71        let name = column.not_null_name.as_deref().unwrap_or("<unnamed>");
72        return Err(constraint_error(
73            sqlstate,
74            format!(
75            "cannot change NO INHERIT status of NOT NULL constraint \"{name}\" on relation \"{}\"",
76            relation.name,
77        ),
78        ));
79    }
80    Ok(())
81}
82
83pub fn take_column_check(column: &mut crate::ast::ColumnDef) -> Option<TableCheck> {
84    let check = TableCheck {
85        expr: column.check.take()?,
86        catalog_oid: column.check_catalog_oid.take(),
87        name: column.check_name.take(),
88        object_id: column.check_object_id.take(),
89        is_local: column.check_is_local,
90        enforced: column.check_enforced,
91        validated: column.check_validated,
92        no_inherit: column.check_no_inherit,
93        partition_constraint: None,
94    };
95    column.check_is_local = true;
96    column.check_enforced = true;
97    column.check_validated = true;
98    column.check_no_inherit = false;
99    Some(check)
100}
101
102pub fn foreign_key_object_id(
103    columns: &[crate::ast::ColumnDef],
104    constraints: &crate::ast::TableConstraintSet,
105    location: ConstraintLocation,
106) -> Option<[u8; 16]> {
107    match location {
108        ConstraintLocation::ColumnForeignKey(index) => columns[index]
109            .references
110            .as_ref()
111            .and_then(|reference| reference.object_id),
112        ConstraintLocation::TableForeignKey(index) => constraints.foreign_keys[index].object_id,
113        _ => None,
114    }
115}
116
117pub trait ConstraintTypeReferrers {
118    fn try_referrers_to(
119        &self,
120        table: &str,
121    ) -> Result<Vec<(String, ForeignKey)>, crate::assignment::columns::ColumnCatalogError>;
122}
123pub struct ConstraintTypeContext<'a> {
124    pub foreign_keys: crate::schema::foreign_keys::ForeignKeyDefinitionContext<'a>,
125    pub referrers: &'a dyn ConstraintTypeReferrers,
126}
127fn ddl_storage_error(
128    action: &str,
129    error: crate::assignment::columns::ColumnCatalogError,
130) -> SQLError {
131    crate::catalog::errors::storage_error(action, error.as_ref())
132}
133pub fn validate_altered_constraint_column_types(
134    context: &ConstraintTypeContext<'_>,
135    table: &str,
136    candidate_columns: &[crate::ast::ColumnDef],
137    key_constraints: &[crate::ast::TableKeyConstraint],
138    foreign_keys: &[ForeignKey],
139) -> Result<(), SQLError> {
140    for constraint in key_constraints
141        .iter()
142        .filter(|constraint| constraint.without_overlaps)
143    {
144        let Some(period_column) = constraint.columns.last() else {
145            return Err(SQLError::Internal(
146                "WITHOUT OVERLAPS constraint has no period column".into(),
147            ));
148        };
149        let period_type = candidate_columns
150            .iter()
151            .find(|column| column.name == *period_column)
152            .map(|column| &column.ty)
153            .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{period_column}")))?;
154        if !matches!(
155            period_type,
156            ColumnType::Range(_) | ColumnType::Multirange(_)
157        ) {
158            return Err(SQLError::Routine {
159                sqlstate: "42804".into(),
160                message: format!(
161                    "column \"{period_column}\" in WITHOUT OVERLAPS is not a range or multirange type"
162                ),
163            });
164        }
165    }
166
167    for foreign_key in foreign_keys.iter().filter(|foreign_key| foreign_key.period) {
168        let (parent_name, parent_columns, parent_keys) =
169            crate::schema::foreign_keys::resolve_foreign_key_parent(
170                &context.foreign_keys,
171                &foreign_key.ref_table,
172            )?;
173        let parent_columns = if parent_name == table {
174            candidate_columns
175        } else {
176            parent_columns.as_slice()
177        };
178        crate::schema::constraints::validate_foreign_key_definition(
179            table,
180            candidate_columns,
181            &parent_name,
182            parent_columns,
183            &parent_keys,
184            foreign_key,
185        )?;
186    }
187
188    for (child_table, foreign_key) in context
189        .referrers
190        .try_referrers_to(table)
191        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
192        .into_iter()
193        .filter(|(_, foreign_key)| foreign_key.period)
194    {
195        let child_columns = if child_table == table {
196            candidate_columns.to_vec()
197        } else {
198            context
199                .foreign_keys
200                .columns
201                .try_describe_table(&child_table)
202                .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
203                .ok_or_else(|| SQLError::UnknownTable(child_table.clone()))?
204        };
205        crate::schema::constraints::validate_foreign_key_definition(
206            &child_table,
207            &child_columns,
208            table,
209            candidate_columns,
210            key_constraints,
211            &foreign_key,
212        )?;
213    }
214    Ok(())
215}
216
217pub struct ConstraintAlterOptions {
218    pub enforceability: Option<bool>,
219    pub deferrability: Option<(bool, bool)>,
220    pub no_inherit: Option<bool>,
221}
222pub struct ConstraintAlterEffects {
223    pub recreated_foreign_key: Option<ForeignKey>,
224    pub validate_after_publish: bool,
225}
226#[expect(
227    clippy::too_many_lines,
228    reason = "preserves ordered constraint alteration rules"
229)]
230pub fn apply_constraint_alteration(
231    table: &str,
232    name: &str,
233    columns: &mut [crate::ast::ColumnDef],
234    constraints: &mut crate::ast::TableConstraintSet,
235    options: ConstraintAlterOptions,
236) -> Result<ConstraintAlterEffects, SQLError> {
237    let ConstraintAlterOptions {
238        enforceability,
239        deferrability,
240        no_inherit,
241    } = options;
242    let location = find_constraint(columns, constraints, name).ok_or_else(|| {
243        constraint_error(
244            "42704",
245            format!("constraint \"{name}\" of relation \"{table}\" does not exist"),
246        )
247    })?;
248    let is_foreign_key = matches!(
249        location,
250        ConstraintLocation::ColumnForeignKey(_) | ConstraintLocation::TableForeignKey(_)
251    );
252    let is_not_null = matches!(location, ConstraintLocation::NotNull(_));
253    if enforceability.is_some() && !is_foreign_key {
254        return Err(constraint_error(
255            "42809",
256            format!("cannot alter enforceability of constraint \"{name}\" of relation \"{table}\""),
257        ));
258    }
259    if deferrability.is_some() && !is_foreign_key {
260        return Err(constraint_error(
261            "42809",
262            format!(
263                "constraint \"{name}\" of relation \"{table}\" is not a foreign key constraint"
264            ),
265        ));
266    }
267    if no_inherit.is_some() && !is_not_null {
268        return Err(constraint_error(
269            "42809",
270            format!("constraint \"{name}\" of relation \"{table}\" is not a not-null constraint"),
271        ));
272    }
273    let recreated_foreign_key = if enforceability == Some(true) {
274        match location {
275            ConstraintLocation::ColumnForeignKey(index) => columns[index]
276                .references
277                .as_ref()
278                .filter(|foreign_key| !foreign_key.enforced)
279                .map(|foreign_key| column_foreign_key(&columns[index], foreign_key)),
280            ConstraintLocation::TableForeignKey(index) => constraints
281                .foreign_keys
282                .get(index)
283                .filter(|foreign_key| !foreign_key.enforced)
284                .cloned(),
285            ConstraintLocation::NotNull(_)
286            | ConstraintLocation::ColumnCheck(_)
287            | ConstraintLocation::TableCheck(_)
288            | ConstraintLocation::Key(_) => None,
289        }
290    } else {
291        None
292    };
293    let mut validate_after_publish = false;
294    match location {
295        ConstraintLocation::NotNull(index) => {
296            if let Some(no_inherit) = no_inherit {
297                columns[index].not_null_no_inherit = no_inherit;
298            }
299        }
300        ConstraintLocation::ColumnForeignKey(index) => {
301            let foreign_key = columns[index]
302                .references
303                .as_mut()
304                .ok_or_else(|| SQLError::Internal("column FOREIGN KEY disappeared".into()))?;
305            if let Some(enforced) = enforceability {
306                if !enforced {
307                    foreign_key.enforced = false;
308                    foreign_key.validated = false;
309                } else if !foreign_key.enforced {
310                    foreign_key.enforced = true;
311                    foreign_key.validated = false;
312                    validate_after_publish = true;
313                }
314            }
315            if let Some((deferrable, initially_deferred)) = deferrability {
316                foreign_key.deferrable = deferrable;
317                foreign_key.initially_deferred = initially_deferred;
318            }
319        }
320        ConstraintLocation::TableForeignKey(index) => {
321            let foreign_key = &mut constraints.foreign_keys[index];
322            if let Some(enforced) = enforceability {
323                if !enforced {
324                    foreign_key.enforced = false;
325                    foreign_key.validated = false;
326                } else if !foreign_key.enforced {
327                    foreign_key.enforced = true;
328                    foreign_key.validated = false;
329                    validate_after_publish = true;
330                }
331            }
332            if let Some((deferrable, initially_deferred)) = deferrability {
333                foreign_key.deferrable = deferrable;
334                foreign_key.initially_deferred = initially_deferred;
335            }
336        }
337        ConstraintLocation::ColumnCheck(_)
338        | ConstraintLocation::TableCheck(_)
339        | ConstraintLocation::Key(_) => {}
340    }
341    Ok(ConstraintAlterEffects {
342        recreated_foreign_key,
343        validate_after_publish,
344    })
345}