Skip to main content

risingwave_stream/executor/
hash_join.rs

1// Copyright 2022 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::assert_matches;
16use std::collections::{BTreeMap, HashSet};
17use std::marker::PhantomData;
18use std::time::Duration;
19
20use anyhow::Context;
21use either::Either;
22use itertools::Itertools;
23use multimap::MultiMap;
24use risingwave_common::array::Op;
25use risingwave_common::hash::{HashKey, NullBitmap};
26use risingwave_common::metrics::LabelGuardedHistogram;
27use risingwave_common::row::RowExt;
28use risingwave_common::types::{DefaultOrd, ToOwnedDatum};
29use risingwave_common::util::epoch::EpochPair;
30use risingwave_common::util::iter_util::ZipEqDebug;
31use risingwave_expr::expr::NonStrictExpression;
32use risingwave_pb::stream_plan::InequalityType;
33use tokio::time::Instant;
34
35use self::builder::JoinChunkBuilder;
36use super::barrier_align::*;
37use super::join::hash_join::*;
38use super::join::row::{JoinEncoding, JoinRow};
39use super::join::*;
40use super::watermark::*;
41use crate::executor::CachedJoinRow;
42use crate::executor::join::builder::JoinStreamChunkBuilder;
43use crate::executor::join::hash_join::CacheResult;
44use crate::executor::prelude::*;
45
46fn is_subset(vec1: Vec<usize>, vec2: Vec<usize>) -> bool {
47    HashSet::<usize>::from_iter(vec1).is_subset(&vec2.into_iter().collect())
48}
49
50/// Information about an inequality condition in the join.
51/// Represents: `left_col <op> right_col` where op is one of `<`, `<=`, `>`, `>=`.
52#[derive(Debug, Clone)]
53pub struct InequalityPairInfo {
54    /// Index of the left side column (from left input).
55    pub left_idx: usize,
56    /// Index of the right side column (from right input).
57    pub right_idx: usize,
58    /// Whether this condition is used to clean left state table.
59    pub clean_left_state: bool,
60    /// Whether this condition is used to clean right state table.
61    pub clean_right_state: bool,
62    /// Comparison operator.
63    pub op: InequalityType,
64}
65
66impl InequalityPairInfo {
67    /// Returns true if left side has larger values (`>` or `>=`).
68    pub fn left_side_is_larger(&self) -> bool {
69        matches!(
70            self.op,
71            InequalityType::GreaterThan | InequalityType::GreaterThanOrEqual
72        )
73    }
74}
75
76pub struct JoinParams {
77    /// Indices of the join keys
78    pub join_key_indices: Vec<usize>,
79    /// Indices of the input pk after dedup
80    pub deduped_pk_indices: Vec<usize>,
81}
82
83impl JoinParams {
84    pub fn new(join_key_indices: Vec<usize>, deduped_pk_indices: Vec<usize>) -> Self {
85        Self {
86            join_key_indices,
87            deduped_pk_indices,
88        }
89    }
90}
91
92struct JoinSide<K: HashKey, S: StateStore, E: JoinEncoding> {
93    /// Store all data from a one side stream
94    ht: JoinHashMap<K, S, E>,
95    /// Indices of the join key columns
96    join_key_indices: Vec<usize>,
97    /// The data type of all columns without degree.
98    all_data_types: Vec<DataType>,
99    /// The start position for the side in output new columns
100    start_pos: usize,
101    /// The mapping from input indices of a side to output columns.
102    i2o_mapping: Vec<(usize, usize)>,
103    i2o_mapping_indexed: MultiMap<usize, usize>,
104    /// The first field of the ith element indicates that when a watermark at the ith column of
105    /// this side comes, what band join conditions should be updated in order to possibly
106    /// generate a new watermark at that column or the corresponding column in the counterpart
107    /// join side.
108    ///
109    /// The second field indicates that whether the column is required less than the
110    /// the corresponding column in the counterpart join side in the band join condition.
111    input2inequality_index: Vec<Vec<(usize, bool)>>,
112    /// Some fields which are required non null to match due to inequalities.
113    non_null_fields: Vec<usize>,
114    /// (i, j) in this `Vec` means that state data in this join side can be cleaned if the value of
115    /// its ith column is less than the synthetic watermark of the jth band join condition.
116    state_clean_columns: Vec<(usize, usize)>,
117    /// Whether degree table is needed for this side.
118    need_degree_table: bool,
119    _marker: std::marker::PhantomData<E>,
120}
121
122impl<K: HashKey, S: StateStore, E: JoinEncoding> std::fmt::Debug for JoinSide<K, S, E> {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("JoinSide")
125            .field("join_key_indices", &self.join_key_indices)
126            .field("col_types", &self.all_data_types)
127            .field("start_pos", &self.start_pos)
128            .field("i2o_mapping", &self.i2o_mapping)
129            .field("need_degree_table", &self.need_degree_table)
130            .finish()
131    }
132}
133
134impl<K: HashKey, S: StateStore, E: JoinEncoding> JoinSide<K, S, E> {
135    // WARNING: Please do not call this until we implement it.
136    fn is_dirty(&self) -> bool {
137        unimplemented!()
138    }
139
140    #[expect(dead_code)]
141    fn clear_cache(&mut self) {
142        assert!(
143            !self.is_dirty(),
144            "cannot clear cache while states of hash join are dirty"
145        );
146
147        // TODO: not working with rearranged chain
148        // self.ht.clear();
149    }
150
151    pub async fn init(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
152        self.ht.init(epoch).await
153    }
154}
155
156/// `HashJoinExecutor` takes two input streams and runs equal hash join on them.
157/// The output columns are the concatenation of left and right columns.
158pub struct HashJoinExecutor<K: HashKey, S: StateStore, const T: JoinTypePrimitive, E: JoinEncoding>
159{
160    ctx: ActorContextRef,
161    info: ExecutorInfo,
162
163    /// Left input executor
164    input_l: Option<Executor>,
165    /// Right input executor
166    input_r: Option<Executor>,
167    /// The data types of the formed new columns
168    actual_output_data_types: Vec<DataType>,
169    /// The parameters of the left join executor
170    side_l: JoinSide<K, S, E>,
171    /// The parameters of the right join executor
172    side_r: JoinSide<K, S, E>,
173    /// Optional non-equi join conditions
174    cond: Option<NonStrictExpression>,
175    /// Inequality pairs with output column indices.
176    /// Each entry contains: (`output_indices`, `InequalityPairInfo`).
177    inequality_pairs: Vec<(Vec<usize>, InequalityPairInfo)>,
178    /// The output watermark of each inequality condition and its value is the minimum of the
179    /// watermarks from both sides. It will be used to generate watermark into downstream
180    /// and do state cleaning based on `clean_left_state`/`clean_right_state` fields.
181    inequality_watermarks: Vec<Option<Watermark>>,
182    /// Join-key positions and cleaning flags for watermark handling.
183    /// Each entry: (join-key position, `do_state_cleaning`).
184    watermark_indices_in_jk: Vec<(usize, bool)>,
185
186    /// Whether the logic can be optimized for append-only stream
187    append_only_optimize: bool,
188
189    metrics: Arc<StreamingMetrics>,
190    /// The maximum size of the chunk produced by executor at a time
191    chunk_size: usize,
192    /// Count the messages received, clear to 0 when counted to `EVICT_EVERY_N_MESSAGES`
193    cnt_rows_received: u32,
194
195    /// watermark column index -> `BufferedWatermarks`
196    watermark_buffers: BTreeMap<usize, BufferedWatermarks<SideTypePrimitive>>,
197
198    /// When to alert high join amplification
199    high_join_amplification_threshold: usize,
200
201    /// Max number of rows that will be cached in the entry state.
202    entry_state_max_rows: usize,
203    /// Number of processed rows between periodic manual evictions of the join cache.
204    join_cache_evict_interval_rows: u32,
205}
206
207impl<K: HashKey, S: StateStore, const T: JoinTypePrimitive, E: JoinEncoding> std::fmt::Debug
208    for HashJoinExecutor<K, S, T, E>
209{
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.debug_struct("HashJoinExecutor")
212            .field("join_type", &T)
213            .field("input_left", &self.input_l.as_ref().unwrap().identity())
214            .field("input_right", &self.input_r.as_ref().unwrap().identity())
215            .field("side_l", &self.side_l)
216            .field("side_r", &self.side_r)
217            .field("stream_key", &self.info.stream_key)
218            .field("schema", &self.info.schema)
219            .field("actual_output_data_types", &self.actual_output_data_types)
220            .field(
221                "join_cache_evict_interval_rows",
222                &self.join_cache_evict_interval_rows,
223            )
224            .finish()
225    }
226}
227
228impl<K: HashKey, S: StateStore, const T: JoinTypePrimitive, E: JoinEncoding> Execute
229    for HashJoinExecutor<K, S, T, E>
230{
231    fn execute(self: Box<Self>) -> BoxedMessageStream {
232        self.into_stream().boxed()
233    }
234}
235
236struct EqJoinArgs<'a, K: HashKey, S: StateStore, E: JoinEncoding> {
237    ctx: &'a ActorContextRef,
238    side_l: &'a mut JoinSide<K, S, E>,
239    side_r: &'a mut JoinSide<K, S, E>,
240    actual_output_data_types: &'a [DataType],
241    cond: &'a mut Option<NonStrictExpression>,
242    inequality_watermarks: &'a [Option<Watermark>],
243    chunk: StreamChunk,
244    append_only_optimize: bool,
245    chunk_size: usize,
246    cnt_rows_received: &'a mut u32,
247    high_join_amplification_threshold: usize,
248    entry_state_max_rows: usize,
249    join_cache_evict_interval_rows: u32,
250    join_matched_join_keys: &'a LabelGuardedHistogram,
251}
252
253impl<K: HashKey, S: StateStore, const T: JoinTypePrimitive, E: JoinEncoding>
254    HashJoinExecutor<K, S, T, E>
255{
256    #[expect(clippy::too_many_arguments)]
257    pub fn new(
258        ctx: ActorContextRef,
259        info: ExecutorInfo,
260        input_l: Executor,
261        input_r: Executor,
262        params_l: JoinParams,
263        params_r: JoinParams,
264        null_safe: Vec<bool>,
265        output_indices: Vec<usize>,
266        cond: Option<NonStrictExpression>,
267        inequality_pairs: Vec<InequalityPairInfo>,
268        state_table_l: StateTable<S>,
269        degree_state_table_l: StateTable<S>,
270        state_table_r: StateTable<S>,
271        degree_state_table_r: StateTable<S>,
272        watermark_epoch: AtomicU64Ref,
273        is_append_only: bool,
274        metrics: Arc<StreamingMetrics>,
275        chunk_size: usize,
276        high_join_amplification_threshold: usize,
277        watermark_indices_in_jk: Vec<(usize, bool)>,
278    ) -> Self {
279        Self::new_with_cache_size(
280            ctx,
281            info,
282            input_l,
283            input_r,
284            params_l,
285            params_r,
286            null_safe,
287            output_indices,
288            cond,
289            inequality_pairs,
290            state_table_l,
291            degree_state_table_l,
292            state_table_r,
293            degree_state_table_r,
294            watermark_epoch,
295            is_append_only,
296            metrics,
297            chunk_size,
298            high_join_amplification_threshold,
299            None,
300            watermark_indices_in_jk,
301        )
302    }
303
304    #[expect(clippy::too_many_arguments)]
305    pub fn new_with_cache_size(
306        ctx: ActorContextRef,
307        info: ExecutorInfo,
308        input_l: Executor,
309        input_r: Executor,
310        params_l: JoinParams,
311        params_r: JoinParams,
312        null_safe: Vec<bool>,
313        output_indices: Vec<usize>,
314        cond: Option<NonStrictExpression>,
315        inequality_pairs: Vec<InequalityPairInfo>,
316        state_table_l: StateTable<S>,
317        degree_state_table_l: StateTable<S>,
318        state_table_r: StateTable<S>,
319        degree_state_table_r: StateTable<S>,
320        watermark_epoch: AtomicU64Ref,
321        is_append_only: bool,
322        metrics: Arc<StreamingMetrics>,
323        chunk_size: usize,
324        high_join_amplification_threshold: usize,
325        entry_state_max_rows: Option<usize>,
326        watermark_indices_in_jk: Vec<(usize, bool)>,
327    ) -> Self {
328        let entry_state_max_rows = match entry_state_max_rows {
329            None => ctx.config.developer.hash_join_entry_state_max_rows,
330            Some(entry_state_max_rows) => entry_state_max_rows,
331        };
332        let join_cache_evict_interval_rows = ctx
333            .config
334            .developer
335            .join_hash_map_evict_interval_rows
336            .max(1);
337        let side_l_column_n = input_l.schema().len();
338
339        let schema_fields = match T {
340            JoinType::LeftSemi | JoinType::LeftAnti => input_l.schema().fields.clone(),
341            JoinType::RightSemi | JoinType::RightAnti => input_r.schema().fields.clone(),
342            _ => [
343                input_l.schema().fields.clone(),
344                input_r.schema().fields.clone(),
345            ]
346            .concat(),
347        };
348
349        let original_output_data_types = schema_fields
350            .iter()
351            .map(|field| field.data_type())
352            .collect_vec();
353        let actual_output_data_types = output_indices
354            .iter()
355            .map(|&idx| original_output_data_types[idx].clone())
356            .collect_vec();
357
358        // Data types of of hash join state.
359        let state_all_data_types_l = input_l.schema().data_types();
360        let state_all_data_types_r = input_r.schema().data_types();
361
362        let state_pk_indices_l = input_l.stream_key().to_vec();
363        let state_pk_indices_r = input_r.stream_key().to_vec();
364
365        let state_join_key_indices_l = params_l.join_key_indices;
366        let state_join_key_indices_r = params_r.join_key_indices;
367
368        let degree_join_key_indices_l = (0..state_join_key_indices_l.len()).collect_vec();
369        let degree_join_key_indices_r = (0..state_join_key_indices_r.len()).collect_vec();
370
371        let degree_pk_indices_l = (state_join_key_indices_l.len()
372            ..state_join_key_indices_l.len() + params_l.deduped_pk_indices.len())
373            .collect_vec();
374        let degree_pk_indices_r = (state_join_key_indices_r.len()
375            ..state_join_key_indices_r.len() + params_r.deduped_pk_indices.len())
376            .collect_vec();
377
378        // If pk is contained in join key.
379        let pk_contained_in_jk_l = is_subset(state_pk_indices_l, state_join_key_indices_l.clone());
380        let pk_contained_in_jk_r = is_subset(state_pk_indices_r, state_join_key_indices_r.clone());
381
382        // check whether join key contains pk in both side
383        let append_only_optimize = is_append_only && pk_contained_in_jk_l && pk_contained_in_jk_r;
384
385        let join_key_data_types_l = state_join_key_indices_l
386            .iter()
387            .map(|idx| state_all_data_types_l[*idx].clone())
388            .collect_vec();
389
390        let join_key_data_types_r = state_join_key_indices_r
391            .iter()
392            .map(|idx| state_all_data_types_r[*idx].clone())
393            .collect_vec();
394
395        assert_eq!(join_key_data_types_l, join_key_data_types_r);
396
397        let null_matched = K::Bitmap::from_bool_vec(null_safe);
398
399        let need_degree_table_l = need_left_degree(T) && !pk_contained_in_jk_r;
400        let need_degree_table_r = need_right_degree(T) && !pk_contained_in_jk_l;
401
402        let (left_to_output, right_to_output) = {
403            let (left_len, right_len) = if is_left_semi_or_anti(T) {
404                (state_all_data_types_l.len(), 0usize)
405            } else if is_right_semi_or_anti(T) {
406                (0usize, state_all_data_types_r.len())
407            } else {
408                (state_all_data_types_l.len(), state_all_data_types_r.len())
409            };
410            JoinStreamChunkBuilder::get_i2o_mapping(&output_indices, left_len, right_len)
411        };
412
413        let l2o_indexed = MultiMap::from_iter(left_to_output.iter().copied());
414        let r2o_indexed = MultiMap::from_iter(right_to_output.iter().copied());
415
416        let left_input_len = input_l.schema().len();
417        let right_input_len = input_r.schema().len();
418        let mut l2inequality_index = vec![vec![]; left_input_len];
419        let mut r2inequality_index = vec![vec![]; right_input_len];
420        let mut l_inequal_state_clean_columns = vec![];
421        let mut r_inequal_state_clean_columns = vec![];
422        let inequality_pairs = inequality_pairs
423            .into_iter()
424            .enumerate()
425            .map(|(index, pair)| {
426                // Map input columns to inequality index
427                // The second field indicates whether this column is required to be less than
428                // the corresponding column on the other side.
429                // For `left >= right`: left is NOT less than right, right IS less than left
430                // For `left <= right`: left IS less than right, right is NOT less than left
431                let left_is_larger = pair.left_side_is_larger();
432                l2inequality_index[pair.left_idx].push((index, !left_is_larger));
433                r2inequality_index[pair.right_idx].push((index, left_is_larger));
434
435                // Determine state cleanup columns
436                if pair.clean_left_state {
437                    l_inequal_state_clean_columns.push((pair.left_idx, index));
438                }
439                if pair.clean_right_state {
440                    r_inequal_state_clean_columns.push((pair.right_idx, index));
441                }
442
443                // Get output indices for watermark emission
444                // We only emit watermarks for the LARGER side's output columns
445                let output_indices = if pair.left_side_is_larger() {
446                    // Left is larger, emit for left output columns
447                    l2o_indexed
448                        .get_vec(&pair.left_idx)
449                        .cloned()
450                        .unwrap_or_default()
451                } else {
452                    // Right is larger, emit for right output columns
453                    r2o_indexed
454                        .get_vec(&pair.right_idx)
455                        .cloned()
456                        .unwrap_or_default()
457                };
458
459                (output_indices, pair)
460            })
461            .collect_vec();
462
463        let mut l_non_null_fields = l2inequality_index
464            .iter()
465            .positions(|inequalities| !inequalities.is_empty())
466            .collect_vec();
467        let mut r_non_null_fields = r2inequality_index
468            .iter()
469            .positions(|inequalities| !inequalities.is_empty())
470            .collect_vec();
471
472        if append_only_optimize {
473            l_inequal_state_clean_columns.clear();
474            r_inequal_state_clean_columns.clear();
475            l_non_null_fields.clear();
476            r_non_null_fields.clear();
477        }
478
479        // Create degree tables with inequality column index for watermark-based cleaning.
480        // The degree_inequality_idx is the column index in the input row that should be
481        // stored in the degree table for inequality-based watermark cleaning.
482        let l_inequality_idx = l_inequal_state_clean_columns
483            .first()
484            .map(|(col_idx, _)| *col_idx);
485        let r_inequality_idx = r_inequal_state_clean_columns
486            .first()
487            .map(|(col_idx, _)| *col_idx);
488
489        let degree_state_l = need_degree_table_l.then(|| {
490            TableInner::new(
491                degree_pk_indices_l,
492                degree_join_key_indices_l,
493                degree_state_table_l,
494                l_inequality_idx,
495            )
496        });
497        let degree_state_r = need_degree_table_r.then(|| {
498            TableInner::new(
499                degree_pk_indices_r,
500                degree_join_key_indices_r,
501                degree_state_table_r,
502                r_inequality_idx,
503            )
504        });
505
506        let inequality_watermarks = vec![None; inequality_pairs.len()];
507        let watermark_buffers = BTreeMap::new();
508        Self {
509            ctx: ctx.clone(),
510            info,
511            input_l: Some(input_l),
512            input_r: Some(input_r),
513            actual_output_data_types,
514            side_l: JoinSide {
515                ht: JoinHashMap::new(
516                    watermark_epoch.clone(),
517                    join_key_data_types_l,
518                    state_join_key_indices_l.clone(),
519                    state_all_data_types_l.clone(),
520                    state_table_l,
521                    params_l.deduped_pk_indices,
522                    degree_state_l,
523                    null_matched.clone(),
524                    pk_contained_in_jk_l,
525                    metrics.clone(),
526                    ctx.id,
527                    ctx.fragment_id,
528                    "left",
529                ),
530                join_key_indices: state_join_key_indices_l,
531                all_data_types: state_all_data_types_l,
532                i2o_mapping: left_to_output,
533                i2o_mapping_indexed: l2o_indexed,
534                input2inequality_index: l2inequality_index,
535                non_null_fields: l_non_null_fields,
536                state_clean_columns: l_inequal_state_clean_columns,
537                start_pos: 0,
538                need_degree_table: need_degree_table_l,
539                _marker: PhantomData,
540            },
541            side_r: JoinSide {
542                ht: JoinHashMap::new(
543                    watermark_epoch,
544                    join_key_data_types_r,
545                    state_join_key_indices_r.clone(),
546                    state_all_data_types_r.clone(),
547                    state_table_r,
548                    params_r.deduped_pk_indices,
549                    degree_state_r,
550                    null_matched,
551                    pk_contained_in_jk_r,
552                    metrics.clone(),
553                    ctx.id,
554                    ctx.fragment_id,
555                    "right",
556                ),
557                join_key_indices: state_join_key_indices_r,
558                all_data_types: state_all_data_types_r,
559                start_pos: side_l_column_n,
560                i2o_mapping: right_to_output,
561                i2o_mapping_indexed: r2o_indexed,
562                input2inequality_index: r2inequality_index,
563                non_null_fields: r_non_null_fields,
564                state_clean_columns: r_inequal_state_clean_columns,
565                need_degree_table: need_degree_table_r,
566                _marker: PhantomData,
567            },
568            cond,
569            inequality_pairs,
570            inequality_watermarks,
571            watermark_indices_in_jk,
572            append_only_optimize,
573            metrics,
574            chunk_size,
575            cnt_rows_received: 0,
576            watermark_buffers,
577            high_join_amplification_threshold,
578            entry_state_max_rows,
579            join_cache_evict_interval_rows,
580        }
581    }
582
583    #[try_stream(ok = Message, error = StreamExecutorError)]
584    async fn into_stream(mut self) {
585        let input_l = self.input_l.take().unwrap();
586        let input_r = self.input_r.take().unwrap();
587        let aligned_stream = barrier_align(
588            input_l.execute(),
589            input_r.execute(),
590            self.ctx.id,
591            self.ctx.fragment_id,
592            self.metrics.clone(),
593            "Join",
594        );
595        pin_mut!(aligned_stream);
596
597        let actor_id = self.ctx.id;
598
599        let barrier = expect_first_barrier_from_aligned_stream(&mut aligned_stream).await?;
600        let first_epoch = barrier.epoch;
601        // The first barrier message should be propagated.
602        yield Message::Barrier(barrier);
603        self.side_l.init(first_epoch).await?;
604        self.side_r.init(first_epoch).await?;
605
606        let actor_id_str = self.ctx.id.to_string();
607        let fragment_id_str = self.ctx.fragment_id.to_string();
608
609        // initialized some metrics
610        let join_actor_input_waiting_duration_ns = self
611            .metrics
612            .join_actor_input_waiting_duration_ns
613            .with_guarded_label_values(&[&actor_id_str, &fragment_id_str]);
614        let left_join_match_duration_ns = self
615            .metrics
616            .join_match_duration_ns
617            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
618        let right_join_match_duration_ns = self
619            .metrics
620            .join_match_duration_ns
621            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
622
623        let barrier_join_match_duration_ns = self
624            .metrics
625            .join_match_duration_ns
626            .with_guarded_label_values(&[
627                actor_id_str.as_str(),
628                fragment_id_str.as_str(),
629                "barrier",
630            ]);
631
632        let left_join_cached_entry_count = self
633            .metrics
634            .join_cached_entry_count
635            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
636
637        let right_join_cached_entry_count = self
638            .metrics
639            .join_cached_entry_count
640            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
641
642        // Bind at executor scope: a per-chunk guard would be dropped between chunks and reset the series.
643        let left_table_id_str = self.side_l.ht.table_id().to_string();
644        let right_table_id_str = self.side_r.ht.table_id().to_string();
645        let left_join_matched_join_keys = self
646            .metrics
647            .join_matched_join_keys
648            .with_guarded_label_values(&[
649                actor_id_str.as_str(),
650                fragment_id_str.as_str(),
651                left_table_id_str.as_str(),
652            ]);
653        let right_join_matched_join_keys = self
654            .metrics
655            .join_matched_join_keys
656            .with_guarded_label_values(&[
657                actor_id_str.as_str(),
658                fragment_id_str.as_str(),
659                right_table_id_str.as_str(),
660            ]);
661
662        let mut start_time = Instant::now();
663
664        while let Some(msg) = aligned_stream
665            .next()
666            .instrument_await("hash_join_barrier_align")
667            .await
668        {
669            join_actor_input_waiting_duration_ns.inc_by(start_time.elapsed().as_nanos() as u64);
670            match msg? {
671                AlignedMessage::WatermarkLeft(watermark) => {
672                    for watermark_to_emit in self.handle_watermark(SideType::Left, watermark)? {
673                        yield Message::Watermark(watermark_to_emit);
674                    }
675                }
676                AlignedMessage::WatermarkRight(watermark) => {
677                    for watermark_to_emit in self.handle_watermark(SideType::Right, watermark)? {
678                        yield Message::Watermark(watermark_to_emit);
679                    }
680                }
681                AlignedMessage::Left(chunk) => {
682                    let mut left_time = Duration::from_nanos(0);
683                    let mut left_start_time = Instant::now();
684                    #[for_await]
685                    for chunk in Self::eq_join_left(EqJoinArgs {
686                        ctx: &self.ctx,
687                        side_l: &mut self.side_l,
688                        side_r: &mut self.side_r,
689                        actual_output_data_types: &self.actual_output_data_types,
690                        cond: &mut self.cond,
691                        inequality_watermarks: &self.inequality_watermarks,
692                        chunk,
693                        append_only_optimize: self.append_only_optimize,
694                        chunk_size: self.chunk_size,
695                        cnt_rows_received: &mut self.cnt_rows_received,
696                        high_join_amplification_threshold: self.high_join_amplification_threshold,
697                        entry_state_max_rows: self.entry_state_max_rows,
698                        join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
699                        join_matched_join_keys: &left_join_matched_join_keys,
700                    }) {
701                        left_time += left_start_time.elapsed();
702                        yield Message::Chunk(chunk?);
703                        left_start_time = Instant::now();
704                    }
705                    left_time += left_start_time.elapsed();
706                    left_join_match_duration_ns.inc_by(left_time.as_nanos() as u64);
707                    self.try_flush_data().await?;
708                }
709                AlignedMessage::Right(chunk) => {
710                    let mut right_time = Duration::from_nanos(0);
711                    let mut right_start_time = Instant::now();
712                    #[for_await]
713                    for chunk in Self::eq_join_right(EqJoinArgs {
714                        ctx: &self.ctx,
715                        side_l: &mut self.side_l,
716                        side_r: &mut self.side_r,
717                        actual_output_data_types: &self.actual_output_data_types,
718                        cond: &mut self.cond,
719                        inequality_watermarks: &self.inequality_watermarks,
720                        chunk,
721                        append_only_optimize: self.append_only_optimize,
722                        chunk_size: self.chunk_size,
723                        cnt_rows_received: &mut self.cnt_rows_received,
724                        high_join_amplification_threshold: self.high_join_amplification_threshold,
725                        entry_state_max_rows: self.entry_state_max_rows,
726                        join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
727                        join_matched_join_keys: &right_join_matched_join_keys,
728                    }) {
729                        right_time += right_start_time.elapsed();
730                        yield Message::Chunk(chunk?);
731                        right_start_time = Instant::now();
732                    }
733                    right_time += right_start_time.elapsed();
734                    right_join_match_duration_ns.inc_by(right_time.as_nanos() as u64);
735                    self.try_flush_data().await?;
736                }
737                AlignedMessage::Barrier(barrier) => {
738                    let barrier_start_time = Instant::now();
739                    let (left_post_commit, right_post_commit) =
740                        self.flush_data(barrier.epoch).await?;
741
742                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
743
744                    // We don't include the time post yielding barrier because the vnode update
745                    // is a one-off and rare operation.
746                    barrier_join_match_duration_ns
747                        .inc_by(barrier_start_time.elapsed().as_nanos() as u64);
748                    yield Message::Barrier(barrier);
749
750                    // Update the vnode bitmap for state tables of both sides if asked.
751                    right_post_commit
752                        .post_yield_barrier(update_vnode_bitmap.clone())
753                        .await?;
754                    if left_post_commit
755                        .post_yield_barrier(update_vnode_bitmap)
756                        .await?
757                        .unwrap_or(false)
758                    {
759                        self.watermark_buffers
760                            .values_mut()
761                            .for_each(|buffers| buffers.clear());
762                        self.inequality_watermarks.fill(None);
763                    }
764
765                    // Report metrics of cached join rows/entries
766                    for (join_cached_entry_count, ht) in [
767                        (&left_join_cached_entry_count, &self.side_l.ht),
768                        (&right_join_cached_entry_count, &self.side_r.ht),
769                    ] {
770                        join_cached_entry_count.set(ht.entry_count() as i64);
771                    }
772                }
773            }
774            start_time = Instant::now();
775        }
776    }
777
778    async fn flush_data(
779        &mut self,
780        epoch: EpochPair,
781    ) -> StreamExecutorResult<(
782        JoinHashMapPostCommit<'_, K, S, E>,
783        JoinHashMapPostCommit<'_, K, S, E>,
784    )> {
785        // All changes to the state has been buffered in the mem-table of the state table. Just
786        // `commit` them here.
787        let left = self.side_l.ht.flush(epoch).await?;
788        let right = self.side_r.ht.flush(epoch).await?;
789        Ok((left, right))
790    }
791
792    async fn try_flush_data(&mut self) -> StreamExecutorResult<()> {
793        // All changes to the state has been buffered in the mem-table of the state table. Just
794        // `commit` them here.
795        self.side_l.ht.try_flush().await?;
796        self.side_r.ht.try_flush().await?;
797        Ok(())
798    }
799
800    // We need to manually evict the cache.
801    fn evict_cache(
802        side_update: &mut JoinSide<K, S, E>,
803        side_match: &mut JoinSide<K, S, E>,
804        cnt_rows_received: &mut u32,
805        join_cache_evict_interval_rows: u32,
806    ) {
807        *cnt_rows_received += 1;
808        if *cnt_rows_received >= join_cache_evict_interval_rows {
809            side_update.ht.evict();
810            side_match.ht.evict();
811            *cnt_rows_received = 0;
812        }
813    }
814
815    fn handle_watermark(
816        &mut self,
817        side: SideTypePrimitive,
818        watermark: Watermark,
819    ) -> StreamExecutorResult<Vec<Watermark>> {
820        let (side_update, side_match) = if side == SideType::Left {
821            (&mut self.side_l, &mut self.side_r)
822        } else {
823            (&mut self.side_r, &mut self.side_l)
824        };
825
826        // Process join-key watermarks
827        let wm_in_jk = side_update
828            .join_key_indices
829            .iter()
830            .positions(|idx| *idx == watermark.col_idx);
831        let mut watermarks_to_emit = vec![];
832        for idx in wm_in_jk {
833            let buffers = self
834                .watermark_buffers
835                .entry(idx)
836                .or_insert_with(|| BufferedWatermarks::with_ids([SideType::Left, SideType::Right]));
837            if let Some(selected_watermark) = buffers.handle_watermark(side, watermark.clone()) {
838                if self
839                    .watermark_indices_in_jk
840                    .iter()
841                    .any(|(jk_pos, do_clean)| *jk_pos == idx && *do_clean)
842                {
843                    side_match
844                        .ht
845                        .update_watermark(selected_watermark.val.clone());
846                    side_update
847                        .ht
848                        .update_watermark(selected_watermark.val.clone());
849                }
850
851                let empty_indices = vec![];
852                let output_indices = side_update
853                    .i2o_mapping_indexed
854                    .get_vec(&side_update.join_key_indices[idx])
855                    .unwrap_or(&empty_indices)
856                    .iter()
857                    .chain(
858                        side_match
859                            .i2o_mapping_indexed
860                            .get_vec(&side_match.join_key_indices[idx])
861                            .unwrap_or(&empty_indices),
862                    );
863                for output_idx in output_indices {
864                    watermarks_to_emit.push(selected_watermark.clone().with_idx(*output_idx));
865                }
866            };
867        }
868
869        // Process inequality watermarks
870        // We can only yield the LARGER side's watermark downstream.
871        // For `left >= right`: left is larger, emit for left output columns
872        // For `left <= right`: right is larger, emit for right output columns
873        let mut update_left_watermark = None;
874        let mut update_right_watermark = None;
875        if let Some(watermark_indices) = side_update.input2inequality_index.get(watermark.col_idx) {
876            for (inequality_index, _) in watermark_indices {
877                let buffers = self
878                    .watermark_buffers
879                    .entry(side_update.join_key_indices.len() + inequality_index)
880                    .or_insert_with(|| {
881                        BufferedWatermarks::with_ids([SideType::Left, SideType::Right])
882                    });
883                if let Some(selected_watermark) = buffers.handle_watermark(side, watermark.clone())
884                {
885                    let (output_indices, pair_info) = &self.inequality_pairs[*inequality_index];
886                    let left_is_larger = pair_info.left_side_is_larger();
887
888                    // Emit watermark for the larger side's output columns only
889                    for output_idx in output_indices {
890                        watermarks_to_emit.push(selected_watermark.clone().with_idx(*output_idx));
891                    }
892                    // Store watermark for state cleaning (via useful_state_clean_columns)
893                    self.inequality_watermarks[*inequality_index] =
894                        Some(selected_watermark.clone());
895
896                    // Mark which side needs state cleaning (only if the clean flag is set)
897                    if left_is_larger && pair_info.clean_left_state {
898                        update_left_watermark = Some(selected_watermark.val.clone());
899                    } else if !left_is_larger && pair_info.clean_right_state {
900                        update_right_watermark = Some(selected_watermark.val.clone());
901                    }
902                }
903            }
904            // Do state cleaning on the larger side.
905            // Now that degree tables include the inequality column, we can clean both
906            // state and degree tables using `update_watermark`.
907            if let Some(val) = update_left_watermark {
908                self.side_l.ht.update_watermark(val);
909            }
910            if let Some(val) = update_right_watermark {
911                self.side_r.ht.update_watermark(val);
912            }
913        }
914        Ok(watermarks_to_emit)
915    }
916
917    fn row_concat(
918        row_update: impl Row,
919        update_start_pos: usize,
920        row_matched: impl Row,
921        matched_start_pos: usize,
922    ) -> OwnedRow {
923        let mut new_row = vec![None; row_update.len() + row_matched.len()];
924
925        for (i, datum_ref) in row_update.iter().enumerate() {
926            new_row[i + update_start_pos] = datum_ref.to_owned_datum();
927        }
928        for (i, datum_ref) in row_matched.iter().enumerate() {
929            new_row[i + matched_start_pos] = datum_ref.to_owned_datum();
930        }
931        OwnedRow::new(new_row)
932    }
933
934    /// Used to forward `eq_join_oneside` to show join side in stack.
935    fn eq_join_left(
936        args: EqJoinArgs<'_, K, S, E>,
937    ) -> impl Stream<Item = Result<StreamChunk, StreamExecutorError>> + '_ {
938        Self::eq_join_oneside::<{ SideType::Left }>(args)
939    }
940
941    /// Used to forward `eq_join_oneside` to show join side in stack.
942    fn eq_join_right(
943        args: EqJoinArgs<'_, K, S, E>,
944    ) -> impl Stream<Item = Result<StreamChunk, StreamExecutorError>> + '_ {
945        Self::eq_join_oneside::<{ SideType::Right }>(args)
946    }
947
948    #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
949    async fn eq_join_oneside<const SIDE: SideTypePrimitive>(args: EqJoinArgs<'_, K, S, E>) {
950        let EqJoinArgs {
951            ctx,
952            side_l,
953            side_r,
954            actual_output_data_types,
955            cond,
956            inequality_watermarks,
957            chunk,
958            append_only_optimize,
959            chunk_size,
960            cnt_rows_received,
961            high_join_amplification_threshold,
962            entry_state_max_rows,
963            join_cache_evict_interval_rows,
964            join_matched_join_keys,
965            ..
966        } = args;
967
968        let (side_update, side_match) = if SIDE == SideType::Left {
969            (side_l, side_r)
970        } else {
971            (side_r, side_l)
972        };
973
974        let useful_state_clean_columns = side_match
975            .state_clean_columns
976            .iter()
977            .filter_map(|(column_idx, inequality_index)| {
978                inequality_watermarks[*inequality_index]
979                    .as_ref()
980                    .map(|watermark| (*column_idx, watermark))
981            })
982            .collect_vec();
983
984        let mut hashjoin_chunk_builder =
985            JoinChunkBuilder::<T, SIDE>::new(JoinStreamChunkBuilder::new(
986                chunk_size,
987                actual_output_data_types.to_vec(),
988                side_update.i2o_mapping.clone(),
989                side_match.i2o_mapping.clone(),
990            ));
991
992        let keys = K::build_many(&side_update.join_key_indices, chunk.data_chunk());
993        for (r, key) in chunk.rows_with_holes().zip_eq_debug(keys.iter()) {
994            let Some((op, row)) = r else {
995                continue;
996            };
997            Self::evict_cache(
998                side_update,
999                side_match,
1000                cnt_rows_received,
1001                join_cache_evict_interval_rows,
1002            );
1003
1004            let cache_lookup_result = {
1005                let probe_non_null_requirement_satisfied = side_update
1006                    .non_null_fields
1007                    .iter()
1008                    .all(|column_idx| unsafe { row.datum_at_unchecked(*column_idx).is_some() });
1009                let build_non_null_requirement_satisfied =
1010                    key.null_bitmap().is_subset(side_match.ht.null_matched());
1011                if probe_non_null_requirement_satisfied && build_non_null_requirement_satisfied {
1012                    side_match.ht.take_state_opt(key)
1013                } else {
1014                    CacheResult::NeverMatch
1015                }
1016            };
1017            let mut total_matches = 0;
1018
1019            macro_rules! match_rows {
1020                ($op:ident) => {
1021                    Self::handle_match_rows::<SIDE, { JoinOp::$op }>(
1022                        cache_lookup_result,
1023                        row,
1024                        key,
1025                        &mut hashjoin_chunk_builder,
1026                        side_match,
1027                        side_update,
1028                        &useful_state_clean_columns,
1029                        cond,
1030                        &mut total_matches,
1031                        append_only_optimize,
1032                        entry_state_max_rows,
1033                    )
1034                };
1035            }
1036
1037            match op {
1038                Op::Insert | Op::UpdateInsert =>
1039                {
1040                    #[for_await]
1041                    for chunk in match_rows!(Insert) {
1042                        let chunk = chunk?;
1043                        yield chunk;
1044                    }
1045                }
1046                Op::Delete | Op::UpdateDelete =>
1047                {
1048                    #[for_await]
1049                    for chunk in match_rows!(Delete) {
1050                        let chunk = chunk?;
1051                        yield chunk;
1052                    }
1053                }
1054            };
1055
1056            join_matched_join_keys.observe(total_matches as _);
1057            if total_matches > high_join_amplification_threshold {
1058                let join_key_data_types = side_update.ht.join_key_data_types();
1059                let key = key.deserialize(join_key_data_types)?;
1060                tracing::warn!(target: "high_join_amplification",
1061                    matched_rows_len = total_matches,
1062                    update_table_id = %side_update.ht.table_id(),
1063                    match_table_id = %side_match.ht.table_id(),
1064                    join_key = ?key,
1065                    actor_id = %ctx.id,
1066                    fragment_id = %ctx.fragment_id,
1067                    "large rows matched for join key"
1068                );
1069            }
1070        }
1071        // NOTE(kwannoel): We don't track metrics for this last chunk.
1072        if let Some(chunk) = hashjoin_chunk_builder.take() {
1073            yield chunk;
1074        }
1075    }
1076
1077    /// For the probe-side row, we need to check if it has values in cache, if not, we need to
1078    /// fetch the matched rows from the state table.
1079    ///
1080    /// Every matched build-side row being processed needs to go through the following phases:
1081    /// 1. Handle join condition evaluation.
1082    /// 2. Always do cache refill, if the state count is good.
1083    /// 3. Handle state cleaning.
1084    /// 4. Handle degree table update.
1085    #[expect(clippy::too_many_arguments)]
1086    #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
1087    async fn handle_match_rows<
1088        'a,
1089        const SIDE: SideTypePrimitive,
1090        const JOIN_OP: JoinOpPrimitive,
1091    >(
1092        cached_lookup_result: CacheResult<E>,
1093        row: RowRef<'a>,
1094        key: &'a K,
1095        hashjoin_chunk_builder: &'a mut JoinChunkBuilder<T, SIDE>,
1096        side_match: &'a mut JoinSide<K, S, E>,
1097        side_update: &'a mut JoinSide<K, S, E>,
1098        useful_state_clean_columns: &'a [(usize, &'a Watermark)],
1099        cond: &'a mut Option<NonStrictExpression>,
1100        total_matches: &'a mut usize,
1101        append_only_optimize: bool,
1102        entry_state_max_rows: usize,
1103    ) {
1104        let cache_hit = matches!(cached_lookup_result, CacheResult::Hit(_));
1105        let mut entry_state: JoinEntryState<E> = JoinEntryState::default();
1106        let mut entry_state_count = 0;
1107
1108        let mut degree = 0;
1109        let mut append_only_matched_row = None;
1110        let mut matched_rows_to_clean = vec![];
1111
1112        macro_rules! match_row {
1113            (
1114                $match_order_key_indices:expr,
1115                $degree_table:expr,
1116                $matched_row:expr,
1117                $matched_row_ref:expr,
1118                $from_cache:literal,
1119                $map_output:expr,
1120            ) => {
1121                Self::handle_match_row::<_, _, SIDE, { JOIN_OP }, { $from_cache }>(
1122                    row,
1123                    $matched_row,
1124                    $matched_row_ref,
1125                    hashjoin_chunk_builder,
1126                    $match_order_key_indices,
1127                    $degree_table,
1128                    side_update.start_pos,
1129                    side_match.start_pos,
1130                    cond,
1131                    &mut degree,
1132                    useful_state_clean_columns,
1133                    total_matches,
1134                    append_only_optimize,
1135                    &mut append_only_matched_row,
1136                    &mut matched_rows_to_clean,
1137                    $map_output,
1138                )
1139            };
1140        }
1141
1142        let entry_state = match cached_lookup_result {
1143            CacheResult::NeverMatch => {
1144                let op = match JOIN_OP {
1145                    JoinOp::Insert => Op::Insert,
1146                    JoinOp::Delete => Op::Delete,
1147                };
1148                if let Some(chunk) = hashjoin_chunk_builder.forward_if_not_matched(op, row) {
1149                    yield chunk;
1150                }
1151                return Ok(());
1152            }
1153            CacheResult::Hit(mut cached_rows) => {
1154                let (match_order_key_indices, match_degree_state) =
1155                    side_match.ht.get_degree_state_mut_ref();
1156                // Handle cached rows which match the probe-side row.
1157                for (matched_row_ref, matched_row) in
1158                    cached_rows.values_mut(&side_match.all_data_types)
1159                {
1160                    let matched_row = matched_row?;
1161                    if let Some(chunk) = match_row!(
1162                        match_order_key_indices,
1163                        match_degree_state,
1164                        matched_row,
1165                        Some(matched_row_ref),
1166                        true,
1167                        Either::Left,
1168                    )
1169                    .await
1170                    {
1171                        yield chunk;
1172                    }
1173                }
1174
1175                cached_rows
1176            }
1177            CacheResult::Miss => {
1178                // Handle rows which are not in cache.
1179                let (matched_rows, match_order_key_indices, degree_table) = side_match
1180                    .ht
1181                    .fetch_matched_rows_and_get_degree_table_ref(key)
1182                    .await?;
1183
1184                #[for_await]
1185                for matched_row in matched_rows {
1186                    let (encoded_pk, matched_row) = matched_row?;
1187
1188                    let mut matched_row_ref = None;
1189
1190                    // cache refill
1191                    if entry_state_count <= entry_state_max_rows {
1192                        let row_ref = entry_state
1193                            .insert(encoded_pk, E::encode(&matched_row))
1194                            .with_context(|| format!("row: {}", row.display(),))?;
1195                        matched_row_ref = Some(row_ref);
1196                        entry_state_count += 1;
1197                    }
1198                    if let Some(chunk) = match_row!(
1199                        match_order_key_indices,
1200                        degree_table,
1201                        matched_row,
1202                        matched_row_ref,
1203                        false,
1204                        Either::Right,
1205                    )
1206                    .await
1207                    {
1208                        yield chunk;
1209                    }
1210                }
1211                Box::new(entry_state)
1212            }
1213        };
1214
1215        // forward rows depending on join types
1216        let op = match JOIN_OP {
1217            JoinOp::Insert => Op::Insert,
1218            JoinOp::Delete => Op::Delete,
1219        };
1220        if degree == 0 {
1221            if let Some(chunk) = hashjoin_chunk_builder.forward_if_not_matched(op, row) {
1222                yield chunk;
1223            }
1224        } else if let Some(chunk) = hashjoin_chunk_builder.forward_exactly_once_if_matched(op, row)
1225        {
1226            yield chunk;
1227        }
1228
1229        // cache refill
1230        if cache_hit || entry_state_count <= entry_state_max_rows {
1231            side_match.ht.update_state(key, entry_state);
1232        }
1233
1234        // watermark state cleaning
1235        for matched_row in matched_rows_to_clean {
1236            // The committed table watermark is responsible for reclaiming rows in the state table.
1237            side_match.ht.delete_row_in_mem(key, &matched_row.row)?;
1238        }
1239
1240        // apply append_only optimization to clean matched_rows which have been persisted
1241        if append_only_optimize && let Some(row) = append_only_matched_row {
1242            assert_matches!(JOIN_OP, JoinOp::Insert);
1243            side_match.ht.delete_handle_degree(key, row)?;
1244            return Ok(());
1245        }
1246
1247        // no append_only optimization, update state table(s).
1248        match JOIN_OP {
1249            JoinOp::Insert => {
1250                side_update
1251                    .ht
1252                    .insert_handle_degree(key, JoinRow::new(row, degree))?;
1253            }
1254            JoinOp::Delete => {
1255                side_update
1256                    .ht
1257                    .delete_handle_degree(key, JoinRow::new(row, degree))?;
1258            }
1259        }
1260    }
1261
1262    #[expect(clippy::too_many_arguments)]
1263    #[inline]
1264    async fn handle_match_row<
1265        'a,
1266        R: Row,  // input row type
1267        RO: Row, // output row type
1268        const SIDE: SideTypePrimitive,
1269        const JOIN_OP: JoinOpPrimitive,
1270        const MATCHED_ROWS_FROM_CACHE: bool,
1271    >(
1272        update_row: RowRef<'a>,
1273        mut matched_row: JoinRow<R>,
1274        mut matched_row_cache_ref: Option<&mut E::EncodedRow>,
1275        hashjoin_chunk_builder: &'a mut JoinChunkBuilder<T, SIDE>,
1276        match_order_key_indices: &[usize],
1277        match_degree_table: &mut Option<TableInner<S>>,
1278        side_update_start_pos: usize,
1279        side_match_start_pos: usize,
1280        cond: &Option<NonStrictExpression>,
1281        update_row_degree: &mut u64,
1282        useful_state_clean_columns: &[(usize, &'a Watermark)],
1283        total_matches: &mut usize,
1284        append_only_optimize: bool,
1285        append_only_matched_row: &mut Option<JoinRow<RO>>,
1286        matched_rows_to_clean: &mut Vec<JoinRow<RO>>,
1287        map_output: impl Fn(R) -> RO,
1288    ) -> Option<StreamChunk> {
1289        let mut need_state_clean = false;
1290        let mut chunk_opt = None;
1291        // TODO(kwannoel): Instead of evaluating this every loop,
1292        // we can call this only if there's a non-equi expression.
1293        // check join cond
1294        let join_condition_satisfied = Self::check_join_condition(
1295            update_row,
1296            side_update_start_pos,
1297            &matched_row.row,
1298            side_match_start_pos,
1299            cond,
1300        )
1301        .await;
1302
1303        if join_condition_satisfied {
1304            *total_matches += 1;
1305            // update degree
1306            *update_row_degree += 1;
1307            // send matched row downstream
1308
1309            // Inserts must happen before degrees are updated,
1310            // FIXME(kwannoel): We should let deletes and inserts happen BEFORE degree updates.
1311            if matches!(JOIN_OP, JoinOp::Insert)
1312                && !forward_exactly_once(T, SIDE)
1313                && let Some(chunk) =
1314                    hashjoin_chunk_builder.with_match::<JOIN_OP>(&update_row, &matched_row)
1315            {
1316                chunk_opt = Some(chunk);
1317            }
1318            // TODO(kwannoel): We can actually statically decide this, using join side + join type.
1319            if let Some(degree_table) = match_degree_table {
1320                update_degree::<S, { JOIN_OP }>(
1321                    match_order_key_indices,
1322                    degree_table,
1323                    &mut matched_row,
1324                );
1325                if MATCHED_ROWS_FROM_CACHE || matched_row_cache_ref.is_some() {
1326                    // update matched row in cache
1327                    match JOIN_OP {
1328                        JoinOp::Insert => matched_row_cache_ref.as_mut().unwrap().increase_degree(),
1329                        JoinOp::Delete => matched_row_cache_ref.as_mut().unwrap().decrease_degree(),
1330                    }
1331                }
1332            }
1333
1334            // Deletes must happen after degree table is updated.
1335            // FIXME(kwannoel): We should let deletes and inserts happen BEFORE degree updates.
1336            if matches!(JOIN_OP, JoinOp::Delete)
1337                && !forward_exactly_once(T, SIDE)
1338                && let Some(chunk) =
1339                    hashjoin_chunk_builder.with_match::<JOIN_OP>(&update_row, &matched_row)
1340            {
1341                chunk_opt = Some(chunk);
1342            }
1343        } else {
1344            // check if need state cleaning
1345            for (column_idx, watermark) in useful_state_clean_columns {
1346                if matched_row.row.datum_at(*column_idx).is_some_and(|scalar| {
1347                    scalar
1348                        .default_cmp(&watermark.val.as_scalar_ref_impl())
1349                        .is_lt()
1350                }) {
1351                    need_state_clean = true;
1352                    break;
1353                }
1354            }
1355        }
1356        // If the stream is append-only and the join key covers pk in both side,
1357        // then we can remove matched rows since pk is unique and will not be
1358        // inserted again
1359        if append_only_optimize {
1360            assert_matches!(JOIN_OP, JoinOp::Insert);
1361            // Since join key contains pk and pk is unique, there should be only
1362            // one row if matched.
1363            assert!(append_only_matched_row.is_none());
1364            *append_only_matched_row = Some(matched_row.map(map_output));
1365        } else if need_state_clean {
1366            debug_assert!(
1367                !append_only_optimize,
1368                "`append_only_optimize` and `need_state_clean` must not both be true"
1369            );
1370            matched_rows_to_clean.push(matched_row.map(map_output));
1371        }
1372
1373        chunk_opt
1374    }
1375
1376    // TODO(yuhao-su): We should find a better way to eval the expression
1377    // without concat two rows.
1378    // if there are non-equi expressions
1379    // NOTE(kwannoel): We can probably let `eval` use `impl Row` instead of `OwnedRow`.
1380    #[inline]
1381    async fn check_join_condition(
1382        row: impl Row,
1383        side_update_start_pos: usize,
1384        matched_row: impl Row,
1385        side_match_start_pos: usize,
1386        join_condition: &Option<NonStrictExpression>,
1387    ) -> bool {
1388        if let Some(join_condition) = join_condition {
1389            let new_row = Self::row_concat(
1390                row,
1391                side_update_start_pos,
1392                matched_row,
1393                side_match_start_pos,
1394            );
1395            join_condition
1396                .eval_row_infallible(&new_row)
1397                .await
1398                .map(|s| *s.as_bool())
1399                .unwrap_or(false)
1400        } else {
1401            true
1402        }
1403    }
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408    use std::sync::atomic::AtomicU64;
1409
1410    use pretty_assertions::assert_eq;
1411    use risingwave_common::array::*;
1412    use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, TableId};
1413    use risingwave_common::config::StreamingConfig;
1414    use risingwave_common::hash::{Key64, Key128};
1415    use risingwave_common::util::epoch::test_epoch;
1416    use risingwave_common::util::sort_util::OrderType;
1417    use risingwave_storage::memory::MemoryStateStore;
1418
1419    use super::*;
1420    use crate::common::table::test_utils::gen_pbtable;
1421    use crate::executor::MemoryEncoding;
1422    use crate::executor::test_utils::expr::build_from_pretty;
1423    use crate::executor::test_utils::{MessageSender, MockSource, StreamExecutorTestExt};
1424
1425    async fn create_in_memory_state_table(
1426        mem_state: MemoryStateStore,
1427        data_types: &[DataType],
1428        order_types: &[OrderType],
1429        pk_indices: &[usize],
1430        table_id: u32,
1431    ) -> (StateTable<MemoryStateStore>, StateTable<MemoryStateStore>) {
1432        create_in_memory_state_table_with_inequality(
1433            mem_state,
1434            data_types,
1435            order_types,
1436            pk_indices,
1437            table_id,
1438            None,
1439        )
1440        .await
1441    }
1442
1443    async fn create_in_memory_state_table_with_inequality(
1444        mem_state: MemoryStateStore,
1445        data_types: &[DataType],
1446        order_types: &[OrderType],
1447        pk_indices: &[usize],
1448        table_id: u32,
1449        degree_inequality_type: Option<DataType>,
1450    ) -> (StateTable<MemoryStateStore>, StateTable<MemoryStateStore>) {
1451        create_in_memory_state_table_with_watermark(
1452            mem_state,
1453            data_types,
1454            order_types,
1455            pk_indices,
1456            table_id,
1457            degree_inequality_type,
1458            vec![],
1459            vec![],
1460        )
1461        .await
1462    }
1463
1464    #[expect(clippy::too_many_arguments)]
1465    async fn create_in_memory_state_table_with_watermark(
1466        mem_state: MemoryStateStore,
1467        data_types: &[DataType],
1468        order_types: &[OrderType],
1469        pk_indices: &[usize],
1470        table_id: u32,
1471        degree_inequality_type: Option<DataType>,
1472        state_clean_watermark_indices: Vec<usize>,
1473        degree_clean_watermark_indices: Vec<usize>,
1474    ) -> (StateTable<MemoryStateStore>, StateTable<MemoryStateStore>) {
1475        let column_descs = data_types
1476            .iter()
1477            .enumerate()
1478            .map(|(id, data_type)| ColumnDesc::unnamed(ColumnId::new(id as i32), data_type.clone()))
1479            .collect_vec();
1480        let mut state_table_catalog = gen_pbtable(
1481            TableId::new(table_id),
1482            column_descs,
1483            order_types.to_vec(),
1484            pk_indices.to_vec(),
1485            0,
1486        );
1487        state_table_catalog.clean_watermark_indices = state_clean_watermark_indices
1488            .into_iter()
1489            .map(|idx| idx as u32)
1490            .collect();
1491        let state_table =
1492            StateTable::from_table_catalog(&state_table_catalog, mem_state.clone(), None).await;
1493
1494        // Create degree table with schema: [pk..., _degree, inequality?]
1495        let mut degree_table_column_descs = vec![];
1496        pk_indices.iter().enumerate().for_each(|(pk_id, idx)| {
1497            degree_table_column_descs.push(ColumnDesc::unnamed(
1498                ColumnId::new(pk_id as i32),
1499                data_types[*idx].clone(),
1500            ))
1501        });
1502        // Add _degree column
1503        degree_table_column_descs.push(ColumnDesc::unnamed(
1504            ColumnId::new(pk_indices.len() as i32),
1505            DataType::Int64,
1506        ));
1507        // Add inequality column if present
1508        if let Some(ineq_type) = degree_inequality_type {
1509            degree_table_column_descs.push(ColumnDesc::unnamed(
1510                ColumnId::new((pk_indices.len() + 1) as i32),
1511                ineq_type,
1512            ));
1513        }
1514        let mut degree_table_catalog = gen_pbtable(
1515            TableId::new(table_id + 1),
1516            degree_table_column_descs,
1517            order_types.to_vec(),
1518            pk_indices.to_vec(),
1519            0,
1520        );
1521        degree_table_catalog.clean_watermark_indices = degree_clean_watermark_indices
1522            .into_iter()
1523            .map(|idx| idx as u32)
1524            .collect();
1525        let degree_state_table =
1526            StateTable::from_table_catalog(&degree_table_catalog, mem_state, None).await;
1527        (state_table, degree_state_table)
1528    }
1529
1530    fn create_cond(condition_text: Option<String>) -> NonStrictExpression {
1531        build_from_pretty(
1532            condition_text
1533                .as_deref()
1534                .unwrap_or("(less_than:boolean $1:int8 $3:int8)"),
1535        )
1536    }
1537
1538    async fn create_executor<const T: JoinTypePrimitive>(
1539        with_condition: bool,
1540        null_safe: bool,
1541        condition_text: Option<String>,
1542        inequality_pairs: Vec<InequalityPairInfo>,
1543    ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1544        let schema = Schema {
1545            fields: vec![
1546                Field::unnamed(DataType::Int64), // join key
1547                Field::unnamed(DataType::Int64),
1548            ],
1549        };
1550        let (tx_l, source_l) = MockSource::channel();
1551        let source_l = source_l.into_executor(schema.clone(), vec![1]);
1552        let (tx_r, source_r) = MockSource::channel();
1553        let source_r = source_r.into_executor(schema, vec![1]);
1554        let params_l = JoinParams::new(vec![0], vec![1]);
1555        let params_r = JoinParams::new(vec![0], vec![1]);
1556        let cond = with_condition.then(|| create_cond(condition_text));
1557
1558        let mem_state = MemoryStateStore::new();
1559
1560        // Determine inequality column types for degree tables based on inequality_pairs
1561        let l_degree_ineq_type = inequality_pairs
1562            .iter()
1563            .find(|pair| pair.clean_left_state)
1564            .map(|_| DataType::Int64); // Column 1 is Int64
1565        let r_degree_ineq_type = inequality_pairs
1566            .iter()
1567            .find(|pair| pair.clean_right_state)
1568            .map(|_| DataType::Int64); // Column 1 is Int64
1569        let l_clean_watermark_indices = inequality_pairs
1570            .iter()
1571            .find(|pair| pair.clean_left_state)
1572            .map(|pair| vec![pair.left_idx])
1573            .unwrap_or_default();
1574        let r_clean_watermark_indices = inequality_pairs
1575            .iter()
1576            .find(|pair| pair.clean_right_state)
1577            .map(|pair| vec![pair.right_idx])
1578            .unwrap_or_default();
1579        let degree_inequality_column_idx = 3;
1580        let l_degree_clean_watermark_indices = l_degree_ineq_type
1581            .as_ref()
1582            .map(|_| vec![degree_inequality_column_idx])
1583            .unwrap_or_default();
1584        let r_degree_clean_watermark_indices = r_degree_ineq_type
1585            .as_ref()
1586            .map(|_| vec![degree_inequality_column_idx])
1587            .unwrap_or_default();
1588
1589        let (state_l, degree_state_l) = create_in_memory_state_table_with_watermark(
1590            mem_state.clone(),
1591            &[DataType::Int64, DataType::Int64],
1592            &[OrderType::ascending(), OrderType::ascending()],
1593            &[0, 1],
1594            0,
1595            l_degree_ineq_type,
1596            l_clean_watermark_indices,
1597            l_degree_clean_watermark_indices,
1598        )
1599        .await;
1600
1601        let (state_r, degree_state_r) = create_in_memory_state_table_with_watermark(
1602            mem_state,
1603            &[DataType::Int64, DataType::Int64],
1604            &[OrderType::ascending(), OrderType::ascending()],
1605            &[0, 1],
1606            2,
1607            r_degree_ineq_type,
1608            r_clean_watermark_indices,
1609            r_degree_clean_watermark_indices,
1610        )
1611        .await;
1612
1613        let schema = match T {
1614            JoinType::LeftSemi | JoinType::LeftAnti => source_l.schema().clone(),
1615            JoinType::RightSemi | JoinType::RightAnti => source_r.schema().clone(),
1616            _ => [source_l.schema().fields(), source_r.schema().fields()]
1617                .concat()
1618                .into_iter()
1619                .collect(),
1620        };
1621        let schema_len = schema.len();
1622        let info = ExecutorInfo::for_test(schema, vec![1], "HashJoinExecutor".to_owned(), 0);
1623
1624        let executor = HashJoinExecutor::<Key64, MemoryStateStore, T, MemoryEncoding>::new(
1625            ActorContext::for_test(123),
1626            info,
1627            source_l,
1628            source_r,
1629            params_l,
1630            params_r,
1631            vec![null_safe],
1632            (0..schema_len).collect_vec(),
1633            cond,
1634            inequality_pairs,
1635            state_l,
1636            degree_state_l,
1637            state_r,
1638            degree_state_r,
1639            Arc::new(AtomicU64::new(0)),
1640            false,
1641            Arc::new(StreamingMetrics::unused()),
1642            1024,
1643            2048,
1644            vec![(0, true)],
1645        );
1646        (tx_l, tx_r, executor.boxed().execute())
1647    }
1648
1649    async fn create_classical_executor<const T: JoinTypePrimitive>(
1650        with_condition: bool,
1651        null_safe: bool,
1652        condition_text: Option<String>,
1653    ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1654        create_executor::<T>(with_condition, null_safe, condition_text, vec![]).await
1655    }
1656
1657    async fn create_append_only_executor<const T: JoinTypePrimitive>(
1658        with_condition: bool,
1659    ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1660        let schema = Schema {
1661            fields: vec![
1662                Field::unnamed(DataType::Int64),
1663                Field::unnamed(DataType::Int64),
1664                Field::unnamed(DataType::Int64),
1665            ],
1666        };
1667        let (tx_l, source_l) = MockSource::channel();
1668        let source_l = source_l.into_executor(schema.clone(), vec![0]);
1669        let (tx_r, source_r) = MockSource::channel();
1670        let source_r = source_r.into_executor(schema, vec![0]);
1671        let params_l = JoinParams::new(vec![0, 1], vec![]);
1672        let params_r = JoinParams::new(vec![0, 1], vec![]);
1673        let cond = with_condition.then(|| create_cond(None));
1674
1675        let mem_state = MemoryStateStore::new();
1676
1677        let (state_l, degree_state_l) = create_in_memory_state_table(
1678            mem_state.clone(),
1679            &[DataType::Int64, DataType::Int64, DataType::Int64],
1680            &[
1681                OrderType::ascending(),
1682                OrderType::ascending(),
1683                OrderType::ascending(),
1684            ],
1685            &[0, 1, 0],
1686            0,
1687        )
1688        .await;
1689
1690        let (state_r, degree_state_r) = create_in_memory_state_table(
1691            mem_state,
1692            &[DataType::Int64, DataType::Int64, DataType::Int64],
1693            &[
1694                OrderType::ascending(),
1695                OrderType::ascending(),
1696                OrderType::ascending(),
1697            ],
1698            &[0, 1, 1],
1699            1,
1700        )
1701        .await;
1702
1703        let schema = match T {
1704            JoinType::LeftSemi | JoinType::LeftAnti => source_l.schema().clone(),
1705            JoinType::RightSemi | JoinType::RightAnti => source_r.schema().clone(),
1706            _ => [source_l.schema().fields(), source_r.schema().fields()]
1707                .concat()
1708                .into_iter()
1709                .collect(),
1710        };
1711        let schema_len = schema.len();
1712        let info = ExecutorInfo::for_test(schema, vec![1], "HashJoinExecutor".to_owned(), 0);
1713
1714        let executor = HashJoinExecutor::<Key128, MemoryStateStore, T, MemoryEncoding>::new(
1715            ActorContext::for_test(123),
1716            info,
1717            source_l,
1718            source_r,
1719            params_l,
1720            params_r,
1721            vec![false],
1722            (0..schema_len).collect_vec(),
1723            cond,
1724            vec![],
1725            state_l,
1726            degree_state_l,
1727            state_r,
1728            degree_state_r,
1729            Arc::new(AtomicU64::new(0)),
1730            true,
1731            Arc::new(StreamingMetrics::unused()),
1732            1024,
1733            2048,
1734            vec![(0, true)],
1735        );
1736        (tx_l, tx_r, executor.boxed().execute())
1737    }
1738
1739    #[tokio::test]
1740    async fn test_inequality_join_watermark() -> StreamExecutorResult<()> {
1741        // Test inequality join with watermark-based state cleanup
1742        // Condition: left.col1 >= right.col1 (left side is larger, clean left state)
1743        // Left state rows with col1 < watermark will be cleaned
1744        let chunk_l1 = StreamChunk::from_pretty(
1745            "  I I
1746             + 2 4
1747             + 2 7
1748             + 3 8",
1749        );
1750        let chunk_r1 = StreamChunk::from_pretty(
1751            "  I I
1752             + 2 6",
1753        );
1754        let chunk_r2 = StreamChunk::from_pretty(
1755            "  I I
1756             + 2 3",
1757        );
1758        // Test with condition: left.col1 >= right.col1
1759        let (mut tx_l, mut tx_r, mut hash_join) = create_executor::<{ JoinType::Inner }>(
1760            true,
1761            false,
1762            Some(String::from(
1763                "(greater_than_or_equal:boolean $1:int8 $3:int8)",
1764            )),
1765            vec![InequalityPairInfo {
1766                left_idx: 1,
1767                right_idx: 1,
1768                clean_left_state: true, // left >= right, left is larger
1769                clean_right_state: false,
1770                op: InequalityType::GreaterThanOrEqual,
1771            }],
1772        )
1773        .await;
1774
1775        // push the init barrier for left and right
1776        tx_l.push_barrier(test_epoch(1), false);
1777        tx_r.push_barrier(test_epoch(1), false);
1778        hash_join.next_unwrap_ready_barrier()?;
1779
1780        // push the left chunk: (2, 4), (2, 7), (3, 8)
1781        tx_l.push_chunk(chunk_l1);
1782        hash_join.next_unwrap_pending();
1783
1784        // push watermarks: left=10, right=6
1785        // Output watermark is min(10, 6) = 6 for both output columns
1786        tx_l.push_watermark(1, DataType::Int64, ScalarImpl::Int64(10));
1787        hash_join.next_unwrap_pending();
1788
1789        tx_r.push_watermark(1, DataType::Int64, ScalarImpl::Int64(6));
1790        let output_watermark = hash_join.next_unwrap_ready_watermark()?;
1791        assert_eq!(
1792            output_watermark,
1793            Watermark::new(1, DataType::Int64, ScalarImpl::Int64(6))
1794        );
1795
1796        // After watermark, left state rows with col1 < 6 are cleaned
1797        // Row (2, 4) should be cleaned (4 < 6)
1798        // Row (2, 7) should remain (7 >= 6)
1799        // Row (3, 8) should remain (8 >= 6)
1800
1801        // push right chunk (2, 6)
1802        // Only (2, 7) can match: 7 >= 6 is TRUE
1803        // (2, 4) was cleaned, so it won't match
1804        tx_r.push_chunk(chunk_r1);
1805        let chunk = hash_join.next_unwrap_ready_chunk()?;
1806        assert_eq!(
1807            chunk,
1808            StreamChunk::from_pretty(
1809                " I I I I
1810                + 2 7 2 6"
1811            )
1812        );
1813
1814        // push right chunk (2, 3)
1815        // (2, 7) can match: 7 >= 3 is TRUE
1816        // (2, 4) was cleaned, won't match
1817        tx_r.push_chunk(chunk_r2);
1818        let chunk = hash_join.next_unwrap_ready_chunk()?;
1819        assert_eq!(
1820            chunk,
1821            StreamChunk::from_pretty(
1822                " I I I I
1823                + 2 7 2 3"
1824            )
1825        );
1826
1827        Ok(())
1828    }
1829
1830    #[tokio::test]
1831    async fn test_streaming_hash_inner_join() -> StreamExecutorResult<()> {
1832        let chunk_l1 = StreamChunk::from_pretty(
1833            "  I I
1834             + 1 4
1835             + 2 5
1836             + 3 6",
1837        );
1838        let chunk_l2 = StreamChunk::from_pretty(
1839            "  I I
1840             + 3 8
1841             - 3 8",
1842        );
1843        let chunk_r1 = StreamChunk::from_pretty(
1844            "  I I
1845             + 2 7
1846             + 4 8
1847             + 6 9",
1848        );
1849        let chunk_r2 = StreamChunk::from_pretty(
1850            "  I  I
1851             + 3 10
1852             + 6 11",
1853        );
1854        let (mut tx_l, mut tx_r, mut hash_join) =
1855            create_classical_executor::<{ JoinType::Inner }>(false, false, None).await;
1856
1857        // push the init barrier for left and right
1858        tx_l.push_barrier(test_epoch(1), false);
1859        tx_r.push_barrier(test_epoch(1), false);
1860        hash_join.next_unwrap_ready_barrier()?;
1861
1862        // push the 1st left chunk
1863        tx_l.push_chunk(chunk_l1);
1864        hash_join.next_unwrap_pending();
1865
1866        // push the init barrier for left and right
1867        tx_l.push_barrier(test_epoch(2), false);
1868        tx_r.push_barrier(test_epoch(2), false);
1869        hash_join.next_unwrap_ready_barrier()?;
1870
1871        // push the 2nd left chunk
1872        tx_l.push_chunk(chunk_l2);
1873        hash_join.next_unwrap_pending();
1874
1875        // push the 1st right chunk
1876        tx_r.push_chunk(chunk_r1);
1877        let chunk = hash_join.next_unwrap_ready_chunk()?;
1878        assert_eq!(
1879            chunk,
1880            StreamChunk::from_pretty(
1881                " I I I I
1882                + 2 5 2 7"
1883            )
1884        );
1885
1886        // push the 2nd right chunk
1887        tx_r.push_chunk(chunk_r2);
1888        let chunk = hash_join.next_unwrap_ready_chunk()?;
1889        assert_eq!(
1890            chunk,
1891            StreamChunk::from_pretty(
1892                " I I I I
1893                + 3 6 3 10"
1894            )
1895        );
1896
1897        Ok(())
1898    }
1899
1900    #[tokio::test]
1901    async fn test_streaming_null_safe_hash_inner_join() -> StreamExecutorResult<()> {
1902        let chunk_l1 = StreamChunk::from_pretty(
1903            "  I I
1904             + 1 4
1905             + 2 5
1906             + . 6",
1907        );
1908        let chunk_l2 = StreamChunk::from_pretty(
1909            "  I I
1910             + . 8
1911             - . 8",
1912        );
1913        let chunk_r1 = StreamChunk::from_pretty(
1914            "  I I
1915             + 2 7
1916             + 4 8
1917             + 6 9",
1918        );
1919        let chunk_r2 = StreamChunk::from_pretty(
1920            "  I  I
1921             + . 10
1922             + 6 11",
1923        );
1924        let (mut tx_l, mut tx_r, mut hash_join) =
1925            create_classical_executor::<{ JoinType::Inner }>(false, true, None).await;
1926
1927        // push the init barrier for left and right
1928        tx_l.push_barrier(test_epoch(1), false);
1929        tx_r.push_barrier(test_epoch(1), false);
1930        hash_join.next_unwrap_ready_barrier()?;
1931
1932        // push the 1st left chunk
1933        tx_l.push_chunk(chunk_l1);
1934        hash_join.next_unwrap_pending();
1935
1936        // push the init barrier for left and right
1937        tx_l.push_barrier(test_epoch(2), false);
1938        tx_r.push_barrier(test_epoch(2), false);
1939        hash_join.next_unwrap_ready_barrier()?;
1940
1941        // push the 2nd left chunk
1942        tx_l.push_chunk(chunk_l2);
1943        hash_join.next_unwrap_pending();
1944
1945        // push the 1st right chunk
1946        tx_r.push_chunk(chunk_r1);
1947        let chunk = hash_join.next_unwrap_ready_chunk()?;
1948        assert_eq!(
1949            chunk,
1950            StreamChunk::from_pretty(
1951                " I I I I
1952                + 2 5 2 7"
1953            )
1954        );
1955
1956        // push the 2nd right chunk
1957        tx_r.push_chunk(chunk_r2);
1958        let chunk = hash_join.next_unwrap_ready_chunk()?;
1959        assert_eq!(
1960            chunk,
1961            StreamChunk::from_pretty(
1962                " I I I I
1963                + . 6 . 10"
1964            )
1965        );
1966
1967        Ok(())
1968    }
1969
1970    #[tokio::test]
1971    async fn test_streaming_hash_left_semi_join() -> StreamExecutorResult<()> {
1972        let chunk_l1 = StreamChunk::from_pretty(
1973            "  I I
1974             + 1 4
1975             + 2 5
1976             + 3 6",
1977        );
1978        let chunk_l2 = StreamChunk::from_pretty(
1979            "  I I
1980             + 3 8
1981             - 3 8",
1982        );
1983        let chunk_r1 = StreamChunk::from_pretty(
1984            "  I I
1985             + 2 7
1986             + 4 8
1987             + 6 9",
1988        );
1989        let chunk_r2 = StreamChunk::from_pretty(
1990            "  I  I
1991             + 3 10
1992             + 6 11",
1993        );
1994        let chunk_l3 = StreamChunk::from_pretty(
1995            "  I I
1996             + 6 10",
1997        );
1998        let chunk_r3 = StreamChunk::from_pretty(
1999            "  I  I
2000             - 6 11",
2001        );
2002        let chunk_r4 = StreamChunk::from_pretty(
2003            "  I  I
2004             - 6 9",
2005        );
2006        let (mut tx_l, mut tx_r, mut hash_join) =
2007            create_classical_executor::<{ JoinType::LeftSemi }>(false, false, None).await;
2008
2009        // push the init barrier for left and right
2010        tx_l.push_barrier(test_epoch(1), false);
2011        tx_r.push_barrier(test_epoch(1), false);
2012        hash_join.next_unwrap_ready_barrier()?;
2013
2014        // push the 1st left chunk
2015        tx_l.push_chunk(chunk_l1);
2016        hash_join.next_unwrap_pending();
2017
2018        // push the init barrier for left and right
2019        tx_l.push_barrier(test_epoch(2), false);
2020        tx_r.push_barrier(test_epoch(2), false);
2021        hash_join.next_unwrap_ready_barrier()?;
2022
2023        // push the 2nd left chunk
2024        tx_l.push_chunk(chunk_l2);
2025        hash_join.next_unwrap_pending();
2026
2027        // push the 1st right chunk
2028        tx_r.push_chunk(chunk_r1);
2029        let chunk = hash_join.next_unwrap_ready_chunk()?;
2030        assert_eq!(
2031            chunk,
2032            StreamChunk::from_pretty(
2033                " I I
2034                + 2 5"
2035            )
2036        );
2037
2038        // push the 2nd right chunk
2039        tx_r.push_chunk(chunk_r2);
2040        let chunk = hash_join.next_unwrap_ready_chunk()?;
2041        assert_eq!(
2042            chunk,
2043            StreamChunk::from_pretty(
2044                " I I
2045                + 3 6"
2046            )
2047        );
2048
2049        // push the 3rd left chunk (tests forward_exactly_once)
2050        tx_l.push_chunk(chunk_l3);
2051        let chunk = hash_join.next_unwrap_ready_chunk()?;
2052        assert_eq!(
2053            chunk,
2054            StreamChunk::from_pretty(
2055                " I I
2056                + 6 10"
2057            )
2058        );
2059
2060        // push the 3rd right chunk
2061        // (tests that no change if there are still matches)
2062        tx_r.push_chunk(chunk_r3);
2063        hash_join.next_unwrap_pending();
2064
2065        // push the 3rd left chunk
2066        // (tests that deletion occurs when there are no more matches)
2067        tx_r.push_chunk(chunk_r4);
2068        let chunk = hash_join.next_unwrap_ready_chunk()?;
2069        assert_eq!(
2070            chunk,
2071            StreamChunk::from_pretty(
2072                " I I
2073                - 6 10"
2074            )
2075        );
2076
2077        Ok(())
2078    }
2079
2080    #[tokio::test]
2081    async fn test_streaming_null_safe_hash_left_semi_join() -> StreamExecutorResult<()> {
2082        let chunk_l1 = StreamChunk::from_pretty(
2083            "  I I
2084             + 1 4
2085             + 2 5
2086             + . 6",
2087        );
2088        let chunk_l2 = StreamChunk::from_pretty(
2089            "  I I
2090             + . 8
2091             - . 8",
2092        );
2093        let chunk_r1 = StreamChunk::from_pretty(
2094            "  I I
2095             + 2 7
2096             + 4 8
2097             + 6 9",
2098        );
2099        let chunk_r2 = StreamChunk::from_pretty(
2100            "  I  I
2101             + . 10
2102             + 6 11",
2103        );
2104        let chunk_l3 = StreamChunk::from_pretty(
2105            "  I I
2106             + 6 10",
2107        );
2108        let chunk_r3 = StreamChunk::from_pretty(
2109            "  I  I
2110             - 6 11",
2111        );
2112        let chunk_r4 = StreamChunk::from_pretty(
2113            "  I  I
2114             - 6 9",
2115        );
2116        let (mut tx_l, mut tx_r, mut hash_join) =
2117            create_classical_executor::<{ JoinType::LeftSemi }>(false, true, None).await;
2118
2119        // push the init barrier for left and right
2120        tx_l.push_barrier(test_epoch(1), false);
2121        tx_r.push_barrier(test_epoch(1), false);
2122        hash_join.next_unwrap_ready_barrier()?;
2123
2124        // push the 1st left chunk
2125        tx_l.push_chunk(chunk_l1);
2126        hash_join.next_unwrap_pending();
2127
2128        // push the init barrier for left and right
2129        tx_l.push_barrier(test_epoch(2), false);
2130        tx_r.push_barrier(test_epoch(2), false);
2131        hash_join.next_unwrap_ready_barrier()?;
2132
2133        // push the 2nd left chunk
2134        tx_l.push_chunk(chunk_l2);
2135        hash_join.next_unwrap_pending();
2136
2137        // push the 1st right chunk
2138        tx_r.push_chunk(chunk_r1);
2139        let chunk = hash_join.next_unwrap_ready_chunk()?;
2140        assert_eq!(
2141            chunk,
2142            StreamChunk::from_pretty(
2143                " I I
2144                + 2 5"
2145            )
2146        );
2147
2148        // push the 2nd right chunk
2149        tx_r.push_chunk(chunk_r2);
2150        let chunk = hash_join.next_unwrap_ready_chunk()?;
2151        assert_eq!(
2152            chunk,
2153            StreamChunk::from_pretty(
2154                " I I
2155                + . 6"
2156            )
2157        );
2158
2159        // push the 3rd left chunk (tests forward_exactly_once)
2160        tx_l.push_chunk(chunk_l3);
2161        let chunk = hash_join.next_unwrap_ready_chunk()?;
2162        assert_eq!(
2163            chunk,
2164            StreamChunk::from_pretty(
2165                " I I
2166                + 6 10"
2167            )
2168        );
2169
2170        // push the 3rd right chunk
2171        // (tests that no change if there are still matches)
2172        tx_r.push_chunk(chunk_r3);
2173        hash_join.next_unwrap_pending();
2174
2175        // push the 3rd left chunk
2176        // (tests that deletion occurs when there are no more matches)
2177        tx_r.push_chunk(chunk_r4);
2178        let chunk = hash_join.next_unwrap_ready_chunk()?;
2179        assert_eq!(
2180            chunk,
2181            StreamChunk::from_pretty(
2182                " I I
2183                - 6 10"
2184            )
2185        );
2186
2187        Ok(())
2188    }
2189
2190    #[tokio::test]
2191    async fn test_streaming_hash_inner_join_append_only() -> StreamExecutorResult<()> {
2192        let chunk_l1 = StreamChunk::from_pretty(
2193            "  I I I
2194             + 1 4 1
2195             + 2 5 2
2196             + 3 6 3",
2197        );
2198        let chunk_l2 = StreamChunk::from_pretty(
2199            "  I I I
2200             + 4 9 4
2201             + 5 10 5",
2202        );
2203        let chunk_r1 = StreamChunk::from_pretty(
2204            "  I I I
2205             + 2 5 1
2206             + 4 9 2
2207             + 6 9 3",
2208        );
2209        let chunk_r2 = StreamChunk::from_pretty(
2210            "  I I I
2211             + 1 4 4
2212             + 3 6 5",
2213        );
2214
2215        let (mut tx_l, mut tx_r, mut hash_join) =
2216            create_append_only_executor::<{ JoinType::Inner }>(false).await;
2217
2218        // push the init barrier for left and right
2219        tx_l.push_barrier(test_epoch(1), false);
2220        tx_r.push_barrier(test_epoch(1), false);
2221        hash_join.next_unwrap_ready_barrier()?;
2222
2223        // push the 1st left chunk
2224        tx_l.push_chunk(chunk_l1);
2225        hash_join.next_unwrap_pending();
2226
2227        // push the init barrier for left and right
2228        tx_l.push_barrier(test_epoch(2), false);
2229        tx_r.push_barrier(test_epoch(2), false);
2230        hash_join.next_unwrap_ready_barrier()?;
2231
2232        // push the 2nd left chunk
2233        tx_l.push_chunk(chunk_l2);
2234        hash_join.next_unwrap_pending();
2235
2236        // push the 1st right chunk
2237        tx_r.push_chunk(chunk_r1);
2238        let chunk = hash_join.next_unwrap_ready_chunk()?;
2239        assert_eq!(
2240            chunk,
2241            StreamChunk::from_pretty(
2242                " I I I I I I
2243                + 2 5 2 2 5 1
2244                + 4 9 4 4 9 2"
2245            )
2246        );
2247
2248        // push the 2nd right chunk
2249        tx_r.push_chunk(chunk_r2);
2250        let chunk = hash_join.next_unwrap_ready_chunk()?;
2251        assert_eq!(
2252            chunk,
2253            StreamChunk::from_pretty(
2254                " I I I I I I
2255                + 1 4 1 1 4 4
2256                + 3 6 3 3 6 5"
2257            )
2258        );
2259
2260        Ok(())
2261    }
2262
2263    #[tokio::test]
2264    async fn test_streaming_hash_left_semi_join_append_only() -> StreamExecutorResult<()> {
2265        let chunk_l1 = StreamChunk::from_pretty(
2266            "  I I I
2267             + 1 4 1
2268             + 2 5 2
2269             + 3 6 3",
2270        );
2271        let chunk_l2 = StreamChunk::from_pretty(
2272            "  I I I
2273             + 4 9 4
2274             + 5 10 5",
2275        );
2276        let chunk_r1 = StreamChunk::from_pretty(
2277            "  I I I
2278             + 2 5 1
2279             + 4 9 2
2280             + 6 9 3",
2281        );
2282        let chunk_r2 = StreamChunk::from_pretty(
2283            "  I I I
2284             + 1 4 4
2285             + 3 6 5",
2286        );
2287
2288        let (mut tx_l, mut tx_r, mut hash_join) =
2289            create_append_only_executor::<{ JoinType::LeftSemi }>(false).await;
2290
2291        // push the init barrier for left and right
2292        tx_l.push_barrier(test_epoch(1), false);
2293        tx_r.push_barrier(test_epoch(1), false);
2294        hash_join.next_unwrap_ready_barrier()?;
2295
2296        // push the 1st left chunk
2297        tx_l.push_chunk(chunk_l1);
2298        hash_join.next_unwrap_pending();
2299
2300        // push the init barrier for left and right
2301        tx_l.push_barrier(test_epoch(2), false);
2302        tx_r.push_barrier(test_epoch(2), false);
2303        hash_join.next_unwrap_ready_barrier()?;
2304
2305        // push the 2nd left chunk
2306        tx_l.push_chunk(chunk_l2);
2307        hash_join.next_unwrap_pending();
2308
2309        // push the 1st right chunk
2310        tx_r.push_chunk(chunk_r1);
2311        let chunk = hash_join.next_unwrap_ready_chunk()?;
2312        assert_eq!(
2313            chunk,
2314            StreamChunk::from_pretty(
2315                " I I I
2316                + 2 5 2
2317                + 4 9 4"
2318            )
2319        );
2320
2321        // push the 2nd right chunk
2322        tx_r.push_chunk(chunk_r2);
2323        let chunk = hash_join.next_unwrap_ready_chunk()?;
2324        assert_eq!(
2325            chunk,
2326            StreamChunk::from_pretty(
2327                " I I I
2328                + 1 4 1
2329                + 3 6 3"
2330            )
2331        );
2332
2333        Ok(())
2334    }
2335
2336    #[tokio::test]
2337    async fn test_streaming_hash_right_semi_join_append_only() -> StreamExecutorResult<()> {
2338        let chunk_l1 = StreamChunk::from_pretty(
2339            "  I I I
2340             + 1 4 1
2341             + 2 5 2
2342             + 3 6 3",
2343        );
2344        let chunk_l2 = StreamChunk::from_pretty(
2345            "  I I I
2346             + 4 9 4
2347             + 5 10 5",
2348        );
2349        let chunk_r1 = StreamChunk::from_pretty(
2350            "  I I I
2351             + 2 5 1
2352             + 4 9 2
2353             + 6 9 3",
2354        );
2355        let chunk_r2 = StreamChunk::from_pretty(
2356            "  I I I
2357             + 1 4 4
2358             + 3 6 5",
2359        );
2360
2361        let (mut tx_l, mut tx_r, mut hash_join) =
2362            create_append_only_executor::<{ JoinType::RightSemi }>(false).await;
2363
2364        // push the init barrier for left and right
2365        tx_l.push_barrier(test_epoch(1), false);
2366        tx_r.push_barrier(test_epoch(1), false);
2367        hash_join.next_unwrap_ready_barrier()?;
2368
2369        // push the 1st left chunk
2370        tx_l.push_chunk(chunk_l1);
2371        hash_join.next_unwrap_pending();
2372
2373        // push the init barrier for left and right
2374        tx_l.push_barrier(test_epoch(2), false);
2375        tx_r.push_barrier(test_epoch(2), false);
2376        hash_join.next_unwrap_ready_barrier()?;
2377
2378        // push the 2nd left chunk
2379        tx_l.push_chunk(chunk_l2);
2380        hash_join.next_unwrap_pending();
2381
2382        // push the 1st right chunk
2383        tx_r.push_chunk(chunk_r1);
2384        let chunk = hash_join.next_unwrap_ready_chunk()?;
2385        assert_eq!(
2386            chunk,
2387            StreamChunk::from_pretty(
2388                " I I I
2389                + 2 5 1
2390                + 4 9 2"
2391            )
2392        );
2393
2394        // push the 2nd right chunk
2395        tx_r.push_chunk(chunk_r2);
2396        let chunk = hash_join.next_unwrap_ready_chunk()?;
2397        assert_eq!(
2398            chunk,
2399            StreamChunk::from_pretty(
2400                " I I I
2401                + 1 4 4
2402                + 3 6 5"
2403            )
2404        );
2405
2406        Ok(())
2407    }
2408
2409    #[tokio::test]
2410    async fn test_streaming_hash_right_semi_join() -> StreamExecutorResult<()> {
2411        let chunk_r1 = StreamChunk::from_pretty(
2412            "  I I
2413             + 1 4
2414             + 2 5
2415             + 3 6",
2416        );
2417        let chunk_r2 = StreamChunk::from_pretty(
2418            "  I I
2419             + 3 8
2420             - 3 8",
2421        );
2422        let chunk_l1 = StreamChunk::from_pretty(
2423            "  I I
2424             + 2 7
2425             + 4 8
2426             + 6 9",
2427        );
2428        let chunk_l2 = StreamChunk::from_pretty(
2429            "  I  I
2430             + 3 10
2431             + 6 11",
2432        );
2433        let chunk_r3 = StreamChunk::from_pretty(
2434            "  I I
2435             + 6 10",
2436        );
2437        let chunk_l3 = StreamChunk::from_pretty(
2438            "  I  I
2439             - 6 11",
2440        );
2441        let chunk_l4 = StreamChunk::from_pretty(
2442            "  I  I
2443             - 6 9",
2444        );
2445        let (mut tx_l, mut tx_r, mut hash_join) =
2446            create_classical_executor::<{ JoinType::RightSemi }>(false, false, None).await;
2447
2448        // push the init barrier for left and right
2449        tx_l.push_barrier(test_epoch(1), false);
2450        tx_r.push_barrier(test_epoch(1), false);
2451        hash_join.next_unwrap_ready_barrier()?;
2452
2453        // push the 1st right chunk
2454        tx_r.push_chunk(chunk_r1);
2455        hash_join.next_unwrap_pending();
2456
2457        // push the init barrier for left and right
2458        tx_l.push_barrier(test_epoch(2), false);
2459        tx_r.push_barrier(test_epoch(2), false);
2460        hash_join.next_unwrap_ready_barrier()?;
2461
2462        // push the 2nd right chunk
2463        tx_r.push_chunk(chunk_r2);
2464        hash_join.next_unwrap_pending();
2465
2466        // push the 1st left chunk
2467        tx_l.push_chunk(chunk_l1);
2468        let chunk = hash_join.next_unwrap_ready_chunk()?;
2469        assert_eq!(
2470            chunk,
2471            StreamChunk::from_pretty(
2472                " I I
2473                + 2 5"
2474            )
2475        );
2476
2477        // push the 2nd left chunk
2478        tx_l.push_chunk(chunk_l2);
2479        let chunk = hash_join.next_unwrap_ready_chunk()?;
2480        assert_eq!(
2481            chunk,
2482            StreamChunk::from_pretty(
2483                " I I
2484                + 3 6"
2485            )
2486        );
2487
2488        // push the 3rd right chunk (tests forward_exactly_once)
2489        tx_r.push_chunk(chunk_r3);
2490        let chunk = hash_join.next_unwrap_ready_chunk()?;
2491        assert_eq!(
2492            chunk,
2493            StreamChunk::from_pretty(
2494                " I I
2495                + 6 10"
2496            )
2497        );
2498
2499        // push the 3rd left chunk
2500        // (tests that no change if there are still matches)
2501        tx_l.push_chunk(chunk_l3);
2502        hash_join.next_unwrap_pending();
2503
2504        // push the 3rd right chunk
2505        // (tests that deletion occurs when there are no more matches)
2506        tx_l.push_chunk(chunk_l4);
2507        let chunk = hash_join.next_unwrap_ready_chunk()?;
2508        assert_eq!(
2509            chunk,
2510            StreamChunk::from_pretty(
2511                " I I
2512                - 6 10"
2513            )
2514        );
2515
2516        Ok(())
2517    }
2518
2519    #[tokio::test]
2520    async fn test_streaming_hash_left_anti_join() -> StreamExecutorResult<()> {
2521        let chunk_l1 = StreamChunk::from_pretty(
2522            "  I I
2523             + 1 4
2524             + 2 5
2525             + 3 6",
2526        );
2527        let chunk_l2 = StreamChunk::from_pretty(
2528            "  I I
2529             + 3 8
2530             - 3 8",
2531        );
2532        let chunk_r1 = StreamChunk::from_pretty(
2533            "  I I
2534             + 2 7
2535             + 4 8
2536             + 6 9",
2537        );
2538        let chunk_r2 = StreamChunk::from_pretty(
2539            "  I  I
2540             + 3 10
2541             + 6 11
2542             + 1 2
2543             + 1 3",
2544        );
2545        let chunk_l3 = StreamChunk::from_pretty(
2546            "  I I
2547             + 9 10",
2548        );
2549        let chunk_r3 = StreamChunk::from_pretty(
2550            "  I I
2551             - 1 2",
2552        );
2553        let chunk_r4 = StreamChunk::from_pretty(
2554            "  I I
2555             - 1 3",
2556        );
2557        let (mut tx_l, mut tx_r, mut hash_join) =
2558            create_classical_executor::<{ JoinType::LeftAnti }>(false, false, None).await;
2559
2560        // push the init barrier for left and right
2561        tx_l.push_barrier(test_epoch(1), false);
2562        tx_r.push_barrier(test_epoch(1), false);
2563        hash_join.next_unwrap_ready_barrier()?;
2564
2565        // push the 1st left chunk
2566        tx_l.push_chunk(chunk_l1);
2567        let chunk = hash_join.next_unwrap_ready_chunk()?;
2568        assert_eq!(
2569            chunk,
2570            StreamChunk::from_pretty(
2571                " I I
2572                + 1 4
2573                + 2 5
2574                + 3 6",
2575            )
2576        );
2577
2578        // push the init barrier for left and right
2579        tx_l.push_barrier(test_epoch(2), false);
2580        tx_r.push_barrier(test_epoch(2), false);
2581        hash_join.next_unwrap_ready_barrier()?;
2582
2583        // push the 2nd left chunk
2584        tx_l.push_chunk(chunk_l2);
2585        let chunk = hash_join.next_unwrap_ready_chunk()?;
2586        assert_eq!(
2587            chunk,
2588            StreamChunk::from_pretty(
2589                "  I I
2590                 + 3 8 D
2591                 - 3 8 D",
2592            )
2593        );
2594
2595        // push the 1st right chunk
2596        tx_r.push_chunk(chunk_r1);
2597        let chunk = hash_join.next_unwrap_ready_chunk()?;
2598        assert_eq!(
2599            chunk,
2600            StreamChunk::from_pretty(
2601                " I I
2602                - 2 5"
2603            )
2604        );
2605
2606        // push the 2nd right chunk
2607        tx_r.push_chunk(chunk_r2);
2608        let chunk = hash_join.next_unwrap_ready_chunk()?;
2609        assert_eq!(
2610            chunk,
2611            StreamChunk::from_pretty(
2612                " I I
2613                - 3 6
2614                - 1 4"
2615            )
2616        );
2617
2618        // push the 3rd left chunk (tests forward_exactly_once)
2619        tx_l.push_chunk(chunk_l3);
2620        let chunk = hash_join.next_unwrap_ready_chunk()?;
2621        assert_eq!(
2622            chunk,
2623            StreamChunk::from_pretty(
2624                " I I
2625                + 9 10"
2626            )
2627        );
2628
2629        // push the 3rd right chunk
2630        // (tests that no change if there are still matches)
2631        tx_r.push_chunk(chunk_r3);
2632        hash_join.next_unwrap_pending();
2633
2634        // push the 4th right chunk
2635        // (tests that insertion occurs when there are no more matches)
2636        tx_r.push_chunk(chunk_r4);
2637        let chunk = hash_join.next_unwrap_ready_chunk()?;
2638        assert_eq!(
2639            chunk,
2640            StreamChunk::from_pretty(
2641                " I I
2642                + 1 4"
2643            )
2644        );
2645
2646        Ok(())
2647    }
2648
2649    #[tokio::test]
2650    async fn test_streaming_hash_right_anti_join() -> StreamExecutorResult<()> {
2651        let chunk_r1 = StreamChunk::from_pretty(
2652            "  I I
2653             + 1 4
2654             + 2 5
2655             + 3 6",
2656        );
2657        let chunk_r2 = StreamChunk::from_pretty(
2658            "  I I
2659             + 3 8
2660             - 3 8",
2661        );
2662        let chunk_l1 = StreamChunk::from_pretty(
2663            "  I I
2664             + 2 7
2665             + 4 8
2666             + 6 9",
2667        );
2668        let chunk_l2 = StreamChunk::from_pretty(
2669            "  I  I
2670             + 3 10
2671             + 6 11
2672             + 1 2
2673             + 1 3",
2674        );
2675        let chunk_r3 = StreamChunk::from_pretty(
2676            "  I I
2677             + 9 10",
2678        );
2679        let chunk_l3 = StreamChunk::from_pretty(
2680            "  I I
2681             - 1 2",
2682        );
2683        let chunk_l4 = StreamChunk::from_pretty(
2684            "  I I
2685             - 1 3",
2686        );
2687        let (mut tx_r, mut tx_l, mut hash_join) =
2688            create_classical_executor::<{ JoinType::LeftAnti }>(false, false, None).await;
2689
2690        // push the init barrier for left and right
2691        tx_r.push_barrier(test_epoch(1), false);
2692        tx_l.push_barrier(test_epoch(1), false);
2693        hash_join.next_unwrap_ready_barrier()?;
2694
2695        // push the 1st right chunk
2696        tx_r.push_chunk(chunk_r1);
2697        let chunk = hash_join.next_unwrap_ready_chunk()?;
2698        assert_eq!(
2699            chunk,
2700            StreamChunk::from_pretty(
2701                " I I
2702                + 1 4
2703                + 2 5
2704                + 3 6",
2705            )
2706        );
2707
2708        // push the init barrier for left and right
2709        tx_r.push_barrier(test_epoch(2), false);
2710        tx_l.push_barrier(test_epoch(2), false);
2711        hash_join.next_unwrap_ready_barrier()?;
2712
2713        // push the 2nd right chunk
2714        tx_r.push_chunk(chunk_r2);
2715        let chunk = hash_join.next_unwrap_ready_chunk()?;
2716        assert_eq!(
2717            chunk,
2718            StreamChunk::from_pretty(
2719                "  I I
2720                 + 3 8 D
2721                 - 3 8 D",
2722            )
2723        );
2724
2725        // push the 1st left chunk
2726        tx_l.push_chunk(chunk_l1);
2727        let chunk = hash_join.next_unwrap_ready_chunk()?;
2728        assert_eq!(
2729            chunk,
2730            StreamChunk::from_pretty(
2731                " I I
2732                - 2 5"
2733            )
2734        );
2735
2736        // push the 2nd left chunk
2737        tx_l.push_chunk(chunk_l2);
2738        let chunk = hash_join.next_unwrap_ready_chunk()?;
2739        assert_eq!(
2740            chunk,
2741            StreamChunk::from_pretty(
2742                " I I
2743                - 3 6
2744                - 1 4"
2745            )
2746        );
2747
2748        // push the 3rd right chunk (tests forward_exactly_once)
2749        tx_r.push_chunk(chunk_r3);
2750        let chunk = hash_join.next_unwrap_ready_chunk()?;
2751        assert_eq!(
2752            chunk,
2753            StreamChunk::from_pretty(
2754                " I I
2755                + 9 10"
2756            )
2757        );
2758
2759        // push the 3rd left chunk
2760        // (tests that no change if there are still matches)
2761        tx_l.push_chunk(chunk_l3);
2762        hash_join.next_unwrap_pending();
2763
2764        // push the 4th left chunk
2765        // (tests that insertion occurs when there are no more matches)
2766        tx_l.push_chunk(chunk_l4);
2767        let chunk = hash_join.next_unwrap_ready_chunk()?;
2768        assert_eq!(
2769            chunk,
2770            StreamChunk::from_pretty(
2771                " I I
2772                + 1 4"
2773            )
2774        );
2775
2776        Ok(())
2777    }
2778
2779    #[tokio::test]
2780    async fn test_streaming_hash_inner_join_with_barrier() -> StreamExecutorResult<()> {
2781        let chunk_l1 = StreamChunk::from_pretty(
2782            "  I I
2783             + 1 4
2784             + 2 5
2785             + 3 6",
2786        );
2787        let chunk_l2 = StreamChunk::from_pretty(
2788            "  I I
2789             + 6 8
2790             + 3 8",
2791        );
2792        let chunk_r1 = StreamChunk::from_pretty(
2793            "  I I
2794             + 2 7
2795             + 4 8
2796             + 6 9",
2797        );
2798        let chunk_r2 = StreamChunk::from_pretty(
2799            "  I  I
2800             + 3 10
2801             + 6 11",
2802        );
2803        let (mut tx_l, mut tx_r, mut hash_join) =
2804            create_classical_executor::<{ JoinType::Inner }>(false, false, None).await;
2805
2806        // push the init barrier for left and right
2807        tx_l.push_barrier(test_epoch(1), false);
2808        tx_r.push_barrier(test_epoch(1), false);
2809        hash_join.next_unwrap_ready_barrier()?;
2810
2811        // push the 1st left chunk
2812        tx_l.push_chunk(chunk_l1);
2813        hash_join.next_unwrap_pending();
2814
2815        // push a barrier to left side
2816        tx_l.push_barrier(test_epoch(2), false);
2817
2818        // push the 2nd left chunk
2819        tx_l.push_chunk(chunk_l2);
2820
2821        // join the first right chunk
2822        tx_r.push_chunk(chunk_r1);
2823
2824        // Consume stream chunk
2825        let chunk = hash_join.next_unwrap_ready_chunk()?;
2826        assert_eq!(
2827            chunk,
2828            StreamChunk::from_pretty(
2829                " I I I I
2830                + 2 5 2 7"
2831            )
2832        );
2833
2834        // push a barrier to right side
2835        tx_r.push_barrier(test_epoch(2), false);
2836
2837        // get the aligned barrier here
2838        let expected_epoch = EpochPair::new_test_epoch(test_epoch(2));
2839        assert!(matches!(
2840            hash_join.next_unwrap_ready_barrier()?,
2841            Barrier {
2842                epoch,
2843                mutation: None,
2844                ..
2845            } if epoch == expected_epoch
2846        ));
2847
2848        // join the 2nd left chunk
2849        let chunk = hash_join.next_unwrap_ready_chunk()?;
2850        assert_eq!(
2851            chunk,
2852            StreamChunk::from_pretty(
2853                " I I I I
2854                + 6 8 6 9"
2855            )
2856        );
2857
2858        // push the 2nd right chunk
2859        tx_r.push_chunk(chunk_r2);
2860        let chunk = hash_join.next_unwrap_ready_chunk()?;
2861        assert_eq!(
2862            chunk,
2863            StreamChunk::from_pretty(
2864                " I I I I
2865                + 3 6 3 10
2866                + 3 8 3 10
2867                + 6 8 6 11"
2868            )
2869        );
2870
2871        Ok(())
2872    }
2873
2874    #[tokio::test]
2875    async fn test_streaming_hash_inner_join_with_null_and_barrier() -> StreamExecutorResult<()> {
2876        let chunk_l1 = StreamChunk::from_pretty(
2877            "  I I
2878             + 1 4
2879             + 2 .
2880             + 3 .",
2881        );
2882        let chunk_l2 = StreamChunk::from_pretty(
2883            "  I I
2884             + 6 .
2885             + 3 8",
2886        );
2887        let chunk_r1 = StreamChunk::from_pretty(
2888            "  I I
2889             + 2 7
2890             + 4 8
2891             + 6 9",
2892        );
2893        let chunk_r2 = StreamChunk::from_pretty(
2894            "  I  I
2895             + 3 10
2896             + 6 11",
2897        );
2898        let (mut tx_l, mut tx_r, mut hash_join) =
2899            create_classical_executor::<{ JoinType::Inner }>(false, false, None).await;
2900
2901        // push the init barrier for left and right
2902        tx_l.push_barrier(test_epoch(1), false);
2903        tx_r.push_barrier(test_epoch(1), false);
2904        hash_join.next_unwrap_ready_barrier()?;
2905
2906        // push the 1st left chunk
2907        tx_l.push_chunk(chunk_l1);
2908        hash_join.next_unwrap_pending();
2909
2910        // push a barrier to left side
2911        tx_l.push_barrier(test_epoch(2), false);
2912
2913        // push the 2nd left chunk
2914        tx_l.push_chunk(chunk_l2);
2915
2916        // join the first right chunk
2917        tx_r.push_chunk(chunk_r1);
2918
2919        // Consume stream chunk
2920        let chunk = hash_join.next_unwrap_ready_chunk()?;
2921        assert_eq!(
2922            chunk,
2923            StreamChunk::from_pretty(
2924                " I I I I
2925                + 2 . 2 7"
2926            )
2927        );
2928
2929        // push a barrier to right side
2930        tx_r.push_barrier(test_epoch(2), false);
2931
2932        // get the aligned barrier here
2933        let expected_epoch = EpochPair::new_test_epoch(test_epoch(2));
2934        assert!(matches!(
2935            hash_join.next_unwrap_ready_barrier()?,
2936            Barrier {
2937                epoch,
2938                mutation: None,
2939                ..
2940            } if epoch == expected_epoch
2941        ));
2942
2943        // join the 2nd left chunk
2944        let chunk = hash_join.next_unwrap_ready_chunk()?;
2945        assert_eq!(
2946            chunk,
2947            StreamChunk::from_pretty(
2948                " I I I I
2949                + 6 . 6 9"
2950            )
2951        );
2952
2953        // push the 2nd right chunk
2954        tx_r.push_chunk(chunk_r2);
2955        let chunk = hash_join.next_unwrap_ready_chunk()?;
2956        assert_eq!(
2957            chunk,
2958            StreamChunk::from_pretty(
2959                " I I I I
2960                + 3 8 3 10
2961                + 3 . 3 10
2962                + 6 . 6 11"
2963            )
2964        );
2965
2966        Ok(())
2967    }
2968
2969    #[tokio::test]
2970    async fn test_streaming_hash_left_join() -> StreamExecutorResult<()> {
2971        let chunk_l1 = StreamChunk::from_pretty(
2972            "  I I
2973             + 1 4
2974             + 2 5
2975             + 3 6",
2976        );
2977        let chunk_l2 = StreamChunk::from_pretty(
2978            "  I I
2979             + 3 8
2980             - 3 8",
2981        );
2982        let chunk_r1 = StreamChunk::from_pretty(
2983            "  I I
2984             + 2 7
2985             + 4 8
2986             + 6 9",
2987        );
2988        let chunk_r2 = StreamChunk::from_pretty(
2989            "  I  I
2990             + 3 10
2991             + 6 11",
2992        );
2993        let (mut tx_l, mut tx_r, mut hash_join) =
2994            create_classical_executor::<{ JoinType::LeftOuter }>(false, false, None).await;
2995
2996        // push the init barrier for left and right
2997        tx_l.push_barrier(test_epoch(1), false);
2998        tx_r.push_barrier(test_epoch(1), false);
2999        hash_join.next_unwrap_ready_barrier()?;
3000
3001        // push the 1st left chunk
3002        tx_l.push_chunk(chunk_l1);
3003        let chunk = hash_join.next_unwrap_ready_chunk()?;
3004        assert_eq!(
3005            chunk,
3006            StreamChunk::from_pretty(
3007                " I I I I
3008                + 1 4 . .
3009                + 2 5 . .
3010                + 3 6 . ."
3011            )
3012        );
3013
3014        // push the 2nd left chunk
3015        tx_l.push_chunk(chunk_l2);
3016        let chunk = hash_join.next_unwrap_ready_chunk()?;
3017        assert_eq!(
3018            chunk,
3019            StreamChunk::from_pretty(
3020                " I I I I
3021                + 3 8 . . D
3022                - 3 8 . . D"
3023            )
3024        );
3025
3026        // push the 1st right chunk
3027        tx_r.push_chunk(chunk_r1);
3028        let chunk = hash_join.next_unwrap_ready_chunk()?;
3029        assert_eq!(
3030            chunk,
3031            StreamChunk::from_pretty(
3032                " I I I I
3033                - 2 5 . .
3034                + 2 5 2 7"
3035            )
3036        );
3037
3038        // push the 2nd right chunk
3039        tx_r.push_chunk(chunk_r2);
3040        let chunk = hash_join.next_unwrap_ready_chunk()?;
3041        assert_eq!(
3042            chunk,
3043            StreamChunk::from_pretty(
3044                " I I I I
3045                - 3 6 . .
3046                + 3 6 3 10"
3047            )
3048        );
3049
3050        Ok(())
3051    }
3052
3053    #[tokio::test]
3054    async fn test_streaming_null_safe_hash_left_join() -> StreamExecutorResult<()> {
3055        let chunk_l1 = StreamChunk::from_pretty(
3056            "  I I
3057             + 1 4
3058             + 2 5
3059             + . 6",
3060        );
3061        let chunk_l2 = StreamChunk::from_pretty(
3062            "  I I
3063             + . 8
3064             - . 8",
3065        );
3066        let chunk_r1 = StreamChunk::from_pretty(
3067            "  I I
3068             + 2 7
3069             + 4 8
3070             + 6 9",
3071        );
3072        let chunk_r2 = StreamChunk::from_pretty(
3073            "  I  I
3074             + . 10
3075             + 6 11",
3076        );
3077        let (mut tx_l, mut tx_r, mut hash_join) =
3078            create_classical_executor::<{ JoinType::LeftOuter }>(false, true, None).await;
3079
3080        // push the init barrier for left and right
3081        tx_l.push_barrier(test_epoch(1), false);
3082        tx_r.push_barrier(test_epoch(1), false);
3083        hash_join.next_unwrap_ready_barrier()?;
3084
3085        // push the 1st left chunk
3086        tx_l.push_chunk(chunk_l1);
3087        let chunk = hash_join.next_unwrap_ready_chunk()?;
3088        assert_eq!(
3089            chunk,
3090            StreamChunk::from_pretty(
3091                " I I I I
3092                + 1 4 . .
3093                + 2 5 . .
3094                + . 6 . ."
3095            )
3096        );
3097
3098        // push the 2nd left chunk
3099        tx_l.push_chunk(chunk_l2);
3100        let chunk = hash_join.next_unwrap_ready_chunk()?;
3101        assert_eq!(
3102            chunk,
3103            StreamChunk::from_pretty(
3104                " I I I I
3105                + . 8 . . D
3106                - . 8 . . D"
3107            )
3108        );
3109
3110        // push the 1st right chunk
3111        tx_r.push_chunk(chunk_r1);
3112        let chunk = hash_join.next_unwrap_ready_chunk()?;
3113        assert_eq!(
3114            chunk,
3115            StreamChunk::from_pretty(
3116                " I I I I
3117                - 2 5 . .
3118                + 2 5 2 7"
3119            )
3120        );
3121
3122        // push the 2nd right chunk
3123        tx_r.push_chunk(chunk_r2);
3124        let chunk = hash_join.next_unwrap_ready_chunk()?;
3125        assert_eq!(
3126            chunk,
3127            StreamChunk::from_pretty(
3128                " I I I I
3129                - . 6 . .
3130                + . 6 . 10"
3131            )
3132        );
3133
3134        Ok(())
3135    }
3136
3137    #[tokio::test]
3138    async fn test_streaming_hash_right_join() -> StreamExecutorResult<()> {
3139        let chunk_l1 = StreamChunk::from_pretty(
3140            "  I I
3141             + 1 4
3142             + 2 5
3143             + 3 6",
3144        );
3145        let chunk_l2 = StreamChunk::from_pretty(
3146            "  I I
3147             + 3 8
3148             - 3 8",
3149        );
3150        let chunk_r1 = StreamChunk::from_pretty(
3151            "  I I
3152             + 2 7
3153             + 4 8
3154             + 6 9",
3155        );
3156        let chunk_r2 = StreamChunk::from_pretty(
3157            "  I  I
3158             + 5 10
3159             - 5 10",
3160        );
3161        let (mut tx_l, mut tx_r, mut hash_join) =
3162            create_classical_executor::<{ JoinType::RightOuter }>(false, false, None).await;
3163
3164        // push the init barrier for left and right
3165        tx_l.push_barrier(test_epoch(1), false);
3166        tx_r.push_barrier(test_epoch(1), false);
3167        hash_join.next_unwrap_ready_barrier()?;
3168
3169        // push the 1st left chunk
3170        tx_l.push_chunk(chunk_l1);
3171        hash_join.next_unwrap_pending();
3172
3173        // push the 2nd left chunk
3174        tx_l.push_chunk(chunk_l2);
3175        hash_join.next_unwrap_pending();
3176
3177        // push the 1st right chunk
3178        tx_r.push_chunk(chunk_r1);
3179        let chunk = hash_join.next_unwrap_ready_chunk()?;
3180        assert_eq!(
3181            chunk,
3182            StreamChunk::from_pretty(
3183                " I I I I
3184                + 2 5 2 7
3185                + . . 4 8
3186                + . . 6 9"
3187            )
3188        );
3189
3190        // push the 2nd right chunk
3191        tx_r.push_chunk(chunk_r2);
3192        let chunk = hash_join.next_unwrap_ready_chunk()?;
3193        assert_eq!(
3194            chunk,
3195            StreamChunk::from_pretty(
3196                " I I I I
3197                + . . 5 10 D
3198                - . . 5 10 D"
3199            )
3200        );
3201
3202        Ok(())
3203    }
3204
3205    #[tokio::test]
3206    async fn test_streaming_hash_left_join_append_only() -> StreamExecutorResult<()> {
3207        let chunk_l1 = StreamChunk::from_pretty(
3208            "  I I I
3209             + 1 4 1
3210             + 2 5 2
3211             + 3 6 3",
3212        );
3213        let chunk_l2 = StreamChunk::from_pretty(
3214            "  I I I
3215             + 4 9 4
3216             + 5 10 5",
3217        );
3218        let chunk_r1 = StreamChunk::from_pretty(
3219            "  I I I
3220             + 2 5 1
3221             + 4 9 2
3222             + 6 9 3",
3223        );
3224        let chunk_r2 = StreamChunk::from_pretty(
3225            "  I I I
3226             + 1 4 4
3227             + 3 6 5",
3228        );
3229
3230        let (mut tx_l, mut tx_r, mut hash_join) =
3231            create_append_only_executor::<{ JoinType::LeftOuter }>(false).await;
3232
3233        // push the init barrier for left and right
3234        tx_l.push_barrier(test_epoch(1), false);
3235        tx_r.push_barrier(test_epoch(1), false);
3236        hash_join.next_unwrap_ready_barrier()?;
3237
3238        // push the 1st left chunk
3239        tx_l.push_chunk(chunk_l1);
3240        let chunk = hash_join.next_unwrap_ready_chunk()?;
3241        assert_eq!(
3242            chunk,
3243            StreamChunk::from_pretty(
3244                " I I I I I I
3245                + 1 4 1 . . .
3246                + 2 5 2 . . .
3247                + 3 6 3 . . ."
3248            )
3249        );
3250
3251        // push the 2nd left chunk
3252        tx_l.push_chunk(chunk_l2);
3253        let chunk = hash_join.next_unwrap_ready_chunk()?;
3254        assert_eq!(
3255            chunk,
3256            StreamChunk::from_pretty(
3257                " I I I I I I
3258                + 4 9 4 . . .
3259                + 5 10 5 . . ."
3260            )
3261        );
3262
3263        // push the 1st right chunk
3264        tx_r.push_chunk(chunk_r1);
3265        let chunk = hash_join.next_unwrap_ready_chunk()?;
3266        assert_eq!(
3267            chunk,
3268            StreamChunk::from_pretty(
3269                " I I I I I I
3270                - 2 5 2 . . .
3271                + 2 5 2 2 5 1
3272                - 4 9 4 . . .
3273                + 4 9 4 4 9 2"
3274            )
3275        );
3276
3277        // push the 2nd right chunk
3278        tx_r.push_chunk(chunk_r2);
3279        let chunk = hash_join.next_unwrap_ready_chunk()?;
3280        assert_eq!(
3281            chunk,
3282            StreamChunk::from_pretty(
3283                " I I I I I I
3284                - 1 4 1 . . .
3285                + 1 4 1 1 4 4
3286                - 3 6 3 . . .
3287                + 3 6 3 3 6 5"
3288            )
3289        );
3290
3291        Ok(())
3292    }
3293
3294    #[tokio::test]
3295    async fn test_streaming_hash_right_join_append_only() -> StreamExecutorResult<()> {
3296        let chunk_l1 = StreamChunk::from_pretty(
3297            "  I I I
3298             + 1 4 1
3299             + 2 5 2
3300             + 3 6 3",
3301        );
3302        let chunk_l2 = StreamChunk::from_pretty(
3303            "  I I I
3304             + 4 9 4
3305             + 5 10 5",
3306        );
3307        let chunk_r1 = StreamChunk::from_pretty(
3308            "  I I I
3309             + 2 5 1
3310             + 4 9 2
3311             + 6 9 3",
3312        );
3313        let chunk_r2 = StreamChunk::from_pretty(
3314            "  I I I
3315             + 1 4 4
3316             + 3 6 5
3317             + 7 7 6",
3318        );
3319
3320        let (mut tx_l, mut tx_r, mut hash_join) =
3321            create_append_only_executor::<{ JoinType::RightOuter }>(false).await;
3322
3323        // push the init barrier for left and right
3324        tx_l.push_barrier(test_epoch(1), false);
3325        tx_r.push_barrier(test_epoch(1), false);
3326        hash_join.next_unwrap_ready_barrier()?;
3327
3328        // push the 1st left chunk
3329        tx_l.push_chunk(chunk_l1);
3330        hash_join.next_unwrap_pending();
3331
3332        // push the 2nd left chunk
3333        tx_l.push_chunk(chunk_l2);
3334        hash_join.next_unwrap_pending();
3335
3336        // push the 1st right chunk
3337        tx_r.push_chunk(chunk_r1);
3338        let chunk = hash_join.next_unwrap_ready_chunk()?;
3339        assert_eq!(
3340            chunk,
3341            StreamChunk::from_pretty(
3342                "  I I I I I I
3343                + 2 5 2 2 5 1
3344                + 4 9 4 4 9 2
3345                + . . . 6 9 3"
3346            )
3347        );
3348
3349        // push the 2nd right chunk
3350        tx_r.push_chunk(chunk_r2);
3351        let chunk = hash_join.next_unwrap_ready_chunk()?;
3352        assert_eq!(
3353            chunk,
3354            StreamChunk::from_pretty(
3355                "  I I I I I I
3356                + 1 4 1 1 4 4
3357                + 3 6 3 3 6 5
3358                + . . . 7 7 6"
3359            )
3360        );
3361
3362        Ok(())
3363    }
3364
3365    #[tokio::test]
3366    async fn test_streaming_hash_full_outer_join() -> StreamExecutorResult<()> {
3367        let chunk_l1 = StreamChunk::from_pretty(
3368            "  I I
3369             + 1 4
3370             + 2 5
3371             + 3 6",
3372        );
3373        let chunk_l2 = StreamChunk::from_pretty(
3374            "  I I
3375             + 3 8
3376             - 3 8",
3377        );
3378        let chunk_r1 = StreamChunk::from_pretty(
3379            "  I I
3380             + 2 7
3381             + 4 8
3382             + 6 9",
3383        );
3384        let chunk_r2 = StreamChunk::from_pretty(
3385            "  I  I
3386             + 5 10
3387             - 5 10",
3388        );
3389        let (mut tx_l, mut tx_r, mut hash_join) =
3390            create_classical_executor::<{ JoinType::FullOuter }>(false, false, None).await;
3391
3392        // push the init barrier for left and right
3393        tx_l.push_barrier(test_epoch(1), false);
3394        tx_r.push_barrier(test_epoch(1), false);
3395        hash_join.next_unwrap_ready_barrier()?;
3396
3397        // push the 1st left chunk
3398        tx_l.push_chunk(chunk_l1);
3399        let chunk = hash_join.next_unwrap_ready_chunk()?;
3400        assert_eq!(
3401            chunk,
3402            StreamChunk::from_pretty(
3403                " I I I I
3404                + 1 4 . .
3405                + 2 5 . .
3406                + 3 6 . ."
3407            )
3408        );
3409
3410        // push the 2nd left chunk
3411        tx_l.push_chunk(chunk_l2);
3412        let chunk = hash_join.next_unwrap_ready_chunk()?;
3413        assert_eq!(
3414            chunk,
3415            StreamChunk::from_pretty(
3416                " I I I I
3417                + 3 8 . . D
3418                - 3 8 . . D"
3419            )
3420        );
3421
3422        // push the 1st right chunk
3423        tx_r.push_chunk(chunk_r1);
3424        let chunk = hash_join.next_unwrap_ready_chunk()?;
3425        assert_eq!(
3426            chunk,
3427            StreamChunk::from_pretty(
3428                " I I I I
3429                - 2 5 . .
3430                + 2 5 2 7
3431                + . . 4 8
3432                + . . 6 9"
3433            )
3434        );
3435
3436        // push the 2nd right chunk
3437        tx_r.push_chunk(chunk_r2);
3438        let chunk = hash_join.next_unwrap_ready_chunk()?;
3439        assert_eq!(
3440            chunk,
3441            StreamChunk::from_pretty(
3442                " I I I I
3443                + . . 5 10 D
3444                - . . 5 10 D"
3445            )
3446        );
3447
3448        Ok(())
3449    }
3450
3451    #[tokio::test]
3452    async fn test_streaming_hash_full_outer_join_update() -> StreamExecutorResult<()> {
3453        let (mut tx_l, mut tx_r, mut hash_join) =
3454            create_classical_executor::<{ JoinType::FullOuter }>(false, false, None).await;
3455
3456        // push the init barrier for left and right
3457        tx_l.push_barrier(test_epoch(1), false);
3458        tx_r.push_barrier(test_epoch(1), false);
3459        hash_join.next_unwrap_ready_barrier()?;
3460
3461        tx_l.push_chunk(StreamChunk::from_pretty(
3462            "  I I
3463             + 1 1
3464            ",
3465        ));
3466        let chunk = hash_join.next_unwrap_ready_chunk()?;
3467        assert_eq!(
3468            chunk,
3469            StreamChunk::from_pretty(
3470                " I I I I
3471                + 1 1 . ."
3472            )
3473        );
3474
3475        tx_r.push_chunk(StreamChunk::from_pretty(
3476            "  I I
3477             + 1 1
3478            ",
3479        ));
3480        let chunk = hash_join.next_unwrap_ready_chunk()?;
3481
3482        assert_eq!(
3483            chunk,
3484            StreamChunk::from_pretty(
3485                " I I I I
3486                - 1 1 . .
3487                + 1 1 1 1"
3488            )
3489        );
3490
3491        tx_l.push_chunk(StreamChunk::from_pretty(
3492            "   I I
3493              - 1 1
3494              + 1 2
3495            ",
3496        ));
3497        let chunk = hash_join.next_unwrap_ready_chunk()?;
3498        let chunk = chunk.compact_vis();
3499        assert_eq!(
3500            chunk,
3501            StreamChunk::from_pretty(
3502                " I I I I
3503                - 1 1 1 1
3504                + 1 2 1 1
3505                "
3506            )
3507        );
3508
3509        Ok(())
3510    }
3511
3512    #[tokio::test]
3513    async fn test_streaming_hash_full_outer_join_with_nonequi_condition() -> StreamExecutorResult<()>
3514    {
3515        let chunk_l1 = StreamChunk::from_pretty(
3516            "  I I
3517             + 1 4
3518             + 2 5
3519             + 3 6
3520             + 3 7",
3521        );
3522        let chunk_l2 = StreamChunk::from_pretty(
3523            "  I I
3524             + 3 8
3525             - 3 8
3526             - 1 4", // delete row to cause an empty JoinHashEntry
3527        );
3528        let chunk_r1 = StreamChunk::from_pretty(
3529            "  I I
3530             + 2 6
3531             + 4 8
3532             + 3 4",
3533        );
3534        let chunk_r2 = StreamChunk::from_pretty(
3535            "  I  I
3536             + 5 10
3537             - 5 10
3538             + 1 2",
3539        );
3540        let (mut tx_l, mut tx_r, mut hash_join) =
3541            create_classical_executor::<{ JoinType::FullOuter }>(true, false, None).await;
3542
3543        // push the init barrier for left and right
3544        tx_l.push_barrier(test_epoch(1), false);
3545        tx_r.push_barrier(test_epoch(1), false);
3546        hash_join.next_unwrap_ready_barrier()?;
3547
3548        // push the 1st left chunk
3549        tx_l.push_chunk(chunk_l1);
3550        let chunk = hash_join.next_unwrap_ready_chunk()?;
3551        assert_eq!(
3552            chunk,
3553            StreamChunk::from_pretty(
3554                " I I I I
3555                + 1 4 . .
3556                + 2 5 . .
3557                + 3 6 . .
3558                + 3 7 . ."
3559            )
3560        );
3561
3562        // push the 2nd left chunk
3563        tx_l.push_chunk(chunk_l2);
3564        let chunk = hash_join.next_unwrap_ready_chunk()?;
3565        assert_eq!(
3566            chunk,
3567            StreamChunk::from_pretty(
3568                " I I I I
3569                + 3 8 . . D
3570                - 3 8 . . D
3571                - 1 4 . ."
3572            )
3573        );
3574
3575        // push the 1st right chunk
3576        tx_r.push_chunk(chunk_r1);
3577        let chunk = hash_join.next_unwrap_ready_chunk()?;
3578        assert_eq!(
3579            chunk,
3580            StreamChunk::from_pretty(
3581                " I I I I
3582                - 2 5 . .
3583                + 2 5 2 6
3584                + . . 4 8
3585                + . . 3 4" /* regression test (#2420): 3 4 should be forwarded only once
3586                            * despite matching on eq join on 2
3587                            * entries */
3588            )
3589        );
3590
3591        // push the 2nd right chunk
3592        tx_r.push_chunk(chunk_r2);
3593        let chunk = hash_join.next_unwrap_ready_chunk()?;
3594        assert_eq!(
3595            chunk,
3596            StreamChunk::from_pretty(
3597                " I I I I
3598                + . . 5 10 D
3599                - . . 5 10 D
3600                + . . 1 2" /* regression test (#2420): 1 2 forwarded even if matches on an empty
3601                            * join entry */
3602            )
3603        );
3604
3605        Ok(())
3606    }
3607
3608    #[tokio::test]
3609    async fn test_streaming_hash_inner_join_with_nonequi_condition() -> StreamExecutorResult<()> {
3610        let chunk_l1 = StreamChunk::from_pretty(
3611            "  I I
3612             + 1 4
3613             + 2 10
3614             + 3 6",
3615        );
3616        let chunk_l2 = StreamChunk::from_pretty(
3617            "  I I
3618             + 3 8
3619             - 3 8",
3620        );
3621        let chunk_r1 = StreamChunk::from_pretty(
3622            "  I I
3623             + 2 7
3624             + 4 8
3625             + 6 9",
3626        );
3627        let chunk_r2 = StreamChunk::from_pretty(
3628            "  I  I
3629             + 3 10
3630             + 6 11",
3631        );
3632        let (mut tx_l, mut tx_r, mut hash_join) =
3633            create_classical_executor::<{ JoinType::Inner }>(true, false, None).await;
3634
3635        // push the init barrier for left and right
3636        tx_l.push_barrier(test_epoch(1), false);
3637        tx_r.push_barrier(test_epoch(1), false);
3638        hash_join.next_unwrap_ready_barrier()?;
3639
3640        // push the 1st left chunk
3641        tx_l.push_chunk(chunk_l1);
3642        hash_join.next_unwrap_pending();
3643
3644        // push the 2nd left chunk
3645        tx_l.push_chunk(chunk_l2);
3646        hash_join.next_unwrap_pending();
3647
3648        // push the 1st right chunk
3649        tx_r.push_chunk(chunk_r1);
3650        hash_join.next_unwrap_pending();
3651
3652        // push the 2nd right chunk
3653        tx_r.push_chunk(chunk_r2);
3654        let chunk = hash_join.next_unwrap_ready_chunk()?;
3655        assert_eq!(
3656            chunk,
3657            StreamChunk::from_pretty(
3658                " I I I I
3659                + 3 6 3 10"
3660            )
3661        );
3662
3663        Ok(())
3664    }
3665
3666    #[tokio::test]
3667    async fn test_streaming_hash_join_watermark() -> StreamExecutorResult<()> {
3668        let (mut tx_l, mut tx_r, mut hash_join) =
3669            create_classical_executor::<{ JoinType::Inner }>(true, false, None).await;
3670
3671        // push the init barrier for left and right
3672        tx_l.push_barrier(test_epoch(1), false);
3673        tx_r.push_barrier(test_epoch(1), false);
3674        hash_join.next_unwrap_ready_barrier()?;
3675
3676        tx_l.push_int64_watermark(0, 100);
3677
3678        tx_l.push_int64_watermark(0, 200);
3679
3680        tx_l.push_barrier(test_epoch(2), false);
3681        tx_r.push_barrier(test_epoch(2), false);
3682        hash_join.next_unwrap_ready_barrier()?;
3683
3684        tx_r.push_int64_watermark(0, 50);
3685
3686        let w1 = hash_join.next().await.unwrap().unwrap();
3687        let w1 = w1.as_watermark().unwrap();
3688
3689        let w2 = hash_join.next().await.unwrap().unwrap();
3690        let w2 = w2.as_watermark().unwrap();
3691
3692        tx_r.push_int64_watermark(0, 100);
3693
3694        let w3 = hash_join.next().await.unwrap().unwrap();
3695        let w3 = w3.as_watermark().unwrap();
3696
3697        let w4 = hash_join.next().await.unwrap().unwrap();
3698        let w4 = w4.as_watermark().unwrap();
3699
3700        assert_eq!(
3701            w1,
3702            &Watermark {
3703                col_idx: 2,
3704                data_type: DataType::Int64,
3705                val: ScalarImpl::Int64(50)
3706            }
3707        );
3708
3709        assert_eq!(
3710            w2,
3711            &Watermark {
3712                col_idx: 0,
3713                data_type: DataType::Int64,
3714                val: ScalarImpl::Int64(50)
3715            }
3716        );
3717
3718        assert_eq!(
3719            w3,
3720            &Watermark {
3721                col_idx: 2,
3722                data_type: DataType::Int64,
3723                val: ScalarImpl::Int64(100)
3724            }
3725        );
3726
3727        assert_eq!(
3728            w4,
3729            &Watermark {
3730                col_idx: 0,
3731                data_type: DataType::Int64,
3732                val: ScalarImpl::Int64(100)
3733            }
3734        );
3735
3736        Ok(())
3737    }
3738
3739    async fn create_executor_with_evict_interval<const T: JoinTypePrimitive>(
3740        evict_interval: u32,
3741    ) -> (MessageSender, MessageSender, BoxedMessageStream) {
3742        let schema = Schema {
3743            fields: vec![
3744                Field::unnamed(DataType::Int64), // join key
3745                Field::unnamed(DataType::Int64),
3746            ],
3747        };
3748        let (tx_l, source_l) = MockSource::channel();
3749        let source_l = source_l.into_executor(schema.clone(), vec![1]);
3750        let (tx_r, source_r) = MockSource::channel();
3751        let source_r = source_r.into_executor(schema, vec![1]);
3752        let params_l = JoinParams::new(vec![0], vec![1]);
3753        let params_r = JoinParams::new(vec![0], vec![1]);
3754
3755        let mem_state = MemoryStateStore::new();
3756
3757        let (state_l, degree_state_l) = create_in_memory_state_table(
3758            mem_state.clone(),
3759            &[DataType::Int64, DataType::Int64],
3760            &[OrderType::ascending(), OrderType::ascending()],
3761            &[0, 1],
3762            0,
3763        )
3764        .await;
3765
3766        let (state_r, degree_state_r) = create_in_memory_state_table(
3767            mem_state,
3768            &[DataType::Int64, DataType::Int64],
3769            &[OrderType::ascending(), OrderType::ascending()],
3770            &[0, 1],
3771            2,
3772        )
3773        .await;
3774
3775        let schema = match T {
3776            JoinType::LeftSemi | JoinType::LeftAnti => source_l.schema().clone(),
3777            JoinType::RightSemi | JoinType::RightAnti => source_r.schema().clone(),
3778            _ => [source_l.schema().fields(), source_r.schema().fields()]
3779                .concat()
3780                .into_iter()
3781                .collect(),
3782        };
3783        let schema_len = schema.len();
3784        let info = ExecutorInfo::for_test(schema, vec![1], "HashJoinExecutor".to_owned(), 0);
3785
3786        let mut streaming_config = StreamingConfig::default();
3787        streaming_config.developer.join_hash_map_evict_interval_rows = evict_interval;
3788
3789        let executor = HashJoinExecutor::<Key64, MemoryStateStore, T, MemoryEncoding>::new(
3790            ActorContext::for_test_with_config(123, streaming_config),
3791            info,
3792            source_l,
3793            source_r,
3794            params_l,
3795            params_r,
3796            vec![false],
3797            (0..schema_len).collect_vec(),
3798            None,
3799            vec![],
3800            state_l,
3801            degree_state_l,
3802            state_r,
3803            degree_state_r,
3804            Arc::new(AtomicU64::new(0)),
3805            false,
3806            Arc::new(StreamingMetrics::unused()),
3807            1024,
3808            2048,
3809            vec![(0, true)],
3810        );
3811        (tx_l, tx_r, executor.boxed().execute())
3812    }
3813
3814    /// Tests that a hash join executor with `join_hash_map_evict_interval_rows = 0`
3815    /// (periodic eviction disabled) produces correct results.
3816    #[tokio::test]
3817    async fn test_hash_join_evict_interval_disabled() -> StreamExecutorResult<()> {
3818        let chunk_l = StreamChunk::from_pretty(
3819            "  I I
3820             + 1 4
3821             + 2 5
3822             + 3 6",
3823        );
3824        let chunk_r = StreamChunk::from_pretty(
3825            "  I I
3826             + 2 7
3827             + 3 8",
3828        );
3829
3830        // 0 disables periodic eviction
3831        let (mut tx_l, mut tx_r, mut hash_join) =
3832            create_executor_with_evict_interval::<{ JoinType::Inner }>(0).await;
3833
3834        tx_l.push_barrier(test_epoch(1), false);
3835        tx_r.push_barrier(test_epoch(1), false);
3836        hash_join.next_unwrap_ready_barrier()?;
3837
3838        tx_l.push_chunk(chunk_l);
3839        hash_join.next_unwrap_pending();
3840
3841        tx_r.push_chunk(chunk_r);
3842        let chunk = hash_join.next_unwrap_ready_chunk()?;
3843        assert_eq!(
3844            chunk,
3845            StreamChunk::from_pretty(
3846                " I I I I
3847                + 2 5 2 7
3848                + 3 6 3 8"
3849            )
3850        );
3851
3852        Ok(())
3853    }
3854
3855    /// Tests that a hash join executor with `join_hash_map_evict_interval_rows = 1`
3856    /// (evict after every row) produces correct results.
3857    #[tokio::test]
3858    async fn test_hash_join_evict_interval_one() -> StreamExecutorResult<()> {
3859        let chunk_l = StreamChunk::from_pretty(
3860            "  I I
3861             + 1 4
3862             + 2 5
3863             + 3 6",
3864        );
3865        let chunk_r = StreamChunk::from_pretty(
3866            "  I I
3867             + 2 7
3868             + 3 8",
3869        );
3870
3871        // evict after every row
3872        let (mut tx_l, mut tx_r, mut hash_join) =
3873            create_executor_with_evict_interval::<{ JoinType::Inner }>(1).await;
3874
3875        tx_l.push_barrier(test_epoch(1), false);
3876        tx_r.push_barrier(test_epoch(1), false);
3877        hash_join.next_unwrap_ready_barrier()?;
3878
3879        tx_l.push_chunk(chunk_l);
3880        hash_join.next_unwrap_pending();
3881
3882        tx_r.push_chunk(chunk_r);
3883        let chunk = hash_join.next_unwrap_ready_chunk()?;
3884        assert_eq!(
3885            chunk,
3886            StreamChunk::from_pretty(
3887                " I I I I
3888                + 2 5 2 7
3889                + 3 6 3 8"
3890            )
3891        );
3892
3893        Ok(())
3894    }
3895
3896    /// Tests that a hash join executor with a custom `join_hash_map_evict_interval_rows`
3897    /// value produces correct results.
3898    #[tokio::test]
3899    async fn test_hash_join_evict_interval_custom() -> StreamExecutorResult<()> {
3900        let chunk_l = StreamChunk::from_pretty(
3901            "  I I
3902             + 1 4
3903             + 2 5
3904             + 3 6
3905             + 4 7
3906             + 5 8",
3907        );
3908        let chunk_r = StreamChunk::from_pretty(
3909            "  I I
3910             + 1 9
3911             + 3 10
3912             + 5 11",
3913        );
3914
3915        // evict every 2 rows
3916        let (mut tx_l, mut tx_r, mut hash_join) =
3917            create_executor_with_evict_interval::<{ JoinType::Inner }>(2).await;
3918
3919        tx_l.push_barrier(test_epoch(1), false);
3920        tx_r.push_barrier(test_epoch(1), false);
3921        hash_join.next_unwrap_ready_barrier()?;
3922
3923        tx_l.push_chunk(chunk_l);
3924        hash_join.next_unwrap_pending();
3925
3926        tx_r.push_chunk(chunk_r);
3927        let chunk = hash_join.next_unwrap_ready_chunk()?;
3928        assert_eq!(
3929            chunk,
3930            StreamChunk::from_pretty(
3931                " I I I I
3932                + 1 4 1 9
3933                + 3 6 3 10
3934                + 5 8 5 11"
3935            )
3936        );
3937
3938        Ok(())
3939    }
3940}