Skip to main content

nodedb_sql/planner/
catalog_expr_fold.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Recursive catalog-dependent expression validation and folding.
4
5use nodedb_types::DatabaseId;
6
7use crate::catalog::SqlCatalog;
8use crate::functions::registry::FunctionRegistry;
9use crate::planner::const_fold::FoldScope;
10use crate::types::{SqlExpr, SqlValue};
11
12pub(super) fn eval_catalog_constant(
13    expr: &SqlExpr,
14    catalog: &dyn SqlCatalog,
15    functions: &FunctionRegistry,
16) -> crate::Result<SqlValue> {
17    if let SqlExpr::Cast {
18        expr: inner,
19        to_type,
20    } = expr
21        && to_type.eq_ignore_ascii_case("regclass")
22        && let SqlExpr::Literal(SqlValue::String(name)) = inner.as_ref()
23    {
24        let normalized = crate::catalog::normalize_regclass_name(name)
25            .ok_or_else(|| crate::SqlError::UnknownTable { name: name.clone() })?;
26        catalog
27            .resolve_regclass(DatabaseId::DEFAULT, 0, name)
28            .ok_or_else(|| crate::SqlError::UnknownTable {
29                name: normalized.clone(),
30            })?;
31        return Ok(SqlValue::String(normalized));
32    }
33    if let Some(value) = eval_sequence_accessor(expr, catalog)? {
34        return Ok(value);
35    }
36    // A from-less SELECT's plan is marked volatile when it holds a volatile
37    // call, so it is never cached. Evaluating the call here therefore serves
38    // this execution only, and the next execution re-plans and re-evaluates.
39    Ok(
40        crate::planner::const_fold::fold_constant_scoped(expr, functions, FoldScope::Once)?
41            .unwrap_or(SqlValue::Null),
42    )
43}
44
45/// Route `nextval` / `currval` / `setval` to the catalog's sequence state.
46///
47/// Returns `Ok(None)` for every other expression. These calls are `Volatile`,
48/// so the constant folder never reaches them and the plan holding the result
49/// is never cached — each execution re-plans and allocates again.
50pub(super) fn eval_sequence_accessor(
51    expr: &SqlExpr,
52    catalog: &dyn SqlCatalog,
53) -> crate::Result<Option<SqlValue>> {
54    let SqlExpr::Function { name, args, .. } = expr else {
55        return Ok(None);
56    };
57    let lowered = name.to_ascii_lowercase();
58    if !crate::functions::sequence_accessor::is_sequence_accessor(&lowered) {
59        return Ok(None);
60    }
61    let sequence = match args.first() {
62        Some(SqlExpr::Literal(SqlValue::String(name))) => name.as_str(),
63        _ => {
64            return Err(crate::SqlError::Arity {
65                detail: format!("{lowered} requires a literal sequence name"),
66            });
67        }
68    };
69    let value = match lowered.as_str() {
70        "nextval" => catalog.sequence_nextval(DatabaseId::DEFAULT, 0, sequence)?,
71        "currval" => catalog.sequence_currval(DatabaseId::DEFAULT, 0, sequence)?,
72        _ => {
73            let Some(SqlExpr::Literal(SqlValue::Int(target))) = args.get(1) else {
74                return Err(crate::SqlError::Arity {
75                    detail: "setval requires a literal bigint second argument".into(),
76                });
77            };
78            catalog.sequence_setval(DatabaseId::DEFAULT, 0, sequence, *target)?
79        }
80    };
81    Ok(Some(SqlValue::Int(value)))
82}
83
84pub(super) fn validate_expr(
85    expr: &SqlExpr,
86    catalog: &dyn SqlCatalog,
87    database_id: DatabaseId,
88    tenant_id: u64,
89) -> crate::Result<()> {
90    match expr {
91        SqlExpr::Cast { expr, to_type }
92            if to_type.eq_ignore_ascii_case("regclass")
93                && matches!(expr.as_ref(), SqlExpr::Literal(SqlValue::String(_))) =>
94        {
95            let SqlExpr::Literal(SqlValue::String(name)) = expr.as_ref() else {
96                unreachable!();
97            };
98            if catalog
99                .resolve_regclass(database_id, tenant_id, name)
100                .is_none()
101            {
102                return Err(crate::SqlError::UnknownTable { name: name.clone() });
103            }
104            Ok(())
105        }
106        SqlExpr::Cast { expr, .. }
107        | SqlExpr::IsNull { expr, .. }
108        | SqlExpr::UnaryOp { expr, .. } => validate_expr(expr, catalog, database_id, tenant_id),
109        SqlExpr::BinaryOp { left, right, .. } => {
110            validate_expr(left, catalog, database_id, tenant_id)?;
111            validate_expr(right, catalog, database_id, tenant_id)
112        }
113        SqlExpr::InList { expr, list, .. } => {
114            validate_expr(expr, catalog, database_id, tenant_id)?;
115            for item in list {
116                validate_expr(item, catalog, database_id, tenant_id)?;
117            }
118            Ok(())
119        }
120        SqlExpr::ArrayLiteral(items) | SqlExpr::Function { args: items, .. } => {
121            for item in items {
122                validate_expr(item, catalog, database_id, tenant_id)?;
123            }
124            Ok(())
125        }
126        SqlExpr::Between {
127            expr, low, high, ..
128        } => {
129            validate_expr(expr, catalog, database_id, tenant_id)?;
130            validate_expr(low, catalog, database_id, tenant_id)?;
131            validate_expr(high, catalog, database_id, tenant_id)
132        }
133        SqlExpr::Like { expr, pattern, .. } => {
134            validate_expr(expr, catalog, database_id, tenant_id)?;
135            validate_expr(pattern, catalog, database_id, tenant_id)
136        }
137        SqlExpr::Case {
138            operand,
139            when_then,
140            else_expr,
141        } => {
142            if let Some(operand) = operand {
143                validate_expr(operand, catalog, database_id, tenant_id)?;
144            }
145            for (when, then) in when_then {
146                validate_expr(when, catalog, database_id, tenant_id)?;
147                validate_expr(then, catalog, database_id, tenant_id)?;
148            }
149            if let Some(expr) = else_expr {
150                validate_expr(expr, catalog, database_id, tenant_id)?;
151            }
152            Ok(())
153        }
154        _ => Ok(()),
155    }
156}
157
158/// Recursively fold catalog-dependent expressions to constants.
159pub(super) fn fold_expr(
160    expr: SqlExpr,
161    catalog: &dyn SqlCatalog,
162    database_id: DatabaseId,
163    tenant_id: u64,
164) -> SqlExpr {
165    match expr {
166        SqlExpr::Cast {
167            expr: inner_expr,
168            to_type,
169        } => {
170            let upper = to_type.to_ascii_uppercase();
171            if upper == "REGCLASS" {
172                if let SqlExpr::Literal(SqlValue::String(ref name)) = *inner_expr
173                    && let Some(oid) = catalog.resolve_regclass(database_id, tenant_id, name)
174                {
175                    return SqlExpr::Literal(SqlValue::Int(oid));
176                }
177            } else if upper == "REGTYPE"
178                && let SqlExpr::Literal(SqlValue::String(ref name)) = *inner_expr
179                && let Some(oid) = catalog.resolve_regtype(name)
180            {
181                return SqlExpr::Literal(SqlValue::Int(oid));
182            }
183            SqlExpr::Cast {
184                expr: Box::new(fold_expr(*inner_expr, catalog, database_id, tenant_id)),
185                to_type,
186            }
187        }
188        SqlExpr::BinaryOp { left, op, right } => SqlExpr::BinaryOp {
189            left: Box::new(fold_expr(*left, catalog, database_id, tenant_id)),
190            op,
191            right: Box::new(fold_expr(*right, catalog, database_id, tenant_id)),
192        },
193        SqlExpr::InList {
194            expr,
195            list,
196            negated,
197        } => SqlExpr::InList {
198            expr: Box::new(fold_expr(*expr, catalog, database_id, tenant_id)),
199            list: list
200                .into_iter()
201                .map(|e| fold_expr(e, catalog, database_id, tenant_id))
202                .collect(),
203            negated,
204        },
205        SqlExpr::IsNull { expr, negated } => SqlExpr::IsNull {
206            expr: Box::new(fold_expr(*expr, catalog, database_id, tenant_id)),
207            negated,
208        },
209        SqlExpr::UnaryOp { op, expr } => SqlExpr::UnaryOp {
210            op,
211            expr: Box::new(fold_expr(*expr, catalog, database_id, tenant_id)),
212        },
213        SqlExpr::ArrayLiteral(elems) => SqlExpr::ArrayLiteral(
214            elems
215                .into_iter()
216                .map(|e| fold_expr(e, catalog, database_id, tenant_id))
217                .collect(),
218        ),
219        SqlExpr::Function {
220            name,
221            args,
222            distinct,
223        } => SqlExpr::Function {
224            name,
225            args: args
226                .into_iter()
227                .map(|arg| fold_expr(arg, catalog, database_id, tenant_id))
228                .collect(),
229            distinct,
230        },
231        SqlExpr::Between {
232            expr,
233            low,
234            high,
235            negated,
236        } => SqlExpr::Between {
237            expr: Box::new(fold_expr(*expr, catalog, database_id, tenant_id)),
238            low: Box::new(fold_expr(*low, catalog, database_id, tenant_id)),
239            high: Box::new(fold_expr(*high, catalog, database_id, tenant_id)),
240            negated,
241        },
242        SqlExpr::Like {
243            expr,
244            pattern,
245            negated,
246            case_insensitive,
247        } => SqlExpr::Like {
248            expr: Box::new(fold_expr(*expr, catalog, database_id, tenant_id)),
249            pattern: Box::new(fold_expr(*pattern, catalog, database_id, tenant_id)),
250            negated,
251            case_insensitive,
252        },
253        SqlExpr::Case {
254            operand,
255            when_then,
256            else_expr,
257        } => SqlExpr::Case {
258            operand: operand.map(|e| Box::new(fold_expr(*e, catalog, database_id, tenant_id))),
259            when_then: when_then
260                .into_iter()
261                .map(|(when, then)| {
262                    (
263                        fold_expr(when, catalog, database_id, tenant_id),
264                        fold_expr(then, catalog, database_id, tenant_id),
265                    )
266                })
267                .collect(),
268            else_expr: else_expr.map(|e| Box::new(fold_expr(*e, catalog, database_id, tenant_id))),
269        },
270        leaf => leaf,
271    }
272}