Skip to main content

uqa_sql/schema/dependencies/
rewrites.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Rewrite stored relation, column, and literal sequence references in SQL schema expressions.
8use super::walk_schema_expr_mut;
9use uqa_core::{RelationIdentity, Value};
10
11pub fn stored_relation_reference_matches(reference: &str, target: &RelationIdentity) -> bool {
12    match RelationIdentity::parse_reference(reference) {
13        Ok((Some(schema), name)) => schema == target.schema && name == target.name,
14        Ok((None, name)) => name == target.name,
15        // Corrupt legacy metadata is never evidence that a dependency is absent; DDL must not leave it dangling.
16        Err(_) => true,
17    }
18}
19
20pub fn upgrade_legacy_schema_function_dispatches(
21    columns: &mut [crate::ast::ColumnDef],
22    constraints: &mut crate::ast::TableConstraintSet,
23) -> bool {
24    let mut changed = false;
25    for column in columns {
26        for expression in [column.default.as_mut(), column.check.as_mut()]
27            .into_iter()
28            .flatten()
29        {
30            changed |= expression.upgrade_legacy_serialized_dispatches();
31        }
32        if let Some(generated) = &mut column.generated {
33            changed |= generated.expression.upgrade_legacy_serialized_dispatches();
34        }
35    }
36    for check in &mut constraints.checks {
37        changed |= check.expr.upgrade_legacy_serialized_dispatches();
38    }
39    changed
40}
41
42pub fn rewrite_sequence_function_references(
43    expression: &mut crate::ast::Expr,
44    visit: &mut impl FnMut(&mut String) -> Result<(), String>,
45) -> Result<(), String> {
46    walk_schema_expr_mut(expression, &mut |node| {
47        let crate::ast::Expr::Func {
48            name,
49            binding,
50            args,
51            ..
52        } = node
53        else {
54            return Ok(());
55        };
56        if !is_sequence_function(name, binding.as_ref()) {
57            return Ok(());
58        }
59        let Some(reference) = args.first_mut().and_then(regclass_literal_mut) else {
60            // Dynamically computed text arguments retain late binding; literal regclass spellings identify catalog dependencies at declaration time.
61            return Ok(());
62        };
63        visit(reference)
64    })
65}
66
67pub(super) fn is_sequence_function(
68    name: &str,
69    binding: Option<&crate::ast::FunctionBinding>,
70) -> bool {
71    let name = if let Some(binding) = binding {
72        if !binding.builtin
73            || binding.object_id.is_some()
74            || binding.dispatch.is_some()
75            || binding.resolution_error.is_some()
76        {
77            return false;
78        }
79        &binding.name
80    } else {
81        name
82    };
83    let lower = name.to_ascii_lowercase();
84    let local = lower.strip_prefix("pg_catalog.").unwrap_or(&lower);
85    matches!(local, "nextval" | "currval" | "setval")
86}
87
88fn regclass_literal_mut(expression: &mut crate::ast::Expr) -> Option<&mut String> {
89    match expression {
90        crate::ast::Expr::Literal(Value::Str(reference)) => Some(reference),
91        crate::ast::Expr::Cast { expr, ty, .. }
92            if ty.eq_ignore_ascii_case("regclass")
93                || ty.eq_ignore_ascii_case("pg_catalog.regclass") =>
94        {
95            regclass_literal_mut(expr)
96        }
97        _ => None,
98    }
99}
100
101pub fn rename_schema_expr_column(
102    expression: &mut crate::ast::Expr,
103    from: &str,
104    to: &str,
105) -> Result<(), String> {
106    walk_schema_expr_mut(expression, &mut |node| {
107        match node {
108            crate::ast::Expr::Star | crate::ast::Expr::QualifiedStar(_) => {
109                return Err("schema expression contains `*` and cannot be rewritten safely".into());
110            }
111            crate::ast::Expr::Column(name) if name == from => *name = to.to_string(),
112            crate::ast::Expr::QualifiedColumn { column, .. } if column == from => {
113                *column = to.to_string();
114            }
115            _ => {}
116        }
117        Ok(())
118    })
119}
120
121pub fn schema_expr_references_relation(
122    expression: &crate::ast::Expr,
123    target: &RelationIdentity,
124) -> bool {
125    let mut expression = expression.clone();
126    let mut referenced = false;
127    let result = walk_schema_expr_mut(&mut expression, &mut |node| {
128        if let crate::ast::Expr::QualifiedColumn { qualifier, .. } = node {
129            referenced |= stored_relation_reference_matches(qualifier, target);
130        }
131        Ok(())
132    });
133    result.is_err() || referenced
134}
135
136pub fn rename_schema_expr_relation(
137    expression: &mut crate::ast::Expr,
138    from: &RelationIdentity,
139    to: &str,
140) -> Result<(), String> {
141    walk_schema_expr_mut(expression, &mut |node| {
142        if let crate::ast::Expr::QualifiedColumn { qualifier, .. } = node {
143            if stored_relation_reference_matches(qualifier, from) {
144                *qualifier = to.to_string();
145            }
146        }
147        Ok(())
148    })
149}
150
151pub fn rename_schema_expr_qualified_column(
152    expression: &mut crate::ast::Expr,
153    table: &RelationIdentity,
154    from: &str,
155    to: &str,
156) -> Result<(), String> {
157    walk_schema_expr_mut(expression, &mut |node| {
158        if let crate::ast::Expr::QualifiedColumn { qualifier, column } = node {
159            if column == from && stored_relation_reference_matches(qualifier, table) {
160                *column = to.to_string();
161            }
162        }
163        Ok(())
164    })
165}
166
167#[cfg(test)]
168mod tests;