Skip to main content

uqa_sql/schema/
constraint_metadata.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Normalize durable constraint names and identities through a caller-owned identity allocator.
8use std::collections::BTreeSet;
9use uqa_core::RelationIdentity;
10
11#[derive(Debug, thiserror::Error)]
12#[error("{0}")]
13pub struct ConstraintMetadataError(pub String);
14pub type ConstraintMetadataResult<T> = Result<T, ConstraintMetadataError>;
15pub type CatalogIdentityAllocator<'a> = dyn FnMut(&str) -> ConstraintMetadataResult<[u8; 16]> + 'a;
16
17pub fn materialize_constraint_metadata(
18    relation: &RelationIdentity,
19    columns: &mut [crate::ast::ColumnDef],
20    constraints: &mut crate::ast::TableConstraintSet,
21    allocate: &mut CatalogIdentityAllocator<'_>,
22) -> ConstraintMetadataResult<bool> {
23    // Releases predating typed table-key persistence stored column-level PRIMARY KEY and UNIQUE declarations only as ColumnDef flags. Promote those legacy flags before assigning names so catalog publication always sees named constraints.
24    let mut changed = promote_legacy_column_key_constraints(columns, constraints);
25    let mut used = BTreeSet::new();
26    for column in columns.iter() {
27        record_constraint_name(&mut used, column.not_null_name.as_deref())?;
28        record_constraint_name(&mut used, column.check_name.as_deref())?;
29        record_constraint_name(
30            &mut used,
31            column
32                .references
33                .as_ref()
34                .and_then(|reference| reference.name.as_deref()),
35        )?;
36    }
37    for constraint in &constraints.key_constraints {
38        record_constraint_name(&mut used, constraint.name.as_deref())?;
39    }
40    for constraint in &constraints.checks {
41        record_constraint_name(&mut used, constraint.name.as_deref())?;
42    }
43    for constraint in &constraints.foreign_keys {
44        record_constraint_name(&mut used, constraint.name.as_deref())?;
45    }
46
47    let mut column_object_ids = BTreeSet::new();
48    for column in columns.iter_mut() {
49        if column
50            .object_id
51            .is_some_and(|object_id| !column_object_ids.insert(object_id))
52        {
53            column.object_id = None;
54        }
55        changed |= assign_catalog_object_id(&mut column.object_id, "column", allocate)?;
56        if let Some(object_id) = column.object_id {
57            column_object_ids.insert(object_id);
58        }
59        if column.not_null {
60            changed |= assign_constraint_name(
61                &mut column.not_null_name,
62                format!("{}_{}_not_null", relation.name, column.name),
63                &mut used,
64            )?;
65        }
66        if column.check.is_some() {
67            changed |= assign_constraint_name(
68                &mut column.check_name,
69                format!("{}_{}_check", relation.name, column.name),
70                &mut used,
71            )?;
72            changed |= assign_catalog_object_id(
73                &mut column.check_object_id,
74                "CHECK constraint",
75                allocate,
76            )?;
77        }
78        if let Some(reference) = &mut column.references {
79            changed |= assign_constraint_name(
80                &mut reference.name,
81                format!("{}_{}_fkey", relation.name, column.name),
82                &mut used,
83            )?;
84            changed |= assign_constraint_object_id(&mut reference.object_id, allocate)?;
85        }
86    }
87    for constraint in &mut constraints.key_constraints {
88        let base = match constraint.kind {
89            crate::ast::TableKeyConstraintKind::PrimaryKey => {
90                format!("{}_pkey", relation.name)
91            }
92            crate::ast::TableKeyConstraintKind::Unique => format!(
93                "{}_{}_key",
94                relation.name,
95                constraint_column_component(&constraint.columns, relation)?
96            ),
97        };
98        changed |= assign_constraint_name(&mut constraint.name, base, &mut used)?;
99    }
100    for constraint in &mut constraints.checks {
101        let mut referenced_columns = Vec::new();
102        collect_constraint_columns(&constraint.expr, &mut referenced_columns);
103        let base = if referenced_columns.len() == 1 {
104            format!("{}_{}_check", relation.name, referenced_columns[0])
105        } else {
106            format!("{}_check", relation.name)
107        };
108        changed |= assign_constraint_name(&mut constraint.name, base, &mut used)?;
109        changed |=
110            assign_catalog_object_id(&mut constraint.object_id, "CHECK constraint", allocate)?;
111    }
112    changed |= synchronize_partition_inherited_foreign_key_ids(constraints);
113    for constraint in &mut constraints.foreign_keys {
114        let component = constraint_column_component(&constraint.local_columns, relation)?;
115        changed |= assign_constraint_name(
116            &mut constraint.name,
117            format!("{}_{}_fkey", relation.name, component),
118            &mut used,
119        )?;
120        changed |= assign_constraint_object_id(&mut constraint.object_id, allocate)?;
121    }
122    changed |= synchronize_partition_inherited_foreign_key_ids(constraints);
123    Ok(changed)
124}
125
126fn promote_legacy_column_key_constraints(
127    columns: &[crate::ast::ColumnDef],
128    constraints: &mut crate::ast::TableConstraintSet,
129) -> bool {
130    let mut changed = false;
131    for column in columns {
132        for (present, kind) in [
133            (
134                column.primary_key,
135                crate::ast::TableKeyConstraintKind::PrimaryKey,
136            ),
137            (column.unique, crate::ast::TableKeyConstraintKind::Unique),
138        ] {
139            if !present
140                || constraints.key_constraints.iter().any(|constraint| {
141                    constraint.kind == kind
142                        && constraint.columns.as_slice() == [column.name.as_str()]
143                })
144            {
145                continue;
146            }
147            constraints
148                .key_constraints
149                .push(crate::ast::TableKeyConstraint {
150                    name: None,
151                    kind,
152                    columns: vec![column.name.clone()],
153                    nulls_not_distinct: false,
154                    without_overlaps: false,
155                });
156            changed = true;
157        }
158    }
159    changed
160}
161
162pub fn foreign_keys_match_without_object_id(
163    left: &crate::ast::ForeignKey,
164    right: &crate::ast::ForeignKey,
165) -> bool {
166    let mut left = left.clone();
167    let mut right = right.clone();
168    left.object_id = None;
169    right.object_id = None;
170    left == right
171}
172
173pub fn synchronize_partition_inherited_foreign_key_ids(
174    constraints: &mut crate::ast::TableConstraintSet,
175) -> bool {
176    let mut changed = false;
177    for inherited_index in 0..constraints.hierarchy.partition_inherited_foreign_keys.len() {
178        let inherited = &constraints.hierarchy.partition_inherited_foreign_keys[inherited_index];
179        let Some(foreign_key_index) = constraints
180            .foreign_keys
181            .iter()
182            .position(|foreign_key| foreign_keys_match_without_object_id(foreign_key, inherited))
183        else {
184            continue;
185        };
186        let object_id = constraints.foreign_keys[foreign_key_index]
187            .object_id
188            .or(inherited.object_id);
189        if constraints.foreign_keys[foreign_key_index].object_id != object_id {
190            constraints.foreign_keys[foreign_key_index].object_id = object_id;
191            changed = true;
192        }
193        if constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].object_id
194            != object_id
195        {
196            constraints.hierarchy.partition_inherited_foreign_keys[inherited_index].object_id =
197                object_id;
198            changed = true;
199        }
200    }
201    changed
202}
203
204fn assign_constraint_object_id(
205    target: &mut Option<[u8; 16]>,
206    allocate: &mut CatalogIdentityAllocator<'_>,
207) -> ConstraintMetadataResult<bool> {
208    assign_catalog_object_id(target, "foreign-key constraint", allocate)
209}
210
211fn assign_catalog_object_id(
212    target: &mut Option<[u8; 16]>,
213    object_kind: &str,
214    allocate: &mut CatalogIdentityAllocator<'_>,
215) -> ConstraintMetadataResult<bool> {
216    if target.is_some() {
217        return Ok(false);
218    }
219    *target = Some(allocate(object_kind)?);
220    Ok(true)
221}
222
223fn record_constraint_name(
224    used: &mut BTreeSet<String>,
225    name: Option<&str>,
226) -> ConstraintMetadataResult<()> {
227    let Some(name) = name else {
228        return Ok(());
229    };
230    if name.is_empty() {
231        return Err(ConstraintMetadataError(
232            "constraint name must not be empty".into(),
233        ));
234    }
235    if !used.insert(name.to_string()) {
236        return Err(ConstraintMetadataError(format!(
237            "constraint `{name}` is declared more than once"
238        )));
239    }
240    Ok(())
241}
242
243fn assign_constraint_name(
244    target: &mut Option<String>,
245    base: String,
246    used: &mut BTreeSet<String>,
247) -> ConstraintMetadataResult<bool> {
248    if target.is_some() {
249        return Ok(false);
250    }
251    if used.insert(base.clone()) {
252        *target = Some(base);
253        return Ok(true);
254    }
255    for suffix in 1_u64.. {
256        let candidate = format!("{base}{suffix}");
257        if used.insert(candidate.clone()) {
258            *target = Some(candidate);
259            return Ok(true);
260        }
261    }
262    Err(ConstraintMetadataError(format!(
263        "constraint name suffix space exhausted for `{base}`"
264    )))
265}
266
267fn constraint_column_component(
268    columns: &[String],
269    relation: &RelationIdentity,
270) -> ConstraintMetadataResult<String> {
271    if columns.is_empty() {
272        return Err(ConstraintMetadataError(format!(
273            "constraint on table `{}` has no columns",
274            relation.qualified_name()
275        )));
276    }
277    Ok(columns.join("_"))
278}
279
280fn collect_constraint_columns(expression: &crate::ast::Expr, output: &mut Vec<String>) {
281    use crate::ast::{Expr, FrameBound};
282    match expression {
283        Expr::Column(name) | Expr::QualifiedColumn { column: name, .. } => {
284            if !output.contains(name) {
285                output.push(name.clone());
286            }
287        }
288        Expr::Func {
289            args,
290            order_by,
291            filter,
292            ..
293        } => {
294            for argument in args {
295                collect_constraint_columns(argument, output);
296            }
297            for order in order_by {
298                collect_constraint_columns(&order.expr, output);
299            }
300            if let Some(filter) = filter {
301                collect_constraint_columns(filter, output);
302            }
303        }
304        Expr::Array(items) | Expr::Row(items) | Expr::And(items) | Expr::Or(items) => {
305            for item in items {
306                collect_constraint_columns(item, output);
307            }
308        }
309        Expr::Binary { lhs, rhs, .. } => {
310            collect_constraint_columns(lhs, output);
311            collect_constraint_columns(rhs, output);
312        }
313        Expr::Not(inner)
314        | Expr::UnaryMinus(inner)
315        | Expr::IsNull { expr: inner, .. }
316        | Expr::Cast { expr: inner, .. } => {
317            collect_constraint_columns(inner, output);
318        }
319        Expr::Between { expr, low, high } => {
320            collect_constraint_columns(expr, output);
321            collect_constraint_columns(low, output);
322            collect_constraint_columns(high, output);
323        }
324        Expr::InList { expr, list, .. } => {
325            collect_constraint_columns(expr, output);
326            for item in list {
327                collect_constraint_columns(item, output);
328            }
329        }
330        Expr::WindowCall { args, spec, .. } => {
331            for argument in args {
332                collect_constraint_columns(argument, output);
333            }
334            for expression in &spec.partition_by {
335                collect_constraint_columns(expression, output);
336            }
337            for order in &spec.order_by {
338                collect_constraint_columns(&order.expr, output);
339            }
340            if let Some(frame) = &spec.frame {
341                for bound in [&frame.start, &frame.end] {
342                    if let FrameBound::Preceding(expression) | FrameBound::Following(expression) =
343                        bound
344                    {
345                        collect_constraint_columns(expression, output);
346                    }
347                }
348            }
349        }
350        Expr::Case {
351            base,
352            when,
353            else_branch,
354        } => {
355            if let Some(base) = base {
356                collect_constraint_columns(base, output);
357            }
358            for (condition, result) in when {
359                collect_constraint_columns(condition, output);
360                collect_constraint_columns(result, output);
361            }
362            if let Some(else_branch) = else_branch {
363                collect_constraint_columns(else_branch, output);
364            }
365        }
366        Expr::InSubquery { expr, .. } => collect_constraint_columns(expr, output),
367        Expr::Default
368        | Expr::Star
369        | Expr::QualifiedStar(_)
370        | Expr::InternalColumn(_)
371        | Expr::Literal(_)
372        | Expr::TypedLiteral { .. }
373        | Expr::Param(_)
374        | Expr::ScalarSubquery(_)
375        | Expr::Exists { .. } => {}
376    }
377}