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