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