Skip to main content

nodedb_sql/planner/
catalog_fold.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Plan-time constant folding for catalog-dependent expressions.
4//!
5//! `fold_catalog_exprs_in_plan` walks a `SqlPlan` and replaces
6//! `Cast { expr: Literal(String(s)), to_type: "regclass" }` and
7//! `Cast { expr: Literal(String(s)), to_type: "regtype" }` nodes with
8//! their resolved OID literals using the `SqlCatalog` trait.  This keeps
9//! the data-plane evaluator pure (no catalog/session context) while still
10//! supporting the `'name'::regclass` / `'name'::regtype` PostgreSQL idiom.
11
12use nodedb_types::DatabaseId;
13
14use crate::catalog::SqlCatalog;
15use crate::types::{Filter, FilterExpr, MergePlanAction, SqlExpr, SqlPlan};
16
17use super::catalog_expr_fold::fold_expr;
18use super::catalog_plan_shapes::{fold_aggregates, fold_projection, fold_sort_keys, fold_windows};
19use super::catalog_plan_validate::validate_catalog_exprs;
20
21/// Walk every `Filter` in `plan` and fold catalog-dependent cast expressions
22/// to their constant OID equivalents.
23///
24/// Mutates the plan in-place (via owned `SqlPlan`). The caller owns the plan
25/// after `plan_sql` returns; this pass runs between planning and physical
26/// conversion where the catalog is still available.
27pub fn fold_catalog_exprs_in_plan(
28    plan: SqlPlan,
29    catalog: &dyn SqlCatalog,
30    database_id: DatabaseId,
31    tenant_id: u64,
32) -> crate::Result<SqlPlan> {
33    validate_catalog_exprs(&plan, catalog, database_id, tenant_id)?;
34    Ok(walk_plan(plan, catalog, database_id, tenant_id))
35}
36
37fn walk_plan(
38    plan: SqlPlan,
39    catalog: &dyn SqlCatalog,
40    database_id: DatabaseId,
41    tenant_id: u64,
42) -> SqlPlan {
43    match plan {
44        SqlPlan::Scan {
45            collection,
46            alias,
47            engine,
48            mut filters,
49            mut projection,
50            mut sort_keys,
51            limit,
52            offset,
53            distinct,
54            mut window_functions,
55            temporal,
56        } => {
57            for f in &mut filters {
58                fold_filter(f, catalog, database_id, tenant_id);
59            }
60            fold_projection(&mut projection, catalog, database_id, tenant_id);
61            fold_sort_keys(&mut sort_keys, catalog, database_id, tenant_id);
62            fold_windows(&mut window_functions, catalog, database_id, tenant_id);
63            SqlPlan::Scan {
64                collection,
65                alias,
66                engine,
67                filters,
68                projection,
69                sort_keys,
70                limit,
71                offset,
72                distinct,
73                window_functions,
74                temporal,
75            }
76        }
77
78        SqlPlan::Union { inputs, distinct } => SqlPlan::Union {
79            inputs: inputs
80                .into_iter()
81                .map(|input| walk_plan(input, catalog, database_id, tenant_id))
82                .collect(),
83            distinct,
84        },
85
86        SqlPlan::Intersect { left, right, all } => SqlPlan::Intersect {
87            left: Box::new(walk_plan(*left, catalog, database_id, tenant_id)),
88            right: Box::new(walk_plan(*right, catalog, database_id, tenant_id)),
89            all,
90        },
91
92        SqlPlan::Except { left, right, all } => SqlPlan::Except {
93            left: Box::new(walk_plan(*left, catalog, database_id, tenant_id)),
94            right: Box::new(walk_plan(*right, catalog, database_id, tenant_id)),
95            all,
96        },
97
98        SqlPlan::Cte { definitions, outer } => SqlPlan::Cte {
99            definitions: definitions
100                .into_iter()
101                .map(|(name, plan)| (name, walk_plan(plan, catalog, database_id, tenant_id)))
102                .collect(),
103            outer: Box::new(walk_plan(*outer, catalog, database_id, tenant_id)),
104        },
105
106        SqlPlan::Join {
107            left,
108            right,
109            on,
110            join_type,
111            condition,
112            limit,
113            mut projection,
114            mut filters,
115        } => {
116            for f in &mut filters {
117                fold_filter(f, catalog, database_id, tenant_id);
118            }
119            let condition = condition.map(|e| fold_expr(e, catalog, database_id, tenant_id));
120            fold_projection(&mut projection, catalog, database_id, tenant_id);
121            SqlPlan::Join {
122                left: Box::new(walk_plan(*left, catalog, database_id, tenant_id)),
123                right: Box::new(walk_plan(*right, catalog, database_id, tenant_id)),
124                on,
125                join_type,
126                condition,
127                limit,
128                projection,
129                filters,
130            }
131        }
132
133        mut plan @ (SqlPlan::PointGet { .. } | SqlPlan::RangeScan { .. }) => {
134            match &mut plan {
135                SqlPlan::PointGet { projection, .. } | SqlPlan::RangeScan { projection, .. } => {
136                    fold_projection(projection, catalog, database_id, tenant_id);
137                }
138                _ => unreachable!(),
139            }
140            plan
141        }
142
143        mut plan @ (SqlPlan::DocumentIndexLookup { .. }
144        | SqlPlan::Update { .. }
145        | SqlPlan::Delete { .. }) => {
146            match &mut plan {
147                SqlPlan::DocumentIndexLookup {
148                    filters,
149                    projection,
150                    sort_keys,
151                    window_functions,
152                    ..
153                } => {
154                    for filter in filters {
155                        fold_filter(filter, catalog, database_id, tenant_id);
156                    }
157                    fold_projection(projection, catalog, database_id, tenant_id);
158                    fold_sort_keys(sort_keys, catalog, database_id, tenant_id);
159                    fold_windows(window_functions, catalog, database_id, tenant_id);
160                }
161                SqlPlan::Delete { filters, .. } => {
162                    for filter in filters {
163                        fold_filter(filter, catalog, database_id, tenant_id);
164                    }
165                }
166                SqlPlan::Update {
167                    assignments,
168                    filters,
169                    ..
170                } => {
171                    for (_, expr) in assignments {
172                        let owned = std::mem::replace(expr, SqlExpr::Wildcard);
173                        *expr = fold_expr(owned, catalog, database_id, tenant_id);
174                    }
175                    for filter in filters {
176                        fold_filter(filter, catalog, database_id, tenant_id);
177                    }
178                }
179                _ => unreachable!(),
180            }
181            plan
182        }
183
184        SqlPlan::UpdateFrom {
185            collection,
186            engine,
187            source,
188            target_join_col,
189            source_join_col,
190            mut assignments,
191            mut target_filters,
192            returning,
193        } => {
194            for (_, expr) in &mut assignments {
195                let owned = std::mem::replace(expr, SqlExpr::Wildcard);
196                *expr = fold_expr(owned, catalog, database_id, tenant_id);
197            }
198            for filter in &mut target_filters {
199                fold_filter(filter, catalog, database_id, tenant_id);
200            }
201            SqlPlan::UpdateFrom {
202                collection,
203                engine,
204                source: Box::new(walk_plan(*source, catalog, database_id, tenant_id)),
205                target_join_col,
206                source_join_col,
207                assignments,
208                target_filters,
209                returning,
210            }
211        }
212
213        SqlPlan::InsertSelect {
214            target,
215            source,
216            limit,
217        } => SqlPlan::InsertSelect {
218            target,
219            source: Box::new(walk_plan(*source, catalog, database_id, tenant_id)),
220            limit,
221        },
222
223        SqlPlan::Aggregate {
224            input,
225            mut group_by,
226            group_by_aliases,
227            output_order,
228            mut aggregates,
229            mut having,
230            limit,
231            grouping_sets,
232            mut sort_keys,
233        } => {
234            for expr in &mut group_by {
235                let owned = std::mem::replace(expr, SqlExpr::Wildcard);
236                *expr = fold_expr(owned, catalog, database_id, tenant_id);
237            }
238            fold_aggregates(&mut aggregates, catalog, database_id, tenant_id);
239            for filter in &mut having {
240                fold_filter(filter, catalog, database_id, tenant_id);
241            }
242            fold_sort_keys(&mut sort_keys, catalog, database_id, tenant_id);
243            SqlPlan::Aggregate {
244                input: Box::new(walk_plan(*input, catalog, database_id, tenant_id)),
245                group_by,
246                group_by_aliases,
247                output_order,
248                aggregates,
249                having,
250                limit,
251                grouping_sets,
252                sort_keys,
253            }
254        }
255
256        SqlPlan::LateralTopK {
257            outer,
258            outer_alias,
259            inner_collection,
260            mut inner_filters,
261            mut inner_order_by,
262            inner_limit,
263            correlation_keys,
264            lateral_alias,
265            mut projection,
266            left_join,
267        } => {
268            for filter in &mut inner_filters {
269                fold_filter(filter, catalog, database_id, tenant_id);
270            }
271            fold_sort_keys(&mut inner_order_by, catalog, database_id, tenant_id);
272            fold_projection(&mut projection, catalog, database_id, tenant_id);
273            SqlPlan::LateralTopK {
274                outer: Box::new(walk_plan(*outer, catalog, database_id, tenant_id)),
275                outer_alias,
276                inner_collection,
277                inner_filters,
278                inner_order_by,
279                inner_limit,
280                correlation_keys,
281                lateral_alias,
282                projection,
283                left_join,
284            }
285        }
286
287        SqlPlan::LateralLoop {
288            outer,
289            outer_alias,
290            inner,
291            correlation_predicates,
292            lateral_alias,
293            mut projection,
294            outer_row_cap,
295            left_join,
296        } => {
297            fold_projection(&mut projection, catalog, database_id, tenant_id);
298            SqlPlan::LateralLoop {
299                outer: Box::new(walk_plan(*outer, catalog, database_id, tenant_id)),
300                outer_alias,
301                inner: Box::new(walk_plan(*inner, catalog, database_id, tenant_id)),
302                correlation_predicates,
303                lateral_alias,
304                projection,
305                outer_row_cap,
306                left_join,
307            }
308        }
309
310        SqlPlan::Merge {
311            target,
312            engine,
313            source,
314            target_join_col,
315            source_join_col,
316            source_alias,
317            mut clauses,
318            returning,
319        } => {
320            for clause in &mut clauses {
321                for filter in &mut clause.extra_predicate {
322                    fold_filter(filter, catalog, database_id, tenant_id);
323                }
324                match &mut clause.action {
325                    MergePlanAction::Update { assignments } => {
326                        for (_, expr) in assignments {
327                            let owned = std::mem::replace(expr, SqlExpr::Wildcard);
328                            *expr = fold_expr(owned, catalog, database_id, tenant_id);
329                        }
330                    }
331                    MergePlanAction::Insert { values, .. } => {
332                        for expr in values {
333                            let owned = std::mem::replace(expr, SqlExpr::Wildcard);
334                            *expr = fold_expr(owned, catalog, database_id, tenant_id);
335                        }
336                    }
337                    MergePlanAction::Delete | MergePlanAction::DoNothing => {}
338                }
339            }
340            SqlPlan::Merge {
341                target,
342                engine,
343                source: Box::new(walk_plan(*source, catalog, database_id, tenant_id)),
344                target_join_col,
345                source_join_col,
346                source_alias,
347                clauses,
348                returning,
349            }
350        }
351
352        mut plan @ (SqlPlan::VectorSearch { .. }
353        | SqlPlan::TextSearch { .. }
354        | SqlPlan::SpatialScan { .. }
355        | SqlPlan::RecursiveScan { .. }) => {
356            match &mut plan {
357                SqlPlan::VectorSearch {
358                    filters,
359                    projection,
360                    ..
361                } => {
362                    for filter in filters {
363                        fold_filter(filter, catalog, database_id, tenant_id);
364                    }
365                    fold_projection(projection, catalog, database_id, tenant_id);
366                }
367                SqlPlan::TextSearch {
368                    filters,
369                    projection,
370                    ..
371                } => {
372                    for filter in filters {
373                        fold_filter(filter, catalog, database_id, tenant_id);
374                    }
375                    fold_projection(projection, catalog, database_id, tenant_id);
376                }
377                SqlPlan::SpatialScan {
378                    attribute_filters,
379                    projection,
380                    ..
381                } => {
382                    for filter in attribute_filters {
383                        fold_filter(filter, catalog, database_id, tenant_id);
384                    }
385                    fold_projection(projection, catalog, database_id, tenant_id);
386                }
387                SqlPlan::RecursiveScan {
388                    base_filters,
389                    recursive_filters,
390                    projection,
391                    ..
392                } => {
393                    for filter in base_filters {
394                        fold_filter(filter, catalog, database_id, tenant_id);
395                    }
396                    for filter in recursive_filters {
397                        fold_filter(filter, catalog, database_id, tenant_id);
398                    }
399                    fold_projection(projection, catalog, database_id, tenant_id);
400                }
401                _ => unreachable!(),
402            }
403            plan
404        }
405
406        mut plan @ (SqlPlan::MultiVectorSearch { .. }
407        | SqlPlan::SparseSearch { .. }
408        | SqlPlan::HybridSearch { .. }
409        | SqlPlan::HybridSearchTriple { .. }) => {
410            match &mut plan {
411                SqlPlan::MultiVectorSearch { projection, .. }
412                | SqlPlan::SparseSearch { projection, .. }
413                | SqlPlan::HybridSearch { projection, .. }
414                | SqlPlan::HybridSearchTriple { projection, .. } => {
415                    fold_projection(projection, catalog, database_id, tenant_id);
416                }
417                _ => unreachable!(),
418            }
419            plan
420        }
421
422        // Plan variants without expression-bearing filters pass through unchanged.
423        other => other,
424    }
425}
426
427fn fold_filter(
428    filter: &mut Filter,
429    catalog: &dyn SqlCatalog,
430    database_id: DatabaseId,
431    tenant_id: u64,
432) {
433    fold_filter_expr(&mut filter.expr, catalog, database_id, tenant_id);
434}
435
436fn fold_filter_expr(
437    expr: &mut FilterExpr,
438    catalog: &dyn SqlCatalog,
439    database_id: DatabaseId,
440    tenant_id: u64,
441) {
442    match expr {
443        FilterExpr::Expr(sql_expr) => {
444            let owned = std::mem::replace(sql_expr, SqlExpr::Wildcard);
445            *sql_expr = fold_expr(owned, catalog, database_id, tenant_id);
446        }
447        FilterExpr::And(children) | FilterExpr::Or(children) => {
448            for child in children {
449                fold_filter_expr(&mut child.expr, catalog, database_id, tenant_id);
450            }
451        }
452        FilterExpr::Not(child) => {
453            fold_filter_expr(&mut child.expr, catalog, database_id, tenant_id);
454        }
455        // Simple comparison, InList, Between, IsNull, IsNotNull — no sub-expressions to fold.
456        FilterExpr::Comparison { .. }
457        | FilterExpr::InList { .. }
458        | FilterExpr::Between { .. }
459        | FilterExpr::IsNull { .. }
460        | FilterExpr::IsNotNull { .. } => {}
461    }
462}