1use 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#[derive(Debug, Clone)]
53pub struct InequalityPairInfo {
54 pub left_idx: usize,
56 pub right_idx: usize,
58 pub clean_left_state: bool,
60 pub clean_right_state: bool,
62 pub op: InequalityType,
64}
65
66impl InequalityPairInfo {
67 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 pub join_key_indices: Vec<usize>,
79 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 ht: JoinHashMap<K, S, E>,
95 join_key_indices: Vec<usize>,
97 all_data_types: Vec<DataType>,
99 start_pos: usize,
101 i2o_mapping: Vec<(usize, usize)>,
103 i2o_mapping_indexed: MultiMap<usize, usize>,
104 input2inequality_index: Vec<Vec<(usize, bool)>>,
112 non_null_fields: Vec<usize>,
114 state_clean_columns: Vec<(usize, usize)>,
117 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 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 }
150
151 pub async fn init(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
152 self.ht.init(epoch).await
153 }
154}
155
156pub struct HashJoinExecutor<K: HashKey, S: StateStore, const T: JoinTypePrimitive, E: JoinEncoding>
159{
160 ctx: ActorContextRef,
161 info: ExecutorInfo,
162
163 input_l: Option<Executor>,
165 input_r: Option<Executor>,
167 actual_output_data_types: Vec<DataType>,
169 side_l: JoinSide<K, S, E>,
171 side_r: JoinSide<K, S, E>,
173 cond: Option<NonStrictExpression>,
175 inequality_pairs: Vec<(Vec<usize>, InequalityPairInfo)>,
178 inequality_watermarks: Vec<Option<Watermark>>,
182 watermark_indices_in_jk: Vec<(usize, bool)>,
185
186 append_only_optimize: bool,
188
189 metrics: Arc<StreamingMetrics>,
190 chunk_size: usize,
192 cnt_rows_received: u32,
194
195 watermark_buffers: BTreeMap<usize, BufferedWatermarks<SideTypePrimitive>>,
197
198 high_join_amplification_threshold: usize,
200
201 entry_state_max_rows: usize,
203 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 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 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 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 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 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 let output_indices = if pair.left_side_is_larger() {
446 l2o_indexed
448 .get_vec(&pair.left_idx)
449 .cloned()
450 .unwrap_or_default()
451 } else {
452 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 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 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 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 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 barrier_join_match_duration_ns
747 .inc_by(barrier_start_time.elapsed().as_nanos() as u64);
748 yield Message::Barrier(barrier);
749
750 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 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 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 self.side_l.ht.try_flush().await?;
796 self.side_r.ht.try_flush().await?;
797 Ok(())
798 }
799
800 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 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 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 for output_idx in output_indices {
890 watermarks_to_emit.push(selected_watermark.clone().with_idx(*output_idx));
891 }
892 self.inequality_watermarks[*inequality_index] =
894 Some(selected_watermark.clone());
895
896 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 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 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 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 if let Some(chunk) = hashjoin_chunk_builder.take() {
1073 yield chunk;
1074 }
1075 }
1076
1077 #[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 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 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 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 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 if cache_hit || entry_state_count <= entry_state_max_rows {
1231 side_match.ht.update_state(key, entry_state);
1232 }
1233
1234 for matched_row in matched_rows_to_clean {
1236 side_match.ht.delete_row_in_mem(key, &matched_row.row)?;
1238 }
1239
1240 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 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, RO: Row, 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 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_row_degree += 1;
1307 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 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 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 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 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 append_only_optimize {
1360 assert_matches!(JOIN_OP, JoinOp::Insert);
1361 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 #[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 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 degree_table_column_descs.push(ColumnDesc::unnamed(
1504 ColumnId::new(pk_indices.len() as i32),
1505 DataType::Int64,
1506 ));
1507 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(°ree_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), 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 let l_degree_ineq_type = inequality_pairs
1562 .iter()
1563 .find(|pair| pair.clean_left_state)
1564 .map(|_| DataType::Int64); let r_degree_ineq_type = inequality_pairs
1566 .iter()
1567 .find(|pair| pair.clean_right_state)
1568 .map(|_| DataType::Int64); 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 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 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, clean_right_state: false,
1770 op: InequalityType::GreaterThanOrEqual,
1771 }],
1772 )
1773 .await;
1774
1775 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 tx_l.push_chunk(chunk_l1);
1782 hash_join.next_unwrap_pending();
1783
1784 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 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 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 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 tx_l.push_chunk(chunk_l1);
1864 hash_join.next_unwrap_pending();
1865
1866 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 tx_l.push_chunk(chunk_l2);
1873 hash_join.next_unwrap_pending();
1874
1875 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 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 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 tx_l.push_chunk(chunk_l1);
1934 hash_join.next_unwrap_pending();
1935
1936 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 tx_l.push_chunk(chunk_l2);
1943 hash_join.next_unwrap_pending();
1944
1945 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 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 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 tx_l.push_chunk(chunk_l1);
2016 hash_join.next_unwrap_pending();
2017
2018 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 tx_l.push_chunk(chunk_l2);
2025 hash_join.next_unwrap_pending();
2026
2027 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 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 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 tx_r.push_chunk(chunk_r3);
2063 hash_join.next_unwrap_pending();
2064
2065 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 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 tx_l.push_chunk(chunk_l1);
2126 hash_join.next_unwrap_pending();
2127
2128 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 tx_l.push_chunk(chunk_l2);
2135 hash_join.next_unwrap_pending();
2136
2137 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 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 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 tx_r.push_chunk(chunk_r3);
2173 hash_join.next_unwrap_pending();
2174
2175 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 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 tx_l.push_chunk(chunk_l1);
2225 hash_join.next_unwrap_pending();
2226
2227 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 tx_l.push_chunk(chunk_l2);
2234 hash_join.next_unwrap_pending();
2235
2236 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 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 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 tx_l.push_chunk(chunk_l1);
2298 hash_join.next_unwrap_pending();
2299
2300 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 tx_l.push_chunk(chunk_l2);
2307 hash_join.next_unwrap_pending();
2308
2309 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 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 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 tx_l.push_chunk(chunk_l1);
2371 hash_join.next_unwrap_pending();
2372
2373 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 tx_l.push_chunk(chunk_l2);
2380 hash_join.next_unwrap_pending();
2381
2382 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 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 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 tx_r.push_chunk(chunk_r1);
2455 hash_join.next_unwrap_pending();
2456
2457 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 tx_r.push_chunk(chunk_r2);
2464 hash_join.next_unwrap_pending();
2465
2466 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 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 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 tx_l.push_chunk(chunk_l3);
2502 hash_join.next_unwrap_pending();
2503
2504 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 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 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 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 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 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 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 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 tx_r.push_chunk(chunk_r3);
2632 hash_join.next_unwrap_pending();
2633
2634 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 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 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 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 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 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 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 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 tx_l.push_chunk(chunk_l3);
2762 hash_join.next_unwrap_pending();
2763
2764 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 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 tx_l.push_chunk(chunk_l1);
2813 hash_join.next_unwrap_pending();
2814
2815 tx_l.push_barrier(test_epoch(2), false);
2817
2818 tx_l.push_chunk(chunk_l2);
2820
2821 tx_r.push_chunk(chunk_r1);
2823
2824 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 tx_r.push_barrier(test_epoch(2), false);
2836
2837 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 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 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 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 tx_l.push_chunk(chunk_l1);
2908 hash_join.next_unwrap_pending();
2909
2910 tx_l.push_barrier(test_epoch(2), false);
2912
2913 tx_l.push_chunk(chunk_l2);
2915
2916 tx_r.push_chunk(chunk_r1);
2918
2919 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 tx_r.push_barrier(test_epoch(2), false);
2931
2932 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 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 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 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 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 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 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 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 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 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 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 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 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 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 tx_l.push_chunk(chunk_l1);
3171 hash_join.next_unwrap_pending();
3172
3173 tx_l.push_chunk(chunk_l2);
3175 hash_join.next_unwrap_pending();
3176
3177 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 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 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 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 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 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 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 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 tx_l.push_chunk(chunk_l1);
3330 hash_join.next_unwrap_pending();
3331
3332 tx_l.push_chunk(chunk_l2);
3334 hash_join.next_unwrap_pending();
3335
3336 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 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 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 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 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 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 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 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", );
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 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 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 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 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" )
3589 );
3590
3591 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" )
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 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 tx_l.push_chunk(chunk_l1);
3642 hash_join.next_unwrap_pending();
3643
3644 tx_l.push_chunk(chunk_l2);
3646 hash_join.next_unwrap_pending();
3647
3648 tx_r.push_chunk(chunk_r1);
3650 hash_join.next_unwrap_pending();
3651
3652 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 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), 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 #[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 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 #[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 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 #[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 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}