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