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