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::future::Future;
17use std::pin::Pin;
18
19use either::Either;
20use futures::stream::select_with_strategy;
21use futures::{Stream, stream};
22use itertools::Itertools;
23use risingwave_common::array::{DataChunk, Op};
24use risingwave_common::bail;
25use risingwave_common::bitmap::BitmapBuilder;
26use risingwave_common::catalog::ColumnDesc;
27use risingwave_common::row::RowExt;
28use risingwave_common::util::sort_util::OrderType;
29use risingwave_connector::parser::{
30    BigintUnsignedHandlingMode, ByteStreamSourceParser, DebeziumParser, DebeziumProps,
31    EncodingProperties, JsonProperties, ProtocolProperties, SourceStreamChunkBuilder,
32    SpecificParserConfig, TimeHandling, TimestampHandling, TimestamptzHandling,
33};
34use risingwave_connector::source::cdc::CdcScanOptions;
35use risingwave_connector::source::cdc::external::{
36    CdcOffset, ExternalCdcTableType, ExternalTableReaderImpl,
37};
38use risingwave_connector::source::{SourceColumnDesc, SourceContext, SourceCtrlOpts};
39use risingwave_pb::common::ThrottleType;
40use rw_futures_util::pausable;
41use thiserror_ext::AsReport;
42use tracing::Instrument;
43
44use crate::executor::backfill::cdc::state::CdcBackfillState;
45use crate::executor::backfill::cdc::upstream_table::external::ExternalStorageTable;
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::source::get_infinite_backoff_strategy;
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
67// The TimestampHandling/TimestamptzHandling/TimeHandling parser's behavior depends on the debezium.time.precision.mode setting:
68// - If left unset, Debezium defaults to time.precision.mode=microseconds (per debezium.properties), and the parser uses Micro.
69// - If set to "connect", Debezium uses time.precision.mode=connect, and the parser uses Milli by design.
70// - If set to any other value, Debezium applies that specific value, and the default parser is used to maintain backward compatibility.
71pub(crate) fn get_cdc_json_parse_handling_from_properties(
72    properties: &BTreeMap<String, String>,
73) -> (
74    Option<TimestampHandling>,
75    Option<TimestamptzHandling>,
76    Option<TimeHandling>,
77    Option<BigintUnsignedHandlingMode>,
78) {
79    let (timestamp_handling, timestamptz_handling, time_handling) = match properties
80        .get("debezium.time.precision.mode")
81    {
82        None => (
83            Some(TimestampHandling::Micro),
84            Some(TimestamptzHandling::Micro),
85            Some(TimeHandling::Micro),
86        ),
87        Some(m) if m == "connect" => (
88            Some(TimestampHandling::Milli),
89            Some(TimestamptzHandling::Milli),
90            Some(TimeHandling::Milli),
91        ),
92        Some(other) => {
93            // backward compatibility.
94            tracing::warn!(
95                "Unsupported debezium.time.precision.mode = {other}, fall back to default parser."
96            );
97            (None, None, None)
98        }
99    };
100    let bigint_unsigned_handling = properties
101        .get("debezium.bigint.unsigned.handling.mode")
102        .is_some_and(|v| v == "precise")
103        .then_some(BigintUnsignedHandlingMode::Precise);
104    (
105        timestamp_handling,
106        timestamptz_handling,
107        time_handling,
108        bigint_unsigned_handling,
109    )
110}
111
112pub struct CdcBackfillExecutor<S: StateStore> {
113    actor_ctx: ActorContextRef,
114
115    /// The external table to be backfilled
116    external_table: ExternalStorageTable,
117
118    /// Upstream changelog stream which may contain metadata columns, e.g. `_rw_offset`
119    upstream: Executor,
120
121    /// The column indices need to be forwarded to the downstream from the upstream and table scan.
122    output_indices: Vec<usize>,
123
124    /// The schema of output chunk, including additional columns if any
125    output_columns: Vec<ColumnDesc>,
126
127    /// State table of the `CdcBackfill` executor
128    state_impl: CdcBackfillState<S>,
129
130    // TODO: introduce a CdcBackfillProgress to report finish to Meta
131    // This object is just a stub right now
132    progress: Option<CreateMviewProgressReporter>,
133
134    metrics: CdcBackfillMetrics,
135
136    /// Rate limit in rows/s.
137    rate_limit_rps: Option<u32>,
138
139    options: CdcScanOptions,
140
141    properties: BTreeMap<String, String>,
142}
143
144impl<S: StateStore> CdcBackfillExecutor<S> {
145    #[expect(clippy::too_many_arguments)]
146    pub fn new(
147        actor_ctx: ActorContextRef,
148        external_table: ExternalStorageTable,
149        upstream: Executor,
150        output_indices: Vec<usize>,
151        output_columns: Vec<ColumnDesc>,
152        progress: Option<CreateMviewProgressReporter>,
153        metrics: Arc<StreamingMetrics>,
154        state_table: StateTable<S>,
155        rate_limit_rps: Option<u32>,
156        options: CdcScanOptions,
157        properties: BTreeMap<String, String>,
158    ) -> Self {
159        let pk_indices = external_table.pk_indices();
160        let upstream_table_id = external_table.table_id();
161        let state_impl = CdcBackfillState::new(
162            upstream_table_id,
163            state_table,
164            pk_indices.len() + METADATA_STATE_LEN,
165        );
166
167        let metrics = metrics.new_cdc_backfill_metrics(external_table.table_id(), actor_ctx.id);
168        Self {
169            actor_ctx,
170            external_table,
171            upstream,
172            output_indices,
173            output_columns,
174            state_impl,
175            progress,
176            metrics,
177            rate_limit_rps,
178            options,
179            properties,
180        }
181    }
182
183    fn report_metrics(
184        metrics: &CdcBackfillMetrics,
185        snapshot_processed_row_count: u64,
186        upstream_processed_row_count: u64,
187    ) {
188        metrics
189            .cdc_backfill_snapshot_read_row_count
190            .inc_by(snapshot_processed_row_count);
191
192        metrics
193            .cdc_backfill_upstream_output_row_count
194            .inc_by(upstream_processed_row_count);
195    }
196
197    fn consume_upstream_chunk_buffer(
198        offset_parse_func: &risingwave_connector::source::cdc::external::CdcOffsetParseFunc,
199        upstream_chunk_buffer: &mut Vec<StreamChunk>,
200        current_pk_pos: Option<&OwnedRow>,
201        pk_compare: PkCompareInfo<'_>,
202        last_binlog_offset: &Option<CdcOffset>,
203        output_indices: &[usize],
204    ) -> StreamExecutorResult<(Vec<StreamChunk>, u64, Option<CdcOffset>)> {
205        let Some(current_pos) = current_pk_pos else {
206            return Ok((vec![], 0, None));
207        };
208
209        let buffered_chunks = std::mem::take(upstream_chunk_buffer);
210        let mut emitted_chunks = Vec::with_capacity(buffered_chunks.len());
211        let mut upstream_processed_row_count = 0;
212        let mut consumed_binlog_offset = None;
213        let mut retained_chunks = Vec::with_capacity(buffered_chunks.len());
214        let mut can_advance_consumed_offset = true;
215
216        for chunk in buffered_chunks {
217            let mut emitted_vis = BitmapBuilder::zeroed(chunk.capacity());
218            let mut retained_vis = BitmapBuilder::zeroed(chunk.capacity());
219            for idx in 0..chunk.capacity() {
220                let (op, row, visible) = chunk.row_at(idx);
221                if !visible {
222                    // Buffered chunks may carry sparse visibility from previous filtering rounds.
223                    continue;
224                }
225                let event_offset = (*offset_parse_func)(
226                    row.iter()
227                        .last()
228                        .flatten()
229                        .expect("cdc offset must exist")
230                        .into_utf8(),
231                )?;
232                let in_binlog_range = last_binlog_offset
233                    .as_ref()
234                    .is_none_or(|binlog_low| *binlog_low <= event_offset);
235
236                let row_pk = row.project(pk_compare.indices);
237                let reached_current_pos = cmp_pk_unsigned_aware(
238                    row_pk.iter(),
239                    current_pos.iter(),
240                    pk_compare.order,
241                    pk_compare.needs_unsigned_i64_compare,
242                )
243                .is_le();
244                if !in_binlog_range {
245                    continue;
246                }
247                let should_emit = reached_current_pos;
248
249                match op {
250                    Op::Insert | Op::Delete => {
251                        if should_emit {
252                            emitted_vis.set(idx, true);
253                            if can_advance_consumed_offset {
254                                consumed_binlog_offset = Some(event_offset);
255                            }
256                            upstream_processed_row_count += 1;
257                        } else {
258                            retained_vis.set(idx, true);
259                            can_advance_consumed_offset = false;
260                        }
261                    }
262                    Op::UpdateDelete | Op::UpdateInsert => {
263                        unreachable!("CDC buffered chunks should not contain update pairs")
264                    }
265                }
266            }
267
268            let emitted_vis = emitted_vis.finish();
269            if emitted_vis.count_ones() > 0 {
270                emitted_chunks.push(mapping_chunk(
271                    chunk.clone_with_vis(emitted_vis),
272                    output_indices,
273                ));
274            }
275
276            let retained_vis = retained_vis.finish();
277            if retained_vis.count_ones() > 0 {
278                retained_chunks.push(chunk.clone_with_vis(retained_vis));
279            }
280        }
281
282        *upstream_chunk_buffer = retained_chunks;
283
284        Ok((
285            emitted_chunks,
286            upstream_processed_row_count,
287            consumed_binlog_offset,
288        ))
289    }
290
291    #[try_stream(ok = Message, error = StreamExecutorError)]
292    async fn execute_inner(mut self) {
293        // The indices to primary key columns
294        let pk_indices = self.external_table.pk_indices().to_vec();
295        let pk_order = self.external_table.pk_order_types().to_vec();
296
297        let table_id = self.external_table.table_id();
298        let upstream_table_name = self.external_table.qualified_table_name();
299        let schema_table_name = self.external_table.schema_table_name().clone();
300        let external_database_name = self.external_table.database_name().to_owned();
301
302        let additional_columns = self
303            .output_columns
304            .iter()
305            .filter(|col| col.additional_column.column_type.is_some())
306            .cloned()
307            .collect_vec();
308
309        let mut upstream = self.upstream.execute();
310
311        // Current position of the upstream_table storage primary key.
312        // `None` means it starts from the beginning.
313        let mut current_pk_pos: Option<OwnedRow>;
314
315        // Poll the upstream to get the first barrier.
316        let first_barrier = expect_first_barrier(&mut upstream).await?;
317
318        let mut is_snapshot_paused = first_barrier.is_pause_on_startup();
319        let first_barrier_epoch = first_barrier.epoch;
320        // The first barrier message should be propagated.
321        yield Message::Barrier(first_barrier);
322
323        // Check whether this parallelism has been assigned splits,
324        // if not, we should bypass the backfill directly.
325        let mut state_impl = self.state_impl;
326
327        state_impl.init_epoch(first_barrier_epoch).await?;
328
329        // restore backfill state
330        let state = state_impl.restore_state().await?;
331        current_pk_pos = state.current_pk_pos.clone();
332
333        let need_backfill = !self.options.disable_backfill && !state.is_finished;
334
335        // Keep track of rows from the snapshot.
336        let mut total_snapshot_row_count = state.row_count as u64;
337
338        let (timestamp_handling, timestamptz_handling, time_handling, bigint_unsigned_handling) =
339            get_cdc_json_parse_handling_from_properties(&self.properties);
340        // Only postgres-cdc connector may trigger TOAST.
341        let handle_toast_columns: bool =
342            self.external_table.table_type() == &ExternalCdcTableType::Postgres;
343        // Make sure to use mapping_message after transform_upstream.
344        let mut upstream = transform_upstream(
345            upstream,
346            self.output_columns.clone(),
347            timestamp_handling,
348            timestamptz_handling,
349            time_handling,
350            bigint_unsigned_handling,
351            handle_toast_columns,
352        )
353        .boxed()
354        .peekable();
355        let mut last_binlog_offset = state.last_cdc_offset.clone();
356
357        // CDC Backfill Algorithm:
358        //
359        // When the first barrier comes from upstream:
360        //  - read the current binlog offset as `binlog_low`
361        //  - start a snapshot read upon upstream table and iterate over the snapshot read stream
362        //  - buffer the changelog event from upstream
363        //
364        // When a new barrier comes from upstream:
365        //  - read the current binlog offset as `binlog_high`
366        //  - for each row of the upstream change log, forward it to downstream if it in the range
367        //    of [binlog_low, binlog_high] and its pk <= `current_pos`, otherwise keep buffering it
368        //    until `current_pos` catches up
369        //  - reconstruct the whole backfill stream with upstream changelog and a new table snapshot
370        //
371        // When a chunk comes from snapshot, we forward it to the downstream and raise
372        // `current_pos`.
373        // When we reach the end of the snapshot read stream, it means backfill has been
374        // finished.
375        //
376        // Once the backfill loop ends, we forward the upstream directly to the downstream.
377        if need_backfill {
378            // After init the state table and forward the initial barrier to downstream,
379            // we now try to create the table reader with retry.
380            // If backfill hasn't finished, we can ignore upstream cdc events before we create the table reader.
381            let mut table_reader: Option<ExternalTableReaderImpl> = None;
382            let external_table = self.external_table.clone();
383            let actor_id = self.actor_ctx.id;
384            let fragment_id = self.actor_ctx.fragment_id;
385            let mut future = Box::pin(async move {
386                let backoff = get_infinite_backoff_strategy();
387                tokio_retry::Retry::spawn(backoff, || async {
388                    match external_table.create_table_reader().await {
389                        Ok(reader) => Ok(reader),
390                        Err(e) => {
391                            tracing::warn!(error = %e.as_report(), actor_id = %actor_id, fragment_id = %fragment_id, "failed to create cdc table reader, retrying...");
392                            Err(e)
393                        }
394                    }
395                })
396                .instrument(tracing::info_span!("create_cdc_table_reader_with_retry"))
397                .await
398                .expect("Retry create cdc table reader until success.")
399            });
400            loop {
401                if let Some(msg) =
402                    build_reader_and_poll_upstream(&mut upstream, &mut table_reader, &mut future)
403                        .await?
404                {
405                    if let Some(msg) = mapping_message(msg, &self.output_indices) {
406                        match msg {
407                            Message::Barrier(barrier) => {
408                                // commit state to bump the epoch of state table
409                                state_impl.commit_state(barrier.epoch).await?;
410                                yield Message::Barrier(barrier);
411                            }
412                            Message::Chunk(_) => {
413                                // ignore chunk if we need backfill, since we can read the data from the snapshot
414                            }
415                            Message::Watermark(_) => {
416                                // ignore watermark
417                            }
418                        }
419                    }
420                } else {
421                    assert!(table_reader.is_some(), "table reader must created");
422                    tracing::info!(
423                        %table_id,
424                        upstream_table_name,
425                        "table reader created successfully"
426                    );
427                    break;
428                }
429            }
430
431            let upstream_table_reader = UpstreamTableReader::new(
432                self.external_table.clone(),
433                table_reader.expect("table reader must created"),
434            );
435
436            if last_binlog_offset.is_none() {
437                // Limit concurrent CDC connections globally to 10 using a semaphore.
438                static CDC_CONN_SEMAPHORE: tokio::sync::Semaphore =
439                    tokio::sync::Semaphore::const_new(10);
440
441                let _permit = CDC_CONN_SEMAPHORE.acquire().await.unwrap();
442                last_binlog_offset = upstream_table_reader.current_cdc_offset().await?;
443            }
444
445            let offset_parse_func = upstream_table_reader.reader.get_cdc_offset_parser();
446            let mut consumed_binlog_offset: Option<CdcOffset> = None;
447
448            // Whether each pk column needs unsigned `i64` comparison. Frontend up-casts narrower
449            // unsigned integers, while unsigned float/double/decimal keep their native comparison
450            // semantics; only `BIGINT UNSIGNED` can overflow into a negative `i64` in RisingWave.
451            let pk_needs_unsigned_i64_compare = {
452                let schema = self.external_table.schema();
453                let pk_names: Vec<String> = pk_indices
454                    .iter()
455                    .map(|&i| schema.fields[i].name.clone())
456                    .collect();
457                upstream_table_reader
458                    .reader
459                    .pk_column_unsigned_i64_compare_flags(&pk_names)?
460            };
461
462            tracing::info!(
463                %table_id,
464                upstream_table_name,
465                initial_binlog_offset = ?last_binlog_offset,
466                ?current_pk_pos,
467                is_finished = state.is_finished,
468                is_snapshot_paused,
469                snapshot_row_count = total_snapshot_row_count,
470                rate_limit = self.rate_limit_rps,
471                disable_backfill = self.options.disable_backfill,
472                snapshot_barrier_interval = self.options.snapshot_barrier_interval,
473                snapshot_batch_size = self.options.snapshot_batch_size,
474                "start cdc backfill",
475            );
476
477            // drive the upstream changelog first to ensure we can receive timely changelog event,
478            // otherwise the upstream changelog may be blocked by the snapshot read stream
479            let _ = Pin::new(&mut upstream).peek().await;
480
481            // wait for a barrier to make sure the backfill starts after upstream source
482            #[for_await]
483            for msg in upstream.by_ref() {
484                match msg? {
485                    Message::Barrier(barrier) => {
486                        match barrier.mutation.as_deref() {
487                            Some(crate::executor::Mutation::Pause) => {
488                                is_snapshot_paused = true;
489                                tracing::info!(
490                                    %table_id,
491                                    upstream_table_name,
492                                    "snapshot is paused by barrier"
493                                );
494                            }
495                            Some(crate::executor::Mutation::Resume) => {
496                                is_snapshot_paused = false;
497                                tracing::info!(
498                                    %table_id,
499                                    upstream_table_name,
500                                    "snapshot is resumed by barrier"
501                                );
502                            }
503                            _ => {
504                                // ignore other mutations
505                            }
506                        }
507                        // commit state just to bump the epoch of state table
508                        state_impl.commit_state(barrier.epoch).await?;
509                        yield Message::Barrier(barrier);
510                        break;
511                    }
512                    Message::Chunk(ref chunk) => {
513                        last_binlog_offset = get_cdc_chunk_last_offset(&offset_parse_func, chunk)?;
514                    }
515                    Message::Watermark(_) => {
516                        // Ignore watermark
517                    }
518                }
519            }
520
521            tracing::info!(%table_id,
522                upstream_table_name,
523                initial_binlog_offset = ?last_binlog_offset,
524                ?current_pk_pos,
525                is_snapshot_paused,
526                "start cdc backfill loop");
527
528            // the buffer will be drained when a barrier comes
529            let mut upstream_chunk_buffer: Vec<StreamChunk> = vec![];
530
531            'backfill_loop: loop {
532                let mut should_bypass_snapshot_stream_patch =
533                    self.rate_limit_rps.is_some_and(|val| val == 0);
534                let left_upstream = upstream.by_ref().map(Either::Left);
535
536                let mut snapshot_read_row_cnt: usize = 0;
537                let read_args = SnapshotReadArgs::new(
538                    current_pk_pos.clone(),
539                    self.rate_limit_rps,
540                    pk_indices.clone(),
541                    additional_columns.clone(),
542                    schema_table_name.clone(),
543                    external_database_name.clone(),
544                );
545                let right_snapshot = pin!(
546                    upstream_table_reader
547                        .snapshot_read_full_table(read_args, self.options.snapshot_batch_size)
548                        .map(Either::Right)
549                );
550                let (right_snapshot, snapshot_valve) = pausable(right_snapshot);
551                if is_snapshot_paused {
552                    snapshot_valve.pause();
553                }
554
555                // Prefer to select upstream, so we can stop snapshot stream when barrier comes.
556                let mut backfill_stream =
557                    select_with_strategy(left_upstream, right_snapshot, |_: &mut ()| {
558                        stream::PollNext::Left
559                    });
560
561                let mut cur_barrier_snapshot_processed_rows: u64 = 0;
562                let mut cur_barrier_upstream_processed_rows: u64 = 0;
563                let mut barrier_count: u32 = 0;
564                let mut pending_barrier = None;
565
566                #[for_await]
567                for either in &mut backfill_stream {
568                    match either {
569                        // Upstream
570                        Either::Left(msg) => {
571                            match msg? {
572                                Message::Barrier(barrier) => {
573                                    // increase the barrier count and check whether need to start a new snapshot
574                                    barrier_count += 1;
575                                    let can_start_new_snapshot =
576                                        barrier_count == self.options.snapshot_barrier_interval;
577                                    let mut needs_rebuild_snapshot = false;
578
579                                    if let Some(mutation) = barrier.mutation.as_deref() {
580                                        use crate::executor::Mutation;
581                                        match mutation {
582                                            Mutation::Pause => {
583                                                is_snapshot_paused = true;
584                                                snapshot_valve.pause();
585                                            }
586                                            Mutation::Resume => {
587                                                is_snapshot_paused = false;
588                                                snapshot_valve.resume();
589                                            }
590                                            Mutation::Throttle(some) => {
591                                                if let Some(entry) =
592                                                    some.get(&self.actor_ctx.fragment_id)
593                                                    && entry.throttle_type()
594                                                        == ThrottleType::Backfill
595                                                    && entry.rate_limit != self.rate_limit_rps
596                                                {
597                                                    // If self.rate_limit_rps is 0, the snapshot stream does not establish a snapshot_read.
598                                                    // Consequently, the later snapshot stream patch is bypassed.
599                                                    should_bypass_snapshot_stream_patch = self
600                                                        .rate_limit_rps
601                                                        .is_some_and(|val| val == 0);
602                                                    self.rate_limit_rps = entry.rate_limit;
603                                                    needs_rebuild_snapshot = true;
604                                                }
605                                            }
606                                            mutation if mutation.is_stop(self.actor_ctx.id) => {
607                                                // the actor has been dropped, exit the backfill loop
608                                                tracing::info!(
609                                                    %table_id,
610                                                    upstream_table_name,
611                                                    "CdcBackfill has been dropped due to config change"
612                                                );
613                                                yield Message::Barrier(barrier);
614                                                break 'backfill_loop;
615                                            }
616                                            _ => (),
617                                        }
618                                    }
619
620                                    // when processing a barrier, check whether can start a new snapshot
621                                    // if the number of barriers reaches the snapshot interval
622                                    if can_start_new_snapshot || needs_rebuild_snapshot {
623                                        // staging the barrier
624                                        pending_barrier = Some(barrier);
625                                        tracing::debug!(
626                                            %table_id,
627                                            ?current_pk_pos,
628                                            ?snapshot_read_row_cnt,
629                                            "Prepare to start a new snapshot"
630                                        );
631                                        // Break the loop for consuming snapshot and prepare to start a new snapshot
632                                        break;
633                                    } else {
634                                        // Drain the in-memory buffer to ensure no data is lost during the recovery process.
635                                        let (
636                                            emitted_upstream_chunks,
637                                            consumed_upstream_row_count,
638                                            consumed_binlog_offset,
639                                        ) = Self::consume_upstream_chunk_buffer(
640                                            &offset_parse_func,
641                                            &mut upstream_chunk_buffer,
642                                            current_pk_pos.as_ref(),
643                                            PkCompareInfo {
644                                                indices: &pk_indices,
645                                                order: &pk_order,
646                                                needs_unsigned_i64_compare:
647                                                    &pk_needs_unsigned_i64_compare,
648                                            },
649                                            &last_binlog_offset,
650                                            &self.output_indices,
651                                        )?;
652                                        cur_barrier_upstream_processed_rows +=
653                                            consumed_upstream_row_count;
654                                        if let Some(consumed_binlog_offset) = consumed_binlog_offset
655                                        {
656                                            last_binlog_offset = Some(consumed_binlog_offset);
657                                        }
658                                        for chunk in emitted_upstream_chunks {
659                                            yield Message::Chunk(chunk);
660                                        }
661
662                                        Self::report_metrics(
663                                            &self.metrics,
664                                            cur_barrier_snapshot_processed_rows,
665                                            cur_barrier_upstream_processed_rows,
666                                        );
667
668                                        // update and persist current backfill progress
669                                        state_impl
670                                            .mutate_state(
671                                                current_pk_pos.clone(),
672                                                last_binlog_offset.clone(),
673                                                total_snapshot_row_count,
674                                                false,
675                                            )
676                                            .await?;
677
678                                        state_impl.commit_state(barrier.epoch).await?;
679
680                                        // emit barrier and continue consume the backfill stream
681                                        yield Message::Barrier(barrier);
682                                    }
683                                }
684                                Message::Chunk(chunk) => {
685                                    // skip empty upstream chunk
686                                    if chunk.cardinality() == 0 {
687                                        continue;
688                                    }
689
690                                    let chunk_binlog_offset =
691                                        get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
692
693                                    tracing::trace!(
694                                        "recv changelog chunk: chunk_offset {:?}, capactiy {}",
695                                        chunk_binlog_offset,
696                                        chunk.capacity()
697                                    );
698
699                                    // Since we don't need changelog before the
700                                    // `last_binlog_offset`, skip the chunk that *only* contains
701                                    // events before `last_binlog_offset`.
702                                    if let Some(last_binlog_offset) = last_binlog_offset.as_ref()
703                                        && let Some(chunk_offset) = chunk_binlog_offset
704                                        && chunk_offset < *last_binlog_offset
705                                    {
706                                        tracing::trace!(
707                                            "skip changelog chunk: chunk_offset {:?}, capacity {}",
708                                            chunk_offset,
709                                            chunk.capacity()
710                                        );
711                                        continue;
712                                    }
713                                    // Buffer the upstream chunk.
714                                    upstream_chunk_buffer.push(chunk.compact_vis());
715                                }
716                                Message::Watermark(_) => {
717                                    // Ignore watermark during backfill.
718                                }
719                            }
720                        }
721                        // Snapshot read
722                        Either::Right(msg) => {
723                            match msg? {
724                                None => {
725                                    tracing::info!(
726                                        %table_id,
727                                        ?last_binlog_offset,
728                                        ?current_pk_pos,
729                                        "snapshot read stream ends"
730                                    );
731                                    // If the snapshot read stream ends, it means all historical
732                                    // data has been loaded.
733                                    // We should not mark the chunk anymore,
734                                    // otherwise, we will ignore some rows in the buffer.
735                                    for chunk in upstream_chunk_buffer.drain(..) {
736                                        yield Message::Chunk(mapping_chunk(
737                                            chunk,
738                                            &self.output_indices,
739                                        ));
740                                    }
741
742                                    // backfill has finished, exit the backfill loop and persist the state when we recv a barrier
743                                    break 'backfill_loop;
744                                }
745                                Some(chunk) => {
746                                    // Raise the current position.
747                                    // As snapshot read streams are ordered by pk, so we can
748                                    // just use the last row to update `current_pos`.
749                                    current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
750
751                                    tracing::trace!(
752                                        "got a snapshot chunk: len {}, current_pk_pos {:?}",
753                                        chunk.cardinality(),
754                                        current_pk_pos
755                                    );
756                                    let chunk_cardinality = chunk.cardinality() as u64;
757                                    cur_barrier_snapshot_processed_rows += chunk_cardinality;
758                                    total_snapshot_row_count += chunk_cardinality;
759                                    yield Message::Chunk(mapping_chunk(
760                                        chunk,
761                                        &self.output_indices,
762                                    ));
763                                }
764                            }
765                        }
766                    }
767                }
768
769                assert!(pending_barrier.is_some(), "pending_barrier must exist");
770                let pending_barrier = pending_barrier.unwrap();
771
772                // The snapshot stream patch:
773                // Here we have to ensure the snapshot stream is consumed at least once,
774                // since the barrier event can kick in anytime.
775                // Otherwise, the result set of the new snapshot stream may become empty.
776                // It maybe a cancellation bug of the mysql driver.
777                let (_, mut snapshot_stream) = backfill_stream.into_inner();
778                // Resume the snapshot stream so that the snapshot stream patch won't block.
779                if !should_bypass_snapshot_stream_patch && is_snapshot_paused {
780                    snapshot_valve.resume();
781                }
782                if !should_bypass_snapshot_stream_patch
783                    && let Some(msg) = snapshot_stream
784                        .next()
785                        .instrument_await("consume_snapshot_stream_once")
786                        .await
787                {
788                    let Either::Right(msg) = msg else {
789                        bail!("BUG: snapshot_read contains upstream messages");
790                    };
791                    match msg? {
792                        None => {
793                            tracing::info!(
794                                %table_id,
795                                ?last_binlog_offset,
796                                ?current_pk_pos,
797                                "snapshot read stream ends in the force emit branch"
798                            );
799                            // End of the snapshot read stream.
800                            // Consume the buffered upstream chunk without filtering by `binlog_low`.
801                            for chunk in upstream_chunk_buffer.drain(..) {
802                                yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
803                            }
804
805                            // mark backfill has finished
806                            state_impl
807                                .mutate_state(
808                                    current_pk_pos.clone(),
809                                    last_binlog_offset.clone(),
810                                    total_snapshot_row_count,
811                                    true,
812                                )
813                                .await?;
814
815                            // commit state because we have received a barrier message
816                            state_impl.commit_state(pending_barrier.epoch).await?;
817                            yield Message::Barrier(pending_barrier);
818                            // end of backfill loop, since backfill has finished
819                            break 'backfill_loop;
820                        }
821                        Some(_) if is_snapshot_paused => {
822                            // Since the snapshot stream is paused, drop the chunk.
823                        }
824                        Some(chunk) => {
825                            // Raise the current pk position.
826                            current_pk_pos = Some(get_new_pos(&chunk, &pk_indices));
827
828                            let row_count = chunk.cardinality() as u64;
829                            cur_barrier_snapshot_processed_rows += row_count;
830                            total_snapshot_row_count += row_count;
831                            snapshot_read_row_cnt += row_count as usize;
832
833                            tracing::debug!(
834                                %table_id,
835                                ?current_pk_pos,
836                                ?snapshot_read_row_cnt,
837                                "force emit a snapshot chunk"
838                            );
839                            yield Message::Chunk(mapping_chunk(chunk, &self.output_indices));
840                        }
841                    }
842                }
843
844                // If the number of barriers reaches the snapshot interval,
845                // consume the buffered upstream chunks.
846                if let Some(current_pos) = &current_pk_pos {
847                    for chunk in upstream_chunk_buffer.drain(..) {
848                        cur_barrier_upstream_processed_rows += chunk.cardinality() as u64;
849
850                        // record the consumed binlog offset that will be
851                        // persisted later
852                        consumed_binlog_offset =
853                            get_cdc_chunk_last_offset(&offset_parse_func, &chunk)?;
854
855                        yield Message::Chunk(mapping_chunk(
856                            mark_cdc_chunk(
857                                &offset_parse_func,
858                                chunk,
859                                current_pos,
860                                &pk_indices,
861                                &pk_order,
862                                &pk_needs_unsigned_i64_compare,
863                                last_binlog_offset.clone(),
864                            )?,
865                            &self.output_indices,
866                        ));
867                    }
868                } else {
869                    // If no current_pos, means we did not process any snapshot yet.
870                    // we can just ignore the upstream buffer chunk in that case.
871                    upstream_chunk_buffer.clear();
872                }
873
874                // Update last seen binlog offset
875                if consumed_binlog_offset.is_some() {
876                    last_binlog_offset.clone_from(&consumed_binlog_offset);
877                }
878
879                Self::report_metrics(
880                    &self.metrics,
881                    cur_barrier_snapshot_processed_rows,
882                    cur_barrier_upstream_processed_rows,
883                );
884
885                // update and persist current backfill progress
886                state_impl
887                    .mutate_state(
888                        current_pk_pos.clone(),
889                        last_binlog_offset.clone(),
890                        total_snapshot_row_count,
891                        false,
892                    )
893                    .await?;
894
895                state_impl.commit_state(pending_barrier.epoch).await?;
896                yield Message::Barrier(pending_barrier);
897            }
898            upstream_table_reader.disconnect().await?;
899        } else if self.options.disable_backfill {
900            // If backfill is disabled, we just mark the backfill as finished
901            tracing::info!(
902                %table_id,
903                upstream_table_name,
904                "CdcBackfill has been disabled"
905            );
906            state_impl
907                .mutate_state(
908                    current_pk_pos.clone(),
909                    last_binlog_offset.clone(),
910                    total_snapshot_row_count,
911                    true,
912                )
913                .await?;
914        }
915
916        tracing::info!(
917            %table_id,
918            upstream_table_name,
919            "CdcBackfill has already finished and will forward messages directly to the downstream"
920        );
921
922        // Wait for first barrier to come after backfill is finished.
923        // So we can update our progress + persist the status.
924        while let Some(Ok(msg)) = upstream.next().await {
925            if let Some(msg) = mapping_message(msg, &self.output_indices) {
926                // If not finished then we need to update state, otherwise no need.
927                if let Message::Barrier(barrier) = &msg {
928                    // finalized the backfill state
929                    // TODO: unify `mutate_state` and `commit_state` into one method
930                    state_impl
931                        .mutate_state(
932                            current_pk_pos.clone(),
933                            last_binlog_offset.clone(),
934                            total_snapshot_row_count,
935                            true,
936                        )
937                        .await?;
938                    state_impl.commit_state(barrier.epoch).await?;
939
940                    // mark progress as finished
941                    if let Some(progress) = self.progress.as_mut() {
942                        progress.finish(barrier.epoch, total_snapshot_row_count);
943                    }
944                    yield msg;
945                    // break after the state have been saved
946                    break;
947                }
948                yield msg;
949            }
950        }
951
952        // After backfill progress finished
953        // we can forward messages directly to the downstream,
954        // as backfill is finished.
955        #[for_await]
956        for msg in upstream {
957            // upstream offsets will be removed from the message before forwarding to
958            // downstream
959            if let Some(msg) = mapping_message(msg?, &self.output_indices) {
960                if let Message::Barrier(barrier) = &msg {
961                    // commit state just to bump the epoch of state table
962                    state_impl.commit_state(barrier.epoch).await?;
963                }
964                yield msg;
965            }
966        }
967    }
968}
969
970pub(crate) async fn build_reader_and_poll_upstream(
971    upstream: &mut (impl Stream<Item = StreamExecutorResult<Message>> + Unpin),
972    table_reader: &mut Option<ExternalTableReaderImpl>,
973    future: &mut Pin<Box<impl Future<Output = ExternalTableReaderImpl>>>,
974) -> StreamExecutorResult<Option<Message>> {
975    if table_reader.is_some() {
976        return Ok(None);
977    }
978    tokio::select! {
979        biased;
980        reader = &mut *future => {
981            *table_reader = Some(reader);
982            Ok(None)
983        }
984        msg = upstream.next() => {
985            msg.transpose()
986        }
987    }
988}
989
990#[try_stream(ok = Message, error = StreamExecutorError)]
991pub async fn transform_upstream(
992    upstream: BoxedMessageStream,
993    output_columns: Vec<ColumnDesc>,
994    timestamp_handling: Option<TimestampHandling>,
995    timestamptz_handling: Option<TimestamptzHandling>,
996    time_handling: Option<TimeHandling>,
997    bigint_unsigned_handling: Option<BigintUnsignedHandlingMode>,
998    handle_toast_columns: bool,
999) {
1000    let props = SpecificParserConfig {
1001        encoding_config: EncodingProperties::Json(JsonProperties {
1002            use_schema_registry: false,
1003            timestamp_handling,
1004            timestamptz_handling,
1005            time_handling,
1006            bigint_unsigned_handling,
1007            handle_toast_columns,
1008        }),
1009        // the cdc message is generated internally so the key must exist.
1010        protocol_config: ProtocolProperties::Debezium(DebeziumProps::default()),
1011    };
1012
1013    // convert to source column desc to feed into parser
1014    let columns_with_meta = output_columns
1015        .iter()
1016        .map(SourceColumnDesc::from)
1017        .collect_vec();
1018    let mut parser = DebeziumParser::new(
1019        props,
1020        columns_with_meta.clone(),
1021        Arc::new(SourceContext::dummy()),
1022    )
1023    .await
1024    .map_err(StreamExecutorError::connector_error)?;
1025
1026    pin_mut!(upstream);
1027    #[for_await]
1028    for msg in upstream {
1029        let mut msg = msg?;
1030        if let Message::Chunk(chunk) = &mut msg {
1031            let parsed_chunk = parse_debezium_chunk(&mut parser, chunk).await?;
1032            let _ = std::mem::replace(chunk, parsed_chunk);
1033        }
1034        yield msg;
1035    }
1036}
1037
1038async fn parse_debezium_chunk(
1039    parser: &mut DebeziumParser,
1040    chunk: &StreamChunk,
1041) -> StreamExecutorResult<StreamChunk> {
1042    // here we transform the input chunk in `(payload varchar, _rw_offset varchar, _rw_table_name varchar)` schema
1043    // to chunk with downstream table schema `info.schema` of MergeNode contains the schema of the
1044    // table job with `_rw_offset` in the end
1045    // see `gen_create_table_plan_for_cdc_source` for details
1046
1047    // use `SourceStreamChunkBuilder` for convenience
1048    let mut builder = SourceStreamChunkBuilder::new(
1049        parser.columns().to_vec(),
1050        SourceCtrlOpts {
1051            chunk_size: chunk.capacity(),
1052            split_txn: false,
1053        },
1054    );
1055
1056    // The schema of input chunk `(payload varchar, _rw_offset varchar, _rw_table_name varchar, _row_id)`
1057    // We should use the debezium parser to parse the first column,
1058    // then chain the parsed row with `_rw_offset` row to get a new row.
1059    let payloads = chunk.data_chunk().project(&[0]);
1060    let offsets = chunk.data_chunk().project(&[1]).compact_vis();
1061
1062    // TODO: preserve the transaction semantics
1063    for payload in payloads.rows() {
1064        let ScalarRefImpl::Jsonb(jsonb_ref) = payload.datum_at(0).expect("payload must exist")
1065        else {
1066            panic!("payload must be jsonb");
1067        };
1068
1069        parser
1070            .parse_inner(
1071                None,
1072                Some(jsonb_ref.to_string().as_bytes().to_vec()),
1073                builder.row_writer(),
1074            )
1075            .await
1076            .unwrap();
1077    }
1078    builder.finish_current_chunk();
1079
1080    let parsed_chunk = {
1081        let mut iter = builder.consume_ready_chunks();
1082        assert_eq!(1, iter.len());
1083        iter.next().unwrap()
1084    };
1085    assert_eq!(parsed_chunk.capacity(), chunk.capacity()); // each payload is expected to generate one row
1086    let (ops, mut columns, vis) = parsed_chunk.into_inner();
1087    // note that `vis` is not necessarily the same as the original chunk's visibilities
1088
1089    // concat the rows in the parsed chunk with the `_rw_offset` column
1090    columns.extend(offsets.into_parts().0);
1091
1092    Ok(StreamChunk::from_parts(
1093        ops,
1094        DataChunk::from_parts(columns.into(), vis),
1095    ))
1096}
1097
1098impl<S: StateStore> Execute for CdcBackfillExecutor<S> {
1099    fn execute(self: Box<Self>) -> BoxedMessageStream {
1100        self.execute_inner().boxed()
1101    }
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106    use std::collections::BTreeMap;
1107    use std::str::FromStr;
1108
1109    use futures::{StreamExt, pin_mut};
1110    use risingwave_common::array::{Array, DataChunk, Op, StreamChunk};
1111    use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema, TableId};
1112    use risingwave_common::row::{OwnedRow, Row};
1113    use risingwave_common::types::{DataType, Datum, JsonbVal, ScalarImpl};
1114    use risingwave_common::util::epoch::test_epoch;
1115    use risingwave_common::util::iter_util::ZipEqFast;
1116    use risingwave_common::util::sort_util::OrderType;
1117    use risingwave_connector::source::cdc::CdcScanOptions;
1118    use risingwave_connector::source::cdc::external::mock_external_table::MockExternalTableReader;
1119    use risingwave_connector::source::cdc::external::mysql::MySqlOffset;
1120    use risingwave_connector::source::cdc::external::{
1121        CdcOffset, ExternalCdcTableType, ExternalTableConfig, SchemaTableName,
1122    };
1123    use risingwave_storage::memory::MemoryStateStore;
1124
1125    use super::PkCompareInfo;
1126    use crate::common::table::test_utils::gen_pbtable;
1127    use crate::executor::backfill::cdc::cdc_backfill::transform_upstream;
1128    use crate::executor::backfill::cdc::state::CdcBackfillState;
1129    use crate::executor::monitor::StreamingMetrics;
1130    use crate::executor::prelude::StateTable;
1131    use crate::executor::source::default_source_internal_table;
1132    use crate::executor::test_utils::MockSource;
1133    use crate::executor::{
1134        ActorContext, Barrier, CdcBackfillExecutor, ExternalStorageTable, Message,
1135    };
1136
1137    #[tokio::test]
1138    async fn test_transform_upstream_chunk() {
1139        let schema = Schema::new(vec![
1140            Field::unnamed(DataType::Jsonb),   // debezium json payload
1141            Field::unnamed(DataType::Varchar), // _rw_offset
1142            Field::unnamed(DataType::Varchar), // _rw_table_name
1143        ]);
1144        let stream_key = vec![1];
1145        let (mut tx, source) = MockSource::channel();
1146        let source = source.into_executor(schema.clone(), stream_key.clone());
1147        // 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();
1148        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 } }"#;
1149
1150        let datums: Vec<Datum> = vec![
1151            Some(JsonbVal::from_str(payload).unwrap().into()),
1152            Some("file: 1.binlog, pos: 100".to_owned().into()),
1153            Some("mydb.orders".to_owned().into()),
1154        ];
1155
1156        let mut builders = schema.create_array_builders(8);
1157        for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1158            builder.append(datum.clone());
1159        }
1160        let columns = builders
1161            .into_iter()
1162            .map(|builder| builder.finish().into())
1163            .collect();
1164
1165        // one row chunk
1166        let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1167
1168        tx.push_chunk(chunk);
1169        let upstream = Box::new(source).execute();
1170
1171        // schema to the debezium parser
1172        let columns = vec![
1173            ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1174            ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1175            ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1176            ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1177            ColumnDesc::named("O_ORDERDATE", ColumnId::new(5), DataType::Date),
1178            ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1179        ];
1180
1181        let parsed_stream = transform_upstream(upstream, columns, None, None, None, None, false);
1182        pin_mut!(parsed_stream);
1183        let message = parsed_stream
1184            .next()
1185            .await
1186            .expect("transform stream should yield the input chunk")
1187            .expect("transforming the CDC chunk should succeed");
1188        let Message::Chunk(chunk) = message else {
1189            panic!("expected a transformed chunk");
1190        };
1191        assert_eq!(
1192            chunk
1193                .rows()
1194                .map(|(op, row)| (op, row.to_owned_row()))
1195                .collect::<Vec<_>>(),
1196            vec![(
1197                Op::Insert,
1198                OwnedRow::new(vec![
1199                    Some(ScalarImpl::Int64(5)),
1200                    Some(ScalarImpl::Int64(44485)),
1201                    Some(ScalarImpl::Utf8("F".into())),
1202                    Some(ScalarImpl::Decimal("144659.20".parse().unwrap())),
1203                    Some(ScalarImpl::Date("1994-07-30".parse().unwrap())),
1204                    None,
1205                    Some(ScalarImpl::Utf8("file: 1.binlog, pos: 100".into())),
1206                ]),
1207            )]
1208        );
1209    }
1210
1211    #[tokio::test]
1212    async fn test_build_reader_and_poll_upstream() {
1213        let actor_context = ActorContext::for_test(1);
1214        let external_storage_table = ExternalStorageTable::for_test_undefined();
1215        let schema = Schema::new(vec![
1216            Field::unnamed(DataType::Jsonb),   // debezium json payload
1217            Field::unnamed(DataType::Varchar), // _rw_offset
1218            Field::unnamed(DataType::Varchar), // _rw_table_name
1219        ]);
1220        let stream_key = vec![1];
1221        let (mut tx, source) = MockSource::channel();
1222        let source = source.into_executor(schema.clone(), stream_key.clone());
1223        let output_indices = vec![1, 0, 4]; //reorder
1224        let output_columns = vec![
1225            ColumnDesc::named("O_ORDERKEY", ColumnId::new(1), DataType::Int64),
1226            ColumnDesc::named("O_CUSTKEY", ColumnId::new(2), DataType::Int64),
1227            ColumnDesc::named("O_ORDERSTATUS", ColumnId::new(3), DataType::Varchar),
1228            ColumnDesc::named("O_TOTALPRICE", ColumnId::new(4), DataType::Decimal),
1229            ColumnDesc::named("O_DUMMY", ColumnId::new(5), DataType::Int64),
1230            ColumnDesc::named("commit_ts", ColumnId::new(6), DataType::Timestamptz),
1231        ];
1232        let store = MemoryStateStore::new();
1233        let state_table =
1234            StateTable::from_table_catalog(&default_source_internal_table(0x2333), store, None)
1235                .await;
1236        let cdc = CdcBackfillExecutor::new(
1237            actor_context,
1238            external_storage_table,
1239            source,
1240            output_indices,
1241            output_columns,
1242            None,
1243            StreamingMetrics::unused().into(),
1244            state_table,
1245            None,
1246            CdcScanOptions {
1247                // We want to mark backfill as finished. However it's not straightforward to do so.
1248                // Here we disable_backfill instead.
1249                disable_backfill: true,
1250                ..CdcScanOptions::default()
1251            },
1252            BTreeMap::default(),
1253        );
1254        // cdc.state_impl.init_epoch(EpochPair::new(test_epoch(4), test_epoch(3))).await.unwrap();
1255        // cdc.state_impl.mutate_state(None, None, 0, true).await.unwrap();
1256        // cdc.state_impl.commit_state(EpochPair::new(test_epoch(5), test_epoch(4))).await.unwrap();
1257        let s = cdc.execute_inner();
1258        pin_mut!(s);
1259
1260        // send first barrier
1261        tx.send_barrier(Barrier::new_test_barrier(test_epoch(8)));
1262        // send chunk
1263        {
1264            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 } }"#;
1265            let datums: Vec<Datum> = vec![
1266                Some(JsonbVal::from_str(payload).unwrap().into()),
1267                Some("file: 1.binlog, pos: 100".to_owned().into()),
1268                Some("mydb.orders".to_owned().into()),
1269            ];
1270            let mut builders = schema.create_array_builders(8);
1271            for (builder, datum) in builders.iter_mut().zip_eq_fast(datums.iter()) {
1272                builder.append(datum.clone());
1273            }
1274            let columns = builders
1275                .into_iter()
1276                .map(|builder| builder.finish().into())
1277                .collect();
1278            // one row chunk
1279            let chunk = StreamChunk::from_parts(vec![Op::Insert], DataChunk::new(columns, 1));
1280
1281            tx.push_chunk(chunk);
1282        }
1283        let _first_barrier = s.next().await.unwrap();
1284        let upstream_change_log = s.next().await.unwrap().unwrap();
1285        let Message::Chunk(chunk) = upstream_change_log else {
1286            panic!("expect chunk");
1287        };
1288        assert_eq!(chunk.columns().len(), 3);
1289        assert_eq!(chunk.rows().count(), 1);
1290        assert_eq!(
1291            chunk.columns()[0].as_int64().iter().collect::<Vec<_>>(),
1292            vec![Some(44485)]
1293        );
1294        assert_eq!(
1295            chunk.columns()[1].as_int64().iter().collect::<Vec<_>>(),
1296            vec![Some(5)]
1297        );
1298        assert_eq!(
1299            chunk.columns()[2].as_int64().iter().collect::<Vec<_>>(),
1300            vec![Some(100)]
1301        );
1302    }
1303
1304    #[tokio::test]
1305    async fn test_disabled_cdc_backfill_finishes_without_table_reader() {
1306        let actor_context = ActorContext::for_test(1);
1307        let external_storage_table = ExternalStorageTable::new(
1308            TableId::new(1234),
1309            SchemaTableName {
1310                schema_name: "public".to_owned(),
1311                table_name: "orders".to_owned(),
1312            },
1313            "db".to_owned(),
1314            ExternalTableConfig::default(),
1315            ExternalCdcTableType::Undefined,
1316            Schema::new(vec![Field::with_name(DataType::Int64, "id")]),
1317            vec![OrderType::ascending()],
1318            vec![0],
1319        );
1320        let schema = Schema::new(vec![
1321            Field::unnamed(DataType::Jsonb),
1322            Field::unnamed(DataType::Varchar),
1323            Field::unnamed(DataType::Varchar),
1324        ]);
1325        let stream_key = vec![1];
1326        let (tx, source) = MockSource::channel();
1327        let source = source.into_executor(schema, stream_key);
1328        let output_columns = vec![ColumnDesc::named("id", ColumnId::new(1), DataType::Int64)];
1329        let memory_state_store = MemoryStateStore::new();
1330        let state_table = create_cdc_state_table(memory_state_store.clone()).await;
1331        let cdc = CdcBackfillExecutor::new(
1332            actor_context,
1333            external_storage_table,
1334            source,
1335            vec![0],
1336            output_columns,
1337            None,
1338            StreamingMetrics::unused().into(),
1339            state_table,
1340            None,
1341            CdcScanOptions {
1342                disable_backfill: true,
1343                ..CdcScanOptions::default()
1344            },
1345            BTreeMap::default(),
1346        );
1347        let executor = cdc.execute_inner();
1348        pin_mut!(executor);
1349
1350        tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1351        assert!(matches!(
1352            executor.next().await.unwrap().unwrap(),
1353            Message::Barrier(_)
1354        ));
1355
1356        tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1357        assert!(matches!(
1358            executor.next().await.unwrap().unwrap(),
1359            Message::Barrier(_)
1360        ));
1361
1362        let mut restored_state = CdcBackfillState::new(
1363            TableId::new(1234),
1364            create_cdc_state_table(memory_state_store).await,
1365            5,
1366        );
1367        restored_state
1368            .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1369            .await
1370            .unwrap();
1371        let state = restored_state.restore_state().await.unwrap();
1372        assert!(state.is_finished);
1373        assert_eq!(state.last_cdc_offset, None);
1374    }
1375
1376    #[tokio::test]
1377    async fn test_unfinished_cdc_backfill_rejects_null_offset_on_restore() {
1378        let memory_state_store = MemoryStateStore::new();
1379        let mut state_writer = CdcBackfillState::new(
1380            TableId::new(1234),
1381            create_cdc_state_table(memory_state_store.clone()).await,
1382            5,
1383        );
1384        state_writer
1385            .init_epoch(Barrier::new_test_barrier(test_epoch(1)).epoch)
1386            .await
1387            .unwrap();
1388        state_writer
1389            .mutate_state(
1390                Some(OwnedRow::new(vec![Some(ScalarImpl::Int64(10))])),
1391                None,
1392                10,
1393                false,
1394            )
1395            .await
1396            .unwrap();
1397        state_writer
1398            .commit_state(Barrier::new_test_barrier(test_epoch(2)).epoch)
1399            .await
1400            .unwrap();
1401
1402        let mut restored_state = CdcBackfillState::new(
1403            TableId::new(1234),
1404            create_cdc_state_table(memory_state_store).await,
1405            5,
1406        );
1407        restored_state
1408            .init_epoch(Barrier::new_test_barrier(test_epoch(2)).epoch)
1409            .await
1410            .unwrap();
1411
1412        assert!(restored_state.restore_state().await.is_err());
1413    }
1414
1415    fn create_raw_cdc_chunk(rows: &[(&str, &str)]) -> StreamChunk {
1416        let schema = Schema::new(vec![
1417            Field::unnamed(DataType::Jsonb),
1418            Field::unnamed(DataType::Varchar),
1419        ]);
1420        let mut builders = schema.create_array_builders(rows.len());
1421        for (payload, offset) in rows {
1422            let payload_datum: Datum = Some(JsonbVal::from_str(payload).unwrap().into());
1423            let offset_datum: Datum = Some((*offset).into());
1424            builders[0].append(payload_datum);
1425            builders[1].append(offset_datum);
1426        }
1427        let columns = builders
1428            .into_iter()
1429            .map(|builder| builder.finish().into())
1430            .collect();
1431        StreamChunk::from_parts(
1432            vec![Op::Insert; rows.len()],
1433            DataChunk::new(columns, rows.len()),
1434        )
1435    }
1436
1437    async fn create_cdc_state_table(store: MemoryStateStore) -> StateTable<MemoryStateStore> {
1438        let state_schema = Schema::new(vec![
1439            Field::with_name(DataType::Varchar, "split_id"),
1440            Field::with_name(DataType::Int64, "id"),
1441            Field::with_name(DataType::Boolean, "backfill_finished"),
1442            Field::with_name(DataType::Int64, "row_count"),
1443            Field::with_name(DataType::Jsonb, "cdc_offset"),
1444        ]);
1445        let column_descs = vec![
1446            ColumnDesc::unnamed(ColumnId::from(0), state_schema[0].data_type.clone()),
1447            ColumnDesc::unnamed(ColumnId::from(1), state_schema[1].data_type.clone()),
1448            ColumnDesc::unnamed(ColumnId::from(2), state_schema[2].data_type.clone()),
1449            ColumnDesc::unnamed(ColumnId::from(3), state_schema[3].data_type.clone()),
1450            ColumnDesc::unnamed(ColumnId::from(4), state_schema[4].data_type.clone()),
1451        ];
1452
1453        StateTable::from_table_catalog(
1454            &gen_pbtable(
1455                TableId::from(0x42),
1456                column_descs,
1457                vec![OrderType::ascending()],
1458                vec![0],
1459                0,
1460            ),
1461            store,
1462            None,
1463        )
1464        .await
1465    }
1466
1467    #[test]
1468    fn test_consume_upstream_chunk_buffer_retains_future_rows() {
1469        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1470            &[
1471                (
1472                    Op::Insert,
1473                    OwnedRow::new(vec![
1474                        Some(ScalarImpl::Int64(1)),
1475                        Some(ScalarImpl::Int64(100)),
1476                        Some(ScalarImpl::Utf8(
1477                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1478                                .into(),
1479                        )),
1480                    ]),
1481                ),
1482                (
1483                    Op::Insert,
1484                    OwnedRow::new(vec![
1485                        Some(ScalarImpl::Int64(6)),
1486                        Some(ScalarImpl::Int64(600)),
1487                        Some(ScalarImpl::Utf8(
1488                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1489                                .into(),
1490                        )),
1491                    ]),
1492                ),
1493            ],
1494            &[DataType::Int64, DataType::Int64, DataType::Varchar],
1495        )];
1496
1497        let (emitted_chunks, drained_row_count, drained_offset) =
1498            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1499                &MockExternalTableReader::get_cdc_offset_parser(),
1500                &mut upstream_chunk_buffer,
1501                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1502                PkCompareInfo {
1503                    indices: &[0],
1504                    order: &[OrderType::ascending()],
1505                    needs_unsigned_i64_compare: &[false],
1506                },
1507                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1508                &[0, 1],
1509            )
1510            .unwrap();
1511
1512        assert_eq!(drained_row_count, 1);
1513        assert_eq!(
1514            drained_offset,
1515            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1516        );
1517        assert_eq!(emitted_chunks.len(), 1);
1518        assert_eq!(emitted_chunks[0].rows().count(), 1);
1519        assert_eq!(
1520            emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
1521            OwnedRow::new(vec![
1522                Some(ScalarImpl::Int64(1)),
1523                Some(ScalarImpl::Int64(100))
1524            ])
1525        );
1526
1527        assert_eq!(upstream_chunk_buffer.len(), 1);
1528        assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
1529        assert_eq!(
1530            upstream_chunk_buffer[0]
1531                .rows()
1532                .next()
1533                .unwrap()
1534                .1
1535                .to_owned_row(),
1536            OwnedRow::new(vec![
1537                Some(ScalarImpl::Int64(6)),
1538                Some(ScalarImpl::Int64(600)),
1539                Some(ScalarImpl::Utf8(
1540                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1541                        .into(),
1542                )),
1543            ])
1544        );
1545    }
1546
1547    #[test]
1548    fn test_consume_buffer_uses_unsigned_bigint_pk_order() {
1549        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1550            &[
1551                (
1552                    Op::Insert,
1553                    OwnedRow::new(vec![
1554                        Some(ScalarImpl::Int64(4)),
1555                        Some(ScalarImpl::Int64(400)),
1556                        Some(ScalarImpl::Utf8(
1557                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1558                                .into(),
1559                        )),
1560                    ]),
1561                ),
1562                (
1563                    Op::Insert,
1564                    OwnedRow::new(vec![
1565                        // `u64::MAX` represented in RisingWave's `i64` storage.
1566                        Some(ScalarImpl::Int64(-1)),
1567                        Some(ScalarImpl::Int64(900)),
1568                        Some(ScalarImpl::Utf8(
1569                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1570                                .into(),
1571                        )),
1572                    ]),
1573                ),
1574            ],
1575            &[DataType::Int64, DataType::Int64, DataType::Varchar],
1576        )];
1577
1578        let (emitted_chunks, drained_row_count, drained_offset) =
1579            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1580                &MockExternalTableReader::get_cdc_offset_parser(),
1581                &mut upstream_chunk_buffer,
1582                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1583                PkCompareInfo {
1584                    indices: &[0],
1585                    order: &[OrderType::ascending()],
1586                    needs_unsigned_i64_compare: &[true],
1587                },
1588                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1589                &[0, 1],
1590            )
1591            .unwrap();
1592
1593        assert_eq!(drained_row_count, 1);
1594        assert_eq!(
1595            drained_offset,
1596            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1597        );
1598        assert_eq!(emitted_chunks.len(), 1);
1599        assert_eq!(
1600            emitted_chunks[0].rows().next().unwrap().1.to_owned_row(),
1601            OwnedRow::new(vec![
1602                Some(ScalarImpl::Int64(4)),
1603                Some(ScalarImpl::Int64(400))
1604            ])
1605        );
1606
1607        assert_eq!(upstream_chunk_buffer.len(), 1);
1608        assert_eq!(
1609            upstream_chunk_buffer[0]
1610                .rows()
1611                .next()
1612                .unwrap()
1613                .1
1614                .to_owned_row(),
1615            OwnedRow::new(vec![
1616                Some(ScalarImpl::Int64(-1)),
1617                Some(ScalarImpl::Int64(900)),
1618                Some(ScalarImpl::Utf8(
1619                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1620                        .into(),
1621                )),
1622            ])
1623        );
1624    }
1625
1626    #[test]
1627    fn test_consume_buffer_non_monotonic_pk_in_chunk() {
1628        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1629            &[
1630                (
1631                    Op::Insert,
1632                    OwnedRow::new(vec![
1633                        Some(ScalarImpl::Int64(1)),
1634                        Some(ScalarImpl::Int64(100)),
1635                        Some(ScalarImpl::Utf8(
1636                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1637                                .into(),
1638                        )),
1639                    ]),
1640                ),
1641                (
1642                    Op::Insert,
1643                    OwnedRow::new(vec![
1644                        Some(ScalarImpl::Int64(6)),
1645                        Some(ScalarImpl::Int64(600)),
1646                        Some(ScalarImpl::Utf8(
1647                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1648                                .into(),
1649                        )),
1650                    ]),
1651                ),
1652                (
1653                    Op::Insert,
1654                    OwnedRow::new(vec![
1655                        Some(ScalarImpl::Int64(2)),
1656                        Some(ScalarImpl::Int64(200)),
1657                        Some(ScalarImpl::Utf8(
1658                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1659                                .into(),
1660                        )),
1661                    ]),
1662                ),
1663            ],
1664            &[DataType::Int64, DataType::Int64, DataType::Varchar],
1665        )];
1666
1667        let (emitted_chunks, drained_row_count, drained_offset) =
1668            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1669                &MockExternalTableReader::get_cdc_offset_parser(),
1670                &mut upstream_chunk_buffer,
1671                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1672                PkCompareInfo {
1673                    indices: &[0],
1674                    order: &[OrderType::ascending()],
1675                    needs_unsigned_i64_compare: &[false],
1676                },
1677                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1678                &[0, 1],
1679            )
1680            .unwrap();
1681
1682        assert_eq!(drained_row_count, 2);
1683        assert_eq!(
1684            drained_offset,
1685            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1686        );
1687        assert_eq!(emitted_chunks.len(), 1);
1688        assert_eq!(emitted_chunks[0].rows().count(), 2);
1689        assert_eq!(
1690            emitted_chunks[0]
1691                .rows()
1692                .map(|(_, row)| row.to_owned_row())
1693                .collect::<Vec<_>>(),
1694            vec![
1695                OwnedRow::new(vec![
1696                    Some(ScalarImpl::Int64(1)),
1697                    Some(ScalarImpl::Int64(100))
1698                ]),
1699                OwnedRow::new(vec![
1700                    Some(ScalarImpl::Int64(2)),
1701                    Some(ScalarImpl::Int64(200))
1702                ]),
1703            ]
1704        );
1705
1706        assert_eq!(upstream_chunk_buffer.len(), 1);
1707        assert_eq!(upstream_chunk_buffer[0].rows().count(), 1);
1708        assert_eq!(
1709            upstream_chunk_buffer[0]
1710                .rows()
1711                .next()
1712                .unwrap()
1713                .1
1714                .to_owned_row(),
1715            OwnedRow::new(vec![
1716                Some(ScalarImpl::Int64(6)),
1717                Some(ScalarImpl::Int64(600)),
1718                Some(ScalarImpl::Utf8(
1719                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1720                        .into(),
1721                )),
1722            ])
1723        );
1724    }
1725
1726    #[test]
1727    fn test_consume_buffer_processes_chunks_after_future_row() {
1728        let mut upstream_chunk_buffer = vec![
1729            StreamChunk::from_rows(
1730                &[
1731                    (
1732                        Op::Insert,
1733                        OwnedRow::new(vec![
1734                            Some(ScalarImpl::Int64(1)),
1735                            Some(ScalarImpl::Int64(100)),
1736                            Some(ScalarImpl::Utf8(
1737                                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#
1738                                    .into(),
1739                            )),
1740                        ]),
1741                    ),
1742                    (
1743                        Op::Insert,
1744                        OwnedRow::new(vec![
1745                            Some(ScalarImpl::Int64(6)),
1746                            Some(ScalarImpl::Int64(600)),
1747                            Some(ScalarImpl::Utf8(
1748                                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1749                                    .into(),
1750                            )),
1751                        ]),
1752                    ),
1753                ],
1754                &[DataType::Int64, DataType::Int64, DataType::Varchar],
1755            ),
1756            StreamChunk::from_rows(
1757                &[(
1758                    Op::Insert,
1759                    OwnedRow::new(vec![
1760                        Some(ScalarImpl::Int64(2)),
1761                        Some(ScalarImpl::Int64(200)),
1762                        Some(ScalarImpl::Utf8(
1763                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1764                                .into(),
1765                        )),
1766                    ]),
1767                )],
1768                &[DataType::Int64, DataType::Int64, DataType::Varchar],
1769            ),
1770        ];
1771
1772        let (emitted_chunks, drained_row_count, drained_offset) =
1773            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1774                &MockExternalTableReader::get_cdc_offset_parser(),
1775                &mut upstream_chunk_buffer,
1776                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(5))])),
1777                PkCompareInfo {
1778                    indices: &[0],
1779                    order: &[OrderType::ascending()],
1780                    needs_unsigned_i64_compare: &[false],
1781                },
1782                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 2))),
1783                &[0, 1],
1784            )
1785            .unwrap();
1786
1787        assert_eq!(drained_row_count, 2);
1788        assert_eq!(
1789            drained_offset,
1790            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3)))
1791        );
1792        assert_eq!(emitted_chunks.len(), 2);
1793        assert_eq!(emitted_chunks[0].rows().count(), 1);
1794        assert_eq!(emitted_chunks[1].rows().count(), 1);
1795        assert_eq!(
1796            emitted_chunks[1].rows().next().unwrap().1.to_owned_row(),
1797            OwnedRow::new(vec![
1798                Some(ScalarImpl::Int64(2)),
1799                Some(ScalarImpl::Int64(200))
1800            ])
1801        );
1802
1803        assert_eq!(upstream_chunk_buffer.len(), 1);
1804        assert_eq!(
1805            upstream_chunk_buffer[0]
1806                .rows()
1807                .next()
1808                .unwrap()
1809                .1
1810                .to_owned_row(),
1811            OwnedRow::new(vec![
1812                Some(ScalarImpl::Int64(6)),
1813                Some(ScalarImpl::Int64(600)),
1814                Some(ScalarImpl::Utf8(
1815                    r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1816                        .into(),
1817                )),
1818            ])
1819        );
1820    }
1821
1822    #[test]
1823    fn test_consume_buffer_advances_offset_after_preceding_rows_are_emitted() {
1824        let mut upstream_chunk_buffer = vec![StreamChunk::from_rows(
1825            &[
1826                (
1827                    Op::Insert,
1828                    OwnedRow::new(vec![
1829                        Some(ScalarImpl::Int64(6)),
1830                        Some(ScalarImpl::Int64(600)),
1831                        Some(ScalarImpl::Utf8(
1832                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#
1833                                .into(),
1834                        )),
1835                    ]),
1836                ),
1837                (
1838                    Op::Insert,
1839                    OwnedRow::new(vec![
1840                        Some(ScalarImpl::Int64(2)),
1841                        Some(ScalarImpl::Int64(200)),
1842                        Some(ScalarImpl::Utf8(
1843                            r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":5},"isHeartbeat":false}"#
1844                                .into(),
1845                        )),
1846                    ]),
1847                ),
1848            ],
1849            &[DataType::Int64, DataType::Int64, DataType::Varchar],
1850        )];
1851
1852        let (_, drained_row_count, drained_offset) =
1853            CdcBackfillExecutor::<MemoryStateStore>::consume_upstream_chunk_buffer(
1854                &MockExternalTableReader::get_cdc_offset_parser(),
1855                &mut upstream_chunk_buffer,
1856                Some(&OwnedRow::new(vec![Some(ScalarImpl::Int64(6))])),
1857                PkCompareInfo {
1858                    indices: &[0],
1859                    order: &[OrderType::ascending()],
1860                    needs_unsigned_i64_compare: &[false],
1861                },
1862                &Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 3))),
1863                &[0, 1],
1864            )
1865            .unwrap();
1866
1867        assert_eq!(drained_row_count, 2);
1868        assert_eq!(
1869            drained_offset,
1870            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 5)))
1871        );
1872        assert!(upstream_chunk_buffer.is_empty());
1873    }
1874
1875    #[tokio::test]
1876    async fn test_cdc_backfill_persists_buffered_offset_on_checkpoint() {
1877        let memory_state_store = MemoryStateStore::new();
1878        let state_table = create_cdc_state_table(memory_state_store.clone()).await;
1879
1880        let (mut tx, source) = MockSource::channel();
1881        let source = source.into_executor(
1882            Schema::new(vec![
1883                Field::unnamed(DataType::Jsonb),
1884                Field::unnamed(DataType::Varchar),
1885            ]),
1886            vec![0],
1887        );
1888
1889        let external_table = ExternalStorageTable::new(
1890            TableId::new(1234),
1891            SchemaTableName {
1892                schema_name: "public".to_owned(),
1893                table_name: "mock_table".to_owned(),
1894            },
1895            "mydb".to_owned(),
1896            ExternalTableConfig::default(),
1897            ExternalCdcTableType::Mock,
1898            Schema::new(vec![
1899                Field::with_name(DataType::Int64, "id"),
1900                Field::with_name(DataType::Float64, "price"),
1901            ]),
1902            vec![OrderType::ascending()],
1903            vec![0],
1904        );
1905        let output_columns = vec![
1906            ColumnDesc::named("id", ColumnId::new(1), DataType::Int64),
1907            ColumnDesc::named("price", ColumnId::new(2), DataType::Float64),
1908        ];
1909
1910        let executor = CdcBackfillExecutor::new(
1911            ActorContext::for_test(0x1a),
1912            external_table,
1913            source,
1914            vec![0, 1],
1915            output_columns,
1916            None,
1917            StreamingMetrics::unused().into(),
1918            state_table,
1919            None,
1920            CdcScanOptions {
1921                snapshot_barrier_interval: 10,
1922                ..Default::default()
1923            },
1924            BTreeMap::default(),
1925        )
1926        .execute_inner();
1927        pin_mut!(executor);
1928
1929        tx.send_barrier(Barrier::new_test_barrier(test_epoch(1)));
1930        assert!(matches!(
1931            executor.next().await.unwrap().unwrap(),
1932            Message::Barrier(_)
1933        ));
1934
1935        tx.send_barrier(Barrier::new_test_barrier(test_epoch(2)));
1936        assert!(matches!(
1937            executor.next().await.unwrap().unwrap(),
1938            Message::Barrier(_)
1939        ));
1940
1941        assert!(matches!(
1942            executor.next().await.unwrap().unwrap(),
1943            Message::Chunk(_)
1944        ));
1945
1946        tx.push_chunk(create_raw_cdc_chunk(&[
1947            (
1948                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 } }"#,
1949                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":3},"isHeartbeat":false}"#,
1950            ),
1951            (
1952                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 } }"#,
1953                r#"{"sourcePartition":{},"sourceOffset":{"file":"1.binlog","pos":4},"isHeartbeat":false}"#,
1954            ),
1955        ]));
1956        tx.send_barrier(Barrier::new_test_barrier(test_epoch(3)));
1957
1958        assert!(matches!(
1959            executor.next().await.unwrap().unwrap(),
1960            Message::Chunk(_)
1961        ));
1962        assert!(matches!(
1963            executor.next().await.unwrap().unwrap(),
1964            Message::Barrier(_)
1965        ));
1966
1967        let mut restored_state = CdcBackfillState::new(
1968            TableId::new(1234),
1969            create_cdc_state_table(memory_state_store).await,
1970            5,
1971        );
1972        restored_state
1973            .init_epoch(Barrier::new_test_barrier(test_epoch(3)).epoch)
1974            .await
1975            .unwrap();
1976        let state = restored_state.restore_state().await.unwrap();
1977        assert_eq!(
1978            state.last_cdc_offset,
1979            Some(CdcOffset::MySql(MySqlOffset::new("1.binlog".to_owned(), 4)))
1980        );
1981    }
1982}