Skip to main content

nodedb_sql/planner/cte/
recursive_scan.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Entry point and collection-backed recursive-scan planning for
4//! `WITH RECURSIVE` CTEs.
5
6use sqlparser::ast::{self, Query, SetExpr};
7
8use crate::error::{Result, SqlError};
9use crate::functions::registry::FunctionRegistry;
10use crate::reserved::check_ast_identifier;
11use crate::types::*;
12
13use super::recursive_value::plan_recursive_value;
14use super::validate::{count_select_cols, validate_self_ref_count};
15
16/// Default maximum recursion depth for WITH RECURSIVE queries.
17pub const DEFAULT_MAX_RECURSION_DEPTH: usize = 1000;
18
19/// Plan a WITH RECURSIVE query.
20///
21/// Dispatches to either `plan_recursive_scan` (collection-backed) or
22/// `plan_recursive_value` (pure expression / value-generating) based on
23/// whether the anchor arm references a real collection.
24pub fn plan_recursive_cte(
25    query: &Query,
26    catalog: &dyn SqlCatalog,
27    functions: &FunctionRegistry,
28    temporal: crate::TemporalScope,
29) -> Result<SqlPlan> {
30    let with = query.with.as_ref().ok_or_else(|| SqlError::Parse {
31        detail: "expected WITH clause".into(),
32    })?;
33
34    let cte = with.cte_tables.first().ok_or_else(|| SqlError::Parse {
35        detail: "empty WITH clause".into(),
36    })?;
37
38    let cte_name = check_ast_identifier(&cte.alias.name)?;
39    let declared_columns: Vec<String> = cte
40        .alias
41        .columns
42        .iter()
43        .map(|column| check_ast_identifier(&column.name))
44        .collect::<Result<_>>()?;
45
46    let cte_query = &cte.query;
47
48    // Validate set operator: only UNION / UNION ALL permitted.
49    let (left, right, set_quantifier) = match &*cte_query.body {
50        SetExpr::SetOperation {
51            op: ast::SetOperator::Union,
52            left,
53            right,
54            set_quantifier,
55        } => (left, right, set_quantifier),
56        SetExpr::SetOperation { op, .. } => {
57            return Err(SqlError::InvalidRecursiveSetOp {
58                op: format!("{op}"),
59            });
60        }
61        _ => {
62            return Err(SqlError::InvalidRecursiveSetOp {
63                op: "non-set-operation".into(),
64            });
65        }
66    };
67
68    // Validate every relation name and alias before any branch-planning
69    // fallback. Recursive planning intentionally interprets some planning
70    // failures as working-table references, so identifier failures must be
71    // rejected first rather than swallowed by that fallback.
72    validate_relation_identifiers(left)?;
73    validate_relation_identifiers(right)?;
74
75    // Validate self-reference count in the recursive arm.
76    validate_self_ref_count(right, &cte_name)?;
77
78    let distinct = !matches!(set_quantifier, ast::SetQuantifier::All);
79
80    // Classify value-generating anchors syntactically. Never reinterpret a
81    // planning error as a different CTE shape: doing so would swallow invalid
82    // relation names and other fail-closed planner errors.
83    let SetExpr::Select(anchor_select) = left.as_ref() else {
84        return plan_recursive_value(left, right, &cte_name, &declared_columns, distinct);
85    };
86    if anchor_select.from.is_empty() {
87        return plan_recursive_value(left, right, &cte_name, &declared_columns, distinct);
88    }
89
90    let base = plan_cte_branch(left, catalog, functions, temporal)?;
91    let collection = extract_collection(&base);
92    if collection.is_empty() {
93        return Err(SqlError::Unsupported {
94            detail: "collection-backed recursive CTE anchor did not produce a relation".into(),
95        });
96    }
97    plan_recursive_scan_from_parts(
98        &cte_name,
99        &base,
100        &RecursiveParts {
101            left,
102            right,
103            declared_columns: &declared_columns,
104            distinct,
105        },
106        catalog,
107        functions,
108        temporal,
109    )
110}
111
112fn validate_relation_identifiers(expr: &SetExpr) -> Result<()> {
113    match expr {
114        SetExpr::Select(select) => {
115            for from in &select.from {
116                validate_table_factor(&from.relation)?;
117                for join in &from.joins {
118                    validate_table_factor(&join.relation)?;
119                }
120            }
121            Ok(())
122        }
123        SetExpr::SetOperation { left, right, .. } => {
124            validate_relation_identifiers(left)?;
125            validate_relation_identifiers(right)
126        }
127        SetExpr::Query(query) => validate_query_identifiers(query),
128        _ => Ok(()),
129    }
130}
131
132fn validate_query_identifiers(query: &Query) -> Result<()> {
133    if let Some(with) = &query.with {
134        for cte in &with.cte_tables {
135            check_ast_identifier(&cte.alias.name)?;
136            for column in &cte.alias.columns {
137                check_ast_identifier(&column.name)?;
138            }
139            validate_query_identifiers(&cte.query)?;
140        }
141    }
142    validate_relation_identifiers(&query.body)
143}
144
145fn validate_table_factor(factor: &ast::TableFactor) -> Result<()> {
146    match factor {
147        ast::TableFactor::Table { .. } => {
148            crate::parser::normalize::table_name_from_factor(factor)?;
149            Ok(())
150        }
151        ast::TableFactor::Derived {
152            subquery, alias, ..
153        } => {
154            if let Some(alias) = alias {
155                crate::reserved::check_ast_identifier(&alias.name)?;
156            }
157            validate_query_identifiers(subquery)
158        }
159        _ => Ok(()),
160    }
161}
162
163// ── Collection-backed recursive scan ─────────────────────────────────────────
164
165struct RecursiveParts<'a> {
166    left: &'a SetExpr,
167    right: &'a SetExpr,
168    declared_columns: &'a [String],
169    distinct: bool,
170}
171
172fn plan_recursive_scan_from_parts(
173    cte_name: &str,
174    base: &SqlPlan,
175    parts: &RecursiveParts<'_>,
176    catalog: &dyn SqlCatalog,
177    functions: &FunctionRegistry,
178    temporal: crate::TemporalScope,
179) -> Result<SqlPlan> {
180    let RecursiveParts {
181        left,
182        right,
183        declared_columns,
184        distinct,
185    } = parts;
186    let collection = extract_collection(base);
187
188    // Validate column count if columns were declared.
189    if !declared_columns.is_empty() {
190        let anchor_cols = count_select_cols(left);
191        if anchor_cols != 0 && anchor_cols != declared_columns.len() {
192            return Err(SqlError::RecursiveColumnMismatch {
193                cte_name: cte_name.to_owned(),
194                anchor_cols,
195                declared_cols: declared_columns.len(),
196            });
197        }
198    }
199
200    // The recursive arm contains the working-table self-reference, which is
201    // intentionally absent from the ordinary catalog. Parse that supported
202    // shape directly instead of attempting ordinary planning and swallowing
203    // whichever error happens to occur first.
204    let (recursive_filters, join_link) =
205        super::join_link::extract_recursive_info(right, cte_name, catalog, functions, temporal)?;
206
207    // The anchor plan carries the CTE's resolved output columns; propagate
208    // them so the recursive scan self-describes its output schema.
209    let projection = match base {
210        SqlPlan::Scan { projection, .. } | SqlPlan::Join { projection, .. } => projection.clone(),
211        _ => Vec::new(),
212    };
213
214    Ok(SqlPlan::RecursiveScan(RecursiveScanPlan {
215        collection,
216        base_filters: extract_filters(base),
217        recursive_filters,
218        join_link,
219        max_iterations: DEFAULT_MAX_RECURSION_DEPTH,
220        distinct: *distinct,
221        // No LIMIT returns every row of the recursion; an explicit LIMIT
222        // overwrites this. The recursion depth is bounded by
223        // `max_iterations`, which fails loudly when passed.
224        limit: usize::MAX,
225        projection,
226    }))
227}
228
229pub(super) fn plan_cte_branch(
230    expr: &SetExpr,
231    catalog: &dyn SqlCatalog,
232    functions: &FunctionRegistry,
233    temporal: crate::TemporalScope,
234) -> Result<SqlPlan> {
235    match expr {
236        SetExpr::Select(select) => {
237            let query = Query {
238                with: None,
239                body: Box::new(SetExpr::Select(select.clone())),
240                order_by: None,
241                limit_clause: None,
242                fetch: None,
243                locks: Vec::new(),
244                for_clause: None,
245                settings: None,
246                format_clause: None,
247                pipe_operators: Vec::new(),
248            };
249            crate::planner::select::plan_query(&query, catalog, functions, temporal)
250        }
251        _ => Err(SqlError::Unsupported {
252            detail: "CTE branch must be SELECT".into(),
253        }),
254    }
255}
256
257pub(super) fn extract_collection(plan: &SqlPlan) -> String {
258    match plan {
259        SqlPlan::Scan { collection, .. } => collection.clone(),
260        _ => String::new(),
261    }
262}
263
264pub(super) fn extract_filters(plan: &SqlPlan) -> Vec<Filter> {
265    match plan {
266        SqlPlan::Scan { filters, .. } => filters.clone(),
267        _ => Vec::new(),
268    }
269}