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 { name, args, .. } = node else {
48            return Ok(());
49        };
50        let lower = name.to_ascii_lowercase();
51        let local = lower.strip_prefix("pg_catalog.").unwrap_or(&lower);
52        if !matches!(local, "nextval" | "currval" | "setval")
53            || (lower.contains('.') && !lower.starts_with("pg_catalog."))
54        {
55            return Ok(());
56        }
57        let Some(reference) = args.first_mut().and_then(regclass_literal_mut) else {
58            // Dynamically computed text arguments retain late binding; literal regclass spellings identify catalog dependencies at declaration time.
59            return Ok(());
60        };
61        visit(reference)
62    })
63}
64
65fn regclass_literal_mut(expression: &mut crate::ast::Expr) -> Option<&mut String> {
66    match expression {
67        crate::ast::Expr::Literal(Value::Str(reference)) => Some(reference),
68        crate::ast::Expr::Cast { expr, ty }
69            if ty.eq_ignore_ascii_case("regclass")
70                || ty.eq_ignore_ascii_case("pg_catalog.regclass") =>
71        {
72            regclass_literal_mut(expr)
73        }
74        _ => None,
75    }
76}
77
78pub fn rename_schema_expr_column(
79    expression: &mut crate::ast::Expr,
80    from: &str,
81    to: &str,
82) -> Result<(), String> {
83    walk_schema_expr_mut(expression, &mut |node| {
84        match node {
85            crate::ast::Expr::Star | crate::ast::Expr::QualifiedStar(_) => {
86                return Err("schema expression contains `*` and cannot be rewritten safely".into());
87            }
88            crate::ast::Expr::Column(name) if name == from => *name = to.to_string(),
89            crate::ast::Expr::QualifiedColumn { column, .. } if column == from => {
90                *column = to.to_string();
91            }
92            _ => {}
93        }
94        Ok(())
95    })
96}
97
98pub fn schema_expr_references_relation(
99    expression: &crate::ast::Expr,
100    target: &RelationIdentity,
101) -> bool {
102    let mut expression = expression.clone();
103    let mut referenced = false;
104    let result = walk_schema_expr_mut(&mut expression, &mut |node| {
105        if let crate::ast::Expr::QualifiedColumn { qualifier, .. } = node {
106            referenced |= stored_relation_reference_matches(qualifier, target);
107        }
108        Ok(())
109    });
110    result.is_err() || referenced
111}
112
113pub fn rename_schema_expr_relation(
114    expression: &mut crate::ast::Expr,
115    from: &RelationIdentity,
116    to: &str,
117) -> Result<(), String> {
118    walk_schema_expr_mut(expression, &mut |node| {
119        if let crate::ast::Expr::QualifiedColumn { qualifier, .. } = node {
120            if stored_relation_reference_matches(qualifier, from) {
121                *qualifier = to.to_string();
122            }
123        }
124        Ok(())
125    })
126}
127
128pub fn rename_schema_expr_qualified_column(
129    expression: &mut crate::ast::Expr,
130    table: &RelationIdentity,
131    from: &str,
132    to: &str,
133) -> Result<(), String> {
134    walk_schema_expr_mut(expression, &mut |node| {
135        if let crate::ast::Expr::QualifiedColumn { qualifier, column } = node {
136            if column == from && stored_relation_reference_matches(qualifier, table) {
137                *column = to.to_string();
138            }
139        }
140        Ok(())
141    })
142}
143
144#[cfg(test)]
145mod tests;