Skip to main content

uqa_sql/schema/constraint_metadata/identity/
foreign_keys.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Foreign-key catalog rows have independent identities even when partition copies share enforcement.
8
9use crate::ast::{ColumnDef, ConstraintCatalogIdentity, TableConstraintSet};
10use crate::schema::constraint_metadata::{
11    CatalogIdentityAllocator, ConstraintMetadataError, ConstraintMetadataResult,
12};
13use std::collections::BTreeSet;
14use uqa_core::RelationIdentity;
15
16pub(crate) fn materialize(
17    target: &mut Option<ConstraintCatalogIdentity>,
18    allocate: &mut CatalogIdentityAllocator<'_>,
19) -> ConstraintMetadataResult<bool> {
20    if let Some(identity) = target {
21        validate_identity(*identity)?;
22        return Ok(false);
23    }
24    let object_id = allocate.allocate_object_id("foreign-key catalog row")?;
25    let identity = ConstraintCatalogIdentity {
26        object_id,
27        oid: allocate.allocate_catalog_oid(
28            crate::schema::constraint_metadata::CatalogOidClass::Constraint,
29            &object_id,
30        )?,
31    };
32    validate_identity(identity)?;
33    *target = Some(identity);
34    Ok(true)
35}
36
37fn validate_identity(identity: ConstraintCatalogIdentity) -> ConstraintMetadataResult<()> {
38    if identity.is_valid() {
39        Ok(())
40    } else {
41        Err(ConstraintMetadataError::Invalid(
42            "invalid foreign-key catalog identity".into(),
43        ))
44    }
45}
46
47pub fn identities<'a>(
48    columns: &'a [ColumnDef],
49    constraints: &'a TableConstraintSet,
50) -> impl Iterator<Item = Option<ConstraintCatalogIdentity>> + 'a {
51    columns
52        .iter()
53        .filter_map(|column| column.references.as_ref())
54        .map(|reference| reference.catalog_identity)
55        .chain(
56            constraints
57                .foreign_keys
58                .iter()
59                .map(|key| key.catalog_identity),
60        )
61}
62
63pub fn validate(
64    columns: &[ColumnDef],
65    constraints: &TableConstraintSet,
66) -> ConstraintMetadataResult<()> {
67    let mut objects = BTreeSet::new();
68    let mut oids = BTreeSet::new();
69    for identity in identities(columns, constraints) {
70        let identity = identity.ok_or_else(|| {
71            ConstraintMetadataError::Invalid(
72                "foreign keys require an initial catalog identity migration".into(),
73            )
74        })?;
75        validate_identity(identity)?;
76        if !objects.insert(identity.object_id) || !oids.insert(identity.oid) {
77            return Err(ConstraintMetadataError::Invalid(
78                "duplicate foreign-key catalog identity".into(),
79            ));
80        }
81    }
82    for inherited in &constraints.hierarchy.partition_inherited_foreign_keys {
83        if !constraints.foreign_keys.iter().any(|key| {
84            key.catalog_identity == inherited.catalog_identity
85                && key.object_id == inherited.object_id
86        }) {
87            return Err(ConstraintMetadataError::Invalid(
88                "partition foreign-key provenance does not identify its catalog row".into(),
89            ));
90        }
91    }
92    Ok(())
93}
94
95/// Capture missing identities before materialization so only legacy catalog rows retain name-derived OIDs.
96pub struct LegacyIdentities {
97    columns: Vec<usize>,
98    constraints: Vec<usize>,
99}
100
101impl LegacyIdentities {
102    pub fn capture(columns: &[ColumnDef], constraints: &TableConstraintSet) -> Self {
103        Self {
104            columns: columns
105                .iter()
106                .enumerate()
107                .filter_map(|(index, column)| {
108                    column
109                        .references
110                        .as_ref()
111                        .filter(|key| key.catalog_identity.is_none())
112                        .map(|_| index)
113                })
114                .collect(),
115            constraints: constraints
116                .foreign_keys
117                .iter()
118                .enumerate()
119                .filter_map(|(index, key)| key.catalog_identity.is_none().then_some(index))
120                .collect(),
121        }
122    }
123
124    pub fn preserve_oids(
125        self,
126        relation: &RelationIdentity,
127        columns: &mut [ColumnDef],
128        constraints: &mut TableConstraintSet,
129    ) -> ConstraintMetadataResult<bool> {
130        let changed = !self.columns.is_empty() || !self.constraints.is_empty();
131        for index in self.columns {
132            let key = columns[index].references.as_mut().ok_or_else(|| {
133                ConstraintMetadataError::Invalid("legacy foreign key disappeared".into())
134            })?;
135            preserve_oid(relation, key.name.as_deref(), key.catalog_identity.as_mut())?;
136        }
137        for index in self.constraints {
138            let key = &mut constraints.foreign_keys[index];
139            let previous = key.catalog_identity;
140            preserve_oid(relation, key.name.as_deref(), key.catalog_identity.as_mut())?;
141            let identity = key.catalog_identity;
142            for inherited in &mut constraints.hierarchy.partition_inherited_foreign_keys {
143                if inherited.catalog_identity == previous {
144                    inherited.catalog_identity = identity;
145                }
146            }
147        }
148        super::super::synchronize_partition_inherited_foreign_key_ids(constraints);
149        validate(columns, constraints)?;
150        Ok(changed)
151    }
152}
153
154fn preserve_oid(
155    relation: &RelationIdentity,
156    name: Option<&str>,
157    identity: Option<&mut ConstraintCatalogIdentity>,
158) -> ConstraintMetadataResult<()> {
159    let name = name
160        .ok_or_else(|| ConstraintMetadataError::Invalid("legacy foreign key has no name".into()))?;
161    let identity = identity.ok_or_else(|| {
162        ConstraintMetadataError::Invalid("legacy foreign key has no catalog identity".into())
163    })?;
164    identity.oid = crate::catalog::oids::stable_oid(
165        "constraint",
166        &format!("{}.{}.{name}", relation.schema, relation.name),
167    );
168    Ok(())
169}
170
171#[cfg(test)]
172mod tests;