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
17pub(super) fn materialize_key_identity(
18    key: &mut crate::ast::TableKeyConstraint,
19    allocate: &mut CatalogIdentityAllocator<'_>,
20) -> ConstraintMetadataResult<bool> {
21    if let Some(identity) = key.catalog_identity {
22        if identity.is_valid() {
23            return Ok(false);
24        }
25        return Err(ConstraintMetadataError::Invalid(
26            "invalid key constraint catalog identity".into(),
27        ));
28    }
29    let object_id = allocate.allocate_object_id("key constraint")?;
30    key.catalog_identity = Some(ConstraintCatalogIdentity {
31        object_id,
32        oid: allocate.allocate_catalog_oid(super::CatalogOidClass::Constraint, &object_id)?,
33    });
34    if !key
35        .catalog_identity
36        .is_some_and(ConstraintCatalogIdentity::is_valid)
37    {
38        return Err(ConstraintMetadataError::Invalid(
39            "invalid key constraint catalog identity".into(),
40        ));
41    }
42    Ok(true)
43}
44
45pub(super) fn materialize_check_oid(
46    object_id: Option<[u8; 16]>,
47    oid: &mut Option<i64>,
48    allocate: &mut CatalogIdentityAllocator<'_>,
49) -> ConstraintMetadataResult<bool> {
50    let object_id = object_id.ok_or_else(|| {
51        ConstraintMetadataError::Invalid("CHECK has no catalog incarnation".into())
52    })?;
53    let changed = oid.is_none();
54    let value = match *oid {
55        Some(value) => value,
56        None => allocate.allocate_catalog_oid(super::CatalogOidClass::Constraint, &object_id)?,
57    };
58    if !(ConstraintCatalogIdentity {
59        object_id,
60        oid: value,
61    })
62    .is_valid()
63    {
64        return Err(ConstraintMetadataError::Invalid(
65            "invalid CHECK catalog identity".into(),
66        ));
67    }
68    *oid = Some(value);
69    Ok(changed)
70}
71
72pub(super) fn materialize_not_null_identity(
73    column: &mut ColumnDef,
74    allocate: &mut CatalogIdentityAllocator<'_>,
75) -> ConstraintMetadataResult<bool> {
76    if let Some(identity) = column.not_null_identity {
77        validate(identity)?;
78        return Ok(false);
79    }
80    let object_id = allocate.allocate_object_id("NOT NULL constraint")?;
81    let identity = ConstraintCatalogIdentity {
82        object_id,
83        oid: allocate.allocate_catalog_oid(super::CatalogOidClass::Constraint, &object_id)?,
84    };
85    validate(identity)?;
86    column.not_null_identity = Some(identity);
87    Ok(true)
88}
89
90fn validate(identity: ConstraintCatalogIdentity) -> ConstraintMetadataResult<()> {
91    if identity.is_valid() {
92        Ok(())
93    } else {
94        Err(ConstraintMetadataError::Invalid(
95            "invalid NOT NULL constraint catalog identity".into(),
96        ))
97    }
98}
99
100pub fn validate_not_null_identities(columns: &[ColumnDef]) -> ConstraintMetadataResult<()> {
101    let mut identities = std::collections::BTreeSet::new();
102    let mut oids = std::collections::BTreeSet::new();
103    for column in columns {
104        match (column.not_null, column.not_null_identity) {
105            (true, Some(identity)) => {
106                validate(identity)?;
107                if !identities.insert(identity.object_id) || !oids.insert(identity.oid) {
108                    return Err(ConstraintMetadataError::Invalid(
109                        "duplicate NOT NULL constraint catalog identity".into(),
110                    ));
111                }
112            }
113            (true, None) => {
114                return Err(ConstraintMetadataError::Invalid(
115                    "NOT NULL constraint requires an initial catalog identity migration".into(),
116                ))
117            }
118            (false, Some(_)) => {
119                return Err(ConstraintMetadataError::Invalid(
120                    "nullable column retains a NOT NULL constraint identity".into(),
121                ))
122            }
123            (false, None) => {}
124        }
125    }
126    Ok(())
127}
128
129/// Called only during the initial catalog transaction, before a legacy database becomes visible.
130pub fn migrate_constraint_metadata(
131    relation: &uqa_core::RelationIdentity,
132    columns: &mut [ColumnDef],
133    constraints: &mut crate::ast::TableConstraintSet,
134    allocate: &mut CatalogIdentityAllocator<'_>,
135) -> ConstraintMetadataResult<bool> {
136    let missing: Vec<_> = columns
137        .iter()
138        .map(|column| column.not_null && column.not_null_identity.is_none())
139        .collect();
140    let changed = super::materialize_constraint_metadata(relation, columns, constraints, allocate)?;
141    for (column, missing) in columns.iter_mut().zip(missing) {
142        if missing {
143            let name = column.not_null_name.as_deref().ok_or_else(|| {
144                ConstraintMetadataError::Invalid("migrated NOT NULL constraint has no name".into())
145            })?;
146            let identity = column.not_null_identity.as_mut().ok_or_else(|| {
147                ConstraintMetadataError::Invalid(
148                    "migrated NOT NULL constraint has no identity".into(),
149                )
150            })?;
151            identity.oid = crate::catalog::oids::stable_oid(
152                "constraint",
153                &format!("{}.{}.{name}", relation.schema, relation.name),
154            );
155        }
156    }
157    validate_not_null_identities(columns)?;
158    Ok(changed)
159}
160
161#[cfg(test)]
162mod tests;