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