nodedb_sql/planner/select/entry/
query.rs1use sqlparser::ast::{Query, SetExpr};
8
9use crate::error::{Result, SqlError};
10use crate::functions::registry::FunctionRegistry;
11use crate::planner::select::cte_catalog::CteCatalog;
12use crate::planner::select::rekey_join_output;
13use crate::reserved::check_ast_identifier;
14use crate::resolver::derived::{infer_subquery_relation, rename_output_columns};
15use crate::temporal::TemporalScope;
16use crate::types::{CtePlan, SqlCatalog, SqlPlan};
17
18pub fn plan_statement_query(
24 query: &Query,
25 catalog: &dyn SqlCatalog,
26 functions: &FunctionRegistry,
27 temporal: TemporalScope,
28) -> Result<SqlPlan> {
29 plan_query_at(query, catalog, functions, temporal, true)
30}
31
32pub fn plan_query(
36 query: &Query,
37 catalog: &dyn SqlCatalog,
38 functions: &FunctionRegistry,
39 temporal: TemporalScope,
40) -> Result<SqlPlan> {
41 plan_query_at(query, catalog, functions, temporal, false)
42}
43
44fn plan_query_at(
47 query: &Query,
48 catalog: &dyn SqlCatalog,
49 functions: &FunctionRegistry,
50 temporal: TemporalScope,
51 statement_output: bool,
52) -> Result<SqlPlan> {
53 if let Some(with) = &query.with
55 && with.recursive
56 {
57 return crate::planner::cte::plan_recursive_cte(query, catalog, functions, temporal);
58 }
59 if let Some(with) = &query.with
61 && !with.cte_tables.is_empty()
62 {
63 let inner_query = Query {
64 with: None,
65 body: query.body.clone(),
66 order_by: query.order_by.clone(),
67 limit_clause: query.limit_clause.clone(),
68 fetch: query.fetch.clone(),
69 locks: query.locks.clone(),
70 for_clause: query.for_clause.clone(),
71 settings: query.settings.clone(),
72 format_clause: query.format_clause.clone(),
73 pipe_operators: query.pipe_operators.clone(),
74 };
75
76 let mut definitions = Vec::new();
80 let mut relations = Vec::new();
81 for cte in &with.cte_tables {
82 let name = check_ast_identifier(&cte.alias.name)?;
83 let declared: Vec<String> = cte
84 .alias
85 .columns
86 .iter()
87 .map(|column| check_ast_identifier(&column.name))
88 .collect::<Result<_>>()?;
89 let earlier = CteCatalog {
90 inner: catalog,
91 relations: relations.clone(),
92 };
93 let cte_plan = plan_query(&cte.query, &earlier, functions, temporal)?;
94 let info = infer_subquery_relation(
95 &earlier,
96 &name,
97 &cte.query,
98 Some(&cte_plan),
99 functions,
100 temporal,
101 )?;
102 definitions.push((name.clone(), rekey_join_output(cte_plan)));
104 relations.push((name, rename_output_columns(info, &declared)));
105 }
106
107 let cte_catalog = CteCatalog {
109 inner: catalog,
110 relations,
111 };
112 let outer = plan_query_at(
113 &inner_query,
114 &cte_catalog,
115 functions,
116 temporal,
117 statement_output,
118 )?;
119
120 return Ok(SqlPlan::Cte(CtePlan {
121 definitions,
122 outer: Box::new(outer),
123 }));
124 }
125
126 match &*query.body {
128 SetExpr::Select(select) => super::search::plan_select_query(
129 query,
130 select,
131 catalog,
132 functions,
133 temporal,
134 statement_output,
135 ),
136 SetExpr::SetOperation {
137 op,
138 left,
139 right,
140 set_quantifier,
141 } => crate::planner::union::plan_set_operation(
142 op,
143 left,
144 right,
145 set_quantifier,
146 catalog,
147 functions,
148 temporal,
149 ),
150 _ => Err(SqlError::Unsupported {
151 detail: format!("query body type: {}", query.body),
152 }),
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::super::fixtures::plan_select_sql;
159 use crate::types::*;
160 #[test]
161 fn aggregate_subquery_join_filters_input_before_aggregation() {
162 let plan = plan_select_sql(
163 "SELECT AVG(price) FROM products WHERE category IN (SELECT DISTINCT category FROM products WHERE qty > 100)",
164 );
165
166 let SqlPlan::Aggregate { input, .. } = plan else {
167 panic!("expected aggregate plan");
168 };
169
170 let SqlPlan::Join {
171 left,
172 join_type,
173 on,
174 ..
175 } = *input
176 else {
177 panic!("expected semi-join below aggregate");
178 };
179
180 assert_eq!(join_type, JoinType::Semi);
181 assert_eq!(on, vec![("category".into(), "category".into())]);
182 assert!(matches!(*left, SqlPlan::Scan { .. }));
183 }
184
185 #[test]
186 fn scalar_subquery_defers_projection_until_after_join_filter() {
187 let plan = plan_select_sql(
188 "SELECT user_id FROM orders WHERE amount > (SELECT AVG(amount) FROM orders)",
189 );
190
191 let SqlPlan::Join {
192 left,
193 projection,
194 filters,
195 ..
196 } = plan
197 else {
198 panic!("expected join plan");
199 };
200
201 let SqlPlan::Scan {
202 projection: scan_projection,
203 ..
204 } = *left
205 else {
206 panic!("expected scan on join left");
207 };
208
209 assert!(scan_projection.is_empty(), "scan projected too early");
210 assert_eq!(projection.len(), 1);
211 match &projection[0] {
212 Projection::Column(name) => assert_eq!(name, "user_id"),
213 other => panic!("expected user_id projection, got {other:?}"),
214 }
215 assert!(
216 !filters.is_empty(),
217 "scalar comparison should stay post-join"
218 );
219 }
220
221 #[test]
222 fn chained_join_preserves_qualified_on_keys() {
223 let plan = plan_select_sql(
224 "SELECT d.name, t.tag, p.theme \
225 FROM docs d \
226 LEFT JOIN tags t ON d.id = t.doc_id \
227 INNER JOIN user_prefs p ON d.id = p.key",
228 );
229
230 let SqlPlan::Join { left, on, .. } = plan else {
231 panic!("expected outer join plan");
232 };
233 assert_eq!(on, vec![("d.id".into(), "p.key".into())]);
234
235 let SqlPlan::Join { on: inner_on, .. } = *left else {
236 panic!("expected nested left join");
237 };
238 assert_eq!(inner_on, vec![("d.id".into(), "t.doc_id".into())]);
239 }
240}