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
15#[cfg(test)]
16mod tests;
17
18use crate::schema::foreign_keys::column_foreign_key;
19use crate::{
20    ast::{ColumnType, ForeignKey, TableCheck},
21    SQLError,
22};
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ConstraintLocation {
25    NotNull(usize),
26    ColumnCheck(usize),
27    ColumnForeignKey(usize),
28    TableCheck(usize),
29    TableForeignKey(usize),
30    Key(usize),
31    /// A constraint the foreign key at a location derives on a referenced partition, by its position among that foreign key's derived constraints.
32    ReferencedPartition(ForeignKeyLocation, usize),
33}
34
35/// Where a relation declares a foreign key.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum ForeignKeyLocation {
38    Column(usize),
39    Table(usize),
40}
41
42impl ForeignKeyLocation {
43    pub const fn constraint(self) -> ConstraintLocation {
44        match self {
45            Self::Column(index) => ConstraintLocation::ColumnForeignKey(index),
46            Self::Table(index) => ConstraintLocation::TableForeignKey(index),
47        }
48    }
49
50    /// The name and derived constraints of the foreign key at this location.
51    pub fn derived<'a>(
52        self,
53        columns: &'a [crate::ast::ColumnDef],
54        constraints: &'a crate::ast::TableConstraintSet,
55    ) -> Option<(&'a str, &'a [crate::ast::ReferencedPartitionConstraint])> {
56        match self {
57            Self::Column(index) => columns
58                .get(index)?
59                .references
60                .as_ref()
61                .and_then(|reference| {
62                    Some((
63                        reference.name.as_deref()?,
64                        reference.referenced_partitions.as_slice(),
65                    ))
66                }),
67            Self::Table(index) => constraints.foreign_keys.get(index).and_then(|foreign_key| {
68                Some((
69                    foreign_key.name.as_deref()?,
70                    foreign_key.referenced_partitions.as_slice(),
71                ))
72            }),
73        }
74    }
75
76    /// The derived constraints of the foreign key at this location, for an edit.
77    pub fn derived_mut<'a>(
78        self,
79        columns: &'a mut [crate::ast::ColumnDef],
80        constraints: &'a mut crate::ast::TableConstraintSet,
81    ) -> Option<&'a mut Vec<crate::ast::ReferencedPartitionConstraint>> {
82        match self {
83            Self::Column(index) => columns
84                .get_mut(index)?
85                .references
86                .as_mut()
87                .map(|reference| &mut reference.referenced_partitions),
88            Self::Table(index) => constraints
89                .foreign_keys
90                .get_mut(index)
91                .map(|foreign_key| &mut foreign_key.referenced_partitions),
92        }
93    }
94}
95
96pub fn constraint_error(sqlstate: &str, message: impl Into<String>) -> SQLError {
97    SQLError::Routine {
98        sqlstate: sqlstate.into(),
99        message: message.into(),
100    }
101}
102
103pub fn find_constraint(
104    columns: &[crate::ast::ColumnDef],
105    constraints: &crate::ast::TableConstraintSet,
106    name: &str,
107) -> Option<ConstraintLocation> {
108    names::ConstraintNames::from_definition(columns, constraints)
109        .entries()
110        .find(|constraint| constraint.name == name)
111        .map(|constraint| constraint.location)
112}
113
114pub fn ensure_constraint_name_available(
115    columns: &[crate::ast::ColumnDef],
116    constraints: &crate::ast::TableConstraintSet,
117    name: Option<&str>,
118    table: &str,
119) -> Result<(), SQLError> {
120    if let Some(name) = name.filter(|name| find_constraint(columns, constraints, name).is_some()) {
121        return Err(constraint_error(
122            "42710",
123            format!("constraint \"{name}\" for relation \"{table}\" already exists"),
124        ));
125    }
126    Ok(())
127}
128
129pub fn ensure_not_null_inheritable(
130    table: &str,
131    column: &crate::ast::ColumnDef,
132    sqlstate: &str,
133) -> Result<(), SQLError> {
134    if column.not_null_no_inherit {
135        let relation = uqa_core::RelationIdentity::from_legacy_name(table)
136            .map_err(|error| SQLError::Internal(format!("resolve NOT NULL relation: {error}")))?;
137        let name = column.not_null_name.as_deref().unwrap_or("<unnamed>");
138        return Err(constraint_error(
139            sqlstate,
140            format!(
141            "cannot change NO INHERIT status of NOT NULL constraint \"{name}\" on relation \"{}\"",
142            relation.name,
143        ),
144        ));
145    }
146    Ok(())
147}
148
149pub fn take_column_check(column: &mut crate::ast::ColumnDef) -> Option<TableCheck> {
150    let check = TableCheck {
151        expr: column.check.take()?,
152        catalog_oid: column.check_catalog_oid.take(),
153        name: column.check_name.take(),
154        object_id: column.check_object_id.take(),
155        is_local: column.check_is_local,
156        enforced: column.check_enforced,
157        validated: column.check_validated,
158        no_inherit: column.check_no_inherit,
159        partition_constraint: None,
160    };
161    column.check_is_local = true;
162    column.check_enforced = true;
163    column.check_validated = true;
164    column.check_no_inherit = false;
165    Some(check)
166}
167
168/// Give a column the CHECK [`take_column_check`] took from it.
169pub fn restore_column_check(column: &mut crate::ast::ColumnDef, check: TableCheck) {
170    column.check = Some(check.expr);
171    column.check_catalog_oid = check.catalog_oid;
172    column.check_name = check.name;
173    column.check_object_id = check.object_id;
174    column.check_is_local = check.is_local;
175    column.check_enforced = check.enforced;
176    column.check_validated = check.validated;
177    column.check_no_inherit = check.no_inherit;
178}
179
180pub fn foreign_key_object_id(
181    columns: &[crate::ast::ColumnDef],
182    constraints: &crate::ast::TableConstraintSet,
183    location: ConstraintLocation,
184) -> Option<[u8; 16]> {
185    match location {
186        ConstraintLocation::ColumnForeignKey(index) => columns[index]
187            .references
188            .as_ref()
189            .and_then(|reference| reference.object_id),
190        ConstraintLocation::TableForeignKey(index) => constraints.foreign_keys[index].object_id,
191        _ => None,
192    }
193}
194
195pub trait ConstraintTypeReferrers {
196    fn try_referrers_to(
197        &self,
198        table: &str,
199    ) -> Result<Vec<(String, ForeignKey)>, crate::assignment::columns::ColumnCatalogError>;
200}
201pub struct ConstraintTypeContext<'a> {
202    pub foreign_keys: crate::schema::foreign_keys::ForeignKeyDefinitionContext<'a>,
203    pub referrers: &'a dyn ConstraintTypeReferrers,
204}
205fn ddl_storage_error(
206    action: &str,
207    error: crate::assignment::columns::ColumnCatalogError,
208) -> SQLError {
209    crate::catalog::errors::storage_error(action, error.as_ref())
210}
211pub fn validate_altered_constraint_column_types(
212    context: &ConstraintTypeContext<'_>,
213    table: &str,
214    candidate_columns: &[crate::ast::ColumnDef],
215    key_constraints: &[crate::ast::TableKeyConstraint],
216    foreign_keys: &[ForeignKey],
217) -> Result<(), SQLError> {
218    for constraint in key_constraints
219        .iter()
220        .filter(|constraint| constraint.without_overlaps)
221    {
222        let Some(period_column) = constraint.columns.last() else {
223            return Err(SQLError::Internal(
224                "WITHOUT OVERLAPS constraint has no period column".into(),
225            ));
226        };
227        let period_type = candidate_columns
228            .iter()
229            .find(|column| column.name == *period_column)
230            .map(|column| &column.ty)
231            .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{period_column}")))?;
232        if !matches!(
233            period_type,
234            ColumnType::Range(_) | ColumnType::Multirange(_)
235        ) {
236            return Err(SQLError::Routine {
237                sqlstate: "42804".into(),
238                message: format!(
239                    "column \"{period_column}\" in WITHOUT OVERLAPS is not a range or multirange type"
240                ),
241            });
242        }
243    }
244
245    for foreign_key in foreign_keys.iter().filter(|foreign_key| foreign_key.period) {
246        let (parent_name, parent_columns, parent_keys) =
247            crate::schema::foreign_keys::resolve_foreign_key_parent(
248                &context.foreign_keys,
249                &foreign_key.ref_table,
250            )?;
251        let parent_columns = if parent_name == table {
252            candidate_columns
253        } else {
254            parent_columns.as_slice()
255        };
256        crate::schema::constraints::validate_foreign_key_definition(
257            table,
258            candidate_columns,
259            &parent_name,
260            parent_columns,
261            &parent_keys,
262            foreign_key,
263        )?;
264    }
265
266    for (child_table, foreign_key) in context
267        .referrers
268        .try_referrers_to(table)
269        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
270        .into_iter()
271        .filter(|(_, foreign_key)| foreign_key.period)
272    {
273        let child_columns = if child_table == table {
274            candidate_columns.to_vec()
275        } else {
276            context
277                .foreign_keys
278                .columns
279                .try_describe_table(&child_table)
280                .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
281                .ok_or_else(|| SQLError::UnknownTable(child_table.clone()))?
282        };
283        crate::schema::constraints::validate_foreign_key_definition(
284            &child_table,
285            &child_columns,
286            table,
287            candidate_columns,
288            key_constraints,
289            &foreign_key,
290        )?;
291    }
292    Ok(())
293}
294
295pub struct ConstraintAlterOptions {
296    pub enforceability: Option<bool>,
297    pub deferrability: Option<(bool, bool)>,
298    pub no_inherit: Option<bool>,
299    /// The constraint without a parent that the altered constraint derives from, when it is a partition's copy of a foreign key.
300    pub ancestor: Option<ConstraintAncestor>,
301}
302
303/// A constraint without a parent, which the constraints derived from it name in diagnostics.
304#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct ConstraintAncestor {
306    pub name: String,
307    pub table: String,
308}
309
310/// `ALTER CONSTRAINT` of a constraint derived from another, which `PostgreSQL` refuses so that the derived constraints keep following the one they derive from.
311pub fn derived_constraint_alteration(
312    table: &str,
313    name: &str,
314    ancestor: &ConstraintAncestor,
315) -> SQLError {
316    let local = |table: &str| match uqa_core::RelationIdentity::from_legacy_name(table) {
317        Ok(relation) => Ok(relation.name),
318        Err(error) => Err(SQLError::Internal(error)),
319    };
320    let (relation, ancestor_relation) = match (local(table), local(&ancestor.table)) {
321        (Ok(relation), Ok(ancestor_relation)) => (relation, ancestor_relation),
322        (Err(error), _) | (_, Err(error)) => return error,
323    };
324    SQLError::Diagnostic {
325        sqlstate: "55000".into(),
326        message: format!("cannot alter constraint \"{name}\" on relation \"{relation}\""),
327        detail: Some(format!(
328            "Constraint \"{name}\" is derived from constraint \"{}\" of relation \"{ancestor_relation}\".",
329            ancestor.name
330        )),
331        hint: Some("You may alter the constraint it derives from instead.".into()),
332    }
333}
334pub struct ConstraintAlterEffects {
335    pub recreated_foreign_key: Option<ForeignKey>,
336    pub validate_after_publish: bool,
337    /// A real change to the selected constraint; unrelated constraints and repeated modes retain their catalog tuples.
338    pub changed: bool,
339}
340/// The kind of constraint each requested change applies to: enforceability and deferrability to foreign keys, inheritability to `NOT NULL` constraints.
341fn ensure_alteration_applies(
342    location: ConstraintLocation,
343    relation: &str,
344    name: &str,
345    enforceability: bool,
346    deferrability: bool,
347    no_inherit: bool,
348) -> Result<(), SQLError> {
349    let is_foreign_key = matches!(
350        location,
351        ConstraintLocation::ColumnForeignKey(_)
352            | ConstraintLocation::TableForeignKey(_)
353            | ConstraintLocation::ReferencedPartition(..)
354    );
355    if enforceability && !is_foreign_key {
356        return Err(constraint_error(
357            "42809",
358            format!(
359                "cannot alter enforceability of constraint \"{name}\" of relation \"{relation}\""
360            ),
361        ));
362    }
363    if deferrability && !is_foreign_key {
364        return Err(constraint_error(
365            "42809",
366            format!(
367                "constraint \"{name}\" of relation \"{relation}\" is not a foreign key constraint"
368            ),
369        ));
370    }
371    if no_inherit && !matches!(location, ConstraintLocation::NotNull(_)) {
372        return Err(constraint_error(
373            "42809",
374            format!(
375                "constraint \"{name}\" of relation \"{relation}\" is not a not-null constraint"
376            ),
377        ));
378    }
379    Ok(())
380}
381
382pub fn apply_constraint_alteration(
383    table: &str,
384    name: &str,
385    columns: &mut [crate::ast::ColumnDef],
386    constraints: &mut crate::ast::TableConstraintSet,
387    options: ConstraintAlterOptions,
388) -> Result<ConstraintAlterEffects, SQLError> {
389    let ConstraintAlterOptions {
390        enforceability,
391        deferrability,
392        no_inherit,
393        ancestor,
394    } = options;
395    // Diagnostics name the relation without its schema, as `RelationGetRelationName` does.
396    let relation = uqa_core::RelationIdentity::from_legacy_name(table)
397        .map_err(SQLError::Internal)?
398        .name;
399    let location = find_constraint(columns, constraints, name).ok_or_else(|| {
400        constraint_error(
401            "42704",
402            format!("constraint \"{name}\" of relation \"{relation}\" does not exist"),
403        )
404    })?;
405    ensure_alteration_applies(
406        location,
407        &relation,
408        name,
409        enforceability.is_some(),
410        deferrability.is_some(),
411        no_inherit.is_some(),
412    )?;
413    // A constraint derived on a referenced partition derives from the foreign key that holds it.
414    let ancestor = match location {
415        ConstraintLocation::ReferencedPartition(foreign_key, _) => foreign_key
416            .derived(columns, constraints)
417            .map(|(foreign_key, _)| ConstraintAncestor {
418                name: foreign_key.to_string(),
419                table: table.to_string(),
420            }),
421        _ => ancestor,
422    };
423    if let Some(ancestor) = &ancestor {
424        return Err(derived_constraint_alteration(table, name, ancestor));
425    }
426    let recreated_foreign_key = if enforceability == Some(true) {
427        match location {
428            ConstraintLocation::ColumnForeignKey(index) => columns[index]
429                .references
430                .as_ref()
431                .filter(|foreign_key| !foreign_key.enforced)
432                .map(|foreign_key| column_foreign_key(&columns[index], foreign_key)),
433            ConstraintLocation::TableForeignKey(index) => constraints
434                .foreign_keys
435                .get(index)
436                .filter(|foreign_key| !foreign_key.enforced)
437                .cloned(),
438            ConstraintLocation::NotNull(_)
439            | ConstraintLocation::ColumnCheck(_)
440            | ConstraintLocation::TableCheck(_)
441            | ConstraintLocation::Key(_)
442            | ConstraintLocation::ReferencedPartition(..) => None,
443        }
444    } else {
445        None
446    };
447    let mut validate_after_publish = false;
448    let mut changed = false;
449    match location {
450        ConstraintLocation::NotNull(index) => {
451            if let Some(no_inherit) = no_inherit {
452                changed = columns[index].not_null_no_inherit != no_inherit;
453                columns[index].not_null_no_inherit = no_inherit;
454            }
455        }
456        ConstraintLocation::ColumnForeignKey(index) => {
457            let effects =
458                crate::schema::inheritance::foreign_keys::DeclaredForeignKey::Column(index)
459                    .alter_with_effects(columns, constraints, enforceability, deferrability);
460            changed = effects.changed;
461            validate_after_publish = effects.became_enforced;
462        }
463        ConstraintLocation::TableForeignKey(index) => {
464            let effects =
465                crate::schema::inheritance::foreign_keys::DeclaredForeignKey::Table(index)
466                    .alter_with_effects(columns, constraints, enforceability, deferrability);
467            changed = effects.changed;
468            validate_after_publish = effects.became_enforced;
469        }
470        ConstraintLocation::ColumnCheck(_)
471        | ConstraintLocation::TableCheck(_)
472        | ConstraintLocation::Key(_)
473        | ConstraintLocation::ReferencedPartition(..) => {}
474    }
475    Ok(ConstraintAlterEffects {
476        recreated_foreign_key,
477        validate_after_publish,
478        changed,
479    })
480}