Skip to main content

risingwave_stream/executor/mview/
materialize.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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;
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
67/// `MaterializeExecutor` materializes changes in stream into a materialized view on storage.
68pub 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 of the *output* of this materialize node.
76    ///
77    /// This can be different from the table PK. Typically, there are 3 cases:
78    ///
79    /// - Normal `TABLE`: `stream_key == pk`, so no special handling is needed.
80    /// - TTL-ed `TABLE` (`WITH TTL`): `stream_key` is a superset of `pk` by appending the TTL
81    ///   watermark column. In this case, updates to the same pk may have different stream keys, so
82    ///   we must rewrite such `Update` into `Delete + Insert` before yielding.
83    /// - `MV` or `INDEX`: `pk` can be a superset of `stream_key` to also include the order key or
84    ///   distribution key specified by the user. No special handling is needed either.
85    stream_key_indices: Vec<usize>,
86
87    /// Columns of arrange keys (including pk, group keys, join keys, etc.)
88    arrange_key_indices: Vec<usize>,
89
90    actor_context: ActorContextRef,
91
92    /// The cache for conflict handling. `None` if conflict behavior is `NoCheck`.
93    materialize_cache: Option<MaterializeCache>,
94
95    /// Indices of columns that may carry a Debezium unchanged-TOAST placeholder
96    /// (PostgreSQL CDC only). Mirrors the value passed into `MaterializeCache`.
97    /// Used at runtime to prevent the `Overwrite -> NoCheck` downgrade from
98    /// bypassing the placeholder replacement path.
99    toastable_column_indices: Option<Vec<usize>>,
100
101    conflict_behavior: ConflictBehavior,
102
103    /// Whether the table can clean itself by TTL watermark, i.e., is defined with `WATERMARK ... WITH TTL`.
104    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    /// No data will be written to hummock table. This Materialize is just a dummy node.
115    /// Used for APPEND ONLY table with iceberg engine. All data will be written to iceberg table directly.
116    is_dummy_table: bool,
117
118    /// Optional refresh arguments and state for refreshable materialized views
119    refresh_args: Option<RefreshableMaterializeArgs<S, SD>>,
120
121    /// Local barrier manager for reporting barrier events
122    local_barrier_manager: LocalBarrierManager,
123}
124
125/// Arguments and state for refreshable materialized views
126pub struct RefreshableMaterializeArgs<S: StateStore, SD: ValueRowSerde> {
127    /// Table catalog for main table
128    pub table_catalog: Table,
129
130    /// Table catalog for staging table
131    pub staging_table_catalog: Table,
132
133    /// During data refresh (between `RefreshStart` and `LoadFinish`),
134    /// data will be written to both the main table and the staging table.
135    ///
136    /// The staging table is PK-only.
137    ///
138    /// After `LoadFinish`, we will do a `DELETE FROM main_table WHERE pk NOT IN (SELECT pk FROM staging_table)`.
139    /// The commit of the `RefreshStart` barrier truncates the staging table, so every cycle loads
140    /// into an empty one.
141    pub staging_table: StateTableInner<S, SD>,
142
143    /// Per-vnode merge progress of the current cycle; the merge resumes from it after every barrier.
144    pub progress_table: RefreshProgressTable<S>,
145
146    /// `prev` epoch of the running cycle's `RefreshStart` barrier, `None` outside a cycle. The
147    /// staging truncation is committed at that epoch, so the merge waits for it to be visible
148    /// locally.
149    pub refresh_start_prev_epoch: Option<u64>,
150
151    /// Table ID for this refreshable materialized view
152    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    /// Create new `RefreshableMaterializeArgs`
161    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        // staging table is pk-only, and we don't need to check value consistency
171        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        // Get primary key length from main table catalog
186        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        // For tables with watermark TTL. Due to async state cleaning, it's uncertain whether an expired
210        // key has been cleaned up by the storage or not, thus we are not sure if to write `Update` or `Insert`
211        // if the key appears again.
212        StateTableOpConsistencyLevel::Inconsistent
213    } else if !subscriber_ids.is_empty() {
214        StateTableOpConsistencyLevel::LogStoreEnabled
215    } else if !may_have_downstream && matches!(conflict_behavior, ConflictBehavior::Overwrite) {
216        // Table with overwrite conflict behavior could disable conflict check
217        // if no downstream mv depends on it, so we use a inconsistent_op to skip sanity check as well.
218        StateTableOpConsistencyLevel::Inconsistent
219    } else {
220        StateTableOpConsistencyLevel::ConsistentOldValue
221    }
222}
223
224impl<S: StateStore, SD: ValueRowSerde> MaterializeExecutor<S, SD> {
225    /// Create a new `MaterializeExecutor` with distribution specified with `distribution_keys` and
226    /// `vnodes`. For singleton distribution, `distribution_keys` should be empty and `vnodes`
227    /// should be `None`.
228    #[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        // Extract TOAST-able column indices from table columns.
252        // Only for PostgreSQL CDC tables.
253        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                    // Currently supports TOAST updates for:
261                    // - jsonb (DataType::Jsonb)
262                    // - varchar (DataType::Varchar)
263                    // - bytea (DataType::Bytea)
264                    // - pgvector (DataType::Vector)
265                    // - One-dimensional arrays of the above types (DataType::List)
266                    //   Note: Some array types may not be fully supported yet, see issue  https://github.com/risingwavelabs/risingwave/issues/22916 for details.
267
268                    // For details on how TOAST values are handled, see comments in `is_debezium_unavailable_value`.
269                    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        // Note: The current implementation could potentially trigger a switch on the inconsistent_op flag. If the storage relies on this flag to perform optimizations, it would be advisable to maintain consistency with it throughout the lifecycle.
312        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; // Save epoch for later use (unused in normal execution)
381        // The first barrier message should be propagated.
382        yield Message::Barrier(barrier);
383        self.state_table.init_epoch(first_epoch).await?;
384
385        // default to normal ingestion
386        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            // A refresh interrupted by recovery is not resumed here: the staging table may be
393            // only partially loaded. Meta abandons the cycle and runs it again.
394            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 execution loop: cycles through Stage 1 -> Stage 2 -> Stage 3 -> Stage 1...
406        '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                                // This is an optimization that handles conflicts only when a particular materialized view downstream has no MV dependencies.
437                                // This optimization is applied only when there is no specified version column and the is_consistent_op flag of the state table is false,
438                                // and the conflict behavior is overwrite. We can rely on the state table to overwrite the conflicting rows in the storage,
439                                // while outputting inconsistent changes to downstream which no one will subscribe to.
440                                // For tables with watermark TTL (indicated by `cleaned_by_ttl_watermark`), conflict check must be enabled.
441                                // TODO(ttl): differentiate between table consistency and downstream consistency.
442                                // When the table carries TOAST-able columns (PostgreSQL CDC), the
443                                // `MaterializeCache` is the only place where the Debezium
444                                // unchanged-TOAST placeholder is detected and swapped for the row's
445                                // previous value. Demoting to `NoCheck` here would skip the cache
446                                // entirely and let the placeholder land in state. Keep the original
447                                // `Overwrite` behavior in that case so the replacement path runs.
448                                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.is_none()
454                                {
455                                    ConflictBehavior::NoCheck
456                                } else {
457                                    self.conflict_behavior
458                                };
459
460                                match optimized_conflict_behavior {
461                                    checked_conflict_behaviors!() => {
462                                        if chunk.cardinality() == 0 {
463                                            // empty chunk
464                                            continue;
465                                        }
466
467                                        // For refreshable materialized views, write to staging table during refresh
468                                        // Do not use generate_output here.
469                                        if let Some(ref mut refresh_args) = self.refresh_args
470                                            && refresh_args.is_refreshing()
471                                        {
472                                            let key_chunk = chunk
473                                                .clone()
474                                                .project(self.state_table.pk_indices());
475                                            tracing::trace!(
476                                                staging_chunk = %key_chunk.to_pretty(),
477                                                input_chunk = %chunk.to_pretty(),
478                                                "writing to staging table"
479                                            );
480                                            if cfg!(debug_assertions) {
481                                                // refreshable source should be append-only
482                                                assert!(
483                                                    key_chunk
484                                                        .ops()
485                                                        .iter()
486                                                        .all(|op| op == &Op::Insert)
487                                                );
488                                            }
489                                            refresh_args
490                                                .staging_table
491                                                .write_chunk(key_chunk.clone());
492                                            refresh_args.staging_table.try_flush().await?;
493                                        }
494
495                                        let cache = self.materialize_cache.as_mut().unwrap();
496                                        let change_buffer =
497                                            cache.handle_new(chunk, &self.state_table).await?;
498
499                                        let output_chunk = if self.stream_key_indices
500                                            == self.state_table.pk_indices()
501                                        {
502                                            change_buffer.into_chunk::<{ cb_kind::RETRACT }>(
503                                                data_types.clone(),
504                                            )
505                                        } else {
506                                            // We only hit this branch for TTL-ed tables for now.
507                                            // Assert stream key is a superset of pk.
508                                            debug_assert!(
509                                                self.state_table
510                                                    .pk_indices()
511                                                    .iter()
512                                                    .all(|&i| self.stream_key_indices.contains(&i))
513                                            );
514                                            change_buffer.into_chunk_with_key(
515                                                data_types.clone(),
516                                                &self.stream_key_indices,
517                                            )
518                                        };
519
520                                        match output_chunk {
521                                            Some(output_chunk) => {
522                                                self.state_table.write_chunk(output_chunk.clone());
523                                                self.state_table.try_flush().await?;
524                                                yield Message::Chunk(output_chunk);
525                                            }
526                                            None => continue,
527                                        }
528                                    }
529                                    ConflictBehavior::NoCheck => {
530                                        self.state_table.write_chunk(chunk.clone());
531                                        self.state_table.try_flush().await?;
532
533                                        // For refreshable materialized views, also write to staging table during refresh
534                                        if let Some(ref mut refresh_args) = self.refresh_args
535                                            && refresh_args.is_refreshing()
536                                        {
537                                            let key_chunk = chunk
538                                                .clone()
539                                                .project(self.state_table.pk_indices());
540                                            tracing::trace!(
541                                                staging_chunk = %key_chunk.to_pretty(),
542                                                input_chunk = %chunk.to_pretty(),
543                                                "writing to staging table"
544                                            );
545                                            if cfg!(debug_assertions) {
546                                                // refreshable source should be append-only
547                                                assert!(
548                                                    key_chunk
549                                                        .ops()
550                                                        .iter()
551                                                        .all(|op| op == &Op::Insert)
552                                                );
553                                            }
554                                            refresh_args
555                                                .staging_table
556                                                .write_chunk(key_chunk.clone());
557                                            refresh_args.staging_table.try_flush().await?;
558                                        }
559
560                                        yield Message::Chunk(chunk);
561                                    }
562                                }
563                            }
564                            Message::Barrier(barrier) => {
565                                *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
566                                    barrier,
567                                    expect_next_state: Box::new(
568                                        MaterializeStreamState::NormalIngestion,
569                                    ),
570                                };
571                                continue 'main_loop;
572                            }
573                        }
574                    }
575
576                    return Err(StreamExecutorError::from(ErrorKind::Uncategorized(
577                        anyhow::anyhow!(
578                            "Input stream terminated unexpectedly during normal ingestion"
579                        ),
580                    )));
581                }
582                MaterializeStreamState::MergingData => {
583                    let Some(refresh_args) = self.refresh_args.as_mut() else {
584                        panic!(
585                            "MaterializeExecutor entered CleanUp state without refresh_args configured"
586                        );
587                    };
588                    tracing::info!(table_id = %refresh_args.table_id, "on_load_finish: Starting table replacement operation");
589
590                    debug_assert_eq!(
591                        self.state_table.vnodes(),
592                        refresh_args.staging_table.vnodes()
593                    );
594                    debug_assert_eq!(
595                        refresh_args.staging_table.vnodes(),
596                        refresh_args.progress_table.vnodes()
597                    );
598
599                    let mut rows_to_delete = vec![];
600                    let mut merge_complete = false;
601                    let mut pending_barrier: Option<Barrier> = None;
602
603                    // Scope to limit immutable borrows to state tables
604                    {
605                        let left_input = input.by_ref().map(Either::Left);
606                        let right_merge_sort = pin!(
607                            Self::make_mergesort_stream(
608                                &self.state_table,
609                                &refresh_args.staging_table,
610                                &mut refresh_args.progress_table
611                            )
612                            .map(Either::Right)
613                        );
614
615                        // Prefer to select input stream to handle barriers promptly
616                        // Rebuild the merge stream each time processing a barrier
617                        let mut merge_stream =
618                            select_with_strategy(left_input, right_merge_sort, |_: &mut ()| {
619                                stream::PollNext::Left
620                            });
621
622                        #[for_await]
623                        'merge_stream: for either in &mut merge_stream {
624                            match either {
625                                Either::Left(msg) => {
626                                    let msg = msg?;
627                                    match msg {
628                                        Message::Watermark(w) => yield Message::Watermark(w),
629                                        Message::Chunk(chunk) => {
630                                            tracing::warn!(chunk = %chunk.to_pretty(), "chunk is ignored during merge phase");
631                                        }
632                                        Message::Barrier(b) => {
633                                            pending_barrier = Some(b);
634                                            break 'merge_stream;
635                                        }
636                                    }
637                                }
638                                Either::Right(result) => {
639                                    match result? {
640                                        Some((_vnode, row)) => {
641                                            rows_to_delete.push(row);
642                                        }
643                                        None => {
644                                            // Merge stream finished
645                                            merge_complete = true;
646
647                                            // If the merge stream finished, we need to wait for the next barrier to commit states
648                                        }
649                                    }
650                                }
651                            }
652                        }
653                    }
654
655                    // Process collected rows for deletion
656                    for row in &rows_to_delete {
657                        self.state_table.delete(row);
658                    }
659                    if let Some(cache) = &mut self.materialize_cache {
660                        cache.invalidate_rows(&rows_to_delete, &self.state_table);
661                    }
662                    if !rows_to_delete.is_empty() {
663                        let to_delete_chunk = StreamChunk::from_rows(
664                            &rows_to_delete
665                                .iter()
666                                .map(|row| (Op::Delete, row))
667                                .collect_vec(),
668                            &self.schema.data_types(),
669                        );
670
671                        yield Message::Chunk(to_delete_chunk);
672                    }
673
674                    // should wait for at least one barrier
675                    assert!(pending_barrier.is_some(), "pending barrier is not set");
676
677                    *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
678                        barrier: pending_barrier.unwrap(),
679                        expect_next_state: if merge_complete {
680                            Box::new(MaterializeStreamState::CleanUp)
681                        } else {
682                            Box::new(MaterializeStreamState::MergingData)
683                        },
684                    };
685                    continue 'main_loop;
686                }
687                MaterializeStreamState::CleanUp => {
688                    let Some(refresh_args) = self.refresh_args.as_mut() else {
689                        panic!(
690                            "MaterializeExecutor entered MergingData state without refresh_args configured"
691                        );
692                    };
693                    tracing::info!(table_id = %refresh_args.table_id, "on_load_finish: resuming CleanUp Stage");
694
695                    #[for_await]
696                    for msg in input.by_ref() {
697                        let msg = msg?;
698                        match msg {
699                            Message::Watermark(w) => {
700                                if self.cleaned_by_ttl_watermark
701                                    && self.state_table.clean_watermark_index == Some(w.col_idx)
702                                {
703                                    self.state_table.update_watermark(w.val.clone());
704                                }
705                                yield Message::Watermark(w)
706                            }
707                            Message::Chunk(chunk) => {
708                                tracing::warn!(chunk = %chunk.to_pretty(), "chunk is ignored during merge phase");
709                            }
710                            Message::Barrier(barrier) if !barrier.is_checkpoint() => {
711                                *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
712                                    barrier,
713                                    expect_next_state: Box::new(MaterializeStreamState::CleanUp),
714                                };
715                                continue 'main_loop;
716                            }
717                            Message::Barrier(barrier) => {
718                                self.local_barrier_manager.report_refresh_finished(
719                                    barrier.epoch,
720                                    self.actor_context.id,
721                                    refresh_args.table_id,
722                                );
723                                tracing::debug!(table_id = %refresh_args.table_id, "on_load_finish: reported refresh finished");
724
725                                *inner_state = MaterializeStreamState::CommitAndYieldBarrier {
726                                    barrier,
727                                    expect_next_state: Box::new(MaterializeStreamState::RefreshEnd),
728                                };
729                                continue 'main_loop;
730                            }
731                        }
732                    }
733                }
734                MaterializeStreamState::RefreshEnd => {
735                    let Some(refresh_args) = self.refresh_args.as_mut() else {
736                        panic!(
737                            "MaterializeExecutor entered RefreshEnd state without refresh_args configured"
738                        );
739                    };
740                    tracing::info!(table_id = %refresh_args.table_id, "RefreshEnd: Refresh completed");
741                    refresh_args.refresh_start_prev_epoch = None;
742                    *inner_state = MaterializeStreamState::NormalIngestion;
743                    continue 'main_loop;
744                }
745                MaterializeStreamState::CommitAndYieldBarrier {
746                    barrier,
747                    mut expect_next_state,
748                } => {
749                    let mut wait_staging_visible = None;
750                    if let Some(ref mut refresh_args) = self.refresh_args {
751                        match barrier.mutation.as_deref() {
752                            Some(Mutation::RefreshStart {
753                                table_id: refresh_table_id,
754                                associated_source_id: _,
755                            }) if *refresh_table_id == refresh_args.table_id => {
756                                if refresh_args.is_refreshing() {
757                                    consistency_panic!(
758                                        table_id = %refresh_table_id,
759                                        "RefreshStart received while a refresh is in progress"
760                                    );
761                                }
762                                refresh_args.refresh_start_prev_epoch = Some(barrier.epoch.prev);
763                                tracing::info!(table_id = %refresh_table_id, "RefreshStart barrier received");
764
765                                // Initialize progress tracking for all VNodes
766                                Self::init_refresh_progress(
767                                    &self.state_table,
768                                    &mut refresh_args.progress_table,
769                                    barrier.epoch.curr,
770                                )?;
771                            }
772                            Some(Mutation::LoadFinish {
773                                associated_source_id: load_finish_source_id,
774                            }) => {
775                                // Get associated source id from table catalog
776                                let associated_source_id: SourceId = match refresh_args
777                                    .table_catalog
778                                    .optional_associated_source_id
779                                {
780                                    Some(id) => id.into(),
781                                    None => unreachable!("associated_source_id is not set"),
782                                };
783
784                                if *load_finish_source_id == associated_source_id {
785                                    if !refresh_args.is_refreshing() {
786                                        // Merging now would run against an empty staging table.
787                                        consistency_panic!(
788                                            table_id = %refresh_args.table_id,
789                                            "LoadFinish received without a refresh in progress"
790                                        );
791                                    } else {
792                                        tracing::info!(
793                                            %load_finish_source_id,
794                                            "LoadFinish received, starting data replacement"
795                                        );
796                                        *expect_next_state =
797                                            MaterializeStreamState::<_>::MergingData;
798                                        wait_staging_visible =
799                                            refresh_args.refresh_start_prev_epoch;
800                                    }
801                                }
802                            }
803                            _ => {}
804                        }
805                    }
806
807                    // ===== normal operation =====
808
809                    // If a downstream mv depends on the current table, we need to do conflict check again.
810                    if !self.may_have_downstream
811                        && barrier.has_more_downstream_fragments(self.actor_context.id)
812                    {
813                        self.may_have_downstream = true;
814                    }
815                    Self::may_update_depended_subscriptions(
816                        &mut self.subscriber_ids,
817                        &barrier,
818                        mv_table_id,
819                    );
820                    let op_consistency_level = get_op_consistency_level(
821                        self.cleaned_by_ttl_watermark,
822                        self.conflict_behavior,
823                        self.may_have_downstream,
824                        &self.subscriber_ids,
825                    );
826                    let post_commit = self
827                        .state_table
828                        .commit_may_switch_consistent_op(barrier.epoch, op_consistency_level)
829                        .await?;
830
831                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(self.actor_context.id);
832
833                    // Commit staging table for refreshable materialized views
834                    let refresh_post_commit = if let Some(ref mut refresh_args) = self.refresh_args
835                    {
836                        Some((
837                            refresh_args.staging_table.commit(barrier.epoch).await?,
838                            refresh_args.progress_table.commit(barrier.epoch).await?,
839                        ))
840                    } else {
841                        None
842                    };
843
844                    let b_epoch = barrier.epoch;
845                    yield Message::Barrier(barrier);
846
847                    // Update the vnode bitmap for the state table if asked.
848                    if let Some((_, cache_may_stale)) = post_commit
849                        .post_yield_barrier(update_vnode_bitmap.clone())
850                        .await?
851                        && cache_may_stale
852                        && let Some(cache) = &mut self.materialize_cache
853                    {
854                        cache.clear();
855                    }
856
857                    // Handle staging table post commit
858                    if let Some((staging_post_commit, progress_post_commit)) = refresh_post_commit {
859                        staging_post_commit
860                            .post_yield_barrier(update_vnode_bitmap.clone())
861                            .await?;
862                        progress_post_commit
863                            .post_yield_barrier(update_vnode_bitmap)
864                            .await?;
865                    }
866
867                    if let (Some(epoch), Some(refresh_args)) =
868                        (wait_staging_visible, self.refresh_args.as_ref())
869                    {
870                        // The truncation of the staging table at the start of the cycle must be
871                        // visible locally before the merge reads it.
872                        refresh_args
873                            .staging_table
874                            .try_wait_committed_epoch(epoch)
875                            .await?;
876                    }
877
878                    self.metrics
879                        .materialize_current_epoch
880                        .set(b_epoch.curr as i64);
881
882                    // ====== transition to next state ======
883
884                    *inner_state = *expect_next_state;
885                }
886            }
887        }
888    }
889
890    /// Stream that yields rows to be deleted from main table.
891    /// Yields `Some((vnode, row))` for rows that exist in main but not in staging.
892    /// Yields `None` when finished processing all vnodes.
893    #[try_stream(ok = Option<(VirtualNode, OwnedRow)>, error = StreamExecutorError)]
894    async fn make_mergesort_stream<'a>(
895        main_table: &'a StateTableInner<S, SD>,
896        staging_table: &'a StateTableInner<S, SD>,
897        progress_table: &'a mut RefreshProgressTable<S>,
898    ) {
899        for vnode in main_table.vnodes().clone().iter_vnodes() {
900            let mut processed_rows = 0;
901            // The merge is rebuilt after every barrier and resumes from the recorded position.
902            let pk_range: (Bound<OwnedRow>, Bound<OwnedRow>) =
903                if let Some(current_entry) = progress_table.get_progress(vnode) {
904                    if current_entry.is_completed {
905                        tracing::debug!(vnode = vnode.to_index(), "vnode already merged");
906                        continue;
907                    }
908                    processed_rows += current_entry.processed_rows;
909                    tracing::debug!(vnode = vnode.to_index(), "Started merging VNode");
910
911                    if let Some(current_state) = &current_entry.current_pos {
912                        (Bound::Excluded(current_state.clone()), Bound::Unbounded)
913                    } else {
914                        (Bound::Unbounded, Bound::Unbounded)
915                    }
916                } else {
917                    (Bound::Unbounded, Bound::Unbounded)
918                };
919
920            let iter_main = main_table
921                .iter_keyed_row_with_vnode(
922                    vnode,
923                    &pk_range,
924                    PrefetchOptions::prefetch_for_large_range_scan(),
925                )
926                .await?;
927            let iter_staging = staging_table
928                .iter_keyed_row_with_vnode(
929                    vnode,
930                    &pk_range,
931                    PrefetchOptions::prefetch_for_large_range_scan(),
932                )
933                .await?;
934
935            pin_mut!(iter_main);
936            pin_mut!(iter_staging);
937
938            // Sort-merge join implementation using dual pointers
939            let mut main_item: Option<KeyedRow<Bytes>> = iter_main.next().await.transpose()?;
940            let mut staging_item: Option<KeyedRow<Bytes>> =
941                iter_staging.next().await.transpose()?;
942
943            while let Some(main_kv) = main_item {
944                let main_key = main_kv.key();
945
946                // Advance staging iterator until we find a key >= main_key
947                let mut should_delete = false;
948                while let Some(staging_kv) = &staging_item {
949                    let staging_key = staging_kv.key();
950                    match main_key.cmp(staging_key) {
951                        std::cmp::Ordering::Greater => {
952                            // main_key > staging_key, advance staging
953                            staging_item = iter_staging.next().await.transpose()?;
954                        }
955                        std::cmp::Ordering::Equal => {
956                            // Keys match, this row exists in both tables, no need to delete
957                            break;
958                        }
959                        std::cmp::Ordering::Less => {
960                            // main_key < staging_key, main row doesn't exist in staging, delete it
961                            should_delete = true;
962                            break;
963                        }
964                    }
965                }
966
967                // If staging_item is None, all remaining main rows should be deleted
968                if staging_item.is_none() {
969                    should_delete = true;
970                }
971
972                if should_delete {
973                    yield Some((vnode, main_kv.row().clone()));
974                }
975
976                // Advance main iterator
977                processed_rows += 1;
978                tracing::debug!(
979                    "set progress table: vnode = {:?}, processed_rows = {:?}",
980                    vnode,
981                    processed_rows
982                );
983                progress_table.set_progress(
984                    vnode,
985                    Some(
986                        main_kv
987                            .row()
988                            .project(main_table.pk_indices())
989                            .to_owned_row(),
990                    ),
991                    false,
992                    processed_rows,
993                )?;
994                main_item = iter_main.next().await.transpose()?;
995            }
996
997            // Mark this VNode as completed
998            if let Some(current_entry) = progress_table.get_progress(vnode) {
999                progress_table.set_progress(
1000                    vnode,
1001                    current_entry.current_pos.clone(),
1002                    true, // completed
1003                    current_entry.processed_rows,
1004                )?;
1005
1006                tracing::debug!(vnode = vnode.to_index(), "Completed merging VNode");
1007            }
1008        }
1009
1010        // Signal completion
1011        yield None;
1012    }
1013
1014    /// return true when changed
1015    fn may_update_depended_subscriptions(
1016        depended_subscriptions: &mut HashSet<SubscriberId>,
1017        barrier: &Barrier,
1018        mv_table_id: TableId,
1019    ) {
1020        for subscriber_id in barrier.added_subscriber_on_mv_table(mv_table_id) {
1021            if !depended_subscriptions.insert(subscriber_id) {
1022                warn!(
1023                    ?depended_subscriptions,
1024                    %mv_table_id,
1025                    %subscriber_id,
1026                    "subscription id already exists"
1027                );
1028            }
1029        }
1030
1031        if let Some(subscriptions_to_drop) = barrier.as_subscriptions_to_drop() {
1032            for SubscriptionUpstreamInfo {
1033                subscriber_id,
1034                upstream_mv_table_id,
1035            } in subscriptions_to_drop
1036            {
1037                if *upstream_mv_table_id == mv_table_id
1038                    && !depended_subscriptions.remove(subscriber_id)
1039                {
1040                    warn!(
1041                        ?depended_subscriptions,
1042                        %mv_table_id,
1043                        %subscriber_id,
1044                        "drop non existing subscriber_id id"
1045                    );
1046                }
1047            }
1048        }
1049    }
1050
1051    /// Initialize refresh progress tracking for all `VNodes`
1052    fn init_refresh_progress(
1053        state_table: &StateTableInner<S, SD>,
1054        progress_table: &mut RefreshProgressTable<S>,
1055        _epoch: u64,
1056    ) -> StreamExecutorResult<()> {
1057        debug_assert_eq!(state_table.vnodes(), progress_table.vnodes());
1058
1059        // Initialize progress for all VNodes in the current bitmap
1060        for vnode in state_table.vnodes().iter_vnodes() {
1061            progress_table.set_progress(
1062                vnode, None,  // initial position
1063                false, // not completed yet
1064                0,     // initial processed rows
1065            )?;
1066        }
1067
1068        tracing::info!(
1069            vnodes_count = state_table.vnodes().count_ones(),
1070            "Initialized refresh progress tracking for all VNodes"
1071        );
1072
1073        Ok(())
1074    }
1075}
1076
1077impl<S: StateStore> MaterializeExecutor<S, BasicSerde> {
1078    /// Create a new `MaterializeExecutor` without distribution info for test purpose.
1079    #[cfg(any(test, feature = "test"))]
1080    pub async fn for_test(
1081        input: Executor,
1082        store: S,
1083        table_id: TableId,
1084        keys: Vec<ColumnOrder>,
1085        column_ids: Vec<risingwave_common::catalog::ColumnId>,
1086        watermark_epoch: AtomicU64Ref,
1087        conflict_behavior: ConflictBehavior,
1088    ) -> Self {
1089        Self::for_test_inner(
1090            input,
1091            store,
1092            table_id,
1093            keys,
1094            column_ids,
1095            watermark_epoch,
1096            conflict_behavior,
1097            None,
1098            None,
1099        )
1100        .await
1101    }
1102
1103    #[cfg(any(test, feature = "test"))]
1104    #[expect(clippy::too_many_arguments)]
1105    pub async fn for_test_with_stream_key(
1106        input: Executor,
1107        store: S,
1108        table_id: TableId,
1109        keys: Vec<ColumnOrder>,
1110        stream_key: Vec<usize>,
1111        column_ids: Vec<risingwave_common::catalog::ColumnId>,
1112        watermark_epoch: AtomicU64Ref,
1113        conflict_behavior: ConflictBehavior,
1114    ) -> Self {
1115        Self::for_test_inner(
1116            input,
1117            store,
1118            table_id,
1119            keys,
1120            column_ids,
1121            watermark_epoch,
1122            conflict_behavior,
1123            Some(stream_key),
1124            None,
1125        )
1126        .await
1127    }
1128
1129    /// Create a TTL-ed `MaterializeExecutor` cleaned by the watermark of `ttl_column`, which
1130    /// is appended to the stream key, for test purpose.
1131    #[cfg(any(test, feature = "test"))]
1132    #[expect(clippy::too_many_arguments)]
1133    pub async fn for_test_with_ttl(
1134        input: Executor,
1135        store: S,
1136        table_id: TableId,
1137        keys: Vec<ColumnOrder>,
1138        column_ids: Vec<risingwave_common::catalog::ColumnId>,
1139        watermark_epoch: AtomicU64Ref,
1140        conflict_behavior: ConflictBehavior,
1141        ttl_column: usize,
1142    ) -> Self {
1143        let stream_key = keys
1144            .iter()
1145            .map(|k| k.column_index)
1146            .chain(std::iter::once(ttl_column))
1147            .collect();
1148        Self::for_test_inner(
1149            input,
1150            store,
1151            table_id,
1152            keys,
1153            column_ids,
1154            watermark_epoch,
1155            conflict_behavior,
1156            Some(stream_key),
1157            Some(ttl_column),
1158        )
1159        .await
1160    }
1161
1162    #[cfg(any(test, feature = "test"))]
1163    #[expect(clippy::too_many_arguments)]
1164    async fn for_test_inner(
1165        input: Executor,
1166        store: S,
1167        table_id: TableId,
1168        keys: Vec<ColumnOrder>,
1169        column_ids: Vec<risingwave_common::catalog::ColumnId>,
1170        watermark_epoch: AtomicU64Ref,
1171        conflict_behavior: ConflictBehavior,
1172        stream_key: Option<Vec<usize>>,
1173        ttl_column: Option<usize>,
1174    ) -> Self {
1175        use risingwave_common::util::iter_util::ZipEqFast;
1176
1177        let arrange_columns: Vec<usize> = keys.iter().map(|k| k.column_index).collect();
1178        let arrange_order_types = keys.iter().map(|k| k.order_type).collect();
1179        let schema = input.schema().clone();
1180        let columns: Vec<ColumnDesc> = column_ids
1181            .into_iter()
1182            .zip_eq_fast(schema.fields.iter())
1183            .map(|(column_id, field)| ColumnDesc::unnamed(column_id, field.data_type()))
1184            .collect_vec();
1185
1186        let row_serde = BasicSerde::new(
1187            Arc::from((0..columns.len()).collect_vec()),
1188            Arc::from(columns.clone().into_boxed_slice()),
1189        );
1190        let stream_key_indices = stream_key.unwrap_or_else(|| arrange_columns.clone());
1191        let mut table_catalog = crate::common::table::test_utils::gen_pbtable(
1192            table_id,
1193            columns,
1194            arrange_order_types,
1195            arrange_columns.clone(),
1196            0,
1197        );
1198        table_catalog.stream_key = stream_key_indices.iter().map(|i| *i as i32).collect();
1199        let state_table = if let Some(ttl_column) = ttl_column {
1200            table_catalog.clean_watermark_indices = vec![ttl_column as u32];
1201            StateTableInner::from_table_catalog_inconsistent_op(&table_catalog, store, None).await
1202        } else {
1203            StateTableInner::from_table_catalog(&table_catalog, store, None).await
1204        };
1205
1206        let unused = StreamingMetrics::unused();
1207        let metrics = unused.new_materialize_metrics(table_id, 1.into(), 2.into());
1208        let cache_metrics = unused.new_materialize_cache_metrics(table_id, 1.into(), 2.into());
1209
1210        Self {
1211            input,
1212            schema,
1213            state_table,
1214            stream_key_indices,
1215            arrange_key_indices: arrange_columns.clone(),
1216            actor_context: ActorContext::for_test(0),
1217            materialize_cache: MaterializeCache::new(
1218                watermark_epoch,
1219                MetricsInfo::for_test(),
1220                row_serde,
1221                vec![],
1222                conflict_behavior,
1223                None,
1224                cache_metrics,
1225            ),
1226            toastable_column_indices: None,
1227            conflict_behavior,
1228            cleaned_by_ttl_watermark: ttl_column.is_some(),
1229            version_column_indices: vec![],
1230            is_dummy_table: false,
1231            may_have_downstream: true,
1232            subscriber_ids: HashSet::new(),
1233            metrics,
1234            refresh_args: None, // Test constructor doesn't support refresh functionality
1235            local_barrier_manager: LocalBarrierManager::for_test(),
1236        }
1237    }
1238}
1239
1240impl<S: StateStore, SD: ValueRowSerde> Execute for MaterializeExecutor<S, SD> {
1241    fn execute(self: Box<Self>) -> BoxedMessageStream {
1242        self.execute_inner().boxed()
1243    }
1244}
1245
1246impl<S: StateStore, SD: ValueRowSerde> std::fmt::Debug for MaterializeExecutor<S, SD> {
1247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1248        f.debug_struct("MaterializeExecutor")
1249            .field("arrange_key_indices", &self.arrange_key_indices)
1250            .field("stream_key_indices", &self.stream_key_indices)
1251            .finish()
1252    }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257
1258    use std::iter;
1259    use std::sync::atomic::AtomicU64;
1260
1261    use rand::rngs::SmallRng;
1262    use rand::{Rng, RngCore, SeedableRng};
1263    use risingwave_common::array::stream_chunk::{StreamChunkMut, StreamChunkTestExt};
1264    use risingwave_common::catalog::Field;
1265    use risingwave_common::util::epoch::test_epoch;
1266    use risingwave_common::util::sort_util::OrderType;
1267    use risingwave_hummock_sdk::HummockReadEpoch;
1268    use risingwave_storage::memory::MemoryStateStore;
1269    use risingwave_storage::table::batch_table::BatchTable;
1270
1271    use super::*;
1272    use crate::executor::test_utils::*;
1273
1274    #[tokio::test]
1275    async fn test_materialize_executor() {
1276        // Prepare storage and memtable.
1277        let memory_state_store = MemoryStateStore::new();
1278        let table_id = TableId::new(1);
1279        // Two columns of int32 type, the first column is PK.
1280        let schema = Schema::new(vec![
1281            Field::unnamed(DataType::Int32),
1282            Field::unnamed(DataType::Int32),
1283        ]);
1284        let column_ids = vec![0.into(), 1.into()];
1285
1286        // Prepare source chunks.
1287        let chunk1 = StreamChunk::from_pretty(
1288            " i i
1289            + 1 4
1290            + 2 5
1291            + 3 6",
1292        );
1293        let chunk2 = StreamChunk::from_pretty(
1294            " i i
1295            + 7 8
1296            - 3 6",
1297        );
1298
1299        // Prepare stream executors.
1300        let source = MockSource::with_messages(vec![
1301            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1302            Message::Chunk(chunk1),
1303            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1304            Message::Chunk(chunk2),
1305            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1306        ])
1307        .into_executor(schema.clone(), StreamKey::new());
1308
1309        let order_types = vec![OrderType::ascending()];
1310        let column_descs = vec![
1311            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1312            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1313        ];
1314
1315        let table = BatchTable::for_test(
1316            memory_state_store.clone(),
1317            table_id,
1318            column_descs,
1319            order_types,
1320            vec![0],
1321            vec![0, 1],
1322        );
1323
1324        let mut materialize_executor = MaterializeExecutor::for_test(
1325            source,
1326            memory_state_store,
1327            table_id,
1328            vec![ColumnOrder::new(0, OrderType::ascending())],
1329            column_ids,
1330            Arc::new(AtomicU64::new(0)),
1331            ConflictBehavior::NoCheck,
1332        )
1333        .await
1334        .boxed()
1335        .execute();
1336        materialize_executor.next().await.transpose().unwrap();
1337
1338        materialize_executor.next().await.transpose().unwrap();
1339
1340        // First stream chunk. We check the existence of (3) -> (3,6)
1341        match materialize_executor.next().await.transpose().unwrap() {
1342            Some(Message::Barrier(_)) => {
1343                let row = table
1344                    .get_row(
1345                        &OwnedRow::new(vec![Some(3_i32.into())]),
1346                        HummockReadEpoch::NoWait(u64::MAX),
1347                    )
1348                    .await
1349                    .unwrap();
1350                assert_eq!(
1351                    row,
1352                    Some(OwnedRow::new(vec![Some(3_i32.into()), Some(6_i32.into())]))
1353                );
1354            }
1355            _ => unreachable!(),
1356        }
1357        materialize_executor.next().await.transpose().unwrap();
1358        // Second stream chunk. We check the existence of (7) -> (7,8)
1359        match materialize_executor.next().await.transpose().unwrap() {
1360            Some(Message::Barrier(_)) => {
1361                let row = table
1362                    .get_row(
1363                        &OwnedRow::new(vec![Some(7_i32.into())]),
1364                        HummockReadEpoch::NoWait(u64::MAX),
1365                    )
1366                    .await
1367                    .unwrap();
1368                assert_eq!(
1369                    row,
1370                    Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
1371                );
1372            }
1373            _ => unreachable!(),
1374        }
1375    }
1376
1377    // https://github.com/risingwavelabs/risingwave/issues/13346
1378    #[tokio::test]
1379    async fn test_upsert_stream() {
1380        // Prepare storage and memtable.
1381        let memory_state_store = MemoryStateStore::new();
1382        let table_id = TableId::new(1);
1383        // Two columns of int32 type, the first column is PK.
1384        let schema = Schema::new(vec![
1385            Field::unnamed(DataType::Int32),
1386            Field::unnamed(DataType::Int32),
1387        ]);
1388        let column_ids = vec![0.into(), 1.into()];
1389
1390        // test double insert one pk, the latter needs to override the former.
1391        let chunk1 = StreamChunk::from_pretty(
1392            " i i
1393            + 1 1",
1394        );
1395
1396        let chunk2 = StreamChunk::from_pretty(
1397            " i i
1398            + 1 2
1399            - 1 2",
1400        );
1401
1402        // Prepare stream executors.
1403        let source = MockSource::with_messages(vec![
1404            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1405            Message::Chunk(chunk1),
1406            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1407            Message::Chunk(chunk2),
1408            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1409        ])
1410        .into_executor(schema.clone(), StreamKey::new());
1411
1412        let order_types = vec![OrderType::ascending()];
1413        let column_descs = vec![
1414            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1415            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1416        ];
1417
1418        let table = BatchTable::for_test(
1419            memory_state_store.clone(),
1420            table_id,
1421            column_descs,
1422            order_types,
1423            vec![0],
1424            vec![0, 1],
1425        );
1426
1427        let mut materialize_executor = MaterializeExecutor::for_test(
1428            source,
1429            memory_state_store,
1430            table_id,
1431            vec![ColumnOrder::new(0, OrderType::ascending())],
1432            column_ids,
1433            Arc::new(AtomicU64::new(0)),
1434            ConflictBehavior::Overwrite,
1435        )
1436        .await
1437        .boxed()
1438        .execute();
1439        materialize_executor.next().await.transpose().unwrap();
1440
1441        materialize_executor.next().await.transpose().unwrap();
1442        materialize_executor.next().await.transpose().unwrap();
1443        materialize_executor.next().await.transpose().unwrap();
1444
1445        match materialize_executor.next().await.transpose().unwrap() {
1446            Some(Message::Barrier(_)) => {
1447                let row = table
1448                    .get_row(
1449                        &OwnedRow::new(vec![Some(1_i32.into())]),
1450                        HummockReadEpoch::NoWait(u64::MAX),
1451                    )
1452                    .await
1453                    .unwrap();
1454                assert!(row.is_none());
1455            }
1456            _ => unreachable!(),
1457        }
1458    }
1459
1460    #[tokio::test]
1461    async fn test_check_insert_conflict() {
1462        // Prepare storage and memtable.
1463        let memory_state_store = MemoryStateStore::new();
1464        let table_id = TableId::new(1);
1465        // Two columns of int32 type, the first column is PK.
1466        let schema = Schema::new(vec![
1467            Field::unnamed(DataType::Int32),
1468            Field::unnamed(DataType::Int32),
1469        ]);
1470        let column_ids = vec![0.into(), 1.into()];
1471
1472        // test double insert one pk, the latter needs to override the former.
1473        let chunk1 = StreamChunk::from_pretty(
1474            " i i
1475            + 1 3
1476            + 1 4
1477            + 2 5
1478            + 3 6",
1479        );
1480
1481        let chunk2 = StreamChunk::from_pretty(
1482            " i i
1483            + 1 3
1484            + 2 6",
1485        );
1486
1487        // test delete wrong value, delete inexistent pk
1488        let chunk3 = StreamChunk::from_pretty(
1489            " i i
1490            + 1 4",
1491        );
1492
1493        // Prepare stream executors.
1494        let source = MockSource::with_messages(vec![
1495            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1496            Message::Chunk(chunk1),
1497            Message::Chunk(chunk2),
1498            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1499            Message::Chunk(chunk3),
1500            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1501        ])
1502        .into_executor(schema.clone(), StreamKey::new());
1503
1504        let order_types = vec![OrderType::ascending()];
1505        let column_descs = vec![
1506            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1507            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1508        ];
1509
1510        let table = BatchTable::for_test(
1511            memory_state_store.clone(),
1512            table_id,
1513            column_descs,
1514            order_types,
1515            vec![0],
1516            vec![0, 1],
1517        );
1518
1519        let mut materialize_executor = MaterializeExecutor::for_test(
1520            source,
1521            memory_state_store,
1522            table_id,
1523            vec![ColumnOrder::new(0, OrderType::ascending())],
1524            column_ids,
1525            Arc::new(AtomicU64::new(0)),
1526            ConflictBehavior::Overwrite,
1527        )
1528        .await
1529        .boxed()
1530        .execute();
1531        materialize_executor.next().await.transpose().unwrap();
1532
1533        materialize_executor.next().await.transpose().unwrap();
1534        materialize_executor.next().await.transpose().unwrap();
1535
1536        // First stream chunk. We check the existence of (3) -> (3,6)
1537        match materialize_executor.next().await.transpose().unwrap() {
1538            Some(Message::Barrier(_)) => {
1539                let row = table
1540                    .get_row(
1541                        &OwnedRow::new(vec![Some(3_i32.into())]),
1542                        HummockReadEpoch::NoWait(u64::MAX),
1543                    )
1544                    .await
1545                    .unwrap();
1546                assert_eq!(
1547                    row,
1548                    Some(OwnedRow::new(vec![Some(3_i32.into()), Some(6_i32.into())]))
1549                );
1550
1551                let row = table
1552                    .get_row(
1553                        &OwnedRow::new(vec![Some(1_i32.into())]),
1554                        HummockReadEpoch::NoWait(u64::MAX),
1555                    )
1556                    .await
1557                    .unwrap();
1558                assert_eq!(
1559                    row,
1560                    Some(OwnedRow::new(vec![Some(1_i32.into()), Some(3_i32.into())]))
1561                );
1562
1563                let row = table
1564                    .get_row(
1565                        &OwnedRow::new(vec![Some(2_i32.into())]),
1566                        HummockReadEpoch::NoWait(u64::MAX),
1567                    )
1568                    .await
1569                    .unwrap();
1570                assert_eq!(
1571                    row,
1572                    Some(OwnedRow::new(vec![Some(2_i32.into()), Some(6_i32.into())]))
1573                );
1574            }
1575            _ => unreachable!(),
1576        }
1577    }
1578
1579    #[tokio::test]
1580    async fn test_delete_and_update_conflict() {
1581        // Prepare storage and memtable.
1582        let memory_state_store = MemoryStateStore::new();
1583        let table_id = TableId::new(1);
1584        // Two columns of int32 type, the first column is PK.
1585        let schema = Schema::new(vec![
1586            Field::unnamed(DataType::Int32),
1587            Field::unnamed(DataType::Int32),
1588        ]);
1589        let column_ids = vec![0.into(), 1.into()];
1590
1591        // test double insert one pk, the latter needs to override the former.
1592        let chunk1 = StreamChunk::from_pretty(
1593            " i i
1594            + 1 4
1595            + 2 5
1596            + 3 6
1597            U- 8 1
1598            U+ 8 2
1599            + 8 3",
1600        );
1601
1602        // test delete wrong value, delete inexistent pk
1603        let chunk2 = StreamChunk::from_pretty(
1604            " i i
1605            + 7 8
1606            - 3 4
1607            - 5 0",
1608        );
1609
1610        // test delete wrong value, delete inexistent pk
1611        let chunk3 = StreamChunk::from_pretty(
1612            " i i
1613            + 1 5
1614            U- 2 4
1615            U+ 2 8
1616            U- 9 0
1617            U+ 9 1",
1618        );
1619
1620        // Prepare stream executors.
1621        let source = MockSource::with_messages(vec![
1622            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1623            Message::Chunk(chunk1),
1624            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1625            Message::Chunk(chunk2),
1626            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1627            Message::Chunk(chunk3),
1628            Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
1629        ])
1630        .into_executor(schema.clone(), StreamKey::new());
1631
1632        let order_types = vec![OrderType::ascending()];
1633        let column_descs = vec![
1634            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1635            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1636        ];
1637
1638        let table = BatchTable::for_test(
1639            memory_state_store.clone(),
1640            table_id,
1641            column_descs,
1642            order_types,
1643            vec![0],
1644            vec![0, 1],
1645        );
1646
1647        let mut materialize_executor = MaterializeExecutor::for_test(
1648            source,
1649            memory_state_store,
1650            table_id,
1651            vec![ColumnOrder::new(0, OrderType::ascending())],
1652            column_ids,
1653            Arc::new(AtomicU64::new(0)),
1654            ConflictBehavior::Overwrite,
1655        )
1656        .await
1657        .boxed()
1658        .execute();
1659        materialize_executor.next().await.transpose().unwrap();
1660
1661        materialize_executor.next().await.transpose().unwrap();
1662
1663        // First stream chunk. We check the existence of (3) -> (3,6)
1664        match materialize_executor.next().await.transpose().unwrap() {
1665            Some(Message::Barrier(_)) => {
1666                // can read (8, 3), check insert after update
1667                let row = table
1668                    .get_row(
1669                        &OwnedRow::new(vec![Some(8_i32.into())]),
1670                        HummockReadEpoch::NoWait(u64::MAX),
1671                    )
1672                    .await
1673                    .unwrap();
1674                assert_eq!(
1675                    row,
1676                    Some(OwnedRow::new(vec![Some(8_i32.into()), Some(3_i32.into())]))
1677                );
1678            }
1679            _ => unreachable!(),
1680        }
1681        materialize_executor.next().await.transpose().unwrap();
1682
1683        match materialize_executor.next().await.transpose().unwrap() {
1684            Some(Message::Barrier(_)) => {
1685                let row = table
1686                    .get_row(
1687                        &OwnedRow::new(vec![Some(7_i32.into())]),
1688                        HummockReadEpoch::NoWait(u64::MAX),
1689                    )
1690                    .await
1691                    .unwrap();
1692                assert_eq!(
1693                    row,
1694                    Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
1695                );
1696
1697                // check delete wrong value
1698                let row = table
1699                    .get_row(
1700                        &OwnedRow::new(vec![Some(3_i32.into())]),
1701                        HummockReadEpoch::NoWait(u64::MAX),
1702                    )
1703                    .await
1704                    .unwrap();
1705                assert_eq!(row, None);
1706
1707                // check delete wrong pk
1708                let row = table
1709                    .get_row(
1710                        &OwnedRow::new(vec![Some(5_i32.into())]),
1711                        HummockReadEpoch::NoWait(u64::MAX),
1712                    )
1713                    .await
1714                    .unwrap();
1715                assert_eq!(row, None);
1716            }
1717            _ => unreachable!(),
1718        }
1719
1720        materialize_executor.next().await.transpose().unwrap();
1721        // Second stream chunk. We check the existence of (7) -> (7,8)
1722        match materialize_executor.next().await.transpose().unwrap() {
1723            Some(Message::Barrier(_)) => {
1724                let row = table
1725                    .get_row(
1726                        &OwnedRow::new(vec![Some(1_i32.into())]),
1727                        HummockReadEpoch::NoWait(u64::MAX),
1728                    )
1729                    .await
1730                    .unwrap();
1731                assert_eq!(
1732                    row,
1733                    Some(OwnedRow::new(vec![Some(1_i32.into()), Some(5_i32.into())]))
1734                );
1735
1736                // check update wrong value
1737                let row = table
1738                    .get_row(
1739                        &OwnedRow::new(vec![Some(2_i32.into())]),
1740                        HummockReadEpoch::NoWait(u64::MAX),
1741                    )
1742                    .await
1743                    .unwrap();
1744                assert_eq!(
1745                    row,
1746                    Some(OwnedRow::new(vec![Some(2_i32.into()), Some(8_i32.into())]))
1747                );
1748
1749                // check update wrong pk, should become insert
1750                let row = table
1751                    .get_row(
1752                        &OwnedRow::new(vec![Some(9_i32.into())]),
1753                        HummockReadEpoch::NoWait(u64::MAX),
1754                    )
1755                    .await
1756                    .unwrap();
1757                assert_eq!(
1758                    row,
1759                    Some(OwnedRow::new(vec![Some(9_i32.into()), Some(1_i32.into())]))
1760                );
1761            }
1762            _ => unreachable!(),
1763        }
1764    }
1765
1766    #[tokio::test]
1767    async fn test_change_buffer_into_chunk_with_stream_key() {
1768        // Table PK is (col0), but stream key is (col0, col1). When the value column changes due
1769        // to overwrite conflict handling, we should output `- old` + `+ new` rather than `U-`/`U+`.
1770        let memory_state_store = MemoryStateStore::new();
1771        let table_id = TableId::new(1);
1772        let schema = Schema::new(vec![
1773            Field::unnamed(DataType::Int32),
1774            Field::unnamed(DataType::Int32),
1775        ]);
1776        let column_ids = vec![0.into(), 1.into()];
1777
1778        let chunk1 = StreamChunk::from_pretty(
1779            " i i
1780            + 1 4",
1781        );
1782        let chunk2 = StreamChunk::from_pretty(
1783            " i i
1784            + 1 5",
1785        );
1786
1787        let source = MockSource::with_messages(vec![
1788            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1789            Message::Chunk(chunk1),
1790            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1791            Message::Chunk(chunk2),
1792            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1793        ])
1794        .into_executor(schema.clone(), StreamKey::new());
1795
1796        let mut materialize_executor = MaterializeExecutor::for_test_with_stream_key(
1797            source,
1798            memory_state_store,
1799            table_id,
1800            vec![ColumnOrder::new(0, OrderType::ascending())],
1801            vec![0, 1],
1802            column_ids,
1803            Arc::new(AtomicU64::new(0)),
1804            ConflictBehavior::Overwrite,
1805        )
1806        .await
1807        .boxed()
1808        .execute();
1809
1810        // init barrier + first insert
1811        materialize_executor.next().await.transpose().unwrap();
1812        materialize_executor.next().await.transpose().unwrap();
1813
1814        // commit barrier
1815        materialize_executor.next().await.transpose().unwrap();
1816
1817        // overwrite conflict should be converted into delete+insert due to stream key mismatch
1818        match materialize_executor.next().await.transpose().unwrap() {
1819            Some(Message::Chunk(chunk)) => {
1820                assert_eq!(
1821                    chunk.compact_vis(),
1822                    StreamChunk::from_pretty(
1823                        " i i
1824                        - 1 4
1825                        + 1 5"
1826                    )
1827                );
1828            }
1829            other => panic!("expect chunk, got {other:?}"),
1830        }
1831    }
1832
1833    #[tokio::test]
1834    async fn test_ignore_insert_conflict() {
1835        // Prepare storage and memtable.
1836        let memory_state_store = MemoryStateStore::new();
1837        let table_id = TableId::new(1);
1838        // Two columns of int32 type, the first column is PK.
1839        let schema = Schema::new(vec![
1840            Field::unnamed(DataType::Int32),
1841            Field::unnamed(DataType::Int32),
1842        ]);
1843        let column_ids = vec![0.into(), 1.into()];
1844
1845        // test double insert one pk, the latter needs to be ignored.
1846        let chunk1 = StreamChunk::from_pretty(
1847            " i i
1848            + 1 3
1849            + 1 4
1850            + 2 5
1851            + 3 6",
1852        );
1853
1854        let chunk2 = StreamChunk::from_pretty(
1855            " i i
1856            + 1 5
1857            + 2 6",
1858        );
1859
1860        // test delete wrong value, delete inexistent pk
1861        let chunk3 = StreamChunk::from_pretty(
1862            " i i
1863            + 1 6",
1864        );
1865
1866        // Prepare stream executors.
1867        let source = MockSource::with_messages(vec![
1868            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1869            Message::Chunk(chunk1),
1870            Message::Chunk(chunk2),
1871            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1872            Message::Chunk(chunk3),
1873            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1874        ])
1875        .into_executor(schema.clone(), StreamKey::new());
1876
1877        let order_types = vec![OrderType::ascending()];
1878        let column_descs = vec![
1879            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1880            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1881        ];
1882
1883        let table = BatchTable::for_test(
1884            memory_state_store.clone(),
1885            table_id,
1886            column_descs,
1887            order_types,
1888            vec![0],
1889            vec![0, 1],
1890        );
1891
1892        let mut materialize_executor = MaterializeExecutor::for_test(
1893            source,
1894            memory_state_store,
1895            table_id,
1896            vec![ColumnOrder::new(0, OrderType::ascending())],
1897            column_ids,
1898            Arc::new(AtomicU64::new(0)),
1899            ConflictBehavior::IgnoreConflict,
1900        )
1901        .await
1902        .boxed()
1903        .execute();
1904        materialize_executor.next().await.transpose().unwrap();
1905
1906        materialize_executor.next().await.transpose().unwrap();
1907        materialize_executor.next().await.transpose().unwrap();
1908
1909        // First stream chunk. We check the existence of (3) -> (3,6)
1910        match materialize_executor.next().await.transpose().unwrap() {
1911            Some(Message::Barrier(_)) => {
1912                let row = table
1913                    .get_row(
1914                        &OwnedRow::new(vec![Some(3_i32.into())]),
1915                        HummockReadEpoch::NoWait(u64::MAX),
1916                    )
1917                    .await
1918                    .unwrap();
1919                assert_eq!(
1920                    row,
1921                    Some(OwnedRow::new(vec![Some(3_i32.into()), Some(6_i32.into())]))
1922                );
1923
1924                let row = table
1925                    .get_row(
1926                        &OwnedRow::new(vec![Some(1_i32.into())]),
1927                        HummockReadEpoch::NoWait(u64::MAX),
1928                    )
1929                    .await
1930                    .unwrap();
1931                assert_eq!(
1932                    row,
1933                    Some(OwnedRow::new(vec![Some(1_i32.into()), Some(3_i32.into())]))
1934                );
1935
1936                let row = table
1937                    .get_row(
1938                        &OwnedRow::new(vec![Some(2_i32.into())]),
1939                        HummockReadEpoch::NoWait(u64::MAX),
1940                    )
1941                    .await
1942                    .unwrap();
1943                assert_eq!(
1944                    row,
1945                    Some(OwnedRow::new(vec![Some(2_i32.into()), Some(5_i32.into())]))
1946                );
1947            }
1948            _ => unreachable!(),
1949        }
1950    }
1951
1952    #[tokio::test]
1953    async fn test_ignore_delete_then_insert() {
1954        // Prepare storage and memtable.
1955        let memory_state_store = MemoryStateStore::new();
1956        let table_id = TableId::new(1);
1957        // Two columns of int32 type, the first column is PK.
1958        let schema = Schema::new(vec![
1959            Field::unnamed(DataType::Int32),
1960            Field::unnamed(DataType::Int32),
1961        ]);
1962        let column_ids = vec![0.into(), 1.into()];
1963
1964        // test insert after delete one pk, the latter insert should succeed.
1965        let chunk1 = StreamChunk::from_pretty(
1966            " i i
1967            + 1 3
1968            - 1 3
1969            + 1 6",
1970        );
1971
1972        // Prepare stream executors.
1973        let source = MockSource::with_messages(vec![
1974            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1975            Message::Chunk(chunk1),
1976            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1977        ])
1978        .into_executor(schema.clone(), StreamKey::new());
1979
1980        let order_types = vec![OrderType::ascending()];
1981        let column_descs = vec![
1982            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
1983            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
1984        ];
1985
1986        let table = BatchTable::for_test(
1987            memory_state_store.clone(),
1988            table_id,
1989            column_descs,
1990            order_types,
1991            vec![0],
1992            vec![0, 1],
1993        );
1994
1995        let mut materialize_executor = MaterializeExecutor::for_test(
1996            source,
1997            memory_state_store,
1998            table_id,
1999            vec![ColumnOrder::new(0, OrderType::ascending())],
2000            column_ids,
2001            Arc::new(AtomicU64::new(0)),
2002            ConflictBehavior::IgnoreConflict,
2003        )
2004        .await
2005        .boxed()
2006        .execute();
2007        let _msg1 = materialize_executor
2008            .next()
2009            .await
2010            .transpose()
2011            .unwrap()
2012            .unwrap()
2013            .as_barrier()
2014            .unwrap();
2015        let _msg2 = materialize_executor
2016            .next()
2017            .await
2018            .transpose()
2019            .unwrap()
2020            .unwrap()
2021            .as_chunk()
2022            .unwrap();
2023        let _msg3 = materialize_executor
2024            .next()
2025            .await
2026            .transpose()
2027            .unwrap()
2028            .unwrap()
2029            .as_barrier()
2030            .unwrap();
2031
2032        let row = table
2033            .get_row(
2034                &OwnedRow::new(vec![Some(1_i32.into())]),
2035                HummockReadEpoch::NoWait(u64::MAX),
2036            )
2037            .await
2038            .unwrap();
2039        assert_eq!(
2040            row,
2041            Some(OwnedRow::new(vec![Some(1_i32.into()), Some(6_i32.into())]))
2042        );
2043    }
2044
2045    #[tokio::test]
2046    async fn test_ignore_delete_and_update_conflict() {
2047        // Prepare storage and memtable.
2048        let memory_state_store = MemoryStateStore::new();
2049        let table_id = TableId::new(1);
2050        // Two columns of int32 type, the first column is PK.
2051        let schema = Schema::new(vec![
2052            Field::unnamed(DataType::Int32),
2053            Field::unnamed(DataType::Int32),
2054        ]);
2055        let column_ids = vec![0.into(), 1.into()];
2056
2057        // test double insert one pk, the latter should be ignored.
2058        let chunk1 = StreamChunk::from_pretty(
2059            " i i
2060            + 1 4
2061            + 2 5
2062            + 3 6
2063            U- 8 1
2064            U+ 8 2
2065            + 8 3",
2066        );
2067
2068        // test delete wrong value, delete inexistent pk
2069        let chunk2 = StreamChunk::from_pretty(
2070            " i i
2071            + 7 8
2072            - 3 4
2073            - 5 0",
2074        );
2075
2076        // test delete wrong value, delete inexistent pk
2077        let chunk3 = StreamChunk::from_pretty(
2078            " i i
2079            + 1 5
2080            U- 2 4
2081            U+ 2 8
2082            U- 9 0
2083            U+ 9 1",
2084        );
2085
2086        // Prepare stream executors.
2087        let source = MockSource::with_messages(vec![
2088            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2089            Message::Chunk(chunk1),
2090            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2091            Message::Chunk(chunk2),
2092            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2093            Message::Chunk(chunk3),
2094            Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
2095        ])
2096        .into_executor(schema.clone(), StreamKey::new());
2097
2098        let order_types = vec![OrderType::ascending()];
2099        let column_descs = vec![
2100            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
2101            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
2102        ];
2103
2104        let table = BatchTable::for_test(
2105            memory_state_store.clone(),
2106            table_id,
2107            column_descs,
2108            order_types,
2109            vec![0],
2110            vec![0, 1],
2111        );
2112
2113        let mut materialize_executor = MaterializeExecutor::for_test(
2114            source,
2115            memory_state_store,
2116            table_id,
2117            vec![ColumnOrder::new(0, OrderType::ascending())],
2118            column_ids,
2119            Arc::new(AtomicU64::new(0)),
2120            ConflictBehavior::IgnoreConflict,
2121        )
2122        .await
2123        .boxed()
2124        .execute();
2125        materialize_executor.next().await.transpose().unwrap();
2126
2127        materialize_executor.next().await.transpose().unwrap();
2128
2129        // First stream chunk. We check the existence of (3) -> (3,6)
2130        match materialize_executor.next().await.transpose().unwrap() {
2131            Some(Message::Barrier(_)) => {
2132                // can read (8, 2), check insert after update
2133                let row = table
2134                    .get_row(
2135                        &OwnedRow::new(vec![Some(8_i32.into())]),
2136                        HummockReadEpoch::NoWait(u64::MAX),
2137                    )
2138                    .await
2139                    .unwrap();
2140                assert_eq!(
2141                    row,
2142                    Some(OwnedRow::new(vec![Some(8_i32.into()), Some(2_i32.into())]))
2143                );
2144            }
2145            _ => unreachable!(),
2146        }
2147        materialize_executor.next().await.transpose().unwrap();
2148
2149        match materialize_executor.next().await.transpose().unwrap() {
2150            Some(Message::Barrier(_)) => {
2151                let row = table
2152                    .get_row(
2153                        &OwnedRow::new(vec![Some(7_i32.into())]),
2154                        HummockReadEpoch::NoWait(u64::MAX),
2155                    )
2156                    .await
2157                    .unwrap();
2158                assert_eq!(
2159                    row,
2160                    Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
2161                );
2162
2163                // check delete wrong value
2164                let row = table
2165                    .get_row(
2166                        &OwnedRow::new(vec![Some(3_i32.into())]),
2167                        HummockReadEpoch::NoWait(u64::MAX),
2168                    )
2169                    .await
2170                    .unwrap();
2171                assert_eq!(row, None);
2172
2173                // check delete wrong pk
2174                let row = table
2175                    .get_row(
2176                        &OwnedRow::new(vec![Some(5_i32.into())]),
2177                        HummockReadEpoch::NoWait(u64::MAX),
2178                    )
2179                    .await
2180                    .unwrap();
2181                assert_eq!(row, None);
2182            }
2183            _ => unreachable!(),
2184        }
2185
2186        materialize_executor.next().await.transpose().unwrap();
2187        // materialize_executor.next().await.transpose().unwrap();
2188        // Second stream chunk. We check the existence of (7) -> (7,8)
2189        match materialize_executor.next().await.transpose().unwrap() {
2190            Some(Message::Barrier(_)) => {
2191                let row = table
2192                    .get_row(
2193                        &OwnedRow::new(vec![Some(1_i32.into())]),
2194                        HummockReadEpoch::NoWait(u64::MAX),
2195                    )
2196                    .await
2197                    .unwrap();
2198                assert_eq!(
2199                    row,
2200                    Some(OwnedRow::new(vec![Some(1_i32.into()), Some(4_i32.into())]))
2201                );
2202
2203                // check update wrong value
2204                let row = table
2205                    .get_row(
2206                        &OwnedRow::new(vec![Some(2_i32.into())]),
2207                        HummockReadEpoch::NoWait(u64::MAX),
2208                    )
2209                    .await
2210                    .unwrap();
2211                assert_eq!(
2212                    row,
2213                    Some(OwnedRow::new(vec![Some(2_i32.into()), Some(8_i32.into())]))
2214                );
2215
2216                // check update wrong pk, should become insert
2217                let row = table
2218                    .get_row(
2219                        &OwnedRow::new(vec![Some(9_i32.into())]),
2220                        HummockReadEpoch::NoWait(u64::MAX),
2221                    )
2222                    .await
2223                    .unwrap();
2224                assert_eq!(
2225                    row,
2226                    Some(OwnedRow::new(vec![Some(9_i32.into()), Some(1_i32.into())]))
2227                );
2228            }
2229            _ => unreachable!(),
2230        }
2231    }
2232
2233    #[tokio::test]
2234    async fn test_do_update_if_not_null_conflict() {
2235        // Prepare storage and memtable.
2236        let memory_state_store = MemoryStateStore::new();
2237        let table_id = TableId::new(1);
2238        // Two columns of int32 type, the first column is PK.
2239        let schema = Schema::new(vec![
2240            Field::unnamed(DataType::Int32),
2241            Field::unnamed(DataType::Int32),
2242        ]);
2243        let column_ids = vec![0.into(), 1.into()];
2244
2245        // should get (8, 2)
2246        let chunk1 = StreamChunk::from_pretty(
2247            " i i
2248            + 1 4
2249            + 2 .
2250            + 3 6
2251            U- 8 .
2252            U+ 8 2
2253            + 8 .",
2254        );
2255
2256        // should not get (3, x), should not get (5, 0)
2257        let chunk2 = StreamChunk::from_pretty(
2258            " i i
2259            + 7 8
2260            - 3 4
2261            - 5 0",
2262        );
2263
2264        // should get (2, None), (7, 8)
2265        let chunk3 = StreamChunk::from_pretty(
2266            " i i
2267            + 1 5
2268            + 7 .
2269            U- 2 4
2270            U+ 2 .
2271            U- 9 0
2272            U+ 9 1",
2273        );
2274
2275        // Prepare stream executors.
2276        let source = MockSource::with_messages(vec![
2277            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2278            Message::Chunk(chunk1),
2279            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2280            Message::Chunk(chunk2),
2281            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2282            Message::Chunk(chunk3),
2283            Message::Barrier(Barrier::new_test_barrier(test_epoch(4))),
2284        ])
2285        .into_executor(schema.clone(), StreamKey::new());
2286
2287        let order_types = vec![OrderType::ascending()];
2288        let column_descs = vec![
2289            ColumnDesc::unnamed(column_ids[0], DataType::Int32),
2290            ColumnDesc::unnamed(column_ids[1], DataType::Int32),
2291        ];
2292
2293        let table = BatchTable::for_test(
2294            memory_state_store.clone(),
2295            table_id,
2296            column_descs,
2297            order_types,
2298            vec![0],
2299            vec![0, 1],
2300        );
2301
2302        let mut materialize_executor = MaterializeExecutor::for_test(
2303            source,
2304            memory_state_store,
2305            table_id,
2306            vec![ColumnOrder::new(0, OrderType::ascending())],
2307            column_ids,
2308            Arc::new(AtomicU64::new(0)),
2309            ConflictBehavior::DoUpdateIfNotNull,
2310        )
2311        .await
2312        .boxed()
2313        .execute();
2314        materialize_executor.next().await.transpose().unwrap();
2315
2316        materialize_executor.next().await.transpose().unwrap();
2317
2318        // First stream chunk. We check the existence of (3) -> (3,6)
2319        match materialize_executor.next().await.transpose().unwrap() {
2320            Some(Message::Barrier(_)) => {
2321                let row = table
2322                    .get_row(
2323                        &OwnedRow::new(vec![Some(8_i32.into())]),
2324                        HummockReadEpoch::NoWait(u64::MAX),
2325                    )
2326                    .await
2327                    .unwrap();
2328                assert_eq!(
2329                    row,
2330                    Some(OwnedRow::new(vec![Some(8_i32.into()), Some(2_i32.into())]))
2331                );
2332
2333                let row = table
2334                    .get_row(
2335                        &OwnedRow::new(vec![Some(2_i32.into())]),
2336                        HummockReadEpoch::NoWait(u64::MAX),
2337                    )
2338                    .await
2339                    .unwrap();
2340                assert_eq!(row, Some(OwnedRow::new(vec![Some(2_i32.into()), None])));
2341            }
2342            _ => unreachable!(),
2343        }
2344        materialize_executor.next().await.transpose().unwrap();
2345
2346        match materialize_executor.next().await.transpose().unwrap() {
2347            Some(Message::Barrier(_)) => {
2348                let row = table
2349                    .get_row(
2350                        &OwnedRow::new(vec![Some(7_i32.into())]),
2351                        HummockReadEpoch::NoWait(u64::MAX),
2352                    )
2353                    .await
2354                    .unwrap();
2355                assert_eq!(
2356                    row,
2357                    Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
2358                );
2359
2360                // check delete wrong value
2361                let row = table
2362                    .get_row(
2363                        &OwnedRow::new(vec![Some(3_i32.into())]),
2364                        HummockReadEpoch::NoWait(u64::MAX),
2365                    )
2366                    .await
2367                    .unwrap();
2368                assert_eq!(row, None);
2369
2370                // check delete wrong pk
2371                let row = table
2372                    .get_row(
2373                        &OwnedRow::new(vec![Some(5_i32.into())]),
2374                        HummockReadEpoch::NoWait(u64::MAX),
2375                    )
2376                    .await
2377                    .unwrap();
2378                assert_eq!(row, None);
2379            }
2380            _ => unreachable!(),
2381        }
2382
2383        materialize_executor.next().await.transpose().unwrap();
2384        // materialize_executor.next().await.transpose().unwrap();
2385        // Second stream chunk. We check the existence of (7) -> (7,8)
2386        match materialize_executor.next().await.transpose().unwrap() {
2387            Some(Message::Barrier(_)) => {
2388                let row = table
2389                    .get_row(
2390                        &OwnedRow::new(vec![Some(7_i32.into())]),
2391                        HummockReadEpoch::NoWait(u64::MAX),
2392                    )
2393                    .await
2394                    .unwrap();
2395                assert_eq!(
2396                    row,
2397                    Some(OwnedRow::new(vec![Some(7_i32.into()), Some(8_i32.into())]))
2398                );
2399
2400                // check update wrong value
2401                let row = table
2402                    .get_row(
2403                        &OwnedRow::new(vec![Some(2_i32.into())]),
2404                        HummockReadEpoch::NoWait(u64::MAX),
2405                    )
2406                    .await
2407                    .unwrap();
2408                assert_eq!(row, Some(OwnedRow::new(vec![Some(2_i32.into()), None])));
2409
2410                // check update wrong pk, should become insert
2411                let row = table
2412                    .get_row(
2413                        &OwnedRow::new(vec![Some(9_i32.into())]),
2414                        HummockReadEpoch::NoWait(u64::MAX),
2415                    )
2416                    .await
2417                    .unwrap();
2418                assert_eq!(
2419                    row,
2420                    Some(OwnedRow::new(vec![Some(9_i32.into()), Some(1_i32.into())]))
2421                );
2422            }
2423            _ => unreachable!(),
2424        }
2425    }
2426
2427    fn gen_fuzz_data(row_number: usize, chunk_size: usize) -> Vec<StreamChunk> {
2428        const KN: u32 = 4;
2429        const SEED: u64 = 998244353;
2430        let mut ret = vec![];
2431        let mut builder =
2432            StreamChunkBuilder::new(chunk_size, vec![DataType::Int32, DataType::Int32]);
2433        let mut rng = SmallRng::seed_from_u64(SEED);
2434
2435        let random_vis = |c: StreamChunk, rng: &mut SmallRng| -> StreamChunk {
2436            let len = c.data_chunk().capacity();
2437            let mut c = StreamChunkMut::from(c);
2438            for i in 0..len {
2439                c.set_vis(i, rng.random_bool(0.5));
2440            }
2441            c.into()
2442        };
2443        for _ in 0..row_number {
2444            let k = (rng.next_u32() % KN) as i32;
2445            let v = rng.next_u32() as i32;
2446            let op = if rng.random_bool(0.5) {
2447                Op::Insert
2448            } else {
2449                Op::Delete
2450            };
2451            if let Some(c) =
2452                builder.append_row(op, OwnedRow::new(vec![Some(k.into()), Some(v.into())]))
2453            {
2454                ret.push(random_vis(c, &mut rng));
2455            }
2456        }
2457        if let Some(c) = builder.take() {
2458            ret.push(random_vis(c, &mut rng));
2459        }
2460        ret
2461    }
2462
2463    async fn fuzz_test_stream_consistent_inner(conflict_behavior: ConflictBehavior) {
2464        const N: usize = 100000;
2465
2466        // Prepare storage and memtable.
2467        let memory_state_store = MemoryStateStore::new();
2468        let table_id = TableId::new(1);
2469        // Two columns of int32 type, the first column is PK.
2470        let schema = Schema::new(vec![
2471            Field::unnamed(DataType::Int32),
2472            Field::unnamed(DataType::Int32),
2473        ]);
2474        let column_ids = vec![0.into(), 1.into()];
2475
2476        let chunks = gen_fuzz_data(N, 128);
2477        let messages = iter::once(Message::Barrier(Barrier::new_test_barrier(test_epoch(1))))
2478            .chain(chunks.into_iter().map(Message::Chunk))
2479            .chain(iter::once(Message::Barrier(Barrier::new_test_barrier(
2480                test_epoch(2),
2481            ))))
2482            .collect();
2483        // Prepare stream executors.
2484        let source =
2485            MockSource::with_messages(messages).into_executor(schema.clone(), StreamKey::new());
2486
2487        let mut materialize_executor = MaterializeExecutor::for_test(
2488            source,
2489            memory_state_store.clone(),
2490            table_id,
2491            vec![ColumnOrder::new(0, OrderType::ascending())],
2492            column_ids,
2493            Arc::new(AtomicU64::new(0)),
2494            conflict_behavior,
2495        )
2496        .await
2497        .boxed()
2498        .execute();
2499        materialize_executor.expect_barrier().await;
2500
2501        let order_types = vec![OrderType::ascending()];
2502        let column_descs = vec![
2503            ColumnDesc::unnamed(0.into(), DataType::Int32),
2504            ColumnDesc::unnamed(1.into(), DataType::Int32),
2505        ];
2506        let pk_indices = vec![0];
2507
2508        let mut table = StateTable::from_table_catalog(
2509            &crate::common::table::test_utils::gen_pbtable(
2510                TableId::from(1002),
2511                column_descs.clone(),
2512                order_types,
2513                pk_indices,
2514                0,
2515            ),
2516            memory_state_store.clone(),
2517            None,
2518        )
2519        .await;
2520
2521        while let Message::Chunk(c) = materialize_executor.next().await.unwrap().unwrap() {
2522            // check with state table's memtable
2523            table.write_chunk(c);
2524        }
2525    }
2526
2527    #[tokio::test]
2528    async fn fuzz_test_stream_consistent_upsert() {
2529        fuzz_test_stream_consistent_inner(ConflictBehavior::Overwrite).await
2530    }
2531
2532    #[tokio::test]
2533    async fn fuzz_test_stream_consistent_ignore() {
2534        fuzz_test_stream_consistent_inner(ConflictBehavior::IgnoreConflict).await
2535    }
2536
2537    /// A TTL-ed table `(k, ts, v)` with pk `k`, cleaned by `ts`, has `(25, 10, 100)` and gets
2538    /// `Watermark(ts = 20)`, then `chunk` in the same epoch or after a barrier. Returns the rows
2539    /// output for `chunk`.
2540    async fn ttl_output_after_watermark(
2541        conflict_behavior: ConflictBehavior,
2542        chunk: &str,
2543        barrier_between: bool,
2544        cold_cache: bool,
2545    ) -> Vec<(Op, OwnedRow)> {
2546        let schema = Schema::new(vec![
2547            Field::unnamed(DataType::Int32),
2548            Field::unnamed(DataType::Int32),
2549            Field::unnamed(DataType::Int32),
2550        ]);
2551        let mut messages = vec![
2552            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2553            Message::Chunk(StreamChunk::from_pretty(
2554                " i i i
2555                + 25 10 100",
2556            )),
2557            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2558            Message::Watermark(Watermark::new(1, DataType::Int32, ScalarImpl::Int32(20))),
2559        ];
2560        let mut last_epoch = 3;
2561        if barrier_between {
2562            messages.push(Message::Barrier(Barrier::new_test_barrier(test_epoch(3))));
2563            last_epoch = 4;
2564        }
2565        messages.push(Message::Chunk(StreamChunk::from_pretty(chunk)));
2566        messages.push(Message::Barrier(Barrier::new_test_barrier(test_epoch(
2567            last_epoch,
2568        ))));
2569        let source = MockSource::with_messages(messages).into_executor(schema, vec![0, 1]);
2570
2571        // The maximum sequence evicts the whole cache before every message.
2572        let cache_sequence = if cold_cache { u64::MAX } else { 0 };
2573        let mut materialize = MaterializeExecutor::for_test_with_ttl(
2574            source,
2575            MemoryStateStore::new(),
2576            TableId::new(1),
2577            vec![ColumnOrder::new(0, OrderType::ascending())],
2578            vec![0.into(), 1.into(), 2.into()],
2579            Arc::new(AtomicU64::new(cache_sequence)),
2580            conflict_behavior,
2581            1,
2582        )
2583        .await
2584        .boxed()
2585        .execute();
2586
2587        let mut watermark_seen = false;
2588        let mut rows = vec![];
2589        while let Some(msg) = materialize.next().await.transpose().unwrap() {
2590            match msg {
2591                Message::Watermark(_) => watermark_seen = true,
2592                Message::Chunk(chunk) if watermark_seen => {
2593                    rows.extend(chunk.rows().map(|(op, row)| (op, row.into_owned_row())))
2594                }
2595                Message::Barrier(barrier) if barrier.epoch.curr == test_epoch(last_epoch) => break,
2596                _ => {}
2597            }
2598        }
2599        assert!(watermark_seen);
2600        rows
2601    }
2602
2603    /// The rows below the forwarded watermark are expired, even before the commit.
2604    #[tokio::test]
2605    async fn test_ttl_expired_by_forwarded_watermark() {
2606        let new_row = OwnedRow::new(vec![
2607            Some(25i32.into()),
2608            Some(30i32.into()),
2609            Some(300i32.into()),
2610        ]);
2611        let cases = [
2612            // An upsert of the expired key is a new row.
2613            (
2614                ConflictBehavior::Overwrite,
2615                " i i i\n + 25 30 300",
2616                vec![(Op::Insert, new_row.clone())],
2617            ),
2618            // A delete of the expired key is ignored.
2619            (ConflictBehavior::Overwrite, " i i i\n - 25 . .", vec![]),
2620            // The expired row does not conflict with the new one.
2621            (
2622                ConflictBehavior::IgnoreConflict,
2623                " i i i\n + 25 30 300",
2624                vec![(Op::Insert, new_row)],
2625            ),
2626        ];
2627        for (conflict_behavior, chunk, expected) in cases {
2628            for barrier_between in [false, true] {
2629                for cold_cache in [false, true] {
2630                    assert_eq!(
2631                        ttl_output_after_watermark(
2632                            conflict_behavior,
2633                            chunk,
2634                            barrier_between,
2635                            cold_cache
2636                        )
2637                        .await,
2638                        expected,
2639                        "{conflict_behavior:?}, {chunk:?}, barrier_between: {barrier_between}, \
2640                         cold_cache: {cold_cache}"
2641                    );
2642                }
2643            }
2644        }
2645    }
2646}