1use crate::physical::common::{hash_value_into, store_value_in_vector, value_hash};
3use crate::physical::types::{HashJoinTable, OperatorResult};
4use akar_common::types::{PhysicalTypeID, Value};
5use akar_common::vector::{DataChunk, ValueVector};
6use std::collections::HashMap;
7use std::hash::{Hash, Hasher};
8
9#[inline]
14fn hash_chunk_cell(chunk: &DataChunk, col: usize, row: usize) -> Option<u64> {
15 if col >= chunk.fields.len() || chunk.is_null(col, row) {
16 return None;
17 }
18 let mut hasher = ahash::AHasher::default();
19 match chunk.field_types[col] {
20 PhysicalTypeID::Int64 => {
21 let v = chunk.get_i64(col, row).unwrap_or(0);
22 v.hash(&mut hasher);
23 }
24 PhysicalTypeID::Int32 => {
25 let v = chunk.get_i32(col, row).unwrap_or(0);
26 v.hash(&mut hasher);
27 }
28 PhysicalTypeID::Double => {
29 let v = chunk.get_f64(col, row).unwrap_or(0.0);
30 v.to_bits().hash(&mut hasher);
31 }
32 PhysicalTypeID::Bool => {
33 let v = chunk.get_bool(col, row).unwrap_or(false);
34 v.hash(&mut hasher);
35 }
36 PhysicalTypeID::String => {
37 if let Some(s) = chunk.get_string(col, row) {
38 s.hash(&mut hasher);
39 }
40 }
41 _ => {
42 if let Some(val) = chunk.get_value(col, row) {
43 hash_value_into(&val, &mut hasher);
44 }
45 }
46 }
47 Some(hasher.finish())
48}
49
50#[inline]
52fn chunk_cells_equal(
53 left: &DataChunk,
54 left_col: usize,
55 left_row: usize,
56 right: &DataChunk,
57 right_col: usize,
58 right_row: usize,
59) -> bool {
60 if left.is_null(left_col, left_row) || right.is_null(right_col, right_row) {
61 return left.is_null(left_col, left_row) && right.is_null(right_col, right_row);
62 }
63 match (left.field_types[left_col], right.field_types[right_col]) {
64 (PhysicalTypeID::Int64, PhysicalTypeID::Int64) => {
65 left.get_i64(left_col, left_row) == right.get_i64(right_col, right_row)
66 }
67 (PhysicalTypeID::Int32, PhysicalTypeID::Int32) => {
68 left.get_i32(left_col, left_row) == right.get_i32(right_col, right_row)
69 }
70 (PhysicalTypeID::Int64, PhysicalTypeID::Int32) | (PhysicalTypeID::Int32, PhysicalTypeID::Int64) => {
71 let a = left
72 .get_i64(left_col, left_row)
73 .or_else(|| left.get_i32(left_col, left_row).map(|v| v as i64));
74 let b = right
75 .get_i64(right_col, right_row)
76 .or_else(|| right.get_i32(right_col, right_row).map(|v| v as i64));
77 a == b
78 }
79 (PhysicalTypeID::Double, PhysicalTypeID::Double) => {
80 left.get_f64(left_col, left_row) == right.get_f64(right_col, right_row)
81 }
82 (PhysicalTypeID::Bool, PhysicalTypeID::Bool) => {
83 left.get_bool(left_col, left_row) == right.get_bool(right_col, right_row)
84 }
85 (PhysicalTypeID::String, PhysicalTypeID::String) => {
86 left.get_string(left_col, left_row) == right.get_string(right_col, right_row)
87 }
88 _ => left.get_value(left_col, left_row) == right.get_value(right_col, right_row),
89 }
90}
91pub struct PhysicalCrossProduct;
99
100impl PhysicalCrossProduct {
101 pub fn execute_binary(&self, left_chunks: &[DataChunk], right_chunks: &[DataChunk]) -> OperatorResult {
102 if left_chunks.is_empty() || right_chunks.is_empty() {
103 return Ok(vec![]);
104 }
105
106 let left_rows: usize = left_chunks.iter().map(|c| c.size).sum();
108 let right_rows: usize = right_chunks.iter().map(|c| c.size).sum();
109
110 if left_rows == 0 || right_rows == 0 {
111 return Ok(vec![]);
112 }
113
114 let num_left_cols = left_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
116 let num_right_cols = right_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
117 let total_cols = num_left_cols + num_right_cols;
118 let total_rows = left_rows * right_rows;
119
120 let mut left_values: Vec<Vec<Value>> = (0..num_left_cols).map(|_| Vec::with_capacity(left_rows)).collect();
121 for chunk in left_chunks {
122 for col in 0..num_left_cols {
123 if chunk.fields.get(col).is_some() {
124 for row in 0..chunk.size {
125 left_values[col].push(chunk.get_value(col, row).unwrap_or(Value::Null));
126 }
127 }
128 }
129 }
130
131 let mut right_values: Vec<Vec<Value>> = (0..num_right_cols).map(|_| Vec::with_capacity(right_rows)).collect();
132 for chunk in right_chunks {
133 for col in 0..num_right_cols {
134 if chunk.fields.get(col).is_some() {
135 for row in 0..chunk.size {
136 right_values[col].push(chunk.get_value(col, row).unwrap_or(Value::Null));
137 }
138 }
139 }
140 }
141
142 let mut output_types: Vec<PhysicalTypeID> = Vec::with_capacity(total_cols);
144 let mut field_names = Vec::with_capacity(total_cols);
145 for col in 0..num_left_cols {
146 if left_chunks[0].fields.get(col).is_some() {
147 output_types.push(left_chunks[0].field_types[col]);
148 }
149 }
150 for col in 0..num_right_cols {
151 if right_chunks[0].fields.get(col).is_some() {
152 output_types.push(right_chunks[0].field_types[col]);
153 }
154 }
155
156 if let Some(c) = left_chunks.first() {
157 field_names.extend(c.field_names.iter().cloned());
158 }
159 if let Some(c) = right_chunks.first() {
160 field_names.extend(c.field_names.iter().cloned());
161 }
162
163 let mut output_fields: Vec<ValueVector> = output_types
165 .iter()
166 .map(|t| ValueVector::new(*t, total_rows.max(1)))
167 .collect();
168
169 let mut out_row = 0usize;
170 for lr in 0..left_rows {
171 for rr in 0..right_rows {
172 for (col, field) in output_fields.iter_mut().enumerate().take(num_left_cols) {
173 let val = &left_values[col][lr];
174 let _ = field.set_value(out_row, val);
175 }
176 for col in 0..num_right_cols {
177 let val = &right_values[col][rr];
178 let _ = output_fields[num_left_cols + col].set_value(out_row, val);
179 }
180 out_row += 1;
181 }
182 }
183
184 for field in &mut output_fields {
185 field.resize(total_rows);
186 }
187
188 let mut output_names: Vec<String> = left_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default();
190 output_names.extend(right_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default());
191 let arrow_fields = output_fields
192 .iter()
193 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
194 .collect::<Vec<_>>();
195 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
196 Ok(vec![DataChunk {
197 fields: arrow_fields,
198 field_types: arrow_field_types,
199 size: total_rows,
200 field_names: output_names,
201 sel_vector: None,
202 }])
203 }
204}
205
206pub struct PhysicalSemiJoin {
211 pub build_columns: Vec<u32>,
212 pub probe_columns: Vec<u32>,
213}
214
215impl PhysicalSemiJoin {
216 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
217 if build_chunks.is_empty() || probe_chunks.is_empty() {
218 return Ok(vec![]);
219 }
220
221 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
222 let probe_col = self.probe_columns.first().copied().unwrap_or(0) as usize;
223
224 let mut hash_map: HashMap<u64, Vec<Value>> = HashMap::new();
229 for chunk in build_chunks {
230 for row in 0..chunk.size {
231 if chunk.fields.get(build_col).is_some() {
232 let key = chunk.get_value(build_col, row).unwrap_or(Value::Null);
233 if matches!(key, Value::Null) {
234 continue;
235 }
236 hash_map.entry(value_hash(&key)).or_default().push(key);
237 }
238 }
239 }
240
241 let num_probe_fields = probe_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
243 let mut probe_types: Vec<PhysicalTypeID> = Vec::with_capacity(num_probe_fields);
244 if let Some(first) = probe_chunks.first() {
245 for col in 0..first.num_fields() {
246 probe_types.push(first.field_types[col]);
247 }
248 }
249
250 let total_probe_rows: usize = probe_chunks.iter().map(|c| c.size).sum();
251 let mut match_rows: Vec<(usize, usize)> = Vec::with_capacity(total_probe_rows);
252 for (ci, chunk) in probe_chunks.iter().enumerate() {
253 for row in 0..chunk.size {
254 if chunk.fields.get(probe_col).is_some() {
255 let key = chunk.get_value(probe_col, row).unwrap_or(Value::Null);
256 if matches!(key, Value::Null) {
257 continue;
258 }
259 let matched = hash_map.get(&value_hash(&key)).map_or(false, |bucket| {
260 bucket.iter().any(|k| *k == key)
261 });
262 if matched {
263 match_rows.push((ci, row));
264 }
265 }
266 }
267 }
268
269 if match_rows.is_empty() {
270 return Ok(vec![]);
271 }
272
273 let num_left_cols = probe_types.len();
275 let mut output_fields: Vec<ValueVector> = probe_types
276 .iter()
277 .map(|t| ValueVector::new(*t, match_rows.len().max(1)))
278 .collect();
279
280 for (out_idx, (ci, row)) in match_rows.iter().enumerate() {
281 if let Some(chunk) = probe_chunks.get(*ci) {
282 for (col, out_field) in output_fields.iter_mut().enumerate().take(num_left_cols) {
283 if chunk.fields.get(col).is_some() {
284 let val = chunk.get_value(col, *row).unwrap_or(Value::Null);
285 let _ = out_field.set_value(out_idx, &val);
286 }
287 }
288 }
289 }
290 for field in &mut output_fields {
291 field.resize(match_rows.len());
292 }
293 let arrow_fields = output_fields
294 .iter()
295 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
296 .collect::<Vec<_>>();
297 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
298 Ok(vec![DataChunk {
299 fields: arrow_fields,
300 field_types: arrow_field_types,
301 size: match_rows.len(),
302 field_names: vec![],
303 sel_vector: None,
304 }])
305 }
306}
307
308pub struct PhysicalAntiJoin {
313 pub build_columns: Vec<u32>,
314 pub probe_columns: Vec<u32>,
315}
316
317impl PhysicalAntiJoin {
318 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
319 if probe_chunks.is_empty() {
320 return Ok(vec![]);
321 }
322 if build_chunks.is_empty() {
323 return Ok(probe_chunks.to_vec());
325 }
326
327 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
328 let probe_col = self.probe_columns.first().copied().unwrap_or(0) as usize;
329
330 let mut hash_map: HashMap<u64, Vec<Value>> = HashMap::new();
335 for chunk in build_chunks {
336 for row in 0..chunk.size {
337 if chunk.fields.get(build_col).is_some() {
338 let key = chunk.get_value(build_col, row).unwrap_or(Value::Null);
339 if matches!(key, Value::Null) {
340 continue;
341 }
342 hash_map.entry(value_hash(&key)).or_default().push(key);
343 }
344 }
345 }
346
347 let num_probe_fields = probe_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
348 let mut probe_types: Vec<PhysicalTypeID> = Vec::with_capacity(num_probe_fields);
349 if let Some(first) = probe_chunks.first() {
350 for col in 0..first.num_fields() {
351 probe_types.push(first.field_types[col]);
352 }
353 }
354
355 let total_probe_rows: usize = probe_chunks.iter().map(|c| c.size).sum();
356 let mut non_match_rows: Vec<(usize, usize)> = Vec::with_capacity(total_probe_rows);
357 for (ci, chunk) in probe_chunks.iter().enumerate() {
358 for row in 0..chunk.size {
359 if let Some(_field) = chunk.fields.get(probe_col) {
360 let key = chunk.get_value(probe_col, row).unwrap_or(Value::Null);
361 if matches!(key, Value::Null) {
362 continue;
363 }
364 let matched = hash_map.get(&value_hash(&key)).map_or(false, |bucket| {
365 bucket.iter().any(|k| *k == key)
366 });
367 if !matched {
368 non_match_rows.push((ci, row));
369 }
370 }
371 }
372 }
373
374 if non_match_rows.is_empty() {
375 return Ok(vec![]);
376 }
377
378 let num_left_cols = probe_types.len();
379 let mut output_fields: Vec<ValueVector> = probe_types
380 .iter()
381 .map(|t| ValueVector::new(*t, non_match_rows.len().max(1)))
382 .collect();
383
384 for (out_idx, (ci, row)) in non_match_rows.iter().enumerate() {
385 if let Some(chunk) = probe_chunks.get(*ci) {
386 for (col, out_field) in output_fields.iter_mut().enumerate().take(num_left_cols) {
387 if let Some(_field) = chunk.fields.get(col) {
388 let val = chunk.get_value(col, *row).unwrap_or(Value::Null);
389 let _ = out_field.set_value(out_idx, &val);
390 }
391 }
392 }
393 }
394 for field in &mut output_fields {
395 field.resize(non_match_rows.len());
396 }
397 let arrow_fields = output_fields
398 .iter()
399 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
400 .collect::<Vec<_>>();
401 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
402 Ok(vec![DataChunk {
403 fields: arrow_fields,
404 field_types: arrow_field_types,
405 size: non_match_rows.len(),
406 field_names: vec![],
407 sel_vector: None,
408 }])
409 }
410}
411
412pub struct PhysicalIntersect {
427 pub num_build_sides: u32,
429 pub probe_key_col: u32,
431 pub build_key_col: u32,
433}
434
435impl PhysicalIntersect {
436 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
437 let num_builds = self.num_build_sides.max(1) as usize;
438 if build_chunks.is_empty() || probe_chunks.is_empty() {
439 return Ok(vec![]);
440 }
441
442 let chunk_group_size = (build_chunks.len() / num_builds).max(1);
445 let mut sides: Vec<Vec<DataChunk>> = Vec::with_capacity(num_builds);
446 for side in 0..num_builds {
447 let start = side * chunk_group_size;
448 let end = (start + chunk_group_size).min(build_chunks.len());
449 sides.push(build_chunks[start..end].to_vec());
450 }
451
452 self.execute_sides(&sides, probe_chunks)
453 }
454
455 pub fn execute_sides(&self, build_sides: &[Vec<DataChunk>], probe_chunks: &[DataChunk]) -> OperatorResult {
464 let num_builds = build_sides.len().max(1);
465 if probe_chunks.is_empty() {
466 return Ok(vec![]);
467 }
468
469 let build_col = self.build_key_col as usize;
470 let probe_col = self.probe_key_col as usize;
471
472 let mut build_tables: Vec<HashJoinTable> = Vec::with_capacity(num_builds);
474 let mut side_field_names: Vec<Vec<String>> = Vec::with_capacity(num_builds);
475 let mut side_field_counts: Vec<usize> = Vec::with_capacity(num_builds);
476
477 for side in build_sides {
478 let mut ht: HashJoinTable = HashMap::new();
479 let mut names: Vec<String> = Vec::new();
480 let mut count = 0usize;
481
482 for (ci, chunk) in side.iter().enumerate() {
483 if ci == 0 {
484 names = chunk.field_names.clone();
485 count = chunk.fields.len();
486 }
487 for row in 0..chunk.size {
488 if chunk.fields.get(build_col).is_none() {
489 continue;
490 }
491 let key = chunk.get_value(build_col, row).unwrap_or(Value::Null);
492 if matches!(key, Value::Null) {
493 continue;
494 }
495 let hash = value_hash(&key);
496 ht.entry(hash).or_default().push((key, vec![(ci, row)]));
497 }
498 }
499
500 side_field_names.push(names);
501 side_field_counts.push(count);
502 build_tables.push(ht);
503 }
504
505 if build_tables.iter().any(|t| t.is_empty()) {
506 return Ok(vec![]);
508 }
509
510 let probe_field_names = probe_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default();
511 let probe_field_count = probe_chunks.first().map(|c| c.fields.len()).unwrap_or(0);
512 let mut output_rows: Vec<Vec<Value>> = Vec::new();
513
514 for (ci, chunk) in probe_chunks.iter().enumerate() {
515 let _ = ci;
516 for row in 0..chunk.size {
517 let probe_key = chunk.get_value(probe_col, row).unwrap_or(Value::Null);
518 if matches!(probe_key, Value::Null) {
519 continue;
520 }
521 let probe_hash = value_hash(&probe_key);
522
523 let mut matches_per_side: Vec<Vec<(usize, usize)>> = Vec::with_capacity(num_builds);
525 let mut all_match = true;
526 for ht in &build_tables {
527 let mut side_matches: Vec<(usize, usize)> = Vec::new();
528 if let Some(bucket) = ht.get(&probe_hash) {
529 for (stored_key, locations) in bucket {
530 if stored_key == &probe_key {
531 side_matches.extend(locations.iter().cloned());
532 }
533 }
534 }
535 if side_matches.is_empty() {
536 all_match = false;
537 break;
538 }
539 matches_per_side.push(side_matches);
540 }
541 if !all_match {
542 continue;
543 }
544
545 let mut combos: Vec<Vec<(usize, usize)>> = vec![vec![]];
547 for side_matches in &matches_per_side {
548 let mut next = Vec::with_capacity(combos.len() * side_matches.len());
549 for combo in &combos {
550 for m in side_matches {
551 let mut c = combo.clone();
552 c.push(*m);
553 next.push(c);
554 }
555 }
556 combos = next;
557 }
558
559 let per_row_cols = probe_field_count + side_field_counts.iter().sum::<usize>();
560 for combo in combos {
561 let mut row_values: Vec<Value> = Vec::with_capacity(per_row_cols);
562 for col_in_probe in 0..probe_field_count {
564 row_values.push(chunk.get_value(col_in_probe, row).unwrap_or(Value::Null));
565 }
566 for (side_idx, &(b_ci, b_row)) in combo.iter().enumerate() {
568 if let Some(side_chunk) = build_sides.get(side_idx).and_then(|s| s.get(b_ci)) {
569 for col in 0..side_chunk.fields.len() {
570 row_values.push(side_chunk.get_value(col, b_row).unwrap_or(Value::Null));
571 }
572 }
573 }
574 output_rows.push(row_values);
575 }
576 }
577 }
578
579 if output_rows.is_empty() {
580 return Ok(vec![]);
581 }
582
583 let output_size = output_rows.len();
585 let mut output_fields: Vec<ValueVector> = Vec::with_capacity(output_rows.first().map(|r| r.len()).unwrap_or(0));
586
587 if let Some(first_row) = output_rows.first() {
588 for val in first_row {
589 let ptype = val.physical_type();
590 let mut vv = ValueVector::new(ptype, output_size);
591 vv.resize(output_size);
592 output_fields.push(vv);
593 }
594 }
595
596 for (out_idx, row_values) in output_rows.iter().enumerate() {
597 for (col, val) in row_values.iter().enumerate() {
598 if let Some(field) = output_fields.get_mut(col) {
599 let _ = field.set_value(out_idx, val);
600 }
601 }
602 }
603
604 let arrow_fields = output_fields
605 .iter()
606 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
607 .collect::<Vec<_>>();
608 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
609
610 let mut field_names: Vec<String> = probe_field_names;
611 for names in &side_field_names {
612 field_names.extend(names.iter().cloned());
613 }
614
615 Ok(vec![DataChunk {
616 fields: arrow_fields,
617 field_types: arrow_field_types,
618 field_names,
619 size: output_size,
620 sel_vector: None,
621 }])
622 }
623}
624
625pub struct JoinHashTable {
635 build_columns: Vec<u32>,
636 probe_columns: Vec<u32>,
637}
638
639impl JoinHashTable {
640 pub fn new(build_columns: Vec<u32>, probe_columns: Vec<u32>) -> Self {
641 Self {
642 build_columns,
643 probe_columns,
644 }
645 }
646
647 pub fn build(&self, build_chunks: &[DataChunk]) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
650 let total_rows: usize = build_chunks.iter().map(|c| c.size).sum();
651 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
652
653 if total_rows > 1000 {
654 self.build_parallel(build_chunks, build_col, total_rows)
655 } else {
656 self.build_sequential(build_chunks, build_col, total_rows)
657 }
658 }
659
660 fn build_sequential(
661 &self,
662 build_chunks: &[DataChunk],
663 build_col: usize,
664 total_rows: usize,
665 ) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
666 let mut table: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
668 hashbrown::HashMap::with_capacity(total_rows * 4 / 3);
669
670 for (ci, chunk) in build_chunks.iter().enumerate() {
671 for row in 0..chunk.size {
672 let Some(hash) = hash_chunk_cell(chunk, build_col, row) else {
673 continue;
674 };
675 table
676 .entry(hash)
677 .or_insert_with(|| Vec::with_capacity(4))
678 .push((ci, row));
679 }
680 }
681 table
682 }
683
684 fn build_parallel(
685 &self,
686 build_chunks: &[DataChunk],
687 build_col: usize,
688 total_rows: usize,
689 ) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
690 use rayon::prelude::*;
691
692 let tables: Vec<hashbrown::HashMap<u64, Vec<(usize, usize)>>> = build_chunks
693 .par_iter()
694 .enumerate()
695 .map(|(ci, chunk)| {
696 let mut local: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
697 hashbrown::HashMap::with_capacity(chunk.size * 4 / 3);
698 for row in 0..chunk.size {
699 let Some(hash) = hash_chunk_cell(chunk, build_col, row) else {
700 continue;
701 };
702 local
703 .entry(hash)
704 .or_insert_with(|| Vec::with_capacity(4))
705 .push((ci, row));
706 }
707 local
708 })
709 .collect();
710
711 let mut merged: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
713 hashbrown::HashMap::with_capacity(total_rows * 4 / 3);
714 for local in tables {
715 for (hash, locations) in local {
716 merged
717 .entry(hash)
718 .or_insert_with(|| Vec::with_capacity(locations.len()))
719 .extend(locations);
720 }
721 }
722 merged
723 }
724
725 pub fn probe(
728 &self,
729 hash_table: &hashbrown::HashMap<u64, Vec<(usize, usize)>>,
730 build_chunks: &[DataChunk],
731 probe_chunks: &[DataChunk],
732 ) -> OperatorResult {
733 let probe_col = self.probe_columns.first().copied().unwrap_or(0) as usize;
734 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
735
736 let num_build_fields = build_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
738 let num_probe_fields = probe_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
739 let total_cols = num_build_fields + num_probe_fields;
740
741 if total_cols == 0 {
742 return Ok(Vec::new());
743 }
744
745 let mut output_types: Vec<PhysicalTypeID> = Vec::with_capacity(total_cols);
747 if let Some(bc) = build_chunks.first() {
748 for col in 0..bc.num_fields() {
749 output_types.push(bc.field_types[col]);
750 }
751 }
752 if let Some(pc) = probe_chunks.first() {
753 for col in 0..pc.num_fields() {
754 output_types.push(pc.field_types[col]);
755 }
756 }
757
758 let total_probe_rows: usize = probe_chunks.iter().map(|c| c.size).sum();
759 let mut matches: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(total_probe_rows);
760
761 for (pci, chunk) in probe_chunks.iter().enumerate() {
762 for row in 0..chunk.size {
763 let Some(probe_hash) = hash_chunk_cell(chunk, probe_col, row) else {
764 continue;
765 };
766
767 if let Some(locations) = hash_table.get(&probe_hash) {
768 for &(bci, brow) in locations {
770 if chunk_cells_equal(&build_chunks[bci], build_col, brow, chunk, probe_col, row) {
771 matches.push((bci, brow, pci, row));
772 }
773 }
774 }
775 }
776 }
777
778 if matches.is_empty() {
779 return Ok(Vec::new());
780 }
781
782 let num_rows = matches.len();
784 let mut result_fields: Vec<ValueVector> = output_types
785 .iter()
786 .map(|t| {
787 let mut v = ValueVector::new(*t, num_rows);
788 v.resize(num_rows);
789 v
790 })
791 .collect();
792
793 for (out_row, &(bci, brow, pci, prow)) in matches.iter().enumerate() {
794 for col in 0..num_build_fields {
796 if let Some(_field) = build_chunks[bci].fields.get(col) {
797 let val = build_chunks[bci].get_value(col, brow).unwrap_or(Value::Null);
798 if matches!(val, Value::Null) {
799 result_fields[col].set_null(out_row, true);
800 } else {
801 store_value_in_vector(&mut result_fields[col], out_row, &val)?;
802 }
803 }
804 }
805 for col in 0..num_probe_fields {
807 if let Some(_field) = probe_chunks[pci].fields.get(col) {
808 let val = probe_chunks[pci].get_value(col, prow).unwrap_or(Value::Null);
809 if matches!(val, Value::Null) {
810 result_fields[num_build_fields + col].set_null(out_row, true);
811 } else {
812 store_value_in_vector(&mut result_fields[num_build_fields + col], out_row, &val)?;
813 }
814 }
815 }
816 }
817
818 let arrow_fields = result_fields
819 .iter()
820 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
821 .collect::<Vec<_>>();
822 let arrow_field_types = result_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
823 Ok(vec![DataChunk {
824 fields: arrow_fields,
825 field_types: arrow_field_types,
826 size: num_rows,
827 field_names: vec![],
828 sel_vector: None,
829 }])
830 }
831}
832
833pub struct PhysicalHashJoin {
836 pub build_columns: Vec<u32>,
837 pub probe_columns: Vec<u32>,
838}
839
840impl PhysicalHashJoin {
841 pub fn new(build_columns: Vec<u32>, probe_columns: Vec<u32>) -> Self {
842 Self {
843 build_columns,
844 probe_columns,
845 }
846 }
847}
848
849impl PhysicalHashJoin {
850 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
851 if build_chunks.is_empty() || probe_chunks.is_empty() {
852 return Ok(vec![]);
853 }
854
855 let join_table = JoinHashTable::new(self.build_columns.clone(), self.probe_columns.clone());
857 let hash_table = join_table.build(build_chunks);
858 let mut result = join_table.probe(&hash_table, build_chunks, probe_chunks)?;
859
860 if !result.is_empty() {
862 let mut output_names: Vec<String> = build_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default();
863 output_names.extend(probe_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default());
864 result[0].field_names = output_names;
865 }
866
867 Ok(result)
868 }
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 fn make_i64_chunk(values: &[i64]) -> DataChunk {
876 let mut v = ValueVector::new(PhysicalTypeID::Int64, values.len().max(1));
877 for (i, val) in values.iter().enumerate() {
878 v.set_i64(i, *val);
879 }
880 v.resize(values.len());
881 let ptype = v.physical_type();
882 let fields = vec![akar_common::arrow_vector::ArrowVector::from_legacy(&v).array];
883 DataChunk::new(fields, vec![ptype])
884 }
885
886 fn make_u64_chunk(values: &[u64]) -> DataChunk {
887 let mut v = ValueVector::new(PhysicalTypeID::UInt64, values.len().max(1));
888 for (i, val) in values.iter().enumerate() {
889 let _ = v.set_value(i, &Value::UInt64(*val));
890 }
891 v.resize(values.len());
892 let ptype = v.physical_type();
893 let fields = vec![akar_common::arrow_vector::ArrowVector::from_legacy(&v).array];
894 DataChunk::new(fields, vec![ptype])
895 }
896
897 fn make_two_col_chunk(rows: &[(i64, i64)]) -> DataChunk {
899 let mut label = ValueVector::new(PhysicalTypeID::Int64, rows.len().max(1));
900 let mut id = ValueVector::new(PhysicalTypeID::Int64, rows.len().max(1));
901 for (i, (l, v)) in rows.iter().enumerate() {
902 label.set_i64(i, *l);
903 id.set_i64(i, *v);
904 }
905 label.resize(rows.len());
906 id.resize(rows.len());
907 let ptype = label.physical_type();
908 let fields = vec![
909 akar_common::arrow_vector::ArrowVector::from_legacy(&label).array,
910 akar_common::arrow_vector::ArrowVector::from_legacy(&id).array,
911 ];
912 let mut chunk = DataChunk::new(fields, vec![ptype, ptype]);
913 chunk.field_names = vec!["label".into(), "id".into()];
914 chunk
915 }
916
917 #[test]
918 fn test_semi_join_hash_collision_no_false_match() {
919 let build = make_i64_chunk(&[7]);
922 let probe = make_u64_chunk(&[7]);
923 let semi = PhysicalSemiJoin {
924 build_columns: vec![0],
925 probe_columns: vec![0],
926 };
927 let result = semi.execute_binary(&[build], &[probe]).unwrap();
928 assert!(
929 result.is_empty(),
930 "hash collision must not produce a semi-join match, got {:?}",
931 result
932 );
933 }
934
935 #[test]
936 fn test_anti_join_hash_collision_keeps_probe() {
937 let build = make_i64_chunk(&[7]);
938 let probe = make_u64_chunk(&[7]);
939 let anti = PhysicalAntiJoin {
940 build_columns: vec![0],
941 probe_columns: vec![0],
942 };
943 let result = anti.execute_binary(&[build], &[probe]).unwrap();
944 assert_eq!(result[0].size, 1, "hash collision must not drop the probe row");
945 }
946
947 #[test]
948 fn test_semi_join_uses_key_column() {
949 let build = make_two_col_chunk(&[(5, 10), (5, 20)]);
951 let probe = make_two_col_chunk(&[(1, 10), (2, 30), (3, 20)]);
952 let semi = PhysicalSemiJoin {
953 build_columns: vec![1],
954 probe_columns: vec![1],
955 };
956 let result = semi.execute_binary(&[build], &[probe]).unwrap();
957 assert_eq!(result[0].size, 2, "probe rows with id 10 and 20 should match");
958 let got = result[0].get_i64(1, 0).unwrap_or(0);
959 assert_eq!(got, 10, "first matched row id");
960 let got = result[0].get_i64(1, 1).unwrap_or(0);
961 assert_eq!(got, 20, "second matched row id");
962 }
963
964 #[test]
965 fn test_anti_join_uses_key_column() {
966 let build = make_two_col_chunk(&[(5, 10), (5, 20)]);
967 let probe = make_two_col_chunk(&[(1, 10), (2, 30), (3, 20)]);
968 let anti = PhysicalAntiJoin {
969 build_columns: vec![1],
970 probe_columns: vec![1],
971 };
972 let result = anti.execute_binary(&[build], &[probe]).unwrap();
973 assert_eq!(result[0].size, 1, "only probe row with id 30 should remain");
974 let got = result[0].get_i64(1, 0).unwrap_or(0);
975 assert_eq!(got, 30, "remaining row id");
976 }
977
978 #[test]
979 fn test_intersect_execute_sides_cross_product() {
980 let intersect = PhysicalIntersect {
981 num_build_sides: 2,
982 probe_key_col: 0,
983 build_key_col: 0,
984 };
985 let build1 = make_i64_chunk(&[1, 1, 5]);
986 let build2 = make_i64_chunk(&[1, 1, 1, 7]);
987 let probe = make_i64_chunk(&[1, 2]);
988 let sides = vec![vec![build1], vec![build2]];
989 let result = intersect.execute_sides(&sides, &[probe]).unwrap();
990 assert!(!result.is_empty(), "expected non-empty result");
991 assert_eq!(result[0].size, 6, "expected 2x3 cross product for probe key 1");
992 assert_eq!(result[0].fields.len(), 3, "probe + 2 build columns");
993 }
994
995 #[test]
996 fn test_intersect_execute_sides_key_resolution() {
997 let mut probe_v = ValueVector::new(PhysicalTypeID::Int64, 2);
998 probe_v.set_i64(0, 10);
999 probe_v.set_i64(1, 20);
1000 let mut probe_id = ValueVector::new(PhysicalTypeID::Int64, 2);
1001 probe_id.set_i64(0, 1);
1002 probe_id.set_i64(1, 2);
1003 let ptype = probe_v.physical_type();
1004 let probe_fields = vec![
1005 akar_common::arrow_vector::ArrowVector::from_legacy(&probe_v).array,
1006 akar_common::arrow_vector::ArrowVector::from_legacy(&probe_id).array,
1007 ];
1008 let mut probe = DataChunk::new(probe_fields, vec![ptype, ptype]);
1009 probe.field_names = vec!["a.other".into(), "a.id".into()];
1010
1011 let mut build_v = ValueVector::new(PhysicalTypeID::Int64, 2);
1012 build_v.set_i64(0, 30);
1013 build_v.set_i64(1, 40);
1014 let mut build_id = ValueVector::new(PhysicalTypeID::Int64, 2);
1015 build_id.set_i64(0, 1);
1016 build_id.set_i64(1, 1);
1017 let build_fields = vec![
1018 akar_common::arrow_vector::ArrowVector::from_legacy(&build_v).array,
1019 akar_common::arrow_vector::ArrowVector::from_legacy(&build_id).array,
1020 ];
1021 let mut build = DataChunk::new(build_fields, vec![ptype, ptype]);
1022 build.field_names = vec!["a.other".into(), "a.id".into()];
1023
1024 let intersect = PhysicalIntersect {
1025 num_build_sides: 1,
1026 probe_key_col: 1,
1027 build_key_col: 1,
1028 };
1029 let result = intersect.execute_sides(&[vec![build]], &[probe]).unwrap();
1030 assert!(!result.is_empty(), "expected non-empty result");
1031 assert_eq!(
1032 result[0].size, 2,
1033 "probe id 1 matches both build rows; id 2 matches nothing"
1034 );
1035 assert_eq!(
1036 result[0].field_names,
1037 vec![
1038 "a.other".to_string(),
1039 "a.id".to_string(),
1040 "a.other".to_string(),
1041 "a.id".to_string()
1042 ]
1043 );
1044 }
1045}