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
107impl DeclaredForeignKey {
108    /// The foreign key of the declaration whose catalog row is `identity`.
109    pub fn by_catalog_identity(
110        columns: &[ColumnDef],
111        constraints: &TableConstraintSet,
112        identity: crate::ast::ConstraintCatalogIdentity,
113    ) -> Option<Self> {
114        columns
115            .iter()
116            .position(|column| {
117                column
118                    .references
119                    .as_ref()
120                    .is_some_and(|reference| reference.catalog_identity == Some(identity))
121            })
122            .map(Self::Column)
123            .or_else(|| {
124                constraints
125                    .foreign_keys
126                    .iter()
127                    .position(|foreign_key| foreign_key.catalog_identity == Some(identity))
128                    .map(Self::Table)
129            })
130    }
131
132    /// The foreign key of the declaration that belongs to the family `object_id`.
133    pub fn by_family(
134        columns: &[ColumnDef],
135        constraints: &TableConstraintSet,
136        object_id: [u8; 16],
137    ) -> Option<Self> {
138        columns
139            .iter()
140            .position(|column| {
141                column
142                    .references
143                    .as_ref()
144                    .is_some_and(|reference| reference.object_id == Some(object_id))
145            })
146            .map(Self::Column)
147            .or_else(|| {
148                constraints
149                    .foreign_keys
150                    .iter()
151                    .position(|foreign_key| foreign_key.object_id == Some(object_id))
152                    .map(Self::Table)
153            })
154    }
155
156    /// The foreign key with its stored targets, as a table-level declaration.
157    pub fn foreign_key(
158        self,
159        columns: &[ColumnDef],
160        constraints: &TableConstraintSet,
161    ) -> Option<ForeignKey> {
162        match self {
163            Self::Column(index) => {
164                let column = columns.get(index)?;
165                column.references.as_ref().map(|reference| {
166                    crate::schema::foreign_keys::column_foreign_key(column, reference)
167                })
168            }
169            Self::Table(index) => constraints.foreign_keys.get(index).cloned(),
170        }
171    }
172
173    pub fn validated(self, columns: &[ColumnDef], constraints: &TableConstraintSet) -> bool {
174        match self {
175            Self::Column(index) => columns[index]
176                .references
177                .as_ref()
178                .is_some_and(|reference| reference.validated),
179            Self::Table(index) => constraints.foreign_keys[index].validated,
180        }
181    }
182
183    pub fn set_validated(
184        self,
185        columns: &mut [ColumnDef],
186        constraints: &mut TableConstraintSet,
187        validated: bool,
188    ) {
189        match self {
190            Self::Column(index) => {
191                if let Some(reference) = columns[index].references.as_mut() {
192                    reference.validated = validated;
193                }
194            }
195            Self::Table(index) => constraints.foreign_keys[index].validated = validated,
196        }
197    }
198
199    /// 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.
200    pub fn alter(
201        self,
202        columns: &mut [ColumnDef],
203        constraints: &mut TableConstraintSet,
204        enforceability: Option<bool>,
205        deferrability: Option<(bool, bool)>,
206    ) -> bool {
207        let (enforced, validated, deferrable, initially_deferred) = match self {
208            Self::Column(index) => {
209                let Some(reference) = columns[index].references.as_mut() else {
210                    return false;
211                };
212                (
213                    &mut reference.enforced,
214                    &mut reference.validated,
215                    &mut reference.deferrable,
216                    &mut reference.initially_deferred,
217                )
218            }
219            Self::Table(index) => {
220                let foreign_key = &mut constraints.foreign_keys[index];
221                (
222                    &mut foreign_key.enforced,
223                    &mut foreign_key.validated,
224                    &mut foreign_key.deferrable,
225                    &mut foreign_key.initially_deferred,
226                )
227            }
228        };
229        let mut became_enforced = false;
230        match enforceability {
231            Some(false) => {
232                *enforced = false;
233                *validated = false;
234            }
235            Some(true) if !*enforced => {
236                *enforced = true;
237                *validated = false;
238                became_enforced = true;
239            }
240            Some(true) | None => {}
241        }
242        if let Some((deferrable_value, initially_deferred_value)) = deferrability {
243            *deferrable = deferrable_value;
244            *initially_deferred = initially_deferred_value;
245        }
246        became_enforced
247    }
248
249    pub fn set_family(
250        self,
251        columns: &mut [ColumnDef],
252        constraints: &mut TableConstraintSet,
253        object_id: [u8; 16],
254    ) {
255        match self {
256            Self::Column(index) => {
257                if let Some(reference) = columns[index].references.as_mut() {
258                    reference.object_id = Some(object_id);
259                }
260            }
261            Self::Table(index) => constraints.foreign_keys[index].object_id = Some(object_id),
262        }
263    }
264}
265
266/// 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.
267pub fn rejoin_foreign_key_families(
268    columns: &mut [ColumnDef],
269    constraints: &mut TableConstraintSet,
270    joined: &BTreeMap<[u8; 16], [u8; 16]>,
271) -> bool {
272    let mut changed = false;
273    for object_id in columns
274        .iter_mut()
275        .filter_map(|column| column.references.as_mut())
276        .map(|reference| &mut reference.object_id)
277        .chain(
278            constraints
279                .foreign_keys
280                .iter_mut()
281                .map(|foreign_key| &mut foreign_key.object_id),
282        )
283    {
284        if let Some(target) = object_id.and_then(|current| joined.get(&current)) {
285            *object_id = Some(*target);
286            changed = true;
287        }
288    }
289    changed
290}