1use std::collections::HashSet;
16use std::ops::Bound;
17
18use bytes::Bytes;
19use futures::future::Either;
20use futures::stream::{self, select_with_strategy};
21use futures_async_stream::try_stream;
22use itertools::Itertools;
23use risingwave_common::array::Op;
24use risingwave_common::bitmap::Bitmap;
25use risingwave_common::catalog::{
26 ColumnDesc, ConflictBehavior, TableId, checked_conflict_behaviors,
27};
28use risingwave_common::hash::{VirtualNode, VnodeBitmapExt};
29use risingwave_common::row::{OwnedRow, RowExt};
30use risingwave_common::types::DataType;
31use risingwave_common::util::sort_util::ColumnOrder;
32use risingwave_common::util::value_encoding::BasicSerde;
33use risingwave_pb::catalog::Table;
34use risingwave_pb::catalog::table::Engine;
35use risingwave_pb::id::{SourceId, SubscriberId};
36use risingwave_pb::stream_plan::SubscriptionUpstreamInfo;
37use risingwave_storage::row_serde::value_serde::{ValueRowSerde, ValueRowSerdeNew};
38use risingwave_storage::store::PrefetchOptions;
39use risingwave_storage::table::KeyedRow;
40
41use crate::common::change_buffer::output_kind as cb_kind;
42use crate::common::metrics::MetricsInfo;
43use crate::common::table::state_table::{
44 StateTableBuilder, StateTableInner, StateTableOpConsistencyLevel,
45};
46use crate::consistency::consistency_panic;
47use crate::executor::error::ErrorKind;
48use crate::executor::monitor::MaterializeMetrics;
49use crate::executor::mview::RefreshProgressTable;
50use crate::executor::mview::cache::{MaterializeCache, toast};
51use crate::executor::prelude::*;
52use crate::executor::{BarrierInner, BarrierMutationType};
53use crate::task::LocalBarrierManager;
54
55#[derive(Debug, Clone)]
56pub enum MaterializeStreamState<M> {
57 NormalIngestion,
58 MergingData,
59 CleanUp,
60 CommitAndYieldBarrier {
61 barrier: BarrierInner<M>,
62 expect_next_state: Box<MaterializeStreamState<M>>,
63 },
64 RefreshEnd,
65}
66
67pub struct MaterializeExecutor<S: StateStore, SD: ValueRowSerde> {
69 input: Executor,
70
71 schema: Schema,
72
73 state_table: StateTableInner<S, SD>,
74
75 stream_key_indices: Vec<usize>,
86
87 arrange_key_indices: Vec<usize>,
89
90 actor_context: ActorContextRef,
91
92 materialize_cache: Option<MaterializeCache>,
94
95 toastable_column_indices: Option<Vec<usize>>,
100
101 conflict_behavior: ConflictBehavior,
102
103 cleaned_by_ttl_watermark: bool,
105
106 version_column_indices: Vec<u32>,
107
108 may_have_downstream: bool,
109
110 subscriber_ids: HashSet<SubscriberId>,
111
112 metrics: MaterializeMetrics,
113
114 is_dummy_table: bool,
117
118 refresh_args: Option<RefreshableMaterializeArgs<S, SD>>,
120
121 local_barrier_manager: LocalBarrierManager,
123}
124
125pub struct RefreshableMaterializeArgs<S: StateStore, SD: ValueRowSerde> {
127 pub table_catalog: Table,
129
130 pub staging_table_catalog: Table,
132
133 pub staging_table: StateTableInner<S, SD>,
142
143 pub progress_table: RefreshProgressTable<S>,
145
146 pub refresh_start_prev_epoch: Option<u64>,
150
151 pub table_id: TableId,
153}
154
155impl<S: StateStore, SD: ValueRowSerde> RefreshableMaterializeArgs<S, SD> {
156 fn is_refreshing(&self) -> bool {
157 self.refresh_start_prev_epoch.is_some()
158 }
159
160 pub async fn new(
162 store: S,
163 table_catalog: &Table,
164 staging_table_catalog: &Table,
165 progress_state_table: &Table,
166 vnodes: Option<Arc<Bitmap>>,
167 ) -> Self {
168 let table_id = table_catalog.id;
169
170 let staging_table = StateTableInner::from_table_catalog_inconsistent_op(
172 staging_table_catalog,
173 store.clone(),
174 vnodes.clone(),
175 )
176 .await;
177
178 let progress_state_table = StateTableInner::from_table_catalog_inconsistent_op(
179 progress_state_table,
180 store,
181 vnodes,
182 )
183 .await;
184
185 let pk_len = table_catalog.pk.len();
187 let progress_table = RefreshProgressTable::new(progress_state_table, pk_len);
188
189 debug_assert_eq!(staging_table.vnodes(), progress_table.vnodes());
190
191 Self {
192 table_catalog: table_catalog.clone(),
193 staging_table_catalog: staging_table_catalog.clone(),
194 staging_table,
195 progress_table,
196 refresh_start_prev_epoch: None,
197 table_id,
198 }
199 }
200}
201
202fn get_op_consistency_level(
203 cleaned_by_ttl_watermark: bool,
204 conflict_behavior: ConflictBehavior,
205 may_have_downstream: bool,
206 subscriber_ids: &HashSet<SubscriberId>,
207) -> StateTableOpConsistencyLevel {
208 if cleaned_by_ttl_watermark {
209 StateTableOpConsistencyLevel::Inconsistent
213 } else if !subscriber_ids.is_empty() {
214 StateTableOpConsistencyLevel::LogStoreEnabled
215 } else if !may_have_downstream && matches!(conflict_behavior, ConflictBehavior::Overwrite) {
216 StateTableOpConsistencyLevel::Inconsistent
219 } else {
220 StateTableOpConsistencyLevel::ConsistentOldValue
221 }
222}
223
224impl<S: StateStore, SD: ValueRowSerde> MaterializeExecutor<S, SD> {
225 #[expect(clippy::too_many_arguments)]
229 pub async fn new(
230 input: Executor,
231 schema: Schema,
232 store: S,
233 arrange_key: Vec<ColumnOrder>,
234 actor_context: ActorContextRef,
235 vnodes: Option<Arc<Bitmap>>,
236 table_catalog: &Table,
237 watermark_epoch: AtomicU64Ref,
238 conflict_behavior: ConflictBehavior,
239 version_column_indices: Vec<u32>,
240 metrics: Arc<StreamingMetrics>,
241 refresh_args: Option<RefreshableMaterializeArgs<S, SD>>,
242 cleaned_by_ttl_watermark: bool,
243 local_barrier_manager: LocalBarrierManager,
244 ) -> Self {
245 let table_columns: Vec<ColumnDesc> = table_catalog
246 .columns
247 .iter()
248 .map(|col| col.column_desc.as_ref().unwrap().into())
249 .collect();
250
251 let toastable_column_indices = if table_catalog.cdc_table_type()
254 == risingwave_pb::catalog::table::CdcTableType::Postgres
255 {
256 let toastable_indices: Vec<usize> = table_columns
257 .iter()
258 .enumerate()
259 .filter_map(|(index, column)| match &column.data_type {
260 DataType::Varchar
270 | DataType::List(_)
271 | DataType::Bytea
272 | DataType::Jsonb
273 | DataType::Vector(_) => Some(index),
274 _ => None,
275 })
276 .collect();
277
278 if toastable_indices.is_empty() {
279 None
280 } else {
281 Some(toastable_indices)
282 }
283 } else {
284 None
285 };
286
287 let row_serde: BasicSerde = BasicSerde::new(
288 Arc::from_iter(table_catalog.value_indices.iter().map(|val| *val as usize)),
289 Arc::from(table_columns.into_boxed_slice()),
290 );
291
292 let stream_key_indices: Vec<usize> = table_catalog
293 .stream_key
294 .iter()
295 .map(|idx| *idx as usize)
296 .collect();
297 let arrange_key_indices: Vec<usize> = arrange_key.iter().map(|k| k.column_index).collect();
298 let may_have_downstream = actor_context.initial_dispatch_num != 0;
299 let subscriber_ids = actor_context.initial_subscriber_ids.clone();
300 let op_consistency_level = get_op_consistency_level(
301 cleaned_by_ttl_watermark,
302 conflict_behavior,
303 may_have_downstream,
304 &subscriber_ids,
305 );
306 let state_table_metrics = metrics.new_state_table_metrics(
307 table_catalog.id,
308 actor_context.id,
309 actor_context.fragment_id,
310 );
311 let state_table = StateTableBuilder::new(table_catalog, store, vnodes)
313 .with_op_consistency_level(op_consistency_level)
314 .enable_preload_all_rows_by_config(&actor_context.config)
315 .enable_vnode_key_stats(
316 actor_context
317 .config
318 .developer
319 .enable_vnode_key_stats_for_materialize,
320 &actor_context.config,
321 )
322 .with_metrics(state_table_metrics)
323 .build()
324 .await;
325
326 let mv_metrics = metrics.new_materialize_metrics(
327 table_catalog.id,
328 actor_context.id,
329 actor_context.fragment_id,
330 );
331 let cache_metrics = metrics.new_materialize_cache_metrics(
332 table_catalog.id,
333 actor_context.id,
334 actor_context.fragment_id,
335 );
336
337 let metrics_info =
338 MetricsInfo::new(metrics, table_catalog.id, actor_context.id, "Materialize");
339
340 let is_dummy_table =
341 table_catalog.engine == Some(Engine::Iceberg as i32) && table_catalog.append_only;
342
343 Self {
344 input,
345 schema,
346 state_table,
347 stream_key_indices,
348 arrange_key_indices,
349 actor_context,
350 materialize_cache: MaterializeCache::new(
351 watermark_epoch,
352 metrics_info,
353 row_serde,
354 version_column_indices.clone(),
355 conflict_behavior,
356 toastable_column_indices.clone(),
357 cache_metrics,
358 ),
359 toastable_column_indices,
360 conflict_behavior,
361 cleaned_by_ttl_watermark,
362 version_column_indices,
363 is_dummy_table,
364 may_have_downstream,
365 subscriber_ids,
366 metrics: mv_metrics,
367 refresh_args,
368 local_barrier_manager,
369 }
370 }
371
372 #[try_stream(ok = Message, error = StreamExecutorError)]
373 async fn execute_inner(mut self) {
374 let mv_table_id = self.state_table.table_id();
375 let data_types = self.schema.data_types();
376 let mut input = self.input.execute();
377
378 let barrier = expect_first_barrier(&mut input).await?;
379 let first_epoch = barrier.epoch;
380 let _barrier_epoch = barrier.epoch; yield Message::Barrier(barrier);
383 self.state_table.init_epoch(first_epoch).await?;
384
385 let mut inner_state =
387 Box::new(MaterializeStreamState::<BarrierMutationType>::NormalIngestion);
388 if let Some(ref mut refresh_args) = self.refresh_args {
389 refresh_args.staging_table.init_epoch(first_epoch).await?;
390 refresh_args.progress_table.recover(first_epoch).await?;
391
392 let progress_stats = refresh_args.progress_table.get_progress_stats();
395 if progress_stats.total_vnodes > 0 && !progress_stats.is_complete() {
396 tracing::info!(
397 table_id = %refresh_args.table_id,
398 completed_vnodes = progress_stats.completed_vnodes,
399 total_vnodes = progress_stats.total_vnodes,
400 "found progress of a refresh interrupted by recovery"
401 );
402 }
403 }
404
405 'main_loop: loop {
407 match *inner_state {
408 MaterializeStreamState::NormalIngestion => {
409 #[for_await]
410 '_normal_ingest: for msg in input.by_ref() {
411 let msg = msg?;
412 if let Some(cache) = &mut self.materialize_cache {
413 cache.evict();
414 }
415
416 match msg {
417 Message::Watermark(w) => {
418 if self.cleaned_by_ttl_watermark
419 && self.state_table.clean_watermark_index == Some(w.col_idx)
420 {
421 self.state_table.update_watermark(w.val.clone());
422 }
423 yield Message::Watermark(w);
424 }
425 Message::Chunk(chunk) if self.is_dummy_table => {
426 self.metrics
427 .materialize_input_row_count
428 .inc_by(chunk.cardinality() as u64);
429 yield Message::Chunk(chunk);
430 }
431 Message::Chunk(chunk) => {
432 self.metrics
433 .materialize_input_row_count
434 .inc_by(chunk.cardinality() as u64);
435
436 let optimized_conflict_behavior = if let ConflictBehavior::Overwrite =
449 self.conflict_behavior
450 && !self.state_table.is_consistent_op()
451 && !self.cleaned_by_ttl_watermark
452 && self.version_column_indices.is_empty()
453 && self.toastable_column_indices.as_ref().is_none_or(
454 |indices| {
455 !toast::chunk_has_unavailable_value(&chunk, indices)
456 },
457 ) {
458 ConflictBehavior::NoCheck
459 } else {
460 self.conflict_behavior
461 };
462
463 match optimized_conflict_behavior {
464 checked_conflict_behaviors!() => {
465 if chunk.cardinality() == 0 {
466 continue;
468 }
469
470 if let Some(ref mut refresh_args) = self.refresh_args
473 && refresh_args.is_refreshing()
474 {
475 let key_chunk = chunk
476 .clone()
477 .project(self.state_table.pk_indices());
478 tracing::trace!(
479 staging_chunk = %key_chunk.to_pretty(),
480 input_chunk = %chunk.to_pretty(),
481 "writing to staging table"
482 );
483 if cfg!(debug_assertions) {
484 assert!(
486 key_chunk
487 .ops()
488 .iter()
489 .all(|op| op == &Op::Insert)
490 );
491 }
492 refresh_args
493 .staging_table
494 .write_chunk(key_chunk.clone());
495 refresh_args.staging_table.try_flush().await?;
496 }
497
498 let cache = self.materialize_cache.as_mut().unwrap();
499 let change_buffer =
500 cache.handle_new(chunk, &self.state_table).await?;
501
502 let output_chunk = if self.stream_key_indices
503 == self.state_table.pk_indices()
504 {
505 change_buffer.into_chunk::<{ cb_kind::RETRACT }>(
506 data_types.clone(),
507 )
508 } else {
509 debug_assert!(
512 self.state_table
513 .pk_indices()
514 .iter()
515 .all(|&i| self.stream_key_indices.contains(&i))
516 );
517 change_buffer.into_chunk_with_key(
518 data_types.clone(),
519 &self.stream_key_indices,
520 )
521 };
522
523 match output_chunk {
524 Some(output_chunk) => {
525 self.state_table.write_chunk(output_chunk.clone());
526 self.state_table.try_flush().await?;
527 yield Message::Chunk(output_chunk);
528 }
529 None => continue,
530 }
531 }
532 ConflictBehavior::NoCheck => {
533 if self.toastable_column_indices.is_some()
536 && let Some(cache) = &mut self.materialize_cache
537 {
538 cache.write_through(&chunk, &self.state_table);
539 }
540 self.state_table.write_chunk(chunk.clone());
541 self.state_table.try_flush().await?;
542
543 if let Some(ref mut refresh_args) = self.refresh_args
545 && refresh_args.is_refreshing()
546 {
547 let key_chunk = chunk
548 .clone()
549 .project(self.state_table.pk_indices());
550 tracing::trace!(
551 staging_chunk = %key_chunk.to_pretty(),
552 input_chunk = %chunk.to_pretty(),
553 "writing to staging table"
554 );
555 if cfg!(debug_assertions) {
556 assert!(
558 key_chunk
559 .ops()
560 .iter()
561 .all(|op| op == &Op::Insert)
562 );
563 }
564 refresh_args
565 .staging_table
566 .write_chunk(key_chunk.clone());
567 refresh_args.staging_table.try_flush().await?;
568 }
569
570 yield Message::Chunk(chunk);
571 }
572 }
573 }
574 Message::Barrier(barrier) => {
575 *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
576 barrier,
577 expect_next_state: Box::new(
578 MaterializeStreamState::NormalIngestion,
579 ),
580 };
581 continue 'main_loop;
582 }
583 }
584 }
585
586 return Err(StreamExecutorError::from(ErrorKind::Uncategorized(
587 anyhow::anyhow!(
588 "Input stream terminated unexpectedly during normal ingestion"
589 ),
590 )));
591 }
592 MaterializeStreamState::MergingData => {
593 let Some(refresh_args) = self.refresh_args.as_mut() else {
594 panic!(
595 "MaterializeExecutor entered CleanUp state without refresh_args configured"
596 );
597 };
598 tracing::info!(table_id = %refresh_args.table_id, "on_load_finish: Starting table replacement operation");
599
600 debug_assert_eq!(
601 self.state_table.vnodes(),
602 refresh_args.staging_table.vnodes()
603 );
604 debug_assert_eq!(
605 refresh_args.staging_table.vnodes(),
606 refresh_args.progress_table.vnodes()
607 );
608
609 let mut rows_to_delete = vec![];
610 let mut merge_complete = false;
611 let mut pending_barrier: Option<Barrier> = None;
612
613 {
615 let left_input = input.by_ref().map(Either::Left);
616 let right_merge_sort = pin!(
617 Self::make_mergesort_stream(
618 &self.state_table,
619 &refresh_args.staging_table,
620 &mut refresh_args.progress_table
621 )
622 .map(Either::Right)
623 );
624
625 let mut merge_stream =
628 select_with_strategy(left_input, right_merge_sort, |_: &mut ()| {
629 stream::PollNext::Left
630 });
631
632 #[for_await]
633 'merge_stream: for either in &mut merge_stream {
634 match either {
635 Either::Left(msg) => {
636 let msg = msg?;
637 match msg {
638 Message::Watermark(w) => yield Message::Watermark(w),
639 Message::Chunk(chunk) => {
640 tracing::warn!(chunk = %chunk.to_pretty(), "chunk is ignored during merge phase");
641 }
642 Message::Barrier(b) => {
643 pending_barrier = Some(b);
644 break 'merge_stream;
645 }
646 }
647 }
648 Either::Right(result) => {
649 match result? {
650 Some((_vnode, row)) => {
651 rows_to_delete.push(row);
652 }
653 None => {
654 merge_complete = true;
656
657 }
659 }
660 }
661 }
662 }
663 }
664
665 for row in &rows_to_delete {
667 self.state_table.delete(row);
668 }
669 if let Some(cache) = &mut self.materialize_cache {
670 cache.invalidate_rows(&rows_to_delete, &self.state_table);
671 }
672 if !rows_to_delete.is_empty() {
673 let to_delete_chunk = StreamChunk::from_rows(
674 &rows_to_delete
675 .iter()
676 .map(|row| (Op::Delete, row))
677 .collect_vec(),
678 &self.schema.data_types(),
679 );
680
681 yield Message::Chunk(to_delete_chunk);
682 }
683
684 assert!(pending_barrier.is_some(), "pending barrier is not set");
686
687 *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
688 barrier: pending_barrier.unwrap(),
689 expect_next_state: if merge_complete {
690 Box::new(MaterializeStreamState::CleanUp)
691 } else {
692 Box::new(MaterializeStreamState::MergingData)
693 },
694 };
695 continue 'main_loop;
696 }
697 MaterializeStreamState::CleanUp => {
698 let Some(refresh_args) = self.refresh_args.as_mut() else {
699 panic!(
700 "MaterializeExecutor entered MergingData state without refresh_args configured"
701 );
702 };
703 tracing::info!(table_id = %refresh_args.table_id, "on_load_finish: resuming CleanUp Stage");
704
705 #[for_await]
706 for msg in input.by_ref() {
707 let msg = msg?;
708 match msg {
709 Message::Watermark(w) => {
710 if self.cleaned_by_ttl_watermark
711 && self.state_table.clean_watermark_index == Some(w.col_idx)
712 {
713 self.state_table.update_watermark(w.val.clone());
714 }
715 yield Message::Watermark(w)
716 }
717 Message::Chunk(chunk) => {
718 tracing::warn!(chunk = %chunk.to_pretty(), "chunk is ignored during merge phase");
719 }
720 Message::Barrier(barrier) if !barrier.is_checkpoint() => {
721 *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
722 barrier,
723 expect_next_state: Box::new(MaterializeStreamState::CleanUp),
724 };
725 continue 'main_loop;
726 }
727 Message::Barrier(barrier) => {
728 self.local_barrier_manager.report_refresh_finished(
729 barrier.epoch,
730 self.actor_context.id,
731 refresh_args.table_id,
732 );
733 tracing::debug!(table_id = %refresh_args.table_id, "on_load_finish: reported refresh finished");
734
735 *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
736 barrier,
737 expect_next_state: Box::new(MaterializeStreamState::RefreshEnd),
738 };
739 continue 'main_loop;
740 }
741 }
742 }
743 }
744 MaterializeStreamState::RefreshEnd => {
745 let Some(refresh_args) = self.refresh_args.as_mut() else {
746 panic!(
747 "MaterializeExecutor entered RefreshEnd state without refresh_args configured"
748 );
749 };
750 tracing::info!(table_id = %refresh_args.table_id, "RefreshEnd: Refresh completed");
751 refresh_args.refresh_start_prev_epoch = None;
752 *inner_state = MaterializeStreamState::NormalIngestion;
753 continue 'main_loop;
754 }
755 MaterializeStreamState::CommitAndYieldBarrier {
756 barrier,
757 mut expect_next_state,
758 } => {
759 let mut wait_staging_visible = None;
760 if let Some(ref mut refresh_args) = self.refresh_args {
761 match barrier.mutation.as_deref() {
762 Some(Mutation::RefreshStart {
763 table_id: refresh_table_id,
764 associated_source_id: _,
765 }) if *refresh_table_id == refresh_args.table_id => {
766 if refresh_args.is_refreshing() {
767 consistency_panic!(
768 table_id = %refresh_table_id,
769 "RefreshStart received while a refresh is in progress"
770 );
771 }
772 refresh_args.refresh_start_prev_epoch = Some(barrier.epoch.prev);
773 tracing::info!(table_id = %refresh_table_id, "RefreshStart barrier received");
774
775 Self::init_refresh_progress(
777 &self.state_table,
778 &mut refresh_args.progress_table,
779 barrier.epoch.curr,
780 )?;
781 }
782 Some(Mutation::LoadFinish {
783 associated_source_id: load_finish_source_id,
784 }) => {
785 let associated_source_id: SourceId = match refresh_args
787 .table_catalog
788 .optional_associated_source_id
789 {
790 Some(id) => id.into(),
791 None => unreachable!("associated_source_id is not set"),
792 };
793
794 if *load_finish_source_id == associated_source_id {
795 if !refresh_args.is_refreshing() {
796 consistency_panic!(
798 table_id = %refresh_args.table_id,
799 "LoadFinish received without a refresh in progress"
800 );
801 } else {
802 tracing::info!(
803 %load_finish_source_id,
804 "LoadFinish received, starting data replacement"
805 );
806 *expect_next_state =
807 MaterializeStreamState::<_>::MergingData;
808 wait_staging_visible =
809 refresh_args.refresh_start_prev_epoch;
810 }
811 }
812 }
813 _ => {}
814 }
815 }
816
817 if !self.may_have_downstream
821 && barrier.has_more_downstream_fragments(self.actor_context.id)
822 {
823 self.may_have_downstream = true;
824 }
825 Self::may_update_depended_subscriptions(
826 &mut self.subscriber_ids,
827 &barrier,
828 mv_table_id,
829 );
830 let op_consistency_level = get_op_consistency_level(
831 self.cleaned_by_ttl_watermark,
832 self.conflict_behavior,
833 self.may_have_downstream,
834 &self.subscriber_ids,
835 );
836 let post_commit = self
837 .state_table
838 .commit_may_switch_consistent_op(barrier.epoch, op_consistency_level)
839 .await?;
840
841 let update_vnode_bitmap = barrier.as_update_vnode_bitmap(self.actor_context.id);
842
843 let refresh_post_commit = if let Some(ref mut refresh_args) = self.refresh_args
845 {
846 Some((
847 refresh_args.staging_table.commit(barrier.epoch).await?,
848 refresh_args.progress_table.commit(barrier.epoch).await?,
849 ))
850 } else {
851 None
852 };
853
854 let b_epoch = barrier.epoch;
855 yield Message::Barrier(barrier);
856
857 if let Some((_, cache_may_stale)) = post_commit
859 .post_yield_barrier(update_vnode_bitmap.clone())
860 .await?
861 && cache_may_stale
862 && let Some(cache) = &mut self.materialize_cache
863 {
864 cache.clear();
865 }
866
867 if let Some((staging_post_commit, progress_post_commit)) = refresh_post_commit {
869 staging_post_commit
870 .post_yield_barrier(update_vnode_bitmap.clone())
871 .await?;
872 progress_post_commit
873 .post_yield_barrier(update_vnode_bitmap)
874 .await?;
875 }
876
877 if let (Some(epoch), Some(refresh_args)) =
878 (wait_staging_visible, self.refresh_args.as_ref())
879 {
880 refresh_args
883 .staging_table
884 .try_wait_committed_epoch(epoch)
885 .await?;
886 }
887
888 self.metrics
889 .materialize_current_epoch
890 .set(b_epoch.curr as i64);
891
892 *inner_state = *expect_next_state;
895 }
896 }
897 }
898 }
899
900 #[try_stream(ok = Option<(VirtualNode, OwnedRow)>, error = StreamExecutorError)]
904 async fn make_mergesort_stream<'a>(
905 main_table: &'a StateTableInner<S, SD>,
906 staging_table: &'a StateTableInner<S, SD>,
907 progress_table: &'a mut RefreshProgressTable<S>,
908 ) {
909 for vnode in main_table.vnodes().clone().iter_vnodes() {
910 let mut processed_rows = 0;
911 let pk_range: (Bound<OwnedRow>, Bound<OwnedRow>) =
913 if let Some(current_entry) = progress_table.get_progress(vnode) {
914 if current_entry.is_completed {
915 tracing::debug!(vnode = vnode.to_index(), "vnode already merged");
916 continue;
917 }
918 processed_rows += current_entry.processed_rows;
919 tracing::debug!(vnode = vnode.to_index(), "Started merging VNode");
920
921 if let Some(current_state) = ¤t_entry.current_pos {
922 (Bound::Excluded(current_state.clone()), Bound::Unbounded)
923 } else {
924 (Bound::Unbounded, Bound::Unbounded)
925 }
926 } else {
927 (Bound::Unbounded, Bound::Unbounded)
928 };
929
930 let iter_main = main_table
931 .iter_keyed_row_with_vnode(
932 vnode,
933 &pk_range,
934 PrefetchOptions::prefetch_for_large_range_scan(),
935 )
936 .await?;
937 let iter_staging = staging_table
938 .iter_keyed_row_with_vnode(
939 vnode,
940 &pk_range,
941 PrefetchOptions::prefetch_for_large_range_scan(),
942 )
943 .await?;
944
945 pin_mut!(iter_main);
946 pin_mut!(iter_staging);
947
948 let mut main_item: Option<KeyedRow<Bytes>> = iter_main.next().await.transpose()?;
950 let mut staging_item: Option<KeyedRow<Bytes>> =
951 iter_staging.next().await.transpose()?;
952
953 while let Some(main_kv) = main_item {
954 let main_key = main_kv.key();
955
956 let mut should_delete = false;
958 while let Some(staging_kv) = &staging_item {
959 let staging_key = staging_kv.key();
960 match main_key.cmp(staging_key) {
961 std::cmp::Ordering::Greater => {
962 staging_item = iter_staging.next().await.transpose()?;
964 }
965 std::cmp::Ordering::Equal => {
966 break;
968 }
969 std::cmp::Ordering::Less => {
970 should_delete = true;
972 break;
973 }
974 }
975 }
976
977 if staging_item.is_none() {
979 should_delete = true;
980 }
981
982 if should_delete {
983 yield Some((vnode, main_kv.row().clone()));
984 }
985
986 processed_rows += 1;
988 tracing::debug!(
989 "set progress table: vnode = {:?}, processed_rows = {:?}",
990 vnode,
991 processed_rows
992 );
993 progress_table.set_progress(
994 vnode,
995 Some(
996 main_kv
997 .row()
998 .project(main_table.pk_indices())
999 .to_owned_row(),
1000 ),
1001 false,
1002 processed_rows,
1003 )?;
1004 main_item = iter_main.next().await.transpose()?;
1005 }
1006
1007 if let Some(current_entry) = progress_table.get_progress(vnode) {
1009 progress_table.set_progress(
1010 vnode,
1011 current_entry.current_pos.clone(),
1012 true, current_entry.processed_rows,
1014 )?;
1015
1016 tracing::debug!(vnode = vnode.to_index(), "Completed merging VNode");
1017 }
1018 }
1019
1020 yield None;
1022 }
1023
1024 fn may_update_depended_subscriptions(
1026 depended_subscriptions: &mut HashSet<SubscriberId>,
1027 barrier: &Barrier,
1028 mv_table_id: TableId,
1029 ) {
1030 for subscriber_id in barrier.added_subscriber_on_mv_table(mv_table_id) {
1031 if !depended_subscriptions.insert(subscriber_id) {
1032 warn!(
1033 ?depended_subscriptions,
1034 %mv_table_id,
1035 %subscriber_id,
1036 "subscription id already exists"
1037 );
1038 }
1039 }
1040
1041 if let Some(subscriptions_to_drop) = barrier.as_subscriptions_to_drop() {
1042 for SubscriptionUpstreamInfo {
1043 subscriber_id,
1044 upstream_mv_table_id,
1045 } in subscriptions_to_drop
1046 {
1047 if *upstream_mv_table_id == mv_table_id
1048 && !depended_subscriptions.remove(subscriber_id)
1049 {
1050 warn!(
1051 ?depended_subscriptions,
1052 %mv_table_id,
1053 %subscriber_id,
1054 "drop non existing subscriber_id id"
1055 );
1056 }
1057 }
1058 }
1059 }
1060
1061 fn init_refresh_progress(
1063 state_table: &StateTableInner<S, SD>,
1064 progress_table: &mut RefreshProgressTable<S>,
1065 _epoch: u64,
1066 ) -> StreamExecutorResult<()> {
1067 debug_assert_eq!(state_table.vnodes(), progress_table.vnodes());
1068
1069 for vnode in state_table.vnodes().iter_vnodes() {
1071 progress_table.set_progress(
1072 vnode, None, false, 0, )?;
1076 }
1077
1078 tracing::info!(
1079 vnodes_count = state_table.vnodes().count_ones(),
1080 "Initialized refresh progress tracking for all VNodes"
1081 );
1082
1083 Ok(())
1084 }
1085}
1086
1087impl<S: StateStore> MaterializeExecutor<S, BasicSerde> {
1088 #[cfg(any(test, feature = "test"))]
1090 pub async fn for_test(
1091 input: Executor,
1092 store: S,
1093 table_id: TableId,
1094 keys: Vec<ColumnOrder>,
1095 column_ids: Vec<risingwave_common::catalog::ColumnId>,
1096 watermark_epoch: AtomicU64Ref,
1097 conflict_behavior: ConflictBehavior,
1098 ) -> Self {
1099 Self::for_test_inner(
1100 input,
1101 store,
1102 table_id,
1103 keys,
1104 column_ids,
1105 watermark_epoch,
1106 conflict_behavior,
1107 None,
1108 None,
1109 None,
1110 true,
1111 )
1112 .await
1113 }
1114
1115 #[cfg(any(test, feature = "test"))]
1116 #[expect(clippy::too_many_arguments)]
1117 pub async fn for_test_with_stream_key(
1118 input: Executor,
1119 store: S,
1120 table_id: TableId,
1121 keys: Vec<ColumnOrder>,
1122 stream_key: Vec<usize>,
1123 column_ids: Vec<risingwave_common::catalog::ColumnId>,
1124 watermark_epoch: AtomicU64Ref,
1125 conflict_behavior: ConflictBehavior,
1126 ) -> Self {
1127 Self::for_test_inner(
1128 input,
1129 store,
1130 table_id,
1131 keys,
1132 column_ids,
1133 watermark_epoch,
1134 conflict_behavior,
1135 Some(stream_key),
1136 None,
1137 None,
1138 true,
1139 )
1140 .await
1141 }
1142
1143 #[cfg(any(test, feature = "test"))]
1146 #[expect(clippy::too_many_arguments)]
1147 pub async fn for_test_with_ttl(
1148 input: Executor,
1149 store: S,
1150 table_id: TableId,
1151 keys: Vec<ColumnOrder>,
1152 column_ids: Vec<risingwave_common::catalog::ColumnId>,
1153 watermark_epoch: AtomicU64Ref,
1154 conflict_behavior: ConflictBehavior,
1155 ttl_column: usize,
1156 ) -> Self {
1157 let stream_key = keys
1158 .iter()
1159 .map(|k| k.column_index)
1160 .chain(std::iter::once(ttl_column))
1161 .collect();
1162 Self::for_test_inner(
1163 input,
1164 store,
1165 table_id,
1166 keys,
1167 column_ids,
1168 watermark_epoch,
1169 conflict_behavior,
1170 Some(stream_key),
1171 Some(ttl_column),
1172 None,
1173 true,
1174 )
1175 .await
1176 }
1177
1178 #[cfg(any(test, feature = "test"))]
1181 pub async fn for_test_with_toast(
1182 input: Executor,
1183 store: S,
1184 table_id: TableId,
1185 keys: Vec<ColumnOrder>,
1186 column_ids: Vec<risingwave_common::catalog::ColumnId>,
1187 toastable_column_indices: Vec<usize>,
1188 ) -> Self {
1189 Self::for_test_inner(
1190 input,
1191 store,
1192 table_id,
1193 keys,
1194 column_ids,
1195 Arc::new(std::sync::atomic::AtomicU64::new(0)),
1196 ConflictBehavior::Overwrite,
1197 None,
1198 None,
1199 Some(toastable_column_indices),
1200 false,
1201 )
1202 .await
1203 }
1204
1205 #[cfg(any(test, feature = "test"))]
1206 #[expect(clippy::too_many_arguments)]
1207 async fn for_test_inner(
1208 input: Executor,
1209 store: S,
1210 table_id: TableId,
1211 keys: Vec<ColumnOrder>,
1212 column_ids: Vec<risingwave_common::catalog::ColumnId>,
1213 watermark_epoch: AtomicU64Ref,
1214 conflict_behavior: ConflictBehavior,
1215 stream_key: Option<Vec<usize>>,
1216 ttl_column: Option<usize>,
1217 toastable_column_indices: Option<Vec<usize>>,
1218 may_have_downstream: bool,
1219 ) -> Self {
1220 use risingwave_common::util::iter_util::ZipEqFast;
1221
1222 let arrange_columns: Vec<usize> = keys.iter().map(|k| k.column_index).collect();
1223 let arrange_order_types = keys.iter().map(|k| k.order_type).collect();
1224 let schema = input.schema().clone();
1225 let columns: Vec<ColumnDesc> = column_ids
1226 .into_iter()
1227 .zip_eq_fast(schema.fields.iter())
1228 .map(|(column_id, field)| ColumnDesc::unnamed(column_id, field.data_type()))
1229 .collect_vec();
1230
1231 let row_serde = BasicSerde::new(
1232 Arc::from((0..columns.len()).collect_vec()),
1233 Arc::from(columns.clone().into_boxed_slice()),
1234 );
1235 let stream_key_indices = stream_key.unwrap_or_else(|| arrange_columns.clone());
1236 let mut table_catalog = crate::common::table::test_utils::gen_pbtable(
1237 table_id,
1238 columns,
1239 arrange_order_types,
1240 arrange_columns.clone(),
1241 0,
1242 );
1243 table_catalog.stream_key = stream_key_indices.iter().map(|i| *i as i32).collect();
1244 if let Some(ttl_column) = ttl_column {
1245 table_catalog.clean_watermark_indices = vec![ttl_column as u32];
1246 }
1247 let op_consistency_level = get_op_consistency_level(
1248 ttl_column.is_some(),
1249 conflict_behavior,
1250 may_have_downstream,
1251 &HashSet::new(),
1252 );
1253 let state_table = if op_consistency_level == StateTableOpConsistencyLevel::Inconsistent {
1254 StateTableInner::from_table_catalog_inconsistent_op(&table_catalog, store, None).await
1255 } else {
1256 StateTableInner::from_table_catalog(&table_catalog, store, None).await
1257 };
1258
1259 let unused = StreamingMetrics::unused();
1260 let metrics = unused.new_materialize_metrics(table_id, 1.into(), 2.into());
1261 let cache_metrics = unused.new_materialize_cache_metrics(table_id, 1.into(), 2.into());
1262
1263 Self {
1264 input,
1265 schema,
1266 state_table,
1267 stream_key_indices,
1268 arrange_key_indices: arrange_columns.clone(),
1269 actor_context: ActorContext::for_test(0),
1270 materialize_cache: MaterializeCache::new(
1271 watermark_epoch,
1272 MetricsInfo::for_test(),
1273 row_serde,
1274 vec![],
1275 conflict_behavior,
1276 toastable_column_indices.clone(),
1277 cache_metrics,
1278 ),
1279 toastable_column_indices,
1280 conflict_behavior,
1281 cleaned_by_ttl_watermark: ttl_column.is_some(),
1282 version_column_indices: vec![],
1283 is_dummy_table: false,
1284 may_have_downstream,
1285 subscriber_ids: HashSet::new(),
1286 metrics,
1287 refresh_args: None, local_barrier_manager: LocalBarrierManager::for_test(),
1289 }
1290 }
1291}
1292
1293impl<S: StateStore, SD: ValueRowSerde> Execute for MaterializeExecutor<S, SD> {
1294 fn execute(self: Box<Self>) -> BoxedMessageStream {
1295 self.execute_inner().boxed()
1296 }
1297}
1298
1299impl<S: StateStore, SD: ValueRowSerde> std::fmt::Debug for MaterializeExecutor<S, SD> {
1300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1301 f.debug_struct("MaterializeExecutor")
1302 .field("arrange_key_indices", &self.arrange_key_indices)
1303 .field("stream_key_indices", &self.stream_key_indices)
1304 .finish()
1305 }
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310
1311 use std::iter;
1312 use std::sync::atomic::AtomicU64;
1313
1314 use rand::rngs::SmallRng;
1315 use rand::{Rng, RngCore, SeedableRng};
1316 use risingwave_common::array::stream_chunk::{StreamChunkMut, StreamChunkTestExt};
1317 use risingwave_common::catalog::Field;
1318 use risingwave_common::util::epoch::test_epoch;
1319 use risingwave_common::util::sort_util::OrderType;
1320 use risingwave_hummock_sdk::HummockReadEpoch;
1321 use risingwave_storage::memory::MemoryStateStore;
1322 use risingwave_storage::table::batch_table::BatchTable;
1323
1324 use super::*;
1325 use crate::executor::test_utils::*;
1326
1327 #[tokio::test]
1328 async fn test_materialize_executor() {
1329 let memory_state_store = MemoryStateStore::new();
1331 let table_id = TableId::new(1);
1332 let schema = Schema::new(vec![
1334 Field::unnamed(DataType::Int32),
1335 Field::unnamed(DataType::Int32),
1336 ]);
1337 let column_ids = vec![0.into(), 1.into()];
1338
1339 let chunk1 = StreamChunk::from_pretty(
1341 " i i
1342 + 1 4
1343 + 2 5
1344 + 3 6",
1345 );
1346 let chunk2 = StreamChunk::from_pretty(
1347 " i i
1348 + 7 8
1349 - 3 6",
1350 );
1351
1352 let source = MockSource::with_messages(vec![
1354 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1355 Message::Chunk(chunk1),
1356 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1357 Message::Chunk(chunk2),
1358 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1359 ])
1360 .into_executor(schema.clone(), StreamKey::new());
1361
1362 let order_types = vec![OrderType::ascending()];
1363 let column_descs = vec![
1364 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1365 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1366 ];
1367
1368 let table = BatchTable::for_test(
1369 memory_state_store.clone(),
1370 table_id,
1371 column_descs,
1372 order_types,
1373 vec![0],
1374 vec![0, 1],
1375 );
1376
1377 let mut materialize_executor = MaterializeExecutor::for_test(
1378 source,
1379 memory_state_store,
1380 table_id,
1381 vec![ColumnOrder::new(0, OrderType::ascending())],
1382 column_ids,
1383 Arc::new(AtomicU64::new(0)),
1384 ConflictBehavior::NoCheck,
1385 )
1386 .await
1387 .boxed()
1388 .execute();
1389 materialize_executor.next().await.transpose().unwrap();
1390
1391 materialize_executor.next().await.transpose().unwrap();
1392
1393 match materialize_executor.next().await.transpose().unwrap() {
1395 Some(Message::Barrier(_)) => {
1396 let row = table
1397 .get_row(
1398 &OwnedRow::new(vec![Some(3_i32.into())]),
1399 HummockReadEpoch::NoWait(u64::MAX),
1400 )
1401 .await
1402 .unwrap();
1403 assert_eq!(
1404 row,
1405 Some(OwnedRow::new(vec![Some(3_i32.into()), Some(6_i32.into())]))
1406 );
1407 }
1408 _ => unreachable!(),
1409 }
1410 materialize_executor.next().await.transpose().unwrap();
1411 match materialize_executor.next().await.transpose().unwrap() {
1413 Some(Message::Barrier(_)) => {
1414 let row = table
1415 .get_row(
1416 &OwnedRow::new(vec![Some(7_i32.into())]),
1417 HummockReadEpoch::NoWait(u64::MAX),
1418 )
1419 .await
1420 .unwrap();
1421 assert_eq!(
1422 row,
1423 Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
1424 );
1425 }
1426 _ => unreachable!(),
1427 }
1428 }
1429
1430 #[tokio::test]
1432 async fn test_upsert_stream() {
1433 let memory_state_store = MemoryStateStore::new();
1435 let table_id = TableId::new(1);
1436 let schema = Schema::new(vec![
1438 Field::unnamed(DataType::Int32),
1439 Field::unnamed(DataType::Int32),
1440 ]);
1441 let column_ids = vec![0.into(), 1.into()];
1442
1443 let chunk1 = StreamChunk::from_pretty(
1445 " i i
1446 + 1 1",
1447 );
1448
1449 let chunk2 = StreamChunk::from_pretty(
1450 " i i
1451 + 1 2
1452 - 1 2",
1453 );
1454
1455 let source = MockSource::with_messages(vec![
1457 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1458 Message::Chunk(chunk1),
1459 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1460 Message::Chunk(chunk2),
1461 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1462 ])
1463 .into_executor(schema.clone(), StreamKey::new());
1464
1465 let order_types = vec![OrderType::ascending()];
1466 let column_descs = vec![
1467 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1468 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1469 ];
1470
1471 let table = BatchTable::for_test(
1472 memory_state_store.clone(),
1473 table_id,
1474 column_descs,
1475 order_types,
1476 vec![0],
1477 vec![0, 1],
1478 );
1479
1480 let mut materialize_executor = MaterializeExecutor::for_test(
1481 source,
1482 memory_state_store,
1483 table_id,
1484 vec![ColumnOrder::new(0, OrderType::ascending())],
1485 column_ids,
1486 Arc::new(AtomicU64::new(0)),
1487 ConflictBehavior::Overwrite,
1488 )
1489 .await
1490 .boxed()
1491 .execute();
1492 materialize_executor.next().await.transpose().unwrap();
1493
1494 materialize_executor.next().await.transpose().unwrap();
1495 materialize_executor.next().await.transpose().unwrap();
1496 materialize_executor.next().await.transpose().unwrap();
1497
1498 match materialize_executor.next().await.transpose().unwrap() {
1499 Some(Message::Barrier(_)) => {
1500 let row = table
1501 .get_row(
1502 &OwnedRow::new(vec![Some(1_i32.into())]),
1503 HummockReadEpoch::NoWait(u64::MAX),
1504 )
1505 .await
1506 .unwrap();
1507 assert!(row.is_none());
1508 }
1509 _ => unreachable!(),
1510 }
1511 }
1512
1513 #[tokio::test]
1514 async fn test_toast_with_skipped_conflict_check() {
1515 use risingwave_common::types::DEBEZIUM_UNAVAILABLE_VALUE as TOAST;
1516
1517 let memory_state_store = MemoryStateStore::new();
1518 let table_id = TableId::new(1);
1519 let schema = Schema::new(vec![
1520 Field::unnamed(DataType::Int32),
1521 Field::unnamed(DataType::Int32),
1522 Field::unnamed(DataType::Varchar),
1523 ]);
1524 let column_ids = vec![0.into(), 1.into(), 2.into()];
1525
1526 let chunks = [
1528 " i i T
1530 + 1 1 v1",
1531 &format!(
1533 " i i T
1534 + 1 2 {TOAST}"
1535 ),
1536 " i i T
1538 + 1 3 v2",
1539 &format!(
1541 " i i T
1542 + 1 4 {TOAST}"
1543 ),
1544 ];
1545 let mut messages = vec![Message::Barrier(Barrier::new_test_barrier(test_epoch(1)))];
1546 for (i, chunk) in chunks.iter().enumerate() {
1547 messages.push(Message::Chunk(StreamChunk::from_pretty(chunk)));
1548 messages.push(Message::Barrier(Barrier::new_test_barrier(test_epoch(
1549 i as u64 + 2,
1550 ))));
1551 }
1552 let source = MockSource::with_messages(messages).into_executor(schema, StreamKey::new());
1553
1554 let table = BatchTable::for_test(
1555 memory_state_store.clone(),
1556 table_id,
1557 vec![
1558 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1559 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1560 ColumnDesc::unnamed(column_ids[2], DataType::Varchar),
1561 ],
1562 vec![OrderType::ascending()],
1563 vec![0],
1564 vec![0, 1, 2],
1565 );
1566
1567 let mut materialize_executor = MaterializeExecutor::for_test_with_toast(
1568 source,
1569 memory_state_store,
1570 table_id,
1571 vec![ColumnOrder::new(0, OrderType::ascending())],
1572 column_ids,
1573 vec![2],
1574 )
1575 .await
1576 .boxed()
1577 .execute();
1578 materialize_executor.next().await.transpose().unwrap();
1579
1580 for (v, toast) in [(1, "v1"), (2, "v1"), (3, "v2"), (4, "v2")] {
1581 while !matches!(
1583 materialize_executor.next().await.transpose().unwrap(),
1584 Some(Message::Barrier(_))
1585 ) {}
1586 let row = table
1587 .get_row(
1588 &OwnedRow::new(vec![Some(1_i32.into())]),
1589 HummockReadEpoch::NoWait(u64::MAX),
1590 )
1591 .await
1592 .unwrap();
1593 assert_eq!(
1594 row,
1595 Some(OwnedRow::new(vec![
1596 Some(1_i32.into()),
1597 Some(v.into()),
1598 Some(toast.into())
1599 ]))
1600 );
1601 }
1602 }
1603
1604 #[tokio::test]
1605 async fn test_check_insert_conflict() {
1606 let memory_state_store = MemoryStateStore::new();
1608 let table_id = TableId::new(1);
1609 let schema = Schema::new(vec![
1611 Field::unnamed(DataType::Int32),
1612 Field::unnamed(DataType::Int32),
1613 ]);
1614 let column_ids = vec![0.into(), 1.into()];
1615
1616 let chunk1 = StreamChunk::from_pretty(
1618 " i i
1619 + 1 3
1620 + 1 4
1621 + 2 5
1622 + 3 6",
1623 );
1624
1625 let chunk2 = StreamChunk::from_pretty(
1626 " i i
1627 + 1 3
1628 + 2 6",
1629 );
1630
1631 let chunk3 = StreamChunk::from_pretty(
1633 " i i
1634 + 1 4",
1635 );
1636
1637 let source = MockSource::with_messages(vec![
1639 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1640 Message::Chunk(chunk1),
1641 Message::Chunk(chunk2),
1642 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1643 Message::Chunk(chunk3),
1644 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1645 ])
1646 .into_executor(schema.clone(), StreamKey::new());
1647
1648 let order_types = vec![OrderType::ascending()];
1649 let column_descs = vec![
1650 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1651 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1652 ];
1653
1654 let table = BatchTable::for_test(
1655 memory_state_store.clone(),
1656 table_id,
1657 column_descs,
1658 order_types,
1659 vec![0],
1660 vec![0, 1],
1661 );
1662
1663 let mut materialize_executor = MaterializeExecutor::for_test(
1664 source,
1665 memory_state_store,
1666 table_id,
1667 vec![ColumnOrder::new(0, OrderType::ascending())],
1668 column_ids,
1669 Arc::new(AtomicU64::new(0)),
1670 ConflictBehavior::Overwrite,
1671 )
1672 .await
1673 .boxed()
1674 .execute();
1675 materialize_executor.next().await.transpose().unwrap();
1676
1677 materialize_executor.next().await.transpose().unwrap();
1678 materialize_executor.next().await.transpose().unwrap();
1679
1680 match materialize_executor.next().await.transpose().unwrap() {
1682 Some(Message::Barrier(_)) => {
1683 let row = table
1684 .get_row(
1685 &OwnedRow::new(vec![Some(3_i32.into())]),
1686 HummockReadEpoch::NoWait(u64::MAX),
1687 )
1688 .await
1689 .unwrap();
1690 assert_eq!(
1691 row,
1692 Some(OwnedRow::new(vec![Some(3_i32.into()), Some(6_i32.into())]))
1693 );
1694
1695 let row = table
1696 .get_row(
1697 &OwnedRow::new(vec![Some(1_i32.into())]),
1698 HummockReadEpoch::NoWait(u64::MAX),
1699 )
1700 .await
1701 .unwrap();
1702 assert_eq!(
1703 row,
1704 Some(OwnedRow::new(vec![Some(1_i32.into()), Some(3_i32.into())]))
1705 );
1706
1707 let row = table
1708 .get_row(
1709 &OwnedRow::new(vec![Some(2_i32.into())]),
1710 HummockReadEpoch::NoWait(u64::MAX),
1711 )
1712 .await
1713 .unwrap();
1714 assert_eq!(
1715 row,
1716 Some(OwnedRow::new(vec![Some(2_i32.into()), Some(6_i32.into())]))
1717 );
1718 }
1719 _ => unreachable!(),
1720 }
1721 }
1722
1723 #[tokio::test]
1724 async fn test_delete_and_update_conflict() {
1725 let memory_state_store = MemoryStateStore::new();
1727 let table_id = TableId::new(1);
1728 let schema = Schema::new(vec![
1730 Field::unnamed(DataType::Int32),
1731 Field::unnamed(DataType::Int32),
1732 ]);
1733 let column_ids = vec![0.into(), 1.into()];
1734
1735 let chunk1 = StreamChunk::from_pretty(
1737 " i i
1738 + 1 4
1739 + 2 5
1740 + 3 6
1741 U- 8 1
1742 U+ 8 2
1743 + 8 3",
1744 );
1745
1746 let chunk2 = StreamChunk::from_pretty(
1748 " i i
1749 + 7 8
1750 - 3 4
1751 - 5 0",
1752 );
1753
1754 let chunk3 = StreamChunk::from_pretty(
1756 " i i
1757 + 1 5
1758 U- 2 4
1759 U+ 2 8
1760 U- 9 0
1761 U+ 9 1",
1762 );
1763
1764 let source = MockSource::with_messages(vec![
1766 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1767 Message::Chunk(chunk1),
1768 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1769 Message::Chunk(chunk2),
1770 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1771 Message::Chunk(chunk3),
1772 Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
1773 ])
1774 .into_executor(schema.clone(), StreamKey::new());
1775
1776 let order_types = vec![OrderType::ascending()];
1777 let column_descs = vec![
1778 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1779 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1780 ];
1781
1782 let table = BatchTable::for_test(
1783 memory_state_store.clone(),
1784 table_id,
1785 column_descs,
1786 order_types,
1787 vec![0],
1788 vec![0, 1],
1789 );
1790
1791 let mut materialize_executor = MaterializeExecutor::for_test(
1792 source,
1793 memory_state_store,
1794 table_id,
1795 vec![ColumnOrder::new(0, OrderType::ascending())],
1796 column_ids,
1797 Arc::new(AtomicU64::new(0)),
1798 ConflictBehavior::Overwrite,
1799 )
1800 .await
1801 .boxed()
1802 .execute();
1803 materialize_executor.next().await.transpose().unwrap();
1804
1805 materialize_executor.next().await.transpose().unwrap();
1806
1807 match materialize_executor.next().await.transpose().unwrap() {
1809 Some(Message::Barrier(_)) => {
1810 let row = table
1812 .get_row(
1813 &OwnedRow::new(vec![Some(8_i32.into())]),
1814 HummockReadEpoch::NoWait(u64::MAX),
1815 )
1816 .await
1817 .unwrap();
1818 assert_eq!(
1819 row,
1820 Some(OwnedRow::new(vec![Some(8_i32.into()), Some(3_i32.into())]))
1821 );
1822 }
1823 _ => unreachable!(),
1824 }
1825 materialize_executor.next().await.transpose().unwrap();
1826
1827 match materialize_executor.next().await.transpose().unwrap() {
1828 Some(Message::Barrier(_)) => {
1829 let row = table
1830 .get_row(
1831 &OwnedRow::new(vec![Some(7_i32.into())]),
1832 HummockReadEpoch::NoWait(u64::MAX),
1833 )
1834 .await
1835 .unwrap();
1836 assert_eq!(
1837 row,
1838 Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
1839 );
1840
1841 let row = table
1843 .get_row(
1844 &OwnedRow::new(vec![Some(3_i32.into())]),
1845 HummockReadEpoch::NoWait(u64::MAX),
1846 )
1847 .await
1848 .unwrap();
1849 assert_eq!(row, None);
1850
1851 let row = table
1853 .get_row(
1854 &OwnedRow::new(vec![Some(5_i32.into())]),
1855 HummockReadEpoch::NoWait(u64::MAX),
1856 )
1857 .await
1858 .unwrap();
1859 assert_eq!(row, None);
1860 }
1861 _ => unreachable!(),
1862 }
1863
1864 materialize_executor.next().await.transpose().unwrap();
1865 match materialize_executor.next().await.transpose().unwrap() {
1867 Some(Message::Barrier(_)) => {
1868 let row = table
1869 .get_row(
1870 &OwnedRow::new(vec![Some(1_i32.into())]),
1871 HummockReadEpoch::NoWait(u64::MAX),
1872 )
1873 .await
1874 .unwrap();
1875 assert_eq!(
1876 row,
1877 Some(OwnedRow::new(vec![Some(1_i32.into()), Some(5_i32.into())]))
1878 );
1879
1880 let row = table
1882 .get_row(
1883 &OwnedRow::new(vec![Some(2_i32.into())]),
1884 HummockReadEpoch::NoWait(u64::MAX),
1885 )
1886 .await
1887 .unwrap();
1888 assert_eq!(
1889 row,
1890 Some(OwnedRow::new(vec![Some(2_i32.into()), Some(8_i32.into())]))
1891 );
1892
1893 let row = table
1895 .get_row(
1896 &OwnedRow::new(vec![Some(9_i32.into())]),
1897 HummockReadEpoch::NoWait(u64::MAX),
1898 )
1899 .await
1900 .unwrap();
1901 assert_eq!(
1902 row,
1903 Some(OwnedRow::new(vec![Some(9_i32.into()), Some(1_i32.into())]))
1904 );
1905 }
1906 _ => unreachable!(),
1907 }
1908 }
1909
1910 #[tokio::test]
1911 async fn test_change_buffer_into_chunk_with_stream_key() {
1912 let memory_state_store = MemoryStateStore::new();
1915 let table_id = TableId::new(1);
1916 let schema = Schema::new(vec![
1917 Field::unnamed(DataType::Int32),
1918 Field::unnamed(DataType::Int32),
1919 ]);
1920 let column_ids = vec![0.into(), 1.into()];
1921
1922 let chunk1 = StreamChunk::from_pretty(
1923 " i i
1924 + 1 4",
1925 );
1926 let chunk2 = StreamChunk::from_pretty(
1927 " i i
1928 + 1 5",
1929 );
1930
1931 let source = MockSource::with_messages(vec![
1932 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1933 Message::Chunk(chunk1),
1934 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1935 Message::Chunk(chunk2),
1936 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1937 ])
1938 .into_executor(schema.clone(), StreamKey::new());
1939
1940 let mut materialize_executor = MaterializeExecutor::for_test_with_stream_key(
1941 source,
1942 memory_state_store,
1943 table_id,
1944 vec![ColumnOrder::new(0, OrderType::ascending())],
1945 vec![0, 1],
1946 column_ids,
1947 Arc::new(AtomicU64::new(0)),
1948 ConflictBehavior::Overwrite,
1949 )
1950 .await
1951 .boxed()
1952 .execute();
1953
1954 materialize_executor.next().await.transpose().unwrap();
1956 materialize_executor.next().await.transpose().unwrap();
1957
1958 materialize_executor.next().await.transpose().unwrap();
1960
1961 match materialize_executor.next().await.transpose().unwrap() {
1963 Some(Message::Chunk(chunk)) => {
1964 assert_eq!(
1965 chunk.compact_vis(),
1966 StreamChunk::from_pretty(
1967 " i i
1968 - 1 4
1969 + 1 5"
1970 )
1971 );
1972 }
1973 other => panic!("expect chunk, got {other:?}"),
1974 }
1975 }
1976
1977 #[tokio::test]
1978 async fn test_ignore_insert_conflict() {
1979 let memory_state_store = MemoryStateStore::new();
1981 let table_id = TableId::new(1);
1982 let schema = Schema::new(vec![
1984 Field::unnamed(DataType::Int32),
1985 Field::unnamed(DataType::Int32),
1986 ]);
1987 let column_ids = vec![0.into(), 1.into()];
1988
1989 let chunk1 = StreamChunk::from_pretty(
1991 " i i
1992 + 1 3
1993 + 1 4
1994 + 2 5
1995 + 3 6",
1996 );
1997
1998 let chunk2 = StreamChunk::from_pretty(
1999 " i i
2000 + 1 5
2001 + 2 6",
2002 );
2003
2004 let chunk3 = StreamChunk::from_pretty(
2006 " i i
2007 + 1 6",
2008 );
2009
2010 let source = MockSource::with_messages(vec![
2012 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2013 Message::Chunk(chunk1),
2014 Message::Chunk(chunk2),
2015 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2016 Message::Chunk(chunk3),
2017 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2018 ])
2019 .into_executor(schema.clone(), StreamKey::new());
2020
2021 let order_types = vec![OrderType::ascending()];
2022 let column_descs = vec![
2023 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
2024 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
2025 ];
2026
2027 let table = BatchTable::for_test(
2028 memory_state_store.clone(),
2029 table_id,
2030 column_descs,
2031 order_types,
2032 vec![0],
2033 vec![0, 1],
2034 );
2035
2036 let mut materialize_executor = MaterializeExecutor::for_test(
2037 source,
2038 memory_state_store,
2039 table_id,
2040 vec![ColumnOrder::new(0, OrderType::ascending())],
2041 column_ids,
2042 Arc::new(AtomicU64::new(0)),
2043 ConflictBehavior::IgnoreConflict,
2044 )
2045 .await
2046 .boxed()
2047 .execute();
2048 materialize_executor.next().await.transpose().unwrap();
2049
2050 materialize_executor.next().await.transpose().unwrap();
2051 materialize_executor.next().await.transpose().unwrap();
2052
2053 match materialize_executor.next().await.transpose().unwrap() {
2055 Some(Message::Barrier(_)) => {
2056 let row = table
2057 .get_row(
2058 &OwnedRow::new(vec![Some(3_i32.into())]),
2059 HummockReadEpoch::NoWait(u64::MAX),
2060 )
2061 .await
2062 .unwrap();
2063 assert_eq!(
2064 row,
2065 Some(OwnedRow::new(vec![Some(3_i32.into()), Some(6_i32.into())]))
2066 );
2067
2068 let row = table
2069 .get_row(
2070 &OwnedRow::new(vec![Some(1_i32.into())]),
2071 HummockReadEpoch::NoWait(u64::MAX),
2072 )
2073 .await
2074 .unwrap();
2075 assert_eq!(
2076 row,
2077 Some(OwnedRow::new(vec![Some(1_i32.into()), Some(3_i32.into())]))
2078 );
2079
2080 let row = table
2081 .get_row(
2082 &OwnedRow::new(vec![Some(2_i32.into())]),
2083 HummockReadEpoch::NoWait(u64::MAX),
2084 )
2085 .await
2086 .unwrap();
2087 assert_eq!(
2088 row,
2089 Some(OwnedRow::new(vec![Some(2_i32.into()), Some(5_i32.into())]))
2090 );
2091 }
2092 _ => unreachable!(),
2093 }
2094 }
2095
2096 #[tokio::test]
2097 async fn test_ignore_delete_then_insert() {
2098 let memory_state_store = MemoryStateStore::new();
2100 let table_id = TableId::new(1);
2101 let schema = Schema::new(vec![
2103 Field::unnamed(DataType::Int32),
2104 Field::unnamed(DataType::Int32),
2105 ]);
2106 let column_ids = vec![0.into(), 1.into()];
2107
2108 let chunk1 = StreamChunk::from_pretty(
2110 " i i
2111 + 1 3
2112 - 1 3
2113 + 1 6",
2114 );
2115
2116 let source = MockSource::with_messages(vec![
2118 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2119 Message::Chunk(chunk1),
2120 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2121 ])
2122 .into_executor(schema.clone(), StreamKey::new());
2123
2124 let order_types = vec![OrderType::ascending()];
2125 let column_descs = vec![
2126 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
2127 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
2128 ];
2129
2130 let table = BatchTable::for_test(
2131 memory_state_store.clone(),
2132 table_id,
2133 column_descs,
2134 order_types,
2135 vec![0],
2136 vec![0, 1],
2137 );
2138
2139 let mut materialize_executor = MaterializeExecutor::for_test(
2140 source,
2141 memory_state_store,
2142 table_id,
2143 vec![ColumnOrder::new(0, OrderType::ascending())],
2144 column_ids,
2145 Arc::new(AtomicU64::new(0)),
2146 ConflictBehavior::IgnoreConflict,
2147 )
2148 .await
2149 .boxed()
2150 .execute();
2151 let _msg1 = materialize_executor
2152 .next()
2153 .await
2154 .transpose()
2155 .unwrap()
2156 .unwrap()
2157 .as_barrier()
2158 .unwrap();
2159 let _msg2 = materialize_executor
2160 .next()
2161 .await
2162 .transpose()
2163 .unwrap()
2164 .unwrap()
2165 .as_chunk()
2166 .unwrap();
2167 let _msg3 = materialize_executor
2168 .next()
2169 .await
2170 .transpose()
2171 .unwrap()
2172 .unwrap()
2173 .as_barrier()
2174 .unwrap();
2175
2176 let row = table
2177 .get_row(
2178 &OwnedRow::new(vec![Some(1_i32.into())]),
2179 HummockReadEpoch::NoWait(u64::MAX),
2180 )
2181 .await
2182 .unwrap();
2183 assert_eq!(
2184 row,
2185 Some(OwnedRow::new(vec![Some(1_i32.into()), Some(6_i32.into())]))
2186 );
2187 }
2188
2189 #[tokio::test]
2190 async fn test_ignore_delete_and_update_conflict() {
2191 let memory_state_store = MemoryStateStore::new();
2193 let table_id = TableId::new(1);
2194 let schema = Schema::new(vec![
2196 Field::unnamed(DataType::Int32),
2197 Field::unnamed(DataType::Int32),
2198 ]);
2199 let column_ids = vec![0.into(), 1.into()];
2200
2201 let chunk1 = StreamChunk::from_pretty(
2203 " i i
2204 + 1 4
2205 + 2 5
2206 + 3 6
2207 U- 8 1
2208 U+ 8 2
2209 + 8 3",
2210 );
2211
2212 let chunk2 = StreamChunk::from_pretty(
2214 " i i
2215 + 7 8
2216 - 3 4
2217 - 5 0",
2218 );
2219
2220 let chunk3 = StreamChunk::from_pretty(
2222 " i i
2223 + 1 5
2224 U- 2 4
2225 U+ 2 8
2226 U- 9 0
2227 U+ 9 1",
2228 );
2229
2230 let source = MockSource::with_messages(vec![
2232 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2233 Message::Chunk(chunk1),
2234 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2235 Message::Chunk(chunk2),
2236 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2237 Message::Chunk(chunk3),
2238 Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
2239 ])
2240 .into_executor(schema.clone(), StreamKey::new());
2241
2242 let order_types = vec![OrderType::ascending()];
2243 let column_descs = vec![
2244 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
2245 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
2246 ];
2247
2248 let table = BatchTable::for_test(
2249 memory_state_store.clone(),
2250 table_id,
2251 column_descs,
2252 order_types,
2253 vec![0],
2254 vec![0, 1],
2255 );
2256
2257 let mut materialize_executor = MaterializeExecutor::for_test(
2258 source,
2259 memory_state_store,
2260 table_id,
2261 vec![ColumnOrder::new(0, OrderType::ascending())],
2262 column_ids,
2263 Arc::new(AtomicU64::new(0)),
2264 ConflictBehavior::IgnoreConflict,
2265 )
2266 .await
2267 .boxed()
2268 .execute();
2269 materialize_executor.next().await.transpose().unwrap();
2270
2271 materialize_executor.next().await.transpose().unwrap();
2272
2273 match materialize_executor.next().await.transpose().unwrap() {
2275 Some(Message::Barrier(_)) => {
2276 let row = table
2278 .get_row(
2279 &OwnedRow::new(vec![Some(8_i32.into())]),
2280 HummockReadEpoch::NoWait(u64::MAX),
2281 )
2282 .await
2283 .unwrap();
2284 assert_eq!(
2285 row,
2286 Some(OwnedRow::new(vec![Some(8_i32.into()), Some(2_i32.into())]))
2287 );
2288 }
2289 _ => unreachable!(),
2290 }
2291 materialize_executor.next().await.transpose().unwrap();
2292
2293 match materialize_executor.next().await.transpose().unwrap() {
2294 Some(Message::Barrier(_)) => {
2295 let row = table
2296 .get_row(
2297 &OwnedRow::new(vec![Some(7_i32.into())]),
2298 HummockReadEpoch::NoWait(u64::MAX),
2299 )
2300 .await
2301 .unwrap();
2302 assert_eq!(
2303 row,
2304 Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
2305 );
2306
2307 let row = table
2309 .get_row(
2310 &OwnedRow::new(vec![Some(3_i32.into())]),
2311 HummockReadEpoch::NoWait(u64::MAX),
2312 )
2313 .await
2314 .unwrap();
2315 assert_eq!(row, None);
2316
2317 let row = table
2319 .get_row(
2320 &OwnedRow::new(vec![Some(5_i32.into())]),
2321 HummockReadEpoch::NoWait(u64::MAX),
2322 )
2323 .await
2324 .unwrap();
2325 assert_eq!(row, None);
2326 }
2327 _ => unreachable!(),
2328 }
2329
2330 materialize_executor.next().await.transpose().unwrap();
2331 match materialize_executor.next().await.transpose().unwrap() {
2334 Some(Message::Barrier(_)) => {
2335 let row = table
2336 .get_row(
2337 &OwnedRow::new(vec![Some(1_i32.into())]),
2338 HummockReadEpoch::NoWait(u64::MAX),
2339 )
2340 .await
2341 .unwrap();
2342 assert_eq!(
2343 row,
2344 Some(OwnedRow::new(vec![Some(1_i32.into()), Some(4_i32.into())]))
2345 );
2346
2347 let row = table
2349 .get_row(
2350 &OwnedRow::new(vec![Some(2_i32.into())]),
2351 HummockReadEpoch::NoWait(u64::MAX),
2352 )
2353 .await
2354 .unwrap();
2355 assert_eq!(
2356 row,
2357 Some(OwnedRow::new(vec![Some(2_i32.into()), Some(8_i32.into())]))
2358 );
2359
2360 let row = table
2362 .get_row(
2363 &OwnedRow::new(vec![Some(9_i32.into())]),
2364 HummockReadEpoch::NoWait(u64::MAX),
2365 )
2366 .await
2367 .unwrap();
2368 assert_eq!(
2369 row,
2370 Some(OwnedRow::new(vec![Some(9_i32.into()), Some(1_i32.into())]))
2371 );
2372 }
2373 _ => unreachable!(),
2374 }
2375 }
2376
2377 #[tokio::test]
2378 async fn test_do_update_if_not_null_conflict() {
2379 let memory_state_store = MemoryStateStore::new();
2381 let table_id = TableId::new(1);
2382 let schema = Schema::new(vec![
2384 Field::unnamed(DataType::Int32),
2385 Field::unnamed(DataType::Int32),
2386 ]);
2387 let column_ids = vec![0.into(), 1.into()];
2388
2389 let chunk1 = StreamChunk::from_pretty(
2391 " i i
2392 + 1 4
2393 + 2 .
2394 + 3 6
2395 U- 8 .
2396 U+ 8 2
2397 + 8 .",
2398 );
2399
2400 let chunk2 = StreamChunk::from_pretty(
2402 " i i
2403 + 7 8
2404 - 3 4
2405 - 5 0",
2406 );
2407
2408 let chunk3 = StreamChunk::from_pretty(
2410 " i i
2411 + 1 5
2412 + 7 .
2413 U- 2 4
2414 U+ 2 .
2415 U- 9 0
2416 U+ 9 1",
2417 );
2418
2419 let source = MockSource::with_messages(vec![
2421 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2422 Message::Chunk(chunk1),
2423 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2424 Message::Chunk(chunk2),
2425 Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2426 Message::Chunk(chunk3),
2427 Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
2428 ])
2429 .into_executor(schema.clone(), StreamKey::new());
2430
2431 let order_types = vec![OrderType::ascending()];
2432 let column_descs = vec![
2433 ColumnDesc::unnamed(column_ids[0], DataType::Int32),
2434 ColumnDesc::unnamed(column_ids[1], DataType::Int32),
2435 ];
2436
2437 let table = BatchTable::for_test(
2438 memory_state_store.clone(),
2439 table_id,
2440 column_descs,
2441 order_types,
2442 vec![0],
2443 vec![0, 1],
2444 );
2445
2446 let mut materialize_executor = MaterializeExecutor::for_test(
2447 source,
2448 memory_state_store,
2449 table_id,
2450 vec![ColumnOrder::new(0, OrderType::ascending())],
2451 column_ids,
2452 Arc::new(AtomicU64::new(0)),
2453 ConflictBehavior::DoUpdateIfNotNull,
2454 )
2455 .await
2456 .boxed()
2457 .execute();
2458 materialize_executor.next().await.transpose().unwrap();
2459
2460 materialize_executor.next().await.transpose().unwrap();
2461
2462 match materialize_executor.next().await.transpose().unwrap() {
2464 Some(Message::Barrier(_)) => {
2465 let row = table
2466 .get_row(
2467 &OwnedRow::new(vec![Some(8_i32.into())]),
2468 HummockReadEpoch::NoWait(u64::MAX),
2469 )
2470 .await
2471 .unwrap();
2472 assert_eq!(
2473 row,
2474 Some(OwnedRow::new(vec![Some(8_i32.into()), Some(2_i32.into())]))
2475 );
2476
2477 let row = table
2478 .get_row(
2479 &OwnedRow::new(vec![Some(2_i32.into())]),
2480 HummockReadEpoch::NoWait(u64::MAX),
2481 )
2482 .await
2483 .unwrap();
2484 assert_eq!(row, Some(OwnedRow::new(vec![Some(2_i32.into()), None])));
2485 }
2486 _ => unreachable!(),
2487 }
2488 materialize_executor.next().await.transpose().unwrap();
2489
2490 match materialize_executor.next().await.transpose().unwrap() {
2491 Some(Message::Barrier(_)) => {
2492 let row = table
2493 .get_row(
2494 &OwnedRow::new(vec![Some(7_i32.into())]),
2495 HummockReadEpoch::NoWait(u64::MAX),
2496 )
2497 .await
2498 .unwrap();
2499 assert_eq!(
2500 row,
2501 Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
2502 );
2503
2504 let row = table
2506 .get_row(
2507 &OwnedRow::new(vec![Some(3_i32.into())]),
2508 HummockReadEpoch::NoWait(u64::MAX),
2509 )
2510 .await
2511 .unwrap();
2512 assert_eq!(row, None);
2513
2514 let row = table
2516 .get_row(
2517 &OwnedRow::new(vec![Some(5_i32.into())]),
2518 HummockReadEpoch::NoWait(u64::MAX),
2519 )
2520 .await
2521 .unwrap();
2522 assert_eq!(row, None);
2523 }
2524 _ => unreachable!(),
2525 }
2526
2527 materialize_executor.next().await.transpose().unwrap();
2528 match materialize_executor.next().await.transpose().unwrap() {
2531 Some(Message::Barrier(_)) => {
2532 let row = table
2533 .get_row(
2534 &OwnedRow::new(vec![Some(7_i32.into())]),
2535 HummockReadEpoch::NoWait(u64::MAX),
2536 )
2537 .await
2538 .unwrap();
2539 assert_eq!(
2540 row,
2541 Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
2542 );
2543
2544 let row = table
2546 .get_row(
2547 &OwnedRow::new(vec![Some(2_i32.into())]),
2548 HummockReadEpoch::NoWait(u64::MAX),
2549 )
2550 .await
2551 .unwrap();
2552 assert_eq!(row, Some(OwnedRow::new(vec![Some(2_i32.into()), None])));
2553
2554 let row = table
2556 .get_row(
2557 &OwnedRow::new(vec![Some(9_i32.into())]),
2558 HummockReadEpoch::NoWait(u64::MAX),
2559 )
2560 .await
2561 .unwrap();
2562 assert_eq!(
2563 row,
2564 Some(OwnedRow::new(vec![Some(9_i32.into()), Some(1_i32.into())]))
2565 );
2566 }
2567 _ => unreachable!(),
2568 }
2569 }
2570
2571 fn gen_fuzz_data(row_number: usize, chunk_size: usize) -> Vec<StreamChunk> {
2572 const KN: u32 = 4;
2573 const SEED: u64 = 998244353;
2574 let mut ret = vec![];
2575 let mut builder =
2576 StreamChunkBuilder::new(chunk_size, vec![DataType::Int32, DataType::Int32]);
2577 let mut rng = SmallRng::seed_from_u64(SEED);
2578
2579 let random_vis = |c: StreamChunk, rng: &mut SmallRng| -> StreamChunk {
2580 let len = c.data_chunk().capacity();
2581 let mut c = StreamChunkMut::from(c);
2582 for i in 0..len {
2583 c.set_vis(i, rng.random_bool(0.5));
2584 }
2585 c.into()
2586 };
2587 for _ in 0..row_number {
2588 let k = (rng.next_u32() % KN) as i32;
2589 let v = rng.next_u32() as i32;
2590 let op = if rng.random_bool(0.5) {
2591 Op::Insert
2592 } else {
2593 Op::Delete
2594 };
2595 if let Some(c) =
2596 builder.append_row(op, OwnedRow::new(vec![Some(k.into()), Some(v.into())]))
2597 {
2598 ret.push(random_vis(c, &mut rng));
2599 }
2600 }
2601 if let Some(c) = builder.take() {
2602 ret.push(random_vis(c, &mut rng));
2603 }
2604 ret
2605 }
2606
2607 async fn fuzz_test_stream_consistent_inner(conflict_behavior: ConflictBehavior) {
2608 const N: usize = 100000;
2609
2610 let memory_state_store = MemoryStateStore::new();
2612 let table_id = TableId::new(1);
2613 let schema = Schema::new(vec![
2615 Field::unnamed(DataType::Int32),
2616 Field::unnamed(DataType::Int32),
2617 ]);
2618 let column_ids = vec![0.into(), 1.into()];
2619
2620 let chunks = gen_fuzz_data(N, 128);
2621 let messages = iter::once(Message::Barrier(Barrier::new_test_barrier(test_epoch(1))))
2622 .chain(chunks.into_iter().map(Message::Chunk))
2623 .chain(iter::once(Message::Barrier(Barrier::new_test_barrier(
2624 test_epoch(2),
2625 ))))
2626 .collect();
2627 let source =
2629 MockSource::with_messages(messages).into_executor(schema.clone(), StreamKey::new());
2630
2631 let mut materialize_executor = MaterializeExecutor::for_test(
2632 source,
2633 memory_state_store.clone(),
2634 table_id,
2635 vec![ColumnOrder::new(0, OrderType::ascending())],
2636 column_ids,
2637 Arc::new(AtomicU64::new(0)),
2638 conflict_behavior,
2639 )
2640 .await
2641 .boxed()
2642 .execute();
2643 materialize_executor.expect_barrier().await;
2644
2645 let order_types = vec![OrderType::ascending()];
2646 let column_descs = vec![
2647 ColumnDesc::unnamed(0.into(), DataType::Int32),
2648 ColumnDesc::unnamed(1.into(), DataType::Int32),
2649 ];
2650 let pk_indices = vec![0];
2651
2652 let mut table = StateTable::from_table_catalog(
2653 &crate::common::table::test_utils::gen_pbtable(
2654 TableId::from(1002),
2655 column_descs.clone(),
2656 order_types,
2657 pk_indices,
2658 0,
2659 ),
2660 memory_state_store.clone(),
2661 None,
2662 )
2663 .await;
2664
2665 while let Message::Chunk(c) = materialize_executor.next().await.unwrap().unwrap() {
2666 table.write_chunk(c);
2668 }
2669 }
2670
2671 #[tokio::test]
2672 async fn fuzz_test_stream_consistent_upsert() {
2673 fuzz_test_stream_consistent_inner(ConflictBehavior::Overwrite).await
2674 }
2675
2676 #[tokio::test]
2677 async fn fuzz_test_stream_consistent_ignore() {
2678 fuzz_test_stream_consistent_inner(ConflictBehavior::IgnoreConflict).await
2679 }
2680
2681 async fn ttl_output_after_watermark(
2685 conflict_behavior: ConflictBehavior,
2686 chunk: &str,
2687 barrier_between: bool,
2688 cold_cache: bool,
2689 ) -> Vec<(Op, OwnedRow)> {
2690 let schema = Schema::new(vec![
2691 Field::unnamed(DataType::Int32),
2692 Field::unnamed(DataType::Int32),
2693 Field::unnamed(DataType::Int32),
2694 ]);
2695 let mut messages = vec![
2696 Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2697 Message::Chunk(StreamChunk::from_pretty(
2698 " i i i
2699 + 25 10 100",
2700 )),
2701 Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2702 Message::Watermark(Watermark::new(1, DataType::Int32, ScalarImpl::Int32(20))),
2703 ];
2704 let mut last_epoch = 3;
2705 if barrier_between {
2706 messages.push(Message::Barrier(Barrier::new_test_barrier(test_epoch(3))));
2707 last_epoch = 4;
2708 }
2709 messages.push(Message::Chunk(StreamChunk::from_pretty(chunk)));
2710 messages.push(Message::Barrier(Barrier::new_test_barrier(test_epoch(
2711 last_epoch,
2712 ))));
2713 let source = MockSource::with_messages(messages).into_executor(schema, vec![0, 1]);
2714
2715 let cache_sequence = if cold_cache { u64::MAX } else { 0 };
2717 let mut materialize = MaterializeExecutor::for_test_with_ttl(
2718 source,
2719 MemoryStateStore::new(),
2720 TableId::new(1),
2721 vec![ColumnOrder::new(0, OrderType::ascending())],
2722 vec![0.into(), 1.into(), 2.into()],
2723 Arc::new(AtomicU64::new(cache_sequence)),
2724 conflict_behavior,
2725 1,
2726 )
2727 .await
2728 .boxed()
2729 .execute();
2730
2731 let mut watermark_seen = false;
2732 let mut rows = vec![];
2733 while let Some(msg) = materialize.next().await.transpose().unwrap() {
2734 match msg {
2735 Message::Watermark(_) => watermark_seen = true,
2736 Message::Chunk(chunk) if watermark_seen => {
2737 rows.extend(chunk.rows().map(|(op, row)| (op, row.into_owned_row())))
2738 }
2739 Message::Barrier(barrier) if barrier.epoch.curr == test_epoch(last_epoch) => break,
2740 _ => {}
2741 }
2742 }
2743 assert!(watermark_seen);
2744 rows
2745 }
2746
2747 #[tokio::test]
2749 async fn test_ttl_expired_by_forwarded_watermark() {
2750 let new_row = OwnedRow::new(vec![
2751 Some(25i32.into()),
2752 Some(30i32.into()),
2753 Some(300i32.into()),
2754 ]);
2755 let cases = [
2756 (
2758 ConflictBehavior::Overwrite,
2759 " i i i\n + 25 30 300",
2760 vec![(Op::Insert, new_row.clone())],
2761 ),
2762 (ConflictBehavior::Overwrite, " i i i\n - 25 . .", vec![]),
2764 (
2766 ConflictBehavior::IgnoreConflict,
2767 " i i i\n + 25 30 300",
2768 vec![(Op::Insert, new_row)],
2769 ),
2770 ];
2771 for (conflict_behavior, chunk, expected) in cases {
2772 for barrier_between in [false, true] {
2773 for cold_cache in [false, true] {
2774 assert_eq!(
2775 ttl_output_after_watermark(
2776 conflict_behavior,
2777 chunk,
2778 barrier_between,
2779 cold_cache
2780 )
2781 .await,
2782 expected,
2783 "{conflict_behavior:?}, {chunk:?}, barrier_between: {barrier_between}, \
2784 cold_cache: {cold_cache}"
2785 );
2786 }
2787 }
2788 }
2789 }
2790}