Skip to main content

risingwave_stream/executor/backfill/cdc/
cdc_backill_v2.rs

1// Copyright 2025 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;
16
17use either::Either;
18use futures::stream;
19use futures::stream::select_with_strategy;
20use itertools::Itertools;
21use risingwave_common::bitmap::BitmapBuilder;
22use risingwave_common::catalog::{ColumnDesc, Field};
23use risingwave_common::row::RowDeserializer;
24use risingwave_common::util::iter_util::ZipEqFast;
25use risingwave_common::util::sort_util::{OrderType, cmp_datum};
26use risingwave_connector::source::cdc::CdcScanOptions;
27use risingwave_connector::source::cdc::external::{CdcOffset, ExternalCdcTableType};
28use risingwave_connector::source::{CdcTableSnapshotSplit, CdcTableSnapshotSplitRaw};
29use rw_futures_util::pausable;
30use thiserror_ext::AsReport;
31
32use crate::executor::backfill::cdc::cdc_backfill::{
33    get_cdc_json_parse_handling_from_properties, transform_upstream,
34};
35use crate::executor::backfill::cdc::state_v2::{CdcStateRecord, ParallelizedCdcBackfillState};
36use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
37use crate::executor::backfill::cdc::upstream_table::reader::{
38    build_reader_and_poll_upstream, create_table_reader_with_retry,
39};
40use crate::executor::backfill::cdc::upstream_table::snapshot::{
41    SplitSnapshotReadArgs, UpstreamTableRead, UpstreamTableReader,
42};
43use crate::executor::backfill::utils::{get_cdc_chunk_last_offset, mapping_chunk, mapping_message};
44use crate::executor::prelude::*;
45use crate::task::ActorId;
46use crate::task::cdc_progress::CdcProgressReporter;
47
48pub struct ParallelizedCdcBackfillExecutor<S: StateStore> {
49    actor_ctx: ActorContextRef,
50
51    /// The external table to be backfilled
52    external_table: ExternalStorageTable,
53
54    /// Upstream changelog stream which may contain metadata columns, e.g. `_rw_offset`
55    upstream: Executor,
56
57    /// The column indices need to be forwarded to the downstream from the upstream and table scan.
58    output_indices: Vec<usize>,
59
60    /// The schema of output chunk, including additional columns if any
61    output_columns: Vec<ColumnDesc>,
62
63    /// Rate limit in rows/s.
64    rate_limit_rps: Option<u32>,
65
66    options: CdcScanOptions,
67
68    state_table: StateTable<S>,
69
70    properties: BTreeMap<String, String>,
71
72    progress: Option<CdcProgressReporter>,
73}
74
75enum SnapshotAttemptState {
76    Reading,
77    Failed,
78    Finished(CdcOffset),
79}
80
81impl<S: StateStore> ParallelizedCdcBackfillExecutor<S> {
82    #[expect(clippy::too_many_arguments)]
83    pub fn new(
84        actor_ctx: ActorContextRef,
85        external_table: ExternalStorageTable,
86        upstream: Executor,
87        output_indices: Vec<usize>,
88        output_columns: Vec<ColumnDesc>,
89        _metrics: Arc<StreamingMetrics>,
90        state_table: StateTable<S>,
91        rate_limit_rps: Option<u32>,
92        options: CdcScanOptions,
93        properties: BTreeMap<String, String>,
94        progress: Option<CdcProgressReporter>,
95    ) -> Self {
96        Self {
97            actor_ctx,
98            external_table,
99            upstream,
100            output_indices,
101            output_columns,
102            rate_limit_rps,
103            options,
104            state_table,
105            properties,
106            progress,
107        }
108    }
109
110    #[try_stream(ok = Message, error = StreamExecutorError)]
111    async fn execute_inner(mut self) {
112        assert!(!self.options.disable_backfill);
113        // The indices to primary key columns
114        let pk_indices = self.external_table.pk_indices().to_vec();
115        let table_id = self.external_table.table_id();
116        let upstream_table_name = self.external_table.qualified_table_name();
117        let schema_table_name = self.external_table.schema_table_name().clone();
118        let external_database_name = self.external_table.database_name().to_owned();
119        let additional_columns = self
120            .output_columns
121            .iter()
122            .filter(|col| col.additional_column.column_type.is_some())
123            .cloned()
124            .collect_vec();
125        assert!(
126            (self.options.backfill_split_pk_column_index as usize) < pk_indices.len(),
127            "split pk column index {} out of bound",
128            self.options.backfill_split_pk_column_index
129        );
130        let snapshot_split_column_index =
131            pk_indices[self.options.backfill_split_pk_column_index as usize];
132        let snapshot_split_column_in_output_index = self
133            .output_indices
134            .iter()
135            .position(|&idx| idx == snapshot_split_column_index)
136            .expect("snapshot split column must be present in CDC backfill output");
137        let cdc_table_snapshot_split_column =
138            vec![self.external_table.schema().fields[snapshot_split_column_index].clone()];
139
140        let mut upstream = self.upstream.execute();
141        // Poll the upstream to get the first barrier.
142        let first_barrier = expect_first_barrier(&mut upstream).await?;
143        // Make sure to use mapping_message after transform_upstream.
144
145        // If user sets debezium.time.precision.mode to "connect", it means the user can guarantee
146        // that the upstream data precision is MilliSecond. In this case, we don't use GuessNumberUnit
147        // mode to guess precision, but use Milli mode directly, which can handle extreme timestamps.
148        let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
149            get_cdc_json_parse_handling_from_properties(&self.properties);
150        // Only postgres-cdc connector may trigger TOAST.
151        let handle_toast_columns: bool =
152            self.external_table.table_type() == &ExternalCdcTableType::Postgres;
153        let mut upstream = transform_upstream(
154            upstream,
155            self.output_columns.clone(),
156            timestamp_handling,
157            timestamptz_handling,
158            time_handling,
159            bigint_unsigned_handling,
160            handle_toast_columns,
161        )
162        .boxed();
163        let mut next_reset_barrier = Some(first_barrier);
164        let mut is_reset = false;
165        let mut state_impl = ParallelizedCdcBackfillState::new(self.state_table);
166        // The buffered chunks have already been mapped.
167        let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
168
169        // Need reset on CDC table snapshot splits reschedule.
170        'with_cdc_table_snapshot_splits: loop {
171            assert!(upstream_chunk_buffer.is_empty());
172            let reset_barrier = next_reset_barrier.take().unwrap();
173            let all_snapshot_splits = match reset_barrier.mutation.as_deref() {
174                Some(Mutation::Add(add)) => &add.actor_cdc_table_snapshot_splits.splits,
175
176                Some(Mutation::Update(update)) => &update.actor_cdc_table_snapshot_splits.splits,
177                _ => {
178                    return Err(anyhow::anyhow!("ParallelizedCdcBackfillExecutor expects either Mutation::Add or Mutation::Update to initialize CDC table snapshot splits.").into());
179                }
180            };
181            let mut actor_snapshot_splits = vec![];
182            let mut generation = None;
183            // TODO(zw): optimization: remove consumed splits to reduce barrier size for downstream.
184            if let Some((splits, snapshot_generation)) = all_snapshot_splits.get(&self.actor_ctx.id)
185            {
186                actor_snapshot_splits = splits
187                    .iter()
188                    .map(|s: &CdcTableSnapshotSplitRaw| {
189                        let de = RowDeserializer::new(
190                            cdc_table_snapshot_split_column
191                                .iter()
192                                .map(Field::data_type)
193                                .collect_vec(),
194                        );
195                        let left_bound_inclusive =
196                            de.deserialize(s.left_bound_inclusive.as_ref()).unwrap();
197                        let right_bound_exclusive =
198                            de.deserialize(s.right_bound_exclusive.as_ref()).unwrap();
199                        CdcTableSnapshotSplit {
200                            split_id: s.split_id,
201                            left_bound_inclusive,
202                            right_bound_exclusive,
203                        }
204                    })
205                    .collect();
206                generation = Some(*snapshot_generation);
207            }
208            tracing::debug!(?actor_snapshot_splits, ?generation, "actor splits");
209            assert_consecutive_splits(&actor_snapshot_splits);
210
211            let mut is_snapshot_paused = reset_barrier.is_pause_on_startup();
212            let barrier_epoch = reset_barrier.epoch;
213            yield Message::Barrier(reset_barrier);
214            if !is_reset {
215                state_impl.init_epoch(barrier_epoch).await?;
216                is_reset = true;
217                tracing::info!(%table_id, "Initialize executor.");
218            } else {
219                tracing::info!(%table_id, "Reset executor.");
220            }
221
222            let mut current_actor_bounds = None;
223            let mut actor_cdc_offset_high: Option<CdcOffset> = None;
224            let mut actor_cdc_offset_low: Option<CdcOffset> = None;
225            let mut split_states = Vec::with_capacity(actor_snapshot_splits.len());
226
227            for split in &actor_snapshot_splits {
228                let state = state_impl.restore_state(split.split_id).await?;
229                extends_current_actor_bound(&mut current_actor_bounds, split);
230
231                if state.is_finished {
232                    if let Some(cdc_offset) = state.cdc_offset_low.as_ref()
233                        && actor_cdc_offset_low
234                            .as_ref()
235                            .is_none_or(|current| current > cdc_offset)
236                    {
237                        actor_cdc_offset_low = Some(cdc_offset.clone());
238                    }
239
240                    if let Some(cdc_offset) = state.cdc_offset_high.as_ref()
241                        && actor_cdc_offset_high
242                            .as_ref()
243                            .is_none_or(|current| current < cdc_offset)
244                    {
245                        actor_cdc_offset_high = Some(cdc_offset.clone());
246                    }
247                }
248
249                split_states.push(state);
250            }
251
252            let next_split_idx = split_states.iter().position(|state| !state.is_finished);
253            let finished_prefix_len = next_split_idx.unwrap_or(actor_snapshot_splits.len());
254            let mut should_report_actor_backfill_progress = (finished_prefix_len > 0).then(|| {
255                (
256                    actor_snapshot_splits[0].split_id,
257                    actor_snapshot_splits[finished_prefix_len - 1].split_id,
258                )
259            });
260
261            for split in &actor_snapshot_splits {
262                // Initialize state so that overall progress can be measured.
263                state_impl.init_state_if_absent(split.split_id).await?;
264            }
265
266            let offset_parse_func = self.external_table.table_type().get_cdc_offset_parser()?;
267
268            // A reader is only needed while at least one assigned snapshot split is unfinished.
269            // Once all splits are complete, the executor only forwards the table-filtered CDC
270            // stream and must not depend on the upstream snapshot table still existing.
271            if let Some(next_split_idx) = next_split_idx {
272                let external_table = self.external_table.clone();
273                let actor_id = self.actor_ctx.id;
274                let fragment_id = self.actor_ctx.fragment_id;
275                let mut future = Box::pin(create_table_reader_with_retry(
276                    external_table,
277                    actor_id,
278                    fragment_id,
279                ));
280
281                let table_reader = loop {
282                    match build_reader_and_poll_upstream(&mut upstream, &mut future).await? {
283                        Either::Left(msg) => match msg {
284                            Message::Barrier(barrier) => {
285                                for chunk in &upstream_chunk_buffer {
286                                    yield Message::Chunk(chunk.clone());
287                                }
288
289                                state_impl.commit_state(barrier.epoch).await?;
290
291                                if let Some(mutation) = barrier.mutation.as_deref() {
292                                    match mutation {
293                                        Mutation::Pause => {
294                                            is_snapshot_paused = true;
295                                        }
296                                        Mutation::Resume => {
297                                            is_snapshot_paused = false;
298                                        }
299                                        Mutation::Throttle(_) => {
300                                            if let Some(entry) = mutation.backfill_throttle_config(
301                                                self.actor_ctx.fragment_id,
302                                            ) {
303                                                self.rate_limit_rps = entry.rate_limit;
304                                            }
305                                        }
306                                        _ => (),
307                                    }
308                                }
309
310                                if is_reset_barrier(&barrier, self.actor_ctx.id) {
311                                    upstream_chunk_buffer.clear();
312                                    next_reset_barrier = Some(barrier);
313                                    continue 'with_cdc_table_snapshot_splits;
314                                }
315                                yield Message::Barrier(barrier);
316                            }
317                            Message::Chunk(chunk) => {
318                                if !chunk.has_visible_rows() {
319                                    continue;
320                                }
321
322                                let chunk = mapping_chunk(chunk, &self.output_indices);
323                                let (forwarded_chunk, buffered_chunk) = route_cdc_chunk(
324                                    chunk,
325                                    &actor_snapshot_splits,
326                                    &split_states,
327                                    next_split_idx,
328                                    snapshot_split_column_in_output_index,
329                                );
330
331                                if let Some(forwarded_chunk) = forwarded_chunk {
332                                    yield Message::Chunk(forwarded_chunk);
333                                }
334
335                                if let Some(buffered_chunk) = buffered_chunk {
336                                    upstream_chunk_buffer.push(buffered_chunk);
337                                }
338                            }
339                            Message::Watermark(_) => {
340                                // Ignore watermark, like the `CdcBackfillExecutor`.
341                            }
342                        },
343                        Either::Right(table_reader) => break table_reader,
344                    }
345                };
346                tracing::info!(
347                    %table_id,
348                    upstream_table_name,
349                    "table reader created successfully"
350                );
351
352                let mut upstream_table_reader =
353                    UpstreamTableReader::new(self.external_table.clone(), table_reader);
354
355                // Backfill snapshot splits sequentially.
356                for (split_idx, (split, restored_state)) in actor_snapshot_splits
357                    .iter()
358                    .zip_eq_fast(split_states.iter())
359                    .enumerate()
360                    .skip(next_split_idx)
361                {
362                    if restored_state.is_finished {
363                        extend_backfill_progress(
364                            &mut should_report_actor_backfill_progress,
365                            split.split_id,
366                        );
367
368                        continue;
369                    }
370                    tracing::info!(
371                        %table_id,
372                        upstream_table_name,
373                        ?split,
374                        is_snapshot_paused,
375                        "start cdc backfill split"
376                    );
377                    let mut durable_row_count = restored_state.row_count as u64;
378                    let mut split_cdc_offset_low = restored_state.cdc_offset_low.clone();
379
380                    let split_cdc_offset_high = 'backfill_loop: loop {
381                        // A rebuilt reader restarts this split from its lower bound, so count each
382                        // snapshot attempt independently instead of accumulating duplicate reads.
383                        let mut attempt_row_count = 0_u64;
384
385                        if split_cdc_offset_low.is_none() {
386                            static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
387                                tokio::sync::Semaphore::const_new(10);
388                            let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
389                            split_cdc_offset_low =
390                                upstream_table_reader.current_cdc_offset().await?;
391                        }
392
393                        if let Some(ref cdc_offset) = split_cdc_offset_low
394                            && actor_cdc_offset_low
395                                .as_ref()
396                                .is_none_or(|cur| cur > cdc_offset)
397                        {
398                            actor_cdc_offset_low = split_cdc_offset_low.clone();
399                        }
400
401                        let attempt_state = {
402                            let left_upstream = upstream.by_ref().map(Either::Left);
403                            let read_args = SplitSnapshotReadArgs::new(
404                                split.left_bound_inclusive.clone(),
405                                split.right_bound_exclusive.clone(),
406                                cdc_table_snapshot_split_column.clone(),
407                                self.rate_limit_rps,
408                                additional_columns.clone(),
409                                schema_table_name.clone(),
410                                external_database_name.clone(),
411                            );
412                            let right_snapshot = pin!(
413                                upstream_table_reader
414                                    .snapshot_read_table_split(read_args)
415                                    .map(Either::Right)
416                            );
417                            let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
418
419                            if is_snapshot_paused {
420                                snapshot_valve.pause();
421                            }
422
423                            let mut backfill_stream = select_with_strategy(
424                                left_upstream,
425                                right_snapshot,
426                                |_: &mut ()| stream::PollNext::Left,
427                            );
428                            let mut attempt_state = SnapshotAttemptState::Reading;
429
430                            #[for_await]
431                            'backfill_stream: for either in &mut backfill_stream {
432                                match either {
433                                    Either::Left(upstream_message) => match upstream_message? {
434                                        Message::Barrier(barrier) => {
435                                            // The source offset advances with this barrier, so make
436                                            // every buffered change durable first. Keep the buffer
437                                            // because a later snapshot row may still overwrite it.
438                                            for chunk in &upstream_chunk_buffer {
439                                                yield Message::Chunk(chunk.clone());
440                                            }
441
442                                            durable_row_count =
443                                                durable_row_count.max(attempt_row_count);
444
445                                            state_impl
446                                                .mutate_state(
447                                                    split.split_id,
448                                                    false,
449                                                    durable_row_count,
450                                                    split_cdc_offset_low.clone(),
451                                                    None,
452                                                )
453                                                .await?;
454                                            state_impl.commit_state(barrier.epoch).await?;
455
456                                            if let Some(mutation) = barrier.mutation.as_deref() {
457                                                use crate::executor::Mutation;
458                                                match mutation {
459                                                    Mutation::Pause => {
460                                                        is_snapshot_paused = true;
461                                                        snapshot_valve.pause();
462                                                    }
463                                                    Mutation::Resume => {
464                                                        is_snapshot_paused = false;
465                                                        snapshot_valve.resume();
466                                                    }
467                                                    Mutation::Throttle(_) => {
468                                                        // TODO(zw): optimization: improve throttle.
469                                                        // 1. Handle rate limit 0. Currently, to resume the process, the actor must be rebuilt.
470                                                        // 2. Apply new rate limit immediately.
471                                                        if let Some(entry) = mutation
472                                                            .backfill_throttle_config(
473                                                                self.actor_ctx.fragment_id,
474                                                            )
475                                                        {
476                                                            // The new rate limit will take effect since next split.
477                                                            self.rate_limit_rps = entry.rate_limit;
478                                                        }
479                                                    }
480                                                    mutation
481                                                        if mutation.is_stop(self.actor_ctx.id) =>
482                                                    {
483                                                        tracing::info!(
484                                                            %table_id,
485                                                            upstream_table_name,
486                                                            "CdcBackfill has been dropped due to config change"
487                                                        );
488
489                                                        yield Message::Barrier(barrier);
490
491                                                        let () = futures::future::pending().await;
492                                                        unreachable!();
493                                                    }
494                                                    _ => (),
495                                                }
496                                            }
497
498                                            if is_reset_barrier(&barrier, self.actor_ctx.id) {
499                                                // the cdc events belonging to this split no longer matters,
500                                                // since a new snapshot will cover the latest state
501                                                upstream_chunk_buffer.clear();
502                                                next_reset_barrier = Some(barrier);
503
504                                                // restart to apply new split state
505                                                continue 'with_cdc_table_snapshot_splits;
506                                            }
507
508                                            if let (Some(split_range), Some(progress)) = (
509                                                should_report_actor_backfill_progress.take(),
510                                                self.progress.as_ref(),
511                                            ) {
512                                                progress.update(
513                                                    self.actor_ctx.fragment_id,
514                                                    self.actor_ctx.id,
515                                                    barrier.epoch,
516                                                    generation.expect("should have set generation when having progress to report"),
517                                                    split_range,
518                                                );
519                                            }
520
521                                            yield Message::Barrier(barrier);
522
523                                            if matches!(attempt_state, SnapshotAttemptState::Failed)
524                                            {
525                                                break 'backfill_stream;
526                                            }
527                                        }
528                                        Message::Chunk(chunk) => {
529                                            if !chunk.has_visible_rows() {
530                                                continue 'backfill_stream;
531                                            }
532
533                                            // TODO(zw): re-enable
534                                            // let chunk_cdc_offset =
535                                            //     get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
536                                            // if *self.external_table.table_type()
537                                            //     == ExternalCdcTableType::Postgres
538                                            //     && let Some(cur) = actor_cdc_offset_low.as_ref()
539                                            //     && let Some(chunk_offset) = chunk_cdc_offset
540                                            //     && chunk_offset < *cur
541                                            // {
542                                            //     continue;
543                                            // }
544
545                                            // emit chunks belonging to past splits which are processed
546                                            let chunk = mapping_chunk(chunk, &self.output_indices);
547                                            let (forwarded_chunk, buffered_chunk) = route_cdc_chunk(
548                                                chunk,
549                                                &actor_snapshot_splits,
550                                                &split_states,
551                                                split_idx,
552                                                snapshot_split_column_in_output_index,
553                                            );
554
555                                            if let Some(forwarded_chunk) = forwarded_chunk {
556                                                yield Message::Chunk(forwarded_chunk);
557                                            }
558
559                                            if let Some(buffered_chunk) = buffered_chunk {
560                                                upstream_chunk_buffer.push(buffered_chunk);
561                                            }
562                                        }
563                                        Message::Watermark(_) => {
564                                            // ignore watermark during backfill
565                                        }
566                                    },
567                                    Either::Right(snapshot) => {
568                                        // if snapshot already failed, continue polling upstream until barrier arrives to reconstruct reader
569                                        if matches!(attempt_state, SnapshotAttemptState::Failed) {
570                                            continue 'backfill_stream;
571                                        }
572
573                                        match snapshot {
574                                            Ok(None) => {
575                                                tracing::info!(
576                                                    %table_id,
577                                                    split_id = split.split_id,
578                                                    "snapshot read stream ends"
579                                                );
580
581                                                for chunk in upstream_chunk_buffer.drain(..) {
582                                                    yield Message::Chunk(chunk);
583                                                }
584
585                                                // This attempt scanned the whole split, so its count
586                                                // is exact even if earlier attempts read duplicate rows.
587                                                durable_row_count = attempt_row_count;
588
589                                                // Limit concurrent CDC connections globally to 10 using a semaphore.
590                                                static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
591                                                    tokio::sync::Semaphore::const_new(10);
592                                                let _permit =
593                                                    CDC_CONN_SEMAPHORE.acquire().await.unwrap();
594                                                let high = upstream_table_reader
595                                                .current_cdc_offset()
596                                                .await?
597                                                .expect(
598                                                    "CDC offset must be available after snapshot completion",
599                                                );
600                                                attempt_state =
601                                                    SnapshotAttemptState::Finished(high);
602
603                                                break 'backfill_stream;
604                                            }
605                                            Ok(Some(chunk)) => {
606                                                attempt_row_count = attempt_row_count
607                                                    .saturating_add(chunk.cardinality() as u64);
608
609                                                yield Message::Chunk(mapping_chunk(
610                                                    chunk,
611                                                    &self.output_indices,
612                                                ));
613                                            }
614                                            Err(error) => {
615                                                attempt_state = SnapshotAttemptState::Failed;
616                                                tracing::warn!(
617                                                    error = %error.as_report(),
618                                                    %table_id,
619                                                    upstream_table_name,
620                                                    "failed to read CDC snapshot; rebuilding reader after a barrier"
621                                                );
622                                            }
623                                        }
624                                    }
625                                }
626                            }
627                            attempt_state
628                        };
629
630                        match attempt_state {
631                            SnapshotAttemptState::Finished(split_cdc_offset_high) => {
632                                break 'backfill_loop split_cdc_offset_high;
633                            }
634                            SnapshotAttemptState::Failed => {
635                                if let Err(error) = upstream_table_reader.disconnect().await {
636                                    tracing::warn!(
637                                        error = %error.as_report(),
638                                        %table_id,
639                                        upstream_table_name,
640                                        "failed to disconnect CDC snapshot reader; continuing with rebuild"
641                                    );
642                                }
643
644                                let mut future = Box::pin(create_table_reader_with_retry(
645                                    self.external_table.clone(),
646                                    self.actor_ctx.id,
647                                    self.actor_ctx.fragment_id,
648                                ));
649                                let table_reader = loop {
650                                    match build_reader_and_poll_upstream(&mut upstream, &mut future)
651                                        .await?
652                                    {
653                                        Either::Left(msg) => match msg {
654                                            Message::Barrier(barrier) => {
655                                                for chunk in &upstream_chunk_buffer {
656                                                    yield Message::Chunk(chunk.clone());
657                                                }
658
659                                                state_impl.commit_state(barrier.epoch).await?;
660
661                                                if let Some(mutation) = barrier.mutation.as_deref()
662                                                {
663                                                    use crate::executor::Mutation;
664                                                    match mutation {
665                                                        Mutation::Pause => {
666                                                            is_snapshot_paused = true;
667                                                        }
668                                                        Mutation::Resume => {
669                                                            is_snapshot_paused = false;
670                                                        }
671                                                        Mutation::Throttle(_) => {
672                                                            if let Some(entry) = mutation
673                                                                .backfill_throttle_config(
674                                                                    self.actor_ctx.fragment_id,
675                                                                )
676                                                            {
677                                                                self.rate_limit_rps =
678                                                                    entry.rate_limit;
679                                                            }
680                                                        }
681                                                        mutation
682                                                            if mutation
683                                                                .is_stop(self.actor_ctx.id) =>
684                                                        {
685                                                            yield Message::Barrier(barrier);
686                                                            let () =
687                                                                futures::future::pending().await;
688                                                            unreachable!();
689                                                        }
690                                                        _ => (),
691                                                    }
692                                                }
693
694                                                if is_reset_barrier(&barrier, self.actor_ctx.id) {
695                                                    upstream_chunk_buffer.clear();
696                                                    next_reset_barrier = Some(barrier);
697
698                                                    continue 'with_cdc_table_snapshot_splits;
699                                                }
700
701                                                yield Message::Barrier(barrier);
702                                            }
703                                            Message::Chunk(chunk) => {
704                                                let chunk =
705                                                    mapping_chunk(chunk, &self.output_indices);
706                                                let (forwarded_chunk, buffered_chunk) =
707                                                    route_cdc_chunk(
708                                                        chunk,
709                                                        &actor_snapshot_splits,
710                                                        &split_states,
711                                                        split_idx,
712                                                        snapshot_split_column_in_output_index,
713                                                    );
714
715                                                if let Some(forwarded_chunk) = forwarded_chunk {
716                                                    yield Message::Chunk(forwarded_chunk);
717                                                }
718
719                                                if let Some(buffered_chunk) = buffered_chunk {
720                                                    upstream_chunk_buffer.push(buffered_chunk);
721                                                }
722                                            }
723                                            Message::Watermark(_) => {}
724                                        },
725                                        Either::Right(table_reader) => {
726                                            break table_reader;
727                                        }
728                                    }
729                                };
730
731                                upstream_table_reader = UpstreamTableReader::new(
732                                    self.external_table.clone(),
733                                    table_reader,
734                                );
735
736                                tracing::info!(
737                                    %table_id,
738                                    upstream_table_name,
739                                    "CDC table reader rebuilt successfully"
740                                );
741
742                                continue 'backfill_loop;
743                            }
744                            SnapshotAttemptState::Reading => {
745                                unreachable!(
746                                    "backfill stream must not end while the snapshot attempt is still reading"
747                                );
748                            }
749                        }
750                    };
751
752                    if actor_cdc_offset_high
753                        .as_ref()
754                        .is_none_or(|cur| cur < &split_cdc_offset_high)
755                    {
756                        actor_cdc_offset_high = Some(split_cdc_offset_high.clone());
757                    }
758
759                    // Mark current split backfill as finished. The state will be persisted by next barrier.
760                    state_impl
761                        .mutate_state(
762                            split.split_id,
763                            true,
764                            durable_row_count,
765                            split_cdc_offset_low.clone(),
766                            Some(split_cdc_offset_high),
767                        )
768                        .await?;
769
770                    extend_backfill_progress(
771                        &mut should_report_actor_backfill_progress,
772                        split.split_id,
773                    );
774                }
775
776                upstream_table_reader.disconnect().await?;
777            }
778            tracing::info!(
779                %table_id,
780                upstream_table_name,
781                "CdcBackfill has already finished and will forward messages directly to the downstream"
782            );
783
784            let mut should_report_actor_backfill_done = false;
785            // After backfill progress finished
786            // we can forward messages directly to the downstream,
787            // as backfill is finished.
788            #[for_await]
789            for msg in &mut upstream {
790                let msg = msg?;
791                match msg {
792                    Message::Barrier(barrier) => {
793                        state_impl.commit_state(barrier.epoch).await?;
794                        if is_reset_barrier(&barrier, self.actor_ctx.id) {
795                            next_reset_barrier = Some(barrier);
796                            continue 'with_cdc_table_snapshot_splits;
797                        }
798                        if let Some(split_range) = should_report_actor_backfill_progress.take()
799                            && let Some(ref progress) = self.progress
800                        {
801                            progress.update(
802                                self.actor_ctx.fragment_id,
803                                self.actor_ctx.id,
804                                barrier.epoch,
805                                generation.expect(
806                                    "should have set generation when having progress to report",
807                                ),
808                                split_range,
809                            );
810                        }
811                        if should_report_actor_backfill_done {
812                            should_report_actor_backfill_done = false;
813                            assert!(!actor_snapshot_splits.is_empty());
814                            if let Some(ref progress) = self.progress {
815                                progress.finish(
816                                    self.actor_ctx.fragment_id,
817                                    self.actor_ctx.id,
818                                    barrier.epoch,
819                                    generation.expect(
820                                        "should have set generation when having progress to report",
821                                    ),
822                                    (
823                                        actor_snapshot_splits[0].split_id,
824                                        actor_snapshot_splits[actor_snapshot_splits.len() - 1]
825                                            .split_id,
826                                    ),
827                                );
828                            }
829                        }
830                        yield Message::Barrier(barrier);
831                    }
832                    Message::Chunk(chunk) => {
833                        if actor_snapshot_splits.is_empty() || !chunk.has_visible_rows() {
834                            continue;
835                        }
836
837                        let chunk_cdc_offset =
838                            get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
839                        // // TODO(zw): re-enable
840                        // if *self.external_table.table_type() == ExternalCdcTableType::Postgres
841                        //     && let Some(cur) = actor_cdc_offset_low.as_ref()
842                        //     && let Some(ref chunk_offset) = chunk_cdc_offset
843                        //     && *chunk_offset < *cur
844                        // {
845                        //     continue;
846                        // }
847
848                        // should_report_actor_backfill_done is set to true at most once.
849                        if let Some(high) = actor_cdc_offset_high.as_ref() {
850                            if state_impl.is_legacy_state() {
851                                // Since the legacy state does not track CDC offsets, report backfill completion immediately.
852                                actor_cdc_offset_high = None;
853                                should_report_actor_backfill_done = true;
854                            } else if let Some(ref chunk_offset) = chunk_cdc_offset
855                                && *chunk_offset >= *high
856                            {
857                                // Report backfill completion once the latest CDC offset exceeds the highest offset tracked during the backfill.
858                                actor_cdc_offset_high = None;
859                                should_report_actor_backfill_done = true;
860                            }
861                        }
862                        let chunk = mapping_chunk(chunk, &self.output_indices);
863                        if let Some(filtered_chunk) = filter_stream_chunk(
864                            chunk,
865                            &current_actor_bounds,
866                            snapshot_split_column_in_output_index,
867                        ) {
868                            yield Message::Chunk(filtered_chunk);
869                        }
870                    }
871                    msg @ Message::Watermark(_) => {
872                        if let Some(msg) = mapping_message(msg, &self.output_indices) {
873                            yield msg;
874                        }
875                    }
876                }
877            }
878        }
879    }
880}
881
882/// Splits CDC rows into chunks to forward immediately or buffer for replay.
883/// Returns `None` for either result when it contains no rows.
884///
885/// Rows in the active split are always buffered because later snapshot rows may overwrite them.
886///
887/// Rows in inactive splits are routed according to their durable snapshot progress:
888/// - not started: discard, because a future snapshot will observe the latest state;
889/// - finished: forward, because no future snapshot will cover the change;
890/// - partially written: forward, because some rows may already exist downstream and, without a
891///   cursor, we cannot determine which side of the durable snapshot progress contains this row.
892fn route_cdc_chunk(
893    chunk: StreamChunk,
894    splits: &[CdcTableSnapshotSplit],
895    states: &[CdcStateRecord],
896    current_split_idx: usize,
897    snapshot_split_column_index: usize,
898) -> (Option<StreamChunk>, Option<StreamChunk>) {
899    let mut forwarded = BitmapBuilder::zeroed(chunk.capacity());
900    let mut buffered = BitmapBuilder::zeroed(chunk.capacity());
901
902    for (_, row) in chunk.rows() {
903        let split_key = row.datum_at(snapshot_split_column_index);
904
905        let Ok(split_idx) = splits.binary_search_by(|split| {
906            let right_bound_le_key = !is_rightmost_bound(&split.right_bound_exclusive)
907                && cmp_datum(
908                    split.right_bound_exclusive.datum_at(0),
909                    split_key,
910                    OrderType::ascending_nulls_first(),
911                )
912                .is_le();
913
914            let left_bound_gt_key = !is_leftmost_bound(&split.left_bound_inclusive)
915                && cmp_datum(
916                    split.left_bound_inclusive.datum_at(0),
917                    split_key,
918                    OrderType::ascending_nulls_first(),
919                )
920                .is_gt();
921
922            if right_bound_le_key {
923                // The split is entirely before the key, so search to the right.
924                std::cmp::Ordering::Less
925            } else if left_bound_gt_key {
926                // The split is entirely after the key, so search to the left.
927                std::cmp::Ordering::Greater
928            } else {
929                // The split contains the key: left <= key < right.
930                std::cmp::Ordering::Equal
931            }
932        }) else {
933            // The key is outside the assigned split ranges or lies in a gap.
934            continue;
935        };
936
937        // Buffer rows in the active split for replay after any later snapshot output.
938        if split_idx == current_split_idx {
939            buffered.set(row.index(), true);
940            continue;
941        }
942
943        let state = &states[split_idx];
944        // A split before the active split is finished by construction. Splits after it may also
945        // be finished or partially written after scaling, for example
946        // [finished, active, finished, partially written, not started].
947        let is_completed = split_idx < current_split_idx || state.is_finished;
948        let is_partially_completed = state.row_count > 0;
949        if is_completed || is_partially_completed {
950            forwarded.set(row.index(), true);
951        }
952
953        // Otherwise leave both bits unset. The split has no durable snapshot output, so its future
954        // full snapshot will cover the change.
955    }
956
957    let forwarded = forwarded.finish();
958    let forwarded_chunk = forwarded
959        .any()
960        .then(|| chunk.clone_with_vis(forwarded).compact_vis());
961
962    let buffered = buffered.finish();
963    let buffered_chunk = buffered
964        .any()
965        .then(|| chunk.clone_with_vis(buffered).compact_vis());
966
967    (forwarded_chunk, buffered_chunk)
968}
969
970fn extend_backfill_progress(progress: &mut Option<(i64, i64)>, split_id: i64) {
971    if let Some((_, right)) = progress {
972        assert!(*right < split_id);
973        *right = split_id;
974    } else {
975        *progress = Some((split_id, split_id));
976    }
977}
978
979/// Keep rows whose snapshot split-column value falls within `bound`'s half-open range
980/// `[left, right)`, preserving their operations and relative order through a visibility bitmap.
981/// Returns `None` when no bounds are supplied or no visible rows fall within the range.
982///
983/// For example, filtering split keys `[50, 100, 150, 200]` with bounds `[100, 200)` keeps
984/// `[100, 150]`: the left bound is inclusive and the right bound is exclusive.
985fn filter_stream_chunk(
986    chunk: StreamChunk,
987    bound: &Option<(OwnedRow, OwnedRow)>,
988    snapshot_split_column_index: usize,
989) -> Option<StreamChunk> {
990    let Some((left, right)) = bound else {
991        return None;
992    };
993    assert_eq!(left.len(), 1, "multiple split columns is not supported yet");
994    assert_eq!(
995        right.len(),
996        1,
997        "multiple split columns is not supported yet"
998    );
999    let left_split_key = left.datum_at(0);
1000    let right_split_key = right.datum_at(0);
1001    let is_leftmost_bound = is_leftmost_bound(left);
1002    let is_rightmost_bound = is_rightmost_bound(right);
1003    if is_leftmost_bound && is_rightmost_bound {
1004        return chunk.has_visible_rows().then_some(chunk);
1005    }
1006    let mut new_bitmap = BitmapBuilder::with_capacity(chunk.capacity());
1007    let (ops, columns, visibility) = chunk.into_inner();
1008    for (row_split_key, v) in columns[snapshot_split_column_index]
1009        .iter()
1010        .zip_eq_fast(visibility.iter())
1011    {
1012        if !v {
1013            new_bitmap.append(false);
1014            continue;
1015        }
1016        let mut is_in_range = true;
1017        if !is_leftmost_bound {
1018            is_in_range = cmp_datum(
1019                row_split_key,
1020                left_split_key,
1021                OrderType::ascending_nulls_first(),
1022            )
1023            .is_ge();
1024        }
1025        if is_in_range && !is_rightmost_bound {
1026            is_in_range = cmp_datum(
1027                row_split_key,
1028                right_split_key,
1029                OrderType::ascending_nulls_first(),
1030            )
1031            .is_lt();
1032        }
1033        if !is_in_range {
1034            tracing::trace!(?row_split_key, ?left_split_key, ?right_split_key, snapshot_split_column_index, data_type = ?columns[snapshot_split_column_index].data_type(), "filter out row")
1035        }
1036        new_bitmap.append(is_in_range);
1037    }
1038
1039    let visibility = new_bitmap.finish();
1040
1041    visibility
1042        .any()
1043        .then_some(StreamChunk::with_visibility(ops, columns, visibility))
1044}
1045
1046// has no left bound, e.g. [-inf, N)
1047fn is_leftmost_bound(row: &OwnedRow) -> bool {
1048    row.iter().all(|d| d.is_none())
1049}
1050
1051// has no right bound, e.g. [N, inf)
1052fn is_rightmost_bound(row: &OwnedRow) -> bool {
1053    row.iter().all(|d| d.is_none())
1054}
1055
1056impl<S: StateStore> Execute for ParallelizedCdcBackfillExecutor<S> {
1057    fn execute(self: Box<Self>) -> BoxedMessageStream {
1058        self.execute_inner().boxed()
1059    }
1060}
1061
1062fn extends_current_actor_bound(
1063    current: &mut Option<(OwnedRow, OwnedRow)>,
1064    split: &CdcTableSnapshotSplit,
1065) {
1066    if current.is_none() {
1067        *current = Some((
1068            split.left_bound_inclusive.clone(),
1069            split.right_bound_exclusive.clone(),
1070        ));
1071    } else {
1072        current.as_mut().unwrap().1 = split.right_bound_exclusive.clone();
1073    }
1074}
1075
1076fn is_reset_barrier(barrier: &Barrier, actor_id: ActorId) -> bool {
1077    match barrier.mutation.as_deref() {
1078        Some(Mutation::Update(update)) => update
1079            .actor_cdc_table_snapshot_splits
1080            .splits
1081            .contains_key(&actor_id),
1082        _ => false,
1083    }
1084}
1085
1086fn assert_consecutive_splits(actor_snapshot_splits: &[CdcTableSnapshotSplit]) {
1087    for i in 1..actor_snapshot_splits.len() {
1088        assert_eq!(
1089            actor_snapshot_splits[i].split_id,
1090            actor_snapshot_splits[i - 1].split_id + 1,
1091            "{:?}",
1092            actor_snapshot_splits
1093        );
1094        assert!(
1095            cmp_datum(
1096                actor_snapshot_splits[i - 1]
1097                    .right_bound_exclusive
1098                    .datum_at(0),
1099                actor_snapshot_splits[i].right_bound_exclusive.datum_at(0),
1100                OrderType::ascending_nulls_last(),
1101            )
1102            .is_lt()
1103        );
1104    }
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109    use std::collections::BTreeMap;
1110    use std::sync::Arc;
1111    use std::time::Duration;
1112
1113    use futures::StreamExt;
1114    use risingwave_common::array::StreamChunk;
1115    use risingwave_common::catalog::{
1116        CdcKeyComparison, ColumnDesc, ColumnId, Field, Schema, TableId,
1117    };
1118    use risingwave_common::row::{OwnedRow, Row};
1119    use risingwave_common::types::{DataType, ScalarImpl};
1120    use risingwave_common::util::epoch::{EpochExt, test_epoch};
1121    use risingwave_common::util::sort_util::OrderType;
1122    use risingwave_connector::source::CdcTableSnapshotSplitRaw;
1123    use risingwave_connector::source::cdc::external::{
1124        ExternalCdcTableType, ExternalTableConfig, SchemaTableName,
1125    };
1126    use risingwave_connector::source::cdc::{
1127        CdcScanOptions, CdcTableSnapshotSplitAssignmentWithGeneration,
1128    };
1129    use risingwave_storage::memory::MemoryStateStore;
1130
1131    use super::*;
1132    use crate::common::table::state_table::StateTable;
1133    use crate::common::table::test_utils::gen_pbtable;
1134    use crate::executor::monitor::StreamingMetrics;
1135    use crate::executor::test_utils::MockSource;
1136    use crate::executor::{AddMutation, Execute, Mutation};
1137
1138    async fn create_parallel_cdc_state_table(
1139        store: MemoryStateStore,
1140    ) -> StateTable<MemoryStateStore> {
1141        let state_schema = Schema::new(vec![
1142            Field::with_name(DataType::Int64, "split_id"),
1143            Field::with_name(DataType::Boolean, "backfill_finished"),
1144            Field::with_name(DataType::Int64, "row_count"),
1145            Field::with_name(DataType::Jsonb, "cdc_offset_low"),
1146            Field::with_name(DataType::Jsonb, "cdc_offset_high"),
1147        ]);
1148        let column_descs = state_schema
1149            .fields
1150            .iter()
1151            .enumerate()
1152            .map(|(idx, field)| {
1153                ColumnDesc::unnamed(ColumnId::new(idx as i32), field.data_type.clone())
1154            })
1155            .collect();
1156
1157        StateTable::from_table_catalog(
1158            &gen_pbtable(
1159                TableId::new(0x42),
1160                column_descs,
1161                vec![OrderType::ascending()],
1162                vec![0],
1163                0,
1164            ),
1165            store,
1166            None,
1167        )
1168        .await
1169    }
1170
1171    #[tokio::test(start_paused = true)]
1172    async fn test_rebuilds_reader_after_snapshot_error() {
1173        let (mut tx, source) = MockSource::channel();
1174
1175        let source = source.into_executor(
1176            Schema::new(vec![
1177                Field::unnamed(DataType::Jsonb),
1178                Field::unnamed(DataType::Varchar),
1179            ]),
1180            vec![0],
1181        );
1182        let external_table = ExternalStorageTable::new(
1183            TableId::new(1234),
1184            SchemaTableName {
1185                schema_name: "public".to_owned(),
1186                table_name: "mock_table".to_owned(),
1187            },
1188            "mydb".to_owned(),
1189            ExternalTableConfig::default(),
1190            ExternalCdcTableType::Mock,
1191            Schema::new(vec![
1192                Field::with_name(DataType::Int64, "id"),
1193                Field::with_name(DataType::Float64, "price"),
1194            ]),
1195            vec![OrderType::ascending()],
1196            Some(vec![CdcKeyComparison::Native]),
1197            vec![0],
1198        )
1199        .with_mock_snapshot_errors([1, 0]);
1200        let external_table_for_assertion = external_table.clone();
1201
1202        let state_table = create_parallel_cdc_state_table(MemoryStateStore::new()).await;
1203
1204        let actor_id = 0x1a.into();
1205        let mut executor = ParallelizedCdcBackfillExecutor::new(
1206            ActorContext::for_test(actor_id),
1207            external_table,
1208            source,
1209            vec![0, 1],
1210            vec![
1211                ColumnDesc::named("id", ColumnId::new(1), DataType::Int64),
1212                ColumnDesc::named("price", ColumnId::new(2), DataType::Float64),
1213            ],
1214            Arc::new(StreamingMetrics::unused()),
1215            state_table,
1216            Some(4),
1217            CdcScanOptions {
1218                backfill_parallelism: 1,
1219                backfill_num_rows_per_split: 100,
1220                ..Default::default()
1221            },
1222            BTreeMap::new(),
1223            None,
1224        )
1225        .boxed()
1226        .execute();
1227
1228        let mut curr_epoch = test_epoch(1);
1229        let snapshot_splits = [(
1230            actor_id,
1231            (
1232                vec![CdcTableSnapshotSplitRaw {
1233                    split_id: 1,
1234                    left_bound_inclusive: OwnedRow::new(vec![Some(ScalarImpl::Int64(1))])
1235                        .value_serialize(),
1236                    right_bound_exclusive: OwnedRow::new(vec![Some(ScalarImpl::Int64(10))])
1237                        .value_serialize(),
1238                }],
1239                10,
1240            ),
1241        )]
1242        .into_iter()
1243        .collect();
1244
1245        // Assign the split to this actor on the initial barrier.
1246        tx.send_barrier(
1247            Barrier::new_test_barrier(curr_epoch).with_mutation(Mutation::Add(AddMutation {
1248                actor_cdc_table_snapshot_splits: CdcTableSnapshotSplitAssignmentWithGeneration {
1249                    splits: snapshot_splits,
1250                },
1251                ..Default::default()
1252            })),
1253        );
1254
1255        assert!(matches!(
1256            executor.next().await.unwrap().unwrap(),
1257            Message::Barrier(Barrier { epoch, .. }) if epoch.curr == curr_epoch
1258        ));
1259
1260        // This poll creates reader 1 and starts the snapshot, consuming its injected error. The
1261        // executor then keeps polling the CDC upstream until a barrier provides a safe recovery
1262        // boundary.
1263        assert!(
1264            tokio::time::timeout(Duration::from_millis(50), executor.next())
1265                .await
1266                .is_err()
1267        );
1268        assert_eq!(external_table_for_assertion.mock_reader_create_count(), 1);
1269
1270        curr_epoch.inc_epoch();
1271        tx.push_barrier(curr_epoch, false);
1272        assert!(matches!(
1273            executor.next().await.unwrap().unwrap(),
1274            Message::Barrier(Barrier { epoch, .. }) if epoch.curr == curr_epoch
1275        ));
1276
1277        // Resuming after the barrier creates reader 2 and produces a snapshot chunk.
1278        assert!(matches!(
1279            executor.next().await.unwrap().unwrap(),
1280            Message::Chunk(_)
1281        ));
1282
1283        assert_eq!(external_table_for_assertion.mock_reader_create_count(), 2);
1284    }
1285
1286    #[test]
1287    fn test_filter_stream_chunk() {
1288        use risingwave_common::array::StreamChunkTestExt;
1289        let chunk = StreamChunk::from_pretty(
1290            "  I I
1291             + 1 6
1292             - 2 .
1293            U- 3 7
1294            U+ 4 .",
1295        );
1296        let bound = None;
1297        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1298        assert!(c.is_none());
1299
1300        let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
1301        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1302        assert_eq!(c.unwrap().compact_vis(), chunk);
1303
1304        let bound = Some((
1305            OwnedRow::new(vec![None]),
1306            OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
1307        ));
1308        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1309        assert_eq!(
1310            c.unwrap().compact_vis(),
1311            StreamChunk::from_pretty(
1312                "  I I
1313             + 1 6
1314             - 2 .",
1315            )
1316        );
1317
1318        let bound = Some((
1319            OwnedRow::new(vec![Some(ScalarImpl::Int64(3))]),
1320            OwnedRow::new(vec![None]),
1321        ));
1322        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1323        assert_eq!(
1324            c.unwrap().compact_vis(),
1325            StreamChunk::from_pretty(
1326                "  I I
1327            U- 3 7
1328            U+ 4 .",
1329            )
1330        );
1331
1332        let bound = Some((
1333            OwnedRow::new(vec![Some(ScalarImpl::Int64(2))]),
1334            OwnedRow::new(vec![Some(ScalarImpl::Int64(4))]),
1335        ));
1336        let c = filter_stream_chunk(chunk.clone(), &bound, 0);
1337        assert_eq!(
1338            c.unwrap().compact_vis(),
1339            StreamChunk::from_pretty(
1340                "  I I
1341             - 2 .
1342            U- 3 7",
1343            )
1344        );
1345
1346        // Test NULL value.
1347        let bound = None;
1348        let c = filter_stream_chunk(chunk.clone(), &bound, 1);
1349        assert!(c.is_none());
1350
1351        let bound = Some((OwnedRow::new(vec![None]), OwnedRow::new(vec![None])));
1352        let c = filter_stream_chunk(chunk.clone(), &bound, 1);
1353        assert_eq!(c.unwrap().compact_vis(), chunk);
1354
1355        let bound = Some((
1356            OwnedRow::new(vec![None]),
1357            OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
1358        ));
1359        let c = filter_stream_chunk(chunk.clone(), &bound, 1);
1360        assert_eq!(
1361            c.unwrap().compact_vis(),
1362            StreamChunk::from_pretty(
1363                "  I I
1364             + 1 6
1365             - 2 .
1366            U+ 4 .",
1367            )
1368        );
1369
1370        let bound = Some((
1371            OwnedRow::new(vec![Some(ScalarImpl::Int64(7))]),
1372            OwnedRow::new(vec![None]),
1373        ));
1374        let c = filter_stream_chunk(chunk, &bound, 1);
1375        assert_eq!(
1376            c.unwrap().compact_vis(),
1377            StreamChunk::from_pretty(
1378                "  I I
1379            U- 3 7",
1380            )
1381        );
1382    }
1383
1384    #[test]
1385    fn test_route_cdc_chunk() {
1386        use risingwave_common::array::StreamChunkTestExt;
1387
1388        let bound = |value: i64| OwnedRow::new(vec![Some(value.into())]);
1389        let splits = [(0, 100), (100, 200), (200, 300), (300, 400), (400, 500)]
1390            .into_iter()
1391            .enumerate()
1392            .map(|(idx, (left, right))| CdcTableSnapshotSplit {
1393                split_id: idx as i64,
1394                left_bound_inclusive: bound(left),
1395                right_bound_exclusive: bound(right),
1396            })
1397            .collect_vec();
1398
1399        // [finished, active, finished, partially written, not started]
1400        let states = [
1401            CdcStateRecord {
1402                is_finished: true,
1403                ..Default::default()
1404            },
1405            CdcStateRecord {
1406                row_count: 1,
1407                ..Default::default()
1408            },
1409            CdcStateRecord {
1410                is_finished: true,
1411                ..Default::default()
1412            },
1413            CdcStateRecord {
1414                row_count: 1,
1415                ..Default::default()
1416            },
1417            CdcStateRecord::default(),
1418        ];
1419
1420        // splits: [0, 100), [100, 200), [200, 300), [300, 400), [400, 500)
1421        // status: finished, active,       finished,     partial,      not started
1422        //
1423        // chunk       expected     split         status
1424        // + -1  1     dropped      outside       N/A
1425        // + 50  5     forwarded    [0, 100)      finished
1426        // + 150 15    buffered     [100, 200)    active
1427        // - 151 16    buffered     [100, 200)    active
1428        // - 250 25    forwarded    [200, 300)    finished
1429        // - 350 35    forwarded    [300, 400)    partially written
1430        // + 450 45    dropped      [400, 500)    not started
1431        // + 500 50    dropped      outside       N/A
1432        let chunk = StreamChunk::from_pretty(
1433            "  I I
1434             + -1 1
1435             + 50 5
1436             + 150 15
1437             - 151 16
1438             - 250 25
1439             - 350 35
1440             + 450 45
1441             + 500 50",
1442        );
1443        let (forwarded_chunk, buffered_chunk) = route_cdc_chunk(chunk, &splits, &states, 1, 0);
1444
1445        assert_eq!(
1446            forwarded_chunk.unwrap(),
1447            StreamChunk::from_pretty(
1448                "  I I
1449                 + 50 5
1450                 - 250 25
1451                 - 350 35",
1452            )
1453        );
1454
1455        assert_eq!(
1456            buffered_chunk.unwrap(),
1457            StreamChunk::from_pretty(
1458                "  I I
1459                 + 150 15
1460                 - 151 16",
1461            )
1462        );
1463    }
1464}