Skip to content

Commit e237100

Browse files
committed
feat: apply post-join WHERE filters and SELECT projection in the hash join executor
The Data Plane hash join handler now accepts projection column names and serialized post-filter bytes. After building join results, filters are deserialized from MessagePack and applied via ScanFilter::matches_binary. Projection is then applied in SELECT column order, dropping unrequested fields and reordering the output map to match the query's column list.
1 parent a2c4e1b commit e237100

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

nodedb/src/data/executor/dispatch/other.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ impl CoreLoop {
4242
on,
4343
join_type,
4444
limit,
45+
projection,
46+
post_filters,
4547
..
4648
}) => self.execute_hash_join(
4749
task,
@@ -51,6 +53,8 @@ impl CoreLoop {
5153
on,
5254
join_type,
5355
*limit,
56+
projection,
57+
post_filters,
5458
),
5559

5660
PhysicalPlan::Meta(MetaOp::WalAppend { payload }) => {
@@ -181,6 +185,8 @@ impl CoreLoop {
181185
on,
182186
join_type,
183187
*limit,
188+
&[],
189+
&[],
184190
)
185191
}
186192

nodedb/src/data/executor/handlers/join/hash.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ impl CoreLoop {
204204
on: &[(String, String)],
205205
join_type: &str,
206206
limit: usize,
207+
projection: &[String],
208+
post_filter_bytes: &[u8],
207209
) -> Response {
208210
debug!(
209211
core = self.core_id,
@@ -246,7 +248,7 @@ impl CoreLoop {
246248
let right_index = HashIndex::build(&right_docs, &right_keys);
247249

248250
// Probe the hash index with left (probe) side.
249-
let results = probe_hash_index(&ProbeParams {
251+
let mut results = probe_hash_index(&ProbeParams {
250252
probe_docs: &left_docs,
251253
index: &right_index,
252254
index_docs: &right_docs,
@@ -257,6 +259,47 @@ impl CoreLoop {
257259
index_collection: right_collection,
258260
});
259261

262+
// Apply post-join WHERE filters.
263+
if !post_filter_bytes.is_empty() {
264+
let filters: Vec<crate::bridge::scan_filter::ScanFilter> =
265+
match zerompk::from_msgpack(post_filter_bytes) {
266+
Ok(f) => f,
267+
Err(e) => {
268+
return self.response_error(
269+
task,
270+
ErrorCode::Internal {
271+
detail: format!("post-join filter deserialization failed: {e}"),
272+
},
273+
);
274+
}
275+
};
276+
if !filters.is_empty() {
277+
results.retain(|row| {
278+
// Convert JSON row to msgpack for ScanFilter::matches_binary.
279+
let ndb_val = nodedb_types::Value::from(row.clone());
280+
match nodedb_types::value_to_msgpack(&ndb_val) {
281+
Ok(bytes) => filters.iter().all(|f| f.matches_binary(&bytes)),
282+
Err(_) => true, // pass through on serialization failure
283+
}
284+
});
285+
}
286+
}
287+
288+
// Apply post-join projection: keep only requested columns, preserving SELECT order.
289+
if !projection.is_empty() {
290+
for row in &mut results {
291+
if let serde_json::Value::Object(map) = row {
292+
let mut reordered = serde_json::Map::with_capacity(projection.len());
293+
for col in projection {
294+
if let Some(val) = map.remove(col.as_str()) {
295+
reordered.insert(col.clone(), val);
296+
}
297+
}
298+
*row = serde_json::Value::Object(reordered);
299+
}
300+
}
301+
}
302+
260303
match super::super::super::response_codec::encode_json_vec(&results) {
261304
Ok(payload) => self.response_with_payload(task, payload),
262305
Err(e) => self.response_error(

0 commit comments

Comments
 (0)