Skip to content

Commit 7f96102

Browse files
committed
feat(sql): implement subquery-to-join rewriting for IN/NOT IN
Replace the subquery.rs stub with a complete implementation that rewrites WHERE-clause subqueries into semi/anti joins, allowing the existing hash-join executor to handle them without a separate subquery engine. Supported patterns: - `WHERE col IN (SELECT col2 FROM tbl)` → semi-join - `WHERE col NOT IN (SELECT col2 FROM tbl)` → anti-join - `WHERE col > (SELECT AGG(...) FROM tbl)` → materialized scalar subquery Both select and join planners now call extract_subqueries() to strip subquery predicates from the WHERE clause before filter conversion, then wrap the base plan with the extracted join nodes. Semi and Anti join operators are also wired into the join operator mapping.
1 parent 809b662 commit 7f96102

3 files changed

Lines changed: 362 additions & 22 deletions

File tree

nodedb-sql/src/planner/join.rs

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::types::*;
1313
pub fn plan_join_from_select(
1414
select: &Select,
1515
scope: &TableScope,
16-
_catalog: &dyn SqlCatalog,
16+
catalog: &dyn SqlCatalog,
1717
functions: &FunctionRegistry,
1818
) -> Result<Option<SqlPlan>> {
1919
let from = &select.from[0];
@@ -83,14 +83,33 @@ pub fn plan_join_from_select(
8383
};
8484
}
8585

86-
// Apply WHERE as filters on the join plan.
87-
// Apply projection.
86+
// Extract subqueries from WHERE and wrap as additional joins.
87+
let (subquery_joins, effective_where) = if let Some(expr) = &select.selection {
88+
let extraction = super::subquery::extract_subqueries(expr, catalog, functions)?;
89+
(extraction.joins, extraction.remaining_where)
90+
} else {
91+
(Vec::new(), None)
92+
};
93+
94+
// Apply remaining WHERE as filters on the join plan.
8895
let _projection = super::select::convert_projection(&select.projection)?;
89-
let _filters = match &select.selection {
96+
let _filters = match &effective_where {
9097
Some(expr) => super::select::convert_where_to_filters(expr)?,
9198
None => Vec::new(),
9299
};
93100

101+
// Wrap with subquery joins (semi/anti/cross) if any.
102+
for sq in subquery_joins {
103+
current_plan = SqlPlan::Join {
104+
left: Box::new(current_plan),
105+
right: Box::new(sq.inner_plan),
106+
on: vec![(sq.outer_column, sq.inner_column)],
107+
join_type: sq.join_type,
108+
condition: None,
109+
limit: 10000,
110+
};
111+
}
112+
94113
// Check if there's aggregation on the join.
95114
let group_by_non_empty = match &select.group_by {
96115
ast::GroupByExpr::All(_) => true,
@@ -124,15 +143,15 @@ type JoinSpec = (JoinType, Vec<(String, String)>, Option<SqlExpr>);
124143
/// Extract join type, equi-join keys, and non-equi condition.
125144
fn extract_join_spec(op: &ast::JoinOperator) -> Result<JoinSpec> {
126145
match op {
127-
ast::JoinOperator::Inner(constraint) => {
146+
ast::JoinOperator::Inner(constraint) | ast::JoinOperator::Join(constraint) => {
128147
let (keys, cond) = extract_join_constraint(constraint)?;
129148
Ok((JoinType::Inner, keys, cond))
130149
}
131-
ast::JoinOperator::LeftOuter(constraint) => {
150+
ast::JoinOperator::Left(constraint) | ast::JoinOperator::LeftOuter(constraint) => {
132151
let (keys, cond) = extract_join_constraint(constraint)?;
133152
Ok((JoinType::Left, keys, cond))
134153
}
135-
ast::JoinOperator::RightOuter(constraint) => {
154+
ast::JoinOperator::Right(constraint) | ast::JoinOperator::RightOuter(constraint) => {
136155
let (keys, cond) = extract_join_constraint(constraint)?;
137156
Ok((JoinType::Right, keys, cond))
138157
}
@@ -144,6 +163,14 @@ fn extract_join_spec(op: &ast::JoinOperator) -> Result<JoinSpec> {
144163
let (keys, cond) = extract_join_constraint(constraint)?;
145164
Ok((JoinType::Cross, keys, cond))
146165
}
166+
ast::JoinOperator::Semi(constraint) | ast::JoinOperator::LeftSemi(constraint) => {
167+
let (keys, cond) = extract_join_constraint(constraint)?;
168+
Ok((JoinType::Semi, keys, cond))
169+
}
170+
ast::JoinOperator::Anti(constraint) | ast::JoinOperator::LeftAnti(constraint) => {
171+
let (keys, cond) = extract_join_constraint(constraint)?;
172+
Ok((JoinType::Anti, keys, cond))
173+
}
147174
_ => Err(SqlError::Unsupported {
148175
detail: format!("join type: {op:?}"),
149176
}),

nodedb-sql/src/planner/select.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,16 @@ fn plan_select(
9393
detail: "multi-table FROM without JOIN".into(),
9494
})?;
9595

96-
// 4. Convert WHERE filters.
97-
let filters = match &select.selection {
96+
// 4. Extract subqueries from WHERE and rewrite as semi/anti joins.
97+
let (subquery_joins, effective_where) = if let Some(expr) = &select.selection {
98+
let extraction = super::subquery::extract_subqueries(expr, catalog, functions)?;
99+
(extraction.joins, extraction.remaining_where)
100+
} else {
101+
(Vec::new(), None)
102+
};
103+
104+
// 5. Convert remaining WHERE filters.
105+
let filters = match &effective_where {
98106
Some(expr) => {
99107
// Check for search-triggering functions in WHERE.
100108
if let Some(plan) = try_extract_where_search(expr, table, functions)? {
@@ -105,19 +113,32 @@ fn plan_select(
105113
None => Vec::new(),
106114
};
107115

108-
// 5. Check for GROUP BY / aggregation.
116+
// 6. Check for GROUP BY / aggregation.
109117
if has_aggregation(select, functions) {
110-
return super::aggregate::plan_aggregate(select, table, &filters, &scope, functions);
118+
let mut plan =
119+
super::aggregate::plan_aggregate(select, table, &filters, &scope, functions)?;
120+
// Wrap with subquery joins if any.
121+
for sq in subquery_joins {
122+
plan = SqlPlan::Join {
123+
left: Box::new(plan),
124+
right: Box::new(sq.inner_plan),
125+
on: vec![(sq.outer_column, sq.inner_column)],
126+
join_type: sq.join_type,
127+
condition: None,
128+
limit: 10000,
129+
};
130+
}
131+
return Ok(plan);
111132
}
112133

113-
// 6. Convert projection.
134+
// 7. Convert projection.
114135
let projection = convert_projection(&select.projection)?;
115136

116-
// 7. Convert window functions (SELECT with OVER).
137+
// 8. Convert window functions (SELECT with OVER).
117138
let window_functions = super::window::extract_window_functions(&select.projection, functions)?;
118139

119-
// 8. Build base scan plan.
120-
Ok(SqlPlan::Scan {
140+
// 9. Build base scan plan.
141+
let mut plan = SqlPlan::Scan {
121142
collection: table.name.clone(),
122143
engine: table.info.engine,
123144
filters,
@@ -127,7 +148,21 @@ fn plan_select(
127148
offset: 0,
128149
distinct: select.distinct.is_some(),
129150
window_functions,
130-
})
151+
};
152+
153+
// 10. Wrap with subquery joins (semi/anti) if any.
154+
for sq in subquery_joins {
155+
plan = SqlPlan::Join {
156+
left: Box::new(plan),
157+
right: Box::new(sq.inner_plan),
158+
on: vec![(sq.outer_column, sq.inner_column)],
159+
join_type: sq.join_type,
160+
condition: None,
161+
limit: 10000,
162+
};
163+
}
164+
165+
Ok(plan)
131166
}
132167

133168
/// Check if a SELECT has aggregation (GROUP BY or aggregate functions in projection).

0 commit comments

Comments
 (0)