Skip to main content

risingwave_stream/executor/
sink.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::{BTreeMap, HashMap, HashSet};
16use std::time::{Duration, Instant};
17use std::{assert_matches, mem};
18
19use anyhow::anyhow;
20use futures::stream::select;
21use futures::{FutureExt, TryFutureExt, TryStreamExt};
22use itertools::Itertools;
23use risingwave_common::array::Op;
24use risingwave_common::array::stream_chunk::StreamChunkMut;
25use risingwave_common::bitmap::Bitmap;
26use risingwave_common::catalog::{ColumnCatalog, Field};
27use risingwave_common::metrics::{GLOBAL_ERROR_METRICS, RelabeledAggregatedIntGauge};
28use risingwave_common::row::RowExt;
29use risingwave_common::util::retry::exponential_backoff;
30use risingwave_common_estimate_size::EstimateSize;
31use risingwave_common_estimate_size::collections::EstimatedVec;
32use risingwave_common_rate_limit::RateLimit;
33use risingwave_connector::dispatch_sink;
34use risingwave_connector::sink::catalog::{SinkId, SinkType};
35use risingwave_connector::sink::log_store::{
36    FlushCurrentEpochOptions, LogReader, LogReaderExt, LogReaderMetrics, LogStoreFactory,
37    LogWriter, LogWriterExt, LogWriterMetrics,
38};
39use risingwave_connector::sink::{
40    GLOBAL_SINK_METRICS, LogSinker, SINK_USER_FORCE_COMPACTION,
41    SINK_USER_PRESERVE_ROW_LEVEL_CHANGES, Sink, SinkImpl, SinkParam, SinkWriterParam,
42};
43use risingwave_pb::common::ThrottleType;
44use risingwave_pb::id::FragmentId;
45use risingwave_pb::stream_plan::stream_node::StreamKind;
46use thiserror_ext::AsReport;
47use tokio::select;
48use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
49use tokio::sync::oneshot;
50use tokio_retry::strategy::jitter;
51
52use crate::common::change_buffer::{OutputKind, output_kind};
53use crate::common::compact_chunk::{
54    InconsistencyBehavior, StreamChunkCompactor, compact_chunk_inline,
55};
56use crate::executor::prelude::*;
57pub struct SinkExecutor<F: LogStoreFactory> {
58    actor_context: ActorContextRef,
59    info: ExecutorInfo,
60    input: Executor,
61    sink: SinkImpl,
62    input_columns: Vec<ColumnCatalog>,
63    sink_param: SinkParam,
64    log_store_factory: F,
65    sink_writer_param: SinkWriterParam,
66    chunk_size: usize,
67    input_data_types: Vec<DataType>,
68    non_append_only_behavior: Option<NonAppendOnlyBehavior>,
69    rate_limit: Option<u32>,
70}
71
72const SINK_RETRY_BACKOFF_RESET_INTERVAL: Duration = Duration::from_secs(60);
73
74fn sink_retry_backoff() -> impl Iterator<Item = Duration> {
75    exponential_backoff(Duration::from_secs(1), 2, Duration::from_secs(30)).map(jitter)
76}
77
78// Drop all the DELETE messages in this chunk and convert UPDATE INSERT into INSERT.
79fn force_append_only(c: StreamChunk) -> StreamChunk {
80    let mut c: StreamChunkMut = c.into();
81    for (_, mut r) in c.to_rows_mut() {
82        match r.op() {
83            Op::Insert => {}
84            Op::Delete | Op::UpdateDelete => r.set_vis(false),
85            Op::UpdateInsert => r.set_op(Op::Insert),
86        }
87    }
88    c.into()
89}
90
91// Drop all the INSERT messages in this chunk and convert UPDATE DELETE into DELETE.
92fn force_delete_only(c: StreamChunk) -> StreamChunk {
93    let mut c: StreamChunkMut = c.into();
94    for (_, mut r) in c.to_rows_mut() {
95        match r.op() {
96            Op::Delete => {}
97            Op::Insert | Op::UpdateInsert => r.set_vis(false),
98            Op::UpdateDelete => r.set_op(Op::Delete),
99        }
100    }
101    c.into()
102}
103
104// Drop the DELETE messages whose downstream key is inserted again in the same barrier, since such
105// a key is updated rather than deleted from the downstream's perspective.
106fn drop_deletes_of_reinserted_keys(
107    delete_chunks: Vec<StreamChunk>,
108    insert_chunks: &[StreamChunk],
109    downstream_pk: &[usize],
110) -> Vec<StreamChunk> {
111    let inserted_keys: HashSet<_> = insert_chunks
112        .iter()
113        .flat_map(|c| c.rows())
114        .map(|(_, row)| row.project(downstream_pk))
115        .collect();
116    delete_chunks
117        .into_iter()
118        .map(|c| {
119            let mut c: StreamChunkMut = c.into();
120            for (row, mut r) in c.to_rows_mut() {
121                if inserted_keys.contains(&row.project(downstream_pk)) {
122                    r.set_vis(false);
123                }
124            }
125            c.into()
126        })
127        .collect()
128}
129
130/// When the sink is non-append-only, i.e. upsert or retract, we need to do some extra work for
131/// correctness and performance.
132#[derive(Clone, Copy, Debug)]
133struct NonAppendOnlyBehavior {
134    /// Whether the user specifies a primary key for the sink, and it matches the derived stream
135    /// key of the stream.
136    ///
137    /// By matching, we mean that stream key is a subset of the downstream pk.
138    pk_specified_and_matched: bool,
139    /// Whether the user forces buffering all chunks between two barriers.
140    force_compaction: bool,
141}
142
143impl NonAppendOnlyBehavior {
144    /// NOTE(kwannoel):
145    ///
146    /// After the optimization in <https://github.com/risingwavelabs/risingwave/pull/12250>,
147    /// `DELETE`s will be sequenced before `INSERT`s in JDBC sinks and PG rust sink.
148    /// There's a risk that adjacent chunks with `DELETE`s on the same PK will get
149    /// merged into a single chunk, since the logstore format doesn't preserve chunk
150    /// boundaries. Then we will have double `DELETE`s followed by unspecified sequence
151    /// of `INSERT`s, and lead to inconsistent data downstream.
152    ///
153    /// We only need to do the compaction for non-append-only sinks, when the upstream and
154    /// downstream PKs are matched. When the upstream and downstream PKs are not matched,
155    /// we will buffer the chunks between two barriers, so the compaction is not needed,
156    /// since the barriers will preserve chunk boundaries.
157    ///
158    /// When `force_compaction` is true, we also skip compaction here, since the buffering
159    /// will also make compaction.
160    ///
161    /// When the sink is an append-only sink, it is either `force_append_only` or
162    /// `append_only`, we should only append to downstream, so there should not be any
163    /// overlapping keys.
164    fn should_compact_in_log_reader(self) -> bool {
165        self.pk_specified_and_matched && !self.force_compaction
166    }
167
168    /// When stream key is different from the user defined primary key columns for sinks.
169    /// The operations could be out of order.
170    ///
171    /// For example, we have a stream with derived stream key `a, b` and user-specified sink
172    /// primary key `a`. Assume that we have `(1, 1)` in the table. Then, we perform two `UPDATE`
173    /// operations:
174    ///
175    /// ```text
176    /// UPDATE SET b = 2 WHERE a = 1 ... which issues:
177    ///   - (1, 1)
178    ///   + (1, 2)
179    ///
180    /// UPDATE SET b = 3 WHERE a = 1 ... which issues:
181    ///   - (1, 2)
182    ///   + (1, 3)
183    /// ```
184    ///
185    /// When these changes go into streaming pipeline, they could be shuffled to different parallelism
186    /// (actor), given that they are under different stream keys.
187    ///
188    /// ```text
189    /// Actor 1:
190    /// - (1, 1)
191    ///
192    /// Actor 2:
193    /// + (1, 2)
194    /// - (1, 2)
195    ///
196    /// Actor 3:
197    /// + (1, 3)
198    /// ```
199    ///
200    /// When these records are merged back into sink actor, we may get the records from different
201    /// parallelism in arbitrary order, like:
202    ///
203    /// ```text
204    /// + (1, 2) -- Actor 2, first row
205    /// + (1, 3) -- Actor 3
206    /// - (1, 1) -- Actor 1
207    /// - (1, 2) -- Actor 2, second row
208    /// ```
209    ///
210    /// Note that in terms of stream key (`a, b`), the operations in the order above are completely
211    /// correct, because we are operating on 3 different rows. However, in terms of user defined sink
212    /// primary key `a`, we're violating the unique constraint all the time.
213    ///
214    /// Therefore, in this case, we have to do additional reordering in the sink executor per barrier.
215    /// Specifically, we need to:
216    ///
217    /// First, compact all the changes with the stream key, so we have:
218    /// ```text
219    /// + (1, 3)
220    /// - (1, 1)
221    /// ```
222    ///
223    /// Then, sink all the delete events before sinking all insert events, so we have:
224    /// ```text
225    /// - (1, 1)
226    /// + (1, 3)
227    /// ```
228    /// Since we've compacted the chunk with the stream key, the `DELETE` records survived must be to
229    /// delete an existing row, so we can safely move them to the front. After the deletion is done,
230    /// we can then safely sink the insert events with uniqueness guarantee.
231    ///
232    /// When `force_compaction` is true, we also perform additional reordering to gain the
233    /// benefits of compaction:
234    /// - reduce the number of output messages;
235    /// - emit at most one update per key within a barrier interval, simplifying downstream logic.
236    fn should_reorder_records(self) -> bool {
237        !self.pk_specified_and_matched || self.force_compaction
238    }
239}
240
241/// Get the output kind for chunk compaction based on the given sink type.
242fn compact_output_kind(sink_type: SinkType) -> OutputKind {
243    match sink_type {
244        SinkType::Upsert => output_kind::UPSERT,
245        SinkType::Retract => output_kind::RETRACT,
246        // There won't be any `Update` or `Delete` in the chunk, so it doesn't matter.
247        SinkType::AppendOnly => output_kind::RETRACT,
248    }
249}
250
251/// Dispatch the code block to different output kinds for chunk compaction based on sink type.
252macro_rules! dispatch_output_kind {
253    ($sink_type:expr, $KIND:ident, $body:tt) => {
254        #[allow(unused_braces)]
255        match compact_output_kind($sink_type) {
256            output_kind::UPSERT => {
257                const KIND: OutputKind = output_kind::UPSERT;
258                $body
259            }
260            output_kind::RETRACT => {
261                const KIND: OutputKind = output_kind::RETRACT;
262                $body
263            }
264        }
265    };
266}
267
268impl<F: LogStoreFactory> SinkExecutor<F> {
269    #[expect(clippy::too_many_arguments)]
270    pub fn new(
271        actor_context: ActorContextRef,
272        info: ExecutorInfo,
273        input: Executor,
274        sink_writer_param: SinkWriterParam,
275        sink: SinkImpl,
276        sink_param: SinkParam,
277        columns: Vec<ColumnCatalog>,
278        log_store_factory: F,
279        chunk_size: usize,
280        input_data_types: Vec<DataType>,
281        rate_limit: Option<u32>,
282    ) -> StreamExecutorResult<Self> {
283        let sink_input_schema: Schema = columns
284            .iter()
285            .map(|column| Field::from(&column.column_desc))
286            .collect();
287
288        if let Some(col_dix) = sink_writer_param.extra_partition_col_idx {
289            // Remove the partition column from the schema.
290            assert_eq!(sink_input_schema.data_types(), {
291                let mut data_type = info.schema.data_types();
292                data_type.remove(col_dix);
293                data_type
294            });
295        } else {
296            assert_eq!(sink_input_schema.data_types(), info.schema.data_types());
297        }
298
299        let non_append_only_behavior = if !sink_param.sink_type.is_append_only() {
300            let stream_key = &info.stream_key;
301            let pk_specified_and_matched = (sink_param.downstream_pk.as_ref())
302                .is_some_and(|downstream_pk| stream_key.iter().all(|i| downstream_pk.contains(i)));
303            let force_compaction = sink_param
304                .properties
305                .get(SINK_USER_FORCE_COMPACTION)
306                .map(|v| v.eq_ignore_ascii_case("true"))
307                .unwrap_or(false);
308            Some(NonAppendOnlyBehavior {
309                pk_specified_and_matched,
310                force_compaction,
311            })
312        } else {
313            None
314        };
315
316        tracing::info!(
317            sink_id = %sink_param.sink_id,
318            actor_id = %actor_context.id,
319            ?non_append_only_behavior,
320            "Sink executor info"
321        );
322
323        Ok(Self {
324            actor_context,
325            info,
326            input,
327            sink,
328            input_columns: columns,
329            sink_param,
330            log_store_factory,
331            sink_writer_param,
332            chunk_size,
333            input_data_types,
334            non_append_only_behavior,
335            rate_limit,
336        })
337    }
338
339    fn execute_inner(self) -> BoxedMessageStream {
340        let sink_id = self.sink_param.sink_id;
341        let actor_id = self.actor_context.id;
342        let fragment_id = self.actor_context.fragment_id;
343
344        let stream_key = self.info.stream_key.clone();
345        let metrics = self.actor_context.streaming_metrics.new_sink_exec_metrics(
346            sink_id,
347            actor_id,
348            fragment_id,
349        );
350
351        let input_kind = self.input.stream_kind();
352        let input = self.input.execute();
353
354        let input = input.inspect_ok(move |msg| {
355            if let Message::Chunk(c) = msg {
356                metrics.sink_input_row_count.inc_by(c.capacity() as u64);
357                metrics.sink_input_bytes.inc_by(c.estimated_size() as u64);
358            }
359        });
360
361        let processed_input = Self::process_msg(
362            input,
363            self.sink_param.sink_type,
364            stream_key,
365            self.chunk_size,
366            self.input_data_types,
367            input_kind,
368            self.sink_param.downstream_pk.clone(),
369            self.non_append_only_behavior,
370            metrics.sink_chunk_buffer_size,
371            self.sink_param
372                .properties
373                .get(SINK_USER_PRESERVE_ROW_LEVEL_CHANGES)
374                .is_some_and(|v| v.eq_ignore_ascii_case("true")),
375            self.sink.is_blackhole(), // skip compact for blackhole for better benchmark results
376        );
377
378        let processed_input = if self.sink_param.ignore_delete {
379            // Drop UPDATE/DELETE messages if specified `ignore_delete` (formerly `force_append_only`).
380            processed_input
381                .map_ok(|msg| match msg {
382                    Message::Chunk(chunk) => Message::Chunk(force_append_only(chunk)),
383                    other => other,
384                })
385                .left_stream()
386        } else {
387            processed_input.right_stream()
388        };
389
390        if self.sink.is_sink_into_table() {
391            // TODO(hzxa21): support rate limit?
392            processed_input.boxed()
393        } else {
394            let labels = [
395                &actor_id.to_string(),
396                &sink_id.to_string(),
397                self.sink_param.sink_name.as_str(),
398            ];
399            let log_store_first_write_epoch = GLOBAL_SINK_METRICS
400                .log_store_first_write_epoch
401                .with_guarded_label_values(&labels);
402            let log_store_latest_write_epoch = GLOBAL_SINK_METRICS
403                .log_store_latest_write_epoch
404                .with_guarded_label_values(&labels);
405            let log_store_write_rows = GLOBAL_SINK_METRICS
406                .log_store_write_rows
407                .with_guarded_label_values(&labels);
408            let log_writer_metrics = LogWriterMetrics {
409                log_store_first_write_epoch,
410                log_store_latest_write_epoch,
411                log_store_write_rows,
412            };
413
414            let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
415            // Init the rate limit
416            rate_limit_tx.send(self.rate_limit.into()).unwrap();
417
418            let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
419
420            self.log_store_factory
421                .build()
422                .map(move |(log_reader, log_writer)| {
423                    let write_log_stream = Self::execute_write_log(
424                        processed_input,
425                        log_writer.monitored(log_writer_metrics),
426                        actor_id,
427                        fragment_id,
428                        sink_id,
429                        rate_limit_tx,
430                        rebuild_sink_tx,
431                    );
432
433                    let consume_log_stream_future = dispatch_sink!(self.sink, sink, {
434                        let consume_log_stream = Self::execute_consume_log(
435                            *sink,
436                            log_reader,
437                            self.input_columns,
438                            self.sink_param,
439                            self.sink_writer_param,
440                            self.non_append_only_behavior,
441                            self.actor_context,
442                            rate_limit_rx,
443                            rebuild_sink_rx,
444                        )
445                        .instrument_await(
446                            await_tree::span!("consume_log (sink_id {sink_id})").long_running(),
447                        )
448                        .map_ok(|never| never); // unify return type to `Message`
449
450                        consume_log_stream.boxed()
451                    });
452                    select(consume_log_stream_future.into_stream(), write_log_stream)
453                })
454                .into_stream()
455                .flatten()
456                .boxed()
457        }
458    }
459
460    #[try_stream(ok = Message, error = StreamExecutorError)]
461    async fn execute_write_log<W: LogWriter>(
462        input: impl MessageStream,
463        mut log_writer: W,
464        actor_id: ActorId,
465        fragment_id: FragmentId,
466        sink_id: SinkId,
467        rate_limit_tx: UnboundedSender<RateLimit>,
468        rebuild_sink_tx: UnboundedSender<RebuildSinkMessage>,
469    ) {
470        pin_mut!(input);
471        let barrier = expect_first_barrier(&mut input).await?;
472        let epoch_pair = barrier.epoch;
473        let is_pause_on_startup = barrier.is_pause_on_startup();
474        // Propagate the first barrier
475        yield Message::Barrier(barrier);
476
477        log_writer.init(epoch_pair, is_pause_on_startup).await?;
478
479        let mut is_paused = false;
480
481        #[for_await]
482        for msg in input {
483            match msg? {
484                Message::Watermark(w) => yield Message::Watermark(w),
485                Message::Chunk(chunk) => {
486                    assert!(
487                        !is_paused,
488                        "Actor {actor_id} should not receive any data after pause"
489                    );
490                    log_writer.write_chunk(chunk.clone()).await?;
491                    yield Message::Chunk(chunk);
492                }
493                Message::Barrier(barrier) => {
494                    let update_vnode_bitmap = barrier.as_update_vnode_bitmap(actor_id);
495                    let schema_change = barrier.as_sink_schema_change(sink_id);
496                    let wait_log_store_flush = barrier.should_flush_sink_log_store(sink_id);
497                    if let Some(schema_change) = &schema_change {
498                        info!(?schema_change, %sink_id, "sink receive schema change");
499                    }
500                    if wait_log_store_flush {
501                        info!(%sink_id, "sink wait for log store flush");
502                    }
503                    let post_flush = log_writer
504                        .flush_current_epoch(
505                            barrier.epoch.curr,
506                            FlushCurrentEpochOptions {
507                                is_checkpoint: barrier.kind.is_checkpoint(),
508                                new_vnode_bitmap: update_vnode_bitmap.clone(),
509                                is_stop: barrier.is_stop(actor_id),
510                                schema_change,
511                                wait_log_store_flush,
512                            },
513                        )
514                        .await?;
515
516                    let mutation = barrier.mutation.clone();
517                    yield Message::Barrier(barrier);
518                    if F::REBUILD_SINK_ON_UPDATE_VNODE_BITMAP
519                        && let Some(new_vnode_bitmap) = update_vnode_bitmap.clone()
520                    {
521                        let (tx, rx) = oneshot::channel();
522                        rebuild_sink_tx
523                            .send(RebuildSinkMessage::RebuildSink(new_vnode_bitmap, tx))
524                            .map_err(|_| {
525                                anyhow!("failed to send the rebuild-sink request to the reader")
526                            })?;
527                        rx.await
528                            .map_err(|_| anyhow!("failed to wait for sink rebuild to finish"))?;
529                    }
530                    post_flush.post_yield_barrier().await?;
531
532                    if let Some(mutation) = mutation.as_deref() {
533                        match mutation {
534                            Mutation::Pause => {
535                                log_writer.pause()?;
536                                is_paused = true;
537                            }
538                            Mutation::Resume => {
539                                log_writer.resume()?;
540                                is_paused = false;
541                            }
542                            Mutation::Throttle(fragment_to_apply) => {
543                                if let Some(entry) = fragment_to_apply.get(&fragment_id)
544                                    && entry.throttle_type() == ThrottleType::Sink
545                                {
546                                    tracing::info!(
547                                        rate_limit = entry.rate_limit,
548                                        "received sink rate limit on actor {actor_id}"
549                                    );
550                                    if let Err(e) = rate_limit_tx.send(entry.rate_limit.into()) {
551                                        error!(
552                                            error = %e.as_report(),
553                                            "failed to send the sink rate limit update"
554                                        );
555                                        return Err(StreamExecutorError::from(
556                                            e.to_report_string(),
557                                        ));
558                                    }
559                                }
560                            }
561                            Mutation::ConnectorPropsChange(config) => {
562                                if let Some(map) = config.get(&sink_id.as_raw_id())
563                                    && let Err(e) = rebuild_sink_tx
564                                        .send(RebuildSinkMessage::UpdateConfig(map.clone()))
565                                {
566                                    error!(
567                                        error = %e.as_report(),
568                                        "failed to send sink property updates"
569                                    );
570                                    return Err(StreamExecutorError::from(e.to_report_string()));
571                                }
572                            }
573                            _ => (),
574                        }
575                    }
576                }
577            }
578        }
579    }
580
581    #[expect(clippy::too_many_arguments)]
582    #[try_stream(ok = Message, error = StreamExecutorError)]
583    async fn process_msg(
584        input: impl MessageStream,
585        sink_type: SinkType,
586        stream_key: StreamKey,
587        chunk_size: usize,
588        input_data_types: Vec<DataType>,
589        input_kind: StreamKind,
590        downstream_pk: Option<Vec<usize>>,
591        non_append_only_behavior: Option<NonAppendOnlyBehavior>,
592        sink_chunk_buffer_size_metrics: RelabeledAggregatedIntGauge,
593        preserve_row_level_changes: bool,
594        skip_compact: bool,
595    ) {
596        // To reorder records, we need to buffer chunks of the entire epoch.
597        if let Some(b) = non_append_only_behavior
598            && b.should_reorder_records()
599        {
600            assert_matches!(sink_type, SinkType::Upsert | SinkType::Retract);
601
602            let mut chunk_buffer = EstimatedVec::new();
603            let mut watermark: Option<super::Watermark> = None;
604            #[for_await]
605            for msg in input {
606                match msg? {
607                    Message::Watermark(w) => watermark = Some(w),
608                    Message::Chunk(c) => {
609                        chunk_buffer.push(c);
610                        sink_chunk_buffer_size_metrics.set(chunk_buffer.estimated_size() as i64);
611                    }
612                    Message::Barrier(barrier) => {
613                        let chunks = mem::take(&mut chunk_buffer).into_inner();
614
615                        // 1. Compact the chunk based on the **stream key**, so that we have at most 2 rows for each
616                        //    stream key. Then, move all delete records to the front.
617                        //    With a downstream pk, keep the old row of each update: on this path an update may
618                        //    change the downstream pk, and the old one must still be deleted. Changes on the same
619                        //    downstream pk are folded below. Upsert input is excluded, as its old rows only
620                        //    guarantee the stream key columns, so the old downstream pk is unknown.
621                        let mut delete_chunks = vec![];
622                        let mut insert_chunks = vec![];
623
624                        let compactor =
625                            StreamChunkCompactor::new(stream_key.clone(), chunks, input_kind);
626                        let compacted =
627                            if downstream_pk.is_some() && input_kind != StreamKind::Upsert {
628                                compactor.into_compacted_chunks_inline::<{ output_kind::RETRACT }>(
629                                    InconsistencyBehavior::Panic,
630                                )
631                            } else {
632                                dispatch_output_kind!(sink_type, KIND, {
633                                    compactor.into_compacted_chunks_inline::<KIND>(
634                                        InconsistencyBehavior::Panic,
635                                    )
636                                })
637                            };
638                        for c in compacted {
639                            let chunk = force_delete_only(c.clone());
640                            if chunk.cardinality() > 0 {
641                                delete_chunks.push(chunk);
642                            }
643                            let chunk = force_append_only(c);
644                            if chunk.cardinality() > 0 {
645                                insert_chunks.push(chunk);
646                            }
647                        }
648                        // 2. If user specifies a primary key, compact the chunk based on the **downstream pk**
649                        //    to eliminate any unnecessary updates to external systems. This also rewrites the
650                        //    `DELETE` and `INSERT` operations on the same key into `UPDATE` operations, which
651                        //    usually have more efficient implementation.
652                        //    Skip this when the target table has special conflict semantics. In that case, the
653                        //    target table must observe every row-level change instead of a pre-compacted final state.
654                        if let Some(downstream_pk) = &downstream_pk
655                            && !preserve_row_level_changes
656                        {
657                            let chunks = delete_chunks
658                                .into_iter()
659                                .chain(insert_chunks.into_iter())
660                                .collect();
661                            // With a downstream pk, the first stage retains retract updates,
662                            // so this stage has the same input semantics as the original stream.
663                            let chunks = dispatch_output_kind!(sink_type, KIND, {
664                                StreamChunkCompactor::new(downstream_pk.clone(), chunks, input_kind)
665                                    .into_compacted_chunks_reconstructed::<KIND>(
666                                        chunk_size,
667                                        input_data_types.clone(),
668                                        // When compacting based on user provided primary key, we should never panic
669                                        // on inconsistency in case the user provided primary key is not unique.
670                                        InconsistencyBehavior::Warn,
671                                    )
672                            });
673                            for c in chunks {
674                                yield Message::Chunk(c);
675                            }
676                        } else {
677                            if let Some(downstream_pk) = &downstream_pk
678                                && compact_output_kind(sink_type) == output_kind::UPSERT
679                            {
680                                delete_chunks = drop_deletes_of_reinserted_keys(
681                                    delete_chunks,
682                                    &insert_chunks,
683                                    downstream_pk,
684                                );
685                            }
686                            let mut chunk_builder =
687                                StreamChunkBuilder::new(chunk_size, input_data_types.clone());
688                            for chunk in delete_chunks.into_iter().chain(insert_chunks) {
689                                for (op, row) in chunk.rows() {
690                                    if let Some(c) = chunk_builder.append_row(op, row) {
691                                        yield Message::Chunk(c);
692                                    }
693                                }
694                            }
695
696                            if let Some(c) = chunk_builder.take() {
697                                yield Message::Chunk(c);
698                            }
699                        };
700
701                        // 3. Forward watermark and barrier.
702                        if let Some(w) = mem::take(&mut watermark) {
703                            yield Message::Watermark(w)
704                        }
705                        yield Message::Barrier(barrier);
706                    }
707                }
708            }
709        } else {
710            // In this branch, we don't need to reorder records, either because the stream key matches
711            // the downstream pk, or the sink is append-only.
712            #[for_await]
713            for msg in input {
714                match msg? {
715                    Message::Watermark(w) => yield Message::Watermark(w),
716                    Message::Chunk(mut chunk) => {
717                        // Compact the chunk to eliminate any unnecessary updates to external systems.
718                        // This should be performed against the downstream pk, not the stream key, to
719                        // ensure correct retract/upsert semantics from the downstream's perspective.
720                        if !sink_type.is_append_only()
721                            && let Some(downstream_pk) = &downstream_pk
722                        {
723                            if preserve_row_level_changes {
724                                // Preserve every row-level change so the target table can apply its
725                                // own conflict semantics.
726                            } else if skip_compact {
727                                // We can only skip compaction if the keys are exactly the same, not just
728                                // matching by being a subset.
729                                assert_eq!(&stream_key, downstream_pk);
730                            } else {
731                                chunk = dispatch_output_kind!(sink_type, KIND, {
732                                    compact_chunk_inline::<KIND>(
733                                        chunk,
734                                        downstream_pk,
735                                        input_kind,
736                                        // When compacting based on user provided primary key, we should never panic
737                                        // on inconsistency in case the user provided primary key is not unique.
738                                        InconsistencyBehavior::Warn,
739                                    )
740                                });
741                            }
742                        }
743                        yield Message::Chunk(chunk);
744                    }
745                    Message::Barrier(barrier) => {
746                        yield Message::Barrier(barrier);
747                    }
748                }
749            }
750        }
751    }
752
753    #[expect(clippy::too_many_arguments)]
754    async fn execute_consume_log<S: Sink, R: LogReader>(
755        mut sink: S,
756        log_reader: R,
757        columns: Vec<ColumnCatalog>,
758        mut sink_param: SinkParam,
759        mut sink_writer_param: SinkWriterParam,
760        non_append_only_behavior: Option<NonAppendOnlyBehavior>,
761        actor_context: ActorContextRef,
762        rate_limit_rx: UnboundedReceiver<RateLimit>,
763        mut rebuild_sink_rx: UnboundedReceiver<RebuildSinkMessage>,
764    ) -> StreamExecutorResult<!> {
765        let mut visible_columns = columns
766            .iter()
767            .enumerate()
768            .filter_map(|(idx, column)| (!column.is_hidden).then_some(idx))
769            .collect_vec();
770
771        let needs_projection = visible_columns.len() != columns.len();
772
773        if needs_projection
774            && let Some(extra_partition_col_idx) = sink_writer_param.extra_partition_col_idx
775        {
776            // The extra partition column is appended after all normal sink columns.
777            debug_assert_eq!(extra_partition_col_idx, columns.len());
778            sink_writer_param.extra_partition_col_idx = Some(visible_columns.len());
779            visible_columns.push(extra_partition_col_idx);
780        }
781
782        let labels = [
783            &actor_context.id.to_string(),
784            sink_writer_param.connector.as_str(),
785            &sink_writer_param.sink_id.to_string(),
786            sink_writer_param.sink_name.as_str(),
787        ];
788        let log_store_reader_wait_new_future_duration_ns = GLOBAL_SINK_METRICS
789            .log_store_reader_wait_new_future_duration_ns
790            .with_guarded_label_values(&labels);
791        let log_store_read_rows = GLOBAL_SINK_METRICS
792            .log_store_read_rows
793            .with_guarded_label_values(&labels);
794        let log_store_read_bytes = GLOBAL_SINK_METRICS
795            .log_store_read_bytes
796            .with_guarded_label_values(&labels);
797        let log_store_latest_read_epoch = GLOBAL_SINK_METRICS
798            .log_store_latest_read_epoch
799            .with_guarded_label_values(&labels);
800        let metrics = LogReaderMetrics {
801            log_store_latest_read_epoch,
802            log_store_read_rows,
803            log_store_read_bytes,
804            log_store_reader_wait_new_future_duration_ns,
805        };
806
807        let downstream_pk = sink_param.downstream_pk.clone();
808
809        let mut log_reader = log_reader
810            .transform_chunk(move |chunk| {
811                let chunk = if let Some(b) = non_append_only_behavior
812                    && b.should_compact_in_log_reader()
813                {
814                    // This guarantees that user has specified a `downstream_pk`.
815                    let downstream_pk = downstream_pk.as_ref().unwrap();
816                    dispatch_output_kind!(sink_param.sink_type, KIND, {
817                        compact_chunk_inline::<KIND>(
818                            chunk,
819                            downstream_pk,
820                            // Writer-side compaction has already converted updates for upsert sinks.
821                            if sink_param.sink_type == SinkType::Upsert {
822                                StreamKind::Upsert
823                            } else {
824                                StreamKind::Retract
825                            },
826                            // When compacting based on user provided primary key, we should never panic
827                            // on inconsistency in case the user provided primary key is not unique.
828                            InconsistencyBehavior::Warn,
829                        )
830                    })
831                } else {
832                    chunk
833                };
834                if needs_projection {
835                    // Do projection here because we may have columns that aren't visible to
836                    // the downstream.
837                    chunk.project(&visible_columns)
838                } else {
839                    chunk
840                }
841            })
842            .monitored(metrics)
843            .rate_limited(rate_limit_rx);
844
845        log_reader.init().await?;
846        let mut retry_backoff = sink_retry_backoff();
847        loop {
848            // Boxed so that `drop` below releases its borrows on `log_reader` and `sink`.
849            let mut future = Box::pin(async {
850                loop {
851                    let attempt_started_at = Instant::now();
852                    let Err(e) = sink
853                        .new_log_sinker(sink_writer_param.clone())
854                        .and_then(|log_sinker| log_sinker.consume_log_and_sink(&mut log_reader))
855                        .await;
856                    let attempt_duration = attempt_started_at.elapsed();
857                    GLOBAL_ERROR_METRICS.user_sink_error.report([
858                        "sink_executor_error".to_owned(),
859                        sink_param.sink_id.to_string(),
860                        sink_param.sink_name.clone(),
861                        actor_context.fragment_id.to_string(),
862                    ]);
863
864                    if let Some(meta_client) = sink_writer_param.meta_client.as_ref() {
865                        meta_client
866                            .add_sink_fail_evet_log(
867                                sink_writer_param.sink_id,
868                                sink_writer_param.sink_name.clone(),
869                                sink_writer_param.connector.clone(),
870                                e.to_report_string(),
871                            )
872                            .await;
873                    }
874
875                    if F::ALLOW_REWIND {
876                        match log_reader.rewind().await {
877                            Ok(()) => {
878                                if attempt_duration >= SINK_RETRY_BACKOFF_RESET_INTERVAL {
879                                    retry_backoff = sink_retry_backoff();
880                                }
881                                let retry_delay =
882                                    retry_backoff.next().expect("retry strategy is infinite");
883                                error!(
884                                    error = %e.as_report(),
885                                    executor_id = %sink_writer_param.executor_id,
886                                    sink_id = %sink_param.sink_id,
887                                    ?retry_delay,
888                                    "reset log reader stream successfully after sink error; retrying after backoff"
889                                );
890                                tokio::time::sleep(retry_delay).await;
891                                Ok(())
892                            }
893                            Err(rewind_err) => {
894                                error!(
895                                    error = %rewind_err.as_report(),
896                                    "failed to rewind the log reader"
897                                );
898                                Err(e)
899                            }
900                        }
901                    } else {
902                        Err(e)
903                    }
904                    .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
905                }
906            });
907            let message = loop {
908                select! {
909                    result = &mut future => {
910                        let Err(e): StreamExecutorResult<!> = result;
911                        return Err(e);
912                    }
913                    result = rebuild_sink_rx.recv() => {
914                        let message = result.ok_or_else(|| anyhow!("failed to receive rebuild sink notify"))?;
915                        // Dropping the consumer costs a rewind, or a recovery when the log
916                        // reader cannot rewind. Not worth it for an update that changes nothing.
917                        if let RebuildSinkMessage::UpdateConfig(config) = &message
918                            && !sink_config_has_changes(&sink_param.properties, config)
919                        {
920                            info!(
921                                executor_id = %sink_writer_param.executor_id,
922                                sink_id = %sink_param.sink_id,
923                                "skip alter sink config because properties are unchanged"
924                            );
925                            continue;
926                        }
927                        break message;
928                    }
929                }
930            };
931            drop(future);
932            match message {
933                RebuildSinkMessage::RebuildSink(new_vnode, notify) => {
934                    sink_writer_param.vnode_bitmap = (*new_vnode).clone();
935                    if notify.send(()).is_err() {
936                        warn!("failed to notify rebuild sink");
937                    }
938                    log_reader.init().await?;
939                }
940                RebuildSinkMessage::UpdateConfig(config) => {
941                    if F::ALLOW_REWIND {
942                        match log_reader.rewind().await {
943                            Ok(()) => {
944                                sink_param.properties.extend(config);
945                                sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
946                                info!(
947                                    executor_id = %sink_writer_param.executor_id,
948                                    sink_id = %sink_param.sink_id,
949                                    "alter sink config successfully with rewind"
950                                );
951                                Ok(())
952                            }
953                            Err(rewind_err) => {
954                                error!(
955                                    error = %rewind_err.as_report(),
956                                    "failed to rewind the log reader for ALTER SINK CONFIG"
957                                );
958                                Err(anyhow!("failed to rewind the log after ALTER SINK CONFIG").into())
959                            }
960                        }
961                    } else {
962                        sink_param.properties.extend(config);
963                        sink = TryFrom::try_from(sink_param.clone()).map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
964                        Err(anyhow!("This is not an actual error condition. The system is intentionally triggering recovery procedures to ensure ALTER SINK CONFIG are fully applied.").into())
965                    }
966                    .map_err(|e| StreamExecutorError::from((e, sink_param.sink_id)))?;
967                }
968            }
969        }
970    }
971}
972
973enum RebuildSinkMessage {
974    RebuildSink(Arc<Bitmap>, oneshot::Sender<()>),
975    UpdateConfig(HashMap<String, String>),
976}
977
978fn sink_config_has_changes(
979    current: &BTreeMap<String, String>,
980    incoming: &HashMap<String, String>,
981) -> bool {
982    incoming
983        .iter()
984        .any(|(key, value)| current.get(key) != Some(value))
985}
986
987impl<F: LogStoreFactory> Execute for SinkExecutor<F> {
988    fn execute(self: Box<Self>) -> BoxedMessageStream {
989        self.execute_inner()
990    }
991}
992
993#[cfg(test)]
994mod test {
995    use std::future::pending;
996    use std::sync::Arc;
997    use std::sync::atomic::{AtomicUsize, Ordering};
998    use std::time::Duration;
999
1000    use risingwave_common::catalog::{ColumnDesc, ColumnId};
1001    use risingwave_common::util::epoch::test_epoch;
1002    use risingwave_connector::sink::build_sink;
1003    use risingwave_connector::sink::log_store::{LogStoreReadItem, LogStoreResult, TruncateOffset};
1004    use risingwave_connector::sink::trivial::BlackHoleSink;
1005    use tokio::sync::Notify;
1006
1007    use super::*;
1008    use crate::common::log_store_impl::in_mem::{
1009        BoundedInMemLogStoreFactory, BoundedInMemLogStoreWriter,
1010    };
1011    use crate::executor::test_utils::*;
1012
1013    #[derive(Default)]
1014    struct RewindRequiredLogReaderState {
1015        start_count: AtomicUsize,
1016        rewind_count: AtomicUsize,
1017        started: Notify,
1018    }
1019
1020    impl RewindRequiredLogReaderState {
1021        async fn wait_for_start_count(&self, expected: usize) {
1022            tokio::time::timeout(Duration::from_secs(5), async {
1023                loop {
1024                    let started = self.started.notified();
1025                    if self.start_count.load(Ordering::SeqCst) >= expected {
1026                        break;
1027                    }
1028                    started.await;
1029                }
1030            })
1031            .await
1032            .unwrap_or_else(|_| panic!("log reader was not started {expected} times"));
1033        }
1034    }
1035
1036    struct RewindRequiredLogReader {
1037        is_reset: bool,
1038        state: Arc<RewindRequiredLogReaderState>,
1039    }
1040
1041    impl LogReader for RewindRequiredLogReader {
1042        async fn init(&mut self) -> LogStoreResult<()> {
1043            self.is_reset = true;
1044            Ok(())
1045        }
1046
1047        async fn start_from(&mut self, _start_offset: Option<u64>) -> LogStoreResult<()> {
1048            assert!(
1049                self.is_reset,
1050                "log reader must be rewound before restarting"
1051            );
1052            self.is_reset = false;
1053            self.state.start_count.fetch_add(1, Ordering::SeqCst);
1054            self.state.started.notify_waiters();
1055            Ok(())
1056        }
1057
1058        async fn next_item(&mut self) -> LogStoreResult<(u64, LogStoreReadItem)> {
1059            pending().await
1060        }
1061
1062        fn truncate(&mut self, _offset: TruncateOffset) -> LogStoreResult<()> {
1063            Ok(())
1064        }
1065
1066        async fn rewind(&mut self) -> LogStoreResult<()> {
1067            self.is_reset = true;
1068            self.state.rewind_count.fetch_add(1, Ordering::SeqCst);
1069            Ok(())
1070        }
1071    }
1072
1073    struct TestLogStoreFactory<const CAN_REWIND: bool>;
1074
1075    impl<const CAN_REWIND: bool> LogStoreFactory for TestLogStoreFactory<CAN_REWIND> {
1076        type Reader = RewindRequiredLogReader;
1077        type Writer = BoundedInMemLogStoreWriter;
1078
1079        const ALLOW_REWIND: bool = CAN_REWIND;
1080        const REBUILD_SINK_ON_UPDATE_VNODE_BITMAP: bool = false;
1081
1082        async fn build(self) -> (Self::Reader, Self::Writer) {
1083            unreachable!()
1084        }
1085    }
1086
1087    fn sink_param_for_config_update_test() -> SinkParam {
1088        SinkParam {
1089            sink_id: 0.into(),
1090            sink_name: "test".into(),
1091            properties: BTreeMap::from([
1092                ("connector".to_owned(), "blackhole".to_owned()),
1093                ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1094            ]),
1095            columns: vec![],
1096            downstream_pk: None,
1097            sink_type: SinkType::AppendOnly,
1098            ignore_delete: false,
1099            format_desc: None,
1100            db_name: "test".into(),
1101            sink_from_name: "test".into(),
1102        }
1103    }
1104
1105    #[test]
1106    fn test_sink_config_has_changes() {
1107        let current = BTreeMap::from([
1108            ("connector".to_owned(), "blackhole".to_owned()),
1109            ("commit_checkpoint_interval".to_owned(), "1".to_owned()),
1110        ]);
1111
1112        assert!(!sink_config_has_changes(
1113            &current,
1114            &HashMap::from([("commit_checkpoint_interval".to_owned(), "1".to_owned())])
1115        ));
1116        assert!(sink_config_has_changes(
1117            &current,
1118            &HashMap::from([("commit_checkpoint_interval".to_owned(), "2".to_owned())])
1119        ));
1120        assert!(sink_config_has_changes(
1121            &current,
1122            &HashMap::from([("force_append_only".to_owned(), "true".to_owned())])
1123        ));
1124    }
1125
1126    fn no_op_config_update() -> RebuildSinkMessage {
1127        RebuildSinkMessage::UpdateConfig(HashMap::from([(
1128            "commit_checkpoint_interval".to_owned(),
1129            "1".to_owned(),
1130        )]))
1131    }
1132
1133    fn changed_config_update() -> RebuildSinkMessage {
1134        RebuildSinkMessage::UpdateConfig(HashMap::from([(
1135            "commit_checkpoint_interval".to_owned(),
1136            "2".to_owned(),
1137        )]))
1138    }
1139
1140    async fn assert_no_op_config_update_keeps_consuming<const CAN_REWIND: bool>() {
1141        let sink_param = sink_param_for_config_update_test();
1142        let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1143        let state = Arc::new(RewindRequiredLogReaderState::default());
1144        let log_reader = RewindRequiredLogReader {
1145            is_reset: false,
1146            state: state.clone(),
1147        };
1148        let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1149        let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1150
1151        let consume_log = SinkExecutor::<TestLogStoreFactory<CAN_REWIND>>::execute_consume_log(
1152            sink,
1153            log_reader,
1154            vec![],
1155            sink_param,
1156            SinkWriterParam::for_test(),
1157            None,
1158            ActorContext::for_test(0),
1159            rate_limit_rx,
1160            rebuild_sink_rx,
1161        );
1162        tokio::pin!(consume_log);
1163
1164        tokio::select! {
1165            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1166            () = state.wait_for_start_count(1) => {},
1167        }
1168
1169        rebuild_sink_tx.send(no_op_config_update()).unwrap();
1170
1171        // Messages are handled in order, so an acknowledged rebuild proves the consumer survived
1172        // the no-op update.
1173        let (notify_tx, notify_rx) = oneshot::channel();
1174        rebuild_sink_tx
1175            .send(RebuildSinkMessage::RebuildSink(
1176                Arc::new(Bitmap::ones(1)),
1177                notify_tx,
1178            ))
1179            .unwrap();
1180        tokio::select! {
1181            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1182            result = notify_rx => result.expect("rebuild sink should be acknowledged"),
1183        }
1184
1185        assert_eq!(state.rewind_count.load(Ordering::SeqCst), 0);
1186        drop(rate_limit_tx);
1187    }
1188
1189    #[tokio::test]
1190    async fn test_no_op_sink_config_update_keeps_consuming_with_rewind() {
1191        assert_no_op_config_update_keeps_consuming::<true>().await;
1192    }
1193
1194    #[tokio::test]
1195    async fn test_no_op_sink_config_update_keeps_consuming_without_rewind() {
1196        assert_no_op_config_update_keeps_consuming::<false>().await;
1197    }
1198
1199    #[tokio::test]
1200    async fn test_sink_config_update_rewinds_log_reader() {
1201        let sink_param = sink_param_for_config_update_test();
1202        let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1203        let state = Arc::new(RewindRequiredLogReaderState::default());
1204        let log_reader = RewindRequiredLogReader {
1205            is_reset: false,
1206            state: state.clone(),
1207        };
1208        let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1209        let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1210
1211        let consume_log = SinkExecutor::<TestLogStoreFactory<true>>::execute_consume_log(
1212            sink,
1213            log_reader,
1214            vec![],
1215            sink_param,
1216            SinkWriterParam::for_test(),
1217            None,
1218            ActorContext::for_test(0),
1219            rate_limit_rx,
1220            rebuild_sink_rx,
1221        );
1222        tokio::pin!(consume_log);
1223
1224        tokio::select! {
1225            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1226            () = state.wait_for_start_count(1) => {},
1227        }
1228
1229        rebuild_sink_tx.send(changed_config_update()).unwrap();
1230
1231        tokio::select! {
1232            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1233            () = state.wait_for_start_count(2) => {},
1234        }
1235
1236        assert_eq!(state.rewind_count.load(Ordering::SeqCst), 1);
1237        drop(rate_limit_tx);
1238    }
1239
1240    #[tokio::test]
1241    async fn test_sink_config_update_triggers_recovery_without_rewind() {
1242        let sink_param = sink_param_for_config_update_test();
1243        let sink = BlackHoleSink::try_from(sink_param.clone()).unwrap();
1244        let state = Arc::new(RewindRequiredLogReaderState::default());
1245        let log_reader = RewindRequiredLogReader {
1246            is_reset: false,
1247            state: state.clone(),
1248        };
1249        let (rate_limit_tx, rate_limit_rx) = unbounded_channel();
1250        let (rebuild_sink_tx, rebuild_sink_rx) = unbounded_channel();
1251
1252        let consume_log = SinkExecutor::<TestLogStoreFactory<false>>::execute_consume_log(
1253            sink,
1254            log_reader,
1255            vec![],
1256            sink_param,
1257            SinkWriterParam::for_test(),
1258            None,
1259            ActorContext::for_test(0),
1260            rate_limit_rx,
1261            rebuild_sink_rx,
1262        );
1263        tokio::pin!(consume_log);
1264
1265        tokio::select! {
1266            result = &mut consume_log => panic!("log consumer exited unexpectedly: {result:?}"),
1267            () = state.wait_for_start_count(1) => {},
1268        }
1269
1270        rebuild_sink_tx.send(changed_config_update()).unwrap();
1271
1272        tokio::time::timeout(Duration::from_secs(5), consume_log)
1273            .await
1274            .expect("log consumer should trigger recovery")
1275            .expect_err("log consumer should return an error");
1276        assert_eq!(state.start_count.load(Ordering::SeqCst), 1);
1277        assert_eq!(state.rewind_count.load(Ordering::SeqCst), 0);
1278        drop(rate_limit_tx);
1279    }
1280
1281    #[test]
1282    fn test_sink_retry_backoff_is_bounded() {
1283        let mut retry_backoff = sink_retry_backoff();
1284        for max_delay in [1, 2, 4, 8, 16, 30, 30].map(Duration::from_secs) {
1285            assert!(retry_backoff.next().expect("retry strategy is infinite") <= max_delay);
1286        }
1287    }
1288
1289    #[tokio::test]
1290    async fn test_force_append_only_sink() {
1291        use risingwave_common::array::StreamChunkTestExt;
1292        use risingwave_common::array::stream_chunk::StreamChunk;
1293        use risingwave_common::types::DataType;
1294
1295        use crate::executor::Barrier;
1296
1297        let properties = maplit::btreemap! {
1298            "connector".into() => "blackhole".into(),
1299            "type".into() => "append-only".into(),
1300            "force_append_only".into() => "true".into()
1301        };
1302
1303        // We have two visible columns and one hidden column. The hidden column will be pruned out
1304        // within the sink executor.
1305        let columns = vec![
1306            ColumnCatalog {
1307                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1308                is_hidden: false,
1309            },
1310            ColumnCatalog {
1311                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1312                is_hidden: false,
1313            },
1314            ColumnCatalog {
1315                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1316                is_hidden: true,
1317            },
1318        ];
1319        let schema: Schema = columns
1320            .iter()
1321            .map(|column| Field::from(column.column_desc.clone()))
1322            .collect();
1323        let stream_key = vec![0];
1324
1325        let source = MockSource::with_messages(vec![
1326            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1327            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1328                " I I I
1329                    + 3 2 1",
1330            ))),
1331            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1332            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1333                "  I I I
1334                    U- 3 2 1
1335                    U+ 3 4 1
1336                     + 5 6 7",
1337            ))),
1338            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1339                " I I I
1340                    - 5 6 7",
1341            ))),
1342        ])
1343        .into_executor(schema.clone(), stream_key.clone());
1344
1345        let sink_param = SinkParam {
1346            sink_id: 0.into(),
1347            sink_name: "test".into(),
1348            properties,
1349
1350            columns: columns
1351                .iter()
1352                .filter(|col| !col.is_hidden)
1353                .map(|col| col.column_desc.clone())
1354                .collect(),
1355            downstream_pk: Some(stream_key.clone()),
1356            sink_type: SinkType::AppendOnly,
1357            ignore_delete: true,
1358            format_desc: None,
1359            db_name: "test".into(),
1360            sink_from_name: "test".into(),
1361        };
1362
1363        let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1364
1365        let sink = build_sink(sink_param.clone()).unwrap();
1366
1367        let sink_executor = SinkExecutor::new(
1368            ActorContext::for_test(0),
1369            info,
1370            source,
1371            SinkWriterParam::for_test(),
1372            sink,
1373            sink_param,
1374            columns.clone(),
1375            BoundedInMemLogStoreFactory::for_test(1),
1376            1024,
1377            vec![DataType::Int32, DataType::Int32, DataType::Int32],
1378            None,
1379        )
1380        .unwrap();
1381
1382        let mut executor = sink_executor.boxed().execute();
1383
1384        // Barrier message.
1385        executor.next().await.unwrap().unwrap();
1386
1387        let chunk_msg = executor.next().await.unwrap().unwrap();
1388        assert_eq!(
1389            chunk_msg.into_chunk().unwrap().compact_vis(),
1390            StreamChunk::from_pretty(
1391                " I I I
1392                + 3 2 1",
1393            )
1394        );
1395
1396        // Barrier message.
1397        executor.next().await.unwrap().unwrap();
1398
1399        let chunk_msg = executor.next().await.unwrap().unwrap();
1400        assert_eq!(
1401            chunk_msg.into_chunk().unwrap().compact_vis(),
1402            StreamChunk::from_pretty(
1403                " I I I
1404                + 3 4 1
1405                + 5 6 7",
1406            )
1407        );
1408
1409        // Should not receive the third stream chunk message because the force-append-only sink
1410        // executor will drop all DELETE messages.
1411
1412        // The last barrier message.
1413        executor.next().await.unwrap().unwrap();
1414    }
1415
1416    #[tokio::test]
1417    async fn stream_key_sink_pk_mismatch_upsert() {
1418        stream_key_sink_pk_mismatch(SinkType::Upsert).await;
1419    }
1420
1421    #[tokio::test]
1422    async fn stream_key_sink_pk_mismatch_retract() {
1423        stream_key_sink_pk_mismatch(SinkType::Retract).await;
1424    }
1425
1426    async fn stream_key_sink_pk_mismatch(sink_type: SinkType) {
1427        use risingwave_common::array::StreamChunkTestExt;
1428        use risingwave_common::array::stream_chunk::StreamChunk;
1429        use risingwave_common::types::DataType;
1430
1431        use crate::executor::Barrier;
1432
1433        let properties = maplit::btreemap! {
1434            "connector".into() => "blackhole".into(),
1435        };
1436
1437        // We have two visible columns and one hidden column. The hidden column will be pruned out
1438        // within the sink executor.
1439        let columns = vec![
1440            ColumnCatalog {
1441                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1442                is_hidden: false,
1443            },
1444            ColumnCatalog {
1445                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1446                is_hidden: false,
1447            },
1448            ColumnCatalog {
1449                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1450                is_hidden: true,
1451            },
1452        ];
1453        let schema: Schema = columns
1454            .iter()
1455            .map(|column| Field::from(column.column_desc.clone()))
1456            .collect();
1457
1458        let source = MockSource::with_messages(vec![
1459            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1460            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1461                " I I I
1462                    + 1 1 10",
1463            ))),
1464            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1465            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1466                " I I I
1467                    + 1 3 30",
1468            ))),
1469            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1470                " I I I
1471                    + 1 2 20
1472                    - 1 2 20",
1473            ))),
1474            Message::Chunk(std::mem::take(&mut StreamChunk::from_pretty(
1475                " I I I
1476                    - 1 1 10
1477                    + 1 1 40",
1478            ))),
1479            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1480        ])
1481        .into_executor(schema.clone(), vec![0, 1]);
1482
1483        let sink_param = SinkParam {
1484            sink_id: 0.into(),
1485            sink_name: "test".into(),
1486            properties,
1487
1488            columns: columns
1489                .iter()
1490                .filter(|col| !col.is_hidden)
1491                .map(|col| col.column_desc.clone())
1492                .collect(),
1493            downstream_pk: Some(vec![0]),
1494            sink_type,
1495            ignore_delete: false,
1496            format_desc: None,
1497            db_name: "test".into(),
1498            sink_from_name: "test".into(),
1499        };
1500
1501        let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1502
1503        let sink = build_sink(sink_param.clone()).unwrap();
1504
1505        let sink_executor = SinkExecutor::new(
1506            ActorContext::for_test(0),
1507            info,
1508            source,
1509            SinkWriterParam::for_test(),
1510            sink,
1511            sink_param,
1512            columns.clone(),
1513            BoundedInMemLogStoreFactory::for_test(1),
1514            1024,
1515            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1516            None,
1517        )
1518        .unwrap();
1519
1520        let mut executor = sink_executor.boxed().execute();
1521
1522        // Barrier message.
1523        executor.next().await.unwrap().unwrap();
1524
1525        let chunk_msg = executor.next().await.unwrap().unwrap();
1526        assert_eq!(
1527            chunk_msg.into_chunk().unwrap().compact_vis(),
1528            StreamChunk::from_pretty(
1529                " I I I
1530                + 1 1 10",
1531            )
1532        );
1533
1534        // Barrier message.
1535        executor.next().await.unwrap().unwrap();
1536
1537        let chunk_msg = executor.next().await.unwrap().unwrap();
1538        let expected = match sink_type {
1539            SinkType::Retract => StreamChunk::from_pretty(
1540                " I I I
1541                U- 1 1 10
1542                U+ 1 1 40",
1543            ),
1544            SinkType::Upsert => StreamChunk::from_pretty(
1545                " I I I
1546                + 1 1 40", // For upsert format, there won't be `U- 1 1 10`.
1547            ),
1548            _ => unreachable!(),
1549        };
1550        assert_eq!(chunk_msg.into_chunk().unwrap().compact_vis(), expected);
1551
1552        // The last barrier message.
1553        executor.next().await.unwrap().unwrap();
1554    }
1555
1556    #[tokio::test]
1557    async fn test_sink_into_table_preserves_special_conflict_rows_for_mismatched_pk() {
1558        use risingwave_common::array::StreamChunkTestExt;
1559        use risingwave_common::array::stream_chunk::StreamChunk;
1560        use risingwave_common::types::DataType;
1561
1562        use crate::executor::Barrier;
1563
1564        let properties = maplit::btreemap! {
1565            "connector".into() => "table".into(),
1566            SINK_USER_PRESERVE_ROW_LEVEL_CHANGES.into() => "true".into(),
1567        };
1568
1569        let columns = vec![
1570            ColumnCatalog {
1571                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1572                is_hidden: false,
1573            },
1574            ColumnCatalog {
1575                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1576                is_hidden: false,
1577            },
1578            ColumnCatalog {
1579                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1580                is_hidden: false,
1581            },
1582        ];
1583        let schema: Schema = columns
1584            .iter()
1585            .map(|column| Field::from(column.column_desc.clone()))
1586            .collect();
1587
1588        let source = MockSource::with_messages(vec![
1589            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1590            Message::Chunk(StreamChunk::from_pretty(
1591                " I  I  I
1592                  + 1 10  1",
1593            )),
1594            Message::Chunk(StreamChunk::from_pretty(
1595                " I  I  I
1596                  + 1 20  2",
1597            )),
1598            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1599            // Downstream key 1 moves to a new stream key, while downstream key 2 is really deleted.
1600            Message::Chunk(StreamChunk::from_pretty(
1601                " I  I  I
1602                  - 1 10  1
1603                  - 1 20  2
1604                  + 1 30  3
1605                  - 2 50  5",
1606            )),
1607            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1608        ])
1609        .into_executor(schema.clone(), vec![0, 2]);
1610
1611        let sink_param = SinkParam {
1612            sink_id: 0.into(),
1613            sink_name: "test".into(),
1614            properties,
1615            columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1616            downstream_pk: Some(vec![0]),
1617            sink_type: SinkType::Upsert,
1618            ignore_delete: false,
1619            format_desc: None,
1620            db_name: "test".into(),
1621            sink_from_name: "test".into(),
1622        };
1623
1624        let info = ExecutorInfo::for_test(schema, vec![0, 2], "SinkExecutor".to_owned(), 0);
1625        let sink = build_sink(sink_param.clone()).unwrap();
1626
1627        let sink_executor = SinkExecutor::new(
1628            ActorContext::for_test(0),
1629            info,
1630            source,
1631            SinkWriterParam::for_test(),
1632            sink,
1633            sink_param,
1634            columns,
1635            BoundedInMemLogStoreFactory::for_test(1),
1636            1024,
1637            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1638            None,
1639        )
1640        .unwrap();
1641
1642        let mut executor = sink_executor.boxed().execute();
1643
1644        executor.next().await.unwrap().unwrap();
1645
1646        let chunk_msg = executor.next().await.unwrap().unwrap();
1647        assert_eq!(
1648            chunk_msg.into_chunk().unwrap().compact_vis(),
1649            StreamChunk::from_pretty(
1650                " I  I  I
1651                  + 1 10  1
1652                  + 1 20  2",
1653            )
1654        );
1655
1656        executor.next().await.unwrap().unwrap();
1657
1658        let chunk_msg = executor.next().await.unwrap().unwrap();
1659        assert_eq!(
1660            chunk_msg.into_chunk().unwrap().compact_vis(),
1661            StreamChunk::from_pretty(
1662                " I  I  I
1663                  - 2 50  5
1664                  + 1 30  3",
1665            )
1666        );
1667
1668        executor.next().await.unwrap().unwrap();
1669    }
1670
1671    #[tokio::test]
1672    async fn test_sink_into_table_keeps_default_compaction_without_special_conflict_semantics() {
1673        use risingwave_common::array::StreamChunkTestExt;
1674        use risingwave_common::array::stream_chunk::StreamChunk;
1675        use risingwave_common::types::DataType;
1676
1677        use crate::executor::Barrier;
1678
1679        let properties = maplit::btreemap! {
1680            "connector".into() => "table".into(),
1681        };
1682
1683        let columns = vec![
1684            ColumnCatalog {
1685                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1686                is_hidden: false,
1687            },
1688            ColumnCatalog {
1689                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1690                is_hidden: false,
1691            },
1692        ];
1693        let schema: Schema = columns
1694            .iter()
1695            .map(|column| Field::from(column.column_desc.clone()))
1696            .collect();
1697
1698        let source = MockSource::with_messages(vec![
1699            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1700            Message::Chunk(StreamChunk::from_pretty(
1701                " I  I
1702                  + 1 10
1703                  + 1 20",
1704            )),
1705            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1706        ])
1707        .into_executor(schema.clone(), vec![0]);
1708
1709        let sink_param = SinkParam {
1710            sink_id: 0.into(),
1711            sink_name: "test".into(),
1712            properties,
1713            columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
1714            downstream_pk: Some(vec![0]),
1715            sink_type: SinkType::Upsert,
1716            ignore_delete: false,
1717            format_desc: None,
1718            db_name: "test".into(),
1719            sink_from_name: "test".into(),
1720        };
1721
1722        let info = ExecutorInfo::for_test(schema, vec![0], "SinkExecutor".to_owned(), 0);
1723        let sink = build_sink(sink_param.clone()).unwrap();
1724
1725        let sink_executor = SinkExecutor::new(
1726            ActorContext::for_test(0),
1727            info,
1728            source,
1729            SinkWriterParam::for_test(),
1730            sink,
1731            sink_param,
1732            columns,
1733            BoundedInMemLogStoreFactory::for_test(1),
1734            1024,
1735            vec![DataType::Int64, DataType::Int64],
1736            None,
1737        )
1738        .unwrap();
1739
1740        let mut executor = sink_executor.boxed().execute();
1741
1742        executor.next().await.unwrap().unwrap();
1743
1744        let chunk_msg = executor.next().await.unwrap().unwrap();
1745        assert_eq!(
1746            chunk_msg.into_chunk().unwrap().compact_vis(),
1747            StreamChunk::from_pretty(
1748                " I  I
1749                  + 1 20",
1750            )
1751        );
1752
1753        executor.next().await.unwrap().unwrap();
1754    }
1755
1756    #[tokio::test]
1757    async fn test_empty_barrier_sink() {
1758        use risingwave_common::types::DataType;
1759
1760        use crate::executor::Barrier;
1761
1762        let properties = maplit::btreemap! {
1763            "connector".into() => "blackhole".into(),
1764            "type".into() => "append-only".into(),
1765            "force_append_only".into() => "true".into()
1766        };
1767        let columns = vec![
1768            ColumnCatalog {
1769                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1770                is_hidden: false,
1771            },
1772            ColumnCatalog {
1773                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1774                is_hidden: false,
1775            },
1776        ];
1777        let schema: Schema = columns
1778            .iter()
1779            .map(|column| Field::from(column.column_desc.clone()))
1780            .collect();
1781        let stream_key = vec![0];
1782
1783        let source = MockSource::with_messages(vec![
1784            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1785            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1786            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1787        ])
1788        .into_executor(schema.clone(), stream_key.clone());
1789
1790        let sink_param = SinkParam {
1791            sink_id: 0.into(),
1792            sink_name: "test".into(),
1793            properties,
1794
1795            columns: columns
1796                .iter()
1797                .filter(|col| !col.is_hidden)
1798                .map(|col| col.column_desc.clone())
1799                .collect(),
1800            downstream_pk: Some(stream_key.clone()),
1801            sink_type: SinkType::AppendOnly,
1802            ignore_delete: true,
1803            format_desc: None,
1804            db_name: "test".into(),
1805            sink_from_name: "test".into(),
1806        };
1807
1808        let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
1809
1810        let sink = build_sink(sink_param.clone()).unwrap();
1811
1812        let sink_executor = SinkExecutor::new(
1813            ActorContext::for_test(0),
1814            info,
1815            source,
1816            SinkWriterParam::for_test(),
1817            sink,
1818            sink_param,
1819            columns,
1820            BoundedInMemLogStoreFactory::for_test(1),
1821            1024,
1822            vec![DataType::Int64, DataType::Int64],
1823            None,
1824        )
1825        .unwrap();
1826
1827        let mut executor = sink_executor.boxed().execute();
1828
1829        // Barrier message.
1830        assert_eq!(
1831            executor.next().await.unwrap().unwrap(),
1832            Message::Barrier(Barrier::new_test_barrier(test_epoch(1)))
1833        );
1834
1835        // Barrier message.
1836        assert_eq!(
1837            executor.next().await.unwrap().unwrap(),
1838            Message::Barrier(Barrier::new_test_barrier(test_epoch(2)))
1839        );
1840
1841        // The last barrier message.
1842        assert_eq!(
1843            executor.next().await.unwrap().unwrap(),
1844            Message::Barrier(Barrier::new_test_barrier(test_epoch(3)))
1845        );
1846    }
1847
1848    #[tokio::test]
1849    async fn test_force_compaction() {
1850        use risingwave_common::array::StreamChunkTestExt;
1851        use risingwave_common::array::stream_chunk::StreamChunk;
1852        use risingwave_common::types::DataType;
1853
1854        use crate::executor::Barrier;
1855
1856        let properties = maplit::btreemap! {
1857            "connector".into() => "blackhole".into(),
1858            "force_compaction".into() => "true".into()
1859        };
1860
1861        // We have two visible columns and one hidden column. The hidden column will be pruned out
1862        // within the sink executor.
1863        let columns = vec![
1864            ColumnCatalog {
1865                column_desc: ColumnDesc::unnamed(ColumnId::new(0), DataType::Int64),
1866                is_hidden: false,
1867            },
1868            ColumnCatalog {
1869                column_desc: ColumnDesc::unnamed(ColumnId::new(1), DataType::Int64),
1870                is_hidden: false,
1871            },
1872            ColumnCatalog {
1873                column_desc: ColumnDesc::unnamed(ColumnId::new(2), DataType::Int64),
1874                is_hidden: true,
1875            },
1876        ];
1877        let schema: Schema = columns
1878            .iter()
1879            .map(|column| Field::from(column.column_desc.clone()))
1880            .collect();
1881
1882        let source = MockSource::with_messages(vec![
1883            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1884            Message::Chunk(StreamChunk::from_pretty(
1885                " I I I
1886                    + 1 1 10",
1887            )),
1888            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1889            Message::Chunk(StreamChunk::from_pretty(
1890                " I I I
1891                    + 1 3 30",
1892            )),
1893            Message::Chunk(StreamChunk::from_pretty(
1894                " I I I
1895                    + 1 2 20
1896                    - 1 2 20
1897                    + 1 4 10",
1898            )),
1899            Message::Chunk(StreamChunk::from_pretty(
1900                " I I I
1901                    - 1 1 10
1902                    + 1 1 40",
1903            )),
1904            Message::Chunk(StreamChunk::from_pretty(
1905                " I I I
1906                    - 1 4 30",
1907            )),
1908            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
1909        ])
1910        .into_executor(schema.clone(), vec![0, 1]);
1911
1912        let sink_param = SinkParam {
1913            sink_id: 0.into(),
1914            sink_name: "test".into(),
1915            properties,
1916
1917            columns: columns
1918                .iter()
1919                .filter(|col| !col.is_hidden)
1920                .map(|col| col.column_desc.clone())
1921                .collect(),
1922            downstream_pk: Some(vec![0, 1]),
1923            sink_type: SinkType::Upsert,
1924            ignore_delete: false,
1925            format_desc: None,
1926            db_name: "test".into(),
1927            sink_from_name: "test".into(),
1928        };
1929
1930        let info = ExecutorInfo::for_test(schema, vec![0, 1], "SinkExecutor".to_owned(), 0);
1931
1932        let sink = build_sink(sink_param.clone()).unwrap();
1933
1934        let sink_executor = SinkExecutor::new(
1935            ActorContext::for_test(0),
1936            info,
1937            source,
1938            SinkWriterParam::for_test(),
1939            sink,
1940            sink_param,
1941            columns.clone(),
1942            BoundedInMemLogStoreFactory::for_test(1),
1943            1024,
1944            vec![DataType::Int64, DataType::Int64, DataType::Int64],
1945            None,
1946        )
1947        .unwrap();
1948
1949        let mut executor = sink_executor.boxed().execute();
1950
1951        // Barrier message.
1952        executor.next().await.unwrap().unwrap();
1953
1954        let chunk_msg = executor.next().await.unwrap().unwrap();
1955        assert_eq!(
1956            chunk_msg.into_chunk().unwrap().compact_vis(),
1957            StreamChunk::from_pretty(
1958                " I I I
1959                + 1 1 10",
1960            )
1961        );
1962
1963        // Barrier message.
1964        executor.next().await.unwrap().unwrap();
1965
1966        let chunk_msg = executor.next().await.unwrap().unwrap();
1967        assert_eq!(
1968            chunk_msg.into_chunk().unwrap().compact_vis(),
1969            StreamChunk::from_pretty(
1970                " I I I
1971                + 1 1 40
1972                + 1 3 30",
1973            )
1974        );
1975
1976        // The last barrier message.
1977        executor.next().await.unwrap().unwrap();
1978    }
1979
1980    #[tokio::test]
1981    async fn test_upsert_input_retains_delete() {
1982        use risingwave_common::array::StreamChunkTestExt;
1983
1984        for force_compaction in [false, true] {
1985            for separate_chunks in [false, true] {
1986                let mut input = vec![
1987                    Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
1988                    Message::Chunk(StreamChunk::from_pretty("I I\n+ 1 10")),
1989                    Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
1990                ];
1991                if separate_chunks {
1992                    input.push(Message::Chunk(StreamChunk::from_pretty("I I\n+ 1 20")));
1993                    input.push(Message::Chunk(StreamChunk::from_pretty("I I\n- 1 .")));
1994                } else {
1995                    input.push(Message::Chunk(StreamChunk::from_pretty(
1996                        "I I\n+ 1 20\n- 1 .",
1997                    )));
1998                }
1999                input.push(Message::Barrier(Barrier::new_test_barrier(test_epoch(3))));
2000                let ctx = ActorContext::for_test(0);
2001                let metrics =
2002                    ctx.streaming_metrics
2003                        .new_sink_exec_metrics(0.into(), ctx.id, ctx.fragment_id);
2004                let output = SinkExecutor::<BoundedInMemLogStoreFactory>::process_msg(
2005                    futures::stream::iter(input.into_iter().map(Ok)),
2006                    SinkType::Upsert,
2007                    vec![0],
2008                    1024,
2009                    vec![DataType::Int64; 2],
2010                    StreamKind::Upsert,
2011                    Some(vec![0]),
2012                    Some(NonAppendOnlyBehavior {
2013                        pk_specified_and_matched: true,
2014                        force_compaction,
2015                    }),
2016                    metrics.sink_chunk_buffer_size,
2017                    false,
2018                    false,
2019                )
2020                .try_collect::<Vec<_>>()
2021                .await
2022                .unwrap();
2023                let chunks = output
2024                    .into_iter()
2025                    .filter_map(|msg| match msg {
2026                        Message::Chunk(chunk) if chunk.has_visible_rows() => Some(chunk),
2027                        _ => None,
2028                    })
2029                    .collect_vec();
2030                let expected = if separate_chunks && !force_compaction {
2031                    StreamChunk::from_pretty("I I\n+ 1 10\n+ 1 20\n- 1 .")
2032                } else {
2033                    StreamChunk::from_pretty("I I\n+ 1 10\n- 1 .")
2034                };
2035                assert_eq!(StreamChunk::concat(chunks), expected);
2036            }
2037        }
2038    }
2039
2040    /// Runs a sink executor over `Int64` columns and returns the output chunks.
2041    async fn run_sink_executor(
2042        properties: BTreeMap<String, String>,
2043        sink_type: SinkType,
2044        input_kind: StreamKind,
2045        stream_key: Vec<usize>,
2046        downstream_pk: Vec<usize>,
2047        input: Vec<Message>,
2048    ) -> Vec<StreamChunk> {
2049        let num_columns = input
2050            .iter()
2051            .find_map(|msg| msg.as_chunk())
2052            .unwrap()
2053            .columns()
2054            .len();
2055        let columns = (0..num_columns)
2056            .map(|i| ColumnCatalog {
2057                column_desc: ColumnDesc::unnamed(ColumnId::new(i as i32), DataType::Int64),
2058                is_hidden: false,
2059            })
2060            .collect_vec();
2061        let schema: Schema = columns
2062            .iter()
2063            .map(|column| Field::from(column.column_desc.clone()))
2064            .collect();
2065        let num_barriers = input.iter().filter(|msg| msg.is_barrier()).count();
2066
2067        let source = Executor::new(
2068            ExecutorInfo {
2069                stream_kind: input_kind,
2070                ..ExecutorInfo::for_test(
2071                    schema.clone(),
2072                    stream_key.clone(),
2073                    "MockSource".to_owned(),
2074                    0,
2075                )
2076            },
2077            MockSource::with_messages(input).boxed(),
2078        );
2079        let sink_param = SinkParam {
2080            sink_id: 0.into(),
2081            sink_name: "test".into(),
2082            properties,
2083            columns: columns.iter().map(|col| col.column_desc.clone()).collect(),
2084            downstream_pk: Some(downstream_pk),
2085            sink_type,
2086            ignore_delete: false,
2087            format_desc: None,
2088            db_name: "test".into(),
2089            sink_from_name: "test".into(),
2090        };
2091        let info = ExecutorInfo::for_test(schema, stream_key, "SinkExecutor".to_owned(), 0);
2092        let sink = build_sink(sink_param.clone()).unwrap();
2093        let mut executor = SinkExecutor::new(
2094            ActorContext::for_test(0),
2095            info,
2096            source,
2097            SinkWriterParam::for_test(),
2098            sink,
2099            sink_param,
2100            columns,
2101            BoundedInMemLogStoreFactory::for_test(1),
2102            1024,
2103            vec![DataType::Int64; num_columns],
2104            None,
2105        )
2106        .unwrap()
2107        .boxed()
2108        .execute();
2109
2110        let mut output = vec![];
2111        for _ in 0..num_barriers {
2112            loop {
2113                match executor.next().await.unwrap().unwrap() {
2114                    Message::Chunk(chunk) => output.push(chunk.compact_vis()),
2115                    Message::Barrier(_) => break,
2116                    Message::Watermark(_) => unreachable!(),
2117                }
2118            }
2119        }
2120        output
2121    }
2122
2123    /// The stream key `(0, 1)` does not cover the sink pk `(0, 2)`, so an update of a stream key
2124    /// may change its sink pk.
2125    fn sink_pk_change_input() -> Vec<Message> {
2126        use risingwave_common::array::StreamChunkTestExt;
2127
2128        vec![
2129            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2130            Message::Chunk(StreamChunk::from_pretty(
2131                " I I I I
2132                + 1 1 6 10
2133                + 2 2 6 10",
2134            )),
2135            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2136            // The sink pk of `(1, 1)` changes, while that of `(2, 2)` does not.
2137            Message::Chunk(StreamChunk::from_pretty(
2138                "  I I I I
2139                U- 1 1 6 10
2140                U+ 1 1 8 10
2141                U- 2 2 6 10
2142                U+ 2 2 6 20",
2143            )),
2144            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2145        ]
2146    }
2147
2148    #[tokio::test]
2149    async fn test_sink_pk_change_upsert() {
2150        test_sink_pk_change(SinkType::Upsert).await;
2151    }
2152
2153    #[tokio::test]
2154    async fn test_sink_pk_change_retract() {
2155        test_sink_pk_change(SinkType::Retract).await;
2156    }
2157
2158    async fn test_sink_pk_change(sink_type: SinkType) {
2159        use risingwave_common::array::StreamChunkTestExt;
2160
2161        let output = run_sink_executor(
2162            maplit::btreemap! { "connector".into() => "blackhole".into() },
2163            sink_type,
2164            StreamKind::Retract,
2165            vec![0, 1],
2166            vec![0, 2],
2167            sink_pk_change_input(),
2168        )
2169        .await;
2170
2171        let expected = match sink_type {
2172            SinkType::Upsert => StreamChunk::from_pretty(
2173                " I I I I
2174                - 1 1 6 10
2175                + 2 2 6 20
2176                + 1 1 8 10",
2177            ),
2178            SinkType::Retract => StreamChunk::from_pretty(
2179                "  I I I I
2180                -  1 1 6 10
2181                U- 2 2 6 10
2182                U+ 2 2 6 20
2183                +  1 1 8 10",
2184            ),
2185            _ => unreachable!(),
2186        };
2187        assert_eq!(
2188            output,
2189            vec![
2190                StreamChunk::from_pretty(
2191                    " I I I I
2192                    + 1 1 6 10
2193                    + 2 2 6 10",
2194                ),
2195                expected,
2196            ]
2197        );
2198    }
2199
2200    #[tokio::test]
2201    async fn test_sink_into_table_preserves_special_conflict_rows_for_pk_change() {
2202        use risingwave_common::array::StreamChunkTestExt;
2203
2204        let output = run_sink_executor(
2205            maplit::btreemap! {
2206                "connector".into() => "table".into(),
2207                SINK_USER_PRESERVE_ROW_LEVEL_CHANGES.into() => "true".into()
2208            },
2209            SinkType::Upsert,
2210            StreamKind::Retract,
2211            vec![0, 1],
2212            vec![0, 2],
2213            sink_pk_change_input(),
2214        )
2215        .await;
2216
2217        // The delete of the unchanged sink pk `(2, 6)` is dropped, as the key is inserted again.
2218        assert_eq!(
2219            output,
2220            vec![
2221                StreamChunk::from_pretty(
2222                    " I I I I
2223                    + 1 1 6 10
2224                    + 2 2 6 10",
2225                ),
2226                StreamChunk::from_pretty(
2227                    " I I I I
2228                    - 1 1 6 10
2229                    + 1 1 8 10
2230                    + 2 2 6 20",
2231                ),
2232            ]
2233        );
2234    }
2235
2236    /// With `force_compaction`, records are reordered even though the stream key `(0)` is covered by
2237    /// the sink pk `(0, 1)`, where an update may change the sink pk as well.
2238    #[tokio::test]
2239    async fn test_force_compaction_sink_pk_change() {
2240        use risingwave_common::array::StreamChunkTestExt;
2241
2242        let input = vec![
2243            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2244            Message::Chunk(StreamChunk::from_pretty(
2245                " I I I
2246                + 1 6 10",
2247            )),
2248            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2249            Message::Chunk(StreamChunk::from_pretty(
2250                "  I I I
2251                U- 1 6 10
2252                U+ 1 8 10",
2253            )),
2254            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2255        ];
2256        let output = run_sink_executor(
2257            maplit::btreemap! {
2258                "connector".into() => "blackhole".into(),
2259                "force_compaction".into() => "true".into()
2260            },
2261            SinkType::Upsert,
2262            StreamKind::Retract,
2263            vec![0],
2264            vec![0, 1],
2265            input,
2266        )
2267        .await;
2268
2269        assert_eq!(
2270            output,
2271            vec![
2272                StreamChunk::from_pretty(
2273                    " I I I
2274                    + 1 6 10",
2275                ),
2276                StreamChunk::from_pretty(
2277                    " I I I
2278                    - 1 6 10
2279                    + 1 8 10",
2280                ),
2281            ]
2282        );
2283    }
2284
2285    /// The old row of an upsert input only guarantees the stream key columns, so its sink pk is
2286    /// unknown and no delete is emitted for it.
2287    #[tokio::test]
2288    async fn test_upsert_input_sink_pk_change() {
2289        use risingwave_common::array::StreamChunkTestExt;
2290
2291        let input = vec![
2292            Message::Barrier(Barrier::new_test_barrier(test_epoch(1))),
2293            Message::Chunk(StreamChunk::from_pretty(
2294                " I I I I
2295                + 1 1 6 10",
2296            )),
2297            Message::Barrier(Barrier::new_test_barrier(test_epoch(2))),
2298            Message::Chunk(StreamChunk::from_pretty(
2299                " I I I I
2300                - 1 1 . .
2301                + 1 1 8 10",
2302            )),
2303            Message::Barrier(Barrier::new_test_barrier(test_epoch(3))),
2304        ];
2305        let output = run_sink_executor(
2306            maplit::btreemap! { "connector".into() => "blackhole".into() },
2307            SinkType::Upsert,
2308            StreamKind::Upsert,
2309            vec![0, 1],
2310            vec![0, 2],
2311            input,
2312        )
2313        .await;
2314
2315        assert_eq!(
2316            output,
2317            vec![
2318                StreamChunk::from_pretty(
2319                    " I I I I
2320                    + 1 1 6 10",
2321                ),
2322                StreamChunk::from_pretty(
2323                    " I I I I
2324                    + 1 1 8 10",
2325                ),
2326            ]
2327        );
2328    }
2329}