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