Skip to main content

risingwave_stream/executor/
asof_join.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! `AsOf` join executor with optional LRU cache.
16//!
17//! When `use_cache` is false, the executor directly queries the state table.
18//! When `use_cache` is true, the `AsOfJoinHashMap` maintains an LRU cache.
19//! Controlled by the session variable `streaming_asof_join_use_cache`.
20
21use std::cmp::Ordering;
22use std::collections::BTreeMap;
23use std::ops::Bound;
24use std::time::Duration;
25
26use either::Either;
27use itertools::Itertools;
28use multimap::MultiMap;
29use risingwave_common::array::Op;
30use risingwave_common::metrics::LabelGuardedHistogram;
31use risingwave_common::row::RowExt;
32use risingwave_common::util::epoch::EpochPair;
33use risingwave_common::util::sort_util::cmp_rows_ascending;
34use tokio::time::Instant;
35
36use super::barrier_align::*;
37use super::join::asof_join::*;
38use super::join::builder::JoinStreamChunkBuilder;
39use super::join::row::JoinRow;
40use super::join::*;
41use super::watermark::*;
42use crate::executor::join::builder::JoinChunkBuilder;
43use crate::executor::prelude::*;
44
45pub struct JoinParams {
46    /// Indices of the join keys
47    pub join_key_indices: Vec<usize>,
48    /// Indices of the input pk after dedup
49    pub deduped_pk_indices: Vec<usize>,
50}
51
52impl JoinParams {
53    pub fn new(join_key_indices: Vec<usize>, deduped_pk_indices: Vec<usize>) -> Self {
54        Self {
55            join_key_indices,
56            deduped_pk_indices,
57        }
58    }
59}
60
61struct JoinSide<S: StateStore, E: AsOfRowEncoding> {
62    /// Store all data from a one side stream
63    ht: AsOfJoinHashMap<S, E>,
64    /// Indices of the join key columns
65    join_key_indices: Vec<usize>,
66    /// The data type of all columns without degree.
67    all_data_types: Vec<DataType>,
68    /// The mapping from input indices of a side to output columns.
69    i2o_mapping: Vec<(usize, usize)>,
70    i2o_mapping_indexed: MultiMap<usize, usize>,
71    /// The index of the inequality column.
72    inequal_key_idx: usize,
73}
74
75impl<S: StateStore, E: AsOfRowEncoding> std::fmt::Debug for JoinSide<S, E> {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("JoinSide")
78            .field("join_key_indices", &self.join_key_indices)
79            .field("col_types", &self.all_data_types)
80            .field("i2o_mapping", &self.i2o_mapping)
81            .finish()
82    }
83}
84
85impl<S: StateStore, E: AsOfRowEncoding> JoinSide<S, E> {
86    pub async fn init(&mut self, epoch: EpochPair) -> StreamExecutorResult<()> {
87        self.ht.init(epoch).await
88    }
89}
90
91/// `AsOfJoinExecutor` for streaming as-of joins.
92///
93/// This executor uses `AsOfJoinHashMap` to support both execution modes:
94/// it can either maintain an LRU cache or query the state table directly,
95/// depending on the `use_cache` configuration.
96pub struct AsOfJoinExecutor<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> {
97    ctx: ActorContextRef,
98    info: ExecutorInfo,
99
100    /// Left input executor
101    input_l: Option<Executor>,
102    /// Right input executor
103    input_r: Option<Executor>,
104    /// The data types of the formed new columns
105    actual_output_data_types: Vec<DataType>,
106    /// The parameters of the left join executor
107    side_l: JoinSide<S, E>,
108    /// The parameters of the right join executor
109    side_r: JoinSide<S, E>,
110
111    /// Whether nulls are considered equal for each join key column
112    null_safe: Vec<bool>,
113
114    metrics: Arc<StreamingMetrics>,
115    /// The maximum size of the chunk produced by executor at a time
116    chunk_size: usize,
117    /// watermark column index -> `BufferedWatermarks`
118    watermark_buffers: BTreeMap<usize, BufferedWatermarks<SideTypePrimitive>>,
119    /// `AsOf` join description
120    asof_desc: AsOfDesc,
121    /// Row counter for periodic cache eviction
122    cnt_rows_received: u32,
123    /// Number of processed rows between periodic manual evictions of the join cache.
124    join_cache_evict_interval_rows: u32,
125    /// Threshold for logging high join amplification warnings.
126    high_join_amplification_threshold: usize,
127}
128
129impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> std::fmt::Debug
130    for AsOfJoinExecutor<S, T, E>
131{
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_struct("AsOfJoinExecutor")
134            .field("join_type", &T)
135            .field("input_left", &self.input_l.as_ref().unwrap().identity())
136            .field("input_right", &self.input_r.as_ref().unwrap().identity())
137            .field("side_l", &self.side_l)
138            .field("side_r", &self.side_r)
139            .field("stream_key", &self.info.stream_key)
140            .field("schema", &self.info.schema)
141            .field("actual_output_data_types", &self.actual_output_data_types)
142            .finish()
143    }
144}
145
146impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> Execute
147    for AsOfJoinExecutor<S, T, E>
148{
149    fn execute(self: Box<Self>) -> BoxedMessageStream {
150        self.into_stream().boxed()
151    }
152}
153
154struct EqJoinArgs<'a, S: StateStore, E: AsOfRowEncoding> {
155    ctx: &'a ActorContextRef,
156    side_l: &'a mut JoinSide<S, E>,
157    side_r: &'a mut JoinSide<S, E>,
158    null_safe: &'a [bool],
159    asof_desc: &'a AsOfDesc,
160    actual_output_data_types: &'a [DataType],
161    chunk: StreamChunk,
162    chunk_size: usize,
163    cnt_rows_received: &'a mut u32,
164    join_cache_evict_interval_rows: u32,
165    high_join_amplification_threshold: usize,
166    /// Bound at executor scope so the guard isn't dropped between chunks (which resets the series).
167    join_matched_join_keys: &'a LabelGuardedHistogram,
168}
169
170impl<S: StateStore, const T: AsOfJoinTypePrimitive, E: AsOfRowEncoding> AsOfJoinExecutor<S, T, E> {
171    #[expect(clippy::too_many_arguments)]
172    pub fn new(
173        ctx: ActorContextRef,
174        info: ExecutorInfo,
175        input_l: Executor,
176        input_r: Executor,
177        params_l: JoinParams,
178        params_r: JoinParams,
179        null_safe: Vec<bool>,
180        output_indices: Vec<usize>,
181        state_table_l: StateTable<S>,
182        state_table_r: StateTable<S>,
183        watermark_epoch: AtomicU64Ref,
184        metrics: Arc<StreamingMetrics>,
185        chunk_size: usize,
186        asof_desc: AsOfDesc,
187        use_cache: bool,
188        high_join_amplification_threshold: usize,
189    ) -> Self {
190        let join_cache_evict_interval_rows = ctx
191            .config
192            .developer
193            .join_hash_map_evict_interval_rows
194            .max(1);
195        let cache_epoch = if use_cache {
196            Some(watermark_epoch)
197        } else {
198            None
199        };
200        let schema_fields = [
201            input_l.schema().fields.clone(),
202            input_r.schema().fields.clone(),
203        ]
204        .concat();
205
206        let original_output_data_types = schema_fields
207            .iter()
208            .map(|field| field.data_type())
209            .collect_vec();
210        let actual_output_data_types = output_indices
211            .iter()
212            .map(|&idx| original_output_data_types[idx].clone())
213            .collect_vec();
214
215        let state_all_data_types_l = input_l.schema().data_types();
216        let state_all_data_types_r = input_r.schema().data_types();
217
218        let state_join_key_indices_l = params_l.join_key_indices;
219        let state_join_key_indices_r = params_r.join_key_indices;
220
221        let join_key_data_types_l = state_join_key_indices_l
222            .iter()
223            .map(|idx| state_all_data_types_l[*idx].clone())
224            .collect_vec();
225
226        let join_key_data_types_r = state_join_key_indices_r
227            .iter()
228            .map(|idx| state_all_data_types_r[*idx].clone())
229            .collect_vec();
230
231        assert_eq!(join_key_data_types_l, join_key_data_types_r);
232
233        let (left_to_output, right_to_output) = {
234            let (left_len, right_len) = if is_left_semi_or_anti(T) {
235                (state_all_data_types_l.len(), 0usize)
236            } else if is_right_semi_or_anti(T) {
237                (0usize, state_all_data_types_r.len())
238            } else {
239                (state_all_data_types_l.len(), state_all_data_types_r.len())
240            };
241            JoinStreamChunkBuilder::get_i2o_mapping(&output_indices, left_len, right_len)
242        };
243
244        let l2o_indexed = MultiMap::from_iter(left_to_output.iter().copied());
245        let r2o_indexed = MultiMap::from_iter(right_to_output.iter().copied());
246
247        let watermark_buffers = BTreeMap::new();
248
249        let inequal_key_idx_l = asof_desc.left_idx;
250        let inequal_key_idx_r = asof_desc.right_idx;
251
252        Self {
253            ctx: ctx.clone(),
254            info,
255            input_l: Some(input_l),
256            input_r: Some(input_r),
257            actual_output_data_types,
258            null_safe,
259            side_l: JoinSide {
260                ht: AsOfJoinHashMap::new(
261                    state_join_key_indices_l.clone(),
262                    state_table_l,
263                    params_l.deduped_pk_indices,
264                    inequal_key_idx_l,
265                    state_all_data_types_l.clone(),
266                    cache_epoch.clone(),
267                    metrics.clone(),
268                    ctx.id,
269                    ctx.fragment_id,
270                    "left",
271                ),
272                join_key_indices: state_join_key_indices_l,
273                all_data_types: state_all_data_types_l,
274                i2o_mapping: left_to_output,
275                i2o_mapping_indexed: l2o_indexed,
276                inequal_key_idx: inequal_key_idx_l,
277            },
278            side_r: JoinSide {
279                ht: AsOfJoinHashMap::new(
280                    state_join_key_indices_r.clone(),
281                    state_table_r,
282                    params_r.deduped_pk_indices,
283                    inequal_key_idx_r,
284                    state_all_data_types_r.clone(),
285                    cache_epoch,
286                    metrics.clone(),
287                    ctx.id,
288                    ctx.fragment_id,
289                    "right",
290                ),
291                join_key_indices: state_join_key_indices_r,
292                all_data_types: state_all_data_types_r,
293                i2o_mapping: right_to_output,
294                i2o_mapping_indexed: r2o_indexed,
295                inequal_key_idx: inequal_key_idx_r,
296            },
297            metrics,
298            chunk_size,
299            watermark_buffers,
300            asof_desc,
301            cnt_rows_received: 0,
302            join_cache_evict_interval_rows,
303            high_join_amplification_threshold,
304        }
305    }
306
307    /// Periodically evict the LRU cache to prevent memory buildup between barriers.
308    fn evict_cache(
309        side_l: &mut JoinSide<S, E>,
310        side_r: &mut JoinSide<S, E>,
311        cnt_rows_received: &mut u32,
312        join_cache_evict_interval_rows: u32,
313    ) {
314        *cnt_rows_received += 1;
315        if *cnt_rows_received >= join_cache_evict_interval_rows {
316            side_l.ht.evict_cache();
317            side_r.ht.evict_cache();
318            *cnt_rows_received = 0;
319        }
320    }
321
322    #[try_stream(ok = Message, error = StreamExecutorError)]
323    async fn into_stream(mut self) {
324        let input_l = self.input_l.take().unwrap();
325        let input_r = self.input_r.take().unwrap();
326        let aligned_stream = barrier_align(
327            input_l.execute(),
328            input_r.execute(),
329            self.ctx.id,
330            self.ctx.fragment_id,
331            self.metrics.clone(),
332            "Join",
333        );
334        pin_mut!(aligned_stream);
335        let actor_id = self.ctx.id;
336
337        let barrier = expect_first_barrier_from_aligned_stream(&mut aligned_stream).await?;
338        let first_epoch = barrier.epoch;
339        yield Message::Barrier(barrier);
340        self.side_l.init(first_epoch).await?;
341        self.side_r.init(first_epoch).await?;
342
343        let actor_id_str = self.ctx.id.to_string();
344        let fragment_id_str = self.ctx.fragment_id.to_string();
345
346        let join_actor_input_waiting_duration_ns = self
347            .metrics
348            .join_actor_input_waiting_duration_ns
349            .with_guarded_label_values(&[&actor_id_str, &fragment_id_str]);
350        let left_join_match_duration_ns = self
351            .metrics
352            .join_match_duration_ns
353            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
354        let right_join_match_duration_ns = self
355            .metrics
356            .join_match_duration_ns
357            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
358
359        let barrier_join_match_duration_ns = self
360            .metrics
361            .join_match_duration_ns
362            .with_guarded_label_values(&[
363                actor_id_str.as_str(),
364                fragment_id_str.as_str(),
365                "barrier",
366            ]);
367
368        let left_join_cached_entry_count = self
369            .metrics
370            .join_cached_entry_count
371            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "left"]);
372
373        let right_join_cached_entry_count = self
374            .metrics
375            .join_cached_entry_count
376            .with_guarded_label_values(&[actor_id_str.as_str(), fragment_id_str.as_str(), "right"]);
377
378        // Bind at executor scope: a per-chunk guard would be dropped between chunks and reset the series.
379        let left_table_id_str = self.side_l.ht.table_id().to_string();
380        let right_table_id_str = self.side_r.ht.table_id().to_string();
381        let left_join_matched_join_keys = self
382            .metrics
383            .join_matched_join_keys
384            .with_guarded_label_values(&[
385                actor_id_str.as_str(),
386                fragment_id_str.as_str(),
387                left_table_id_str.as_str(),
388            ]);
389        let right_join_matched_join_keys = self
390            .metrics
391            .join_matched_join_keys
392            .with_guarded_label_values(&[
393                actor_id_str.as_str(),
394                fragment_id_str.as_str(),
395                right_table_id_str.as_str(),
396            ]);
397
398        let mut start_time = Instant::now();
399
400        while let Some(msg) = aligned_stream
401            .next()
402            .instrument_await("hash_join_barrier_align")
403            .await
404        {
405            join_actor_input_waiting_duration_ns.inc_by(start_time.elapsed().as_nanos() as u64);
406            match msg? {
407                AlignedMessage::WatermarkLeft(watermark) => {
408                    for watermark_to_emit in self.handle_watermark(SideType::Left, watermark)? {
409                        yield Message::Watermark(watermark_to_emit);
410                    }
411                }
412                AlignedMessage::WatermarkRight(watermark) => {
413                    for watermark_to_emit in self.handle_watermark(SideType::Right, watermark)? {
414                        yield Message::Watermark(watermark_to_emit);
415                    }
416                }
417                AlignedMessage::Left(chunk) => {
418                    let mut left_time = Duration::from_nanos(0);
419                    let mut left_start_time = Instant::now();
420                    #[for_await]
421                    for chunk in Self::eq_join_left(EqJoinArgs {
422                        ctx: &self.ctx,
423                        side_l: &mut self.side_l,
424                        side_r: &mut self.side_r,
425                        null_safe: &self.null_safe,
426                        asof_desc: &self.asof_desc,
427                        actual_output_data_types: &self.actual_output_data_types,
428                        chunk,
429                        chunk_size: self.chunk_size,
430                        cnt_rows_received: &mut self.cnt_rows_received,
431                        join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
432                        high_join_amplification_threshold: self.high_join_amplification_threshold,
433                        join_matched_join_keys: &left_join_matched_join_keys,
434                    }) {
435                        left_time += left_start_time.elapsed();
436                        yield Message::Chunk(chunk?);
437                        left_start_time = Instant::now();
438                    }
439                    left_time += left_start_time.elapsed();
440                    left_join_match_duration_ns.inc_by(left_time.as_nanos() as u64);
441                    self.try_flush_data().await?;
442                }
443                AlignedMessage::Right(chunk) => {
444                    let mut right_time = Duration::from_nanos(0);
445                    let mut right_start_time = Instant::now();
446                    #[for_await]
447                    for chunk in Self::eq_join_right(EqJoinArgs {
448                        ctx: &self.ctx,
449                        side_l: &mut self.side_l,
450                        side_r: &mut self.side_r,
451                        null_safe: &self.null_safe,
452                        asof_desc: &self.asof_desc,
453                        actual_output_data_types: &self.actual_output_data_types,
454                        chunk,
455                        chunk_size: self.chunk_size,
456                        cnt_rows_received: &mut self.cnt_rows_received,
457                        join_cache_evict_interval_rows: self.join_cache_evict_interval_rows,
458                        high_join_amplification_threshold: self.high_join_amplification_threshold,
459                        join_matched_join_keys: &right_join_matched_join_keys,
460                    }) {
461                        right_time += right_start_time.elapsed();
462                        yield Message::Chunk(chunk?);
463                        right_start_time = Instant::now();
464                    }
465                    right_time += right_start_time.elapsed();
466                    right_join_match_duration_ns.inc_by(right_time.as_nanos() as u64);
467                    self.try_flush_data().await?;
468                }
469                AlignedMessage::Barrier(barrier) => {
470                    let barrier_start_time = Instant::now();
471                    let (left_post_commit, right_post_commit) =
472                        self.flush_data(barrier.epoch).await?;
473
474                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
475                    yield Message::Barrier(barrier);
476
477                    // Update the vnode bitmap for state tables of both sides if asked.
478                    right_post_commit
479                        .post_yield_barrier(update_vnode_bitmap.clone())
480                        .await?;
481                    if left_post_commit
482                        .post_yield_barrier(update_vnode_bitmap)
483                        .await?
484                        .unwrap_or(false)
485                    {
486                        self.watermark_buffers
487                            .values_mut()
488                            .for_each(|buffers| buffers.clear());
489                    }
490
491                    // Report metrics of cached join rows/entries
492                    for (join_cached_entry_count, ht) in [
493                        (&left_join_cached_entry_count, &self.side_l.ht),
494                        (&right_join_cached_entry_count, &self.side_r.ht),
495                    ] {
496                        join_cached_entry_count.set(ht.entry_count() as i64);
497                    }
498
499                    barrier_join_match_duration_ns
500                        .inc_by(barrier_start_time.elapsed().as_nanos() as u64);
501                }
502            }
503            start_time = Instant::now();
504        }
505    }
506
507    async fn flush_data(
508        &mut self,
509        epoch: EpochPair,
510    ) -> StreamExecutorResult<(
511        AsOfJoinHashMapPostCommit<'_, S, E>,
512        AsOfJoinHashMapPostCommit<'_, S, E>,
513    )> {
514        let left = self.side_l.ht.flush(epoch).await?;
515        let right = self.side_r.ht.flush(epoch).await?;
516        Ok((left, right))
517    }
518
519    async fn try_flush_data(&mut self) -> StreamExecutorResult<()> {
520        self.side_l.ht.try_flush().await?;
521        self.side_r.ht.try_flush().await?;
522        Ok(())
523    }
524
525    fn handle_watermark(
526        &mut self,
527        side: SideTypePrimitive,
528        watermark: Watermark,
529    ) -> StreamExecutorResult<Vec<Watermark>> {
530        let (side_update, side_match) = if side == SideType::Left {
531            (&mut self.side_l, &mut self.side_r)
532        } else {
533            (&mut self.side_r, &mut self.side_l)
534        };
535
536        // State cleaning
537        if side_update.join_key_indices[0] == watermark.col_idx {
538            side_match.ht.update_watermark(watermark.val.clone());
539        }
540
541        // Select watermarks to yield.
542        let wm_in_jk = side_update
543            .join_key_indices
544            .iter()
545            .positions(|idx| *idx == watermark.col_idx);
546        let mut watermarks_to_emit = vec![];
547        for idx in wm_in_jk {
548            let buffers = self
549                .watermark_buffers
550                .entry(idx)
551                .or_insert_with(|| BufferedWatermarks::with_ids([SideType::Left, SideType::Right]));
552            if let Some(selected_watermark) = buffers.handle_watermark(side, watermark.clone()) {
553                let empty_indices = vec![];
554                let output_indices = side_update
555                    .i2o_mapping_indexed
556                    .get_vec(&side_update.join_key_indices[idx])
557                    .unwrap_or(&empty_indices)
558                    .iter()
559                    .chain(
560                        side_match
561                            .i2o_mapping_indexed
562                            .get_vec(&side_match.join_key_indices[idx])
563                            .unwrap_or(&empty_indices),
564                    );
565                for output_idx in output_indices {
566                    watermarks_to_emit.push(selected_watermark.clone().with_idx(*output_idx));
567                }
568            };
569        }
570        Ok(watermarks_to_emit)
571    }
572
573    #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
574    async fn eq_join_left(args: EqJoinArgs<'_, S, E>) {
575        let EqJoinArgs {
576            ctx,
577            side_l,
578            side_r,
579            null_safe,
580            asof_desc,
581            actual_output_data_types,
582            chunk,
583            chunk_size,
584            cnt_rows_received,
585            join_cache_evict_interval_rows,
586            high_join_amplification_threshold,
587            join_matched_join_keys,
588        } = args;
589
590        let (side_update, side_match) = (side_l, side_r);
591
592        let mut join_chunk_builder =
593            JoinChunkBuilder::<T, { SideType::Left }>::new(JoinStreamChunkBuilder::new(
594                chunk_size,
595                actual_output_data_types.to_vec(),
596                side_update.i2o_mapping.clone(),
597                side_match.i2o_mapping.clone(),
598            ));
599
600        // The inequality key is always a single column; wrap in an array for `project()`.
601        let inequal_key_idx_update = [side_update.inequal_key_idx];
602
603        for r in chunk.rows_with_holes() {
604            let Some((op, row)) = r else {
605                continue;
606            };
607            Self::evict_cache(
608                side_update,
609                side_match,
610                cnt_rows_received,
611                join_cache_evict_interval_rows,
612            );
613
614            // Check null-safe: if any non-null-safe join key column is NULL, skip matching.
615            // Rows that can never match are not stored in state (consistent with old behavior).
616            let join_key_null_not_safe = side_update
617                .join_key_indices
618                .iter()
619                .zip_eq(null_safe.iter())
620                .any(|(idx, ns)| !ns && row.datum_at(*idx).is_none());
621            if join_key_null_not_safe {
622                match op {
623                    Op::Insert | Op::UpdateInsert => {
624                        if let Some(chunk) =
625                            join_chunk_builder.forward_if_not_matched(Op::Insert, row)
626                        {
627                            yield chunk;
628                        }
629                    }
630                    Op::Delete | Op::UpdateDelete => {
631                        if let Some(chunk) =
632                            join_chunk_builder.forward_if_not_matched(Op::Delete, row)
633                        {
634                            yield chunk;
635                        }
636                    }
637                }
638                join_matched_join_keys.observe(0.0);
639                continue;
640            }
641
642            let join_key = row.project(&side_update.join_key_indices);
643
644            let inequal_key_is_null = side_update.ht.check_inequal_key_null(&row);
645            let inequal_key = row.project(&inequal_key_idx_update);
646
647            let mut join_matched_rows_cnt = 0;
648
649            if !inequal_key_is_null {
650                let matched_row_by_inequality = match asof_desc.inequality_type {
651                    AsOfInequalityType::Lt => {
652                        side_match
653                            .ht
654                            .lower_bound_by_inequality_with_jk_prefix(
655                                &join_key,
656                                Bound::Excluded(&inequal_key),
657                            )
658                            .await
659                    }
660                    AsOfInequalityType::Le => {
661                        side_match
662                            .ht
663                            .lower_bound_by_inequality_with_jk_prefix(
664                                &join_key,
665                                Bound::Included(&inequal_key),
666                            )
667                            .await
668                    }
669                    AsOfInequalityType::Gt => {
670                        side_match
671                            .ht
672                            .upper_bound_by_inequality_with_jk_prefix(
673                                &join_key,
674                                Bound::Excluded(&inequal_key),
675                            )
676                            .await
677                    }
678                    AsOfInequalityType::Ge => {
679                        side_match
680                            .ht
681                            .upper_bound_by_inequality_with_jk_prefix(
682                                &join_key,
683                                Bound::Included(&inequal_key),
684                            )
685                            .await
686                    }
687                }?
688                .map(|row| JoinRow::new(row, 0));
689                match op {
690                    Op::Insert | Op::UpdateInsert => {
691                        if let Some(matched_row) = matched_row_by_inequality {
692                            join_matched_rows_cnt += 1;
693                            if let Some(chunk) =
694                                join_chunk_builder.with_match_on_insert(&row, &matched_row)
695                            {
696                                yield chunk;
697                            }
698                        } else if let Some(chunk) =
699                            join_chunk_builder.forward_if_not_matched(Op::Insert, row)
700                        {
701                            yield chunk;
702                        }
703                        side_update.ht.insert(row)?;
704                    }
705                    Op::Delete | Op::UpdateDelete => {
706                        if let Some(matched_row) = matched_row_by_inequality {
707                            join_matched_rows_cnt += 1;
708                            if let Some(chunk) =
709                                join_chunk_builder.with_match_on_delete(&row, &matched_row)
710                            {
711                                yield chunk;
712                            }
713                        } else if let Some(chunk) =
714                            join_chunk_builder.forward_if_not_matched(Op::Delete, row)
715                        {
716                            yield chunk;
717                        }
718                        side_update.ht.delete(row)?;
719                    }
720                }
721            } else {
722                // Inequality key is NULL, which can never satisfy the inequality predicate,
723                // so we skip storing and only forward as unmatched for left outer join.
724                match op {
725                    Op::Insert | Op::UpdateInsert => {
726                        if let Some(chunk) =
727                            join_chunk_builder.forward_if_not_matched(Op::Insert, row)
728                        {
729                            yield chunk;
730                        }
731                    }
732                    Op::Delete | Op::UpdateDelete => {
733                        if let Some(chunk) =
734                            join_chunk_builder.forward_if_not_matched(Op::Delete, row)
735                        {
736                            yield chunk;
737                        }
738                    }
739                }
740            }
741            join_matched_join_keys.observe(join_matched_rows_cnt as _);
742            if join_matched_rows_cnt > high_join_amplification_threshold {
743                tracing::warn!(target: "high_join_amplification",
744                    matched_rows_len = join_matched_rows_cnt,
745                    update_table_id = %side_update.ht.table_id(),
746                    match_table_id = %side_match.ht.table_id(),
747                    join_key = ?join_key,
748                    actor_id = %ctx.id,
749                    fragment_id = %ctx.fragment_id,
750                    "large rows matched for join key when AsOf join updating left side",
751                );
752            }
753        }
754        if let Some(chunk) = join_chunk_builder.take() {
755            yield chunk;
756        }
757    }
758
759    fn cmp_pk_rows(pk1: &impl Row, pk2: &impl Row) -> Ordering {
760        cmp_rows_ascending(pk1, pk2)
761    }
762
763    #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
764    async fn eq_join_right(args: EqJoinArgs<'_, S, E>) {
765        let EqJoinArgs {
766            ctx,
767            side_l,
768            side_r,
769            null_safe,
770            asof_desc,
771            actual_output_data_types,
772            chunk,
773            chunk_size,
774            cnt_rows_received,
775            join_cache_evict_interval_rows,
776            high_join_amplification_threshold,
777            join_matched_join_keys,
778        } = args;
779
780        let (side_update, side_match) = (side_r, side_l);
781
782        let mut join_chunk_builder = JoinStreamChunkBuilder::new(
783            chunk_size,
784            actual_output_data_types.to_vec(),
785            side_update.i2o_mapping.clone(),
786            side_match.i2o_mapping.clone(),
787        );
788
789        // The inequality key is always a single column; wrap in an array for `project()`.
790        let inequal_key_idx_update = [side_update.inequal_key_idx];
791
792        for r in chunk.rows_with_holes() {
793            let Some((op, row)) = r else {
794                continue;
795            };
796            Self::evict_cache(
797                side_update,
798                side_match,
799                cnt_rows_received,
800                join_cache_evict_interval_rows,
801            );
802
803            // Check null-safe: if any non-null-safe join key column is NULL, skip.
804            // Rows that can never match are not stored in state (consistent with old behavior).
805            let join_key_null_not_safe = side_update
806                .join_key_indices
807                .iter()
808                .zip_eq(null_safe.iter())
809                .any(|(idx, ns)| !ns && row.datum_at(*idx).is_none());
810            if join_key_null_not_safe {
811                join_matched_join_keys.observe(0.0);
812                continue;
813            }
814
815            let join_key = row.project(&side_update.join_key_indices);
816
817            let inequal_key_is_null = side_update.ht.check_inequal_key_null(&row);
818            let inequal_key = row.project(&inequal_key_idx_update);
819
820            let mut join_matched_rows_cnt = 0;
821
822            if !inequal_key_is_null {
823                let (row_to_delete_r, row_to_insert_r) = {
824                    let (first_row, second_row) = side_update
825                        .ht
826                        .first_two_by_inequality_with_jk_prefix(&join_key, &inequal_key)
827                        .await?;
828                    if let Some(first_row) = first_row {
829                        let row_pk = side_update.ht.get_pk_from_row(row);
830                        let first_pk = side_update.ht.get_pk_from_row(&first_row).to_owned_row();
831                        match op {
832                            Op::Insert | Op::UpdateInsert => {
833                                // If there are multiple rows match the inequality key in the right table, we use one with smallest pk.
834                                if Self::cmp_pk_rows(&first_pk, &row_pk) == Ordering::Greater {
835                                    (Some(Either::Left(first_row)), Some(Either::Right(row)))
836                                } else {
837                                    // No affected row in the right table.
838                                    (None, None)
839                                }
840                            }
841                            Op::Delete | Op::UpdateDelete => {
842                                if Self::cmp_pk_rows(&first_pk, &row_pk) == Ordering::Equal {
843                                    if let Some(second_row) = second_row {
844                                        (Some(Either::Right(row)), Some(Either::Left(second_row)))
845                                    } else {
846                                        (Some(Either::Right(row)), None)
847                                    }
848                                } else {
849                                    // No affected row in the right table.
850                                    (None, None)
851                                }
852                            }
853                        }
854                    } else {
855                        match op {
856                            // Decide the row_to_delete later
857                            Op::Insert | Op::UpdateInsert => (None, Some(Either::Right(row))),
858                            // Decide the row_to_insert later
859                            Op::Delete | Op::UpdateDelete => (Some(Either::Right(row)), None),
860                        }
861                    }
862                };
863                // 4 cases for row_to_delete_r and row_to_insert_r:
864                // 1. Some(_), Some(_): delete row_to_delete_r and insert row_to_insert_r
865                // 2. None, Some(_)   : row_to_delete to be decided by the nearest inequality key
866                // 3. Some(_), None   : row_to_insert to be decided by the nearest inequality key
867                // 4. None, None      : do nothing
868                if row_to_delete_r.is_none() && row_to_insert_r.is_none() {
869                    // no row to delete or insert.
870                } else {
871                    let prev_inequality_key = side_update
872                        .ht
873                        .upper_bound_by_inequality_with_jk_prefix(
874                            &join_key,
875                            Bound::Excluded(&inequal_key),
876                        )
877                        .await?
878                        .map(|r| r.project(&inequal_key_idx_update));
879                    let next_inequality_key = side_update
880                        .ht
881                        .lower_bound_by_inequality_with_jk_prefix(
882                            &join_key,
883                            Bound::Excluded(&inequal_key),
884                        )
885                        .await?
886                        .map(|r| r.project(&inequal_key_idx_update));
887
888                    let affected_inequality_key_r = match asof_desc.inequality_type {
889                        AsOfInequalityType::Lt | AsOfInequalityType::Le => &next_inequality_key,
890                        AsOfInequalityType::Gt | AsOfInequalityType::Ge => &prev_inequality_key,
891                    };
892                    let affected_row_r =
893                        if let Some(affected_inequality_key_r) = affected_inequality_key_r {
894                            side_update
895                                .ht
896                                .first_by_inequality_with_jk_prefix(
897                                    &join_key,
898                                    &affected_inequality_key_r,
899                                )
900                                .await?
901                        } else {
902                            None
903                        }
904                        .map(Either::Left);
905
906                    let (row_to_delete_r, row_to_insert_r) =
907                        match (&row_to_delete_r, &row_to_insert_r) {
908                            (Some(_), Some(_)) => (row_to_delete_r, row_to_insert_r),
909                            (None, Some(_)) => (affected_row_r, row_to_insert_r),
910                            (Some(_), None) => (row_to_delete_r, affected_row_r),
911                            (None, None) => unreachable!(),
912                        };
913                    let range = match asof_desc.inequality_type {
914                        AsOfInequalityType::Lt => (
915                            prev_inequality_key
916                                .map(Either::Left)
917                                .map_or_else(|| Bound::Unbounded, Bound::Included),
918                            Bound::Excluded(Either::Right(&inequal_key)),
919                        ),
920                        AsOfInequalityType::Le => (
921                            prev_inequality_key
922                                .map(Either::Left)
923                                .map_or_else(|| Bound::Unbounded, Bound::Excluded),
924                            Bound::Included(Either::Right(&inequal_key)),
925                        ),
926                        AsOfInequalityType::Gt => (
927                            Bound::Excluded(Either::Right(&inequal_key)),
928                            next_inequality_key
929                                .map(Either::Left)
930                                .map_or_else(|| Bound::Unbounded, Bound::Included),
931                        ),
932                        AsOfInequalityType::Ge => (
933                            Bound::Included(Either::Right(&inequal_key)),
934                            next_inequality_key
935                                .map(Either::Left)
936                                .map_or_else(|| Bound::Unbounded, Bound::Excluded),
937                        ),
938                    };
939
940                    let rows_l_stream = side_match
941                        .ht
942                        .range_by_inequality_with_jk_prefix(&join_key, &range)
943                        .await?;
944                    #[for_await]
945                    for row_l in rows_l_stream {
946                        let row_l = row_l?;
947                        join_matched_rows_cnt += 1;
948                        if let Some(row_to_delete_r) = &row_to_delete_r {
949                            if let Some(chunk) =
950                                join_chunk_builder.append_row(Op::Delete, row_to_delete_r, &row_l)
951                            {
952                                yield chunk;
953                            }
954                        } else if is_as_of_left_outer(T)
955                            && let Some(chunk) =
956                                join_chunk_builder.append_row_matched(Op::Delete, &row_l)
957                        {
958                            yield chunk;
959                        }
960                        if let Some(row_to_insert_r) = &row_to_insert_r {
961                            if let Some(chunk) =
962                                join_chunk_builder.append_row(Op::Insert, row_to_insert_r, &row_l)
963                            {
964                                yield chunk;
965                            }
966                        } else if is_as_of_left_outer(T)
967                            && let Some(chunk) =
968                                join_chunk_builder.append_row_matched(Op::Insert, &row_l)
969                        {
970                            yield chunk;
971                        }
972                    }
973                }
974
975                match op {
976                    Op::Insert | Op::UpdateInsert => {
977                        side_update.ht.insert(row)?;
978                    }
979                    Op::Delete | Op::UpdateDelete => {
980                        side_update.ht.delete(row)?;
981                    }
982                }
983            } else {
984                // Inequality key is NULL, which can never satisfy the inequality predicate,
985                // so we skip storing. No action needed on the right side.
986            }
987            join_matched_join_keys.observe(join_matched_rows_cnt as _);
988            if join_matched_rows_cnt > high_join_amplification_threshold {
989                let join_key = row.project(&side_update.join_key_indices);
990                tracing::warn!(target: "high_join_amplification",
991                    matched_rows_len = join_matched_rows_cnt,
992                    update_table_id = %side_update.ht.table_id(),
993                    match_table_id = %side_match.ht.table_id(),
994                    join_key = ?join_key,
995                    actor_id = %ctx.id,
996                    fragment_id = %ctx.fragment_id,
997                    "large rows matched for join key when AsOf join updating right side",
998                );
999            }
1000        }
1001        if let Some(chunk) = join_chunk_builder.take() {
1002            yield chunk;
1003        }
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use std::sync::atomic::AtomicU64;
1010
1011    use prometheus::Registry;
1012    use risingwave_common::array::*;
1013    use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, TableId};
1014    use risingwave_common::config::MetricLevel;
1015    use risingwave_common::metrics::get_label;
1016    use risingwave_common::util::epoch::test_epoch;
1017    use risingwave_common::util::sort_util::OrderType;
1018    use risingwave_storage::memory::MemoryStateStore;
1019
1020    use super::*;
1021    use crate::common::table::test_utils::gen_pbtable;
1022    use crate::executor::test_utils::{MessageSender, MockSource, StreamExecutorTestExt};
1023
1024    async fn create_in_memory_state_table(
1025        mem_state: MemoryStateStore,
1026        data_types: &[DataType],
1027        order_types: &[OrderType],
1028        pk_indices: &[usize],
1029        table_id: u32,
1030    ) -> StateTable<MemoryStateStore> {
1031        let column_descs = data_types
1032            .iter()
1033            .enumerate()
1034            .map(|(id, data_type)| ColumnDesc::unnamed(ColumnId::new(id as i32), data_type.clone()))
1035            .collect_vec();
1036        StateTable::from_table_catalog(
1037            &gen_pbtable(
1038                TableId::new(table_id),
1039                column_descs,
1040                order_types.to_vec(),
1041                pk_indices.to_vec(),
1042                0,
1043            ),
1044            mem_state.clone(),
1045            None,
1046        )
1047        .await
1048    }
1049
1050    async fn create_executor<const T: AsOfJoinTypePrimitive>(
1051        asof_desc: AsOfDesc,
1052        use_cache: bool,
1053    ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1054        create_executor_with_metrics::<T>(
1055            asof_desc,
1056            use_cache,
1057            Arc::new(StreamingMetrics::unused()),
1058        )
1059        .await
1060    }
1061
1062    async fn create_executor_with_metrics<const T: AsOfJoinTypePrimitive>(
1063        asof_desc: AsOfDesc,
1064        use_cache: bool,
1065        metrics: Arc<StreamingMetrics>,
1066    ) -> (MessageSender, MessageSender, BoxedMessageStream) {
1067        let schema = Schema {
1068            fields: vec![
1069                Field::unnamed(DataType::Int64), // join key
1070                Field::unnamed(DataType::Int64),
1071                Field::unnamed(DataType::Int64),
1072            ],
1073        };
1074        let (tx_l, source_l) = MockSource::channel();
1075        let source_l = source_l.into_executor(schema.clone(), vec![1]);
1076        let (tx_r, source_r) = MockSource::channel();
1077        let source_r = source_r.into_executor(schema, vec![1]);
1078        let params_l = JoinParams::new(vec![0], vec![1]);
1079        let params_r = JoinParams::new(vec![0], vec![1]);
1080
1081        let mem_state = MemoryStateStore::new();
1082
1083        let state_l = create_in_memory_state_table(
1084            mem_state.clone(),
1085            &[DataType::Int64, DataType::Int64, DataType::Int64],
1086            &[
1087                OrderType::ascending(),
1088                OrderType::ascending(),
1089                OrderType::ascending(),
1090            ],
1091            &[0, asof_desc.left_idx, 1],
1092            0,
1093        )
1094        .await;
1095
1096        let state_r = create_in_memory_state_table(
1097            mem_state,
1098            &[DataType::Int64, DataType::Int64, DataType::Int64],
1099            &[
1100                OrderType::ascending(),
1101                OrderType::ascending(),
1102                OrderType::ascending(),
1103            ],
1104            &[0, asof_desc.right_idx, 1],
1105            1,
1106        )
1107        .await;
1108
1109        let schema: Schema = [source_l.schema().fields(), source_r.schema().fields()]
1110            .concat()
1111            .into_iter()
1112            .collect();
1113        let schema_len = schema.len();
1114        let info = ExecutorInfo::for_test(schema, vec![1], "AsOfJoinExecutor".to_owned(), 0);
1115
1116        let executor = AsOfJoinExecutor::<MemoryStateStore, T, AsOfCpuEncoding>::new(
1117            ActorContext::for_test(123),
1118            info,
1119            source_l,
1120            source_r,
1121            params_l,
1122            params_r,
1123            vec![false],
1124            (0..schema_len).collect_vec(),
1125            state_l,
1126            state_r,
1127            Arc::new(AtomicU64::new(0)),
1128            metrics,
1129            1024,
1130            asof_desc,
1131            use_cache,
1132            2048, // high_join_amplification_threshold
1133        );
1134        (tx_l, tx_r, executor.boxed().execute())
1135    }
1136
1137    fn join_matched_sample_count(registry: &Registry, table_id: &str) -> u64 {
1138        registry
1139            .gather()
1140            .iter()
1141            .find(|metric_family| metric_family.name() == "stream_join_matched_join_keys")
1142            .and_then(|metric_family| {
1143                metric_family.get_metric().iter().find(|metric| {
1144                    get_label::<String>(metric, "table_id").as_deref() == Some(table_id)
1145                })
1146            })
1147            .map(|metric| metric.get_histogram().get_sample_count())
1148            .unwrap_or(0)
1149    }
1150
1151    #[tokio::test]
1152    async fn test_asof_join_records_null_equi_key_probe_metrics() -> StreamExecutorResult<()> {
1153        let asof_desc = AsOfDesc {
1154            left_idx: 1,
1155            right_idx: 1,
1156            inequality_type: AsOfInequalityType::Lt,
1157        };
1158        let registry = Registry::new();
1159        let metrics = Arc::new(StreamingMetrics::new(&registry, MetricLevel::Debug));
1160        let (mut tx_l, mut tx_r, mut hash_join) =
1161            create_executor_with_metrics::<{ AsOfJoinType::Inner }>(asof_desc, false, metrics)
1162                .await;
1163
1164        tx_l.push_barrier(test_epoch(1), false);
1165        tx_r.push_barrier(test_epoch(1), false);
1166        hash_join.next_unwrap_ready_barrier()?;
1167
1168        tx_l.push_chunk(StreamChunk::from_pretty(
1169            "  I I I
1170             + . 10 1",
1171        ));
1172        hash_join.next_unwrap_pending();
1173
1174        tx_r.push_chunk(StreamChunk::from_pretty(
1175            "  I I I
1176             + . 20 2",
1177        ));
1178        hash_join.next_unwrap_pending();
1179
1180        assert_eq!(join_matched_sample_count(&registry, "0"), 1);
1181        assert_eq!(join_matched_sample_count(&registry, "1"), 1);
1182
1183        Ok(())
1184    }
1185
1186    #[tokio::test]
1187    async fn test_asof_inner_join() -> StreamExecutorResult<()> {
1188        test_asof_inner_join_impl(false).await
1189    }
1190
1191    #[tokio::test]
1192    async fn test_asof_inner_join_with_cache() -> StreamExecutorResult<()> {
1193        test_asof_inner_join_impl(true).await
1194    }
1195
1196    async fn test_asof_inner_join_impl(use_cache: bool) -> StreamExecutorResult<()> {
1197        let asof_desc = AsOfDesc {
1198            left_idx: 0,
1199            right_idx: 2,
1200            inequality_type: AsOfInequalityType::Lt,
1201        };
1202
1203        let chunk_l1 = StreamChunk::from_pretty(
1204            "  I I I
1205             + 1 4 7
1206             + 2 5 8
1207             + 3 6 9",
1208        );
1209        let chunk_l2 = StreamChunk::from_pretty(
1210            "  I I I
1211             + 3 8 1
1212             - 3 8 1",
1213        );
1214        let chunk_r1 = StreamChunk::from_pretty(
1215            "  I I I
1216             + 2 1 7
1217             + 2 2 1
1218             + 2 3 4
1219             + 2 4 2
1220             + 6 1 9
1221             + 6 2 9",
1222        );
1223        let chunk_r2 = StreamChunk::from_pretty(
1224            "  I I I
1225             - 2 3 4",
1226        );
1227        let chunk_r3 = StreamChunk::from_pretty(
1228            "  I I I
1229             + 2 3 3",
1230        );
1231        let chunk_l3 = StreamChunk::from_pretty(
1232            "  I I I
1233             - 2 5 8",
1234        );
1235        let chunk_l4 = StreamChunk::from_pretty(
1236            "  I I I
1237             + 6 3 1
1238             + 6 4 1",
1239        );
1240        let chunk_r4 = StreamChunk::from_pretty(
1241            "  I I I
1242             - 6 1 9",
1243        );
1244
1245        let (mut tx_l, mut tx_r, mut hash_join) =
1246            create_executor::<{ AsOfJoinType::Inner }>(asof_desc, use_cache).await;
1247
1248        // push the init barrier for left and right
1249        tx_l.push_barrier(test_epoch(1), false);
1250        tx_r.push_barrier(test_epoch(1), false);
1251        hash_join.next_unwrap_ready_barrier()?;
1252
1253        // push the 1st left chunk
1254        tx_l.push_chunk(chunk_l1);
1255        hash_join.next_unwrap_pending();
1256
1257        // push the init barrier for left and right
1258        tx_l.push_barrier(test_epoch(2), false);
1259        tx_r.push_barrier(test_epoch(2), false);
1260        hash_join.next_unwrap_ready_barrier()?;
1261
1262        // push the 2nd left chunk
1263        tx_l.push_chunk(chunk_l2);
1264        hash_join.next_unwrap_pending();
1265
1266        // push the 1st right chunk
1267        tx_r.push_chunk(chunk_r1);
1268        let chunk = hash_join.next_unwrap_ready_chunk()?;
1269        assert_eq!(
1270            chunk,
1271            StreamChunk::from_pretty(
1272                " I I I I I I
1273                + 2 5 8 2 1 7
1274                - 2 5 8 2 1 7
1275                + 2 5 8 2 3 4"
1276            )
1277        );
1278
1279        // push the 2nd right chunk
1280        tx_r.push_chunk(chunk_r2);
1281        let chunk = hash_join.next_unwrap_ready_chunk()?;
1282        assert_eq!(
1283            chunk,
1284            StreamChunk::from_pretty(
1285                " I I I I I I
1286                - 2 5 8 2 3 4
1287                + 2 5 8 2 1 7"
1288            )
1289        );
1290
1291        // push the 3rd right chunk
1292        tx_r.push_chunk(chunk_r3);
1293        let chunk = hash_join.next_unwrap_ready_chunk()?;
1294        assert_eq!(
1295            chunk,
1296            StreamChunk::from_pretty(
1297                " I I I I I I
1298                - 2 5 8 2 1 7
1299                + 2 5 8 2 3 3"
1300            )
1301        );
1302
1303        // push the 3rd left chunk
1304        tx_l.push_chunk(chunk_l3);
1305        let chunk = hash_join.next_unwrap_ready_chunk()?;
1306        assert_eq!(
1307            chunk,
1308            StreamChunk::from_pretty(
1309                " I I I I I I
1310                - 2 5 8 2 3 3"
1311            )
1312        );
1313
1314        // push the 4th left chunk
1315        tx_l.push_chunk(chunk_l4);
1316        let chunk = hash_join.next_unwrap_ready_chunk()?;
1317        assert_eq!(
1318            chunk,
1319            StreamChunk::from_pretty(
1320                " I I I I I I
1321                + 6 3 1 6 1 9
1322                + 6 4 1 6 1 9"
1323            )
1324        );
1325
1326        // push the 4th right chunk
1327        tx_r.push_chunk(chunk_r4);
1328        let chunk = hash_join.next_unwrap_ready_chunk()?;
1329        assert_eq!(
1330            chunk,
1331            StreamChunk::from_pretty(
1332                " I I I I I I
1333                - 6 3 1 6 1 9
1334                + 6 3 1 6 2 9
1335                - 6 4 1 6 1 9
1336                + 6 4 1 6 2 9"
1337            )
1338        );
1339
1340        Ok(())
1341    }
1342
1343    #[tokio::test]
1344    async fn test_asof_left_outer_join() -> StreamExecutorResult<()> {
1345        test_asof_left_outer_join_impl(false).await
1346    }
1347
1348    #[tokio::test]
1349    async fn test_asof_left_outer_join_with_cache() -> StreamExecutorResult<()> {
1350        test_asof_left_outer_join_impl(true).await
1351    }
1352
1353    async fn test_asof_left_outer_join_impl(use_cache: bool) -> StreamExecutorResult<()> {
1354        let asof_desc = AsOfDesc {
1355            left_idx: 1,
1356            right_idx: 2,
1357            inequality_type: AsOfInequalityType::Ge,
1358        };
1359
1360        let chunk_l1 = StreamChunk::from_pretty(
1361            "  I I I
1362             + 1 4 7
1363             + 2 5 8
1364             + 3 6 9",
1365        );
1366        let chunk_l2 = StreamChunk::from_pretty(
1367            "  I I I
1368             + 3 8 1
1369             - 3 8 1",
1370        );
1371        let chunk_r1 = StreamChunk::from_pretty(
1372            "  I I I
1373             + 2 3 4
1374             + 2 2 5
1375             + 2 1 5
1376             + 6 1 8
1377             + 6 2 9",
1378        );
1379        let chunk_r2 = StreamChunk::from_pretty(
1380            "  I I I
1381             - 2 3 4
1382             - 2 1 5
1383             - 2 2 5",
1384        );
1385        let chunk_l3 = StreamChunk::from_pretty(
1386            "  I I I
1387             + 6 8 9",
1388        );
1389        let chunk_r3 = StreamChunk::from_pretty(
1390            "  I I I
1391             - 6 1 8",
1392        );
1393
1394        let (mut tx_l, mut tx_r, mut hash_join) =
1395            create_executor::<{ AsOfJoinType::LeftOuter }>(asof_desc, use_cache).await;
1396
1397        // push the init barrier for left and right
1398        tx_l.push_barrier(test_epoch(1), false);
1399        tx_r.push_barrier(test_epoch(1), false);
1400        hash_join.next_unwrap_ready_barrier()?;
1401
1402        // push the 1st left chunk
1403        tx_l.push_chunk(chunk_l1);
1404        let chunk = hash_join.next_unwrap_ready_chunk()?;
1405        assert_eq!(
1406            chunk,
1407            StreamChunk::from_pretty(
1408                " I I I I I I
1409                + 1 4 7 . . .
1410                + 2 5 8 . . .
1411                + 3 6 9 . . ."
1412            )
1413        );
1414
1415        // push the init barrier for left and right
1416        tx_l.push_barrier(test_epoch(2), false);
1417        tx_r.push_barrier(test_epoch(2), false);
1418        hash_join.next_unwrap_ready_barrier()?;
1419
1420        // push the 2nd left chunk
1421        tx_l.push_chunk(chunk_l2);
1422        let chunk = hash_join.next_unwrap_ready_chunk()?;
1423        assert_eq!(
1424            chunk,
1425            StreamChunk::from_pretty(
1426                " I I I I I I
1427                + 3 8 1 . . . D
1428                - 3 8 1 . . . D"
1429            )
1430        );
1431
1432        // push the 1st right chunk
1433        tx_r.push_chunk(chunk_r1);
1434        let chunk = hash_join.next_unwrap_ready_chunk()?;
1435        assert_eq!(
1436            chunk,
1437            StreamChunk::from_pretty(
1438                " I I I I I I
1439                - 2 5 8 . . .
1440                + 2 5 8 2 3 4
1441                - 2 5 8 2 3 4
1442                + 2 5 8 2 2 5
1443                - 2 5 8 2 2 5
1444                + 2 5 8 2 1 5"
1445            )
1446        );
1447
1448        // push the 2nd right chunk
1449        tx_r.push_chunk(chunk_r2);
1450        let chunk = hash_join.next_unwrap_ready_chunk()?;
1451        assert_eq!(
1452            chunk,
1453            StreamChunk::from_pretty(
1454                " I I I I I I
1455                - 2 5 8 2 1 5
1456                + 2 5 8 2 2 5
1457                - 2 5 8 2 2 5
1458                + 2 5 8 . . ."
1459            )
1460        );
1461
1462        // push the 3rd left chunk
1463        tx_l.push_chunk(chunk_l3);
1464        let chunk = hash_join.next_unwrap_ready_chunk()?;
1465        assert_eq!(
1466            chunk,
1467            StreamChunk::from_pretty(
1468                " I I I I I I
1469                + 6 8 9 6 1 8"
1470            )
1471        );
1472
1473        // push the 3rd right chunk
1474        tx_r.push_chunk(chunk_r3);
1475        let chunk = hash_join.next_unwrap_ready_chunk()?;
1476        assert_eq!(
1477            chunk,
1478            StreamChunk::from_pretty(
1479                " I I I I I I
1480                - 6 8 9 6 1 8
1481                + 6 8 9 . . ."
1482            )
1483        );
1484        Ok(())
1485    }
1486}