Skip to main content

risingwave_batch_executors/executor/join/
hash_join.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
53/// Hash Join Executor
54///
55/// High-level idea:
56/// 1. Iterate over the build side (i.e. right table) and build a hash map.
57/// 2. Iterate over the probe side (i.e. left table) and compute the hash value of each row.
58///    Then find the matched build side row for each probe side row in the hash map.
59/// 3. Concatenate the matched pair of probe side row and build side row into a single row and push
60///    it into the data chunk builder.
61/// 4. Yield chunks from the builder.
62pub struct HashJoinExecutor<K> {
63    /// Join type e.g. inner, left outer, ...
64    join_type: JoinType,
65    /// Output schema without applying `output_indices`
66    #[expect(dead_code)]
67    original_schema: Schema,
68    /// Output schema after applying `output_indices`
69    schema: Schema,
70    /// `output_indices` are the indices of the columns that we needed.
71    output_indices: Vec<usize>,
72    /// Left child executor
73    probe_side_source: BoxedExecutor,
74    /// Right child executor
75    build_side_source: BoxedExecutor,
76    /// Column indices of left keys in equi join
77    probe_key_idxs: Vec<usize>,
78    /// Column indices of right keys in equi join
79    build_key_idxs: Vec<usize>,
80    /// Non-equi join condition (optional)
81    cond: Option<Arc<BoxedExpression>>,
82    /// Whether or not to enable 'IS NOT DISTINCT FROM' semantics for a specific probe/build key
83    /// column
84    null_matched: Vec<bool>,
85    identity: String,
86    chunk_size: usize,
87    /// Whether the join is an as-of join
88    asof_desc: Option<AsOfDesc>,
89
90    spill_backend: Option<SpillBackend>,
91    spill_metrics: Arc<BatchSpillMetrics>,
92    /// The upper bound of memory usage for this executor.
93    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
115/// In `JoinHashMap`, we only save the row id of the first build row that has the hash key.
116/// In fact, in the build side there may be multiple rows with the same hash key. To handle this
117/// case, we use `ChunkedData` to link them together. For example:
118///
119/// | id | key | row |
120/// | --- | --- | --- |
121/// | 0 | 1 | (1, 2, 3) |
122/// | 1 | 4 | (4, 5, 6) |
123/// | 2 | 1 | (1, 3, 7) |
124/// | 3 | 1 | (1, 3, 2) |
125/// | 4 | 3 | (3, 2, 1) |
126///
127/// The corresponding join hash map is:
128///
129/// | key | value |
130/// | --- | --- |
131/// | 1 | 0 |
132/// | 4 | 1 |
133/// | 3 | 4 |
134///
135/// And we save build rows with the same key like this:
136///
137/// | id | value |
138/// | --- | --- |
139/// | 0 | 2 |
140/// | 1 | None |
141/// | 2 | 3 |
142/// | 3 | None |
143/// | 4 | None |
144///
145/// This can be seen as an implicit linked list. For convenience, we use `RowIdIter` to iterate all
146/// build side row ids with the given key.
147pub 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/// What a join type outputs for probe or build rows, besides matched pairs.
224#[derive(Clone, Copy, PartialEq, Eq)]
225enum RowOutput {
226    /// Nothing.
227    Omit,
228    /// Each matched row, once (semi join).
229    Matched,
230    /// Each unmatched row (anti join).
231    Unmatched,
232    /// Each unmatched row, padded with NULLs for the other side (outer join).
233    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/// What a join type outputs. See [`HashJoinExecutor::do_probe`].
247#[derive(Clone, Copy)]
248struct JoinOutput {
249    /// Whether to output each matched pair of a probe row and a build row.
250    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    /// Whether all candidates of a probe row are needed. If not, only whether the probe row
276    /// matched matters, and its remaining candidates are skipped once it has.
277    fn needs_every_candidate(self) -> bool {
278        self.pairs || self.build_rows != RowOutput::Omit
279    }
280
281    /// Data types of the rows output for probe or build rows.
282    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
297/// Whether each probe row of the current probe chunk and each build row has matched. Each side
298/// is only tracked if the join outputs rows of that side.
299struct 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/// The probe and build row ids of the candidate pairs buffered for evaluating the non-equi
324/// condition, in the order of the buffered rows. Only the ids of the sides whose match state is
325/// tracked are kept.
326#[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    /// Evaluates `cond` on `pairs`, the buffered candidate pairs, and marks the rows of each pair
343    /// that satisfies it as matched. Returns the matched pairs if the join outputs them.
344    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
383/// `JoinSpillManager` is used to manage how to write spill data file and read them back.
384/// The spill data first need to be partitioned. Each partition contains 2 files: `join_probe_side_file` and `join_build_side_file`.
385/// The spill file consume a data chunk and serialize the chunk into a protobuf bytes.
386/// Finally, spill file content will look like the below.
387/// The file write pattern is append-only and the read pattern is sequential scan.
388/// This can maximize the disk IO performance.
389///
390/// ```text
391/// [proto_len]
392/// [proto_bytes]
393/// ...
394/// [proto_len]
395/// [proto_bytes]
396/// ```
397impl 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        // This invocation owns its accounting. Its private context releases remaining charges
600        // on completion, errors, or cancellation, even if the shared parent stays alive.
601        let mem_ctx = MemoryContext::new(Some(self.mem_ctx.clone()), TrAdderAtomic::new(0));
602        let mut need_to_spill = false;
603        // If the memory upper bound is less than 1MB, we don't need to check memory usage.
604        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            // Unmatched build rows are emitted by walking all physical rows, so drop invisible rows
621            // here to keep them out of right outer/anti and full outer join results.
622            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                // push build_chunk to build_side before checking memory limit, otherwise we will lose that chunk when spilling.
627                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 hash map
652            '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                    // Only insert key to hash map if it is consistent with the null safe restriction.
662                    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                        // Retain and charge the key before deciding whether to spill the table.
667                        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            // A spilling version of hash join based on the RFC: Spill Hash Join https://github.com/risingwavelabs/rfcs/pull/91
684            // When HashJoinExecutor told memory is insufficient, JoinSpillManager will start to partition the hash table and spill to disk.
685            // After spilling the hash table, JoinSpillManager will consume all chunks from its build side input executor and probe side input executor.
686            // Finally, we would get e.g. 20 partitions. Each partition should contain a portion of the original build side input and probr side input data.
687            // A sub HashJoinExecutor would be used to consume each partition one by one.
688            // If memory is still not enough in the sub HashJoinExecutor, it will spill its inputs recursively.
689            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            // Release memory occupied by the hash map
705            drop(hash_map);
706            drop(next_build_row_with_same_key);
707            mem_ctx.add_unchecked(-mem_added_by_hash_table);
708
709            // Spill buffered build side chunks
710            for chunk in build_side {
711                // Release the memory occupied by the buffered chunks
712                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            // Spill build side chunks
730            #[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            // Spill probe side chunks
749            #[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            // Process each partition one by one.
770            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                // Clear files of the current partition.
814                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            // Build data can be reused throughout the join. The private context releases its
842            // remaining charges when this invocation exits.
843        }
844    }
845
846    /// Probes the hash table, projects the output to `output_indices`, and yields chunks of
847    /// `params.chunk_size` rows.
848    ///
849    /// AS OF joins apply their condition through `asof_desc`, so `cond` is ignored for them.
850    #[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            // Evaluating the condition hides rows and leaves partial chunks behind, so realign
863            // the output chunks.
864            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    /// Probes the hash table with each probe row and produces the join output.
885    ///
886    /// The candidates of a probe row are the build rows with the same key, or for an AS OF join,
887    /// the closest of them. A candidate pair matches if it satisfies the non-equi condition
888    /// `cond`, or always if there is none. A probe or build row is matched if it is part of a
889    /// matched pair. [`JoinOutput`] gives what each join type outputs:
890    ///
891    /// | join type         | matched pairs | probe rows            | build rows            |
892    /// |-------------------|---------------|-----------------------|-----------------------|
893    /// | inner             | yes           |                       |                       |
894    /// | left outer        | yes           | unmatched, with NULLs |                       |
895    /// | left semi / anti  |               | matched / unmatched   |                       |
896    /// | right outer       | yes           |                       | unmatched, with NULLs |
897    /// | right semi / anti |               |                       | matched / unmatched   |
898    /// | full outer        | yes           | unmatched, with NULLs | unmatched, with NULLs |
899    ///
900    /// With a condition, candidate pairs are buffered and the condition is evaluated on a whole
901    /// chunk of them at a time. Probe rows are output per probe chunk: if the join outputs them,
902    /// all candidates of a probe chunk are evaluated before moving on to the next one, so that
903    /// they have their final match state. Build rows are output after the last probe chunk.
904    #[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        // One row of NULLs for each side, to pad the unmatched rows of the other side.
930        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                    // Semi and anti joins without a condition: every candidate matches, and only
957                    // which rows matched is needed.
958                    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            // Evaluate the remaining candidates of this probe chunk, so that its probe rows have
1021            // their final match state before they are output.
1022            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    /// Sort each row in the data chunk and compare with the rows in the data chunk.
1512    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        // Sort and compare
1521        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    /// Sql for creating test data:
1554    /// ```sql
1555    /// drop table t1 if exists;
1556    /// create table t1(v1 int, v2 float);
1557    /// insert into t1 values
1558    /// (1, 6.1::FLOAT), (2, null), (null, 8.4::FLOAT), (3, 3.9::FLOAT), (null, null),
1559    /// (4, 6.6::FLOAT), (3, null), (null, 0.7::FLOAT), (5, null), (null, 5.5::FLOAT);
1560    ///
1561    /// drop table t2 if exists;
1562    /// create table t2(v1 int, v2 real);
1563    /// insert into t2 values
1564    /// (8, 6.1::REAL), (2, null), (null, 8.9::REAL), (3, null), (null, 3.5::REAL),
1565    /// (6, null), (4, 7.5::REAL), (6, null), (null, 8::REAL), (7, null),
1566    /// (null, 9.1::REAL), (9, null), (3, 3.7::REAL), (9, null), (null, 9.6::REAL),
1567    /// (100, null), (null, 8.18::REAL), (200, null);
1568    /// ```
1569    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            // Test spill
1740            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                // TODO: Replace this with unsorted comparison
1807                // assert_eq!(expected, result_chunk);
1808                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            // Test `ShutdownMsg::Cancel`
1816            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            // Test `ShutdownMsg::Abort`
1837            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    /// Sql:
1860    /// ```sql
1861    /// select * from t1 join t2 on t1.v1 = t2.v1;
1862    /// ```
1863    #[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    /// Sql:
1881    /// ```sql
1882    /// select * from t1 join t2 on t1.v1 is not distinct from t2.v1;
1883    /// ```
1884    #[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    /// Sql:
1926    /// ```sql
1927    /// select * from t1 join t2 on t1.v1 = t2.v1 and t1.v2 < t2.v2;
1928    /// ```
1929    #[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    /// Sql:
1942    /// ```sql
1943    /// select t1.v2 as t1_v2, t2.v2 as t2_v2 from t1 left outer join t2 on t1.v1 = t2.v1;
1944    /// ```
1945    #[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    /// Sql:
1969    /// ```sql
1970    /// select * from t1 left outer join t2 on t1.v1 = t2.v1 and t1.v2 < t2.v2;
1971    /// ```
1972    #[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    /// Sql:
1994    /// ```sql
1995    /// select * from t1 right outer join t2 on t1.v1 = t2.v1;
1996    /// ```
1997    #[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    /// Sql:
2029    /// ```sql
2030    /// select * from t1 left outer join t2 on t1.v1 = t2.v1 and t1.v2 < t2.v2;
2031    /// ```
2032    #[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    /// ```sql
2062    /// select * from t1 full outer join t2 on t1.v1 = t2.v1;
2063    /// ```
2064    #[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    /// ```sql
2102    /// select * from t1 full outer join t2 on t1.v1 = t2.v1 and t1.v2 < t2.v2;
2103    /// ```
2104    #[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    /// Tests handling of edge case:
2207    /// Match is found for a `probe_row`,
2208    /// but there are still candidate rows in the iterator for that `probe_row`.
2209    /// These should not be buffered or we will have duplicate rows in output.
2210    #[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        // Build side
2220        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        // Probe side
2231        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    /// Build rows hidden by visibility must not be emitted as unmatched build rows.
2351    #[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        // Le: left <= right, so probe <= build.
2412        // probe=5, builds=[10, 20]. Both satisfy 5<=10 and 5<=20.
2413        // Closest (minimum build) = 10 at row 0.
2414        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        // Ge: left >= right, so probe >= build.
2448        // probe=12, builds=[5, 10]. Both satisfy 12>=5 and 12>=10.
2449        // Closest (maximum build) = 10 at row 1.
2450        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}