Skip to main content

uqa_sql/schema/inheritance/
foreign_keys.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! The foreign keys a partition holds for the foreign keys of its partitioned parent: an equivalent foreign key of its own that it attaches, or a copy, as `PostgreSQL`'s `tryAttachPartitionForeignKey` and `addFkConstraint` decide.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use crate::ast::{ColumnDef, ForeignKey, TableConstraintSet};
12use crate::SQLError;
13
14#[cfg(test)]
15mod tests;
16
17fn local_name(table: &str) -> Result<String, SQLError> {
18    uqa_core::RelationIdentity::from_legacy_name(table)
19        .map(|relation| relation.name)
20        .map_err(SQLError::Internal)
21}
22
23/// The foreign key of `partition` that `parent_key` attaches among `candidates`, the partition's foreign keys that copy none of its parent's, in name order: the first that references the same key through the same columns with the same deferrability, actions and match type. A candidate that differs from `parent_key` in enforceability alone is an error, as `PostgreSQL` refuses to attach it or to add a second constraint beside it.
24pub fn attachable_foreign_key<'a>(
25    partition: &str,
26    parent_key: &ForeignKey,
27    candidates: &'a [ForeignKey],
28) -> Result<Option<&'a ForeignKey>, SQLError> {
29    let mut ordered = candidates.iter().collect::<Vec<_>>();
30    ordered.sort_by(|left, right| left.name.cmp(&right.name));
31    for candidate in ordered {
32        if candidate.ref_table != parent_key.ref_table
33            || candidate.local_columns != parent_key.local_columns
34            || candidate.ref_columns != parent_key.ref_columns
35            || candidate.period != parent_key.period
36        {
37            continue;
38        }
39        if candidate.enforced != parent_key.enforced {
40            return Err(SQLError::Diagnostic {
41                sqlstate: "42P16".into(),
42                message: format!(
43                    "constraint \"{}\" enforceability conflicts with constraint \"{}\" on relation \"{}\"",
44                    parent_key.name.as_deref().unwrap_or_default(),
45                    candidate.name.as_deref().unwrap_or_default(),
46                    local_name(partition)?
47                ),
48                detail: None,
49                hint: None,
50            });
51        }
52        if candidate.deferrable == parent_key.deferrable
53            && candidate.initially_deferred == parent_key.initially_deferred
54            && candidate.on_update == parent_key.on_update
55            && candidate.on_delete == parent_key.on_delete
56            && candidate.match_type == parent_key.match_type
57        {
58            return Ok(Some(candidate));
59        }
60    }
61    Ok(None)
62}
63
64/// A partition's copy of `parent_key`, which keeps the parent's name unless one of the partition's constraints in `used` holds it, when it takes the parent's name with the first numeric suffix that no constraint of the partition's schema, in `schema`, holds, as `PostgreSQL`'s `addFkConstraint` names it.
65pub fn partition_foreign_key_copy(
66    parent_key: &ForeignKey,
67    used: &BTreeSet<String>,
68    schema: &mut BTreeSet<String>,
69) -> Result<ForeignKey, SQLError> {
70    let mut copy = parent_key.clone();
71    copy.catalog_identity = None;
72    // Only the foreign key without a parent derives constraints on referenced partitions.
73    copy.referenced_partitions.clear();
74    if let Some(name) = copy.name.as_ref().filter(|name| used.contains(*name)) {
75        copy.name = Some(
76            crate::schema::constraint_metadata::choose_suffixed_constraint_name(name, schema)
77                .map_err(|error| SQLError::Internal(error.to_string()))?,
78        );
79    }
80    Ok(copy)
81}
82
83/// The object identities of the foreign keys a declaration holds, which its partitions' copies share.
84pub fn declared_foreign_key_families(
85    columns: &[ColumnDef],
86    constraints: &TableConstraintSet,
87) -> BTreeSet<[u8; 16]> {
88    columns
89        .iter()
90        .filter_map(|column| column.references.as_ref()?.object_id)
91        .chain(
92            constraints
93                .foreign_keys
94                .iter()
95                .filter_map(|foreign_key| foreign_key.object_id),
96        )
97        .collect()
98}
99
100/// A foreign key of a declaration, by the catalog row it publishes.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum DeclaredForeignKey {
103    Column(usize),
104    Table(usize),
105}
106
107/// Whether ALTER changes this constraint's catalog tuple and requires validation of newly enforced rows.
108#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
109pub struct ForeignKeyAlterEffects {
110    pub changed: bool,
111    pub became_enforced: bool,
112}
113
114impl DeclaredForeignKey {
115    /// The foreign key of the declaration whose catalog row is `identity`.
116    pub fn by_catalog_identity(
117        columns: &[ColumnDef],
118        constraints: &TableConstraintSet,
119        identity: crate::ast::ConstraintCatalogIdentity,
120    ) -> Option<Self> {
121        columns
122            .iter()
123            .position(|column| {
124                column
125                    .references
126                    .as_ref()
127                    .is_some_and(|reference| reference.catalog_identity == Some(identity))
128            })
129            .map(Self::Column)
130            .or_else(|| {
131                constraints
132                    .foreign_keys
133                    .iter()
134                    .position(|foreign_key| foreign_key.catalog_identity == Some(identity))
135                    .map(Self::Table)
136            })
137    }
138
139    /// The foreign key of the declaration that belongs to the family `object_id`.
140    pub fn by_family(
141        columns: &[ColumnDef],
142        constraints: &TableConstraintSet,
143        object_id: [u8; 16],
144    ) -> Option<Self> {
145        columns
146            .iter()
147            .position(|column| {
148                column
149                    .references
150                    .as_ref()
151                    .is_some_and(|reference| reference.object_id == Some(object_id))
152            })
153            .map(Self::Column)
154            .or_else(|| {
155                constraints
156                    .foreign_keys
157                    .iter()
158                    .position(|foreign_key| foreign_key.object_id == Some(object_id))
159                    .map(Self::Table)
160            })
161    }
162
163    /// The foreign key with its stored targets, as a table-level declaration.
164    pub fn foreign_key(
165        self,
166        columns: &[ColumnDef],
167        constraints: &TableConstraintSet,
168    ) -> Option<ForeignKey> {
169        match self {
170            Self::Column(index) => {
171                let column = columns.get(index)?;
172                column.references.as_ref().map(|reference| {
173                    crate::schema::foreign_keys::column_foreign_key(column, reference)
174                })
175            }
176            Self::Table(index) => constraints.foreign_keys.get(index).cloned(),
177        }
178    }
179
180    pub fn validated(self, columns: &[ColumnDef], constraints: &TableConstraintSet) -> bool {
181        match self {
182            Self::Column(index) => columns[index]
183                .references
184                .as_ref()
185                .is_some_and(|reference| reference.validated),
186            Self::Table(index) => constraints.foreign_keys[index].validated,
187        }
188    }
189
190    pub fn set_validated(
191        self,
192        columns: &mut [ColumnDef],
193        constraints: &mut TableConstraintSet,
194        validated: bool,
195    ) {
196        match self {
197            Self::Column(index) => {
198                if let Some(reference) = columns[index].references.as_mut() {
199                    reference.validated = validated;
200                }
201            }
202            Self::Table(index) => constraints.foreign_keys[index].validated = validated,
203        }
204    }
205
206    /// Apply an `ALTER CONSTRAINT` enforceability and deferrability change. A foreign key that stops being enforced is no longer valid, and one that becomes enforced is not valid until its rows are validated; reports whether it became enforced.
207    pub fn alter(
208        self,
209        columns: &mut [ColumnDef],
210        constraints: &mut TableConstraintSet,
211        enforceability: Option<bool>,
212        deferrability: Option<(bool, bool)>,
213    ) -> bool {
214        self.alter_with_effects(columns, constraints, enforceability, deferrability)
215            .became_enforced
216    }
217
218    /// Retain the actual catalog-write decision separately from validation. Repeating an already selected enforcement or deferral mode does not replace the constraint tuple.
219    pub fn alter_with_effects(
220        self,
221        columns: &mut [ColumnDef],
222        constraints: &mut TableConstraintSet,
223        enforceability: Option<bool>,
224        deferrability: Option<(bool, bool)>,
225    ) -> ForeignKeyAlterEffects {
226        let (enforced, validated, deferrable, initially_deferred) = match self {
227            Self::Column(index) => {
228                let Some(reference) = columns[index].references.as_mut() else {
229                    return ForeignKeyAlterEffects::default();
230                };
231                (
232                    &mut reference.enforced,
233                    &mut reference.validated,
234                    &mut reference.deferrable,
235                    &mut reference.initially_deferred,
236                )
237            }
238            Self::Table(index) => {
239                let foreign_key = &mut constraints.foreign_keys[index];
240                (
241                    &mut foreign_key.enforced,
242                    &mut foreign_key.validated,
243                    &mut foreign_key.deferrable,
244                    &mut foreign_key.initially_deferred,
245                )
246            }
247        };
248        let previous = (*enforced, *validated, *deferrable, *initially_deferred);
249        let mut became_enforced = false;
250        match enforceability {
251            Some(false) => {
252                *enforced = false;
253                *validated = false;
254            }
255            Some(true) if !*enforced => {
256                *enforced = true;
257                *validated = false;
258                became_enforced = true;
259            }
260            Some(true) | None => {}
261        }
262        if let Some((deferrable_value, initially_deferred_value)) = deferrability {
263            *deferrable = deferrable_value;
264            *initially_deferred = initially_deferred_value;
265        }
266        ForeignKeyAlterEffects {
267            changed: previous != (*enforced, *validated, *deferrable, *initially_deferred),
268            became_enforced,
269        }
270    }
271
272    pub fn set_family(
273        self,
274        columns: &mut [ColumnDef],
275        constraints: &mut TableConstraintSet,
276        object_id: [u8; 16],
277    ) {
278        match self {
279            Self::Column(index) => {
280                if let Some(reference) = columns[index].references.as_mut() {
281                    reference.object_id = Some(object_id);
282                }
283            }
284            Self::Table(index) => constraints.foreign_keys[index].object_id = Some(object_id),
285        }
286    }
287}
288
289/// Move the foreign keys of a declaration whose families joined others into those families: the copies of a foreign key that a partition attached to its parent's follow it. Reports whether any moved.
290pub fn rejoin_foreign_key_families(
291    columns: &mut [ColumnDef],
292    constraints: &mut TableConstraintSet,
293    joined: &BTreeMap<[u8; 16], [u8; 16]>,
294) -> bool {
295    let mut changed = false;
296    for object_id in columns
297        .iter_mut()
298        .filter_map(|column| column.references.as_mut())
299        .map(|reference| &mut reference.object_id)
300        .chain(
301            constraints
302                .foreign_keys
303                .iter_mut()
304                .map(|foreign_key| &mut foreign_key.object_id),
305        )
306    {
307        if let Some(target) = object_id.and_then(|current| joined.get(&current)) {
308            *object_id = Some(*target);
309            changed = true;
310        }
311    }
312    changed
313}