Skip to main content

uqa_sql/schema/dependencies/
regclass.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Bind literal relation references in persisted SQL schema expressions to the relations they name when the expressions are published or loaded. Analysis reads them earlier, in `oid_alias`.
8use super::walk_schema_expr_mut;
9use crate::ast::{ColumnDef, ColumnType, Expr, TableCheck};
10use crate::schema::sequences::implicit_ownership::StoredSequenceNames;
11use uqa_core::Value;
12
13#[cfg(test)]
14mod tests;
15
16pub trait SchemaReferenceCatalog {
17    fn loaded_relation_name(&self, reference: &str) -> Result<Option<String>, String>;
18    fn bound_relation_oid(&self, canonical: &str) -> Result<Option<i64>, String>;
19    fn visible_relation_oid(&self, reference: &str) -> Result<Option<i64>, String>;
20    fn sequence_for_binding(&self, reference: &str) -> Result<String, String>;
21}
22
23fn is_regclass(ty: &str) -> bool {
24    ColumnType::from_sql_name(ty).is_ok_and(|ty| matches!(ty, ColumnType::Regclass))
25}
26
27pub fn bind_schema_regclass_constants(
28    catalog: &dyn SchemaReferenceCatalog,
29    expression: &mut Expr,
30    loaded: bool,
31) -> Result<bool, String> {
32    let mut changed = false;
33    walk_schema_expr_mut(expression, &mut |node| {
34        let Expr::Cast { expr, ty, .. } = node else {
35            return Ok(());
36        };
37        if !is_regclass(ty) {
38            return Ok(());
39        }
40        let Expr::Literal(Value::Str(reference)) = expr.as_ref() else {
41            return Ok(());
42        };
43        let oid = if loaded {
44            match reference.parse::<u32>() {
45                Ok(oid) => Some(i64::from(oid)),
46                Err(_) => match catalog.loaded_relation_name(reference)? {
47                    Some(canonical) => catalog.bound_relation_oid(&canonical)?,
48                    None => None,
49                },
50            }
51        } else {
52            catalog.visible_relation_oid(reference)?
53        }
54        .ok_or_else(|| format!("relation \"{reference}\" does not exist"))?;
55        **expr = Expr::TypedLiteral {
56            collation_oid: None,
57            composite_source: None,
58            value: Value::Int(oid),
59            ty: ColumnType::Regclass.catalog_name(),
60        };
61        changed = true;
62        Ok(())
63    })?;
64    Ok(changed)
65}
66
67pub fn bind_table_schema_regclass_constants(
68    catalog: &dyn SchemaReferenceCatalog,
69    columns: &mut [ColumnDef],
70    checks: &mut [TableCheck],
71    loaded: bool,
72) -> Result<bool, String> {
73    let mut changed = false;
74    for column in columns {
75        for expression in [&mut column.default, &mut column.check]
76            .into_iter()
77            .flatten()
78        {
79            changed |= bind_schema_regclass_constants(catalog, expression, loaded)?;
80        }
81        if let Some(generated) = &mut column.generated {
82            changed |= bind_schema_regclass_constants(catalog, &mut generated.expression, loaded)?;
83        }
84    }
85    for check in checks {
86        changed |= bind_schema_regclass_constants(catalog, &mut check.expr, loaded)?;
87    }
88    Ok(changed)
89}
90pub fn bind_sequence_references_in_expr(
91    catalog: &dyn SchemaReferenceCatalog,
92    expression: &mut Expr,
93) -> Result<(), String> {
94    super::rewrites::rewrite_sequence_function_references(expression, &mut |reference| {
95        *reference = catalog.sequence_for_binding(reference)?;
96        Ok(())
97    })?;
98    bind_schema_regclass_constants(catalog, expression, false)?;
99    Ok(())
100}
101
102/// Convert a legacy selected builtin's creation-bound sequence name to its OID.
103/// The caller owns initial-open publication; this only changes its candidate.
104/// Explicit text conversions and user or unresolved routine bindings remain unchanged.
105pub fn bind_legacy_sequence_regclass_constants(
106    catalog: &dyn SchemaReferenceCatalog,
107    sequences: &dyn StoredSequenceNames,
108    expression: &mut Expr,
109) -> Result<bool, String> {
110    let mut changed = false;
111    walk_schema_expr_mut(expression, &mut |node| {
112        let Expr::Func {
113            name,
114            binding: Some(binding),
115            args,
116            ..
117        } = node
118        else {
119            return Ok(());
120        };
121        if !super::rewrites::is_sequence_function(name, Some(binding)) {
122            return Ok(());
123        }
124        let Some(argument @ Expr::Literal(Value::Str(_))) = args.first_mut() else {
125            return Ok(());
126        };
127        let Expr::Literal(Value::Str(reference)) = &*argument else {
128            unreachable!("legacy sequence argument is a string literal");
129        };
130        let canonical = sequences.stored_sequence_name(reference)?;
131        let oid = catalog
132            .bound_relation_oid(&canonical)?
133            .ok_or_else(|| format!("relation \"{canonical}\" does not exist"))?;
134        *argument = Expr::TypedLiteral {
135            collation_oid: None,
136            composite_source: None,
137            value: Value::Int(oid),
138            ty: ColumnType::Regclass.catalog_name(),
139        };
140        if let Some(target) = binding.argument_types.first_mut() {
141            "regclass".clone_into(target);
142        }
143        changed = true;
144        Ok(())
145    })?;
146    Ok(changed)
147}