Skip to main content

nodedb_sql/planner/
catalog_plan_validate.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Catalog-dependent expression validation across nested SQL plan shapes.
4
5use crate::types::{
6    CtePlan, DocumentIndexLookupPlan, HybridSearchPlan, HybridSearchTriplePlan, LateralLoopPlan,
7    LateralTopKPlan, MergePlan, RangeScanPlan, RecursiveScanPlan, TextSearchPlan,
8    VectorPrimaryDeletePlan, VectorPrimaryUpdatePlan,
9};
10use nodedb_types::DatabaseId;
11
12use crate::catalog::SqlCatalog;
13use crate::types::{FilterExpr, MergePlanAction, SqlPlan};
14
15use super::catalog_expr_fold::validate_expr;
16use super::catalog_plan_shapes::{
17    validate_aggregates, validate_projection, validate_sort_keys, validate_windows,
18};
19
20pub(super) fn validate_catalog_exprs(
21    plan: &SqlPlan,
22    catalog: &dyn SqlCatalog,
23    database_id: DatabaseId,
24    tenant_id: u64,
25) -> crate::Result<()> {
26    match plan {
27        SqlPlan::Scan {
28            filters,
29            projection,
30            sort_keys,
31            window_functions,
32            ..
33        }
34        | SqlPlan::DocumentIndexLookup(DocumentIndexLookupPlan {
35            filters,
36            projection,
37            sort_keys,
38            window_functions,
39            ..
40        }) => {
41            validate_filters(filters, catalog, database_id, tenant_id)?;
42            validate_projection(projection, catalog, database_id, tenant_id)?;
43            validate_sort_keys(sort_keys, catalog, database_id, tenant_id)?;
44            validate_windows(window_functions, catalog, database_id, tenant_id)?;
45        }
46        SqlPlan::PointGet { projection, .. }
47        | SqlPlan::RangeScan(RangeScanPlan { projection, .. }) => {
48            validate_projection(projection, catalog, database_id, tenant_id)?;
49        }
50        SqlPlan::Update {
51            assignments,
52            filters,
53            ..
54        }
55        | SqlPlan::VectorPrimaryUpdate(VectorPrimaryUpdatePlan {
56            assignments,
57            filters,
58            ..
59        }) => {
60            for (_, expr) in assignments {
61                validate_expr(expr, catalog, database_id, tenant_id)?;
62            }
63            validate_filters(filters, catalog, database_id, tenant_id)?;
64        }
65        SqlPlan::Delete { filters, .. }
66        | SqlPlan::VectorPrimaryDelete(VectorPrimaryDeletePlan { filters, .. }) => {
67            validate_filters(filters, catalog, database_id, tenant_id)?
68        }
69        SqlPlan::UpdateFrom {
70            source,
71            assignments,
72            target_filters,
73            ..
74        } => {
75            validate_catalog_exprs(source, catalog, database_id, tenant_id)?;
76            for (_, expr) in assignments {
77                validate_expr(expr, catalog, database_id, tenant_id)?;
78            }
79            validate_filters(target_filters, catalog, database_id, tenant_id)?;
80        }
81        SqlPlan::InsertSelect { source, .. } => {
82            validate_catalog_exprs(source, catalog, database_id, tenant_id)?
83        }
84        SqlPlan::Aggregate {
85            input,
86            group_by,
87            aggregates,
88            having,
89            sort_keys,
90            ..
91        } => {
92            validate_catalog_exprs(input, catalog, database_id, tenant_id)?;
93            for expr in group_by {
94                validate_expr(expr, catalog, database_id, tenant_id)?;
95            }
96            validate_aggregates(aggregates, catalog, database_id, tenant_id)?;
97            validate_filters(having, catalog, database_id, tenant_id)?;
98            validate_sort_keys(sort_keys, catalog, database_id, tenant_id)?;
99        }
100        SqlPlan::Union { inputs, .. } => {
101            for input in inputs {
102                validate_catalog_exprs(input, catalog, database_id, tenant_id)?;
103            }
104        }
105        SqlPlan::Intersect { left, right, .. } | SqlPlan::Except { left, right, .. } => {
106            validate_catalog_exprs(left, catalog, database_id, tenant_id)?;
107            validate_catalog_exprs(right, catalog, database_id, tenant_id)?;
108        }
109        SqlPlan::Cte(CtePlan { definitions, outer }) => {
110            for (_, definition) in definitions {
111                validate_catalog_exprs(definition, catalog, database_id, tenant_id)?;
112            }
113            validate_catalog_exprs(outer, catalog, database_id, tenant_id)?;
114        }
115        // The post-processing tail's body keeps its own expressions; stopping
116        // the walk at the wrapper skips validating all of them.
117        SqlPlan::Subquery {
118            input,
119            filters,
120            projection,
121            window_functions,
122            sort_keys,
123            ..
124        } => {
125            validate_catalog_exprs(input, catalog, database_id, tenant_id)?;
126            validate_filters(filters, catalog, database_id, tenant_id)?;
127            validate_projection(projection, catalog, database_id, tenant_id)?;
128            validate_windows(window_functions, catalog, database_id, tenant_id)?;
129            validate_sort_keys(sort_keys, catalog, database_id, tenant_id)?;
130        }
131        SqlPlan::Join {
132            left,
133            right,
134            condition,
135            projection,
136            filters,
137            ..
138        } => {
139            validate_catalog_exprs(left, catalog, database_id, tenant_id)?;
140            validate_catalog_exprs(right, catalog, database_id, tenant_id)?;
141            if let Some(condition) = condition {
142                validate_expr(condition, catalog, database_id, tenant_id)?;
143            }
144            validate_projection(projection, catalog, database_id, tenant_id)?;
145            validate_filters(filters, catalog, database_id, tenant_id)?;
146        }
147        SqlPlan::LateralTopK(LateralTopKPlan {
148            outer,
149            inner_filters,
150            inner_order_by,
151            projection,
152            ..
153        }) => {
154            validate_catalog_exprs(outer, catalog, database_id, tenant_id)?;
155            validate_filters(inner_filters, catalog, database_id, tenant_id)?;
156            validate_sort_keys(inner_order_by, catalog, database_id, tenant_id)?;
157            validate_projection(projection, catalog, database_id, tenant_id)?;
158        }
159        SqlPlan::LateralLoop(LateralLoopPlan {
160            outer,
161            inner,
162            projection,
163            ..
164        }) => {
165            validate_catalog_exprs(outer, catalog, database_id, tenant_id)?;
166            validate_catalog_exprs(inner, catalog, database_id, tenant_id)?;
167            validate_projection(projection, catalog, database_id, tenant_id)?;
168        }
169        SqlPlan::Merge(MergePlan {
170            source, clauses, ..
171        }) => {
172            validate_catalog_exprs(source, catalog, database_id, tenant_id)?;
173            for clause in clauses {
174                validate_filters(&clause.extra_predicate, catalog, database_id, tenant_id)?;
175                match &clause.action {
176                    MergePlanAction::Update { assignments } => {
177                        for (_, expr) in assignments {
178                            validate_expr(expr, catalog, database_id, tenant_id)?;
179                        }
180                    }
181                    MergePlanAction::Insert { values, .. } => {
182                        for expr in values {
183                            validate_expr(expr, catalog, database_id, tenant_id)?;
184                        }
185                    }
186                    MergePlanAction::Delete | MergePlanAction::DoNothing => {}
187                }
188            }
189        }
190        SqlPlan::VectorSearch {
191            filters,
192            projection,
193            ..
194        } => {
195            validate_filters(filters, catalog, database_id, tenant_id)?;
196            validate_projection(projection, catalog, database_id, tenant_id)?;
197        }
198        SqlPlan::MultiVectorSearch { projection, .. }
199        | SqlPlan::SparseSearch { projection, .. } => {
200            validate_projection(projection, catalog, database_id, tenant_id)?;
201        }
202        SqlPlan::HybridSearch(HybridSearchPlan {
203            filters,
204            projection,
205            ..
206        })
207        | SqlPlan::HybridSearchTriple(HybridSearchTriplePlan {
208            filters,
209            projection,
210            ..
211        }) => {
212            validate_filters(filters, catalog, database_id, tenant_id)?;
213            validate_projection(projection, catalog, database_id, tenant_id)?;
214        }
215        SqlPlan::TextSearch(TextSearchPlan {
216            filters,
217            projection,
218            ..
219        }) => {
220            validate_filters(filters, catalog, database_id, tenant_id)?;
221            validate_projection(projection, catalog, database_id, tenant_id)?;
222        }
223        SqlPlan::SpatialScan {
224            attribute_filters,
225            projection,
226            ..
227        } => {
228            validate_filters(attribute_filters, catalog, database_id, tenant_id)?;
229            validate_projection(projection, catalog, database_id, tenant_id)?;
230        }
231        SqlPlan::RecursiveScan(RecursiveScanPlan {
232            base_filters,
233            recursive_filters,
234            projection,
235            ..
236        }) => {
237            validate_filters(base_filters, catalog, database_id, tenant_id)?;
238            validate_filters(recursive_filters, catalog, database_id, tenant_id)?;
239            validate_projection(projection, catalog, database_id, tenant_id)?;
240        }
241        SqlPlan::ConstantResult { .. }
242        | SqlPlan::Insert(_)
243        | SqlPlan::KvInsert(_)
244        | SqlPlan::Upsert(_)
245        | SqlPlan::Truncate { .. }
246        | SqlPlan::TimeseriesScan(_)
247        | SqlPlan::TimeseriesIngest(_)
248        | SqlPlan::RecursiveValue(_)
249        | SqlPlan::CreateArray(_)
250        | SqlPlan::DropArray { .. }
251        | SqlPlan::AlterArray(_)
252        | SqlPlan::InsertArray(_)
253        | SqlPlan::DeleteArray(_)
254        | SqlPlan::ArraySlice(_)
255        | SqlPlan::ArrayProject(_)
256        | SqlPlan::ArrayAgg(_)
257        | SqlPlan::ArrayElementwise(_)
258        | SqlPlan::ArrayFlush { .. }
259        | SqlPlan::ArrayCompact { .. }
260        | SqlPlan::VectorPrimaryInsert(_)
261        | SqlPlan::VectorPrimaryTruncate(_)
262        | SqlPlan::CreateIndex(_)
263        | SqlPlan::DropIndex(_) => {}
264    }
265    Ok(())
266}
267
268fn validate_filters(
269    filters: &[crate::types::Filter],
270    catalog: &dyn SqlCatalog,
271    database_id: DatabaseId,
272    tenant_id: u64,
273) -> crate::Result<()> {
274    for filter in filters {
275        validate_filter_expr(&filter.expr, catalog, database_id, tenant_id)?;
276    }
277    Ok(())
278}
279
280fn validate_filter_expr(
281    expr: &FilterExpr,
282    catalog: &dyn SqlCatalog,
283    database_id: DatabaseId,
284    tenant_id: u64,
285) -> crate::Result<()> {
286    match expr {
287        FilterExpr::Expr(expr) => validate_expr(expr, catalog, database_id, tenant_id),
288        FilterExpr::And(children) | FilterExpr::Or(children) => {
289            validate_filters(children, catalog, database_id, tenant_id)
290        }
291        FilterExpr::Not(child) => {
292            validate_filter_expr(&child.expr, catalog, database_id, tenant_id)
293        }
294        _ => Ok(()),
295    }
296}