Skip to main content

risingwave_stream/executor/backfill/cdc/
cdc_backfill.rs

1// Copyright 2023 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;
16use std::pin::Pin;
17
18use either::Either;
19use futures::stream;
20use futures::stream::select_with_strategy;
21use itertools::Itertools;
22use risingwave_common::array::{DataChunk, Op};
23use risingwave_common::bail;
24use risingwave_common::bitmap::BitmapBuilder;
25use risingwave_common::catalog::{CdcKeyComparison, ColumnDesc};
26use risingwave_common::row::RowExt;
27use risingwave_common::util::sort_util::OrderType;
28use risingwave_connector::parser::{
29    BigintUnsignedHandlingMode, ByteStreamSourceParser, DebeziumParser, DebeziumProps,
30    EncodingProperties, JsonProperties, ProtocolProperties, SourceStreamChunkBuilder,
31    SpecificParserConfig, TimeHandling, TimestampHandling, TimestamptzHandling,
32};
33use risingwave_connector::source::cdc::CdcScanOptions;
34use risingwave_connector::source::cdc::external::{
35    CdcOffset, CdcOffsetParseFunc, ExternalCdcTableType, ExternalTableReaderImpl,
36};
37use risingwave_connector::source::{SourceColumnDesc, SourceContext, SourceCtrlOpts};
38use rw_futures_util::pausable;
39use thiserror_ext::AsReport;
40
41use crate::executor::backfill::cdc::state::CdcBackfillState;
42use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
43use crate::executor::backfill::cdc::upstream_table::reader::{
44    build_reader_and_poll_upstream, create_table_reader_with_retry,
45};
46use crate::executor::backfill::cdc::upstream_table::snapshot::{
47    SnapshotReadArgs, UpstreamTableRead, UpstreamTableReader,
48};
49use crate::executor::backfill::utils::{
50    cmp_pk_unsigned_aware, get_cdc_chunk_last_offset, get_new_pos, mapping_chunk, mapping_message,
51    mark_cdc_chunk,
52};
53use crate::executor::monitor::CdcBackfillMetrics;
54use crate::executor::prelude::*;
55use crate::executor::{Mutation, TableId};
56use crate::task::CreateMviewProgressReporter;
57
58/// `split_id`, `is_finished`, `row_count`, `cdc_offset` all occupy 1 column each.
59const METADATA_STATE_LEN: usize = 4;
60
61struct PkCompareInfo<'a> {
62    indices: &'a [usize],
63    order: &'a [OrderType],
64    needs_unsigned_i64_compare: &'a [bool],
65}
66
67fn can_poll_upstream_while_creating_reader(
68    current_pk_pos: Option<&OwnedRow>,
69    pk_comparisons_are_known: bool,
70) -> bool {
71    current_pk_pos.is_none() || pk_comparisons_are_known
72}
73
74/// Result of handling an upstream message while no snapshot is running.
75enum NoSnapshotUpstreamOutcome {
76    /// Forward the message downstream, if any, and keep going.
77    /// `None` means nothing to emit: a watermark, or a chunk with no visible rows to emit.
78    Continue(Option<Message>),
79    /// The actor is stopped: forward the barrier and exit the executor.
80    Stop(Barrier),
81}
82
83/// Snapshot cursor together with the PK comparisons needed to filter upstream changes
84/// against it.
85#[derive(Clone, Copy)]
86struct SnapshotCursor<'a> {
87    pk_pos: &'a OwnedRow,
88    pk_needs_unsigned_i64_compare: &'a [bool],
89}
90
91struct NoSnapshotUpstreamHandler<'a, S: StateStore> {
92    actor_ctx: &'a ActorContextRef,
93    metrics: &'a CdcBackfillMetrics,
94    table_id: TableId,
95    upstream_table_name: &'a str,
96    offset_parse_func: &'a mut CdcOffsetParseFunc,
97    output_indices: &'a [usize],
98    pk_indices: &'a [usize],
99    pk_order: &'a [OrderType],
100    /// `None` means the next snapshot starts from the beginning.
101    cursor: Option<SnapshotCursor<'a>>,
102    total_snapshot_row_count: u64,
103    state_impl: &'a mut CdcBackfillState<S>,
104    last_binlog_offset: &'a mut Option<CdcOffset>,
105    is_snapshot_paused: &'a mut bool,
106    rate_limit_rps: &'a mut Option<u32>,
107}
108
109impl<S: StateStore> NoSnapshotUpstreamHandler<'_, S> {
110    async fn handle_upstream(
111        &mut self,
112        msg: Message,
113    ) -> StreamExecutorResult<NoSnapshotUpstreamOutcome> {
114        match msg {
115            Message::Barrier(barrier) => {
116                if let Some(mutation) = barrier.mutation.as_deref() {
117                    match mutation {
118                        Mutation::Pause => {
119                            *self.is_snapshot_paused = true;
120                            tracing::info!(
121                                table_id = %self.table_id,
122                                upstream_table_name = self.upstream_table_name,
123                                "snapshot is paused by barrier"
124                            );
125                        }
126                        Mutation::Resume => {
127                            *self.is_snapshot_paused = false;
128                            tracing::info!(
129                                table_id = %self.table_id,
130                                upstream_table_name = self.upstream_table_name,
131                                "snapshot is resumed by barrier"
132                            );
133                        }
134                        Mutation::Throttle(_) => {
135                            if let Some(entry) =
136                                mutation.backfill_throttle_config(self.actor_ctx.fragment_id)
137                            {
138                                *self.rate_limit_rps = entry.rate_limit;
139                            }
140                        }
141                        mutation if mutation.is_stop(self.actor_ctx.id) => {
142                            tracing::info!(
143                                table_id = %self.table_id,
144                                upstream_table_name = self.upstream_table_name,
145                                "CdcBackfill has been dropped due to config change"
146                            );
147                            self.state_impl.commit_state(barrier.epoch).await?;
148
149                            return Ok(NoSnapshotUpstreamOutcome::Stop(barrier));
150                        }
151                        _ => (),
152                    }
153                }
154
155                // Commit after all preceding upstream chunks have been emitted.
156                self.state_impl
157                    .mutate_and_commit_state(
158                        self.cursor.map(|cursor| cursor.pk_pos.clone()),
159                        self.last_binlog_offset.clone(),
160                        self.total_snapshot_row_count,
161                        false,
162                        barrier.epoch,
163                    )
164                    .await?;
165
166                Ok(NoSnapshotUpstreamOutcome::Continue(Some(Message::Barrier(
167                    barrier,
168                ))))
169            }
170            Message::Chunk(chunk) => {
171                let Some(cursor) = self.cursor else {
172                    // The next snapshot starts from the beginning and covers these changes.
173                    return Ok(NoSnapshotUpstreamOutcome::Continue(None));
174                };
175
176                if !chunk.has_visible_rows() {
177                    return Ok(NoSnapshotUpstreamOutcome::Continue(None));
178                }
179
180                let (chunk, consumed_offset) = CdcBackfillExecutor::<S>::filter_recovery_chunk(
181                    self.offset_parse_func,
182                    chunk,
183                    cursor.pk_pos,
184                    PkCompareInfo {
185                        indices: self.pk_indices,
186                        order: self.pk_order,
187                        needs_unsigned_i64_compare: cursor.pk_needs_unsigned_i64_compare,
188                    },
189                    self.last_binlog_offset,
190                    self.output_indices,
191                )?;
192
193                if let Some(consumed_offset) = consumed_offset {
194                    *self.last_binlog_offset = Some(consumed_offset);
195                }
196
197                let chunk = chunk.map(|chunk| {
198                    CdcBackfillExecutor::<S>::report_metrics(
199                        self.metrics,
200                        0,
201                        chunk.cardinality() as u64,
202                    );
203
204                    Message::Chunk(chunk)
205                });
206
207                Ok(NoSnapshotUpstreamOutcome::Continue(chunk))
208            }
209            Message::Watermark(_) => Ok(NoSnapshotUpstreamOutcome::Continue(None)),
210        }
211    }
212
213    async fn wait_for_reader(
214        &mut self,
215        upstream: &mut (impl Stream<Item = StreamExecutorResult<Message>> + Unpin),
216        future: &mut Pin<Box<impl Future<Output = ExternalTableReaderImpl>>>,
217    ) -> StreamExecutorResult<Either<NoSnapshotUpstreamOutcome, ExternalTableReaderImpl>> {
218        match build_reader_and_poll_upstream(upstream, future).await? {
219            Either::Left(msg) => Ok(Either::Left(self.handle_upstream(msg).await?)),
220            Either::Right(table_reader) => Ok(Either::Right(table_reader)),
221        }
222    }
223}
224
225// The TimestampHandling/TimestamptzHandling/TimeHandling parser's behavior depends on the debezium.time.precision.mode setting:
226// - If left unset, Debezium defaults to time.precision.mode=microseconds (per debezium.properties), and the parser uses Micro.
227// - If set to "connect", Debezium uses time.precision.mode=connect, and the parser uses Milli by design.
228// - If set to any other value, Debezium applies that specific value, and the default parser is used to maintain backward compatibility.
229pub(crate) fn get_cdc_json_parse_handling_from_properties(
230    properties: &BTreeMap<String, String>,
231) -> (
232    Option<TimestampHandling>,
233    Option<TimestamptzHandling>,
234    Option<TimeHandling>,
235    Option<BigintUnsignedHandlingMode>,
236) {
237    let (timestamp_handling, timestamptz_handling, time_handling) = match properties
238        .get("debezium.time.precision.mode")
239    {
240        None => (
241            Some(TimestampHandling::Micro),
242            Some(TimestamptzHandling::Micro),
243            Some(TimeHandling::Micro),
244        ),
245        Some(m) if m == "connect" => (
246            Some(TimestampHandling::Milli),
247            Some(TimestamptzHandling::Milli),
248            Some(TimeHandling::Milli),
249        ),
250        Some(other) => {
251            // backward compatibility.
252            tracing::warn!(
253                "Unsupported debezium.time.precision.mode = {other}, fall back to default parser."
254            );
255            (None, None, None)
256        }
257    };
258    let bigint_unsigned_handling = properties
259        .get("debezium.bigint.unsigned.handling.mode")
260        .is_some_and(|v| v == "precise")
261        .then_some(BigintUnsignedHandlingMode::Precise);
262    (
263        timestamp_handling,
264        timestamptz_handling,
265        time_handling,
266        bigint_unsigned_handling,
267    )
268}
269
270pub struct CdcBackfillExecutor<S: StateStore> {
271    actor_ctx: ActorContextRef,
272
273    /// The external table to be backfilled
274    external_table: ExternalStorageTable,
275
276    /// Upstream changelog stream which may contain metadata columns, e.g. `_rw_offset`
277    upstream: Executor,
278
279    /// The column indices need to be forwarded to the downstream from the upstream and table scan.
280    output_indices: Vec<usize>,
281
282    /// The schema of output chunk, including additional columns if any
283    output_columns: Vec<ColumnDesc>,
284
285    /// State table of the `CdcBackfill` executor
286    state_impl: CdcBackfillState<S>,
287
288    // TODO: introduce a CdcBackfillProgress to report finish to Meta
289    // This object is just a stub right now
290    progress: Option<CreateMviewProgressReporter>,
291
292    metrics: CdcBackfillMetrics,
293
294    /// Rate limit in rows/s.
295    rate_limit_rps: Option<u32>,
296
297    options: CdcScanOptions,
298
299    properties: BTreeMap<String, String>,
300}
301
302impl<S: StateStore> CdcBackfillExecutor<S> {
303    #[expect(clippy::too_many_arguments)]
304    pub fn new(
305        actor_ctx: ActorContextRef,
306        external_table: ExternalStorageTable,
307        upstream: Executor,
308        output_indices: Vec<usize>,
309        output_columns: Vec<ColumnDesc>,
310        progress: Option<CreateMviewProgressReporter>,
311        metrics: Arc<StreamingMetrics>,
312        state_table: StateTable<S>,
313        rate_limit_rps: Option<u32>,
314        options: CdcScanOptions,
315        properties: BTreeMap<String, String>,
316    ) -> Self {
317        let pk_indices = external_table.pk_indices();
318        let upstream_table_id = external_table.table_id();
319        let state_impl = CdcBackfillState::new(
320            upstream_table_id,
321            state_table,
322            pk_indices.len() + METADATA_STATE_LEN,
323        );
324
325        let metrics = metrics.new_cdc_backfill_metrics(external_table.table_id(), actor_ctx.id);
326        Self {
327            actor_ctx,
328            external_table,
329            upstream,
330            output_indices,
331            output_columns,
332            state_impl,
333            progress,
334            metrics,
335            rate_limit_rps,
336            options,
337            properties,
338        }
339    }
340
341    fn report_metrics(
342        metrics: &CdcBackfillMetrics,
343        snapshot_processed_row_count: u64,
344        upstream_processed_row_count: u64,
345    ) {
346        metrics
347            .cdc_backfill_snapshot_read_row_count
348            .inc_by(snapshot_processed_row_count);
349
350        metrics
351            .cdc_backfill_upstream_output_row_count
352            .inc_by(upstream_processed_row_count);
353    }
354
355    /// Partitions buffered CDC rows into emitted and retained chunk views.
356    ///
357    /// Rows in the active binlog range with `PK <= current_pk_pos` are visible in
358    /// the emitted chunks. Rows with `PK > current_pk_pos` are visible in the
359    /// retained buffer.
360    fn consume_upstream_chunk_buffer(
361        offset_parse_func: &CdcOffsetParseFunc,
362        upstream_chunk_buffer: &mut Vec<StreamChunk>,
363        current_pk_pos: Option<&OwnedRow>,
364        pk_compare: PkCompareInfo<'_>,
365        last_binlog_offset: &Option<CdcOffset>,
366        output_indices: &[usize],
367    ) -> StreamExecutorResult<(Vec<StreamChunk>, u64, Option<CdcOffset>)> {
368        let Some(current_pos) = current_pk_pos else {
369            return Ok((vec![], 0, None));
370        };
371
372        let buffered_chunks = std::mem::take(upstream_chunk_buffer);
373        let mut emitted_chunks = Vec::with_capacity(buffered_chunks.len());
374        let mut upstream_processed_row_count = 0;
375        let mut consumed_binlog_offset = None;
376        let mut retained_chunks = Vec::with_capacity(buffered_chunks.len());
377        let mut can_advance_consumed_offset = true;
378
379        for chunk in buffered_chunks {
380            let mut emitted_vis = BitmapBuilder::zeroed(chunk.capacity());
381            let mut retained_vis = BitmapBuilder::zeroed(chunk.capacity());
382            for idx in 0..chunk.capacity() {
383                let (op, row, visible) = chunk.row_at(idx);
384                if !visible {
385                    // Buffered chunks may carry sparse visibility from previous filtering rounds.
386                    continue;
387                }
388                let event_offset = (*offset_parse_func)(
389                    row.iter()
390                        .last()
391                        .flatten()
392                        .expect("cdc offset must exist")
393                        .into_utf8(),
394                )?;
395                let in_binlog_range = last_binlog_offset
396                    .as_ref()
397                    .is_none_or(|binlog_low| *binlog_low <= event_offset);
398
399                let row_pk = row.project(pk_compare.indices);
400                let reached_current_pos = cmp_pk_unsigned_aware(
401                    row_pk.iter(),
402                    current_pos.iter(),
403                    pk_compare.order,
404                    pk_compare.needs_unsigned_i64_compare,
405                )
406                .is_le();
407                if !in_binlog_range {
408                    continue;
409                }
410                let should_emit = reached_current_pos;
411
412                match op {
413                    Op::Insert | Op::Delete => {
414                        if should_emit {
415                            emitted_vis.set(idx, true);
416                            if can_advance_consumed_offset {
417                                consumed_binlog_offset = Some(event_offset);
418                            }
419                            upstream_processed_row_count += 1;
420                        } else {
421                            retained_vis.set(idx, true);
422                            can_advance_consumed_offset = false;
423                        }
424                    }
425                    Op::UpdateDelete | Op::UpdateInsert => {
426                        unreachable!("CDC buffered chunks should not contain update pairs")
427                    }
428                }
429            }
430
431            let emitted_vis = emitted_vis.finish();
432            if emitted_vis.count_ones() > 0 {
433                emitted_chunks.push(mapping_chunk(
434                    chunk.clone_with_vis(emitted_vis),
435                    output_indices,
436                ));
437            }
438
439            let retained_vis = retained_vis.finish();
440            if retained_vis.count_ones() > 0 {
441                retained_chunks.push(chunk.clone_with_vis(retained_vis));
442            }
443        }
444
445        *upstream_chunk_buffer = retained_chunks;
446
447        Ok((
448            emitted_chunks,
449            upstream_processed_row_count,
450            consumed_binlog_offset,
451        ))
452    }
453
454    /// Returned chunk has rows safe to send downstream marked as visible, unsafe rows marked invisible.
455    ///
456    /// Rows are safe to send downstream if in binlog range and `PK <= current_pk_pos`.
457    fn filter_recovery_chunk(
458        offset_parse_func: &CdcOffsetParseFunc,
459        chunk: StreamChunk,
460        current_pos: &OwnedRow,
461        pk_compare: PkCompareInfo<'_>,
462        last_binlog_offset: &Option<CdcOffset>,
463        output_indices: &[usize],
464    ) -> StreamExecutorResult<(Option<StreamChunk>, Option<CdcOffset>)> {
465        let chunk_offset = get_cdc_chunk_last_offset(offset_parse_func, &chunk)?;
466        let chunk = mapping_chunk(
467            mark_cdc_chunk(
468                offset_parse_func,
469                chunk,
470                current_pos,
471                pk_compare.indices,
472                pk_compare.order,
473                pk_compare.needs_unsigned_i64_compare,
474                last_binlog_offset.clone(),
475            )?,
476            output_indices,
477        );
478        let chunk = (chunk.cardinality() > 0).then_some(chunk);
479        let consumed_offset = chunk_offset.filter(|chunk_offset| {
480            last_binlog_offset
481                .as_ref()
482                .is_none_or(|last| *last < *chunk_offset)
483        });
484        Ok((chunk, consumed_offset))
485    }
486
487    #[try_stream(ok = Message, error = StreamExecutorError)]
488    async fn execute_inner(mut self) {
489        // The indices to primary key columns
490        let pk_indices = self.external_table.pk_indices().to_vec();
491        let pk_order = self.external_table.pk_order_types().to_vec();
492        let pk_needs_unsigned_i64_compare =
493            self.external_table.pk_comparisons().map(|comparisons| {
494                comparisons
495                    .iter()
496                    .map(|comparison| *comparison == CdcKeyComparison::UnsignedInt64)
497                    .collect_vec()
498            });
499
500        let table_id = self.external_table.table_id();
501        let upstream_table_name = self.external_table.qualified_table_name();
502        let schema_table_name = self.external_table.schema_table_name().clone();
503        let external_database_name = self.external_table.database_name().to_owned();
504
505        let additional_columns = self
506            .output_columns
507            .iter()
508            .filter(|col| col.additional_column.column_type.is_some())
509            .cloned()
510            .collect_vec();
511
512        let mut upstream = self.upstream.execute();
513
514        // Current position of the upstream_table storage primary key.
515        // `None` means it starts from the beginning.
516        let mut current_pk_pos: Option<OwnedRow>;
517
518        // Poll the upstream to get the first barrier.
519        let first_barrier = expect_first_barrier(&mut upstream).await?;
520
521        let mut is_snapshot_paused = first_barrier.is_pause_on_startup();
522        let first_barrier_epoch = first_barrier.epoch;
523        // The first barrier message should be propagated.
524        yield Message::Barrier(first_barrier);
525
526        let mut state_impl = self.state_impl;
527
528        state_impl.init_epoch(first_barrier_epoch).await?;
529
530        // restore backfill state
531        let state = state_impl.restore_state().await?;
532        current_pk_pos = state.current_pk_pos.clone();
533
534        let need_backfill = !self.options.disable_backfill && !state.is_finished;
535
536        // Keep track of rows from the snapshot.
537        let mut total_snapshot_row_count = state.row_count as u64;
538
539        let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
540            get_cdc_json_parse_handling_from_properties(&self.properties);
541        // Only postgres-cdc connector may trigger TOAST.
542        let handle_toast_columns: bool =
543            self.external_table.table_type() == &ExternalCdcTableType::Postgres;
544        // Make sure to use mapping_message after transform_upstream.
545        let mut upstream = transform_upstream(
546            upstream,
547            self.output_columns.clone(),
548            timestamp_handling,
549            timestamptz_handling,
550            time_handling,
551            bigint_unsigned_handling,
552            handle_toast_columns,
553        )
554        .boxed()
555        .peekable();
556        let mut last_binlog_offset = state.last_cdc_offset.clone();
557
558        // CDC Backfill Algorithm:
559        //
560        // When the first barrier comes from upstream:
561        //  - read the current binlog offset as `binlog_low`
562        //  - start a snapshot read upon upstream table and iterate over the snapshot read stream
563        //  - buffer the changelog event from upstream
564        //
565        // When a new barrier comes from upstream:
566        //  - read the current binlog offset as `binlog_high`
567        //  - for each row of the upstream change log, forward it to downstream if it in the range
568        //    of [binlog_low, binlog_high] and its pk <= `current_pos`, otherwise keep buffering it
569        //    until `current_pos` catches up
570        //  - reconstruct the whole backfill stream with upstream changelog and a new table snapshot
571        //
572        // When a chunk comes from snapshot, we forward it to the downstream and raise
573        // `current_pos`.
574        // When we reach the end of the snapshot read stream, it means backfill has been
575        // finished.
576        //
577        // Once the backfill loop ends, we forward the upstream directly to the downstream.
578        if need_backfill {
579            // After init the state table and forward the initial barrier to downstream,
580            // we now try to create the table reader with retry.
581            //
582            // A fresh backfill can ignore CDC events here because its snapshot starts from the
583            // beginning. Recovery preserves events for the already-scanned prefix while reader
584            // creation is retried. Legacy MySQL graphs with Int64 PK columns must first recover
585            // comparison metadata from the reader so events use unsigned-aware ordering if needed.
586            let mut offset_parse_func = self.external_table.table_type().get_cdc_offset_parser()?;
587            let mut future = Box::pin(create_table_reader_with_retry(
588                self.external_table.clone(),
589                self.actor_ctx.id,
590                self.actor_ctx.fragment_id,
591            ));
592            let table_reader = if can_poll_upstream_while_creating_reader(
593                current_pk_pos.as_ref(),
594                pk_needs_unsigned_i64_compare.is_some(),
595            ) {
596                let mut handler = NoSnapshotUpstreamHandler {
597                    actor_ctx: &self.actor_ctx,
598                    metrics: &self.metrics,
599                    table_id,
600                    upstream_table_name: &upstream_table_name,
601                    offset_parse_func: &mut offset_parse_func,
602                    output_indices: &self.output_indices,
603                    pk_indices: &pk_indices,
604                    pk_order: &pk_order,
605                    cursor: current_pk_pos.as_ref().map(|pk_pos| SnapshotCursor {
606                        pk_pos,
607                        pk_needs_unsigned_i64_compare: pk_needs_unsigned_i64_compare
608                            .as_deref()
609                            .expect("checked by can_poll_upstream_while_creating_reader"),
610                    }),
611                    total_snapshot_row_count,
612                    state_impl: &mut state_impl,
613                    last_binlog_offset: &mut last_binlog_offset,
614                    is_snapshot_paused: &mut is_snapshot_paused,
615                    rate_limit_rps: &mut self.rate_limit_rps,
616                };
617                loop {
618                    match handler.wait_for_reader(&mut upstream, &mut future).await? {
619                        Either::Left(NoSnapshotUpstreamOutcome::Continue(msg)) => {
620                            if let Some(msg) = msg {
621                                yield msg;
622                            }
623                        }
624                        Either::Left(NoSnapshotUpstreamOutcome::Stop(barrier)) => {
625                            yield Message::Barrier(barrier);
626
627                            return Ok(());
628                        }
629                        Either::Right(table_reader) => break table_reader,
630                    }
631                }
632            } else {
633                tracing::warn!(
634                    %table_id,
635                    upstream_table_name,
636                    "waiting for the CDC table reader to recover legacy MySQL primary-key comparison metadata; checkpoint progress is paused"
637                );
638                future.as_mut().await
639            };
640            let pk_names = self.external_table.pk_names();
641            let pk_needs_unsigned_i64_compare = match pk_needs_unsigned_i64_compare {
642                Some(comparisons) => comparisons,
643                None => {
644                    let comparisons = table_reader.pk_column_comparisons(&pk_names)?;
645                    assert_eq!(comparisons.len(), pk_indices.len());
646                    comparisons
647                        .into_iter()
648                        .map(|comparison| comparison == CdcKeyComparison::UnsignedInt64)
649                        .collect_vec()
650                }
651            };
652            tracing::info!(
653                %table_id,
654                upstream_table_name,
655                "table reader created successfully"
656            );
657
658            let mut upstream_table_reader =
659                UpstreamTableReader::new(self.external_table.clone(), table_reader);
660
661            if last_binlog_offset.is_none() {
662                // Limit concurrent CDC connections globally to 10 using a semaphore.
663                static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
664                    tokio::sync::Semaphore::const_new(10);
665
666                let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
667                last_binlog_offset = upstream_table_reader.current_cdc_offset().await?;
668            }
669
670            tracing::info!(
671                %table_id,
672                upstream_table_name,
673                initial_binlog_offset = ?last_binlog_offset,
674                ?current_pk_pos,
675                is_finished = state.is_finished,
676                is_snapshot_paused,
677                snapshot_row_count = total_snapshot_row_count,
678                rate_limit = self.rate_limit_rps,
679                disable_backfill = self.options.disable_backfill,
680                snapshot_barrier_interval = self.options.snapshot_barrier_interval,
681                snapshot_batch_size = self.options.snapshot_batch_size,
682                "start cdc backfill",
683            );
684
685            // drive the upstream changelog first to ensure we can receive timely changelog event,
686            // otherwise the upstream changelog may be blocked by the snapshot read stream
687            let _ = Pin::new(&mut upstream).peek().await;
688
689            // wait for a barrier to make sure the backfill starts after upstream source
690            let mut handler = NoSnapshotUpstreamHandler {
691                actor_ctx: &self.actor_ctx,
692                metrics: &self.metrics,
693                table_id,
694                upstream_table_name: &upstream_table_name,
695                offset_parse_func: &mut offset_parse_func,
696                output_indices: &self.output_indices,
697                pk_indices: &pk_indices,
698                pk_order: &pk_order,
699                cursor: current_pk_pos.as_ref().map(|pk_pos| SnapshotCursor {
700                    pk_pos,
701                    pk_needs_unsigned_i64_compare: &pk_needs_unsigned_i64_compare,
702                }),
703                total_snapshot_row_count,
704                state_impl: &mut state_impl,
705                last_binlog_offset: &mut last_binlog_offset,
706                is_snapshot_paused: &mut is_snapshot_paused,
707                rate_limit_rps: &mut self.rate_limit_rps,
708            };
709            #[for_await]
710            for msg in upstream.by_ref() {
711                match msg? {
712                    Message::Chunk(chunk) if handler.cursor.is_none() => {
713                        // Without a snapshot cursor, the snapshot covers these changes, but the
714                        // binlog offset still advances past them.
715                        if chunk.has_visible_rows()
716                            && let Some(chunk_offset) =
717                                get_cdc_chunk_last_offset(handler.offset_parse_func, &chunk)?
718                            && handler
719                                .last_binlog_offset
720                                .as_ref()
721                                .is_none_or(|last| *last < chunk_offset)
722                        {
723                            *handler.last_binlog_offset = Some(chunk_offset);
724                        }
725                    }
726                    msg => match handler.handle_upstream(msg).await? {
727                        NoSnapshotUpstreamOutcome::Continue(Some(msg)) => {
728                            let is_barrier = matches!(msg, Message::Barrier(_));
729
730                            yield msg;
731
732                            if is_barrier {
733                                break;
734                            }
735                        }
736                        NoSnapshotUpstreamOutcome::Continue(None) => {}
737                        NoSnapshotUpstreamOutcome::Stop(barrier) => {
738                            yield Message::Barrier(barrier);
739
740                            return Ok(());
741                        }
742                    },
743                }
744            }
745
746            tracing::info!(%table_id,
747                upstream_table_name,
748                initial_binlog_offset = ?last_binlog_offset,
749                ?current_pk_pos,
750                is_snapshot_paused,
751                "start cdc backfill loop");
752
753            // the buffer will be drained when a barrier comes
754            let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
755            let mut needs_reader_rebuild = false;
756
757            'backfill_loop: loop {
758                if needs_reader_rebuild {
759                    needs_reader_rebuild = false;
760                    tracing::info!(
761                        %table_id,
762                        upstream_table_name,
763                        "rebuilding CDC table reader after snapshot read failure"
764                    );
765
766                    if let Err(error) = upstream_table_reader.disconnect().await {
767                        tracing::warn!(
768                            error = %error.as_report(),
769                            %table_id,
770                            upstream_table_name,
771                            "failed to disconnect CDC snapshot reader; continuing with rebuild"
772                        );
773                    }
774
775                    let mut future = Box::pin(create_table_reader_with_retry(
776                        self.external_table.clone(),
777                        self.actor_ctx.id,
778                        self.actor_ctx.fragment_id,
779                    ));
780
781                    let mut handler = NoSnapshotUpstreamHandler {
782                        actor_ctx: &self.actor_ctx,
783                        metrics: &self.metrics,
784                        table_id,
785                        upstream_table_name: &upstream_table_name,
786                        offset_parse_func: &mut offset_parse_func,
787                        output_indices: &self.output_indices,
788                        pk_indices: &pk_indices,
789                        pk_order: &pk_order,
790                        cursor: current_pk_pos.as_ref().map(|pk_pos| SnapshotCursor {
791                            pk_pos,
792                            pk_needs_unsigned_i64_compare: &pk_needs_unsigned_i64_compare,
793                        }),
794                        total_snapshot_row_count,
795                        state_impl: &mut state_impl,
796                        last_binlog_offset: &mut last_binlog_offset,
797                        is_snapshot_paused: &mut is_snapshot_paused,
798                        rate_limit_rps: &mut self.rate_limit_rps,
799                    };
800                    let table_reader = loop {
801                        match handler.wait_for_reader(&mut upstream, &mut future).await? {
802                            Either::Left(NoSnapshotUpstreamOutcome::Continue(msg)) => {
803                                if let Some(msg) = msg {
804                                    yield msg;
805                                }
806                            }
807                            Either::Left(NoSnapshotUpstreamOutcome::Stop(barrier)) => {
808                                yield Message::Barrier(barrier);
809
810                                return Ok(());
811                            }
812                            Either::Right(table_reader) => break table_reader,
813                        }
814                    };
815
816                    upstream_table_reader =
817                        UpstreamTableReader::new(self.external_table.clone(), table_reader);
818                    tracing::info!(
819                        %table_id,
820                        upstream_table_name,
821                        "CDC table reader rebuilt successfully"
822                    );
823                }
824
825                let mut should_bypass_snapshot_stream_patch =
826                    self.rate_limit_rps.is_some_and(|val| val == 0);
827                let left_upstream = upstream.by_ref().map(Either::Left);
828
829                let mut snapshot_read_row_cnt: usize = 0;
830                let read_args = SnapshotReadArgs::new(
831                    current_pk_pos.clone(),
832                    self.rate_limit_rps,
833                    pk_indices.clone(),
834                    additional_columns.clone(),
835                    schema_table_name.clone(),
836                    external_database_name.clone(),
837                );
838                let right_snapshot = pin!(
839                    upstream_table_reader
840                        .snapshot_read_full_table(read_args, self.options.snapshot_batch_size)
841                        .map(Either::Right)
842                );
843                let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
844                if is_snapshot_paused {
845                    snapshot_valve.pause();
846                }
847
848                // Prefer to select upstream, so we can stop snapshot stream when barrier comes.
849                let mut backfill_stream =
850                    select_with_strategy(left_upstream, right_snapshot, |_: &mut ()| {
851                        stream::PollNext::Left
852                    });
853
854                let mut cur_barrier_snapshot_processed_rows: u64 = 0;
855                let mut cur_barrier_upstream_processed_rows: u64 = 0;
856                let mut barrier_count: u32 = 0;
857                let mut pending_barrier = None;
858
859                #[for_await]
860                for either in &mut backfill_stream {
861                    match either {
862                        // Upstream
863                        Either::Left(msg) => {
864                            match msg? {
865                                Message::Barrier(barrier) => {
866                                    // increase the barrier count and check whether need to start a new snapshot
867                                    barrier_count += 1;
868                                    let mut needs_rebuild_snapshot =
869                                        barrier_count == self.options.snapshot_barrier_interval;
870
871                                    if let Some(mutation) = barrier.mutation.as_deref() {
872                                        match mutation {
873                                            Mutation::Pause => {
874                                                is_snapshot_paused = true;
875                                                snapshot_valve.pause();
876                                            }
877                                            Mutation::Resume => {
878                                                is_snapshot_paused = false;
879                                                snapshot_valve.resume();
880                                            }
881                                            Mutation::Throttle(_) => {
882                                                if let Some(entry) = mutation
883                                                    .backfill_throttle_config(
884                                                        self.actor_ctx.fragment_id,
885                                                    )
886                                                    && entry.rate_limit != self.rate_limit_rps
887                                                {
888                                                    // If self.rate_limit_rps is 0, the snapshot stream does not establish a snapshot_read.
889                                                    // Consequently, the later snapshot stream patch is bypassed.
890                                                    should_bypass_snapshot_stream_patch = self
891                                                        .rate_limit_rps
892                                                        .is_some_and(|val| val == 0);
893                                                    self.rate_limit_rps = entry.rate_limit;
894                                                    needs_rebuild_snapshot = true;
895                                                }
896                                            }
897                                            mutation if mutation.is_stop(self.actor_ctx.id) => {
898                                                // the actor has been dropped, exit the backfill loop
899                                                tracing::info!(
900                                                    %table_id,
901                                                    upstream_table_name,
902                                                    "CdcBackfill has been dropped due to config change"
903                                                );
904                                                state_impl.commit_state(barrier.epoch).await?;
905                                                yield Message::Barrier(barrier);
906                                                break 'backfill_loop;
907                                            }
908                                            _ => (),
909                                        }
910                                    }
911
912                                    // when processing a barrier, check whether can start a new snapshot
913                                    // if the number of barriers reaches the snapshot interval
914                                    if needs_rebuild_snapshot || needs_reader_rebuild {
915                                        // staging the barrier
916                                        pending_barrier = Some(barrier);
917                                        tracing::debug!(
918                                            %table_id,
919                                            ?current_pk_pos,
920                                            ?snapshot_read_row_cnt,
921                                            "Prepare to start a new snapshot"
922                                        );
923                                        // Break the loop for consuming snapshot and prepare to start a new snapshot
924                                        break;
925                                    } else {
926                                        // Drain the in-memory buffer to ensure no data is lost during the recovery process.
927                                        let (
928                                            emitted_upstream_chunks,
929                                            consumed_upstream_row_count,
930                                            consumed_binlog_offset,
931                                        ) = Self::consume_upstream_chunk_buffer(
932                                            &offset_parse_func,
933                                            &mut upstream_chunk_buffer,
934                                            current_pk_pos.as_ref(),
935                                            PkCompareInfo {
936                                                indices: &pk_indices,
937                                                order: &pk_order,
938                                                needs_unsigned_i64_compare:
939                                                    &pk_needs_unsigned_i64_compare,
940                                            },
941                                            &last_binlog_offset,
942                                            &self.output_indices,
943                                        )?;
944                                        cur_barrier_upstream_processed_rows +=
945                                            consumed_upstream_row_count;
946                                        if let Some(consumed_binlog_offset) = consumed_binlog_offset
947                                        {
948                                            last_binlog_offset = Some(consumed_binlog_offset);
949                                        }
950                                        for chunk in emitted_upstream_chunks {
951                                            yield Message::Chunk(chunk);
952                                        }
953
954                                        Self::report_metrics(
955                                            &self.metrics,
956                                            cur_barrier_snapshot_processed_rows,
957                                            cur_barrier_upstream_processed_rows,
958                                        );
959
960                                        // update and persist current backfill progress
961                                        state_impl
962                                            .mutate_and_commit_state(
963                                                current_pk_pos.clone(),
964                                                last_binlog_offset.clone(),
965                                                total_snapshot_row_count,
966                                                false,
967                                                barrier.epoch,
968                                            )
969                                            .await?;
970
971                                        // emit barrier and continue consume the backfill stream
972                                        yield Message::Barrier(barrier);
973                                    }
974                                }
975                                Message::Chunk(chunk) => {
976                                    // skip empty upstream chunk
977                                    if chunk.cardinality() == 0 {
978                                        continue;
979                                    }
980
981                                    let chunk_binlog_offset =
982                                        get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
983
984                                    tracing::trace!(
985                                        "recv changelog chunk: chunk_offset {:?}, capactiy {}",
986                                        chunk_binlog_offset,
987                                        chunk.capacity()
988                                    );
989
990                                    // Since we don't need changelog before the
991                                    // `last_binlog_offset`, skip the chunk that *only* contains
992                                    // events before `last_binlog_offset`.
993                                    if let (Some(last_binlog_offset), Some(chunk_offset)) =
994                                        (last_binlog_offset.as_ref(), chunk_binlog_offset)
995                                        && chunk_offset < *last_binlog_offset
996                                    {
997                                        tracing::trace!(
998                                            "skip changelog chunk: chunk_offset {:?}, capacity {}",
999                                            chunk_offset,
1000                                            chunk.capacity()
1001                                        );
1002                                        continue;
1003                                    }
1004
1005                                    // Buffer the upstream chunk.
1006                                    upstream_chunk_buffer.push(chunk.compact_vis());
1007                                }
1008                                Message::Watermark(_) => {
1009                                    // Ignore watermark during backfill.
1010                                }
1011                            }
1012                        }
1013                        // Snapshot read
1014                        Either::Right(msg) => {
1015                            match msg {
1016                                Ok(None) => {
1017                                    tracing::info!(
1018                                        %table_id,
1019                                        ?last_binlog_offset,
1020                                        ?current_pk_pos,
1021                                        "snapshot read stream ends"
1022                                    );
1023                                    // If the snapshot read stream ends, it means all historical
1024                                    // data has been loaded.
1025                                    // We should not mark the chunk anymore,
1026                                    // otherwise, we will ignore some rows in the buffer.
1027                                    for chunk in upstream_chunk_buffer.drain(..) {
1028                                        yield Message::Chunk(mapping_chunk(
1029                                            chunk,
1030                                            &self.output_indices,
1031                                        ));
1032                                    }
1033
1034                                    // backfill has finished, exit the backfill loop and persist the state when we recv a barrier
1035                                    break 'backfill_loop;
1036                                }
1037                                Ok(Some(chunk)) => {
1038                                    // Raise the current position.
1039                                    // As snapshot read streams are ordered by pk, so we can
1040                                    // just use the last row to update `current_pos`.
1041                                    current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
1042
1043                                    tracing::trace!(
1044                                        "got a snapshot chunk: len {}, current_pk_pos {:?}",
1045                                        chunk.cardinality(),
1046                                        current_pk_pos
1047                                    );
1048                                    let chunk_cardinality = chunk.cardinality() as u64;
1049                                    cur_barrier_snapshot_processed_rows += chunk_cardinality;
1050                                    total_snapshot_row_count += chunk_cardinality;
1051                                    yield Message::Chunk(mapping_chunk(
1052                                        chunk,
1053                                        &self.output_indices,
1054                                    ));
1055                                }
1056                                Err(error) => {
1057                                    needs_reader_rebuild = true;
1058                                    tracing::warn!(
1059                                        error = %error.as_report(),
1060                                        %table_id,
1061                                        upstream_table_name,
1062                                        "failed to read CDC snapshot; rebuilding reader after a barrier"
1063                                    );
1064                                }
1065                            }
1066                        }
1067                    }
1068                }
1069
1070                assert!(pending_barrier.is_some(), "pending_barrier must exist");
1071                let pending_barrier = pending_barrier.unwrap();
1072
1073                // The snapshot stream patch:
1074                // Here we have to ensure the snapshot stream is consumed at least once,
1075                // since the barrier event can kick in anytime.
1076                // Otherwise, the result set of the new snapshot stream may become empty.
1077                // It maybe a cancellation bug of the mysql driver.
1078                let (_, mut snapshot_stream) = backfill_stream.into_inner();
1079                // Resume the snapshot stream so that the snapshot stream patch won't block.
1080                if !should_bypass_snapshot_stream_patch && is_snapshot_paused {
1081                    snapshot_valve.resume();
1082                }
1083                if !should_bypass_snapshot_stream_patch
1084                    && let Some(msg) = snapshot_stream
1085                        .next()
1086                        .instrument_await("consume_snapshot_stream_once")
1087                        .await
1088                {
1089                    let Either::Right(msg) = msg else {
1090                        bail!("BUG: snapshot_read contains upstream messages");
1091                    };
1092                    match msg {
1093                        Ok(None) => {
1094                            tracing::info!(
1095                                %table_id,
1096                                ?last_binlog_offset,
1097                                ?current_pk_pos,
1098                                "snapshot read stream ends in the force emit branch"
1099                            );
1100                            // End of the snapshot read stream.
1101                            // Consume the buffered upstream chunk without filtering by `binlog_low`.
1102                            for chunk in upstream_chunk_buffer.drain(..) {
1103                                yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
1104                            }
1105
1106                            // mark backfill has finished
1107                            state_impl
1108                                .mutate_and_commit_state(
1109                                    current_pk_pos.clone(),
1110                                    last_binlog_offset.clone(),
1111                                    total_snapshot_row_count,
1112                                    true,
1113                                    pending_barrier.epoch,
1114                                )
1115                                .await?;
1116
1117                            yield Message::Barrier(pending_barrier);
1118                            // end of backfill loop, since backfill has finished
1119                            break 'backfill_loop;
1120                        }
1121                        Ok(Some(_)) if is_snapshot_paused => {
1122                            // Since the snapshot stream is paused, drop the chunk.
1123                        }
1124                        Ok(Some(chunk)) => {
1125                            // Raise the current pk position.
1126                            current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
1127
1128                            let row_count = chunk.cardinality() as u64;
1129                            cur_barrier_snapshot_processed_rows += row_count;
1130                            total_snapshot_row_count += row_count;
1131                            snapshot_read_row_cnt += row_count as usize;
1132
1133                            tracing::debug!(
1134                                %table_id,
1135                                ?current_pk_pos,
1136                                ?snapshot_read_row_cnt,
1137                                "force emit a snapshot chunk"
1138                            );
1139                            yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
1140                        }
1141                        Err(error) => {
1142                            needs_reader_rebuild = true;
1143                            tracing::warn!(
1144                                error = %error.as_report(),
1145                                %table_id,
1146                                upstream_table_name,
1147                                "failed to read CDC snapshot; rebuilding reader after a barrier"
1148                            );
1149                        }
1150                    }
1151                }
1152
1153                // If the number of barriers reaches the snapshot interval,
1154                // consume the buffered upstream chunks.
1155                let mut consumed_binlog_offset = None;
1156
1157                if let Some(current_pos) = &current_pk_pos {
1158                    for chunk in upstream_chunk_buffer.drain(..) {
1159                        cur_barrier_upstream_processed_rows += chunk.cardinality() as u64;
1160
1161                        // record the consumed binlog offset that will be
1162                        // persisted later
1163                        consumed_binlog_offset =
1164                            get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
1165
1166                        yield Message::Chunk(mapping_chunk(
1167                            mark_cdc_chunk(
1168                                &offset_parse_func,
1169                                chunk,
1170                                current_pos,
1171                                &pk_indices,
1172                                &pk_order,
1173                                &pk_needs_unsigned_i64_compare,
1174                                last_binlog_offset.clone(),
1175                            )?,
1176                            &self.output_indices,
1177                        ));
1178                    }
1179                } else {
1180                    // If no current_pos, means we did not process any snapshot yet.
1181                    // we can just ignore the upstream buffer chunk in that case.
1182                    upstream_chunk_buffer.clear();
1183                }
1184
1185                // Update last seen binlog offset
1186                if consumed_binlog_offset.is_some() {
1187                    last_binlog_offset.clone_from(&consumed_binlog_offset);
1188                }
1189
1190                Self::report_metrics(
1191                    &self.metrics,
1192                    cur_barrier_snapshot_processed_rows,
1193                    cur_barrier_upstream_processed_rows,
1194                );
1195
1196                // update and persist current backfill progress
1197                state_impl
1198                    .mutate_and_commit_state(
1199                        current_pk_pos.clone(),
1200                        last_binlog_offset.clone(),
1201                        total_snapshot_row_count,
1202                        false,
1203                        pending_barrier.epoch,
1204                    )
1205                    .await?;
1206
1207                yield Message::Barrier(pending_barrier);
1208            }
1209            upstream_table_reader.disconnect().await?;
1210        } else if self.options.disable_backfill {
1211            // If backfill is disabled, we just mark the backfill as finished
1212            tracing::info!(
1213                %table_id,
1214                upstream_table_name,
1215                "CdcBackfill has been disabled"
1216            );
1217            state_impl
1218                .mutate_state(
1219                    current_pk_pos.clone(),
1220                    last_binlog_offset.clone(),
1221                    total_snapshot_row_count,
1222                    true,
1223                )
1224                .await?;
1225        }
1226
1227        tracing::info!(
1228            %table_id,
1229            upstream_table_name,
1230            "CdcBackfill has already finished and will forward messages directly to the downstream"
1231        );
1232
1233        // Wait for first barrier to come after backfill is finished.
1234        // So we can update our progress + persist the status.
1235        while let Some(Ok(msg)) = upstream.next().await {
1236            if let Some(msg) = mapping_message(msg, &self.output_indices) {
1237                // If not finished then we need to update state, otherwise no need.
1238                if let Message::Barrier(barrier) = &msg {
1239                    // finalized the backfill state
1240                    state_impl
1241                        .mutate_and_commit_state(
1242                            current_pk_pos.clone(),
1243                            last_binlog_offset.clone(),
1244                            total_snapshot_row_count,
1245                            true,
1246                            barrier.epoch,
1247                        )
1248                        .await?;
1249
1250                    // mark progress as finished
1251                    if let Some(progress) = self.progress.as_mut() {
1252                        progress.finish(barrier.epoch, total_snapshot_row_count);
1253                    }
1254                    yield msg;
1255                    // break after the state have been saved
1256                    break;
1257                }
1258                yield msg;
1259            }
1260        }
1261
1262        // After backfill progress finished
1263        // we can forward messages directly to the downstream,
1264        // as backfill is finished.
1265        #[for_await]
1266        for msg in upstream {
1267            // upstream offsets will be removed from the message before forwarding to
1268            // downstream
1269            if let Some(msg) = mapping_message(msg?, &self.output_indices) {
1270                if let Message::Barrier(barrier) = &msg {
1271                    // commit state just to bump the epoch of state table
1272                    state_impl.commit_state(barrier.epoch).await?;
1273                }
1274                yield msg;
1275            }
1276        }
1277    }
1278}
1279
1280#[try_stream(ok = Message, error = StreamExecutorError)]
1281pub async fn transform_upstream(
1282    upstream: BoxedMessageStream,
1283    output_columns: Vec<ColumnDesc>,
1284    timestamp_handling: Option<TimestampHandling>,
1285    timestamptz_handling: Option<TimestamptzHandling>,
1286    time_handling: Option<TimeHandling>,
1287    bigint_unsigned_handling: Option<BigintUnsignedHandlingMode>,
1288    handle_toast_columns: bool,
1289) {
1290    let props = SpecificParserConfig {
1291        encoding_config: EncodingProperties::Json(JsonProperties {
1292            use_schema_registry: false,
1293            timestamp_handling,
1294            timestamptz_handling,
1295            time_handling,
1296            bigint_unsigned_handling,
1297            handle_toast_columns,
1298        }),
1299        // the cdc message is generated internally so the key must exist.
1300        protocol_config: ProtocolProperties::Debezium(DebeziumProps::default()),
1301    };
1302
1303    // convert to source column desc to feed into parser
1304    let columns_with_meta = output_columns
1305        .iter()
1306        .map(SourceColumnDesc::from)
1307        .collect_vec();
1308    let mut parser = DebeziumParser::new(
1309        props,
1310        columns_with_meta.clone(),
1311        Arc::new(SourceContext::dummy()),
1312    )
1313    .await
1314    .map_err(StreamExecutorError::connector_error)?;
1315
1316    pin_mut!(upstream);
1317    #[for_await]
1318    for msg in upstream {
1319        let mut msg = msg?;
1320        if let Message::Chunk(chunk) = &mut msg {
1321            let parsed_chunk = parse_debezium_chunk(&mut parser, chunk).await?;
1322            let _ = std::mem::replace(chunk, parsed_chunk);
1323        }
1324        yield msg;
1325    }
1326}
1327
1328async fn parse_debezium_chunk(
1329    parser: &mut DebeziumParser,
1330    chunk: &StreamChunk,
1331) -> StreamExecutorResult<StreamChunk> {
1332    // here we transform the input chunk in `(payload varchar, _rw_offset varchar, _rw_table_name varchar)` schema
1333    // to chunk with downstream table schema `info.schema` of MergeNode contains the schema of the
1334    // table job with `_rw_offset` in the end
1335    // see `gen_create_table_plan_for_cdc_source` for details
1336
1337    // use `SourceStreamChunkBuilder` for convenience
1338    let mut builder = SourceStreamChunkBuilder::new(
1339        parser.columns().to_vec(),
1340        SourceCtrlOpts {
1341            chunk_size: chunk.capacity(),
1342            split_txn: false,
1343        },
1344    );
1345
1346    // The schema of input chunk `(payload varchar, _rw_offset varchar, _rw_table_name varchar, _row_id)`
1347    // We should use the debezium parser to parse the first column,
1348    // then chain the parsed row with `_rw_offset` row to get a new row.
1349    let payloads = chunk.data_chunk().project(&[0]);
1350    let offsets = chunk.data_chunk().project(&[1]).compact_vis();
1351
1352    // TODO: preserve the transaction semantics
1353    for payload in payloads.rows() {
1354        let ScalarRefImpl::Jsonb(jsonb_ref) = payload.datum_at(0).expect("payload must exist")
1355        else {
1356            panic!("payload must be jsonb");
1357        };
1358
1359        parser
1360            .parse_inner(
1361                None,
1362                Some(jsonb_ref.to_string().as_bytes().to_vec()),
1363                builder.row_writer(),
1364            )
1365            .await
1366            .unwrap();
1367    }
1368    builder.finish_current_chunk();
1369
1370    let parsed_chunk = {
1371        let mut iter = builder.consume_ready_chunks();
1372        assert_eq!(1, iter.len());
1373        iter.next().unwrap()
1374    };
1375    assert_eq!(parsed_chunk.capacity(), chunk.capacity()); // each payload is expected to generate one row
1376    let (ops, mut columns, vis) = parsed_chunk.into_inner();
1377    // note that `vis` is not necessarily the same as the original chunk's visibilities
1378
1379    // concat the rows in the parsed chunk with the `_rw_offset` column
1380    columns.extend(offsets.into_parts().0);
1381
1382    Ok(StreamChunk::from_parts(
1383        ops,
1384        DataChunk::from_parts(columns.into(), vis),
1385    ))
1386}
1387
1388impl<S: StateStore> Execute for CdcBackfillExecutor<S> {
1389    fn execute(self: Box<Self>) -> BoxedMessageStream {
1390        self.execute_inner().boxed()
1391    }
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396    use std::collections::BTreeMap;
1397    use std::str::FromStr;
1398    use std::time::Duration;
1399
1400    use either::Either;
1401    use futures::{StreamExt, pin_mut, stream};
1402    use risingwave_common::array::{Array, DataChunk, Op, StreamChunk};
1403    use risingwave_common::catalog::{
1404        CdcKeyComparison, ColumnDesc, ColumnId, Field, Schema, TableId,
1405    };
1406    use risingwave_common::row::{OwnedRow, Row};
1407    use risingwave_common::types::{DataType, Datum, JsonbVal, ScalarImpl};
1408    use risingwave_common::util::epoch::test_epoch;
1409    use risingwave_common::util::iter_util::ZipEqFast;
1410    use risingwave_common::util::sort_util::OrderType;
1411    use risingwave_connector::source::cdc::CdcScanOptions;
1412    use risingwave_connector::source::cdc::external::mock_external_table::MockExternalTableReader;
1413    use risingwave_connector::source::cdc::external::mysql::MysqlOffset;
1414    use risingwave_connector::source::cdc::external::{
1415        CdcOffset, ExternalCdcTableType, ExternalTableConfig, ExternalTableReaderImpl,
1416        SchemaTableName,
1417    };
1418    use risingwave_storage::memory::MemoryStateStore;
1419
1420    use super::{
1421        PkCompareInfo, build_reader_and_poll_upstream, can_poll_upstream_while_creating_reader,
1422    };
1423    use crate::common::table::test_utils::gen_pbtable;
1424    use crate::executor::backfill::cdc::cdc_backfill::transform_upstream;
1425    use crate::executor::backfill::cdc::state::CdcBackfillState;
1426    use crate::executor::monitor::StreamingMetrics;
1427    use crate::executor::prelude::StateTable;
1428    use crate::executor::test_utils::{MessageSender, MockSource};
1429    use crate::executor::{
1430        ActorContext, Barrier, CdcBackfillExecutor, ExternalStorageTable, Message,
1431    };
1432
1433    #[tokio::test]
1434    async fn test_transform_upstream_chunk() {
1435        let schema = Schema::new(vec![
1436            Field::unnamed(DataType::Jsonb),   // debezium json payload
1437            Field::unnamed(DataType::Varchar), // _rw_offset
1438            Field::unnamed(DataType::Varchar), // _rw_table_name
1439        ]);
1440        let stream_key = vec![1];
1441        let (mut tx, source) = MockSource::channel();
1442        let source = source.into_executor(schema.clone(), stream_key.clone());
1443        // let payload = r#"{"before": null,"after":{"O_ORDERKEY": 5, "O_CUSTKEY": 44485, "O_ORDERSTATUS": "F", "O_TOTALPRICE": "144659.20", "O_ORDERDATE": "1994-07-30" },"source":{"version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002", "ts_ms": 1695277757000, "snapshot": "last", "db": "mydb", "sequence": null, "table": "orders_new", "server_id": 0, "gtid": null, "file": "binlog.000008", "pos": 3693, "row": 0, "thread": null, "query": null},"op":"r","ts_ms":1695277757017,"transaction":null}"#.to_string();
1444        let payload = r#"{ "payload": { "before": null, "after": { "O_ORDERKEY": 5, "O_CUSTKEY": 44485, "O_ORDERSTATUS": "F", "O_TOTALPRICE": "144659.20", "O_ORDERDATE": "1994-07-30" }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002", "ts_ms": 1695277757000, "snapshot": "last", "db": "mydb", "sequence": null, "table": "orders_new", "server_id": 0, "gtid": null, "file": "binlog.000008", "pos": 3693, "row": 0, "thread": null, "query": null }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#;
1445
1446        let datums: Vec<Datum> = vec![
1447            Some(JsonbVal::from_str(payload).unwrap().into()),
1448            Some("file: 1.binlog, pos: 100".to_owned().into()),
1449            Some("mydb.orders".to_owned().into()),
1450        ];
1451
1452        let mut builders = schema.create_array_builders(8);
1453        for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1454            builder.append(datum.clone());
1455        }
1456        let columns = builders
1457            .into_iter()
1458            .map(|builder| builder.finish().into())
1459            .collect();
1460
1461        // one row chunk
1462        let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1463
1464        tx.push_chunk(chunk);
1465        let upstream = Box::new(source).execute();
1466
1467        // schema to the debezium parser
1468        let columns = vec![
1469            ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1470            ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1471            ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1472            ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1473            ColumnDesc::named("O_ORDERDATE", ColumnId::new(5), DataType::Date),
1474            ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1475        ];
1476
1477        let parsed_stream = transform_upstream(upstream, columns, None, None, None, None, false);
1478        pin_mut!(parsed_stream);
1479        let message = parsed_stream
1480            .next()
1481            .await
1482            .expect("transform stream should yield the input chunk")
1483            .expect("transforming the CDC chunk should succeed");
1484        let Message::Chunk(chunk) = message else {
1485            panic!("expected a transformed chunk");
1486        };
1487        assert_eq!(
1488            chunk
1489                .rows()
1490                .map(|(op, row)| (op, row.to_owned_row()))
1491                .collect::<Vec<_>>(),
1492            vec![(
1493                Op::Insert,
1494                OwnedRow::new(vec![
1495                    Some(ScalarImpl::Int64(5)),
1496                    Some(ScalarImpl::Int64(44485)),
1497                    Some(ScalarImpl::Utf8("F".into())),
1498                    Some(ScalarImpl::Decimal("144659.20".parse().unwrap())),
1499                    Some(ScalarImpl::Date("1994-07-30".parse().unwrap())),
1500                    None,
1501                    Some(ScalarImpl::Utf8("file: 1.binlog, pos: 100".into())),
1502                ]),
1503            )]
1504        );
1505    }
1506
1507    #[tokio::test]
1508    async fn test_finished_backfill_forwards_without_reader() {
1509        let actor_context = ActorContext::for_test(1);
1510        let table_id = TableId::new(1234);
1511        let external_storage_table = ExternalStorageTable::new(
1512            table_id,
1513            SchemaTableName {
1514                schema_name: "public".to_owned(),
1515                table_name: "missing_table".to_owned(),
1516            },
1517            "mydb".to_owned(),
1518            ExternalTableConfig::default(),
1519            ExternalCdcTableType::Unspecified,
1520            Schema::new(vec![Field::with_name(DataType::Int64, "O_ORDERKEY")]),
1521            vec![OrderType::ascending()],
1522            Some(vec![CdcKeyComparison::Native]),
1523            vec![0],
1524        );
1525        let schema = Schema::new(vec![
1526            Field::unnamed(DataType::Jsonb),   // debezium json payload
1527            Field::unnamed(DataType::Varchar), // _rw_offset
1528            Field::unnamed(DataType::Varchar), // _rw_table_name
1529        ]);
1530        let stream_key = vec![1];
1531        let (mut tx, source) = MockSource::channel();
1532        let source = source
1533            .stop_on_finish(false)
1534            .into_executor(schema.clone(), stream_key.clone());
1535        let output_indices = vec![1, 0, 4]; //reorder
1536        let output_columns = vec![
1537            ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1538            ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1539            ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1540            ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1541            ColumnDesc::named("O_DUMMY", ColumnId::new(5), DataType::Int64),
1542            ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1543        ];
1544        let store = MemoryStateStore::new();
1545        let mut persisted_state =
1546            CdcBackfillState::new(table_id, create_cdc_state_table(store.clone()).await, 5);
1547        persisted_state
1548            .init_epoch(Barrier::new_test_barrier(test_epoch(6)).epoch)
1549            .await
1550            .unwrap();
1551        persisted_state
1552            .mutate_and_commit_state(
1553                Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1554                Some(CdcOffset::Mysql(MysqlOffset::new(
1555                    "1.binlog".to_owned(),
1556                    100,
1557                ))),
1558                1,
1559                true,
1560                Barrier::new_test_barrier(test_epoch(7)).epoch,
1561            )
1562            .await
1563            .unwrap();
1564        let state_table = create_cdc_state_table(store).await;
1565        let cdc = CdcBackfillExecutor::new(
1566            actor_context,
1567            external_storage_table,
1568            source,
1569            output_indices,
1570            output_columns,
1571            None,
1572            StreamingMetrics::unused().into(),
1573            state_table,
1574            None,
1575            CdcScanOptions::default(),
1576            BTreeMap::default(),
1577        );
1578        let s = cdc.execute_inner();
1579        pin_mut!(s);
1580
1581        // send first barrier
1582        tx.send_barrier(Barrier::new_test_barrier(test_epoch(8)));
1583        // send chunk
1584        {
1585            let payload = r#"{ "payload": { "before": null, "after": { "O_ORDERKEY": 5, "O_CUSTKEY": 44485, "O_ORDERSTATUS": "F", "O_TOTALPRICE": "144659.20", "O_DUMMY": 100 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002", "ts_ms": 1695277757000, "snapshot": "last", "db": "mydb", "sequence": null, "table": "orders_new", "server_id": 0, "gtid": null, "file": "binlog.000008", "pos": 3693, "row": 0, "thread": null, "query": null }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#;
1586            let datums: Vec<Datum> = vec![
1587                Some(JsonbVal::from_str(payload).unwrap().into()),
1588                Some("file: 1.binlog, pos: 100".to_owned().into()),
1589                Some("mydb.orders".to_owned().into()),
1590            ];
1591            let mut builders = schema.create_array_builders(8);
1592            for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1593                builder.append(datum.clone());
1594            }
1595            let columns = builders
1596                .into_iter()
1597                .map(|builder| builder.finish().into())
1598                .collect();
1599            // one row chunk
1600            let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1601
1602            tx.push_chunk(chunk);
1603        }
1604        let _first_barrier = s.next().await.unwrap();
1605        let upstream_change_log = s.next().await.unwrap().unwrap();
1606        let Message::Chunk(chunk) = upstream_change_log else {
1607            panic!("expect chunk");
1608        };
1609        assert_eq!(chunk.columns().len(), 3);
1610        assert_eq!(chunk.rows().count(), 1);
1611        assert_eq!(
1612            chunk.columns()[0].as_int64().iter().collect::<Vec<_>>(),
1613            vec![Some(44485)]
1614        );
1615        assert_eq!(
1616            chunk.columns()[1].as_int64().iter().collect::<Vec<_>>(),
1617            vec![Some(5)]
1618        );
1619        assert_eq!(
1620            chunk.columns()[2].as_int64().iter().collect::<Vec<_>>(),
1621            vec![Some(100)]
1622        );
1623
1624        drop(tx);
1625        assert!(s.next().await.is_none());
1626    }
1627
1628    #[test]
1629    fn test_reader_startup_polling_selection() {
1630        let recovered_position = OwnedRow::new(vec![Some(ScalarImpl::Int64(1))]);
1631
1632        assert!(!can_poll_upstream_while_creating_reader(
1633            Some(&recovered_position),
1634            false,
1635        ));
1636        assert!(can_poll_upstream_while_creating_reader(None, false));
1637        assert!(can_poll_upstream_while_creating_reader(
1638            Some(&recovered_position),
1639            true,
1640        ));
1641    }
1642
1643    #[tokio::test]
1644    async fn test_snapshot_read_error_rebuilds_reader() {
1645        let external_table = create_mock_external_table().with_mock_snapshot_errors([1, 0]);
1646        let reader_create_probe = external_table.clone();
1647        let (tx, executor) = create_mock_cdc_backfill(
1648            external_table,
1649            create_cdc_state_table(MemoryStateStore::new()).await,
1650            CdcScanOptions {
1651                snapshot_barrier_interval: 1,
1652                ..Default::default()
1653            },
1654        );
1655        let executor = executor.execute_inner();
1656        pin_mut!(executor);
1657
1658        tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1659        assert!(matches!(
1660            executor.next().await.unwrap().unwrap(),
1661            Message::Barrier(_)
1662        ));
1663
1664        // The second barrier starts the snapshot. The first reader fails when it is polled.
1665        tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1666        assert!(matches!(
1667            executor.next().await.unwrap().unwrap(),
1668            Message::Barrier(_)
1669        ));
1670
1671        assert!(
1672            tokio::time::timeout(Duration::from_millis(20), executor.next())
1673                .await
1674                .is_err()
1675        );
1676        assert_eq!(reader_create_probe.mock_reader_create_count(), 1);
1677
1678        // The next barrier checkpoints the resume position before the failed reader is replaced.
1679        tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
1680        assert!(matches!(
1681            executor.next().await.unwrap().unwrap(),
1682            Message::Barrier(_)
1683        ));
1684
1685        // The replacement reader succeeds, proving that retries do not reuse the failed reader.
1686        let Message::Chunk(chunk) = executor.next().await.unwrap().unwrap() else {
1687            panic!("replacement reader should produce a snapshot chunk");
1688        };
1689        assert_eq!(chunk.cardinality(), 6);
1690        assert_eq!(reader_create_probe.mock_reader_create_count(), 2);
1691
1692        tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
1693        assert!(matches!(
1694            executor.next().await.unwrap().unwrap(),
1695            Message::Barrier(_)
1696        ));
1697    }
1698
1699    #[tokio::test]
1700    async fn test_snapshot_read_error_before_first_chunk_survives_recovery() {
1701        let scan_options = || CdcScanOptions {
1702            snapshot_barrier_interval: 1,
1703            ..Default::default()
1704        };
1705
1706        let memory_state_store = MemoryStateStore::new();
1707
1708        // The first snapshot read fails before any row is emitted, and the next barrier
1709        // checkpoints while there is no snapshot cursor.
1710        {
1711            let (tx, executor) = create_mock_cdc_backfill(
1712                create_mock_external_table().with_mock_snapshot_errors([1]),
1713                create_cdc_state_table(memory_state_store.clone()).await,
1714                scan_options(),
1715            );
1716            let executor = executor.execute_inner();
1717
1718            pin_mut!(executor);
1719
1720            for epoch in 1..=3 {
1721                tx.send_barrier(Barrier::new_test_barrier(test_epoch(epoch)));
1722                assert!(matches!(
1723                    executor.next().await.unwrap().unwrap(),
1724                    Message::Barrier(_)
1725                ));
1726            }
1727        }
1728
1729        let mut restored_state = CdcBackfillState::new(
1730            TableId::new(1234),
1731            create_cdc_state_table(memory_state_store.clone()).await,
1732            5,
1733        );
1734        restored_state
1735            .init_epoch(Barrier::new_test_barrier(test_epoch(4)).epoch)
1736            .await
1737            .unwrap();
1738
1739        // No snapshot progress was made, so no backfill state should be persisted.
1740        let state = restored_state.restore_state().await.unwrap();
1741        assert!(!state.is_finished);
1742        assert_eq!(state.current_pk_pos, None);
1743        assert_eq!(state.last_cdc_offset, None);
1744        assert_eq!(state.row_count, 0);
1745
1746        // After recovery, the snapshot must restart from the beginning instead of resuming
1747        // from a NULL cursor.
1748        let (tx, executor) = create_mock_cdc_backfill(
1749            create_mock_external_table(),
1750            create_cdc_state_table(memory_state_store).await,
1751            scan_options(),
1752        );
1753        let executor = executor.execute_inner();
1754
1755        pin_mut!(executor);
1756
1757        for epoch in 4..=5 {
1758            tx.send_barrier(Barrier::new_test_barrier(test_epoch(epoch)));
1759            assert!(matches!(
1760                executor.next().await.unwrap().unwrap(),
1761                Message::Barrier(_)
1762            ));
1763        }
1764        let Message::Chunk(chunk) = tokio::time::timeout(Duration::from_secs(5), executor.next())
1765            .await
1766            .expect("recovered backfill should read the snapshot")
1767            .unwrap()
1768            .unwrap()
1769        else {
1770            panic!("recovered reader should produce a snapshot chunk");
1771        };
1772
1773        assert_eq!(chunk.cardinality(), 6);
1774    }
1775
1776    #[tokio::test]
1777    async fn test_disabled_cdc_backfill_finishes_without_table_reader() {
1778        let actor_context = ActorContext::for_test(1);
1779        let external_storage_table = ExternalStorageTable::new(
1780            TableId::new(1234),
1781            SchemaTableName {
1782                schema_name: "public".to_owned(),
1783                table_name: "orders".to_owned(),
1784            },
1785            "db".to_owned(),
1786            ExternalTableConfig::default(),
1787            ExternalCdcTableType::Unspecified,
1788            Schema::new(vec![Field::with_name(DataType::Int64, "id")]),
1789            vec![OrderType::ascending()],
1790            Some(vec![CdcKeyComparison::Native]),
1791            vec![0],
1792        );
1793        let schema = Schema::new(vec![
1794            Field::unnamed(DataType::Jsonb),
1795            Field::unnamed(DataType::Varchar),
1796            Field::unnamed(DataType::Varchar),
1797        ]);
1798        let stream_key = vec![1];
1799        let (tx, source) = MockSource::channel();
1800        let source = source.into_executor(schema, stream_key);
1801        let output_columns = vec![ColumnDesc::named("id", ColumnId::new(1), DataType::Int64)];
1802        let memory_state_store = MemoryStateStore::new();
1803        let state_table = create_cdc_state_table(memory_state_store.clone()).await;
1804        let cdc = CdcBackfillExecutor::new(
1805            actor_context,
1806            external_storage_table,
1807            source,
1808            vec![0],
1809            output_columns,
1810            None,
1811            StreamingMetrics::unused().into(),
1812            state_table,
1813            None,
1814            CdcScanOptions {
1815                disable_backfill: true,
1816                ..CdcScanOptions::default()
1817            },
1818            BTreeMap::default(),
1819        );
1820        let executor = cdc.execute_inner();
1821        pin_mut!(executor);
1822
1823        tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1824        assert!(matches!(
1825            executor.next().await.unwrap().unwrap(),
1826            Message::Barrier(_)
1827        ));
1828
1829        tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1830        assert!(matches!(
1831            executor.next().await.unwrap().unwrap(),
1832            Message::Barrier(_)
1833        ));
1834
1835        let mut restored_state = CdcBackfillState::new(
1836            TableId::new(1234),
1837            create_cdc_state_table(memory_state_store).await,
1838            5,
1839        );
1840        restored_state
1841            .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1842            .await
1843            .unwrap();
1844        let state = restored_state.restore_state().await.unwrap();
1845        assert!(state.is_finished);
1846        assert_eq!(state.last_cdc_offset, None);
1847    }
1848
1849    #[tokio::test]
1850    async fn test_unfinished_cdc_backfill_rejects_null_offset_on_restore() {
1851        let memory_state_store = MemoryStateStore::new();
1852        let mut state_writer = CdcBackfillState::new(
1853            TableId::new(1234),
1854            create_cdc_state_table(memory_state_store.clone()).await,
1855            5,
1856        );
1857        state_writer
1858            .init_epoch(Barrier::new_test_barrier(test_epoch(1)).epoch)
1859            .await
1860            .unwrap();
1861        state_writer
1862            .mutate_and_commit_state(
1863                Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(10))])),
1864                None,
1865                10,
1866                false,
1867                Barrier::new_test_barrier(test_epoch(2)).epoch,
1868            )
1869            .await
1870            .unwrap();
1871
1872        let mut restored_state = CdcBackfillState::new(
1873            TableId::new(1234),
1874            create_cdc_state_table(memory_state_store).await,
1875            5,
1876        );
1877        restored_state
1878            .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1879            .await
1880            .unwrap();
1881
1882        assert!(restored_state.restore_state().await.is_err());
1883    }
1884
1885    fn create_raw_cdc_chunk(rows: &[(&str, &str)]) -> StreamChunk {
1886        let schema = Schema::new(vec![
1887            Field::unnamed(DataType::Jsonb),
1888            Field::unnamed(DataType::Varchar),
1889        ]);
1890        let mut builders = schema.create_array_builders(rows.len());
1891        for (payload, offset) in rows {
1892            let payload_datum: Datum = Some(JsonbVal::from_str(payload).unwrap().into());
1893            let offset_datum: Datum = Some((*offset).into());
1894            builders[0].append(payload_datum);
1895            builders[1].append(offset_datum);
1896        }
1897        let columns = builders
1898            .into_iter()
1899            .map(|builder| builder.finish().into())
1900            .collect();
1901        StreamChunk::from_parts(
1902            vec![Op::Insert; rows.len()],
1903            DataChunk::new(columns, rows.len()),
1904        )
1905    }
1906
1907    fn create_mock_external_table() -> ExternalStorageTable {
1908        ExternalStorageTable::new(
1909            TableId::new(1234),
1910            SchemaTableName {
1911                schema_name: "public".to_owned(),
1912                table_name: "mock_table".to_owned(),
1913            },
1914            "mydb".to_owned(),
1915            ExternalTableConfig::default(),
1916            ExternalCdcTableType::Mock,
1917            Schema::new(vec![
1918                Field::with_name(DataType::Int64, "id"),
1919                Field::with_name(DataType::Float64, "price"),
1920            ]),
1921            vec![OrderType::ascending()],
1922            Some(vec![CdcKeyComparison::Native]),
1923            vec![0],
1924        )
1925    }
1926
1927    /// Creates a CDC backfill executor over the `id`/`price` mock table, along with the sender
1928    /// for its upstream.
1929    fn create_mock_cdc_backfill(
1930        external_table: ExternalStorageTable,
1931        state_table: StateTable<MemoryStateStore>,
1932        scan_options: CdcScanOptions,
1933    ) -> (MessageSender, CdcBackfillExecutor<MemoryStateStore>) {
1934        let (tx, source) = MockSource::channel();
1935        let source = source.into_executor(
1936            Schema::new(vec![
1937                Field::unnamed(DataType::Jsonb),
1938                Field::unnamed(DataType::Varchar),
1939            ]),
1940            vec![0],
1941        );
1942        let executor = CdcBackfillExecutor::new(
1943            ActorContext::for_test(0x1a),
1944            external_table,
1945            source,
1946            vec![0, 1],
1947            vec![
1948                ColumnDesc::named("id", ColumnId::new(1), DataType::Int64),
1949                ColumnDesc::named("price", ColumnId::new(2), DataType::Float64),
1950            ],
1951            None,
1952            StreamingMetrics::unused().into(),
1953            state_table,
1954            None,
1955            scan_options,
1956            BTreeMap::default(),
1957        );
1958        (tx, executor)
1959    }
1960
1961    async fn create_cdc_state_table(store: MemoryStateStore) -> StateTable<MemoryStateStore> {
1962        let state_schema = Schema::new(vec![
1963            Field::with_name(DataType::Varchar, "split_id"),
1964            Field::with_name(DataType::Int64, "id"),
1965            Field::with_name(DataType::Boolean, "backfill_finished"),
1966            Field::with_name(DataType::Int64, "row_count"),
1967            Field::with_name(DataType::Jsonb, "cdc_offset"),
1968        ]);
1969        let column_descs = vec![
1970            ColumnDesc::unnamed(ColumnId::from(0), state_schema[0].data_type.clone()),
1971            ColumnDesc::unnamed(ColumnId::from(1), state_schema[1].data_type.clone()),
1972            ColumnDesc::unnamed(ColumnId::from(2), state_schema[2].data_type.clone()),
1973            ColumnDesc::unnamed(ColumnId::from(3), state_schema[3].data_type.clone()),
1974            ColumnDesc::unnamed(ColumnId::from(4), state_schema[4].data_type.clone()),
1975        ];
1976
1977        StateTable::from_table_catalog(
1978            &gen_pbtable(
1979                TableId::from(0x42),
1980                column_descs,
1981                vec![OrderType::ascending()],
1982                vec![0],
1983                0,
1984            ),
1985            store,
1986            None,
1987        )
1988        .await
1989    }
1990
1991    async fn create_recovering_cdc_backfill() -> (
1992        MessageSender,
1993        CdcBackfillExecutor<MemoryStateStore>,
1994        MemoryStateStore,
1995    ) {
1996        let memory_state_store = MemoryStateStore::new();
1997        let mut state_writer = CdcBackfillState::new(
1998            TableId::new(1234),
1999            create_cdc_state_table(memory_state_store.clone()).await,
2000            5,
2001        );
2002        state_writer
2003            .init_epoch(Barrier::new_test_barrier(test_epoch(1)).epoch)
2004            .await
2005            .unwrap();
2006        state_writer
2007            .mutate_and_commit_state(
2008                Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2009                Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2010                5,
2011                false,
2012                Barrier::new_test_barrier(test_epoch(2)).epoch,
2013            )
2014            .await
2015            .unwrap();
2016
2017        let (tx, executor) = create_mock_cdc_backfill(
2018            create_mock_external_table(),
2019            create_cdc_state_table(memory_state_store.clone()).await,
2020            CdcScanOptions::default(),
2021        );
2022        (tx, executor, memory_state_store)
2023    }
2024
2025    async fn assert_recovery_offset(
2026        memory_state_store: MemoryStateStore,
2027        expected_position: u64,
2028        epoch: u64,
2029    ) {
2030        let mut restored_state = CdcBackfillState::new(
2031            TableId::new(1234),
2032            create_cdc_state_table(memory_state_store).await,
2033            5,
2034        );
2035        restored_state
2036            .init_epoch(Barrier::new_test_barrier(epoch).epoch)
2037            .await
2038            .unwrap();
2039        let state = restored_state.restore_state().await.unwrap();
2040        assert_eq!(
2041            state.last_cdc_offset,
2042            Some(CdcOffset::Mysql(MysqlOffset::new(
2043                "1.binlog".to_owned(),
2044                expected_position,
2045            )))
2046        );
2047    }
2048
2049    #[tokio::test]
2050    async fn test_recovery_filters_upstream_while_reader_is_pending() {
2051        let chunk = StreamChunk::from_rows(
2052            &[
2053                (
2054                    Op::Insert,
2055                    OwnedRow::new(vec![
2056                        Some(ScalarImpl::Int64(4)),
2057                        Some(ScalarImpl::Float64(44.04.into())),
2058                        Some(ScalarImpl::Utf8(
2059                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2060                                .into(),
2061                        )),
2062                    ]),
2063                ),
2064                (
2065                    Op::Insert,
2066                    OwnedRow::new(vec![
2067                        Some(ScalarImpl::Int64(6)),
2068                        Some(ScalarImpl::Float64(66.06.into())),
2069                        Some(ScalarImpl::Utf8(
2070                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2071                                .into(),
2072                        )),
2073                    ]),
2074                ),
2075            ],
2076            &[DataType::Int64, DataType::Float64, DataType::Varchar],
2077        );
2078        let mut upstream = stream::iter([
2079            Ok(Message::Chunk(chunk)),
2080            Ok(Message::Barrier(Barrier::new_test_barrier(test_epoch(4)))),
2081        ]);
2082        let mut reader_future = Box::pin(std::future::pending::<ExternalTableReaderImpl>());
2083
2084        let Either::Left(Message::Chunk(chunk)) =
2085            build_reader_and_poll_upstream(&mut upstream, &mut reader_future)
2086                .await
2087                .unwrap()
2088        else {
2089            panic!("expected an upstream chunk while the reader is pending");
2090        };
2091
2092        let parser = ExternalCdcTableType::Mock.get_cdc_offset_parser().unwrap();
2093        let (chunk, consumed_offset) =
2094            CdcBackfillExecutor::<MemoryStateStore>::filter_recovery_chunk(
2095                &parser,
2096                chunk,
2097                &OwnedRow::new(vec![Some(ScalarImpl::Int64(5))]),
2098                PkCompareInfo {
2099                    indices: &[0],
2100                    order: &[OrderType::ascending()],
2101                    needs_unsigned_i64_compare: &[false],
2102                },
2103                &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2104                &[0, 1],
2105            )
2106            .unwrap();
2107        let chunk = chunk.expect("the scanned row should be emitted");
2108        assert_eq!(chunk.cardinality(), 1);
2109        assert_eq!(
2110            chunk.row_at(0).1.to_owned_row()[0],
2111            Some(ScalarImpl::Int64(4))
2112        );
2113        assert_eq!(
2114            consumed_offset,
2115            Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 4)))
2116        );
2117        assert!(matches!(
2118            build_reader_and_poll_upstream(&mut upstream, &mut reader_future)
2119                .await
2120                .unwrap(),
2121            Either::Left(Message::Barrier(_))
2122        ));
2123    }
2124
2125    #[tokio::test]
2126    async fn test_recovery_filters_updates_before_backfill_barrier() {
2127        let (mut tx, executor, memory_state_store) = create_recovering_cdc_backfill().await;
2128        let executor = executor.execute_inner();
2129        pin_mut!(executor);
2130
2131        tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2132        assert!(matches!(
2133            executor.next().await.unwrap().unwrap(),
2134            Message::Barrier(_)
2135        ));
2136
2137        tx.push_chunk(create_raw_cdc_chunk(&[
2138            (
2139                r#"{ "payload": { "before": { "id": 1, "price": 11.01 }, "after": { "id": 1, "price": 12.01 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "u", "ts_ms": 1695277757017, "transaction": null } }"#,
2140                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2141            ),
2142            (
2143                r#"{ "payload": { "before": { "id": 6, "price": 66.06 }, "after": { "id": 6, "price": 67.06 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "u", "ts_ms": 1695277757017, "transaction": null } }"#,
2144                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
2145            ),
2146        ]));
2147        tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
2148
2149        let Message::Chunk(chunk) = executor.next().await.unwrap().unwrap() else {
2150            panic!("expected the recovered update before the barrier");
2151        };
2152        let rows = chunk
2153            .rows()
2154            .map(|(op, row)| (op, row.to_owned_row()))
2155            .collect::<Vec<_>>();
2156        assert_eq!(rows.len(), 1);
2157        assert_eq!(rows[0].0, Op::Insert);
2158        assert_eq!(rows[0].1[0], Some(ScalarImpl::Int64(1)));
2159        assert!(matches!(
2160            executor.next().await.unwrap().unwrap(),
2161            Message::Barrier(_)
2162        ));
2163        assert_recovery_offset(memory_state_store, 4, test_epoch(4)).await;
2164    }
2165
2166    #[tokio::test]
2167    async fn test_recovery_preserves_scanned_inserts_and_deletes() {
2168        let (mut tx, executor, _) = create_recovering_cdc_backfill().await;
2169        let executor = executor.execute_inner();
2170        pin_mut!(executor);
2171
2172        tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2173        assert!(matches!(
2174            executor.next().await.unwrap().unwrap(),
2175            Message::Barrier(_)
2176        ));
2177        // This replayed event predates the saved offset and must not lower the offset filter.
2178        tx.push_chunk(create_raw_cdc_chunk(&[(
2179            r#"{ "payload": { "before": null, "after": { "id": 3, "price": 30.01 }, "op": "c" } }"#,
2180            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":1},"isHeartbeat":false}"#,
2181        )]));
2182        tx.push_chunk(create_raw_cdc_chunk(&[
2183            (
2184                r#"{ "payload": { "before": null, "after": { "id": 3, "price": 31.01 }, "op": "c" } }"#,
2185                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":1},"isHeartbeat":false}"#,
2186            ),
2187            (
2188                r#"{ "payload": { "before": null, "after": { "id": 4, "price": 44.04 }, "op": "c" } }"#,
2189                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":2},"isHeartbeat":false}"#,
2190            ),
2191            (
2192                r#"{ "payload": { "before": { "id": 5, "price": 55.05 }, "after": null, "op": "d" } }"#,
2193                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2194            ),
2195        ]));
2196        tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
2197
2198        let Message::Chunk(chunk) = executor.next().await.unwrap().unwrap() else {
2199            panic!("expected the scanned-prefix changes before the barrier");
2200        };
2201        let rows = chunk
2202            .rows()
2203            .map(|(op, row)| (op, row.to_owned_row()))
2204            .collect::<Vec<_>>();
2205        assert_eq!(rows.len(), 2);
2206        assert_eq!(rows[0].0, Op::Insert);
2207        assert_eq!(rows[0].1[0], Some(ScalarImpl::Int64(4)));
2208        assert_eq!(rows[1].0, Op::Delete);
2209        assert_eq!(rows[1].1[0], Some(ScalarImpl::Int64(5)));
2210        assert!(matches!(
2211            executor.next().await.unwrap().unwrap(),
2212            Message::Barrier(_)
2213        ));
2214    }
2215
2216    #[tokio::test]
2217    async fn test_recovery_does_not_leave_deleted_unscanned_row() {
2218        let (mut tx, mut executor, memory_state_store) = create_recovering_cdc_backfill().await;
2219        executor.options.snapshot_barrier_interval = 1;
2220        let executor = executor.execute_inner();
2221        pin_mut!(executor);
2222
2223        tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2224        assert!(matches!(
2225            executor.next().await.unwrap().unwrap(),
2226            Message::Barrier(_)
2227        ));
2228
2229        // Recover at PK 5, then receive an insert for an unscanned key during startup.
2230        tx.push_chunk(create_raw_cdc_chunk(&[(
2231            r#"{ "payload": { "before": null, "after": { "id": 100, "price": 100.01 }, "op": "c" } }"#,
2232            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2233        )]));
2234        tx.send_barrier(Barrier::new_test_barrier(test_epoch(4)));
2235
2236        // Normal backfill receives its delete before the snapshot reaches PK 100.
2237        // The snapshot refresh at barrier 5 scans up to PK 8 and filters out the delete.
2238        tx.push_chunk(create_raw_cdc_chunk(&[(
2239            r#"{ "payload": { "before": { "id": 100, "price": 100.01 }, "after": null, "op": "d" } }"#,
2240            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
2241        )]));
2242        tx.send_barrier(Barrier::new_test_barrier(test_epoch(5)));
2243        // The next snapshot is empty, so backfill completes at barrier 6.
2244        tx.send_barrier(Barrier::new_test_barrier(test_epoch(6)));
2245
2246        let mut materialized = BTreeMap::new();
2247        loop {
2248            match executor.next().await.unwrap().unwrap() {
2249                Message::Chunk(chunk) => {
2250                    for (op, row) in chunk.rows() {
2251                        let pk = row.datum_at(0).unwrap().into_int64();
2252                        match op {
2253                            Op::Insert => {
2254                                materialized.insert(pk, row.to_owned_row());
2255                            }
2256                            Op::Delete => {
2257                                materialized.remove(&pk);
2258                            }
2259                            _ => unreachable!("CDC updates are converted to inserts"),
2260                        }
2261                    }
2262                }
2263                Message::Barrier(barrier) if barrier.epoch.curr == test_epoch(6) => break,
2264                Message::Barrier(_) => {}
2265                Message::Watermark(_) => panic!("unexpected watermark during backfill"),
2266            }
2267        }
2268
2269        assert!(!materialized.contains_key(&100));
2270        let mut restored_state = CdcBackfillState::new(
2271            TableId::new(1234),
2272            create_cdc_state_table(memory_state_store).await,
2273            5,
2274        );
2275        restored_state
2276            .init_epoch(Barrier::new_test_barrier(test_epoch(6)).epoch)
2277            .await
2278            .unwrap();
2279        assert!(restored_state.restore_state().await.unwrap().is_finished);
2280    }
2281
2282    #[test]
2283    fn test_consume_upstream_chunk_buffer_retains_future_rows() {
2284        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2285            &[
2286                (
2287                    Op::Insert,
2288                    OwnedRow::new(vec![
2289                        Some(ScalarImpl::Int64(1)),
2290                        Some(ScalarImpl::Int64(100)),
2291                        Some(ScalarImpl::Utf8(
2292                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2293                                .into(),
2294                        )),
2295                    ]),
2296                ),
2297                (
2298                    Op::Insert,
2299                    OwnedRow::new(vec![
2300                        Some(ScalarImpl::Int64(6)),
2301                        Some(ScalarImpl::Int64(600)),
2302                        Some(ScalarImpl::Utf8(
2303                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2304                                .into(),
2305                        )),
2306                    ]),
2307                ),
2308            ],
2309            &[DataType::Int64, DataType::Int64, DataType::Varchar],
2310        )];
2311
2312        let (emitted_chunks, drained_row_count, drained_offset) =
2313            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2314                &MockExternalTableReader::get_cdc_offset_parser(),
2315                &mut upstream_chunk_buffer,
2316                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2317                PkCompareInfo {
2318                    indices: &[0],
2319                    order: &[OrderType::ascending()],
2320                    needs_unsigned_i64_compare: &[false],
2321                },
2322                &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2323                &[0, 1],
2324            )
2325            .unwrap();
2326
2327        assert_eq!(drained_row_count, 1);
2328        assert_eq!(
2329            drained_offset,
2330            Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2331        );
2332        assert_eq!(emitted_chunks.len(), 1);
2333        assert_eq!(emitted_chunks[0].rows().count(), 1);
2334        assert_eq!(
2335            emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
2336            OwnedRow::new(vec![
2337                Some(ScalarImpl::Int64(1)),
2338                Some(ScalarImpl::Int64(100))
2339            ])
2340        );
2341
2342        assert_eq!(upstream_chunk_buffer.len(), 1);
2343        assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
2344        assert_eq!(
2345            upstream_chunk_buffer[0]
2346                .rows()
2347                .next()
2348                .unwrap()
2349                .1
2350                .to_owned_row(),
2351            OwnedRow::new(vec![
2352                Some(ScalarImpl::Int64(6)),
2353                Some(ScalarImpl::Int64(600)),
2354                Some(ScalarImpl::Utf8(
2355                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2356                        .into(),
2357                )),
2358            ])
2359        );
2360    }
2361
2362    #[test]
2363    fn test_consume_buffer_uses_unsigned_bigint_pk_order() {
2364        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2365            &[
2366                (
2367                    Op::Insert,
2368                    OwnedRow::new(vec![
2369                        Some(ScalarImpl::Int64(4)),
2370                        Some(ScalarImpl::Int64(400)),
2371                        Some(ScalarImpl::Utf8(
2372                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2373                                .into(),
2374                        )),
2375                    ]),
2376                ),
2377                (
2378                    Op::Insert,
2379                    OwnedRow::new(vec![
2380                        // `u64::MAX` represented in RisingWave's `i64` storage.
2381                        Some(ScalarImpl::Int64(-1)),
2382                        Some(ScalarImpl::Int64(900)),
2383                        Some(ScalarImpl::Utf8(
2384                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2385                                .into(),
2386                        )),
2387                    ]),
2388                ),
2389            ],
2390            &[DataType::Int64, DataType::Int64, DataType::Varchar],
2391        )];
2392
2393        let (emitted_chunks, drained_row_count, drained_offset) =
2394            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2395                &MockExternalTableReader::get_cdc_offset_parser(),
2396                &mut upstream_chunk_buffer,
2397                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2398                PkCompareInfo {
2399                    indices: &[0],
2400                    order: &[OrderType::ascending()],
2401                    needs_unsigned_i64_compare: &[true],
2402                },
2403                &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2404                &[0, 1],
2405            )
2406            .unwrap();
2407
2408        assert_eq!(drained_row_count, 1);
2409        assert_eq!(
2410            drained_offset,
2411            Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2412        );
2413        assert_eq!(emitted_chunks.len(), 1);
2414        assert_eq!(
2415            emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
2416            OwnedRow::new(vec![
2417                Some(ScalarImpl::Int64(4)),
2418                Some(ScalarImpl::Int64(400))
2419            ])
2420        );
2421
2422        assert_eq!(upstream_chunk_buffer.len(), 1);
2423        assert_eq!(
2424            upstream_chunk_buffer[0]
2425                .rows()
2426                .next()
2427                .unwrap()
2428                .1
2429                .to_owned_row(),
2430            OwnedRow::new(vec![
2431                Some(ScalarImpl::Int64(-1)),
2432                Some(ScalarImpl::Int64(900)),
2433                Some(ScalarImpl::Utf8(
2434                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2435                        .into(),
2436                )),
2437            ])
2438        );
2439    }
2440
2441    #[test]
2442    fn test_consume_buffer_non_monotonic_pk_in_chunk() {
2443        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2444            &[
2445                (
2446                    Op::Insert,
2447                    OwnedRow::new(vec![
2448                        Some(ScalarImpl::Int64(1)),
2449                        Some(ScalarImpl::Int64(100)),
2450                        Some(ScalarImpl::Utf8(
2451                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2452                                .into(),
2453                        )),
2454                    ]),
2455                ),
2456                (
2457                    Op::Insert,
2458                    OwnedRow::new(vec![
2459                        Some(ScalarImpl::Int64(6)),
2460                        Some(ScalarImpl::Int64(600)),
2461                        Some(ScalarImpl::Utf8(
2462                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2463                                .into(),
2464                        )),
2465                    ]),
2466                ),
2467                (
2468                    Op::Insert,
2469                    OwnedRow::new(vec![
2470                        Some(ScalarImpl::Int64(2)),
2471                        Some(ScalarImpl::Int64(200)),
2472                        Some(ScalarImpl::Utf8(
2473                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
2474                                .into(),
2475                        )),
2476                    ]),
2477                ),
2478            ],
2479            &[DataType::Int64, DataType::Int64, DataType::Varchar],
2480        )];
2481
2482        let (emitted_chunks, drained_row_count, drained_offset) =
2483            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2484                &MockExternalTableReader::get_cdc_offset_parser(),
2485                &mut upstream_chunk_buffer,
2486                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2487                PkCompareInfo {
2488                    indices: &[0],
2489                    order: &[OrderType::ascending()],
2490                    needs_unsigned_i64_compare: &[false],
2491                },
2492                &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2493                &[0, 1],
2494            )
2495            .unwrap();
2496
2497        assert_eq!(drained_row_count, 2);
2498        assert_eq!(
2499            drained_offset,
2500            Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2501        );
2502        assert_eq!(emitted_chunks.len(), 1);
2503        assert_eq!(emitted_chunks[0].rows().count(), 2);
2504        assert_eq!(
2505            emitted_chunks[0]
2506                .rows()
2507                .map(|(_, row)| row.to_owned_row())
2508                .collect::<Vec<_>>(),
2509            vec![
2510                OwnedRow::new(vec![
2511                    Some(ScalarImpl::Int64(1)),
2512                    Some(ScalarImpl::Int64(100))
2513                ]),
2514                OwnedRow::new(vec![
2515                    Some(ScalarImpl::Int64(2)),
2516                    Some(ScalarImpl::Int64(200))
2517                ]),
2518            ]
2519        );
2520
2521        assert_eq!(upstream_chunk_buffer.len(), 1);
2522        assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
2523        assert_eq!(
2524            upstream_chunk_buffer[0]
2525                .rows()
2526                .next()
2527                .unwrap()
2528                .1
2529                .to_owned_row(),
2530            OwnedRow::new(vec![
2531                Some(ScalarImpl::Int64(6)),
2532                Some(ScalarImpl::Int64(600)),
2533                Some(ScalarImpl::Utf8(
2534                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2535                        .into(),
2536                )),
2537            ])
2538        );
2539    }
2540
2541    #[test]
2542    fn test_consume_buffer_processes_chunks_after_future_row() {
2543        let mut upstream_chunk_buffer = vec![
2544            StreamChunk::from_rows(
2545                &[
2546                    (
2547                        Op::Insert,
2548                        OwnedRow::new(vec![
2549                            Some(ScalarImpl::Int64(1)),
2550                            Some(ScalarImpl::Int64(100)),
2551                            Some(ScalarImpl::Utf8(
2552                                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
2553                                    .into(),
2554                            )),
2555                        ]),
2556                    ),
2557                    (
2558                        Op::Insert,
2559                        OwnedRow::new(vec![
2560                            Some(ScalarImpl::Int64(6)),
2561                            Some(ScalarImpl::Int64(600)),
2562                            Some(ScalarImpl::Utf8(
2563                                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2564                                    .into(),
2565                            )),
2566                        ]),
2567                    ),
2568                ],
2569                &[DataType::Int64, DataType::Int64, DataType::Varchar],
2570            ),
2571            StreamChunk::from_rows(
2572                &[(
2573                    Op::Insert,
2574                    OwnedRow::new(vec![
2575                        Some(ScalarImpl::Int64(2)),
2576                        Some(ScalarImpl::Int64(200)),
2577                        Some(ScalarImpl::Utf8(
2578                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
2579                                .into(),
2580                        )),
2581                    ]),
2582                )],
2583                &[DataType::Int64, DataType::Int64, DataType::Varchar],
2584            ),
2585        ];
2586
2587        let (emitted_chunks, drained_row_count, drained_offset) =
2588            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2589                &MockExternalTableReader::get_cdc_offset_parser(),
2590                &mut upstream_chunk_buffer,
2591                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
2592                PkCompareInfo {
2593                    indices: &[0],
2594                    order: &[OrderType::ascending()],
2595                    needs_unsigned_i64_compare: &[false],
2596                },
2597                &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 2))),
2598                &[0, 1],
2599            )
2600            .unwrap();
2601
2602        assert_eq!(drained_row_count, 2);
2603        assert_eq!(
2604            drained_offset,
2605            Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3)))
2606        );
2607        assert_eq!(emitted_chunks.len(), 2);
2608        assert_eq!(emitted_chunks[0].rows().count(), 1);
2609        assert_eq!(emitted_chunks[1].rows().count(), 1);
2610        assert_eq!(
2611            emitted_chunks[1].rows().next().unwrap().1.to_owned_row(),
2612            OwnedRow::new(vec![
2613                Some(ScalarImpl::Int64(2)),
2614                Some(ScalarImpl::Int64(200))
2615            ])
2616        );
2617
2618        assert_eq!(upstream_chunk_buffer.len(), 1);
2619        assert_eq!(
2620            upstream_chunk_buffer[0]
2621                .rows()
2622                .next()
2623                .unwrap()
2624                .1
2625                .to_owned_row(),
2626            OwnedRow::new(vec![
2627                Some(ScalarImpl::Int64(6)),
2628                Some(ScalarImpl::Int64(600)),
2629                Some(ScalarImpl::Utf8(
2630                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2631                        .into(),
2632                )),
2633            ])
2634        );
2635    }
2636
2637    #[test]
2638    fn test_consume_buffer_advances_offset_after_preceding_rows_are_emitted() {
2639        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
2640            &[
2641                (
2642                    Op::Insert,
2643                    OwnedRow::new(vec![
2644                        Some(ScalarImpl::Int64(6)),
2645                        Some(ScalarImpl::Int64(600)),
2646                        Some(ScalarImpl::Utf8(
2647                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
2648                                .into(),
2649                        )),
2650                    ]),
2651                ),
2652                (
2653                    Op::Insert,
2654                    OwnedRow::new(vec![
2655                        Some(ScalarImpl::Int64(2)),
2656                        Some(ScalarImpl::Int64(200)),
2657                        Some(ScalarImpl::Utf8(
2658                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
2659                                .into(),
2660                        )),
2661                    ]),
2662                ),
2663            ],
2664            &[DataType::Int64, DataType::Int64, DataType::Varchar],
2665        )];
2666
2667        let (_, drained_row_count, drained_offset) =
2668            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
2669                &MockExternalTableReader::get_cdc_offset_parser(),
2670                &mut upstream_chunk_buffer,
2671                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(6))])),
2672                PkCompareInfo {
2673                    indices: &[0],
2674                    order: &[OrderType::ascending()],
2675                    needs_unsigned_i64_compare: &[false],
2676                },
2677                &Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 3))),
2678                &[0, 1],
2679            )
2680            .unwrap();
2681
2682        assert_eq!(drained_row_count, 2);
2683        assert_eq!(
2684            drained_offset,
2685            Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 5)))
2686        );
2687        assert!(upstream_chunk_buffer.is_empty());
2688    }
2689
2690    #[tokio::test]
2691    async fn test_cdc_backfill_persists_buffered_offset_on_checkpoint() {
2692        let memory_state_store = MemoryStateStore::new();
2693        let state_table = create_cdc_state_table(memory_state_store.clone()).await;
2694
2695        let (mut tx, executor) = create_mock_cdc_backfill(
2696            create_mock_external_table(),
2697            state_table,
2698            CdcScanOptions {
2699                snapshot_barrier_interval: 10,
2700                ..Default::default()
2701            },
2702        );
2703        let executor = executor.execute_inner();
2704        pin_mut!(executor);
2705
2706        tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
2707        assert!(matches!(
2708            executor.next().await.unwrap().unwrap(),
2709            Message::Barrier(_)
2710        ));
2711
2712        tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
2713        assert!(matches!(
2714            executor.next().await.unwrap().unwrap(),
2715            Message::Barrier(_)
2716        ));
2717
2718        assert!(matches!(
2719            executor.next().await.unwrap().unwrap(),
2720            Message::Chunk(_)
2721        ));
2722
2723        tx.push_chunk(create_raw_cdc_chunk(&[
2724            (
2725                r#"{ "payload": { "before": null, "after": { "id": 1, "price": 10.01 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#,
2726                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
2727            ),
2728            (
2729                r#"{ "payload": { "before": null, "after": { "id": 6, "price": 66.06 }, "source": { "version": "1.9.7.Final", "connector": "mysql", "name": "RW_CDC_1002" }, "op": "r", "ts_ms": 1695277757017, "transaction": null } }"#,
2730                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
2731            ),
2732        ]));
2733        tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
2734
2735        assert!(matches!(
2736            executor.next().await.unwrap().unwrap(),
2737            Message::Chunk(_)
2738        ));
2739        assert!(matches!(
2740            executor.next().await.unwrap().unwrap(),
2741            Message::Barrier(_)
2742        ));
2743
2744        let mut restored_state = CdcBackfillState::new(
2745            TableId::new(1234),
2746            create_cdc_state_table(memory_state_store).await,
2747            5,
2748        );
2749        restored_state
2750            .init_epoch(Barrier::new_test_barrier(test_epoch(3)).epoch)
2751            .await
2752            .unwrap();
2753        let state = restored_state.restore_state().await.unwrap();
2754        assert_eq!(
2755            state.last_cdc_offset,
2756            Some(CdcOffset::Mysql(MysqlOffset::new("1.binlog".to_owned(), 4)))
2757        );
2758    }
2759}