1use std::cmp::Ordering;
16use std::iter::empty;
17use std::marker::PhantomData;
18use std::sync::Arc;
19
20use bytes::Bytes;
21use futures_async_stream::try_stream;
22use itertools::{Either, Itertools};
23use prometheus::core::Atomic;
24use risingwave_common::array::{Array, DataChunk, RowRef};
25use risingwave_common::bitmap::{Bitmap, FilterByBitmap};
26use risingwave_common::catalog::Schema;
27use risingwave_common::hash::{HashKey, HashKeyDispatcher, PrecomputedBuildHasher};
28use risingwave_common::memory::{MemoryContext, MonitoredGlobalAlloc};
29use risingwave_common::metrics::TrAdderAtomic;
30use risingwave_common::row::Row;
31use risingwave_common::types::{DataType, DefaultOrd};
32use risingwave_common::util::chunk_coalesce::DataChunkBuilder;
33use risingwave_common_estimate_size::EstimateSize;
34use risingwave_expr::expr::{BoxedExpression, build_from_prost};
35use risingwave_pb::Message;
36use risingwave_pb::batch_plan::plan_node::NodeBody;
37use risingwave_pb::data::DataChunk as PbDataChunk;
38
39use super::{AsOfDesc, AsOfInequalityType, ChunkedData, JoinType, RowId};
40use crate::error::{BatchError, Result};
41use crate::executor::{
42 BoxedDataChunkStream, BoxedExecutor, BoxedExecutorBuilder, Executor, ExecutorBuilder,
43 WrapStreamExecutor,
44};
45use crate::monitor::BatchSpillMetrics;
46use crate::risingwave_common::hash::NullBitmap;
47use crate::spill::spill_op::SpillBackend::Disk;
48use crate::spill::spill_op::{
49 DEFAULT_SPILL_PARTITION_NUM, SPILL_AT_LEAST_MEMORY, SpillBackend, SpillBuildHasher, SpillOp,
50};
51use crate::task::ShutdownToken;
52
53pub struct HashJoinExecutor<K> {
63 join_type: JoinType,
65 #[expect(dead_code)]
67 original_schema: Schema,
68 schema: Schema,
70 output_indices: Vec<usize>,
72 probe_side_source: BoxedExecutor,
74 build_side_source: BoxedExecutor,
76 probe_key_idxs: Vec<usize>,
78 build_key_idxs: Vec<usize>,
80 cond: Option<Arc<BoxedExpression>>,
82 null_matched: Vec<bool>,
85 identity: String,
86 chunk_size: usize,
87 asof_desc: Option<AsOfDesc>,
89
90 spill_backend: Option<SpillBackend>,
91 spill_metrics: Arc<BatchSpillMetrics>,
92 memory_upper_bound: Option<u64>,
94
95 shutdown_rx: ShutdownToken,
96
97 mem_ctx: MemoryContext,
98 _phantom: PhantomData<K>,
99}
100
101impl<K: HashKey> Executor for HashJoinExecutor<K> {
102 fn schema(&self) -> &Schema {
103 &self.schema
104 }
105
106 fn identity(&self) -> &str {
107 &self.identity
108 }
109
110 fn execute(self: Box<Self>) -> BoxedDataChunkStream {
111 self.do_execute()
112 }
113}
114
115pub type JoinHashMap<K> =
148 hashbrown::HashMap<K, RowId, PrecomputedBuildHasher, MonitoredGlobalAlloc>;
149
150struct RowIdIter<'a> {
151 current_row_id: Option<RowId>,
152 next_row_id: &'a ChunkedData<Option<RowId>>,
153}
154
155impl ChunkedData<Option<RowId>> {
156 fn row_id_iter(&self, begin: Option<RowId>) -> RowIdIter<'_> {
157 RowIdIter {
158 current_row_id: begin,
159 next_row_id: self,
160 }
161 }
162}
163
164impl Iterator for RowIdIter<'_> {
165 type Item = RowId;
166
167 fn next(&mut self) -> Option<Self::Item> {
168 self.current_row_id.inspect(|row_id| {
169 self.current_row_id = self.next_row_id[*row_id];
170 })
171 }
172}
173
174pub struct EquiJoinParams<K> {
175 probe_side: BoxedExecutor,
176 probe_data_types: Vec<DataType>,
177 probe_key_idxs: Vec<usize>,
178 build_side: Vec<DataChunk, MonitoredGlobalAlloc>,
179 build_data_types: Vec<DataType>,
180 full_data_types: Vec<DataType>,
181 hash_map: JoinHashMap<K>,
182 next_build_row_with_same_key: ChunkedData<Option<RowId>>,
183 chunk_size: usize,
184 shutdown_rx: ShutdownToken,
185 asof_desc: Option<AsOfDesc>,
186}
187
188impl<K> EquiJoinParams<K> {
189 #[expect(clippy::too_many_arguments)]
190 pub(super) fn new(
191 probe_side: BoxedExecutor,
192 probe_data_types: Vec<DataType>,
193 probe_key_idxs: Vec<usize>,
194 build_side: Vec<DataChunk, MonitoredGlobalAlloc>,
195 build_data_types: Vec<DataType>,
196 full_data_types: Vec<DataType>,
197 hash_map: JoinHashMap<K>,
198 next_build_row_with_same_key: ChunkedData<Option<RowId>>,
199 chunk_size: usize,
200 shutdown_rx: ShutdownToken,
201 asof_desc: Option<AsOfDesc>,
202 ) -> Self {
203 Self {
204 probe_side,
205 probe_data_types,
206 probe_key_idxs,
207 build_side,
208 build_data_types,
209 full_data_types,
210 hash_map,
211 next_build_row_with_same_key,
212 chunk_size,
213 shutdown_rx,
214 asof_desc,
215 }
216 }
217
218 pub(crate) fn is_asof_join(&self) -> bool {
219 self.asof_desc.is_some()
220 }
221}
222
223#[derive(Clone, Copy, PartialEq, Eq)]
225enum RowOutput {
226 Omit,
228 Matched,
230 Unmatched,
232 UnmatchedWithNulls,
234}
235
236impl RowOutput {
237 fn emits(self, matched: bool) -> bool {
238 match self {
239 RowOutput::Omit => false,
240 RowOutput::Matched => matched,
241 RowOutput::Unmatched | RowOutput::UnmatchedWithNulls => !matched,
242 }
243 }
244}
245
246#[derive(Clone, Copy)]
248struct JoinOutput {
249 pairs: bool,
251 probe_rows: RowOutput,
252 build_rows: RowOutput,
253}
254
255impl JoinOutput {
256 fn new(join_type: JoinType) -> Self {
257 use RowOutput::*;
258 let (pairs, probe_rows, build_rows) = match join_type {
259 JoinType::Inner | JoinType::AsOfInner => (true, Omit, Omit),
260 JoinType::LeftOuter | JoinType::AsOfLeftOuter => (true, UnmatchedWithNulls, Omit),
261 JoinType::LeftSemi => (false, Matched, Omit),
262 JoinType::LeftAnti => (false, Unmatched, Omit),
263 JoinType::RightOuter => (true, Omit, UnmatchedWithNulls),
264 JoinType::RightSemi => (false, Omit, Matched),
265 JoinType::RightAnti => (false, Omit, Unmatched),
266 JoinType::FullOuter => (true, UnmatchedWithNulls, UnmatchedWithNulls),
267 };
268 Self {
269 pairs,
270 probe_rows,
271 build_rows,
272 }
273 }
274
275 fn needs_every_candidate(self) -> bool {
278 self.pairs || self.build_rows != RowOutput::Omit
279 }
280
281 fn row_data_types(
283 self,
284 probe_data_types: &[DataType],
285 build_data_types: &[DataType],
286 full_data_types: &[DataType],
287 ) -> Vec<DataType> {
288 use RowOutput::*;
289 match (self.probe_rows, self.build_rows) {
290 (Matched | Unmatched, _) => probe_data_types.to_vec(),
291 (_, Matched | Unmatched) => build_data_types.to_vec(),
292 _ => full_data_types.to_vec(),
293 }
294 }
295}
296
297struct MatchState {
300 probe: Option<Vec<bool>>,
301 build: Option<ChunkedData<bool>>,
302}
303
304impl MatchState {
305 fn mark(&mut self, probe_row_id: usize, build_row_id: RowId) {
306 self.mark_probe(probe_row_id);
307 if let Some(build) = &mut self.build {
308 build[build_row_id] = true;
309 }
310 }
311
312 fn mark_probe(&mut self, probe_row_id: usize) {
313 if let Some(probe) = &mut self.probe {
314 probe[probe_row_id] = true;
315 }
316 }
317
318 fn probe_matched(&self, probe_row_id: usize) -> bool {
319 self.probe.as_ref().is_some_and(|probe| probe[probe_row_id])
320 }
321}
322
323#[derive(Default)]
327struct PairRowIds {
328 probe: Vec<usize>,
329 build: Vec<RowId>,
330}
331
332impl PairRowIds {
333 fn push(&mut self, matched: &MatchState, probe_row_id: usize, build_row_id: RowId) {
334 if matched.probe.is_some() {
335 self.probe.push(probe_row_id);
336 }
337 if matched.build.is_some() {
338 self.build.push(build_row_id);
339 }
340 }
341
342 async fn evaluate(
345 &mut self,
346 mut pairs: DataChunk,
347 cond: &BoxedExpression,
348 matched: &mut MatchState,
349 output_pairs: bool,
350 ) -> Result<Option<DataChunk>> {
351 let satisfied: Bitmap = cond.eval(&pairs).await?.as_bool().iter().collect();
352 for pair in satisfied.iter_ones() {
353 if let Some(probe) = &mut matched.probe {
354 probe[self.probe[pair]] = true;
355 }
356 if let Some(build) = &mut matched.build {
357 build[self.build[pair]] = true;
358 }
359 }
360 self.probe.clear();
361 self.build.clear();
362 Ok(output_pairs.then(|| {
363 pairs.set_visibility(satisfied);
364 pairs
365 }))
366 }
367}
368
369pub struct JoinSpillManager {
370 op: SpillOp,
371 partition_num: usize,
372 probe_side_writers: Vec<opendal::Writer>,
373 build_side_writers: Vec<opendal::Writer>,
374 probe_side_chunk_builders: Vec<DataChunkBuilder>,
375 build_side_chunk_builders: Vec<DataChunkBuilder>,
376 spill_build_hasher: SpillBuildHasher,
377 probe_side_data_types: Vec<DataType>,
378 build_side_data_types: Vec<DataType>,
379 spill_chunk_size: usize,
380 spill_metrics: Arc<BatchSpillMetrics>,
381}
382
383impl JoinSpillManager {
398 pub fn new(
399 spill_backend: SpillBackend,
400 join_identity: &String,
401 partition_num: usize,
402 probe_side_data_types: Vec<DataType>,
403 build_side_data_types: Vec<DataType>,
404 spill_chunk_size: usize,
405 spill_metrics: Arc<BatchSpillMetrics>,
406 ) -> Result<Self> {
407 let suffix_uuid = uuid::Uuid::new_v4();
408 let dir = format!("{}-{}/", join_identity, suffix_uuid);
409 let op = SpillOp::create(dir, spill_backend)?;
410 let probe_side_writers = Vec::with_capacity(partition_num);
411 let build_side_writers = Vec::with_capacity(partition_num);
412 let probe_side_chunk_builders = Vec::with_capacity(partition_num);
413 let build_side_chunk_builders = Vec::with_capacity(partition_num);
414 let spill_build_hasher = SpillBuildHasher(suffix_uuid.as_u64_pair().1);
415 Ok(Self {
416 op,
417 partition_num,
418 probe_side_writers,
419 build_side_writers,
420 probe_side_chunk_builders,
421 build_side_chunk_builders,
422 spill_build_hasher,
423 probe_side_data_types,
424 build_side_data_types,
425 spill_chunk_size,
426 spill_metrics,
427 })
428 }
429
430 pub async fn init_writers(&mut self) -> Result<()> {
431 for i in 0..self.partition_num {
432 let join_probe_side_partition_file_name = format!("join-probe-side-p{}", i);
433 let w = self
434 .op
435 .writer_with(&join_probe_side_partition_file_name)
436 .await?;
437 self.probe_side_writers.push(w);
438
439 let join_build_side_partition_file_name = format!("join-build-side-p{}", i);
440 let w = self
441 .op
442 .writer_with(&join_build_side_partition_file_name)
443 .await?;
444 self.build_side_writers.push(w);
445 self.probe_side_chunk_builders.push(DataChunkBuilder::new(
446 self.probe_side_data_types.clone(),
447 self.spill_chunk_size,
448 ));
449 self.build_side_chunk_builders.push(DataChunkBuilder::new(
450 self.build_side_data_types.clone(),
451 self.spill_chunk_size,
452 ));
453 }
454 Ok(())
455 }
456
457 pub async fn write_probe_side_chunk(
458 &mut self,
459 chunk: DataChunk,
460 hash_codes: Vec<u64>,
461 ) -> Result<()> {
462 let (columns, vis) = chunk.into_parts_v2();
463 for partition in 0..self.partition_num {
464 let new_vis = vis.clone()
465 & Bitmap::from_iter(
466 hash_codes
467 .iter()
468 .map(|hash_code| (*hash_code as usize % self.partition_num) == partition),
469 );
470 let new_chunk = DataChunk::from_parts(columns.clone(), new_vis);
471 for output_chunk in self.probe_side_chunk_builders[partition].append_chunk(new_chunk) {
472 let chunk_pb: PbDataChunk = output_chunk.to_protobuf();
473 let buf = Message::encode_to_vec(&chunk_pb);
474 let len_bytes = Bytes::copy_from_slice(&(buf.len() as u32).to_le_bytes());
475 self.spill_metrics
476 .batch_spill_write_bytes
477 .inc_by((buf.len() + len_bytes.len()) as u64);
478 self.probe_side_writers[partition].write(len_bytes).await?;
479 self.probe_side_writers[partition].write(buf).await?;
480 }
481 }
482 Ok(())
483 }
484
485 pub async fn write_build_side_chunk(
486 &mut self,
487 chunk: DataChunk,
488 hash_codes: Vec<u64>,
489 ) -> Result<()> {
490 let (columns, vis) = chunk.into_parts_v2();
491 for partition in 0..self.partition_num {
492 let new_vis = vis.clone()
493 & Bitmap::from_iter(
494 hash_codes
495 .iter()
496 .map(|hash_code| (*hash_code as usize % self.partition_num) == partition),
497 );
498 let new_chunk = DataChunk::from_parts(columns.clone(), new_vis);
499 for output_chunk in self.build_side_chunk_builders[partition].append_chunk(new_chunk) {
500 let chunk_pb: PbDataChunk = output_chunk.to_protobuf();
501 let buf = Message::encode_to_vec(&chunk_pb);
502 let len_bytes = Bytes::copy_from_slice(&(buf.len() as u32).to_le_bytes());
503 self.spill_metrics
504 .batch_spill_write_bytes
505 .inc_by((buf.len() + len_bytes.len()) as u64);
506 self.build_side_writers[partition].write(len_bytes).await?;
507 self.build_side_writers[partition].write(buf).await?;
508 }
509 }
510 Ok(())
511 }
512
513 pub async fn close_writers(&mut self) -> Result<()> {
514 for partition in 0..self.partition_num {
515 if let Some(output_chunk) = self.probe_side_chunk_builders[partition].consume_all() {
516 let chunk_pb: PbDataChunk = output_chunk.to_protobuf();
517 let buf = Message::encode_to_vec(&chunk_pb);
518 let len_bytes = Bytes::copy_from_slice(&(buf.len() as u32).to_le_bytes());
519 self.spill_metrics
520 .batch_spill_write_bytes
521 .inc_by((buf.len() + len_bytes.len()) as u64);
522 self.probe_side_writers[partition].write(len_bytes).await?;
523 self.probe_side_writers[partition].write(buf).await?;
524 }
525
526 if let Some(output_chunk) = self.build_side_chunk_builders[partition].consume_all() {
527 let chunk_pb: PbDataChunk = output_chunk.to_protobuf();
528 let buf = Message::encode_to_vec(&chunk_pb);
529 let len_bytes = Bytes::copy_from_slice(&(buf.len() as u32).to_le_bytes());
530 self.spill_metrics
531 .batch_spill_write_bytes
532 .inc_by((buf.len() + len_bytes.len()) as u64);
533 self.build_side_writers[partition].write(len_bytes).await?;
534 self.build_side_writers[partition].write(buf).await?;
535 }
536 }
537
538 for mut w in self.probe_side_writers.drain(..) {
539 w.close().await?;
540 }
541 for mut w in self.build_side_writers.drain(..) {
542 w.close().await?;
543 }
544 Ok(())
545 }
546
547 async fn read_probe_side_partition(
548 &mut self,
549 partition: usize,
550 ) -> Result<BoxedDataChunkStream> {
551 let join_probe_side_partition_file_name = format!("join-probe-side-p{}", partition);
552 let r = self
553 .op
554 .reader_with(&join_probe_side_partition_file_name)
555 .await?;
556 Ok(SpillOp::read_stream(r, self.spill_metrics.clone()))
557 }
558
559 async fn read_build_side_partition(
560 &mut self,
561 partition: usize,
562 ) -> Result<BoxedDataChunkStream> {
563 let join_build_side_partition_file_name = format!("join-build-side-p{}", partition);
564 let r = self
565 .op
566 .reader_with(&join_build_side_partition_file_name)
567 .await?;
568 Ok(SpillOp::read_stream(r, self.spill_metrics.clone()))
569 }
570
571 pub async fn estimate_partition_size(&self, partition: usize) -> Result<u64> {
572 let join_probe_side_partition_file_name = format!("join-probe-side-p{}", partition);
573 let probe_size = self
574 .op
575 .stat(&join_probe_side_partition_file_name)
576 .await?
577 .content_length();
578 let join_build_side_partition_file_name = format!("join-build-side-p{}", partition);
579 let build_size = self
580 .op
581 .stat(&join_build_side_partition_file_name)
582 .await?
583 .content_length();
584 Ok(probe_size + build_size)
585 }
586
587 async fn clear_partition(&mut self, partition: usize) -> Result<()> {
588 let join_probe_side_partition_file_name = format!("join-probe-side-p{}", partition);
589 self.op.delete(&join_probe_side_partition_file_name).await?;
590 let join_build_side_partition_file_name = format!("join-build-side-p{}", partition);
591 self.op.delete(&join_build_side_partition_file_name).await?;
592 Ok(())
593 }
594}
595
596impl<K: HashKey> HashJoinExecutor<K> {
597 #[try_stream(boxed, ok = DataChunk, error = BatchError)]
598 async fn do_execute(self: Box<Self>) {
599 let mem_ctx = MemoryContext::new(Some(self.mem_ctx.clone()), TrAdderAtomic::new(0));
602 let mut need_to_spill = false;
603 let check_memory = match self.memory_upper_bound {
605 Some(upper_bound) => upper_bound > SPILL_AT_LEAST_MEMORY,
606 None => true,
607 };
608
609 let probe_schema = self.probe_side_source.schema().clone();
610 let build_schema = self.build_side_source.schema().clone();
611 let probe_data_types = self.probe_side_source.schema().data_types();
612 let build_data_types = self.build_side_source.schema().data_types();
613 let full_data_types = [probe_data_types.clone(), build_data_types.clone()].concat();
614
615 let mut build_side = Vec::new_in(mem_ctx.global_allocator());
616 let mut build_row_count = 0;
617 let mut build_side_stream = self.build_side_source.execute();
618 #[for_await]
619 for build_chunk in &mut build_side_stream {
620 let build_chunk = build_chunk?.compact_vis();
623 if build_chunk.cardinality() > 0 {
624 build_row_count += build_chunk.cardinality();
625 let chunk_estimated_heap_size = build_chunk.estimated_heap_size();
626 build_side.push(build_chunk);
628 mem_ctx.add_unchecked(chunk_estimated_heap_size as i64);
629 if check_memory && !mem_ctx.check_memory_usage() {
630 if self.spill_backend.is_some() {
631 need_to_spill = true;
632 break;
633 } else {
634 Err(BatchError::OutOfMemory(self.mem_ctx.mem_limit()))?;
635 }
636 }
637 }
638 }
639 let mut hash_map = JoinHashMap::with_capacity_and_hasher_in(
640 build_row_count,
641 PrecomputedBuildHasher,
642 mem_ctx.global_allocator(),
643 );
644 let mut next_build_row_with_same_key =
645 ChunkedData::with_chunk_sizes(build_side.iter().map(|c| c.capacity()))?;
646
647 let null_matched = K::Bitmap::from_bool_vec(self.null_matched.clone());
648
649 let mut mem_added_by_hash_table = 0;
650 if !need_to_spill {
651 'build: for (build_chunk_id, build_chunk) in build_side.iter().enumerate() {
653 let build_keys = K::build_many(&self.build_key_idxs, build_chunk);
654
655 for (build_row_id, build_key) in build_keys
656 .into_iter()
657 .enumerate()
658 .filter_by_bitmap(build_chunk.visibility())
659 {
660 self.shutdown_rx.check()?;
661 if build_key.null_bitmap().is_subset(&null_matched) {
663 let row_id = RowId::new(build_chunk_id, build_row_id);
664 let build_key_size = build_key.estimated_heap_size() as i64;
665 next_build_row_with_same_key[row_id] = hash_map.insert(build_key, row_id);
666 mem_added_by_hash_table += build_key_size;
668 mem_ctx.add_unchecked(build_key_size);
669 if check_memory && !mem_ctx.check_memory_usage() {
670 if self.spill_backend.is_some() {
671 need_to_spill = true;
672 break 'build;
673 } else {
674 Err(BatchError::OutOfMemory(self.mem_ctx.mem_limit()))?;
675 }
676 }
677 }
678 }
679 }
680 }
681
682 if need_to_spill {
683 info!(
690 "batch hash join executor {} starts to spill out",
691 &self.identity
692 );
693 let mut join_spill_manager = JoinSpillManager::new(
694 self.spill_backend.clone().unwrap(),
695 &self.identity,
696 DEFAULT_SPILL_PARTITION_NUM,
697 probe_data_types.clone(),
698 build_data_types.clone(),
699 self.chunk_size,
700 self.spill_metrics.clone(),
701 )?;
702 join_spill_manager.init_writers().await?;
703
704 drop(hash_map);
706 drop(next_build_row_with_same_key);
707 mem_ctx.add_unchecked(-mem_added_by_hash_table);
708
709 for chunk in build_side {
711 let chunk_size = chunk.estimated_heap_size() as i64;
713 let hash_codes = chunk.get_hash_values(
714 self.build_key_idxs.as_slice(),
715 join_spill_manager.spill_build_hasher,
716 );
717 join_spill_manager
718 .write_build_side_chunk(
719 chunk,
720 hash_codes
721 .into_iter()
722 .map(|hash_code| hash_code.value())
723 .collect(),
724 )
725 .await?;
726 mem_ctx.add_unchecked(-chunk_size);
727 }
728
729 #[for_await]
731 for chunk in build_side_stream {
732 let chunk = chunk?;
733 let hash_codes = chunk.get_hash_values(
734 self.build_key_idxs.as_slice(),
735 join_spill_manager.spill_build_hasher,
736 );
737 join_spill_manager
738 .write_build_side_chunk(
739 chunk,
740 hash_codes
741 .into_iter()
742 .map(|hash_code| hash_code.value())
743 .collect(),
744 )
745 .await?;
746 }
747
748 #[for_await]
750 for chunk in self.probe_side_source.execute() {
751 let chunk = chunk?;
752 let hash_codes = chunk.get_hash_values(
753 self.probe_key_idxs.as_slice(),
754 join_spill_manager.spill_build_hasher,
755 );
756 join_spill_manager
757 .write_probe_side_chunk(
758 chunk,
759 hash_codes
760 .into_iter()
761 .map(|hash_code| hash_code.value())
762 .collect(),
763 )
764 .await?;
765 }
766
767 join_spill_manager.close_writers().await?;
768
769 for i in 0..join_spill_manager.partition_num {
771 let partition_size = join_spill_manager.estimate_partition_size(i).await?;
772 let probe_side_stream = join_spill_manager.read_probe_side_partition(i).await?;
773 let build_side_stream = join_spill_manager.read_build_side_partition(i).await?;
774
775 let sub_hash_join_executor: HashJoinExecutor<K> = HashJoinExecutor::new_inner(
776 self.join_type,
777 self.output_indices.clone(),
778 Box::new(WrapStreamExecutor::new(
779 probe_schema.clone(),
780 probe_side_stream,
781 )),
782 Box::new(WrapStreamExecutor::new(
783 build_schema.clone(),
784 build_side_stream,
785 )),
786 self.probe_key_idxs.clone(),
787 self.build_key_idxs.clone(),
788 self.null_matched.clone(),
789 self.cond.clone(),
790 format!("{}-sub{}", self.identity.clone(), i),
791 self.chunk_size,
792 self.asof_desc.clone(),
793 self.spill_backend.clone(),
794 self.spill_metrics.clone(),
795 Some(partition_size),
796 self.shutdown_rx.clone(),
797 self.mem_ctx.clone(),
798 );
799
800 debug!(
801 "create sub_hash_join {} for hash_join {} to spill",
802 sub_hash_join_executor.identity, self.identity
803 );
804
805 let sub_hash_join_executor = Box::new(sub_hash_join_executor).execute();
806
807 #[for_await]
808 for chunk in sub_hash_join_executor {
809 let chunk = chunk?;
810 yield chunk;
811 }
812
813 join_spill_manager.clear_partition(i).await?;
815 }
816 } else {
817 let params = EquiJoinParams::new(
818 self.probe_side_source,
819 probe_data_types,
820 self.probe_key_idxs,
821 build_side,
822 build_data_types,
823 full_data_types,
824 hash_map,
825 next_build_row_with_same_key,
826 self.chunk_size,
827 self.shutdown_rx.clone(),
828 self.asof_desc,
829 );
830
831 #[for_await]
832 for chunk in Self::probe_and_project(
833 params,
834 self.join_type,
835 self.cond.as_deref(),
836 &self.output_indices,
837 self.schema.data_types(),
838 ) {
839 yield chunk?
840 }
841 }
844 }
845
846 #[try_stream(boxed, ok = DataChunk, error = BatchError)]
851 pub(super) async fn probe_and_project<'a>(
852 params: EquiJoinParams<K>,
853 join_type: JoinType,
854 cond: Option<&'a BoxedExpression>,
855 output_indices: &'a [usize],
856 output_data_types: Vec<DataType>,
857 ) {
858 let cond = cond.filter(|_| !params.is_asof_join());
859 let chunk_size = params.chunk_size;
860 let stream = Self::do_probe(params, join_type, cond);
861 if cond.is_some() {
862 let mut output_chunk_builder = DataChunkBuilder::new(output_data_types, chunk_size);
865 #[for_await]
866 for chunk in stream {
867 for output_chunk in
868 output_chunk_builder.append_chunk(chunk?.project(output_indices))
869 {
870 yield output_chunk
871 }
872 }
873 if let Some(output_chunk) = output_chunk_builder.consume_all() {
874 yield output_chunk
875 }
876 } else {
877 #[for_await]
878 for chunk in stream {
879 yield chunk?.project(output_indices)
880 }
881 }
882 }
883
884 #[try_stream(boxed, ok = DataChunk, error = BatchError)]
905 async fn do_probe(
906 EquiJoinParams {
907 probe_side,
908 probe_data_types,
909 probe_key_idxs,
910 build_side,
911 build_data_types,
912 full_data_types,
913 hash_map,
914 next_build_row_with_same_key,
915 chunk_size,
916 shutdown_rx,
917 asof_desc,
918 }: EquiJoinParams<K>,
919 join_type: JoinType,
920 cond: Option<&BoxedExpression>,
921 ) {
922 let output = JoinOutput::new(join_type);
923 let mut pair_builder = DataChunkBuilder::new(full_data_types.clone(), chunk_size);
924 let mut pair_row_ids = PairRowIds::default();
925 let mut row_builder = DataChunkBuilder::new(
926 output.row_data_types(&probe_data_types, &build_data_types, &full_data_types),
927 chunk_size,
928 );
929 let null_probe_row = Self::null_row(&probe_data_types);
931 let null_build_row = Self::null_row(&build_data_types);
932 let mut matched = MatchState {
933 probe: (output.probe_rows != RowOutput::Omit).then(Vec::new),
934 build: (output.build_rows != RowOutput::Omit)
935 .then(|| ChunkedData::with_chunk_sizes(build_side.iter().map(|c| c.capacity())))
936 .transpose()?,
937 };
938
939 #[for_await]
940 for probe_chunk in probe_side.execute() {
941 shutdown_rx.check()?;
942 let probe_chunk = probe_chunk?;
943 let probe_keys = K::build_many(&probe_key_idxs, &probe_chunk);
944 if let Some(probe_matched) = &mut matched.probe {
945 probe_matched.clear();
946 probe_matched.resize(probe_chunk.capacity(), false);
947 }
948
949 for (probe_row_id, probe_key) in probe_keys
950 .iter()
951 .enumerate()
952 .filter_by_bitmap(probe_chunk.visibility())
953 {
954 let first_same_key_row = hash_map.get(probe_key).copied();
955 if cond.is_none() && !output.pairs {
956 if first_same_key_row.is_some() {
959 matched.mark_probe(probe_row_id);
960 }
961 if let Some(build_matched) = &mut matched.build {
962 for build_row_id in
963 next_build_row_with_same_key.row_id_iter(first_same_key_row)
964 {
965 shutdown_rx.check()?;
966 build_matched[build_row_id] = true;
967 }
968 }
969 continue;
970 }
971 let same_key_rows = next_build_row_with_same_key.row_id_iter(first_same_key_row);
972 let candidates = match &asof_desc {
973 Some(asof_desc) => Either::Right(
974 Self::find_asof_matched_rows(
975 probe_chunk.row_at_unchecked_vis(probe_row_id),
976 &build_side,
977 same_key_rows,
978 asof_desc,
979 )
980 .into_iter(),
981 ),
982 None => Either::Left(same_key_rows),
983 };
984 for build_row_id in candidates {
985 shutdown_rx.check()?;
986 if !output.needs_every_candidate() && matched.probe_matched(probe_row_id) {
987 break;
988 }
989 if let Some(cond) = cond {
990 pair_row_ids.push(&matched, probe_row_id, build_row_id);
991 if let Some(pairs) = Self::append_one_row(
992 &mut pair_builder,
993 &probe_chunk,
994 probe_row_id,
995 &build_side[build_row_id.chunk_id()],
996 build_row_id.row_id(),
997 ) && let Some(chunk) = pair_row_ids
998 .evaluate(pairs, cond, &mut matched, output.pairs)
999 .await?
1000 {
1001 yield chunk
1002 }
1003 } else {
1004 matched.mark(probe_row_id, build_row_id);
1005 if output.pairs
1006 && let Some(chunk) = Self::append_one_row(
1007 &mut pair_builder,
1008 &probe_chunk,
1009 probe_row_id,
1010 &build_side[build_row_id.chunk_id()],
1011 build_row_id.row_id(),
1012 )
1013 {
1014 yield chunk
1015 }
1016 }
1017 }
1018 }
1019
1020 if let Some(cond) = cond
1023 && output.probe_rows != RowOutput::Omit
1024 && let Some(pairs) = pair_builder.consume_all()
1025 && let Some(chunk) = pair_row_ids
1026 .evaluate(pairs, cond, &mut matched, output.pairs)
1027 .await?
1028 {
1029 yield chunk
1030 }
1031 if let Some(probe_matched) = &matched.probe {
1032 for probe_row_id in probe_chunk.visibility().iter_ones() {
1033 if output.probe_rows.emits(probe_matched[probe_row_id])
1034 && let Some(chunk) = Self::append_probe_row_output(
1035 &mut row_builder,
1036 output.probe_rows,
1037 &probe_chunk,
1038 probe_row_id,
1039 &null_build_row,
1040 )
1041 {
1042 yield chunk
1043 }
1044 }
1045 }
1046 }
1047
1048 if let Some(pairs) = pair_builder.consume_all() {
1049 if let Some(cond) = cond {
1050 if let Some(chunk) = pair_row_ids
1051 .evaluate(pairs, cond, &mut matched, output.pairs)
1052 .await?
1053 {
1054 yield chunk
1055 }
1056 } else {
1057 yield pairs
1058 }
1059 }
1060 if let Some(build_matched) = &matched.build {
1061 for build_row_id in build_matched.all_row_ids() {
1062 if output.build_rows.emits(build_matched[build_row_id])
1063 && let Some(chunk) = Self::append_build_row_output(
1064 &mut row_builder,
1065 output.build_rows,
1066 &build_side[build_row_id.chunk_id()],
1067 build_row_id.row_id(),
1068 &null_probe_row,
1069 )
1070 {
1071 yield chunk
1072 }
1073 }
1074 }
1075 if let Some(chunk) = row_builder.consume_all() {
1076 yield chunk
1077 }
1078 }
1079
1080 fn null_row(data_types: &[DataType]) -> DataChunk {
1081 let columns = data_types
1082 .iter()
1083 .map(|data_type| {
1084 let mut builder = data_type.create_array_builder(1);
1085 builder.append_null();
1086 builder.finish().into()
1087 })
1088 .collect();
1089 DataChunk::new(columns, 1)
1090 }
1091
1092 fn append_probe_row_output(
1093 chunk_builder: &mut DataChunkBuilder,
1094 output: RowOutput,
1095 probe_chunk: &DataChunk,
1096 probe_row_id: usize,
1097 null_build_row: &DataChunk,
1098 ) -> Option<DataChunk> {
1099 if output == RowOutput::UnmatchedWithNulls {
1100 Self::append_one_row(chunk_builder, probe_chunk, probe_row_id, null_build_row, 0)
1101 } else {
1102 Self::append_one_probe_row(chunk_builder, probe_chunk, probe_row_id)
1103 }
1104 }
1105
1106 fn append_build_row_output(
1107 chunk_builder: &mut DataChunkBuilder,
1108 output: RowOutput,
1109 build_chunk: &DataChunk,
1110 build_row_id: usize,
1111 null_probe_row: &DataChunk,
1112 ) -> Option<DataChunk> {
1113 if output == RowOutput::UnmatchedWithNulls {
1114 Self::append_one_row(chunk_builder, null_probe_row, 0, build_chunk, build_row_id)
1115 } else {
1116 Self::append_one_build_row(chunk_builder, build_chunk, build_row_id)
1117 }
1118 }
1119
1120 fn append_one_row(
1121 chunk_builder: &mut DataChunkBuilder,
1122 probe_chunk: &DataChunk,
1123 probe_row_id: usize,
1124 build_chunk: &DataChunk,
1125 build_row_id: usize,
1126 ) -> Option<DataChunk> {
1127 chunk_builder.append_one_row_from_array_elements(
1128 probe_chunk.columns().iter().map(|c| c.as_ref()),
1129 probe_row_id,
1130 build_chunk.columns().iter().map(|c| c.as_ref()),
1131 build_row_id,
1132 )
1133 }
1134
1135 fn append_one_probe_row(
1136 chunk_builder: &mut DataChunkBuilder,
1137 probe_chunk: &DataChunk,
1138 probe_row_id: usize,
1139 ) -> Option<DataChunk> {
1140 chunk_builder.append_one_row_from_array_elements(
1141 probe_chunk.columns().iter().map(|c| c.as_ref()),
1142 probe_row_id,
1143 empty(),
1144 0,
1145 )
1146 }
1147
1148 fn append_one_build_row(
1149 chunk_builder: &mut DataChunkBuilder,
1150 build_chunk: &DataChunk,
1151 build_row_id: usize,
1152 ) -> Option<DataChunk> {
1153 chunk_builder.append_one_row_from_array_elements(
1154 empty(),
1155 0,
1156 build_chunk.columns().iter().map(|c| c.as_ref()),
1157 build_row_id,
1158 )
1159 }
1160
1161 fn find_asof_matched_rows(
1162 probe_row_ref: RowRef<'_>,
1163 build_side: &[DataChunk],
1164 build_side_row_iter: RowIdIter<'_>,
1165 asof_join_condition: &AsOfDesc,
1166 ) -> Option<RowId> {
1167 let probe_inequality_value = probe_row_ref.datum_at(asof_join_condition.left_idx);
1168 if let Some(probe_inequality_scalar) = probe_inequality_value {
1169 let mut result_row_id: Option<RowId> = None;
1170 let mut build_row_ref;
1171
1172 for build_row_id in build_side_row_iter {
1173 build_row_ref =
1174 build_side[build_row_id.chunk_id()].row_at_unchecked_vis(build_row_id.row_id());
1175 let build_inequality_value = build_row_ref.datum_at(asof_join_condition.right_idx);
1176 if let Some(build_inequality_scalar) = build_inequality_value {
1177 let mut pick_result = |compare: fn(Ordering) -> bool| {
1178 if let Some(result_row_id_inner) = result_row_id {
1179 let result_row_ref = build_side[result_row_id_inner.chunk_id()]
1180 .row_at_unchecked_vis(result_row_id_inner.row_id());
1181 let result_inequality_scalar = result_row_ref
1182 .datum_at(asof_join_condition.right_idx)
1183 .unwrap();
1184 if compare(
1185 probe_inequality_scalar.default_cmp(&build_inequality_scalar),
1186 ) && compare(
1187 build_inequality_scalar.default_cmp(&result_inequality_scalar),
1188 ) {
1189 result_row_id = Some(build_row_id);
1190 }
1191 } else if compare(
1192 probe_inequality_scalar.default_cmp(&build_inequality_scalar),
1193 ) {
1194 result_row_id = Some(build_row_id);
1195 }
1196 };
1197 match asof_join_condition.inequality_type {
1198 AsOfInequalityType::Lt => {
1199 pick_result(Ordering::is_lt);
1200 }
1201 AsOfInequalityType::Le => {
1202 pick_result(Ordering::is_le);
1203 }
1204 AsOfInequalityType::Gt => {
1205 pick_result(Ordering::is_gt);
1206 }
1207 AsOfInequalityType::Ge => {
1208 pick_result(Ordering::is_ge);
1209 }
1210 }
1211 }
1212 }
1213 result_row_id
1214 } else {
1215 None
1216 }
1217 }
1218}
1219
1220impl BoxedExecutorBuilder for HashJoinExecutor<()> {
1221 async fn new_boxed_executor(
1222 context: &ExecutorBuilder<'_>,
1223 inputs: Vec<BoxedExecutor>,
1224 ) -> Result<BoxedExecutor> {
1225 let [left_child, right_child]: [_; 2] = inputs.try_into().unwrap();
1226
1227 let hash_join_node = try_match_expand!(
1228 context.plan_node().get_node_body().unwrap(),
1229 NodeBody::HashJoin
1230 )?;
1231
1232 let join_type = JoinType::from_prost(hash_join_node.get_join_type()?);
1233
1234 let cond = match hash_join_node.get_condition() {
1235 Ok(cond_prost) => Some(build_from_prost(cond_prost)?),
1236 Err(_) => None,
1237 };
1238
1239 let left_key_idxs = hash_join_node
1240 .get_left_key()
1241 .iter()
1242 .map(|&idx| idx as usize)
1243 .collect_vec();
1244 let right_key_idxs = hash_join_node
1245 .get_right_key()
1246 .iter()
1247 .map(|&idx| idx as usize)
1248 .collect_vec();
1249
1250 ensure!(left_key_idxs.len() == right_key_idxs.len());
1251
1252 let right_data_types = right_child.schema().data_types();
1253 let right_key_types = right_key_idxs
1254 .iter()
1255 .map(|&idx| right_data_types[idx].clone())
1256 .collect_vec();
1257
1258 let output_indices: Vec<usize> = hash_join_node
1259 .get_output_indices()
1260 .iter()
1261 .map(|&x| x as usize)
1262 .collect();
1263
1264 let identity = context.plan_node().get_identity().clone();
1265
1266 let asof_desc = hash_join_node
1267 .asof_desc
1268 .map(|desc| AsOfDesc::from_protobuf(&desc))
1269 .transpose()?;
1270
1271 Ok(HashJoinExecutorArgs {
1272 join_type,
1273 output_indices,
1274 probe_side_source: left_child,
1275 build_side_source: right_child,
1276 probe_key_idxs: left_key_idxs,
1277 build_key_idxs: right_key_idxs,
1278 null_matched: hash_join_node.get_null_safe().clone(),
1279 cond,
1280 identity: identity.clone(),
1281 right_key_types,
1282 chunk_size: context.context().get_config().developer.chunk_size,
1283 asof_desc,
1284 spill_backend: if context.context().get_config().enable_spill {
1285 Some(Disk)
1286 } else {
1287 None
1288 },
1289 spill_metrics: context.context().spill_metrics(),
1290 shutdown_rx: context.shutdown_rx().clone(),
1291 mem_ctx: context.context().create_executor_mem_context(&identity),
1292 }
1293 .dispatch())
1294 }
1295}
1296
1297struct HashJoinExecutorArgs {
1298 join_type: JoinType,
1299 output_indices: Vec<usize>,
1300 probe_side_source: BoxedExecutor,
1301 build_side_source: BoxedExecutor,
1302 probe_key_idxs: Vec<usize>,
1303 build_key_idxs: Vec<usize>,
1304 null_matched: Vec<bool>,
1305 cond: Option<BoxedExpression>,
1306 identity: String,
1307 right_key_types: Vec<DataType>,
1308 chunk_size: usize,
1309 asof_desc: Option<AsOfDesc>,
1310 spill_backend: Option<SpillBackend>,
1311 spill_metrics: Arc<BatchSpillMetrics>,
1312 shutdown_rx: ShutdownToken,
1313 mem_ctx: MemoryContext,
1314}
1315
1316impl HashKeyDispatcher for HashJoinExecutorArgs {
1317 type Output = BoxedExecutor;
1318
1319 fn dispatch_impl<K: HashKey>(self) -> Self::Output {
1320 Box::new(HashJoinExecutor::<K>::new(
1321 self.join_type,
1322 self.output_indices,
1323 self.probe_side_source,
1324 self.build_side_source,
1325 self.probe_key_idxs,
1326 self.build_key_idxs,
1327 self.null_matched,
1328 self.cond.map(Arc::new),
1329 self.identity,
1330 self.chunk_size,
1331 self.asof_desc,
1332 self.spill_backend,
1333 self.spill_metrics,
1334 self.shutdown_rx,
1335 self.mem_ctx,
1336 ))
1337 }
1338
1339 fn data_types(&self) -> &[DataType] {
1340 &self.right_key_types
1341 }
1342}
1343
1344impl<K> HashJoinExecutor<K> {
1345 #[expect(clippy::too_many_arguments)]
1346 pub fn new(
1347 join_type: JoinType,
1348 output_indices: Vec<usize>,
1349 probe_side_source: BoxedExecutor,
1350 build_side_source: BoxedExecutor,
1351 probe_key_idxs: Vec<usize>,
1352 build_key_idxs: Vec<usize>,
1353 null_matched: Vec<bool>,
1354 cond: Option<Arc<BoxedExpression>>,
1355 identity: String,
1356 chunk_size: usize,
1357 asof_desc: Option<AsOfDesc>,
1358 spill_backend: Option<SpillBackend>,
1359 spill_metrics: Arc<BatchSpillMetrics>,
1360 shutdown_rx: ShutdownToken,
1361 mem_ctx: MemoryContext,
1362 ) -> Self {
1363 Self::new_inner(
1364 join_type,
1365 output_indices,
1366 probe_side_source,
1367 build_side_source,
1368 probe_key_idxs,
1369 build_key_idxs,
1370 null_matched,
1371 cond,
1372 identity,
1373 chunk_size,
1374 asof_desc,
1375 spill_backend,
1376 spill_metrics,
1377 None,
1378 shutdown_rx,
1379 mem_ctx,
1380 )
1381 }
1382
1383 #[expect(clippy::too_many_arguments)]
1384 fn new_inner(
1385 join_type: JoinType,
1386 output_indices: Vec<usize>,
1387 probe_side_source: BoxedExecutor,
1388 build_side_source: BoxedExecutor,
1389 probe_key_idxs: Vec<usize>,
1390 build_key_idxs: Vec<usize>,
1391 null_matched: Vec<bool>,
1392 cond: Option<Arc<BoxedExpression>>,
1393 identity: String,
1394 chunk_size: usize,
1395 asof_desc: Option<AsOfDesc>,
1396 spill_backend: Option<SpillBackend>,
1397 spill_metrics: Arc<BatchSpillMetrics>,
1398 memory_upper_bound: Option<u64>,
1399 shutdown_rx: ShutdownToken,
1400 mem_ctx: MemoryContext,
1401 ) -> Self {
1402 assert_eq!(probe_key_idxs.len(), build_key_idxs.len());
1403 assert_eq!(probe_key_idxs.len(), null_matched.len());
1404 let original_schema = match join_type {
1405 JoinType::LeftSemi | JoinType::LeftAnti => probe_side_source.schema().clone(),
1406 JoinType::RightSemi | JoinType::RightAnti => build_side_source.schema().clone(),
1407 _ => Schema::from_iter(
1408 probe_side_source
1409 .schema()
1410 .fields()
1411 .iter()
1412 .chain(build_side_source.schema().fields().iter())
1413 .cloned(),
1414 ),
1415 };
1416 let schema = Schema::from_iter(
1417 output_indices
1418 .iter()
1419 .map(|&idx| original_schema[idx].clone()),
1420 );
1421 Self {
1422 join_type,
1423 original_schema,
1424 schema,
1425 output_indices,
1426 probe_side_source,
1427 build_side_source,
1428 probe_key_idxs,
1429 build_key_idxs,
1430 null_matched,
1431 cond,
1432 identity,
1433 chunk_size,
1434 asof_desc,
1435 shutdown_rx,
1436 spill_backend,
1437 spill_metrics,
1438 memory_upper_bound,
1439 mem_ctx,
1440 _phantom: PhantomData,
1441 }
1442 }
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447 use futures::StreamExt;
1448 use futures_async_stream::for_await;
1449 use itertools::Itertools;
1450 use risingwave_common::array::{ArrayBuilderImpl, DataChunk};
1451 use risingwave_common::catalog::{Field, Schema};
1452 use risingwave_common::hash::Key32;
1453 use risingwave_common::memory::MemoryContext;
1454 use risingwave_common::metrics::LabelGuardedIntGauge;
1455 use risingwave_common::test_prelude::DataChunkTestExt;
1456 use risingwave_common::types::DataType;
1457 use risingwave_common::util::iter_util::ZipEqDebug;
1458 use risingwave_common::util::memcmp_encoding::encode_chunk;
1459 use risingwave_common::util::sort_util::{ColumnOrder, OrderType};
1460 use risingwave_expr::expr::{BoxedExpression, build_from_pretty};
1461
1462 use super::{AsOfDesc, AsOfInequalityType, ChunkedData, HashJoinExecutor, JoinType, RowId};
1463 use crate::error::Result;
1464 use crate::executor::test_utils::MockExecutor;
1465 use crate::executor::{BoxedExecutor, Executor};
1466 use crate::monitor::BatchSpillMetrics;
1467 use crate::spill::spill_op::SpillBackend;
1468 use crate::task::ShutdownToken;
1469
1470 const CHUNK_SIZE: usize = 1024;
1471
1472 struct DataChunkMerger {
1473 array_builders: Vec<ArrayBuilderImpl>,
1474 array_len: usize,
1475 }
1476
1477 impl DataChunkMerger {
1478 fn new(data_types: Vec<DataType>) -> Result<Self> {
1479 let array_builders = data_types
1480 .iter()
1481 .map(|data_type| data_type.create_array_builder(CHUNK_SIZE))
1482 .collect();
1483
1484 Ok(Self {
1485 array_builders,
1486 array_len: 0,
1487 })
1488 }
1489
1490 fn append(&mut self, data_chunk: &DataChunk) -> Result<()> {
1491 ensure!(self.array_builders.len() == data_chunk.dimension());
1492 for idx in 0..self.array_builders.len() {
1493 self.array_builders[idx].append_array(data_chunk.column_at(idx));
1494 }
1495 self.array_len += data_chunk.capacity();
1496
1497 Ok(())
1498 }
1499
1500 fn finish(self) -> Result<DataChunk> {
1501 let columns = self
1502 .array_builders
1503 .into_iter()
1504 .map(|b| b.finish().into())
1505 .collect();
1506
1507 Ok(DataChunk::new(columns, self.array_len))
1508 }
1509 }
1510
1511 fn compare_data_chunk_with_rowsort(left: &DataChunk, right: &DataChunk) -> bool {
1513 assert!(left.is_vis_compacted());
1514 assert!(right.is_vis_compacted());
1515
1516 if left.cardinality() != right.cardinality() {
1517 return false;
1518 }
1519
1520 let column_orders = (0..left.columns().len())
1522 .map(|i| ColumnOrder::new(i, OrderType::ascending()))
1523 .collect_vec();
1524 let left_encoded_chunk = encode_chunk(left, &column_orders).unwrap();
1525 let mut sorted_left = left_encoded_chunk
1526 .into_iter()
1527 .enumerate()
1528 .map(|(row_id, row)| (left.row_at_unchecked_vis(row_id), row))
1529 .collect_vec();
1530 sorted_left.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
1531
1532 let right_encoded_chunk = encode_chunk(right, &column_orders).unwrap();
1533 let mut sorted_right = right_encoded_chunk
1534 .into_iter()
1535 .enumerate()
1536 .map(|(row_id, row)| (right.row_at_unchecked_vis(row_id), row))
1537 .collect_vec();
1538 sorted_right.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
1539
1540 sorted_left
1541 .into_iter()
1542 .map(|(row, _)| row)
1543 .zip_eq_debug(sorted_right.into_iter().map(|(row, _)| row))
1544 .all(|(row1, row2)| row1 == row2)
1545 }
1546
1547 struct TestFixture {
1548 left_types: Vec<DataType>,
1549 right_types: Vec<DataType>,
1550 join_type: JoinType,
1551 }
1552
1553 impl TestFixture {
1570 fn with_join_type(join_type: JoinType) -> Self {
1571 Self {
1572 left_types: vec![DataType::Int32, DataType::Float32],
1573 right_types: vec![DataType::Int32, DataType::Float64],
1574 join_type,
1575 }
1576 }
1577
1578 fn create_left_executor(&self) -> BoxedExecutor {
1579 let schema = Schema {
1580 fields: vec![
1581 Field::unnamed(DataType::Int32),
1582 Field::unnamed(DataType::Float32),
1583 ],
1584 };
1585 let mut executor = MockExecutor::new(schema);
1586
1587 executor.add(DataChunk::from_pretty(
1588 "i f
1589 1 6.1
1590 2 .
1591 . 8.4
1592 3 3.9
1593 . . ",
1594 ));
1595
1596 executor.add(DataChunk::from_pretty(
1597 "i f
1598 4 6.6
1599 3 .
1600 . 0.7
1601 5 .
1602 . 5.5",
1603 ));
1604
1605 Box::new(executor)
1606 }
1607
1608 fn create_right_executor(&self) -> BoxedExecutor {
1609 let schema = Schema {
1610 fields: vec![
1611 Field::unnamed(DataType::Int32),
1612 Field::unnamed(DataType::Float64),
1613 ],
1614 };
1615 let mut executor = MockExecutor::new(schema);
1616
1617 executor.add(DataChunk::from_pretty(
1618 "i F
1619 8 6.1
1620 2 .
1621 . 8.9
1622 3 .
1623 . 3.5
1624 6 . ",
1625 ));
1626
1627 executor.add(DataChunk::from_pretty(
1628 "i F
1629 4 7.5
1630 6 .
1631 . 8
1632 7 .
1633 . 9.1
1634 9 . ",
1635 ));
1636
1637 executor.add(DataChunk::from_pretty(
1638 " i F
1639 3 3.7
1640 9 .
1641 . 9.6
1642 100 .
1643 . 8.18
1644 200 . ",
1645 ));
1646
1647 Box::new(executor)
1648 }
1649
1650 fn output_data_types(&self) -> Vec<DataType> {
1651 let join_type = self.join_type;
1652 if join_type.keep_all() {
1653 [self.left_types.clone(), self.right_types.clone()].concat()
1654 } else if join_type.keep_left() {
1655 self.left_types.clone()
1656 } else if join_type.keep_right() {
1657 self.right_types.clone()
1658 } else {
1659 unreachable!()
1660 }
1661 }
1662
1663 fn create_cond() -> BoxedExpression {
1664 build_from_pretty("(less_than:boolean $1:float4 $3:float8)")
1665 }
1666
1667 fn create_join_executor_with_chunk_size_and_executors(
1668 &self,
1669 has_non_equi_cond: bool,
1670 null_safe: bool,
1671 chunk_size: usize,
1672 left_child: BoxedExecutor,
1673 right_child: BoxedExecutor,
1674 shutdown_rx: ShutdownToken,
1675 parent_mem_ctx: Option<MemoryContext>,
1676 test_spill: bool,
1677 ) -> BoxedExecutor {
1678 let join_type = self.join_type;
1679
1680 let output_indices = (0..match join_type {
1681 JoinType::LeftSemi | JoinType::LeftAnti => left_child.schema().fields().len(),
1682 JoinType::RightSemi | JoinType::RightAnti => right_child.schema().fields().len(),
1683 _ => left_child.schema().fields().len() + right_child.schema().fields().len(),
1684 })
1685 .collect();
1686
1687 let cond = if has_non_equi_cond {
1688 Some(Self::create_cond().into())
1689 } else {
1690 None
1691 };
1692
1693 let mem_ctx = if test_spill {
1694 MemoryContext::new_with_mem_limit(
1695 parent_mem_ctx,
1696 LabelGuardedIntGauge::test_int_gauge::<4>(),
1697 0,
1698 )
1699 } else {
1700 MemoryContext::new(parent_mem_ctx, LabelGuardedIntGauge::test_int_gauge::<4>())
1701 };
1702 Box::new(HashJoinExecutor::<Key32>::new(
1703 join_type,
1704 output_indices,
1705 left_child,
1706 right_child,
1707 vec![0],
1708 vec![0],
1709 vec![null_safe],
1710 cond,
1711 "HashJoinExecutor".to_owned(),
1712 chunk_size,
1713 None,
1714 if test_spill {
1715 Some(SpillBackend::Memory)
1716 } else {
1717 None
1718 },
1719 BatchSpillMetrics::for_test(),
1720 shutdown_rx,
1721 mem_ctx,
1722 ))
1723 }
1724
1725 async fn do_test(&self, expected: DataChunk, has_non_equi_cond: bool, null_safe: bool) {
1726 let left_executor = self.create_left_executor();
1727 let right_executor = self.create_right_executor();
1728 self.do_test_with_chunk_size_and_executors(
1729 expected.clone(),
1730 has_non_equi_cond,
1731 null_safe,
1732 self::CHUNK_SIZE,
1733 left_executor,
1734 right_executor,
1735 false,
1736 )
1737 .await;
1738
1739 let left_executor = self.create_left_executor();
1741 let right_executor = self.create_right_executor();
1742 self.do_test_with_chunk_size_and_executors(
1743 expected,
1744 has_non_equi_cond,
1745 null_safe,
1746 self::CHUNK_SIZE,
1747 left_executor,
1748 right_executor,
1749 true,
1750 )
1751 .await;
1752 }
1753
1754 async fn do_test_with_chunk_size_and_executors(
1755 &self,
1756 expected: DataChunk,
1757 has_non_equi_cond: bool,
1758 null_safe: bool,
1759 chunk_size: usize,
1760 left_executor: BoxedExecutor,
1761 right_executor: BoxedExecutor,
1762 test_spill: bool,
1763 ) {
1764 let parent_mem_context =
1765 MemoryContext::root(LabelGuardedIntGauge::test_int_gauge::<4>(), u64::MAX);
1766
1767 {
1768 let join_executor = self.create_join_executor_with_chunk_size_and_executors(
1769 has_non_equi_cond,
1770 null_safe,
1771 chunk_size,
1772 left_executor,
1773 right_executor,
1774 ShutdownToken::empty(),
1775 Some(parent_mem_context.clone()),
1776 test_spill,
1777 );
1778
1779 let mut data_chunk_merger = DataChunkMerger::new(self.output_data_types()).unwrap();
1780
1781 let fields = &join_executor.schema().fields;
1782
1783 if self.join_type.keep_all() {
1784 assert_eq!(fields[1].data_type, DataType::Float32);
1785 assert_eq!(fields[3].data_type, DataType::Float64);
1786 } else if self.join_type.keep_left() {
1787 assert_eq!(fields[1].data_type, DataType::Float32);
1788 } else if self.join_type.keep_right() {
1789 assert_eq!(fields[1].data_type, DataType::Float64)
1790 } else {
1791 unreachable!()
1792 }
1793
1794 let mut stream = join_executor.execute();
1795
1796 while let Some(data_chunk) = stream.next().await {
1797 let data_chunk = data_chunk.unwrap();
1798 let data_chunk = data_chunk.compact_vis();
1799 data_chunk_merger.append(&data_chunk).unwrap();
1800 }
1801
1802 let result_chunk = data_chunk_merger.finish().unwrap();
1803 println!("expected: {:?}", expected);
1804 println!("result: {:?}", result_chunk);
1805
1806 assert!(compare_data_chunk_with_rowsort(&expected, &result_chunk));
1809 }
1810
1811 assert_eq!(0, parent_mem_context.get_bytes_used());
1812 }
1813
1814 async fn do_test_shutdown(&self, has_non_equi_cond: bool) {
1815 let left_executor = self.create_left_executor();
1817 let right_executor = self.create_right_executor();
1818 let (shutdown_tx, shutdown_rx) = ShutdownToken::new();
1819 let join_executor = self.create_join_executor_with_chunk_size_and_executors(
1820 has_non_equi_cond,
1821 false,
1822 self::CHUNK_SIZE,
1823 left_executor,
1824 right_executor,
1825 shutdown_rx,
1826 None,
1827 false,
1828 );
1829 shutdown_tx.cancel();
1830 #[for_await]
1831 for chunk in join_executor.execute() {
1832 assert!(chunk.is_err());
1833 break;
1834 }
1835
1836 let left_executor = self.create_left_executor();
1838 let right_executor = self.create_right_executor();
1839 let (shutdown_tx, shutdown_rx) = ShutdownToken::new();
1840 let join_executor = self.create_join_executor_with_chunk_size_and_executors(
1841 has_non_equi_cond,
1842 false,
1843 self::CHUNK_SIZE,
1844 left_executor,
1845 right_executor,
1846 shutdown_rx,
1847 None,
1848 false,
1849 );
1850 shutdown_tx.abort("test");
1851 #[for_await]
1852 for chunk in join_executor.execute() {
1853 assert!(chunk.is_err());
1854 break;
1855 }
1856 }
1857 }
1858
1859 #[tokio::test]
1864 async fn test_inner_join() {
1865 let test_fixture = TestFixture::with_join_type(JoinType::Inner);
1866
1867 let expected_chunk = DataChunk::from_pretty(
1868 "i f i F
1869 2 . 2 .
1870 3 3.9 3 3.7
1871 3 3.9 3 .
1872 4 6.6 4 7.5
1873 3 . 3 3.7
1874 3 . 3 .",
1875 );
1876
1877 test_fixture.do_test(expected_chunk, false, false).await;
1878 }
1879
1880 #[tokio::test]
1885 async fn test_null_safe_inner_join() {
1886 let test_fixture = TestFixture::with_join_type(JoinType::Inner);
1887
1888 let expected_chunk = DataChunk::from_pretty(
1889 "i f i F
1890 2 . 2 .
1891 . 8.4 . 8.18
1892 . 8.4 . 9.6
1893 . 8.4 . 9.1
1894 . 8.4 . 8
1895 . 8.4 . 3.5
1896 . 8.4 . 8.9
1897 3 3.9 3 3.7
1898 3 3.9 3 .
1899 . . . 8.18
1900 . . . 9.6
1901 . . . 9.1
1902 . . . 8
1903 . . . 3.5
1904 . . . 8.9
1905 4 6.6 4 7.5
1906 3 . 3 3.7
1907 3 . 3 .
1908 . 0.7 . 8.18
1909 . 0.7 . 9.6
1910 . 0.7 . 9.1
1911 . 0.7 . 8
1912 . 0.7 . 3.5
1913 . 0.7 . 8.9
1914 . 5.5 . 8.18
1915 . 5.5 . 9.6
1916 . 5.5 . 9.1
1917 . 5.5 . 8
1918 . 5.5 . 3.5
1919 . 5.5 . 8.9",
1920 );
1921
1922 test_fixture.do_test(expected_chunk, false, true).await;
1923 }
1924
1925 #[tokio::test]
1930 async fn test_inner_join_with_non_equi_condition() {
1931 let test_fixture = TestFixture::with_join_type(JoinType::Inner);
1932
1933 let expected_chunk = DataChunk::from_pretty(
1934 "i f i F
1935 4 6.6 4 7.5",
1936 );
1937
1938 test_fixture.do_test(expected_chunk, true, false).await;
1939 }
1940
1941 #[tokio::test]
1946 async fn test_left_outer_join() {
1947 let test_fixture = TestFixture::with_join_type(JoinType::LeftOuter);
1948
1949 let expected_chunk = DataChunk::from_pretty(
1950 "i f i F
1951 1 6.1 . .
1952 2 . 2 .
1953 . 8.4 . .
1954 3 3.9 3 3.7
1955 3 3.9 3 .
1956 . . . .
1957 4 6.6 4 7.5
1958 3 . 3 3.7
1959 3 . 3 .
1960 . 0.7 . .
1961 5 . . .
1962 . 5.5 . .",
1963 );
1964
1965 test_fixture.do_test(expected_chunk, false, false).await;
1966 }
1967
1968 #[tokio::test]
1973 async fn test_left_outer_join_with_non_equi_condition() {
1974 let test_fixture = TestFixture::with_join_type(JoinType::LeftOuter);
1975
1976 let expected_chunk = DataChunk::from_pretty(
1977 "i f i F
1978 2 . . .
1979 3 3.9 . .
1980 4 6.6 4 7.5
1981 3 . . .
1982 1 6.1 . .
1983 . 8.4 . .
1984 . . . .
1985 . 0.7 . .
1986 5 . . .
1987 . 5.5 . .",
1988 );
1989
1990 test_fixture.do_test(expected_chunk, true, false).await;
1991 }
1992
1993 #[tokio::test]
1998 async fn test_right_outer_join() {
1999 let test_fixture = TestFixture::with_join_type(JoinType::RightOuter);
2000
2001 let expected_chunk = DataChunk::from_pretty(
2002 "i f i F
2003 2 . 2 .
2004 3 3.9 3 3.7
2005 3 3.9 3 .
2006 4 6.6 4 7.5
2007 3 . 3 3.7
2008 3 . 3 .
2009 . . 8 6.1
2010 . . . 8.9
2011 . . . 3.5
2012 . . 6 .
2013 . . 6 .
2014 . . . 8
2015 . . 7 .
2016 . . . 9.1
2017 . . 9 .
2018 . . 9 .
2019 . . . 9.6
2020 . . 100 .
2021 . . . 8.18
2022 . . 200 .",
2023 );
2024
2025 test_fixture.do_test(expected_chunk, false, false).await;
2026 }
2027
2028 #[tokio::test]
2033 async fn test_right_outer_join_with_non_equi_condition() {
2034 let test_fixture = TestFixture::with_join_type(JoinType::RightOuter);
2035
2036 let expected_chunk = DataChunk::from_pretty(
2037 "i f i F
2038 4 6.6 4 7.5
2039 . . 8 6.1
2040 . . 2 .
2041 . . . 8.9
2042 . . 3 .
2043 . . . 3.5
2044 . . 6 .
2045 . . 6 .
2046 . . . 8
2047 . . 7 .
2048 . . . 9.1
2049 . . 9 .
2050 . . 3 3.7
2051 . . 9 .
2052 . . . 9.6
2053 . . 100 .
2054 . . . 8.18
2055 . . 200 .",
2056 );
2057
2058 test_fixture.do_test(expected_chunk, true, false).await;
2059 }
2060
2061 #[tokio::test]
2065 async fn test_full_outer_join() {
2066 let test_fixture = TestFixture::with_join_type(JoinType::FullOuter);
2067
2068 let expected_chunk = DataChunk::from_pretty(
2069 "i f i F
2070 1 6.1 . .
2071 2 . 2 .
2072 . 8.4 . .
2073 3 3.9 3 3.7
2074 3 3.9 3 .
2075 . . . .
2076 4 6.6 4 7.5
2077 3 . 3 3.7
2078 3 . 3 .
2079 . 0.7 . .
2080 5 . . .
2081 . 5.5 . .
2082 . . 8 6.1
2083 . . . 8.9
2084 . . . 3.5
2085 . . 6 .
2086 . . 6 .
2087 . . . 8
2088 . . 7 .
2089 . . . 9.1
2090 . . 9 .
2091 . . 9 .
2092 . . . 9.6
2093 . . 100 .
2094 . . . 8.18
2095 . . 200 .",
2096 );
2097
2098 test_fixture.do_test(expected_chunk, false, false).await;
2099 }
2100
2101 #[tokio::test]
2105 async fn test_full_outer_join_with_non_equi_condition() {
2106 let test_fixture = TestFixture::with_join_type(JoinType::FullOuter);
2107
2108 let expected_chunk = DataChunk::from_pretty(
2109 "i f i F
2110 2 . . .
2111 3 3.9 . .
2112 4 6.6 4 7.5
2113 3 . . .
2114 1 6.1 . .
2115 . 8.4 . .
2116 . . . .
2117 . 0.7 . .
2118 5 . . .
2119 . 5.5 . .
2120 . . 8 6.1
2121 . . 2 .
2122 . . . 8.9
2123 . . 3 .
2124 . . . 3.5
2125 . . 6 .
2126 . . 6 .
2127 . . . 8
2128 . . 7 .
2129 . . . 9.1
2130 . . 9 .
2131 . . 3 3.7
2132 . . 9 .
2133 . . . 9.6
2134 . . 100 .
2135 . . . 8.18
2136 . . 200 .",
2137 );
2138
2139 test_fixture.do_test(expected_chunk, true, false).await;
2140 }
2141
2142 #[tokio::test]
2143 async fn test_left_anti_join() {
2144 let test_fixture = TestFixture::with_join_type(JoinType::LeftAnti);
2145
2146 let expected_chunk = DataChunk::from_pretty(
2147 "i f
2148 1 6.1
2149 . 8.4
2150 . .
2151 . 0.7
2152 5 .
2153 . 5.5",
2154 );
2155
2156 test_fixture.do_test(expected_chunk, false, false).await;
2157 }
2158
2159 #[tokio::test]
2160 async fn test_left_anti_join_with_non_equi_condition() {
2161 let test_fixture = TestFixture::with_join_type(JoinType::LeftAnti);
2162
2163 let expected_chunk = DataChunk::from_pretty(
2164 "i f
2165 2 .
2166 3 3.9
2167 3 .
2168 1 6.1
2169 . 8.4
2170 . .
2171 . 0.7
2172 5 .
2173 . 5.5",
2174 );
2175
2176 test_fixture.do_test(expected_chunk, true, false).await;
2177 }
2178
2179 #[tokio::test]
2180 async fn test_left_semi_join() {
2181 let test_fixture = TestFixture::with_join_type(JoinType::LeftSemi);
2182
2183 let expected_chunk = DataChunk::from_pretty(
2184 "i f
2185 2 .
2186 3 3.9
2187 4 6.6
2188 3 .",
2189 );
2190
2191 test_fixture.do_test(expected_chunk, false, false).await;
2192 }
2193
2194 #[tokio::test]
2195 async fn test_left_semi_join_with_non_equi_condition() {
2196 let test_fixture = TestFixture::with_join_type(JoinType::LeftSemi);
2197
2198 let expected_chunk = DataChunk::from_pretty(
2199 "i f
2200 4 6.6",
2201 );
2202
2203 test_fixture.do_test(expected_chunk, true, false).await;
2204 }
2205
2206 #[tokio::test]
2211 async fn test_left_semi_join_with_non_equi_condition_duplicates() {
2212 let schema = Schema {
2213 fields: vec![
2214 Field::unnamed(DataType::Int32),
2215 Field::unnamed(DataType::Float32),
2216 ],
2217 };
2218
2219 let mut left_executor = MockExecutor::new(schema);
2221 left_executor.add(DataChunk::from_pretty(
2222 "i f
2223 1 1.0
2224 1 1.0
2225 1 1.0
2226 1 1.0
2227 2 1.0",
2228 ));
2229
2230 let schema = Schema {
2232 fields: vec![
2233 Field::unnamed(DataType::Int32),
2234 Field::unnamed(DataType::Float64),
2235 ],
2236 };
2237 let mut right_executor = MockExecutor::new(schema);
2238 right_executor.add(DataChunk::from_pretty(
2239 "i F
2240 1 2.0
2241 1 2.0
2242 1 2.0
2243 1 2.0
2244 2 2.0",
2245 ));
2246
2247 let test_fixture = TestFixture::with_join_type(JoinType::LeftSemi);
2248 let expected_chunk = DataChunk::from_pretty(
2249 "i f
2250 1 1.0
2251 1 1.0
2252 1 1.0
2253 1 1.0
2254 2 1.0",
2255 );
2256
2257 test_fixture
2258 .do_test_with_chunk_size_and_executors(
2259 expected_chunk,
2260 true,
2261 false,
2262 3,
2263 Box::new(left_executor),
2264 Box::new(right_executor),
2265 false,
2266 )
2267 .await;
2268 }
2269
2270 #[tokio::test]
2271 async fn test_right_anti_join() {
2272 let test_fixture = TestFixture::with_join_type(JoinType::RightAnti);
2273
2274 let expected_chunk = DataChunk::from_pretty(
2275 "i F
2276 8 6.1
2277 . 8.9
2278 . 3.5
2279 6 .
2280 6 .
2281 . 8.0
2282 7 .
2283 . 9.1
2284 9 .
2285 9 .
2286 . 9.6
2287 100 .
2288 . 8.18
2289 200 .",
2290 );
2291
2292 test_fixture.do_test(expected_chunk, false, false).await;
2293 }
2294
2295 #[tokio::test]
2296 async fn test_right_anti_join_with_non_equi_condition() {
2297 let test_fixture = TestFixture::with_join_type(JoinType::RightAnti);
2298
2299 let expected_chunk = DataChunk::from_pretty(
2300 "i F
2301 8 6.1
2302 2 .
2303 . 8.9
2304 3 .
2305 . 3.5
2306 6 .
2307 6 .
2308 . 8
2309 7 .
2310 . 9.1
2311 9 .
2312 3 3.7
2313 9 .
2314 . 9.6
2315 100 .
2316 . 8.18
2317 200 .",
2318 );
2319
2320 test_fixture.do_test(expected_chunk, true, false).await;
2321 }
2322
2323 #[tokio::test]
2324 async fn test_right_semi_join() {
2325 let test_fixture = TestFixture::with_join_type(JoinType::RightSemi);
2326
2327 let expected_chunk = DataChunk::from_pretty(
2328 "i F
2329 2 .
2330 3 .
2331 4 7.5
2332 3 3.7",
2333 );
2334
2335 test_fixture.do_test(expected_chunk, false, false).await;
2336 }
2337
2338 #[tokio::test]
2339 async fn test_right_semi_join_with_non_equi_condition() {
2340 let test_fixture = TestFixture::with_join_type(JoinType::RightSemi);
2341
2342 let expected_chunk = DataChunk::from_pretty(
2343 "i F
2344 4 7.5",
2345 );
2346
2347 test_fixture.do_test(expected_chunk, true, false).await;
2348 }
2349
2350 #[tokio::test]
2352 async fn test_invisible_build_rows_are_not_emitted() {
2353 let cases = [
2354 (
2355 JoinType::RightOuter,
2356 "i f i F
2357 1 1.0 1 2.0
2358 . . 3 4.0",
2359 ),
2360 (
2361 JoinType::FullOuter,
2362 "i f i F
2363 1 1.0 1 2.0
2364 . . 3 4.0",
2365 ),
2366 (
2367 JoinType::RightAnti,
2368 "i F
2369 3 4.0",
2370 ),
2371 ];
2372 for (join_type, expected) in cases {
2373 for has_non_equi_cond in [false, true] {
2374 for test_spill in [false, true] {
2375 let mut left = MockExecutor::new(Schema::new(vec![
2376 Field::unnamed(DataType::Int32),
2377 Field::unnamed(DataType::Float32),
2378 ]));
2379 left.add(DataChunk::from_pretty(
2380 "i f
2381 1 1.0",
2382 ));
2383 let mut right = MockExecutor::new(Schema::new(vec![
2384 Field::unnamed(DataType::Int32),
2385 Field::unnamed(DataType::Float64),
2386 ]));
2387 right.add(DataChunk::from_pretty(
2388 "i F
2389 1 2.0
2390 2 3.0 D
2391 3 4.0",
2392 ));
2393 TestFixture::with_join_type(join_type)
2394 .do_test_with_chunk_size_and_executors(
2395 DataChunk::from_pretty(expected),
2396 has_non_equi_cond,
2397 false,
2398 CHUNK_SIZE,
2399 Box::new(left),
2400 Box::new(right),
2401 test_spill,
2402 )
2403 .await;
2404 }
2405 }
2406 }
2407 }
2408
2409 #[test]
2410 fn test_find_asof_matched_rows_prefers_closest_le_match() {
2411 let probe_chunk = DataChunk::from_pretty(
2415 "i
2416 5",
2417 );
2418 let build_chunk = DataChunk::from_pretty(
2419 "i
2420 10
2421 20",
2422 );
2423 let build_side = vec![build_chunk];
2424
2425 let mut next_row_id = ChunkedData::with_chunk_sizes([2]).unwrap();
2426 next_row_id[RowId::new(0, 0)] = Some(RowId::new(0, 1));
2427 next_row_id[RowId::new(0, 1)] = None;
2428
2429 let asof_desc = AsOfDesc {
2430 left_idx: 0,
2431 right_idx: 0,
2432 inequality_type: AsOfInequalityType::Le,
2433 };
2434
2435 let matched = HashJoinExecutor::<Key32>::find_asof_matched_rows(
2436 probe_chunk.row_at_unchecked_vis(0),
2437 &build_side,
2438 next_row_id.row_id_iter(Some(RowId::new(0, 0))),
2439 &asof_desc,
2440 );
2441
2442 assert_eq!(matched, Some(RowId::new(0, 0)));
2443 }
2444
2445 #[test]
2446 fn test_find_asof_matched_rows_prefers_closest_ge_match() {
2447 let probe_chunk = DataChunk::from_pretty(
2451 "i
2452 12",
2453 );
2454 let build_chunk = DataChunk::from_pretty(
2455 "i
2456 5
2457 10",
2458 );
2459 let build_side = vec![build_chunk];
2460
2461 let mut next_row_id = ChunkedData::with_chunk_sizes([2]).unwrap();
2462 next_row_id[RowId::new(0, 0)] = Some(RowId::new(0, 1));
2463 next_row_id[RowId::new(0, 1)] = None;
2464
2465 let asof_desc = AsOfDesc {
2466 left_idx: 0,
2467 right_idx: 0,
2468 inequality_type: AsOfInequalityType::Ge,
2469 };
2470
2471 let matched = HashJoinExecutor::<Key32>::find_asof_matched_rows(
2472 probe_chunk.row_at_unchecked_vis(0),
2473 &build_side,
2474 next_row_id.row_id_iter(Some(RowId::new(0, 0))),
2475 &asof_desc,
2476 );
2477
2478 assert_eq!(matched, Some(RowId::new(0, 1)));
2479 }
2480
2481 #[tokio::test]
2482 async fn test_batch_hash_join_asof_ge_returns_closest_match() {
2483 let left_schema = Schema {
2484 fields: vec![
2485 Field::unnamed(DataType::Int32),
2486 Field::unnamed(DataType::Int32),
2487 ],
2488 };
2489 let right_schema = Schema {
2490 fields: vec![
2491 Field::unnamed(DataType::Int32),
2492 Field::unnamed(DataType::Int32),
2493 ],
2494 };
2495
2496 let mut left_executor = MockExecutor::new(left_schema);
2497 left_executor.add(DataChunk::from_pretty(
2498 "i i
2499 3 12",
2500 ));
2501
2502 let mut right_executor = MockExecutor::new(right_schema);
2503 right_executor.add(DataChunk::from_pretty(
2504 "i i
2505 3 5
2506 3 10",
2507 ));
2508
2509 let join_executor = Box::new(HashJoinExecutor::<Key32>::new(
2510 JoinType::Inner,
2511 vec![0, 1, 2, 3],
2512 Box::new(left_executor),
2513 Box::new(right_executor),
2514 vec![0],
2515 vec![0],
2516 vec![false],
2517 None,
2518 "HashJoinExecutor".to_owned(),
2519 CHUNK_SIZE,
2520 Some(AsOfDesc {
2521 left_idx: 1,
2522 right_idx: 1,
2523 inequality_type: AsOfInequalityType::Ge,
2524 }),
2525 None,
2526 BatchSpillMetrics::for_test(),
2527 ShutdownToken::empty(),
2528 MemoryContext::new(None, LabelGuardedIntGauge::test_int_gauge::<4>()),
2529 ));
2530
2531 let mut stream = join_executor.execute();
2532 let chunk = stream.next().await.unwrap().unwrap().compact_vis();
2533 let expected = DataChunk::from_pretty(
2534 "i i i i
2535 3 12 3 10",
2536 );
2537
2538 assert!(compare_data_chunk_with_rowsort(&expected, &chunk));
2539 assert!(stream.next().await.is_none());
2540 }
2541
2542 #[tokio::test]
2543 async fn test_shutdown() {
2544 let test_fixture = TestFixture::with_join_type(JoinType::Inner);
2545 test_fixture.do_test_shutdown(false).await;
2546 test_fixture.do_test_shutdown(true).await;
2547
2548 let test_fixture = TestFixture::with_join_type(JoinType::FullOuter);
2549 test_fixture.do_test_shutdown(false).await;
2550 test_fixture.do_test_shutdown(true).await;
2551
2552 let test_fixture = TestFixture::with_join_type(JoinType::LeftAnti);
2553 test_fixture.do_test_shutdown(false).await;
2554 test_fixture.do_test_shutdown(true).await;
2555
2556 let test_fixture = TestFixture::with_join_type(JoinType::LeftOuter);
2557 test_fixture.do_test_shutdown(false).await;
2558 test_fixture.do_test_shutdown(true).await;
2559
2560 let test_fixture = TestFixture::with_join_type(JoinType::LeftSemi);
2561 test_fixture.do_test_shutdown(false).await;
2562 test_fixture.do_test_shutdown(true).await;
2563
2564 let test_fixture = TestFixture::with_join_type(JoinType::RightAnti);
2565 test_fixture.do_test_shutdown(false).await;
2566 test_fixture.do_test_shutdown(true).await;
2567
2568 let test_fixture = TestFixture::with_join_type(JoinType::RightOuter);
2569 test_fixture.do_test_shutdown(false).await;
2570 test_fixture.do_test_shutdown(true).await;
2571
2572 let test_fixture = TestFixture::with_join_type(JoinType::RightSemi);
2573 test_fixture.do_test_shutdown(false).await;
2574 test_fixture.do_test_shutdown(true).await;
2575 }
2576}