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 crate::ast::ColumnType::from_sql_name(ty)
93                .is_ok_and(|ty| matches!(ty, crate::ast::ColumnType::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 => {
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        referenced |= match node {
129            crate::ast::Expr::QualifiedColumn { qualifier, .. } => {
130                stored_relation_reference_matches(qualifier, target)
131            }
132            crate::ast::Expr::QualifiedStar(qualifier) => {
133                qualifier == &target.name || stored_relation_reference_matches(qualifier, target)
134            }
135            _ => false,
136        };
137        Ok(())
138    });
139    result.is_err() || referenced
140}
141
142pub fn rename_schema_expr_relation(
143    expression: &mut crate::ast::Expr,
144    from: &RelationIdentity,
145    to: &str,
146) -> Result<(), String> {
147    walk_schema_expr_mut(expression, &mut |node| {
148        match node {
149            crate::ast::Expr::QualifiedColumn { qualifier, .. }
150                if stored_relation_reference_matches(qualifier, from) =>
151            {
152                *qualifier = to.to_string();
153            }
154            crate::ast::Expr::QualifiedStar(qualifier)
155                if qualifier == &from.name
156                    || stored_relation_reference_matches(qualifier, from) =>
157            {
158                // Analyzed whole rows use the decoded local range name; DDL supplies a SQL-qualified destination.
159                *qualifier = RelationIdentity::parse_reference(to)?.1;
160            }
161            _ => {}
162        }
163        Ok(())
164    })
165}
166
167pub fn rename_schema_expr_qualified_column(
168    expression: &mut crate::ast::Expr,
169    table: &RelationIdentity,
170    from: &str,
171    to: &str,
172) -> Result<(), String> {
173    walk_schema_expr_mut(expression, &mut |node| {
174        if let crate::ast::Expr::QualifiedColumn { qualifier, column } = node {
175            if column == from && stored_relation_reference_matches(qualifier, table) {
176                *column = to.to_string();
177            }
178        }
179        Ok(())
180    })
181}
182
183#[cfg(test)]
184mod tests;