Skip to main content

uqa_sql/schema/sequences/
implicit_ownership.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Resolve declared SERIAL and IDENTITY owners against the loaded sequence namespace.
8use super::implicit::stored_owner_names_current;
9use crate::ast::ColumnDef;
10use uqa_core::RelationIdentity;
11
12pub trait StoredSequenceNames {
13    fn stored_sequence_name(&self, reference: &str) -> Result<String, String>;
14}
15pub struct BoundImplicitSequenceOwner {
16    pub sequence: String,
17    pub table_object_id: [u8; 16],
18    pub column_object_id: [u8; 16],
19    pub identity: bool,
20}
21pub fn bind_implicit_sequence_owners(
22    catalog: &dyn StoredSequenceNames,
23    table_name: &str,
24    table_object_id: [u8; 16],
25    columns: &[ColumnDef],
26) -> Result<Vec<BoundImplicitSequenceOwner>, String> {
27    let relation = RelationIdentity::from_legacy_name(table_name)?;
28    let mut bindings = Vec::new();
29    for column in columns {
30        let Some(provenance) = column.auto_increment.as_ref() else {
31            continue;
32        };
33        let Some(named_owner) = provenance.owner.as_ref() else {
34            continue;
35        };
36        if !stored_owner_names_current(&relation, column, named_owner) {
37            continue;
38        }
39        let Some(sequence) = provenance.sequence.as_deref() else {
40            continue;
41        };
42        let sequence = catalog.stored_sequence_name(sequence)?;
43        let column_object_id = column.object_id.ok_or_else(|| {
44            format!(
45                "column `{table_name}`.`{}` has no object identity",
46                column.name
47            )
48        })?;
49        bindings.push(BoundImplicitSequenceOwner {
50            sequence,
51            table_object_id,
52            column_object_id,
53            identity: provenance.is_identity(),
54        });
55    }
56    Ok(bindings)
57}
58
59/// Remove legacy named owner markers for one canonically identified sequence.
60pub fn clear_auto_increment_owner_markers(
61    columns: &mut [crate::ast::ColumnDef],
62    target: &uqa_core::RelationIdentity,
63) -> bool {
64    let mut changed = false;
65    for column in columns {
66        let Some(provenance) = column.auto_increment.as_mut() else {
67            continue;
68        };
69        if provenance.owner.is_some()
70            && provenance.sequence.as_deref().is_some_and(|reference| {
71                crate::schema::dependencies::rewrites::stored_relation_reference_matches(
72                    reference, target,
73                )
74            })
75        {
76            provenance.owner = None;
77            changed = true;
78        }
79    }
80    changed
81}
82
83/// Resolve a declaration's owners after its columns and constraints exist, including the table name in an explicitly named sequence's schema.
84pub fn bind_declared_sequence_owners(
85    names: &dyn StoredSequenceNames,
86    catalog: &dyn super::ownership::SequenceOwnerCatalog,
87    table_name: &str,
88    table_object_id: [u8; 16],
89    columns: &[ColumnDef],
90) -> Result<Vec<BoundImplicitSequenceOwner>, crate::SQLError> {
91    let mut bindings = bind_implicit_sequence_owners(names, table_name, table_object_id, columns)
92        .map_err(crate::SQLError::Internal)?;
93    let relation =
94        RelationIdentity::from_legacy_name(table_name).map_err(crate::SQLError::Internal)?;
95    for column in columns {
96        let Some(provenance) = column.auto_increment.as_ref() else {
97            continue;
98        };
99        let (Some(owner), Some(sequence)) = (&provenance.owner, &provenance.sequence) else {
100            continue;
101        };
102        if stored_owner_names_current(&relation, column, owner) {
103            continue;
104        }
105        let sequence = names
106            .stored_sequence_name(sequence)
107            .map_err(crate::SQLError::Internal)?;
108        let ownership = crate::ast::SequenceOwnership::Column {
109            table: owner.table.clone(),
110            column: owner.column.clone(),
111        };
112        let Some(identity) = super::ownership::bind_sequence_owner(catalog, &sequence, &ownership)?
113        else {
114            return Err(crate::SQLError::Internal(
115                "declared sequence owner disappeared".into(),
116            ));
117        };
118        bindings.push(BoundImplicitSequenceOwner {
119            sequence,
120            table_object_id: identity.table_object_id,
121            column_object_id: identity.column_object_id,
122            identity: provenance.is_identity(),
123        });
124    }
125    Ok(bindings)
126}
127
128/// Resolve the identity sequence owned by this column or its declarative-partition ancestor. A sequence explicitly linked to an unrelated table is not its DEFAULT source.
129pub fn identity_column_sequence(
130    catalog: &dyn crate::semantics::partition::PartitionCatalog,
131    table: &str,
132    column: &ColumnDef,
133) -> Result<Option<String>, crate::SQLError> {
134    use crate::SQLError;
135    let Some(provenance) = column
136        .auto_increment
137        .as_ref()
138        .filter(|provenance| provenance.is_identity())
139    else {
140        return Ok(None);
141    };
142    let missing = || SQLError::Routine {
143        sqlstate: "XX000".into(),
144        message: "no owned sequence found".into(),
145    };
146    let owner = provenance.owner.as_ref().ok_or_else(missing)?;
147    let mut current = table.to_string();
148    let mut visited = std::collections::BTreeSet::new();
149    loop {
150        let relation = RelationIdentity::from_legacy_name(&current).map_err(SQLError::Internal)?;
151        if stored_owner_names_current(&relation, column, owner) {
152            break;
153        }
154        if !visited.insert(current.clone()) {
155            return Err(SQLError::Internal(
156                "cycle in identity sequence ancestry".into(),
157            ));
158        }
159        let hierarchy = catalog
160            .try_table_hierarchy(&current)
161            .map_err(SQLError::Internal)?;
162        if !hierarchy.is_partition() {
163            return Err(missing());
164        }
165        current.clone_from(hierarchy.parents.first().ok_or_else(missing)?);
166    }
167    provenance.sequence.clone().map(Some).ok_or_else(missing)
168}
169
170#[cfg(test)]
171mod tests;