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_set: std::collections::HashSet<u64> = std::collections::HashSet::new();
226 for chunk in build_chunks {
227 for row in 0..chunk.size {
228 if chunk.fields.get(build_col).is_some() {
229 let key = chunk.get_value(build_col, row).unwrap_or(Value::Null);
230 if matches!(key, Value::Null) {
231 continue;
232 }
233 hash_set.insert(value_hash(&key));
234 }
235 }
236 }
237
238 let num_probe_fields = probe_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
240 let mut probe_types: Vec<PhysicalTypeID> = Vec::with_capacity(num_probe_fields);
241 if let Some(first) = probe_chunks.first() {
242 for col in 0..first.num_fields() {
243 probe_types.push(first.field_types[col]);
244 }
245 }
246
247 let total_probe_rows: usize = probe_chunks.iter().map(|c| c.size).sum();
248 let mut match_rows: Vec<(usize, usize)> = Vec::with_capacity(total_probe_rows);
249 for (ci, chunk) in probe_chunks.iter().enumerate() {
250 for row in 0..chunk.size {
251 if chunk.fields.get(probe_col).is_some() {
252 let key = chunk.get_value(probe_col, row).unwrap_or(Value::Null);
253 if matches!(key, Value::Null) {
254 continue;
255 }
256 if hash_set.contains(&value_hash(&key)) {
257 match_rows.push((ci, row));
258 }
259 }
260 }
261 }
262
263 if match_rows.is_empty() {
264 return Ok(vec![]);
265 }
266
267 let num_left_cols = probe_types.len();
269 let mut output_fields: Vec<ValueVector> = probe_types
270 .iter()
271 .map(|t| ValueVector::new(*t, match_rows.len().max(1)))
272 .collect();
273
274 for (out_idx, (ci, row)) in match_rows.iter().enumerate() {
275 if let Some(chunk) = probe_chunks.get(*ci) {
276 for (col, out_field) in output_fields.iter_mut().enumerate().take(num_left_cols) {
277 if chunk.fields.get(col).is_some() {
278 let val = chunk.get_value(col, *row).unwrap_or(Value::Null);
279 let _ = out_field.set_value(out_idx, &val);
280 }
281 }
282 }
283 }
284 for field in &mut output_fields {
285 field.resize(match_rows.len());
286 }
287 let arrow_fields = output_fields
288 .iter()
289 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
290 .collect::<Vec<_>>();
291 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
292 Ok(vec![DataChunk {
293 fields: arrow_fields,
294 field_types: arrow_field_types,
295 size: match_rows.len(),
296 field_names: vec![],
297 sel_vector: None,
298 }])
299 }
300}
301
302pub struct PhysicalAntiJoin {
307 pub build_columns: Vec<u32>,
308 pub probe_columns: Vec<u32>,
309}
310
311impl PhysicalAntiJoin {
312 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
313 if probe_chunks.is_empty() {
314 return Ok(vec![]);
315 }
316 if build_chunks.is_empty() {
317 return Ok(probe_chunks.to_vec());
319 }
320
321 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
322 let probe_col = self.probe_columns.first().copied().unwrap_or(0) as usize;
323
324 let mut hash_set: std::collections::HashSet<u64> = std::collections::HashSet::new();
326 for chunk in build_chunks {
327 for row in 0..chunk.size {
328 if chunk.fields.get(build_col).is_some() {
329 let key = chunk.get_value(build_col, row).unwrap_or(Value::Null);
330 if matches!(key, Value::Null) {
331 continue;
332 }
333 hash_set.insert(value_hash(&key));
334 }
335 }
336 }
337
338 let num_probe_fields = probe_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
339 let mut probe_types: Vec<PhysicalTypeID> = Vec::with_capacity(num_probe_fields);
340 if let Some(first) = probe_chunks.first() {
341 for col in 0..first.num_fields() {
342 probe_types.push(first.field_types[col]);
343 }
344 }
345
346 let total_probe_rows: usize = probe_chunks.iter().map(|c| c.size).sum();
347 let mut non_match_rows: Vec<(usize, usize)> = Vec::with_capacity(total_probe_rows);
348 for (ci, chunk) in probe_chunks.iter().enumerate() {
349 for row in 0..chunk.size {
350 if let Some(_field) = chunk.fields.get(probe_col) {
351 let key = chunk.get_value(probe_col, row).unwrap_or(Value::Null);
352 if matches!(key, Value::Null) {
353 continue;
354 }
355 if !hash_set.contains(&value_hash(&key)) {
356 non_match_rows.push((ci, row));
357 }
358 }
359 }
360 }
361
362 if non_match_rows.is_empty() {
363 return Ok(vec![]);
364 }
365
366 let num_left_cols = probe_types.len();
367 let mut output_fields: Vec<ValueVector> = probe_types
368 .iter()
369 .map(|t| ValueVector::new(*t, non_match_rows.len().max(1)))
370 .collect();
371
372 for (out_idx, (ci, row)) in non_match_rows.iter().enumerate() {
373 if let Some(chunk) = probe_chunks.get(*ci) {
374 for (col, out_field) in output_fields.iter_mut().enumerate().take(num_left_cols) {
375 if let Some(_field) = chunk.fields.get(col) {
376 let val = chunk.get_value(col, *row).unwrap_or(Value::Null);
377 let _ = out_field.set_value(out_idx, &val);
378 }
379 }
380 }
381 }
382 for field in &mut output_fields {
383 field.resize(non_match_rows.len());
384 }
385 let arrow_fields = output_fields
386 .iter()
387 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
388 .collect::<Vec<_>>();
389 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
390 Ok(vec![DataChunk {
391 fields: arrow_fields,
392 field_types: arrow_field_types,
393 size: non_match_rows.len(),
394 field_names: vec![],
395 sel_vector: None,
396 }])
397 }
398}
399
400pub struct PhysicalIntersect {
415 pub num_build_sides: u32,
417 pub probe_key_col: u32,
419 pub build_key_col: u32,
421}
422
423impl PhysicalIntersect {
424 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
425 let num_builds = self.num_build_sides.max(1) as usize;
426 if build_chunks.is_empty() || probe_chunks.is_empty() {
427 return Ok(vec![]);
428 }
429
430 let chunk_group_size = (build_chunks.len() / num_builds).max(1);
433 let mut sides: Vec<Vec<DataChunk>> = Vec::with_capacity(num_builds);
434 for side in 0..num_builds {
435 let start = side * chunk_group_size;
436 let end = (start + chunk_group_size).min(build_chunks.len());
437 sides.push(build_chunks[start..end].to_vec());
438 }
439
440 self.execute_sides(&sides, probe_chunks)
441 }
442
443 pub fn execute_sides(&self, build_sides: &[Vec<DataChunk>], probe_chunks: &[DataChunk]) -> OperatorResult {
452 let num_builds = build_sides.len().max(1);
453 if probe_chunks.is_empty() {
454 return Ok(vec![]);
455 }
456
457 let build_col = self.build_key_col as usize;
458 let probe_col = self.probe_key_col as usize;
459
460 let mut build_tables: Vec<HashJoinTable> = Vec::with_capacity(num_builds);
462 let mut side_field_names: Vec<Vec<String>> = Vec::with_capacity(num_builds);
463 let mut side_field_counts: Vec<usize> = Vec::with_capacity(num_builds);
464
465 for side in build_sides {
466 let mut ht: HashJoinTable = HashMap::new();
467 let mut names: Vec<String> = Vec::new();
468 let mut count = 0usize;
469
470 for (ci, chunk) in side.iter().enumerate() {
471 if ci == 0 {
472 names = chunk.field_names.clone();
473 count = chunk.fields.len();
474 }
475 for row in 0..chunk.size {
476 if chunk.fields.get(build_col).is_none() {
477 continue;
478 }
479 let key = chunk.get_value(build_col, row).unwrap_or(Value::Null);
480 if matches!(key, Value::Null) {
481 continue;
482 }
483 let hash = value_hash(&key);
484 ht.entry(hash).or_default().push((key, vec![(ci, row)]));
485 }
486 }
487
488 side_field_names.push(names);
489 side_field_counts.push(count);
490 build_tables.push(ht);
491 }
492
493 if build_tables.iter().any(|t| t.is_empty()) {
494 return Ok(vec![]);
496 }
497
498 let probe_field_names = probe_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default();
499 let probe_field_count = probe_chunks.first().map(|c| c.fields.len()).unwrap_or(0);
500 let mut output_rows: Vec<Vec<Value>> = Vec::new();
501
502 for (ci, chunk) in probe_chunks.iter().enumerate() {
503 let _ = ci;
504 for row in 0..chunk.size {
505 let probe_key = chunk.get_value(probe_col, row).unwrap_or(Value::Null);
506 if matches!(probe_key, Value::Null) {
507 continue;
508 }
509 let probe_hash = value_hash(&probe_key);
510
511 let mut matches_per_side: Vec<Vec<(usize, usize)>> = Vec::with_capacity(num_builds);
513 let mut all_match = true;
514 for ht in &build_tables {
515 let mut side_matches: Vec<(usize, usize)> = Vec::new();
516 if let Some(bucket) = ht.get(&probe_hash) {
517 for (stored_key, locations) in bucket {
518 if stored_key == &probe_key {
519 side_matches.extend(locations.iter().cloned());
520 }
521 }
522 }
523 if side_matches.is_empty() {
524 all_match = false;
525 break;
526 }
527 matches_per_side.push(side_matches);
528 }
529 if !all_match {
530 continue;
531 }
532
533 let mut combos: Vec<Vec<(usize, usize)>> = vec![vec![]];
535 for side_matches in &matches_per_side {
536 let mut next = Vec::with_capacity(combos.len() * side_matches.len());
537 for combo in &combos {
538 for m in side_matches {
539 let mut c = combo.clone();
540 c.push(*m);
541 next.push(c);
542 }
543 }
544 combos = next;
545 }
546
547 let per_row_cols = probe_field_count + side_field_counts.iter().sum::<usize>();
548 for combo in combos {
549 let mut row_values: Vec<Value> = Vec::with_capacity(per_row_cols);
550 for col_in_probe in 0..probe_field_count {
552 row_values.push(chunk.get_value(col_in_probe, row).unwrap_or(Value::Null));
553 }
554 for (side_idx, &(b_ci, b_row)) in combo.iter().enumerate() {
556 if let Some(side_chunk) = build_sides.get(side_idx).and_then(|s| s.get(b_ci)) {
557 for col in 0..side_chunk.fields.len() {
558 row_values.push(side_chunk.get_value(col, b_row).unwrap_or(Value::Null));
559 }
560 }
561 }
562 output_rows.push(row_values);
563 }
564 }
565 }
566
567 if output_rows.is_empty() {
568 return Ok(vec![]);
569 }
570
571 let output_size = output_rows.len();
573 let mut output_fields: Vec<ValueVector> = Vec::with_capacity(output_rows.first().map(|r| r.len()).unwrap_or(0));
574
575 if let Some(first_row) = output_rows.first() {
576 for val in first_row {
577 let ptype = val.physical_type();
578 let mut vv = ValueVector::new(ptype, output_size);
579 vv.resize(output_size);
580 output_fields.push(vv);
581 }
582 }
583
584 for (out_idx, row_values) in output_rows.iter().enumerate() {
585 for (col, val) in row_values.iter().enumerate() {
586 if let Some(field) = output_fields.get_mut(col) {
587 let _ = field.set_value(out_idx, val);
588 }
589 }
590 }
591
592 let arrow_fields = output_fields
593 .iter()
594 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
595 .collect::<Vec<_>>();
596 let arrow_field_types = output_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
597
598 let mut field_names: Vec<String> = probe_field_names;
599 for names in &side_field_names {
600 field_names.extend(names.iter().cloned());
601 }
602
603 Ok(vec![DataChunk {
604 fields: arrow_fields,
605 field_types: arrow_field_types,
606 field_names,
607 size: output_size,
608 sel_vector: None,
609 }])
610 }
611}
612
613pub struct JoinHashTable {
623 build_columns: Vec<u32>,
624 probe_columns: Vec<u32>,
625}
626
627impl JoinHashTable {
628 pub fn new(build_columns: Vec<u32>, probe_columns: Vec<u32>) -> Self {
629 Self {
630 build_columns,
631 probe_columns,
632 }
633 }
634
635 pub fn build(&self, build_chunks: &[DataChunk]) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
638 let total_rows: usize = build_chunks.iter().map(|c| c.size).sum();
639 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
640
641 if total_rows > 1000 {
642 self.build_parallel(build_chunks, build_col, total_rows)
643 } else {
644 self.build_sequential(build_chunks, build_col, total_rows)
645 }
646 }
647
648 fn build_sequential(
649 &self,
650 build_chunks: &[DataChunk],
651 build_col: usize,
652 total_rows: usize,
653 ) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
654 let mut table: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
656 hashbrown::HashMap::with_capacity(total_rows * 4 / 3);
657
658 for (ci, chunk) in build_chunks.iter().enumerate() {
659 for row in 0..chunk.size {
660 let Some(hash) = hash_chunk_cell(chunk, build_col, row) else {
661 continue;
662 };
663 table
664 .entry(hash)
665 .or_insert_with(|| Vec::with_capacity(4))
666 .push((ci, row));
667 }
668 }
669 table
670 }
671
672 fn build_parallel(
673 &self,
674 build_chunks: &[DataChunk],
675 build_col: usize,
676 total_rows: usize,
677 ) -> hashbrown::HashMap<u64, Vec<(usize, usize)>> {
678 use rayon::prelude::*;
679
680 let tables: Vec<hashbrown::HashMap<u64, Vec<(usize, usize)>>> = build_chunks
681 .par_iter()
682 .enumerate()
683 .map(|(ci, chunk)| {
684 let mut local: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
685 hashbrown::HashMap::with_capacity(chunk.size * 4 / 3);
686 for row in 0..chunk.size {
687 let Some(hash) = hash_chunk_cell(chunk, build_col, row) else {
688 continue;
689 };
690 local
691 .entry(hash)
692 .or_insert_with(|| Vec::with_capacity(4))
693 .push((ci, row));
694 }
695 local
696 })
697 .collect();
698
699 let mut merged: hashbrown::HashMap<u64, Vec<(usize, usize)>> =
701 hashbrown::HashMap::with_capacity(total_rows * 4 / 3);
702 for local in tables {
703 for (hash, locations) in local {
704 merged
705 .entry(hash)
706 .or_insert_with(|| Vec::with_capacity(locations.len()))
707 .extend(locations);
708 }
709 }
710 merged
711 }
712
713 pub fn probe(
716 &self,
717 hash_table: &hashbrown::HashMap<u64, Vec<(usize, usize)>>,
718 build_chunks: &[DataChunk],
719 probe_chunks: &[DataChunk],
720 ) -> OperatorResult {
721 let probe_col = self.probe_columns.first().copied().unwrap_or(0) as usize;
722 let build_col = self.build_columns.first().copied().unwrap_or(0) as usize;
723
724 let num_build_fields = build_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
726 let num_probe_fields = probe_chunks.first().map(|c| c.num_fields()).unwrap_or(0);
727 let total_cols = num_build_fields + num_probe_fields;
728
729 if total_cols == 0 {
730 return Ok(Vec::new());
731 }
732
733 let mut output_types: Vec<PhysicalTypeID> = Vec::with_capacity(total_cols);
735 if let Some(bc) = build_chunks.first() {
736 for col in 0..bc.num_fields() {
737 output_types.push(bc.field_types[col]);
738 }
739 }
740 if let Some(pc) = probe_chunks.first() {
741 for col in 0..pc.num_fields() {
742 output_types.push(pc.field_types[col]);
743 }
744 }
745
746 let total_probe_rows: usize = probe_chunks.iter().map(|c| c.size).sum();
747 let mut matches: Vec<(usize, usize, usize, usize)> = Vec::with_capacity(total_probe_rows);
748
749 for (pci, chunk) in probe_chunks.iter().enumerate() {
750 for row in 0..chunk.size {
751 let Some(probe_hash) = hash_chunk_cell(chunk, probe_col, row) else {
752 continue;
753 };
754
755 if let Some(locations) = hash_table.get(&probe_hash) {
756 for &(bci, brow) in locations {
758 if chunk_cells_equal(&build_chunks[bci], build_col, brow, chunk, probe_col, row) {
759 matches.push((bci, brow, pci, row));
760 }
761 }
762 }
763 }
764 }
765
766 if matches.is_empty() {
767 return Ok(Vec::new());
768 }
769
770 let num_rows = matches.len();
772 let mut result_fields: Vec<ValueVector> = output_types
773 .iter()
774 .map(|t| {
775 let mut v = ValueVector::new(*t, num_rows);
776 v.resize(num_rows);
777 v
778 })
779 .collect();
780
781 for (out_row, &(bci, brow, pci, prow)) in matches.iter().enumerate() {
782 for col in 0..num_build_fields {
784 if let Some(_field) = build_chunks[bci].fields.get(col) {
785 let val = build_chunks[bci].get_value(col, brow).unwrap_or(Value::Null);
786 if matches!(val, Value::Null) {
787 result_fields[col].set_null(out_row, true);
788 } else {
789 store_value_in_vector(&mut result_fields[col], out_row, &val)?;
790 }
791 }
792 }
793 for col in 0..num_probe_fields {
795 if let Some(_field) = probe_chunks[pci].fields.get(col) {
796 let val = probe_chunks[pci].get_value(col, prow).unwrap_or(Value::Null);
797 if matches!(val, Value::Null) {
798 result_fields[num_build_fields + col].set_null(out_row, true);
799 } else {
800 store_value_in_vector(&mut result_fields[num_build_fields + col], out_row, &val)?;
801 }
802 }
803 }
804 }
805
806 let arrow_fields = result_fields
807 .iter()
808 .map(|v| akar_common::arrow_vector::ArrowVector::from_legacy(v).array)
809 .collect::<Vec<_>>();
810 let arrow_field_types = result_fields.iter().map(|v| v.physical_type()).collect::<Vec<_>>();
811 Ok(vec![DataChunk {
812 fields: arrow_fields,
813 field_types: arrow_field_types,
814 size: num_rows,
815 field_names: vec![],
816 sel_vector: None,
817 }])
818 }
819}
820
821pub struct PhysicalHashJoin {
824 pub build_columns: Vec<u32>,
825 pub probe_columns: Vec<u32>,
826}
827
828impl PhysicalHashJoin {
829 pub fn new(build_columns: Vec<u32>, probe_columns: Vec<u32>) -> Self {
830 Self {
831 build_columns,
832 probe_columns,
833 }
834 }
835}
836
837impl PhysicalHashJoin {
838 pub fn execute_binary(&self, build_chunks: &[DataChunk], probe_chunks: &[DataChunk]) -> OperatorResult {
839 if build_chunks.is_empty() || probe_chunks.is_empty() {
840 return Ok(vec![]);
841 }
842
843 let join_table = JoinHashTable::new(self.build_columns.clone(), self.probe_columns.clone());
845 let hash_table = join_table.build(build_chunks);
846 let mut result = join_table.probe(&hash_table, build_chunks, probe_chunks)?;
847
848 if !result.is_empty() {
850 let mut output_names: Vec<String> = build_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default();
851 output_names.extend(probe_chunks.first().map(|c| c.field_names.clone()).unwrap_or_default());
852 result[0].field_names = output_names;
853 }
854
855 Ok(result)
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862
863 fn make_i64_chunk(values: &[i64]) -> DataChunk {
864 let mut v = ValueVector::new(PhysicalTypeID::Int64, values.len().max(1));
865 for (i, val) in values.iter().enumerate() {
866 v.set_i64(i, *val);
867 }
868 v.resize(values.len());
869 let ptype = v.physical_type();
870 let fields = vec![akar_common::arrow_vector::ArrowVector::from_legacy(&v).array];
871 DataChunk::new(fields, vec![ptype])
872 }
873
874 #[test]
875 fn test_intersect_execute_sides_cross_product() {
876 let intersect = PhysicalIntersect {
877 num_build_sides: 2,
878 probe_key_col: 0,
879 build_key_col: 0,
880 };
881 let build1 = make_i64_chunk(&[1, 1, 5]);
882 let build2 = make_i64_chunk(&[1, 1, 1, 7]);
883 let probe = make_i64_chunk(&[1, 2]);
884 let sides = vec![vec![build1], vec![build2]];
885 let result = intersect.execute_sides(&sides, &[probe]).unwrap();
886 assert!(!result.is_empty(), "expected non-empty result");
887 assert_eq!(result[0].size, 6, "expected 2x3 cross product for probe key 1");
888 assert_eq!(result[0].fields.len(), 3, "probe + 2 build columns");
889 }
890
891 #[test]
892 fn test_intersect_execute_sides_key_resolution() {
893 let mut probe_v = ValueVector::new(PhysicalTypeID::Int64, 2);
894 probe_v.set_i64(0, 10);
895 probe_v.set_i64(1, 20);
896 let mut probe_id = ValueVector::new(PhysicalTypeID::Int64, 2);
897 probe_id.set_i64(0, 1);
898 probe_id.set_i64(1, 2);
899 let ptype = probe_v.physical_type();
900 let probe_fields = vec![
901 akar_common::arrow_vector::ArrowVector::from_legacy(&probe_v).array,
902 akar_common::arrow_vector::ArrowVector::from_legacy(&probe_id).array,
903 ];
904 let mut probe = DataChunk::new(probe_fields, vec![ptype, ptype]);
905 probe.field_names = vec!["a.other".into(), "a.id".into()];
906
907 let mut build_v = ValueVector::new(PhysicalTypeID::Int64, 2);
908 build_v.set_i64(0, 30);
909 build_v.set_i64(1, 40);
910 let mut build_id = ValueVector::new(PhysicalTypeID::Int64, 2);
911 build_id.set_i64(0, 1);
912 build_id.set_i64(1, 1);
913 let build_fields = vec![
914 akar_common::arrow_vector::ArrowVector::from_legacy(&build_v).array,
915 akar_common::arrow_vector::ArrowVector::from_legacy(&build_id).array,
916 ];
917 let mut build = DataChunk::new(build_fields, vec![ptype, ptype]);
918 build.field_names = vec!["a.other".into(), "a.id".into()];
919
920 let intersect = PhysicalIntersect {
921 num_build_sides: 1,
922 probe_key_col: 1,
923 build_key_col: 1,
924 };
925 let result = intersect.execute_sides(&[vec![build]], &[probe]).unwrap();
926 assert!(!result.is_empty(), "expected non-empty result");
927 assert_eq!(
928 result[0].size, 2,
929 "probe id 1 matches both build rows; id 2 matches nothing"
930 );
931 assert_eq!(
932 result[0].field_names,
933 vec![
934 "a.other".to_string(),
935 "a.id".to_string(),
936 "a.other".to_string(),
937 "a.id".to_string()
938 ]
939 );
940 }
941}