1use std::collections::HashMap;
16use std::collections::hash_map::Entry;
17use std::ops::Bound;
18
19use anyhow::Context;
20use either::Either;
21use futures::TryStreamExt;
22use futures::stream::{self, PollNext};
23use itertools::Itertools;
24use risingwave_common::array::Op;
25use risingwave_common::bitmap::BitmapBuilder;
26use risingwave_common::hash::{HashKey, NullBitmap};
27use risingwave_common::row::RowExt;
28use risingwave_common::util::iter_util::ZipEqDebug;
29use risingwave_common_estimate_size::{EstimateSize, KvSize};
30use risingwave_expr::expr::NonStrictExpression;
31use risingwave_storage::row_serde::value_serde::ValueRowSerde;
32use risingwave_storage::store::PrefetchOptions;
33
34use super::join::{JoinType, JoinTypePrimitive};
35use super::monitor::TemporalJoinMetrics;
36use crate::cache::ManagedLruCache;
37use crate::common::metrics::MetricsInfo;
38use crate::common::table::state_table::ReplicatedStateTable;
39use crate::executor::join::builder::JoinStreamChunkBuilder;
40use crate::executor::prelude::*;
41
42pub struct TemporalJoinExecutor<
43 K: HashKey,
44 S: StateStore,
45 SD: ValueRowSerde,
46 const T: JoinTypePrimitive,
47 const APPEND_ONLY: bool,
48> {
49 ctx: ActorContextRef,
50 #[expect(dead_code)]
51 info: ExecutorInfo,
52 left: Executor,
53 right: Executor,
54 right_table: TemporalSide<K, S, SD>,
55 left_join_keys: Vec<usize>,
56 right_join_keys: Vec<usize>,
57 null_safe: Vec<bool>,
58 condition: Option<NonStrictExpression>,
59 output_indices: Vec<usize>,
60 chunk_size: usize,
61 memo_table: Option<StateTable<S>>,
62 metrics: TemporalJoinMetrics,
63 is_broadcast: bool,
64}
65
66#[derive(Default)]
67pub struct JoinEntry {
68 cached: HashMap<OwnedRow, OwnedRow>,
70 kv_heap_size: KvSize,
71}
72
73impl EstimateSize for JoinEntry {
74 fn estimated_heap_size(&self) -> usize {
75 self.kv_heap_size.size()
78 }
79}
80
81impl JoinEntry {
82 pub fn insert(&mut self, key: OwnedRow, value: OwnedRow) {
84 if let Entry::Vacant(e) = self.cached.entry(key) {
87 self.kv_heap_size.add(e.key(), &value);
88 e.insert(value);
89 } else {
90 panic!("value {:?} double insert", value);
91 }
92 }
93
94 pub fn remove(&mut self, key: &OwnedRow) {
96 if let Some(value) = self.cached.remove(key) {
97 self.kv_heap_size.sub(key, &value);
98 } else {
99 panic!("key {:?} should be in the cache", key);
100 }
101 }
102
103 pub fn is_empty(&self) -> bool {
104 self.cached.is_empty()
105 }
106}
107
108struct TemporalSide<K: HashKey, S: StateStore, SD: ValueRowSerde> {
109 source: ReplicatedStateTable<S, SD>,
110 table_stream_key_indices: Vec<usize>,
111 cache: ManagedLruCache<K, JoinEntry>,
112 join_key_data_types: Vec<DataType>,
113}
114
115impl<K: HashKey, S: StateStore, SD: ValueRowSerde> TemporalSide<K, S, SD> {
116 async fn fetch_or_promote_keys(
119 &mut self,
120 keys: impl Iterator<Item = &K>,
121 metrics: &TemporalJoinMetrics,
122 ) -> StreamExecutorResult<()> {
123 let mut futs = Vec::with_capacity(keys.size_hint().1.unwrap_or(0));
124 for key in keys {
125 metrics.temporal_join_total_query_cache_count.inc();
126
127 if self.cache.get(key).is_none() {
128 metrics.temporal_join_cache_miss_count.inc();
129
130 futs.push(async {
131 let pk_prefix = key.deserialize(&self.join_key_data_types)?;
132
133 let iter = self
134 .source
135 .iter_with_prefix(
136 &pk_prefix,
137 &(Bound::<OwnedRow>::Unbounded, Bound::<OwnedRow>::Unbounded),
138 PrefetchOptions::default(),
139 )
140 .await?;
141
142 let mut entry = JoinEntry::default();
143
144 pin_mut!(iter);
145 while let Some(row) = iter.next().await {
146 let row: OwnedRow = row?;
147 entry.insert(
148 row.as_ref()
149 .project(&self.table_stream_key_indices)
150 .into_owned_row(),
151 row,
152 );
153 }
154 let key = key.clone();
155 Ok((key, entry)) as StreamExecutorResult<_>
156 });
157 }
158 }
159
160 #[for_await]
161 for res in stream::iter(futs).buffered(16) {
162 let (key, entry) = res?;
163 self.cache.put(key, entry);
164 }
165
166 Ok(())
167 }
168
169 fn force_peek(&self, key: &K) -> &JoinEntry {
170 self.cache.peek(key).expect("key should exist")
171 }
172
173 fn update(
174 &mut self,
175 chunks: Vec<StreamChunk>,
176 join_keys: &[usize],
177 right_stream_key_indices: &[usize],
178 ) -> StreamExecutorResult<()> {
179 for chunk in chunks {
180 let keys = K::build_many(join_keys, chunk.data_chunk());
181 for (r, key) in chunk.rows_with_holes().zip_eq_debug(keys.into_iter()) {
182 let Some((op, row)) = r else {
183 continue;
184 };
185 if self.cache.contains(&key) {
186 let mut entry = self.cache.get_mut(&key).unwrap();
188 let stream_key = row.project(right_stream_key_indices).into_owned_row();
189 match op {
190 Op::Insert | Op::UpdateInsert => {
191 entry.insert(stream_key, row.into_owned_row())
192 }
193 Op::Delete | Op::UpdateDelete => entry.remove(&stream_key),
194 };
195 }
196 }
197 self.source.write_chunk(chunk);
198 }
199 Ok(())
200 }
201}
202
203pub(super) enum InternalMessage {
204 Chunk(StreamChunk),
205 Barrier(Vec<StreamChunk>, Barrier),
206 WaterMark(Watermark),
207}
208
209#[try_stream(ok = StreamChunk, error = StreamExecutorError)]
210async fn chunks_until_barrier(stream: impl MessageStream, expected_barrier: Barrier) {
211 #[for_await]
212 for item in stream {
213 match item? {
214 Message::Watermark(_) => {
215 }
217 Message::Chunk(c) => yield c,
218 Message::Barrier(b) if b.epoch != expected_barrier.epoch => {
219 return Err(StreamExecutorError::align_barrier(expected_barrier, b));
220 }
221 Message::Barrier(_) => return Ok(()),
222 }
223 }
224}
225
226#[try_stream(ok = InternalMessage, error = StreamExecutorError)]
227async fn internal_messages_until_barrier(stream: impl MessageStream, expected_barrier: Barrier) {
228 #[for_await]
229 for item in stream {
230 match item? {
231 Message::Watermark(w) => {
232 yield InternalMessage::WaterMark(w);
233 }
234 Message::Chunk(c) => yield InternalMessage::Chunk(c),
235 Message::Barrier(b) if b.epoch != expected_barrier.epoch => {
236 return Err(StreamExecutorError::align_barrier(expected_barrier, b));
237 }
238 Message::Barrier(_) => return Ok(()),
239 }
240 }
241}
242
243pub(super) async fn expect_first_barrier(
244 stream: &mut (impl Stream<Item = StreamExecutorResult<InternalMessage>> + Unpin),
245) -> StreamExecutorResult<Barrier> {
246 let InternalMessage::Barrier(updates, barrier) = stream
247 .try_next()
248 .instrument_await("expect_first_barrier")
249 .await?
250 .context("failed to extract the first message: stream closed unexpectedly")?
251 else {
252 unreachable!("unexpected internal message");
253 };
254 assert!(updates.is_empty());
255 Ok(barrier)
256}
257
258#[try_stream(ok = InternalMessage, error = StreamExecutorError)]
263pub(super) async fn align_input<const YIELD_RIGHT_CHUNKS: bool>(left: Executor, right: Executor) {
264 let mut left = pin!(left.execute());
265 let mut right = pin!(right.execute());
266 loop {
268 let mut right_chunks = vec![];
269 'inner: loop {
271 let mut combined = stream::select_with_strategy(
272 left.by_ref().map(Either::Left),
273 right.by_ref().map(Either::Right),
274 |_: &mut ()| PollNext::Left,
275 );
276 match combined.next().await {
277 Some(Either::Left(Ok(Message::Chunk(c)))) => yield InternalMessage::Chunk(c),
278 Some(Either::Right(Ok(Message::Chunk(c)))) => {
279 if YIELD_RIGHT_CHUNKS {
280 right_chunks.push(c);
281 }
282 }
283 Some(Either::Left(Ok(Message::Barrier(b)))) => {
284 let mut remain = chunks_until_barrier(right.by_ref(), b.clone())
285 .try_collect()
286 .await?;
287 if YIELD_RIGHT_CHUNKS {
288 right_chunks.append(&mut remain);
289 }
290 yield InternalMessage::Barrier(right_chunks, b);
291 break 'inner;
292 }
293 Some(Either::Right(Ok(Message::Barrier(b)))) => {
294 #[for_await]
295 for internal_message in
296 internal_messages_until_barrier(left.by_ref(), b.clone())
297 {
298 yield internal_message?;
299 }
300 yield InternalMessage::Barrier(right_chunks, b);
301 break 'inner;
302 }
303 Some(Either::Left(Err(e)) | Either::Right(Err(e))) => return Err(e),
304 Some(Either::Left(Ok(Message::Watermark(w)))) => {
305 yield InternalMessage::WaterMark(w);
306 }
307 Some(Either::Right(Ok(Message::Watermark(_)))) => {
308 }
310 None => return Ok(()),
311 }
312 }
313 }
314}
315
316pub(super) fn apply_indices_map(chunk: StreamChunk, indices: &[usize]) -> StreamChunk {
317 let (data_chunk, ops) = chunk.into_parts();
318 let (columns, vis) = data_chunk.into_parts();
319 let output_columns = indices
320 .iter()
321 .cloned()
322 .map(|idx| columns[idx].clone())
323 .collect();
324 StreamChunk::with_visibility(ops, output_columns, vis)
325}
326
327pub(super) mod phase1 {
328 use std::ops::Bound;
329
330 use futures::{StreamExt, pin_mut};
331 use futures_async_stream::try_stream;
332 use risingwave_common::array::stream_chunk_builder::StreamChunkBuilder;
333 use risingwave_common::array::{Op, StreamChunk};
334 use risingwave_common::hash::{HashKey, NullBitmap};
335 use risingwave_common::row::{self, OwnedRow, Row, RowExt};
336 use risingwave_common::types::{DataType, DatumRef};
337 use risingwave_common::util::iter_util::ZipEqDebug;
338 use risingwave_storage::StateStore;
339 use risingwave_storage::row_serde::value_serde::ValueRowSerde;
340
341 use super::{StreamExecutorError, TemporalSide};
342 use crate::common::table::state_table::StateTable;
343 use crate::executor::monitor::TemporalJoinMetrics;
344
345 pub trait Phase1Evaluation {
346 #[must_use = "consume chunk if produced"]
348 fn append_matched_row(
349 op: Op,
350 builder: &mut StreamChunkBuilder,
351 left_row: impl Row,
352 right_row: impl Row,
353 ) -> Option<StreamChunk>;
354
355 #[must_use = "consume chunk if produced"]
357 fn match_end(
358 builder: &mut StreamChunkBuilder,
359 op: Op,
360 left_row: impl Row,
361 right_size: usize,
362 matched: bool,
363 ) -> Option<StreamChunk>;
364 }
365
366 pub struct Inner;
367 pub struct LeftOuter;
368 pub struct LeftOuterWithCond;
369
370 impl Phase1Evaluation for Inner {
371 fn append_matched_row(
372 op: Op,
373 builder: &mut StreamChunkBuilder,
374 left_row: impl Row,
375 right_row: impl Row,
376 ) -> Option<StreamChunk> {
377 builder.append_row(op, left_row.chain(right_row))
378 }
379
380 fn match_end(
381 _builder: &mut StreamChunkBuilder,
382 _op: Op,
383 _left_row: impl Row,
384 _right_size: usize,
385 _matched: bool,
386 ) -> Option<StreamChunk> {
387 None
388 }
389 }
390
391 impl Phase1Evaluation for LeftOuter {
392 fn append_matched_row(
393 op: Op,
394 builder: &mut StreamChunkBuilder,
395 left_row: impl Row,
396 right_row: impl Row,
397 ) -> Option<StreamChunk> {
398 builder.append_row(op, left_row.chain(right_row))
399 }
400
401 fn match_end(
402 builder: &mut StreamChunkBuilder,
403 op: Op,
404 left_row: impl Row,
405 right_size: usize,
406 matched: bool,
407 ) -> Option<StreamChunk> {
408 if !matched {
409 builder.append_row(
411 op,
412 left_row.chain(row::repeat_n(DatumRef::None, right_size)),
413 )
414 } else {
415 None
416 }
417 }
418 }
419
420 impl Phase1Evaluation for LeftOuterWithCond {
421 fn append_matched_row(
422 op: Op,
423 builder: &mut StreamChunkBuilder,
424 left_row: impl Row,
425 right_row: impl Row,
426 ) -> Option<StreamChunk> {
427 builder.append_row(op, left_row.chain(right_row))
428 }
429
430 fn match_end(
431 builder: &mut StreamChunkBuilder,
432 op: Op,
433 left_row: impl Row,
434 right_size: usize,
435 _matched: bool,
436 ) -> Option<StreamChunk> {
437 builder.append_row_invisible(
440 op,
441 left_row.chain(row::repeat_n(DatumRef::None, right_size)),
442 )
443 }
444 }
445
446 #[try_stream(ok = StreamChunk, error = StreamExecutorError)]
447 #[expect(clippy::too_many_arguments)]
448 pub(super) async fn handle_chunk<
449 'a,
450 K: HashKey,
451 S: StateStore,
452 SD: ValueRowSerde,
453 E: Phase1Evaluation,
454 const APPEND_ONLY: bool,
455 >(
456 chunk_size: usize,
457 right_size: usize,
458 full_schema: Vec<DataType>,
459 left_join_keys: &'a [usize],
460 right_table: &'a mut TemporalSide<K, S, SD>,
461 memo_table_lookup_prefix: &'a [usize],
462 memo_table: &'a mut Option<StateTable<S>>,
463 null_matched: &'a K::Bitmap,
464 chunk: StreamChunk,
465 metrics: &'a TemporalJoinMetrics,
466 ) {
467 let mut builder = StreamChunkBuilder::new(chunk_size, full_schema);
468 let keys = K::build_many(left_join_keys, chunk.data_chunk());
469 let to_fetch_keys = chunk
470 .visibility()
471 .iter()
472 .zip_eq_debug(keys.iter())
473 .zip_eq_debug(chunk.ops())
474 .filter_map(|((vis, key), op)| {
475 if vis {
476 if APPEND_ONLY {
477 assert_eq!(*op, Op::Insert);
478 Some(key)
479 } else {
480 match op {
481 Op::Insert | Op::UpdateInsert => Some(key),
482 Op::Delete | Op::UpdateDelete => None,
483 }
484 }
485 } else {
486 None
487 }
488 });
489 right_table
490 .fetch_or_promote_keys(to_fetch_keys, metrics)
491 .await?;
492
493 for (r, key) in chunk.rows_with_holes().zip_eq_debug(keys.into_iter()) {
494 let Some((op, left_row)) = r else {
495 continue;
496 };
497
498 let mut matched = false;
499
500 if APPEND_ONLY {
501 if key.null_bitmap().is_subset(null_matched)
503 && let join_entry = right_table.force_peek(&key)
504 && !join_entry.is_empty()
505 {
506 matched = true;
507 for right_row in join_entry.cached.values() {
508 if let Some(chunk) =
509 E::append_matched_row(op, &mut builder, left_row, right_row)
510 {
511 yield chunk;
512 }
513 }
514 }
515 } else {
516 let memo_table = memo_table.as_mut().unwrap();
532 match op {
533 Op::Insert | Op::UpdateInsert => {
534 if key.null_bitmap().is_subset(null_matched)
535 && let join_entry = right_table.force_peek(&key)
536 && !join_entry.is_empty()
537 {
538 matched = true;
539 for right_row in join_entry.cached.values() {
540 let right_row: OwnedRow = right_row.clone();
541 memo_table.insert(right_row.clone().chain(
543 left_row.project(memo_table_lookup_prefix).into_owned_row(),
544 ));
545 if let Some(chunk) = E::append_matched_row(
546 Op::Insert,
547 &mut builder,
548 left_row,
549 right_row,
550 ) {
551 yield chunk;
552 }
553 }
554 }
555 }
556 Op::Delete | Op::UpdateDelete => {
557 let mut memo_rows_to_delete = vec![];
558 if key.null_bitmap().is_subset(null_matched) {
559 let sub_range: &(Bound<OwnedRow>, Bound<OwnedRow>) =
560 &(Bound::Unbounded, Bound::Unbounded);
561 let prefix = left_row.project(memo_table_lookup_prefix);
562 let state_table_iter = memo_table
563 .iter_with_prefix(prefix, sub_range, Default::default())
564 .await?;
565 pin_mut!(state_table_iter);
566
567 while let Some(memo_row) = state_table_iter.next().await {
568 matched = true;
569 let memo_row = memo_row?.into_owned_row();
570 memo_rows_to_delete.push(memo_row.clone());
571 if let Some(chunk) = E::append_matched_row(
572 Op::Delete,
573 &mut builder,
574 left_row,
575 memo_row.slice(0..right_size),
576 ) {
577 yield chunk;
578 }
579 }
580 }
581 for memo_row in memo_rows_to_delete {
582 memo_table.delete(memo_row);
584 }
585 }
586 }
587 }
588 if let Some(chunk) = E::match_end(
589 &mut builder,
590 match op {
591 Op::Insert | Op::UpdateInsert => Op::Insert,
592 Op::Delete | Op::UpdateDelete => Op::Delete,
593 },
594 left_row,
595 right_size,
596 matched,
597 ) {
598 yield chunk;
599 }
600 }
601
602 if let Some(chunk) = builder.take() {
603 yield chunk;
604 }
605 }
606}
607
608impl<
609 K: HashKey,
610 S: StateStore,
611 SD: ValueRowSerde,
612 const T: JoinTypePrimitive,
613 const APPEND_ONLY: bool,
614> TemporalJoinExecutor<K, S, SD, T, APPEND_ONLY>
615{
616 #[expect(clippy::too_many_arguments)]
617 pub fn new(
618 ctx: ActorContextRef,
619 info: ExecutorInfo,
620 left: Executor,
621 right: Executor,
622 table: ReplicatedStateTable<S, SD>,
623 left_join_keys: Vec<usize>,
624 right_join_keys: Vec<usize>,
625 null_safe: Vec<bool>,
626 condition: Option<NonStrictExpression>,
627 output_indices: Vec<usize>,
628 table_stream_key_indices: Vec<usize>,
629 watermark_sequence: AtomicU64Ref,
630 metrics: Arc<StreamingMetrics>,
631 chunk_size: usize,
632 join_key_data_types: Vec<DataType>,
633 memo_table: Option<StateTable<S>>,
634 is_broadcast: bool,
635 ) -> Self {
636 let metrics_info =
637 MetricsInfo::new(metrics.clone(), table.table_id(), ctx.id, "temporal join");
638 let cache = ManagedLruCache::unbounded(watermark_sequence, metrics_info);
639
640 let metrics = metrics.new_temporal_join_metrics(table.table_id(), ctx.id, ctx.fragment_id);
641
642 Self {
643 ctx,
644 info,
645 left,
646 right,
647 right_table: TemporalSide {
648 source: table,
649 table_stream_key_indices,
650 cache,
651 join_key_data_types,
652 },
653 left_join_keys,
654 right_join_keys,
655 null_safe,
656 condition,
657 output_indices,
658 chunk_size,
659 memo_table,
660 metrics,
661 is_broadcast,
662 }
663 }
664
665 #[try_stream(ok = Message, error = StreamExecutorError)]
666 async fn into_stream(mut self) {
667 let right_size = self.right.schema().len();
668
669 let (left_map, _right_map) = JoinStreamChunkBuilder::get_i2o_mapping(
670 &self.output_indices,
671 self.left.schema().len(),
672 right_size,
673 );
674
675 let left_to_output: HashMap<usize, usize> = HashMap::from_iter(left_map.iter().cloned());
676
677 let left_stream_key_indices = self.left.stream_key().to_vec();
678 let right_stream_key_indices = self.right.stream_key().to_vec();
679 let memo_table_lookup_prefix = self
680 .left_join_keys
681 .iter()
682 .cloned()
683 .chain(left_stream_key_indices)
684 .collect_vec();
685
686 let null_matched = K::Bitmap::from_bool_vec(self.null_safe);
687
688 let full_schema: Vec<_> = self
689 .left
690 .schema()
691 .data_types()
692 .into_iter()
693 .chain(self.right.schema().data_types().into_iter())
694 .collect();
695
696 let input = align_input::<true>(self.left, self.right);
697 pin_mut!(input);
698 let barrier = expect_first_barrier(&mut input).await?;
699 let barrier_epoch = barrier.epoch;
700 yield Message::Barrier(barrier);
701 self.right_table.source.init_epoch(barrier_epoch).await?;
702 if !APPEND_ONLY {
703 self.memo_table
704 .as_mut()
705 .unwrap()
706 .init_epoch(barrier_epoch)
707 .await?;
708 }
709
710 #[for_await]
711 for msg in input {
712 self.right_table.cache.evict();
713 self.metrics
714 .temporal_join_cached_entry_count
715 .set(self.right_table.cache.len() as i64);
716 match msg? {
717 InternalMessage::WaterMark(watermark) => {
718 let output_watermark_col_idx = *left_to_output.get(&watermark.col_idx).unwrap();
719 yield Message::Watermark(watermark.with_idx(output_watermark_col_idx));
720 }
721 InternalMessage::Chunk(chunk) => {
722 let full_schema = full_schema.clone();
723
724 if T == JoinType::Inner {
725 let st1 = phase1::handle_chunk::<K, S, SD, phase1::Inner, APPEND_ONLY>(
726 self.chunk_size,
727 right_size,
728 full_schema,
729 &self.left_join_keys,
730 &mut self.right_table,
731 &memo_table_lookup_prefix,
732 &mut self.memo_table,
733 &null_matched,
734 chunk,
735 &self.metrics,
736 );
737 #[for_await]
738 for chunk in st1 {
739 let chunk = chunk?;
740 let new_chunk = if let Some(ref cond) = self.condition {
741 let (data_chunk, ops) = chunk.into_parts();
742 let passed_bitmap = cond.eval_infallible(&data_chunk).await;
743 let passed_bitmap =
744 Arc::unwrap_or_clone(passed_bitmap).into_bool().to_bitmap();
745 let (columns, vis) = data_chunk.into_parts();
746 let new_vis = vis & passed_bitmap;
747 StreamChunk::with_visibility(ops, columns, new_vis)
748 } else {
749 chunk
750 };
751 let new_chunk = apply_indices_map(new_chunk, &self.output_indices);
752 yield Message::Chunk(new_chunk);
753 }
754 } else if let Some(ref cond) = self.condition {
755 let st1 = phase1::handle_chunk::<
757 K,
758 S,
759 SD,
760 phase1::LeftOuterWithCond,
761 APPEND_ONLY,
762 >(
763 self.chunk_size,
764 right_size,
765 full_schema,
766 &self.left_join_keys,
767 &mut self.right_table,
768 &memo_table_lookup_prefix,
769 &mut self.memo_table,
770 &null_matched,
771 chunk,
772 &self.metrics,
773 );
774 let mut matched_count = 0usize;
775 #[for_await]
776 for chunk in st1 {
777 let chunk = chunk?;
778 let (data_chunk, ops) = chunk.into_parts();
779 let passed_bitmap = cond.eval_infallible(&data_chunk).await;
780 let passed_bitmap =
781 Arc::unwrap_or_clone(passed_bitmap).into_bool().to_bitmap();
782 let (columns, vis) = data_chunk.into_parts();
783 let mut new_vis = BitmapBuilder::with_capacity(vis.len());
784 for (passed, not_match_end) in
785 passed_bitmap.iter().zip_eq_debug(vis.iter())
786 {
787 let is_match_end = !not_match_end;
788 let vis = if is_match_end && matched_count == 0 {
789 true
791 } else if is_match_end {
792 matched_count = 0;
794 false
796 } else {
797 if passed {
798 matched_count += 1;
799 }
800 passed
801 };
802 new_vis.append(vis);
803 }
804 let new_chunk = apply_indices_map(
805 StreamChunk::with_visibility(ops, columns, new_vis.finish()),
806 &self.output_indices,
807 );
808 yield Message::Chunk(new_chunk);
809 }
810 assert_eq!(matched_count, 0);
812 } else {
813 let st1 = phase1::handle_chunk::<K, S, SD, phase1::LeftOuter, APPEND_ONLY>(
814 self.chunk_size,
815 right_size,
816 full_schema,
817 &self.left_join_keys,
818 &mut self.right_table,
819 &memo_table_lookup_prefix,
820 &mut self.memo_table,
821 &null_matched,
822 chunk,
823 &self.metrics,
824 );
825 #[for_await]
826 for chunk in st1 {
827 let chunk = chunk?;
828 let new_chunk = apply_indices_map(chunk, &self.output_indices);
829 yield Message::Chunk(new_chunk);
830 }
831 }
832 }
833 InternalMessage::Barrier(updates, barrier) => {
834 let update_vnode_bitmap = barrier.as_update_vnode_bitmap(self.ctx.id);
835 let right_update_vnode_bitmap = if self.is_broadcast {
836 None
837 } else {
838 update_vnode_bitmap.clone()
839 };
840
841 self.right_table.update(
844 updates,
845 &self.right_join_keys,
846 &right_stream_key_indices,
847 )?;
848 let right_post_commit = self.right_table.source.commit(barrier.epoch).await?;
849 let memo_post_commit = if !APPEND_ONLY {
850 Some(
851 self.memo_table
852 .as_mut()
853 .unwrap()
854 .commit(barrier.epoch)
855 .await?,
856 )
857 } else {
858 None
859 };
860
861 yield Message::Barrier(barrier);
862
863 if let Some((_, true)) = right_post_commit
864 .post_yield_barrier(right_update_vnode_bitmap)
865 .await?
866 {
867 self.right_table.cache.clear();
868 }
869 if let Some(memo_post_commit) = memo_post_commit {
870 memo_post_commit
871 .post_yield_barrier(update_vnode_bitmap.clone())
872 .await?;
873 }
874 }
875 }
876 }
877 }
878}
879
880impl<
881 K: HashKey,
882 S: StateStore,
883 SD: ValueRowSerde,
884 const T: JoinTypePrimitive,
885 const APPEND_ONLY: bool,
886> Execute for TemporalJoinExecutor<K, S, SD, T, APPEND_ONLY>
887{
888 fn execute(self: Box<Self>) -> super::BoxedMessageStream {
889 self.into_stream().boxed()
890 }
891}
892
893#[cfg(test)]
894mod tests {
895 use std::collections::HashSet;
896 use std::sync::Arc;
897 use std::sync::atomic::AtomicU64;
898
899 use risingwave_common::array::*;
900 use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema, TableId};
901 use risingwave_common::hash::Key32;
902 use risingwave_common::types::{DataType, ScalarRefImpl};
903 use risingwave_common::util::epoch::{EpochPair, test_epoch};
904 use risingwave_common::util::sort_util::OrderType;
905 use risingwave_common::util::value_encoding::BasicSerde;
906 use risingwave_hummock_test::test_utils::prepare_hummock_test_env;
907 use risingwave_storage::hummock::HummockStorage;
908
909 use super::*;
910 use crate::common::table::state_table::{
911 StateTable, StateTableBuilder, StateTableOpConsistencyLevel,
912 };
913 use crate::common::table::test_utils::gen_pbtable;
914 use crate::executor::monitor::StreamingMetrics;
915 use crate::executor::test_utils::{MockSource, StreamExecutorTestExt};
916 use crate::executor::{ActorContext, ExecutorInfo, JoinType};
917
918 #[tokio::test]
935 async fn test_temporal_join_pk_prefix_staging_merge() {
936 let test_env = prepare_hummock_test_env().await;
937 let table_id = TableId::new(1);
938
939 let right_col_descs = vec![
943 ColumnDesc::unnamed(ColumnId::new(1), DataType::Int32),
944 ColumnDesc::unnamed(ColumnId::new(2), DataType::Int32),
945 ColumnDesc::unnamed(ColumnId::new(3), DataType::Int32),
946 ];
947 let order_types = vec![OrderType::ascending(), OrderType::ascending()];
948 let pk_indices = vec![0usize, 1];
949 let pbtable = gen_pbtable(table_id, right_col_descs, order_types, pk_indices, 2);
950
951 test_env.register_table(pbtable.clone()).await;
952
953 {
955 let mut setup_table = StateTable::<HummockStorage>::from_table_catalog_inconsistent_op(
956 &pbtable,
957 test_env.storage.clone(),
958 None,
959 )
960 .await;
961 test_env
962 .storage
963 .start_epoch(test_epoch(1), HashSet::from_iter([table_id]));
964 setup_table
965 .init_epoch(EpochPair::new_test_epoch(test_epoch(1)))
966 .await
967 .unwrap();
968 setup_table.insert(OwnedRow::new(vec![
969 Some(1i32.into()),
970 Some(1i32.into()),
971 Some(100i32.into()),
972 ]));
973 setup_table.insert(OwnedRow::new(vec![
974 Some(1i32.into()),
975 Some(2i32.into()),
976 Some(200i32.into()),
977 ]));
978 setup_table.insert(OwnedRow::new(vec![
979 Some(2i32.into()),
980 Some(1i32.into()),
981 Some(300i32.into()),
982 ]));
983 test_env
984 .storage
985 .start_epoch(test_epoch(2), HashSet::from_iter([table_id]));
986 setup_table
987 .commit_for_test(EpochPair::new_test_epoch(test_epoch(2)))
988 .await
989 .unwrap();
990 test_env.commit_epoch(test_epoch(1)).await;
992 }
993
994 let output_column_ids = vec![ColumnId::new(1), ColumnId::new(2), ColumnId::new(3)];
996 let right_table = StateTableBuilder::<_, BasicSerde, true, _>::new(
997 &pbtable,
998 test_env.storage.clone(),
999 None,
1000 )
1001 .with_op_consistency_level(StateTableOpConsistencyLevel::Inconsistent)
1002 .with_output_column_ids(output_column_ids)
1003 .forbid_preload_all_rows()
1004 .build()
1005 .await;
1006
1007 let left_schema = Schema::new(vec![
1009 Field::unnamed(DataType::Int32),
1010 Field::unnamed(DataType::Int32),
1011 ]);
1012 let (mut left_tx, left_source) = MockSource::channel();
1013 let left_executor = left_source.into_executor(left_schema.clone(), vec![0]);
1014
1015 let right_schema = Schema::new(vec![
1018 Field::unnamed(DataType::Int32),
1019 Field::unnamed(DataType::Int32),
1020 Field::unnamed(DataType::Int32),
1021 ]);
1022 let (mut right_tx, right_source) = MockSource::channel();
1023 let right_executor = right_source.into_executor(right_schema.clone(), vec![0, 1]);
1024
1025 let table_stream_key_indices = vec![0usize, 1];
1027
1028 let left_join_keys = vec![0usize];
1030 let right_join_keys = vec![0usize];
1031 let null_safe = vec![false];
1032 let join_key_data_types = vec![DataType::Int32];
1033
1034 let output_indices = vec![0usize, 1, 2, 3, 4];
1036 let output_schema = Schema::new(vec![
1037 Field::unnamed(DataType::Int32),
1038 Field::unnamed(DataType::Int32),
1039 Field::unnamed(DataType::Int32),
1040 Field::unnamed(DataType::Int32),
1041 Field::unnamed(DataType::Int32),
1042 ]);
1043 let info = ExecutorInfo::for_test(output_schema, vec![], "TemporalJoinTest".to_owned(), 0);
1044
1045 let executor = TemporalJoinExecutor::<
1046 Key32,
1047 HummockStorage,
1048 BasicSerde,
1049 { JoinType::Inner },
1050 true,
1051 >::new(
1052 ActorContext::for_test(0),
1053 info.clone(),
1054 left_executor,
1055 right_executor,
1056 right_table,
1057 left_join_keys,
1058 right_join_keys,
1059 null_safe,
1060 None, output_indices,
1062 table_stream_key_indices,
1063 Arc::new(AtomicU64::new(0)),
1064 Arc::new(StreamingMetrics::unused()),
1065 1024,
1066 join_key_data_types,
1067 None, false,
1069 );
1070
1071 let mut stream = Box::new(executor).execute();
1072
1073 left_tx.push_barrier_with_prev_epoch_for_test(test_epoch(2), test_epoch(1), false);
1075 right_tx.push_barrier_with_prev_epoch_for_test(test_epoch(2), test_epoch(1), false);
1076 stream.expect_barrier().await;
1077
1078 right_tx.push_chunk(StreamChunk::from_pretty(
1082 " i i i
1083 + 3 1 400",
1084 ));
1085 test_env
1087 .storage
1088 .start_epoch(test_epoch(3), HashSet::from_iter([table_id]));
1089 left_tx.push_barrier_with_prev_epoch_for_test(test_epoch(3), test_epoch(2), false);
1090 right_tx.push_barrier_with_prev_epoch_for_test(test_epoch(3), test_epoch(2), false);
1091 stream.expect_barrier().await;
1092
1093 test_env
1095 .storage
1096 .start_epoch(test_epoch(4), HashSet::from_iter([table_id]));
1097
1098 left_tx.push_chunk(StreamChunk::from_pretty(
1102 " i i
1103 + 1 111
1104 + 3 333",
1105 ));
1106 left_tx.push_barrier_with_prev_epoch_for_test(test_epoch(4), test_epoch(3), true);
1107 right_tx.push_barrier_with_prev_epoch_for_test(test_epoch(4), test_epoch(3), true);
1108
1109 let mut output_rows: Vec<[i32; 5]> = vec![];
1111 loop {
1112 match stream.next().await.unwrap().unwrap() {
1113 Message::Chunk(chunk) => {
1114 for (op, row) in chunk.rows() {
1115 assert_eq!(op, Op::Insert);
1116 let row: [i32; 5] =
1117 std::array::from_fn(|i| match row.datum_at(i).unwrap() {
1118 ScalarRefImpl::Int32(v) => v,
1119 _ => panic!("expected Int32"),
1120 });
1121 output_rows.push(row);
1122 }
1123 }
1124 Message::Barrier(_) => break,
1125 _ => {}
1126 }
1127 }
1128
1129 output_rows.sort();
1130 assert_eq!(
1131 output_rows,
1132 vec![
1133 [1, 111, 1, 1, 100], [1, 111, 1, 2, 200], [3, 333, 3, 1, 400], ]
1137 );
1138 }
1139}