Skip to main content

uqa_sql/catalog/index/
relationships.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Constraint ownership and index partition ancestry are independent catalog edges.
8
9use super::IndexDefinition;
10use crate::ast::{ColumnDef, IndexKey, TableKeyConstraint, TableKeyConstraintKind};
11use crate::schema::constraint_metadata::{ConstraintMetadataError, ConstraintMetadataResult};
12
13#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct IndexRelationships {
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub owning_constraint: Option<[u8; 16]>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub parent_index: Option<[u8; 16]>,
20}
21
22impl IndexRelationships {
23    pub fn is_empty(&self) -> bool {
24        self.owning_constraint.is_none() && self.parent_index.is_none()
25    }
26
27    pub fn validate(&self, index: [u8; 16]) -> ConstraintMetadataResult<()> {
28        if [self.owning_constraint, self.parent_index]
29            .into_iter()
30            .flatten()
31            .any(|target| target == [0; 16] || target == index)
32        {
33            return Err(ConstraintMetadataError::Invalid(
34                "invalid index ownership or parent identity".into(),
35            ));
36        }
37        Ok(())
38    }
39}
40
41pub struct IndexAttachmentShape<'a> {
42    pub method: &'a str,
43    pub keys: &'a [IndexKey],
44    pub definition: &'a IndexDefinition,
45    pub constraint_kind: Option<TableKeyConstraintKind>,
46}
47
48impl IndexDefinition {
49    pub fn for_constraint(
50        constraint: &TableKeyConstraint,
51        columns: &[ColumnDef],
52    ) -> ConstraintMetadataResult<Self> {
53        let owner = constraint.catalog_identity.ok_or_else(|| {
54            ConstraintMetadataError::Invalid("index owner has no constraint identity".into())
55        })?;
56        if !owner.is_valid() {
57            return Err(ConstraintMetadataError::Invalid(
58                "index owner has an invalid identity".into(),
59            ));
60        }
61        let key_types = constraint
62            .columns
63            .iter()
64            .map(|name| {
65                columns
66                    .iter()
67                    .find(|column| column.name == *name)
68                    .map(|column| column.ty.clone())
69                    .ok_or_else(|| {
70                        ConstraintMetadataError::Invalid(format!(
71                            "constraint index references missing column `{name}`"
72                        ))
73                    })
74            })
75            .collect::<ConstraintMetadataResult<_>>()?;
76        Ok(Self {
77            relationships: IndexRelationships {
78                owning_constraint: Some(owner.object_id),
79                parent_index: None,
80            },
81            key_names: constraint.columns.clone(),
82            key_types,
83            unique: true,
84            nulls_not_distinct: constraint.nulls_not_distinct,
85            ..Self::default()
86        })
87    }
88}
89
90/// A constraint-owned parent requires a same-kind child constraint; an independent parent can attach either an independent or constraint-owned child index. Object identities, physical namespaces and display names are not index-shape properties.
91pub fn can_attach_index(
92    parent: &IndexAttachmentShape<'_>,
93    child: &IndexAttachmentShape<'_>,
94) -> bool {
95    parent
96        .constraint_kind
97        .is_none_or(|kind| child.constraint_kind == Some(kind))
98        && parent.method.eq_ignore_ascii_case(child.method)
99        && parent.keys == child.keys
100        && parent.definition.unique == child.definition.unique
101        && parent.definition.nulls_not_distinct == child.definition.nulls_not_distinct
102        && parent.definition.included_columns == child.definition.included_columns
103        && parent.definition.predicate == child.definition.predicate
104        && (0..parent.keys.len()).all(|position| {
105            parent
106                .definition
107                .column_order
108                .get(position)
109                .copied()
110                .unwrap_or_default()
111                == child
112                    .definition
113                    .column_order
114                    .get(position)
115                    .copied()
116                    .unwrap_or_default()
117        })
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn constraint_ownership_controls_attachment_in_only_the_parent_direction() {
126        let definition = IndexDefinition {
127            unique: true,
128            ..IndexDefinition::default()
129        };
130        let keys = [IndexKey::Column("k".into())];
131        let shape = |constraint_kind| IndexAttachmentShape {
132            method: "btree",
133            keys: &keys,
134            definition: &definition,
135            constraint_kind,
136        };
137        use TableKeyConstraintKind::{PrimaryKey, Unique};
138        for parent in [None, Some(PrimaryKey), Some(Unique)] {
139            for child in [None, Some(PrimaryKey), Some(Unique)] {
140                assert_eq!(
141                    can_attach_index(&shape(parent), &shape(child)),
142                    parent.is_none() || parent == child,
143                    "parent {parent:?}, child {child:?}"
144                );
145            }
146        }
147        let other = [IndexKey::Column("other".into())];
148        let mut child = shape(None);
149        child.keys = &other;
150        assert!(!can_attach_index(&shape(None), &child));
151    }
152
153    #[test]
154    fn index_edges_reject_zero_and_self_references_and_remain_independent() {
155        let valid = IndexRelationships {
156            owning_constraint: Some([1; 16]),
157            parent_index: Some([2; 16]),
158        };
159        valid.validate([3; 16]).unwrap();
160        for invalid in [[0; 16], [3; 16]] {
161            for relationships in [
162                IndexRelationships {
163                    owning_constraint: Some(invalid),
164                    ..valid.clone()
165                },
166                IndexRelationships {
167                    parent_index: Some(invalid),
168                    ..valid.clone()
169                },
170            ] {
171                assert!(relationships.validate([3; 16]).is_err());
172            }
173        }
174        let detached = IndexRelationships {
175            parent_index: None,
176            ..valid
177        };
178        assert_eq!(detached.owning_constraint, Some([1; 16]));
179        assert!(!detached.is_empty());
180    }
181}