Skip to content

Commit 556e316

Browse files
committed
feat: wire join projection, filters, and UNION DISTINCT through the SQL planner
The join planner now attaches the SELECT projection and remaining WHERE filters to the outermost join node instead of dropping them, replacing the previous stub that delegated projection to the Origin mapper post-join. sql_plan_convert inlines the former convert_join helper into the SqlPlan::Join arm and passes the resolved projection names and serialized filter bytes directly into QueryOp::HashJoin. UNION DISTINCT is now functional: when distinct is set, all produced PhysicalTasks receive post_dedup = true so the router merges and deduplicates their payloads before returning results. Window function serialization is implemented — specs are converted to bridge WindowFuncSpec and encoded as MessagePack instead of returning an empty stub.
1 parent 08c51ed commit 556e316

2 files changed

Lines changed: 116 additions & 42 deletions

File tree

nodedb-sql/src/planner/join.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ pub fn plan_join_from_select(
8080
join_type,
8181
condition,
8282
limit: 10000,
83+
projection: Vec::new(),
84+
filters: Vec::new(),
8385
};
8486
}
8587

@@ -91,9 +93,9 @@ pub fn plan_join_from_select(
9193
(Vec::new(), None)
9294
};
9395

94-
// Apply remaining WHERE as filters on the join plan.
95-
let _projection = super::select::convert_projection(&select.projection)?;
96-
let _filters = match &effective_where {
96+
// Apply remaining WHERE as filters and SELECT projection on the join plan.
97+
let projection = super::select::convert_projection(&select.projection)?;
98+
let filters = match &effective_where {
9799
Some(expr) => super::select::convert_where_to_filters(expr)?,
98100
None => Vec::new(),
99101
};
@@ -107,6 +109,8 @@ pub fn plan_join_from_select(
107109
join_type: sq.join_type,
108110
condition: None,
109111
limit: 10000,
112+
projection: Vec::new(),
113+
filters: Vec::new(),
110114
};
111115
}
112116

@@ -132,8 +136,16 @@ pub fn plan_join_from_select(
132136
}));
133137
}
134138

135-
// TODO: attach projection and filters to the join plan
136-
// For now, the Origin mapper handles projection post-join.
139+
// Attach SELECT projection and WHERE filters to the outermost join.
140+
if let SqlPlan::Join {
141+
projection: ref mut proj,
142+
filters: ref mut filt,
143+
..
144+
} = current_plan
145+
{
146+
*proj = projection;
147+
*filt = filters;
148+
}
137149
Ok(Some(current_plan))
138150
}
139151

nodedb/src/control/planner/sql_plan_convert.rs

Lines changed: 99 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
//! serializes filters to msgpack, and handles broadcast join decisions.
55
66
use nodedb_sql::types::{
7-
AggregateExpr, EngineType, Filter, FilterExpr, JoinType, Projection, SortKey, SqlExpr, SqlPlan,
8-
SqlValue,
7+
AggregateExpr, EngineType, Filter, FilterExpr, Projection, SortKey, SqlExpr, SqlPlan, SqlValue,
98
};
109

1110
use std::sync::Arc;
@@ -57,6 +56,7 @@ fn convert_one(
5756
tenant_id,
5857
vshard_id: VShardId::from_collection(""),
5958
plan: PhysicalPlan::Meta(MetaOp::RawResponse { payload }),
59+
post_dedup: false,
6060
}])
6161
}
6262

@@ -121,6 +121,7 @@ fn convert_one(
121121
tenant_id,
122122
vshard_id: vshard,
123123
plan: physical,
124+
post_dedup: false,
124125
}])
125126
}
126127

@@ -147,6 +148,7 @@ fn convert_one(
147148
tenant_id,
148149
vshard_id: vshard,
149150
plan: physical,
151+
post_dedup: false,
150152
}])
151153
}
152154

@@ -188,6 +190,7 @@ fn convert_one(
188190
plan: PhysicalPlan::Document(DocumentOp::Truncate {
189191
collection: collection.clone(),
190192
}),
193+
post_dedup: false,
191194
}])
192195
}
193196

@@ -197,8 +200,33 @@ fn convert_one(
197200
on,
198201
join_type,
199202
limit,
203+
projection,
204+
filters,
200205
..
201-
} => convert_join(left, right, on, join_type, *limit, tenant_id),
206+
} => {
207+
let left_collection = extract_collection_name(left);
208+
let right_collection = extract_collection_name(right);
209+
let vshard = VShardId::from_collection(&left_collection);
210+
let proj_names = extract_projection_names(projection);
211+
let filter_bytes = serialize_filters(filters)?;
212+
213+
Ok(vec![PhysicalTask {
214+
tenant_id,
215+
vshard_id: vshard,
216+
plan: PhysicalPlan::Query(QueryOp::HashJoin {
217+
left_collection,
218+
right_collection,
219+
on: on.to_vec(),
220+
join_type: join_type.as_str().to_string(),
221+
limit: *limit,
222+
post_group_by: Vec::new(),
223+
post_aggregates: Vec::new(),
224+
projection: proj_names,
225+
post_filters: filter_bytes,
226+
}),
227+
post_dedup: false,
228+
}])
229+
}
202230

203231
SqlPlan::Aggregate {
204232
input,
@@ -260,6 +288,7 @@ fn convert_one(
260288
gap_fill: gap_fill.clone(),
261289
rls_filters: Vec::new(),
262290
}),
291+
post_dedup: false,
263292
}])
264293
}
265294

@@ -275,6 +304,7 @@ fn convert_one(
275304
format: "json".into(),
276305
wal_lsn: None,
277306
}),
307+
post_dedup: false,
278308
}])
279309
}
280310

@@ -300,6 +330,7 @@ fn convert_one(
300330
field_name: field.clone(),
301331
rls_filters: filter_bytes,
302332
}),
333+
post_dedup: false,
303334
}])
304335
}
305336

@@ -321,6 +352,7 @@ fn convert_one(
321352
fuzzy: *fuzzy,
322353
rls_filters: Vec::new(),
323354
}),
355+
post_dedup: false,
324356
}])
325357
}
326358

@@ -348,6 +380,7 @@ fn convert_one(
348380
filter_bitmap: None,
349381
rls_filters: Vec::new(),
350382
}),
383+
post_dedup: false,
351384
}])
352385
}
353386

@@ -384,6 +417,7 @@ fn convert_one(
384417
projection: proj_names,
385418
rls_filters: Vec::new(),
386419
}),
420+
post_dedup: false,
387421
}])
388422
}
389423

@@ -392,8 +426,11 @@ fn convert_one(
392426
for input in inputs {
393427
all_tasks.extend(convert_one(input, tenant_id, ctx)?);
394428
}
395-
// TODO: if distinct == true, wrap in a dedup stage.
396-
let _ = distinct;
429+
if *distinct {
430+
for task in &mut all_tasks {
431+
task.post_dedup = true;
432+
}
433+
}
397434
Ok(all_tasks)
398435
}
399436

@@ -417,6 +454,7 @@ fn convert_one(
417454
distinct: *distinct,
418455
limit: *limit,
419456
}),
457+
post_dedup: false,
420458
}])
421459
}
422460

@@ -462,6 +500,7 @@ fn convert_insert(
462500
value: value_bytes,
463501
ttl_ms: 0,
464502
}),
503+
post_dedup: false,
465504
});
466505
}
467506
_ => {
@@ -473,6 +512,7 @@ fn convert_insert(
473512
document_id: doc_id,
474513
value: value_bytes,
475514
}),
515+
post_dedup: false,
476516
});
477517
}
478518
}
@@ -515,6 +555,7 @@ fn convert_update(
515555
key: sql_value_to_bytes(key),
516556
updates: field_updates,
517557
}),
558+
post_dedup: false,
518559
});
519560
}
520561
return Ok(tasks);
@@ -533,6 +574,7 @@ fn convert_update(
533574
updates: updates.clone(),
534575
returning,
535576
}),
577+
post_dedup: false,
536578
});
537579
}
538580
Ok(tasks)
@@ -546,6 +588,7 @@ fn convert_update(
546588
updates,
547589
returning,
548590
}),
591+
post_dedup: false,
549592
}])
550593
}
551594
}
@@ -569,6 +612,7 @@ fn convert_delete(
569612
collection: collection.into(),
570613
keys,
571614
}),
615+
post_dedup: false,
572616
}]);
573617
}
574618

@@ -582,6 +626,7 @@ fn convert_delete(
582626
collection: collection.into(),
583627
document_id: sql_value_to_string(key),
584628
}),
629+
post_dedup: false,
585630
});
586631
}
587632
Ok(tasks)
@@ -594,39 +639,11 @@ fn convert_delete(
594639
collection: collection.into(),
595640
filters: filter_bytes,
596641
}),
642+
post_dedup: false,
597643
}])
598644
}
599645
}
600646

601-
fn convert_join(
602-
left: &SqlPlan,
603-
right: &SqlPlan,
604-
on: &[(String, String)],
605-
join_type: &JoinType,
606-
limit: usize,
607-
tenant_id: TenantId,
608-
) -> crate::Result<Vec<PhysicalTask>> {
609-
let left_collection = extract_collection_name(left);
610-
let right_collection = extract_collection_name(right);
611-
let vshard = VShardId::from_collection(&left_collection);
612-
613-
let on_pairs: Vec<(String, String)> = on.to_vec();
614-
615-
Ok(vec![PhysicalTask {
616-
tenant_id,
617-
vshard_id: vshard,
618-
plan: PhysicalPlan::Query(QueryOp::HashJoin {
619-
left_collection,
620-
right_collection,
621-
on: on_pairs,
622-
join_type: join_type.as_str().to_string(),
623-
limit,
624-
post_group_by: Vec::new(),
625-
post_aggregates: Vec::new(),
626-
}),
627-
}])
628-
}
629-
630647
fn convert_aggregate(
631648
input: &SqlPlan,
632649
group_by: &[SqlExpr],
@@ -666,7 +683,10 @@ fn convert_aggregate(
666683
limit: *join_limit,
667684
post_group_by: group_strs,
668685
post_aggregates: agg_pairs,
686+
projection: Vec::new(),
687+
post_filters: Vec::new(),
669688
}),
689+
post_dedup: false,
670690
}]);
671691
}
672692

@@ -698,6 +718,7 @@ fn convert_aggregate(
698718
sub_group_by: Vec::new(),
699719
sub_aggregates: Vec::new(),
700720
}),
721+
post_dedup: false,
701722
}])
702723
}
703724

@@ -749,9 +770,50 @@ fn extract_computed_columns(proj: &[Projection]) -> crate::Result<Vec<u8>> {
749770
})
750771
}
751772

752-
fn serialize_window_functions(_specs: &[nodedb_sql::types::WindowSpec]) -> crate::Result<Vec<u8>> {
753-
// TODO: serialize window function specs.
754-
Ok(Vec::new())
773+
fn serialize_window_functions(specs: &[nodedb_sql::types::WindowSpec]) -> crate::Result<Vec<u8>> {
774+
if specs.is_empty() {
775+
return Ok(Vec::new());
776+
}
777+
let bridge_specs: Vec<crate::bridge::window_func::WindowFuncSpec> = specs
778+
.iter()
779+
.map(|s| crate::bridge::window_func::WindowFuncSpec {
780+
alias: s.alias.clone(),
781+
func_name: s.function.clone(),
782+
args: s.args.iter().map(sql_expr_to_bridge_expr).collect(),
783+
partition_by: s
784+
.partition_by
785+
.iter()
786+
.filter_map(|e| match e {
787+
SqlExpr::Column { name, .. } => Some(name.clone()),
788+
_ => None,
789+
})
790+
.collect(),
791+
order_by: s
792+
.order_by
793+
.iter()
794+
.filter_map(|k| match &k.expr {
795+
SqlExpr::Column { name, .. } => Some((name.clone(), k.ascending)),
796+
_ => None,
797+
})
798+
.collect(),
799+
frame: crate::bridge::window_func::WindowFrame::default(),
800+
})
801+
.collect();
802+
zerompk::to_msgpack_vec(&bridge_specs).map_err(|e| crate::Error::Internal {
803+
detail: format!("serialize window functions: {e}"),
804+
})
805+
}
806+
807+
/// Convert a `nodedb_sql::types::SqlExpr` (parser AST) to a
808+
/// `nodedb_query::expr::SqlExpr` (bridge evaluation type).
809+
fn sql_expr_to_bridge_expr(expr: &SqlExpr) -> crate::bridge::expr_eval::SqlExpr {
810+
use crate::bridge::expr_eval::SqlExpr as BExpr;
811+
match expr {
812+
SqlExpr::Column { name, .. } => BExpr::Column(name.clone()),
813+
SqlExpr::Literal(v) => BExpr::Literal(sql_value_to_json(v)),
814+
SqlExpr::Wildcard => BExpr::Column("*".into()),
815+
_ => BExpr::Literal(serde_json::Value::Null),
816+
}
755817
}
756818

757819
fn convert_sort_keys(keys: &[SortKey]) -> Vec<(String, bool)> {

0 commit comments

Comments
 (0)