Skip to main content

uqa_sql/schema/constraint_metadata/
identity.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Allocate constraint incarnations and preserve legacy NOT NULL OIDs during initial migration.
8
9use super::{CatalogIdentityAllocator, ConstraintMetadataError, ConstraintMetadataResult};
10use crate::ast::{ColumnDef, ConstraintCatalogIdentity};
11
12pub mod claims;
13pub mod foreign_keys;
14pub mod keys;
15pub mod legacy;
16
17/// `DefineIndex` and `index_constraint_create`: a key's enforcing index takes its OID, then the constraint takes its own.
18pub fn materialize_key_identity(
19    key: &mut crate::ast::TableKeyConstraint,
20    allocate: &mut CatalogIdentityAllocator<'_>,
21) -> ConstraintMetadataResult<bool> {
22    if let Some(identity) = key.catalog_identity {
23        if identity.is_valid() {
24            return Ok(false);
25        }
26        return Err(ConstraintMetadataError::Invalid(
27            "invalid key constraint catalog identity".into(),
28        ));
29    }
30    // The enforcing index takes its OID before the constraint does.
31    let index_object_id = allocate.allocate_object_id("index")?;
32    key.index_identity = Some(ConstraintCatalogIdentity {
33        object_id: index_object_id,
34        oid: allocate.allocate_catalog_oid(super::CatalogOidClass::Relation, &index_object_id)?,
35    });
36    let object_id = allocate.allocate_object_id("key constraint")?;
37    key.catalog_identity = Some(ConstraintCatalogIdentity {
38        object_id,
39        oid: allocate.allocate_catalog_oid(super::CatalogOidClass::Constraint, &object_id)?,
40    });
41    if !key
42        .catalog_identity
43        .is_some_and(ConstraintCatalogIdentity::is_valid)
44    {
45        return Err(ConstraintMetadataError::Invalid(
46            "invalid key constraint catalog identity".into(),
47        ));
48    }
49    Ok(true)
50}
51
52pub(super) fn materialize_check_oid(
53    object_id: Option<[u8; 16]>,
54    oid: &mut Option<i64>,
55    allocate: &mut CatalogIdentityAllocator<'_>,
56) -> ConstraintMetadataResult<bool> {
57    let object_id = object_id.ok_or_else(|| {
58        ConstraintMetadataError::Invalid("CHECK has no catalog incarnation".into())
59    })?;
60    let changed = oid.is_none();
61    let value = match *oid {
62        Some(value) => value,
63        None => allocate.allocate_catalog_oid(super::CatalogOidClass::Constraint, &object_id)?,
64    };
65    if !(ConstraintCatalogIdentity {
66        object_id,
67        oid: value,
68    })
69    .is_valid()
70    {
71        return Err(ConstraintMetadataError::Invalid(
72            "invalid CHECK catalog identity".into(),
73        ));
74    }
75    *oid = Some(value);
76    Ok(changed)
77}
78
79/// `StoreAttrDefault`: a column's default or generation expression has its own `pg_attrdef` row, whose OID it keeps until the expression is replaced. A column without one has no row.
80pub fn materialize_default_oid(
81    column: &mut ColumnDef,
82    allocate: &mut CatalogIdentityAllocator<'_>,
83) -> ConstraintMetadataResult<bool> {
84    let has_expression = column.default.is_some()
85        || column.generated.is_some()
86        || column
87            .auto_increment
88            .as_ref()
89            .is_some_and(crate::ast::AutoIncrement::is_legacy);
90    if !has_expression {
91        return Ok(column.default_catalog_oid.take().is_some());
92    }
93    if column.default_catalog_oid.is_some() {
94        return Ok(false);
95    }
96    let object_id = column.object_id.ok_or_else(|| {
97        ConstraintMetadataError::Invalid("column default has no column incarnation".into())
98    })?;
99    column.default_catalog_oid =
100        Some(allocate.allocate_catalog_oid(super::CatalogOidClass::AttributeDefault, &object_id)?);
101    Ok(true)
102}
103
104/// `StoreRelNotNull`: a NOT NULL constraint takes its incarnation and `pg_constraint` OID when it is stored.
105pub fn materialize_not_null_identity(
106    column: &mut ColumnDef,
107    allocate: &mut CatalogIdentityAllocator<'_>,
108) -> ConstraintMetadataResult<bool> {
109    if let Some(identity) = column.not_null_identity {
110        validate(identity)?;
111        return Ok(false);
112    }
113    let object_id = allocate.allocate_object_id("NOT NULL constraint")?;
114    let identity = ConstraintCatalogIdentity {
115        object_id,
116        oid: allocate.allocate_catalog_oid(super::CatalogOidClass::Constraint, &object_id)?,
117    };
118    validate(identity)?;
119    column.not_null_identity = Some(identity);
120    Ok(true)
121}
122
123fn validate(identity: ConstraintCatalogIdentity) -> ConstraintMetadataResult<()> {
124    if identity.is_valid() {
125        Ok(())
126    } else {
127        Err(ConstraintMetadataError::Invalid(
128            "invalid NOT NULL constraint catalog identity".into(),
129        ))
130    }
131}
132
133pub fn validate_not_null_identities(columns: &[ColumnDef]) -> ConstraintMetadataResult<()> {
134    let mut identities = std::collections::BTreeSet::new();
135    let mut oids = std::collections::BTreeSet::new();
136    for column in columns {
137        match (column.not_null, column.not_null_identity) {
138            (true, Some(identity)) => {
139                validate(identity)?;
140                if !identities.insert(identity.object_id) || !oids.insert(identity.oid) {
141                    return Err(ConstraintMetadataError::Invalid(
142                        "duplicate NOT NULL constraint catalog identity".into(),
143                    ));
144                }
145            }
146            (true, None) => {
147                return Err(ConstraintMetadataError::Invalid(
148                    "NOT NULL constraint requires an initial catalog identity migration".into(),
149                ))
150            }
151            (false, Some(_)) => {
152                return Err(ConstraintMetadataError::Invalid(
153                    "nullable column retains a NOT NULL constraint identity".into(),
154                ))
155            }
156            (false, None) => {}
157        }
158    }
159    Ok(())
160}
161
162/// Called only during the initial catalog transaction, before a legacy database becomes visible.
163pub fn migrate_constraint_metadata(
164    relation: &uqa_core::RelationIdentity,
165    columns: &mut [ColumnDef],
166    constraints: &mut crate::ast::TableConstraintSet,
167    allocate: &mut CatalogIdentityAllocator<'_>,
168) -> ConstraintMetadataResult<bool> {
169    let missing: Vec<_> = columns
170        .iter()
171        .map(|column| column.not_null && column.not_null_identity.is_none())
172        .collect();
173    let changed = super::materialize_constraint_metadata(relation, columns, constraints, allocate)?;
174    for (column, missing) in columns.iter_mut().zip(missing) {
175        if missing {
176            let name = column.not_null_name.as_deref().ok_or_else(|| {
177                ConstraintMetadataError::Invalid("migrated NOT NULL constraint has no name".into())
178            })?;
179            let identity = column.not_null_identity.as_mut().ok_or_else(|| {
180                ConstraintMetadataError::Invalid(
181                    "migrated NOT NULL constraint has no identity".into(),
182                )
183            })?;
184            identity.oid = crate::catalog::oids::stable_oid(
185                "constraint",
186                &format!("{}.{}.{name}", relation.schema, relation.name),
187            );
188        }
189    }
190    validate_not_null_identities(columns)?;
191    Ok(changed)
192}
193
194#[cfg(test)]
195mod tests;