Skip to main content

uqa_sql/binding/
legacy_patterns.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Restore predecessor compiler pattern markers only at retained-definition boundaries.
8
9use crate::{
10    ast::{ColumnDef, Expr, FunctionBinding, FunctionDispatch, PatternOperator, TableCheck},
11    plan::QueryPlan,
12    FunctionTypeResolver, ScalarExpr,
13};
14
15type PatternCall<'a, T> = (
16    &'a mut String,
17    &'a mut Option<FunctionBinding>,
18    &'a mut Vec<T>,
19);
20
21trait PatternNode: Sized {
22    fn call(&mut self) -> Option<PatternCall<'_, Self>>;
23    fn negated(&mut self) -> Option<&mut Self>;
24    fn native(dispatch: FunctionDispatch, arguments: Vec<Self>) -> Self;
25    fn null() -> Self;
26}
27
28macro_rules! pattern_node {
29    ($node:ty) => {
30        impl PatternNode for $node {
31            fn call(&mut self) -> Option<PatternCall<'_, Self>> {
32                match self {
33                    Self::Func {
34                        name,
35                        binding,
36                        args,
37                        distinct: false,
38                        order_by,
39                        filter: None,
40                        ..
41                    } if order_by.is_empty() => Some((name, binding, args)),
42                    _ => None,
43                }
44            }
45
46            fn negated(&mut self) -> Option<&mut Self> {
47                match self {
48                    Self::Not(inner) => Some(inner),
49                    _ => None,
50                }
51            }
52
53            fn native(dispatch: FunctionDispatch, args: Vec<Self>) -> Self {
54                let binding = FunctionBinding::dispatched(dispatch);
55                Self::Func {
56                    name: binding.name.clone(),
57                    binding: Some(binding),
58                    args,
59                    order_syntax: crate::ast::FunctionCallSyntax::Ordinary,
60                    distinct: false,
61                    order_by: Vec::new(),
62                    filter: None,
63                }
64            }
65
66            fn null() -> Self {
67                Self::Literal(uqa_core::Value::Null)
68            }
69        }
70    };
71}
72
73pattern_node!(Expr);
74pattern_node!(ScalarExpr);
75
76fn restore_call<T: PatternNode>(
77    expression: &mut T,
78    negated: bool,
79    resolver: &dyn FunctionTypeResolver,
80) -> bool {
81    let Some((name, binding, arguments)) = expression.call() else {
82        return false;
83    };
84    if binding.as_ref().is_some_and(|binding| {
85        !binding.builtin
86            || binding.dispatch.is_some()
87            || binding.object_id.is_some()
88            || binding.resolution_error.is_some()
89            || !binding.argument_types.is_empty()
90    }) || binding.is_none() && resolver.has_untyped_function(name)
91    {
92        return false;
93    }
94    let (operator, similar) = match name.as_str() {
95        "like" if (2..=3).contains(&arguments.len()) => (
96            if negated {
97                PatternOperator::NotLike
98            } else {
99                PatternOperator::Like
100            },
101            false,
102        ),
103        "ilike" if (2..=3).contains(&arguments.len()) => (
104            if negated {
105                PatternOperator::NotILike
106            } else {
107                PatternOperator::ILike
108            },
109            false,
110        ),
111        "similar_to" if (2..=3).contains(&arguments.len()) => (
112            if negated {
113                PatternOperator::NotRegex
114            } else {
115                PatternOperator::Regex
116            },
117            true,
118        ),
119        "regex_match_op" if arguments.len() == 2 => (
120            if negated {
121                PatternOperator::NotRegex
122            } else {
123                PatternOperator::Regex
124            },
125            false,
126        ),
127        "regex_imatch_op" if arguments.len() == 2 => (
128            if negated {
129                PatternOperator::NotRegexInsensitive
130            } else {
131                PatternOperator::RegexInsensitive
132            },
133            false,
134        ),
135        _ => return false,
136    };
137    if similar || arguments.len() == 3 {
138        let helper_arguments = arguments.split_off(1);
139        arguments.push(T::native(
140            FunctionDispatch::PatternEscape { similar },
141            helper_arguments,
142        ));
143    }
144    let restored = FunctionBinding::dispatched(FunctionDispatch::Pattern {
145        operator,
146        oid: None,
147    });
148    name.clone_from(&restored.name);
149    *binding = Some(restored);
150    true
151}
152
153fn restore_node<T: PatternNode>(
154    expression: &mut T,
155    negative_only: bool,
156    resolver: &dyn FunctionTypeResolver,
157) -> bool {
158    if let Some(inner) = expression.negated() {
159        if restore_call(inner, true, resolver) {
160            *expression = std::mem::replace(inner, T::null());
161            return true;
162        }
163    }
164    !negative_only && restore_call(expression, false, resolver)
165}
166
167/// The scalar walker visits children first. Identify legacy negative calls before replacing positive markers.
168pub fn restore_query(plan: &mut QueryPlan, resolver: &dyn FunctionTypeResolver) -> bool {
169    let mut changed = false;
170    for negative_only in [true, false] {
171        plan.rewrite_scalar_expressions(&mut |expression| {
172            changed |= restore_node(expression, negative_only, resolver);
173        });
174    }
175    changed
176}
177
178/// Keep analyzed rule constants and subquery identities while upgrading their pattern nodes.
179pub fn restore_expression_plan(
180    plan: &mut crate::plan::ExpressionPlan,
181    resolver: &dyn FunctionTypeResolver,
182) -> bool {
183    let mut changed = false;
184    for negative_only in [true, false] {
185        crate::plan::rewrite_scalar_expression(&mut plan.scalar, &mut |expression| {
186            changed |= restore_node(expression, negative_only, resolver);
187        });
188    }
189    for subquery in &mut plan.subqueries {
190        changed |= restore_query(subquery, resolver);
191    }
192    changed
193}
194
195/// Restore retained statement syntax before binding copies, so new escape children have matching syntax sites.
196pub fn restore_statement(
197    statement: &mut crate::ast::Statement,
198    resolver: &dyn FunctionTypeResolver,
199) -> Result<bool, crate::SQLError> {
200    let mut changed = false;
201    crate::catalog::stored_ast::visit_stored_statement_expressions(statement, &mut |expression| {
202        changed |= restore_node(expression, false, resolver);
203        Ok(())
204    })?;
205    Ok(changed)
206}
207
208/// Stored syntax traversal visits parents first, retaining outer negation before its child changes.
209pub fn restore_expression(
210    expression: &mut Expr,
211    resolver: &dyn FunctionTypeResolver,
212) -> Result<bool, String> {
213    let mut changed = false;
214    crate::catalog::stored_ast::visit_stored_expression(expression, &mut |expression| {
215        changed |= restore_node(expression, false, resolver);
216        Ok(())
217    })
218    .map_err(|error| error.to_string())?;
219    Ok(changed)
220}
221
222#[derive(Default)]
223pub struct SchemaPatternMigration {
224    pub changed: bool,
225    pub generated: bool,
226}
227
228/// The retained-resource adapter uses these flags to rebind and publish in its existing initial-open transaction.
229pub fn restore_schema(
230    columns: &mut [ColumnDef],
231    checks: &mut [TableCheck],
232    resolver: &dyn FunctionTypeResolver,
233) -> Result<SchemaPatternMigration, String> {
234    let mut migration = SchemaPatternMigration::default();
235    for column in columns {
236        for expression in [&mut column.default, &mut column.check]
237            .into_iter()
238            .flatten()
239        {
240            migration.changed |= restore_expression(expression, resolver)?;
241        }
242        if let Some(generated) = &mut column.generated {
243            migration.generated |= restore_expression(&mut generated.expression, resolver)?;
244        }
245    }
246    for check in checks {
247        migration.changed |= restore_expression(&mut check.expr, resolver)?;
248    }
249    migration.changed |= migration.generated;
250    Ok(migration)
251}
252
253pub(crate) fn requires_binding(binding: &FunctionBinding) -> bool {
254    matches!(
255        binding.dispatch,
256        Some(FunctionDispatch::Pattern { oid: None, .. })
257    ) || matches!(
258        binding.dispatch,
259        Some(FunctionDispatch::PatternEscape { .. })
260    ) && binding.argument_types.is_empty()
261}
262
263#[cfg(test)]
264mod tests;