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 base = build_chunks.len() / num_builds;
449 let extra = build_chunks.len() % num_builds;
450 let mut sides: Vec<Vec<DataChunk>> = Vec::with_capacity(num_builds);
451 let mut start = 0usize;
452 for side in 0..num_builds {
453 let size = base + usize::from(side < extra);
454 let end = start + size;
455 sides.push(build_chunks[start..end].to_vec());
456 start = end;
457 }
458
459 self.execute_sides(&sides, probe_chunks)
460 }
461
462 pub fn execute_sides(&self, build_sides: &[Vec<DataChunk>], probe_chunks: &[DataChunk]) -> OperatorResult {
471 let num_builds = build_sides.len().max(1);
472 if probe_chunks.is_empty() {
473 return Ok(vec![]);
474 }
475
476 let build_col = self.build_key_col as usize;
477 let probe_col = self.probe_key_col as usize;
478
479 let mut build_tables: Vec<HashJoinTable> = Vec::with_capacity(num_builds);
481 let mut side_field_names: Vec<Vec<String>> = Vec::with_capacity(num_builds);
482 let mut side_field_counts: Vec<usize> = Vec::with_capacity(num_builds);
483
484 for side in build_sides {
485 let mut ht: HashJoinTable = HashMap::new();
486 let mut names: Vec<String> = Vec::new();
487 let mut count = 0usize;
488
489 for (ci, chunk) in side.iter().enumerate() {
490 if ci == 0 {
491 names = chunk.field_names.clone();
492 count = chunk.fields.len();
493 }
494 for row in 0..chunk.size {
495 if chunk.fields.get(build_col).is_none() {
496 continue;
497 }
498 let key = chunk.get_value(build_col, row).unwrap_or(Value::Null);
499 if matches!(key, Value::Null) {
500 continue;
501 }
502 let hash = value_hash(&key);
503 ht.entry(hash).or_default().push((key, vec![(ci, row)]));
504 }
505 }
506
507 side_field_names.push(names);
508 side_field_counts.push(count);
509 build_tables.push(ht);
510 }
511
512 if build_tables.iter().any(|t| t.is_empty()) {
513 return Ok(vec![]);
515 }
516
517 let probe_field_names = probe_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default();
518 let probe_field_count = probe_chunks.first().map(|c| c.fields.len()).unwrap_or(0);
519 let mut output_rows: Vec<Vec<Value>> = Vec::new();
520
521 for (ci, chunk) in probe_chunks.iter().enumerate() {
522 let _ = ci;
523 for row in 0..chunk.size {
524 let probe_key = chunk.get_value(probe_col, row).unwrap_or(Value::Null);
525 if matches!(probe_key, Value::Null) {
526 continue;
527 }
528 let probe_hash = value_hash(&probe_key);
529
530 let mut matches_per_side: Vec<Vec<(usize, usize)>> = Vec::with_capacity(num_builds);
532 let mut all_match = true;
533 for ht in &build_tables {
534 let mut side_matches: Vec<(usize, usize)> = Vec::new();
535 if let Some(bucket) = ht.get(&probe_hash) {
536 for (stored_key, locations) in bucket {
537 if stored_key == &probe_key {
538 side_matches.extend(locations.iter().cloned());
539 }
540 }
541 }
542 if side_matches.is_empty() {
543 all_match = false;
544 break;
545 }
546 matches_per_side.push(side_matches);
547 }
548 if !all_match {
549 continue;
550 }
551
552 let mut combos: Vec<Vec<(usize, usize)>> = vec![vec![]];
554 for side_matches in &matches_per_side {
555 let mut next = Vec::with_capacity(combos.len() * side_matches.len());
556 for combo in &combos {
557 for m in side_matches {
558 let mut c = combo.clone();
559 c.push(*m);
560 next.push(c);
561 }
562 }
563 combos = next;
564 }
565
566 let per_row_cols = probe_field_count + side_field_counts.iter().sum::<usize>();
567 for combo in combos {
568 let mut row_values: Vec<Value> = Vec::with_capacity(per_row_cols);
569 for col_in_probe in 0..probe_field_count {
571 row_values.push(chunk.get_value(col_in_probe, row).unwrap_or(Value::Null));
572 }
573 for (side_idx, &(b_ci, b_row)) in combo.iter().enumerate() {
575 if let Some(side_chunk) = build_sides.get(side_idx).and_then(|s| s.get(b_ci)) {
576 for col in 0..side_chunk.fields.len() {
577 row_values.push(side_chunk.get_value(col, b_row).unwrap_or(Value::Null));
578 }
579 }
580 }
581 output_rows.push(row_values);
582 }
583 }
584 }
585
586 if output_rows.is_empty() {
587 return Ok(vec![]);
588 }
589
590 let output_size = output_rows.len();
592 let mut output_fields: Vec<ValueVector> = Vec::with_capacity(output_rows.first().map(|r| r.len()).unwrap_or(0));
593
594 if let Some(first_row) = output_rows.first() {
595 for val in first_row {
596 let ptype = val.physical_type();
597 let mut vv = ValueVector::new(ptype, output_size);
598 vv.resize(output_size);
599 output_fields.push(vv);
600 }
601 }
602
603 for (out_idx, row_values) in output_rows.iter().enumerate() {
604 for (col, val) in row_values.iter().enumerate() {
605 if let Some(field) = output_fields.get_mut(col) {
606 let _ = field.set_value(out_idx, val);
607 }
608 }
609 }
610
611 let arrow_fields = output_fields
612 .iter()
613 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
614 .collect::<Vec<_>>();
615 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
616
617 let mut field_names: Vec<String> = probe_field_names;
618 for names in &side_field_names {
619 field_names.extend(names.iter().cloned());
620 }
621
622 Ok(vec![DataChunk {
623 fields: arrow_fields,
624 field_types: arrow_field_types,
625 field_names,
626 size: output_size,
627 sel_vector: None,
628 }])
629 }
630}
631
632pub struct JoinHashTable {
642 build_columns: Vec<u32>,
643 probe_columns: Vec<u32>,
644}
645
646impl JoinHashTable {
647 pub fn new(build_columns: Vec<u32>, probe_columns: Vec<u32>) -> Self {
648 Self {
649 build_columns,
650 probe_columns,
651 }
652 }
653
654 pub fn build(&self, build_chunks: &[DataChunk]) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
657 let total_rows: usize = build_chunks.iter().map(|c| c.size).sum();
658 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
659
660 if total_rows > 1000 {
661 self.build_parallel(build_chunks, build_col, total_rows)
662 } else {
663 self.build_sequential(build_chunks, build_col, total_rows)
664 }
665 }
666
667 fn build_sequential(
668 &self,
669 build_chunks: &[DataChunk],
670 build_col: usize,
671 total_rows: usize,
672 ) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
673 let mut table: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
675 hashbrown::HashMap::with_capacity(total_rows * 4 / 3);
676
677 for (ci, chunk) in build_chunks.iter().enumerate() {
678 for row in 0..chunk.size {
679 let Some(hash) = hash_chunk_cell(chunk, build_col, row) else {
680 continue;
681 };
682 table
683 .entry(hash)
684 .or_insert_with(|| Vec::with_capacity(4))
685 .push((ci, row));
686 }
687 }
688 table
689 }
690
691 fn build_parallel(
692 &self,
693 build_chunks: &[DataChunk],
694 build_col: usize,
695 total_rows: usize,
696 ) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
697 use rayon::prelude::*;
698
699 let tables: Vec<hashbrown::HashMap<u64, Vec<(usize, usize)>>> = build_chunks
700 .par_iter()
701 .enumerate()
702 .map(|(ci, chunk)| {
703 let mut local: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
704 hashbrown::HashMap::with_capacity(chunk.size * 4 / 3);
705 for row in 0..chunk.size {
706 let Some(hash) = hash_chunk_cell(chunk, build_col, row) else {
707 continue;
708 };
709 local
710 .entry(hash)
711 .or_insert_with(|| Vec::with_capacity(4))
712 .push((ci, row));
713 }
714 local
715 })
716 .collect();
717
718 let mut merged: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
720 hashbrown::HashMap::with_capacity(total_rows * 4 / 3);
721 for local in tables {
722 for (hash, locations) in local {
723 merged
724 .entry(hash)
725 .or_insert_with(|| Vec::with_capacity(locations.len()))
726 .extend(locations);
727 }
728 }
729 merged
730 }
731
732 pub fn probe(
735 &self,
736 hash_table: &hashbrown::HashMap<u64, Vec<(usize, usize)>>,
737 build_chunks: &[DataChunk],
738 probe_chunks: &[DataChunk],
739 ) -> OperatorResult {
740 let probe_col = self.probe_columns.first().copied().unwrap_or(0) as usize;
741 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
742
743 let num_build_fields = build_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
745 let num_probe_fields = probe_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
746 let total_cols = num_build_fields + num_probe_fields;
747
748 if total_cols == 0 {
749 return Ok(Vec::new());
750 }
751
752 let mut output_types: Vec<PhysicalTypeID> = Vec::with_capacity(total_cols);
754 if let Some(bc) = build_chunks.first() {
755 for col in 0..bc.num_fields() {
756 output_types.push(bc.field_types[col]);
757 }
758 }
759 if let Some(pc) = probe_chunks.first() {
760 for col in 0..pc.num_fields() {
761 output_types.push(pc.field_types[col]);
762 }
763 }
764
765 let total_probe_rows: usize = probe_chunks.iter().map(|c| c.size).sum();
766 let mut matches: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(total_probe_rows);
767
768 for (pci, chunk) in probe_chunks.iter().enumerate() {
769 for row in 0..chunk.size {
770 let Some(probe_hash) = hash_chunk_cell(chunk, probe_col, row) else {
771 continue;
772 };
773
774 if let Some(locations) = hash_table.get(&probe_hash) {
775 for &(bci, brow) in locations {
777 if chunk_cells_equal(&build_chunks[bci], build_col, brow, chunk, probe_col, row) {
778 matches.push((bci, brow, pci, row));
779 }
780 }
781 }
782 }
783 }
784
785 if matches.is_empty() {
786 return Ok(Vec::new());
787 }
788
789 let num_rows = matches.len();
794
795 let mut build_offsets: Vec<usize> = Vec::with_capacity(build_chunks.len());
797 let mut probe_offsets: Vec<usize> = Vec::with_capacity(probe_chunks.len());
798 let mut acc = 0usize;
799 for c in build_chunks {
800 build_offsets.push(acc);
801 acc += c.size;
802 }
803 let mut acc = 0usize;
804 for c in probe_chunks {
805 probe_offsets.push(acc);
806 acc += c.size;
807 }
808 let build_take: arrow::array::UInt32Array = matches
809 .iter()
810 .map(|&(bci, brow, _, _)| (build_offsets[bci] + brow) as u32)
811 .collect();
812 let probe_take: arrow::array::UInt32Array = matches
813 .iter()
814 .map(|&(_, _, pci, prow)| (probe_offsets[pci] + prow) as u32)
815 .collect();
816
817 let mut result_fields: Vec<ArrayRef> = Vec::with_capacity(total_cols);
818 for col in 0..num_build_fields {
819 let parts: Vec<ArrayRef> = build_chunks.iter().map(|c| c.fields[col].clone()).collect();
820 let concat = concat_parts(parts)?;
821 result_fields.push(arrow::compute::take(concat.as_ref(), &build_take, None).map_err(|e| e.to_string())?);
822 }
823 for col in 0..num_probe_fields {
824 let parts: Vec<ArrayRef> = probe_chunks.iter().map(|c| c.fields[col].clone()).collect();
825 let concat = concat_parts(parts)?;
826 result_fields.push(arrow::compute::take(concat.as_ref(), &probe_take, None).map_err(|e| e.to_string())?);
827 }
828
829 Ok(vec![DataChunk {
830 fields: result_fields,
831 field_types: output_types,
832 size: num_rows,
833 field_names: vec![],
834 sel_vector: None,
835 }])
836 }
837}
838
839fn concat_parts(parts: Vec<ArrayRef>) -> Result<ArrayRef, String> {
841 if parts.len() == 1 {
842 Ok(parts.into_iter().next().unwrap())
843 } else {
844 let refs: Vec<&dyn arrow::array::Array> = parts.iter().map(|a| a.as_ref()).collect();
845 arrow::compute::concat(&refs).map_err(|e| e.to_string())
846 }
847}
848
849pub struct PhysicalHashJoin {
852 pub build_columns: Vec<u32>,
853 pub probe_columns: Vec<u32>,
854}
855
856impl PhysicalHashJoin {
857 pub fn new(build_columns: Vec<u32>, probe_columns: Vec<u32>) -> Self {
858 Self {
859 build_columns,
860 probe_columns,
861 }
862 }
863}
864
865impl PhysicalHashJoin {
866 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
867 if build_chunks.is_empty() || probe_chunks.is_empty() {
868 return Ok(vec![]);
869 }
870
871 let join_table = JoinHashTable::new(self.build_columns.clone(), self.probe_columns.clone());
873 let hash_table = join_table.build(build_chunks);
874 let mut result = join_table.probe(&hash_table, build_chunks, probe_chunks)?;
875
876 if !result.is_empty() {
878 let mut output_names: Vec<String> = build_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default();
879 output_names.extend(probe_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default());
880 result[0].field_names = output_names;
881 }
882
883 Ok(result)
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 fn make_i64_chunk(values: &[i64]) -> DataChunk {
892 let mut v = ValueVector::new(PhysicalTypeID::Int64, values.len().max(1));
893 for (i, val) in values.iter().enumerate() {
894 v.set_i64(i, *val);
895 }
896 v.resize(values.len());
897 let ptype = v.physical_type();
898 let fields = vec![akar_common::arrow_vector::ArrowVector::from_legacy(&v).array];
899 DataChunk::new(fields, vec![ptype])
900 }
901
902 fn make_u64_chunk(values: &[u64]) -> DataChunk {
903 let mut v = ValueVector::new(PhysicalTypeID::UInt64, values.len().max(1));
904 for (i, val) in values.iter().enumerate() {
905 let _ = v.set_value(i, &Value::UInt64(*val));
906 }
907 v.resize(values.len());
908 let ptype = v.physical_type();
909 let fields = vec![akar_common::arrow_vector::ArrowVector::from_legacy(&v).array];
910 DataChunk::new(fields, vec![ptype])
911 }
912
913 fn make_two_col_chunk(rows: &[(i64, i64)]) -> DataChunk {
915 let mut label = ValueVector::new(PhysicalTypeID::Int64, rows.len().max(1));
916 let mut id = ValueVector::new(PhysicalTypeID::Int64, rows.len().max(1));
917 for (i, (l, v)) in rows.iter().enumerate() {
918 label.set_i64(i, *l);
919 id.set_i64(i, *v);
920 }
921 label.resize(rows.len());
922 id.resize(rows.len());
923 let ptype = label.physical_type();
924 let fields = vec![
925 akar_common::arrow_vector::ArrowVector::from_legacy(&label).array,
926 akar_common::arrow_vector::ArrowVector::from_legacy(&id).array,
927 ];
928 let mut chunk = DataChunk::new(fields, vec![ptype, ptype]);
929 chunk.field_names = vec!["label".into(), "id".into()];
930 chunk
931 }
932
933 #[test]
934 fn test_semi_join_hash_collision_no_false_match() {
935 let build = make_i64_chunk(&[7]);
938 let probe = make_u64_chunk(&[7]);
939 let semi = PhysicalSemiJoin {
940 build_columns: vec![0],
941 probe_columns: vec![0],
942 };
943 let result = semi.execute_binary(&[build], &[probe]).unwrap();
944 assert!(
945 result.is_empty(),
946 "hash collision must not produce a semi-join match, got {:?}",
947 result
948 );
949 }
950
951 #[test]
952 fn test_anti_join_hash_collision_keeps_probe() {
953 let build = make_i64_chunk(&[7]);
954 let probe = make_u64_chunk(&[7]);
955 let anti = PhysicalAntiJoin {
956 build_columns: vec![0],
957 probe_columns: vec![0],
958 };
959 let result = anti.execute_binary(&[build], &[probe]).unwrap();
960 assert_eq!(result[0].size, 1, "hash collision must not drop the probe row");
961 }
962
963 #[test]
964 fn test_semi_join_uses_key_column() {
965 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 semi = PhysicalSemiJoin {
969 build_columns: vec![1],
970 probe_columns: vec![1],
971 };
972 let result = semi.execute_binary(&[build], &[probe]).unwrap();
973 assert_eq!(result[0].size, 2, "probe rows with id 10 and 20 should match");
974 let got = result[0].get_i64(1, 0).unwrap_or(0);
975 assert_eq!(got, 10, "first matched row id");
976 let got = result[0].get_i64(1, 1).unwrap_or(0);
977 assert_eq!(got, 20, "second matched row id");
978 }
979
980 #[test]
981 fn test_anti_join_uses_key_column() {
982 let build = make_two_col_chunk(&[(5, 10), (5, 20)]);
983 let probe = make_two_col_chunk(&[(1, 10), (2, 30), (3, 20)]);
984 let anti = PhysicalAntiJoin {
985 build_columns: vec![1],
986 probe_columns: vec![1],
987 };
988 let result = anti.execute_binary(&[build], &[probe]).unwrap();
989 assert_eq!(result[0].size, 1, "only probe row with id 30 should remain");
990 let got = result[0].get_i64(1, 0).unwrap_or(0);
991 assert_eq!(got, 30, "remaining row id");
992 }
993
994 #[test]
995 fn test_intersect_execute_sides_cross_product() {
996 let intersect = PhysicalIntersect {
997 num_build_sides: 2,
998 probe_key_col: 0,
999 build_key_col: 0,
1000 };
1001 let build1 = make_i64_chunk(&[1, 1, 5]);
1002 let build2 = make_i64_chunk(&[1, 1, 1, 7]);
1003 let probe = make_i64_chunk(&[1, 2]);
1004 let sides = vec![vec![build1], vec![build2]];
1005 let result = intersect.execute_sides(&sides, &[probe]).unwrap();
1006 assert!(!result.is_empty(), "expected non-empty result");
1007 assert_eq!(result[0].size, 6, "expected 2x3 cross product for probe key 1");
1008 assert_eq!(result[0].fields.len(), 3, "probe + 2 build columns");
1009 }
1010
1011 #[test]
1012 fn test_intersect_execute_sides_key_resolution() {
1013 let mut probe_v = ValueVector::new(PhysicalTypeID::Int64, 2);
1014 probe_v.set_i64(0, 10);
1015 probe_v.set_i64(1, 20);
1016 let mut probe_id = ValueVector::new(PhysicalTypeID::Int64, 2);
1017 probe_id.set_i64(0, 1);
1018 probe_id.set_i64(1, 2);
1019 let ptype = probe_v.physical_type();
1020 let probe_fields = vec![
1021 akar_common::arrow_vector::ArrowVector::from_legacy(&probe_v).array,
1022 akar_common::arrow_vector::ArrowVector::from_legacy(&probe_id).array,
1023 ];
1024 let mut probe = DataChunk::new(probe_fields, vec![ptype, ptype]);
1025 probe.field_names = vec!["a.other".into(), "a.id".into()];
1026
1027 let mut build_v = ValueVector::new(PhysicalTypeID::Int64, 2);
1028 build_v.set_i64(0, 30);
1029 build_v.set_i64(1, 40);
1030 let mut build_id = ValueVector::new(PhysicalTypeID::Int64, 2);
1031 build_id.set_i64(0, 1);
1032 build_id.set_i64(1, 1);
1033 let build_fields = vec![
1034 akar_common::arrow_vector::ArrowVector::from_legacy(&build_v).array,
1035 akar_common::arrow_vector::ArrowVector::from_legacy(&build_id).array,
1036 ];
1037 let mut build = DataChunk::new(build_fields, vec![ptype, ptype]);
1038 build.field_names = vec!["a.other".into(), "a.id".into()];
1039
1040 let intersect = PhysicalIntersect {
1041 num_build_sides: 1,
1042 probe_key_col: 1,
1043 build_key_col: 1,
1044 };
1045 let result = intersect.execute_sides(&[vec![build]], &[probe]).unwrap();
1046 assert!(!result.is_empty(), "expected non-empty result");
1047 assert_eq!(
1048 result[0].size, 2,
1049 "probe id 1 matches both build rows; id 2 matches nothing"
1050 );
1051 assert_eq!(
1052 result[0].field_names,
1053 vec![
1054 "a.other".to_string(),
1055 "a.id".to_string(),
1056 "a.other".to_string(),
1057 "a.id".to_string()
1058 ]
1059 );
1060 }
1061}