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: crate::schema::indexes::keys::key_names(
82                &crate::schema::indexes::names::constraint_index_attributes(constraint),
83            ),
84            key_types,
85            included_columns: constraint.included_columns.clone(),
86            unique: true,
87            nulls_not_distinct: constraint.nulls_not_distinct,
88            ..Self::default()
89        })
90    }
91}
92
93/// 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.
94pub fn can_attach_index(
95    parent: &IndexAttachmentShape<'_>,
96    child: &IndexAttachmentShape<'_>,
97) -> bool {
98    parent
99        .constraint_kind
100        .is_none_or(|kind| child.constraint_kind == Some(kind))
101        && parent.method.eq_ignore_ascii_case(child.method)
102        && parent.keys == child.keys
103        && parent.definition.unique == child.definition.unique
104        && parent.definition.nulls_not_distinct == child.definition.nulls_not_distinct
105        && parent.definition.included_columns == child.definition.included_columns
106        && parent.definition.predicate == child.definition.predicate
107        && (0..parent.keys.len()).all(|position| {
108            parent
109                .definition
110                .column_order
111                .get(position)
112                .copied()
113                .unwrap_or_default()
114                == child
115                    .definition
116                    .column_order
117                    .get(position)
118                    .copied()
119                    .unwrap_or_default()
120        })
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn constraint_ownership_controls_attachment_in_only_the_parent_direction() {
129        let definition = IndexDefinition {
130            unique: true,
131            ..IndexDefinition::default()
132        };
133        let keys = [IndexKey::Column("k".into())];
134        let shape = |constraint_kind| IndexAttachmentShape {
135            method: "btree",
136            keys: &keys,
137            definition: &definition,
138            constraint_kind,
139        };
140        use TableKeyConstraintKind::{PrimaryKey, Unique};
141        for parent in [None, Some(PrimaryKey), Some(Unique)] {
142            for child in [None, Some(PrimaryKey), Some(Unique)] {
143                assert_eq!(
144                    can_attach_index(&shape(parent), &shape(child)),
145                    parent.is_none() || parent == child,
146                    "parent {parent:?}, child {child:?}"
147                );
148            }
149        }
150        let other = [IndexKey::Column("other".into())];
151        let mut child = shape(None);
152        child.keys = &other;
153        assert!(!can_attach_index(&shape(None), &child));
154    }
155
156    #[test]
157    fn index_edges_reject_zero_and_self_references_and_remain_independent() {
158        let valid = IndexRelationships {
159            owning_constraint: Some([1; 16]),
160            parent_index: Some([2; 16]),
161        };
162        valid.validate([3; 16]).unwrap();
163        for invalid in [[0; 16], [3; 16]] {
164            for relationships in [
165                IndexRelationships {
166                    owning_constraint: Some(invalid),
167                    ..valid.clone()
168                },
169                IndexRelationships {
170                    parent_index: Some(invalid),
171                    ..valid.clone()
172                },
173            ] {
174                assert!(relationships.validate([3; 16]).is_err());
175            }
176        }
177        let detached = IndexRelationships {
178            parent_index: None,
179            ..valid
180        };
181        assert_eq!(detached.owning_constraint, Some([1; 16]));
182        assert!(!detached.is_empty());
183    }
184}