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
165pub fn foreign_key_object_id(
166    columns: &[crate::ast::ColumnDef],
167    constraints: &crate::ast::TableConstraintSet,
168    location: ConstraintLocation,
169) -> Option<[u8; 16]> {
170    match location {
171        ConstraintLocation::ColumnForeignKey(index) => columns[index]
172            .references
173            .as_ref()
174            .and_then(|reference| reference.object_id),
175        ConstraintLocation::TableForeignKey(index) => constraints.foreign_keys[index].object_id,
176        _ => None,
177    }
178}
179
180pub trait ConstraintTypeReferrers {
181    fn try_referrers_to(
182        &self,
183        table: &str,
184    ) -> Result<Vec<(String, ForeignKey)>, crate::assignment::columns::ColumnCatalogError>;
185}
186pub struct ConstraintTypeContext<'a> {
187    pub foreign_keys: crate::schema::foreign_keys::ForeignKeyDefinitionContext<'a>,
188    pub referrers: &'a dyn ConstraintTypeReferrers,
189}
190fn ddl_storage_error(
191    action: &str,
192    error: crate::assignment::columns::ColumnCatalogError,
193) -> SQLError {
194    crate::catalog::errors::storage_error(action, error.as_ref())
195}
196pub fn validate_altered_constraint_column_types(
197    context: &ConstraintTypeContext<'_>,
198    table: &str,
199    candidate_columns: &[crate::ast::ColumnDef],
200    key_constraints: &[crate::ast::TableKeyConstraint],
201    foreign_keys: &[ForeignKey],
202) -> Result<(), SQLError> {
203    for constraint in key_constraints
204        .iter()
205        .filter(|constraint| constraint.without_overlaps)
206    {
207        let Some(period_column) = constraint.columns.last() else {
208            return Err(SQLError::Internal(
209                "WITHOUT OVERLAPS constraint has no period column".into(),
210            ));
211        };
212        let period_type = candidate_columns
213            .iter()
214            .find(|column| column.name == *period_column)
215            .map(|column| &column.ty)
216            .ok_or_else(|| SQLError::UnknownColumn(format!("{table}.{period_column}")))?;
217        if !matches!(
218            period_type,
219            ColumnType::Range(_) | ColumnType::Multirange(_)
220        ) {
221            return Err(SQLError::Routine {
222                sqlstate: "42804".into(),
223                message: format!(
224                    "column \"{period_column}\" in WITHOUT OVERLAPS is not a range or multirange type"
225                ),
226            });
227        }
228    }
229
230    for foreign_key in foreign_keys.iter().filter(|foreign_key| foreign_key.period) {
231        let (parent_name, parent_columns, parent_keys) =
232            crate::schema::foreign_keys::resolve_foreign_key_parent(
233                &context.foreign_keys,
234                &foreign_key.ref_table,
235            )?;
236        let parent_columns = if parent_name == table {
237            candidate_columns
238        } else {
239            parent_columns.as_slice()
240        };
241        crate::schema::constraints::validate_foreign_key_definition(
242            table,
243            candidate_columns,
244            &parent_name,
245            parent_columns,
246            &parent_keys,
247            foreign_key,
248        )?;
249    }
250
251    for (child_table, foreign_key) in context
252        .referrers
253        .try_referrers_to(table)
254        .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
255        .into_iter()
256        .filter(|(_, foreign_key)| foreign_key.period)
257    {
258        let child_columns = if child_table == table {
259            candidate_columns.to_vec()
260        } else {
261            context
262                .foreign_keys
263                .columns
264                .try_describe_table(&child_table)
265                .map_err(|error| ddl_storage_error("ALTER COLUMN TYPE", error))?
266                .ok_or_else(|| SQLError::UnknownTable(child_table.clone()))?
267        };
268        crate::schema::constraints::validate_foreign_key_definition(
269            &child_table,
270            &child_columns,
271            table,
272            candidate_columns,
273            key_constraints,
274            &foreign_key,
275        )?;
276    }
277    Ok(())
278}
279
280pub struct ConstraintAlterOptions {
281    pub enforceability: Option<bool>,
282    pub deferrability: Option<(bool, bool)>,
283    pub no_inherit: Option<bool>,
284    /// The constraint without a parent that the altered constraint derives from, when it is a partition's copy of a foreign key.
285    pub ancestor: Option<ConstraintAncestor>,
286}
287
288/// A constraint without a parent, which the constraints derived from it name in diagnostics.
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct ConstraintAncestor {
291    pub name: String,
292    pub table: String,
293}
294
295/// `ALTER CONSTRAINT` of a constraint derived from another, which `PostgreSQL` refuses so that the derived constraints keep following the one they derive from.
296pub fn derived_constraint_alteration(
297    table: &str,
298    name: &str,
299    ancestor: &ConstraintAncestor,
300) -> SQLError {
301    let local = |table: &str| match uqa_core::RelationIdentity::from_legacy_name(table) {
302        Ok(relation) => Ok(relation.name),
303        Err(error) => Err(SQLError::Internal(error)),
304    };
305    let (relation, ancestor_relation) = match (local(table), local(&ancestor.table)) {
306        (Ok(relation), Ok(ancestor_relation)) => (relation, ancestor_relation),
307        (Err(error), _) | (_, Err(error)) => return error,
308    };
309    SQLError::Diagnostic {
310        sqlstate: "55000".into(),
311        message: format!("cannot alter constraint \"{name}\" on relation \"{relation}\""),
312        detail: Some(format!(
313            "Constraint \"{name}\" is derived from constraint \"{}\" of relation \"{ancestor_relation}\".",
314            ancestor.name
315        )),
316        hint: Some("You may alter the constraint it derives from instead.".into()),
317    }
318}
319pub struct ConstraintAlterEffects {
320    pub recreated_foreign_key: Option<ForeignKey>,
321    pub validate_after_publish: bool,
322}
323/// The kind of constraint each requested change applies to: enforceability and deferrability to foreign keys, inheritability to `NOT NULL` constraints.
324fn ensure_alteration_applies(
325    location: ConstraintLocation,
326    relation: &str,
327    name: &str,
328    enforceability: bool,
329    deferrability: bool,
330    no_inherit: bool,
331) -> Result<(), SQLError> {
332    let is_foreign_key = matches!(
333        location,
334        ConstraintLocation::ColumnForeignKey(_)
335            | ConstraintLocation::TableForeignKey(_)
336            | ConstraintLocation::ReferencedPartition(..)
337    );
338    if enforceability && !is_foreign_key {
339        return Err(constraint_error(
340            "42809",
341            format!(
342                "cannot alter enforceability of constraint \"{name}\" of relation \"{relation}\""
343            ),
344        ));
345    }
346    if deferrability && !is_foreign_key {
347        return Err(constraint_error(
348            "42809",
349            format!(
350                "constraint \"{name}\" of relation \"{relation}\" is not a foreign key constraint"
351            ),
352        ));
353    }
354    if no_inherit && !matches!(location, ConstraintLocation::NotNull(_)) {
355        return Err(constraint_error(
356            "42809",
357            format!(
358                "constraint \"{name}\" of relation \"{relation}\" is not a not-null constraint"
359            ),
360        ));
361    }
362    Ok(())
363}
364
365pub fn apply_constraint_alteration(
366    table: &str,
367    name: &str,
368    columns: &mut [crate::ast::ColumnDef],
369    constraints: &mut crate::ast::TableConstraintSet,
370    options: ConstraintAlterOptions,
371) -> Result<ConstraintAlterEffects, SQLError> {
372    let ConstraintAlterOptions {
373        enforceability,
374        deferrability,
375        no_inherit,
376        ancestor,
377    } = options;
378    // Diagnostics name the relation without its schema, as `RelationGetRelationName` does.
379    let relation = uqa_core::RelationIdentity::from_legacy_name(table)
380        .map_err(SQLError::Internal)?
381        .name;
382    let location = find_constraint(columns, constraints, name).ok_or_else(|| {
383        constraint_error(
384            "42704",
385            format!("constraint \"{name}\" of relation \"{relation}\" does not exist"),
386        )
387    })?;
388    ensure_alteration_applies(
389        location,
390        &relation,
391        name,
392        enforceability.is_some(),
393        deferrability.is_some(),
394        no_inherit.is_some(),
395    )?;
396    // A constraint derived on a referenced partition derives from the foreign key that holds it.
397    let ancestor = match location {
398        ConstraintLocation::ReferencedPartition(foreign_key, _) => foreign_key
399            .derived(columns, constraints)
400            .map(|(foreign_key, _)| ConstraintAncestor {
401                name: foreign_key.to_string(),
402                table: table.to_string(),
403            }),
404        _ => ancestor,
405    };
406    if let Some(ancestor) = &ancestor {
407        return Err(derived_constraint_alteration(table, name, ancestor));
408    }
409    let recreated_foreign_key = if enforceability == Some(true) {
410        match location {
411            ConstraintLocation::ColumnForeignKey(index) => columns[index]
412                .references
413                .as_ref()
414                .filter(|foreign_key| !foreign_key.enforced)
415                .map(|foreign_key| column_foreign_key(&columns[index], foreign_key)),
416            ConstraintLocation::TableForeignKey(index) => constraints
417                .foreign_keys
418                .get(index)
419                .filter(|foreign_key| !foreign_key.enforced)
420                .cloned(),
421            ConstraintLocation::NotNull(_)
422            | ConstraintLocation::ColumnCheck(_)
423            | ConstraintLocation::TableCheck(_)
424            | ConstraintLocation::Key(_)
425            | ConstraintLocation::ReferencedPartition(..) => None,
426        }
427    } else {
428        None
429    };
430    let mut validate_after_publish = false;
431    match location {
432        ConstraintLocation::NotNull(index) => {
433            if let Some(no_inherit) = no_inherit {
434                columns[index].not_null_no_inherit = no_inherit;
435            }
436        }
437        ConstraintLocation::ColumnForeignKey(index) => {
438            validate_after_publish =
439                crate::schema::inheritance::foreign_keys::DeclaredForeignKey::Column(index).alter(
440                    columns,
441                    constraints,
442                    enforceability,
443                    deferrability,
444                );
445        }
446        ConstraintLocation::TableForeignKey(index) => {
447            validate_after_publish =
448                crate::schema::inheritance::foreign_keys::DeclaredForeignKey::Table(index).alter(
449                    columns,
450                    constraints,
451                    enforceability,
452                    deferrability,
453                );
454        }
455        ConstraintLocation::ColumnCheck(_)
456        | ConstraintLocation::TableCheck(_)
457        | ConstraintLocation::Key(_)
458        | ConstraintLocation::ReferencedPartition(..) => {}
459    }
460    Ok(ConstraintAlterEffects {
461        recreated_foreign_key,
462        validate_after_publish,
463    })
464}