1use std::marker::PhantomData;
16
17use anyhow::Context;
18use risingwave_common::types::JsonbVal;
19use serde::{Deserialize, Serialize};
20
21use crate::error::ConnectorResult;
22use crate::source::cdc::external::DebeziumOffset;
23use crate::source::cdc::external::postgres::PostgresOffset;
24use crate::source::cdc::{CdcSourceType, CdcSourceTypeTrait, Mysql, Postgres, SqlServer};
25use crate::source::{SplitId, SplitMetaData};
26
27#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
30pub struct CdcSplitBase {
31 pub split_id: u32,
32 pub start_offset: Option<String>,
33 pub snapshot_done: bool,
34}
35
36impl CdcSplitBase {
37 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
38 Self {
39 split_id,
40 start_offset,
41 snapshot_done: false,
42 }
43 }
44}
45
46trait CdcSplitTrait: Send + Sync {
47 fn split_id(&self) -> u32;
48 fn start_offset(&self) -> &Option<String>;
49 fn is_snapshot_done(&self) -> bool;
50 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()>;
51
52 fn extract_snapshot_flag(&self, start_offset: &str) -> ConnectorResult<bool> {
54 let mut snapshot_done = self.is_snapshot_done();
56 if snapshot_done {
57 return Ok(snapshot_done);
58 }
59
60 let dbz_offset: DebeziumOffset = serde_json::from_str(start_offset).with_context(|| {
61 format!(
62 "invalid cdc offset: {}, split: {}",
63 start_offset,
64 self.split_id()
65 )
66 })?;
67
68 if !dbz_offset.is_heartbeat {
70 snapshot_done = match dbz_offset.source_offset.snapshot {
71 Some(val) => !val,
72 None => true,
73 };
74 }
75 Ok(snapshot_done)
76 }
77}
78
79#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
80pub struct MysqlCdcSplit {
81 pub inner: CdcSplitBase,
82}
83
84#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
85pub struct PostgresCdcSplit {
86 pub inner: CdcSplitBase,
87}
88
89#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
90pub struct MongodbCdcSplit {
91 pub inner: CdcSplitBase,
92}
93
94#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
95pub struct SqlServerCdcSplit {
96 pub inner: CdcSplitBase,
97}
98
99#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
100pub struct OracleCdcSplit {
101 pub inner: CdcSplitBase,
102}
103
104impl MysqlCdcSplit {
105 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
106 let split = CdcSplitBase {
107 split_id,
108 start_offset,
109 snapshot_done: false,
110 };
111 Self { inner: split }
112 }
113
114 pub fn mysql_binlog_offset(&self) -> Option<(u64, u64)> {
132 let offset_str = self.inner.start_offset.as_ref()?;
133 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
134 let source_offset = offset.get("sourceOffset")?;
135
136 let file = source_offset.get("file")?.as_str()?;
137 let pos = source_offset.get("pos")?.as_u64()?;
138
139 let file_seq = extract_binlog_file_seq(file)?;
140
141 Some((file_seq, pos))
142 }
143}
144
145impl CdcSplitTrait for MysqlCdcSplit {
146 fn split_id(&self) -> u32 {
147 self.inner.split_id
148 }
149
150 fn start_offset(&self) -> &Option<String> {
151 &self.inner.start_offset
152 }
153
154 fn is_snapshot_done(&self) -> bool {
155 self.inner.snapshot_done
156 }
157
158 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
159 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
161 self.inner.start_offset = Some(last_seen_offset);
162 Ok(())
163 }
164}
165
166impl PostgresCdcSplit {
167 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
168 let split = CdcSplitBase {
169 split_id,
170 start_offset,
171 snapshot_done: false,
172 };
173 Self { inner: split }
174 }
175
176 pub fn pg_lsn(&self) -> Option<u64> {
181 let offset_str = self.inner.start_offset.as_ref()?;
182 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
183 let source_offset = offset.get("sourceOffset")?;
184 let lsn = source_offset.get("lsn")?;
185 lsn.as_u64()
186 }
187
188 pub fn pg_lsn_commit(&self) -> Option<u64> {
190 let offset_str = self.inner.start_offset.as_ref()?;
191 extract_postgres_lsn_commit_from_offset_str(offset_str)
192 }
193
194 pub fn pg_lsn_proc(&self) -> Option<u64> {
196 let offset_str = self.inner.start_offset.as_ref()?;
197 extract_postgres_lsn_proc_from_offset_str(offset_str)
198 }
199}
200
201impl CdcSplitTrait for PostgresCdcSplit {
202 fn split_id(&self) -> u32 {
203 self.inner.split_id
204 }
205
206 fn start_offset(&self) -> &Option<String> {
207 &self.inner.start_offset
208 }
209
210 fn is_snapshot_done(&self) -> bool {
211 self.inner.snapshot_done
212 }
213
214 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
215 let new_snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
216
217 if let (Some(old_offset), Some(new_offset)) = (
252 self.inner
253 .start_offset
254 .as_deref()
255 .and_then(parse_postgres_offset_from_offset_str),
256 parse_postgres_offset_from_offset_str(&last_seen_offset),
257 ) && new_offset < old_offset
258 {
259 tracing::warn!(
260 split_id = self.inner.split_id,
261 ?old_offset,
262 ?new_offset,
263 "Rejecting backward Postgres CDC offset update; \
264 keeping current state to prevent state-table regression."
265 );
266 return Ok(());
267 }
268
269 self.inner.snapshot_done = new_snapshot_done;
270 self.inner.start_offset = Some(last_seen_offset);
271 Ok(())
272 }
273
274 fn extract_snapshot_flag(&self, start_offset: &str) -> ConnectorResult<bool> {
275 let mut snapshot_done = self.is_snapshot_done();
277 if snapshot_done {
278 return Ok(snapshot_done);
279 }
280
281 let dbz_offset: DebeziumOffset = serde_json::from_str(start_offset).with_context(|| {
282 format!(
283 "invalid postgres offset: {}, split: {}",
284 start_offset, self.inner.split_id
285 )
286 })?;
287
288 if !dbz_offset.is_heartbeat {
290 snapshot_done = dbz_offset
291 .source_offset
292 .last_snapshot_record
293 .unwrap_or(false);
294 }
295 Ok(snapshot_done)
296 }
297}
298
299impl MongodbCdcSplit {
300 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
301 let split = CdcSplitBase {
302 split_id,
303 start_offset,
304 snapshot_done: false,
305 };
306 Self { inner: split }
307 }
308}
309
310impl CdcSplitTrait for MongodbCdcSplit {
311 fn split_id(&self) -> u32 {
312 self.inner.split_id
313 }
314
315 fn start_offset(&self) -> &Option<String> {
316 &self.inner.start_offset
317 }
318
319 fn is_snapshot_done(&self) -> bool {
320 self.inner.snapshot_done
321 }
322
323 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
324 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
326 self.inner.start_offset = Some(last_seen_offset);
327 Ok(())
328 }
329}
330
331impl SqlServerCdcSplit {
332 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
333 let split = CdcSplitBase {
334 split_id,
335 start_offset,
336 snapshot_done: false,
337 };
338 Self { inner: split }
339 }
340
341 pub fn sql_server_change_lsn(&self) -> Option<u128> {
343 let offset_str = self.inner.start_offset.as_ref()?;
344 extract_sql_server_change_lsn_from_offset_str(offset_str)
345 }
346
347 pub fn sql_server_commit_lsn(&self) -> Option<u128> {
349 let offset_str = self.inner.start_offset.as_ref()?;
350 extract_sql_server_commit_lsn_from_offset_str(offset_str)
351 }
352}
353
354impl CdcSplitTrait for SqlServerCdcSplit {
355 fn split_id(&self) -> u32 {
356 self.inner.split_id
357 }
358
359 fn start_offset(&self) -> &Option<String> {
360 &self.inner.start_offset
361 }
362
363 fn is_snapshot_done(&self) -> bool {
364 self.inner.snapshot_done
365 }
366
367 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
368 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
370 self.inner.start_offset = Some(last_seen_offset);
371 Ok(())
372 }
373}
374
375impl OracleCdcSplit {
376 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
377 Self {
378 inner: CdcSplitBase::new(split_id, start_offset),
379 }
380 }
381}
382
383impl CdcSplitTrait for OracleCdcSplit {
384 fn split_id(&self) -> u32 {
385 self.inner.split_id
386 }
387
388 fn start_offset(&self) -> &Option<String> {
389 &self.inner.start_offset
390 }
391
392 fn is_snapshot_done(&self) -> bool {
393 self.inner.snapshot_done
394 }
395
396 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
397 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
398 self.inner.start_offset = Some(last_seen_offset);
399 Ok(())
400 }
401}
402
403#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
405pub struct DebeziumCdcSplit<T: CdcSourceTypeTrait> {
406 pub mysql_split: Option<MysqlCdcSplit>,
407
408 #[serde(rename = "pg_split")] pub postgres_split: Option<PostgresCdcSplit>,
410 pub mongodb_split: Option<MongodbCdcSplit>,
411 pub sql_server_split: Option<SqlServerCdcSplit>,
412 pub oracle_split: Option<OracleCdcSplit>,
413
414 #[serde(skip)]
415 pub _phantom: PhantomData<T>,
416}
417
418macro_rules! dispatch_cdc_split_inner {
419 ($dbz_split:expr, $as_type:tt, {$({$cdc_source_type:tt, $cdc_source_split:tt}),*}, $body:expr) => {
420 match T::source_type() {
421 $(
422 CdcSourceType::$cdc_source_type => {
423 $crate::paste! {
424 $dbz_split.[<$cdc_source_split>]
425 .[<as_ $as_type>]()
426 .expect(concat!(stringify!([<$cdc_source_type:lower>]), " split must exist"))
427 .$body
428 }
429 }
430 )*
431 CdcSourceType::Unspecified => {
432 unreachable!("invalid debezium split");
433 }
434 }
435 }
436}
437
438macro_rules! dispatch_cdc_split {
440 ($dbz_split:expr, $as_type:tt, $body:expr) => {
441 dispatch_cdc_split_inner!($dbz_split, $as_type, {
442 {Mysql, mysql_split},
443 {Postgres, postgres_split},
444 {Mongodb, mongodb_split},
445 {SqlServer, sql_server_split},
446 {Oracle, oracle_split}
447 }, $body)
448 }
449}
450
451impl<T: CdcSourceTypeTrait> SplitMetaData for DebeziumCdcSplit<T> {
452 fn id(&self) -> SplitId {
453 format!("{}", self.split_id()).into()
454 }
455
456 fn encode_to_json(&self) -> JsonbVal {
457 serde_json::to_value(self.clone()).unwrap().into()
458 }
459
460 fn restore_from_json(value: JsonbVal) -> ConnectorResult<Self> {
461 serde_json::from_value(value.take()).map_err(Into::into)
462 }
463
464 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
465 self.update_offset_inner(last_seen_offset)
466 }
467}
468
469impl<T: CdcSourceTypeTrait> DebeziumCdcSplit<T> {
470 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
471 let mut ret = Self {
472 mysql_split: None,
473 postgres_split: None,
474 mongodb_split: None,
475 sql_server_split: None,
476 oracle_split: None,
477 _phantom: PhantomData,
478 };
479 match T::source_type() {
480 CdcSourceType::Mysql => {
481 let split = MysqlCdcSplit::new(split_id, start_offset);
482 ret.mysql_split = Some(split);
483 }
484 CdcSourceType::Postgres => {
485 let split = PostgresCdcSplit::new(split_id, start_offset);
486 ret.postgres_split = Some(split);
487 }
488 CdcSourceType::Mongodb => {
489 let split = MongodbCdcSplit::new(split_id, start_offset);
490 ret.mongodb_split = Some(split);
491 }
492 CdcSourceType::SqlServer => {
493 let split = SqlServerCdcSplit::new(split_id, start_offset);
494 ret.sql_server_split = Some(split);
495 }
496 CdcSourceType::Oracle => {
497 let split = OracleCdcSplit::new(split_id, start_offset);
498 ret.oracle_split = Some(split);
499 }
500 CdcSourceType::Unspecified => {
501 unreachable!("invalid debezium split")
502 }
503 }
504 ret
505 }
506
507 pub fn split_id(&self) -> u32 {
508 dispatch_cdc_split!(self, ref, split_id())
509 }
510
511 pub fn start_offset(&self) -> &Option<String> {
512 dispatch_cdc_split!(self, ref, start_offset())
513 }
514
515 pub fn snapshot_done(&self) -> bool {
516 dispatch_cdc_split!(self, ref, is_snapshot_done())
517 }
518
519 pub fn update_offset_inner(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
520 dispatch_cdc_split!(self, mut, update_offset(last_seen_offset)?);
521 Ok(())
522 }
523}
524
525impl DebeziumCdcSplit<Postgres> {
526 pub fn pg_lsn(&self) -> Option<u64> {
530 self.postgres_split.as_ref()?.pg_lsn()
531 }
532}
533
534impl DebeziumCdcSplit<Mysql> {
535 pub fn mysql_binlog_offset(&self) -> Option<(u64, u64)> {
541 self.mysql_split.as_ref()?.mysql_binlog_offset()
542 }
543}
544
545impl DebeziumCdcSplit<SqlServer> {
546 pub fn sql_server_change_lsn(&self) -> Option<u128> {
548 self.sql_server_split.as_ref()?.sql_server_change_lsn()
549 }
550
551 pub fn sql_server_commit_lsn(&self) -> Option<u128> {
553 self.sql_server_split.as_ref()?.sql_server_commit_lsn()
554 }
555}
556
557pub fn extract_binlog_file_seq(file_name: &str) -> Option<u64> {
562 file_name.rsplit('.').next()?.parse::<u64>().ok()
563}
564
565pub fn extract_postgres_lsn_from_offset_str(offset_str: &str) -> Option<u64> {
572 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
573 let source_offset = offset.get("sourceOffset")?;
574 let lsn = source_offset.get("lsn")?;
575 lsn.as_u64()
576}
577
578fn parse_postgres_offset_from_offset_str(offset_str: &str) -> Option<PostgresOffset> {
579 PostgresOffset::parse_debezium_offset(offset_str).ok()
580}
581
582pub fn extract_postgres_lsn_commit_from_offset_str(offset_str: &str) -> Option<u64> {
591 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
592 let source_offset = offset.get("sourceOffset")?;
593 let lsn = source_offset.get("lsn_commit")?;
594 lsn.as_u64()
595}
596
597pub fn extract_postgres_lsn_proc_from_offset_str(offset_str: &str) -> Option<u64> {
605 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
606 let source_offset = offset.get("sourceOffset")?;
607 let lsn = source_offset.get("lsn_proc")?;
608 lsn.as_u64()
609}
610
611pub fn parse_sql_server_lsn_str(lsn: &str) -> Option<u128> {
613 let mut parts = lsn.split(':');
614 let part0 = u32::from_str_radix(parts.next()?, 16).ok()? as u128;
615 let part1 = u32::from_str_radix(parts.next()?, 16).ok()? as u128;
616 let part2 = u16::from_str_radix(parts.next()?, 16).ok()? as u128;
617 if parts.next().is_some() {
618 return None;
619 }
620
621 Some((part0 << 48) | (part1 << 16) | part2)
622}
623
624pub fn extract_sql_server_change_lsn_from_offset_str(offset_str: &str) -> Option<u128> {
626 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
627 let source_offset = offset.get("sourceOffset")?;
628 let lsn = source_offset.get("change_lsn")?.as_str()?;
629 parse_sql_server_lsn_str(lsn)
630}
631
632pub fn extract_sql_server_commit_lsn_from_offset_str(offset_str: &str) -> Option<u128> {
634 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
635 let source_offset = offset.get("sourceOffset")?;
636 let lsn = source_offset.get("commit_lsn")?.as_str()?;
637 parse_sql_server_lsn_str(lsn)
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643
644 #[test]
645 fn test_parse_sql_server_lsn_str() {
646 let lsn = "00000027:00000ac0:0002";
647 let parsed = parse_sql_server_lsn_str(lsn).unwrap();
648 let expected = ((0x00000027_u128) << 48) | ((0x00000ac0_u128) << 16) | (0x0002_u128);
649 assert_eq!(parsed, expected);
650 }
651
652 fn pg_streaming_offset_json(lsn: u64) -> String {
656 pg_streaming_offset_json_full(lsn, lsn, lsn)
657 }
658
659 fn pg_streaming_offset_json_full(lsn: u64, lsn_proc: u64, lsn_commit: u64) -> String {
663 format!(
664 r#"{{
665 "sourcePartition": {{"server": "RW_CDC_1001"}},
666 "sourceOffset": {{
667 "last_snapshot_record": false,
668 "lsn": {lsn},
669 "lsn_proc": {lsn_proc},
670 "lsn_commit": {lsn_commit},
671 "txId": 12345,
672 "ts_usec": 1700000000000000
673 }},
674 "isHeartbeat": false
675 }}"#
676 )
677 }
678
679 #[test]
680 fn test_postgres_offset_monotonic_guard_rejects_backward_lsn() {
681 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
682
683 split
685 .update_offset(pg_streaming_offset_json(150))
686 .expect("update_offset must not error on rejection");
687 assert_eq!(split.pg_lsn(), Some(200));
688 }
689
690 #[test]
691 fn test_postgres_offset_monotonic_guard_allows_forward_lsn() {
692 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
693
694 split.update_offset(pg_streaming_offset_json(250)).unwrap();
695 assert_eq!(split.pg_lsn(), Some(250));
696 }
697
698 #[test]
699 fn test_postgres_offset_monotonic_guard_allows_equal_lsn() {
700 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
701
702 split.update_offset(pg_streaming_offset_json(200)).unwrap();
704 assert_eq!(split.pg_lsn(), Some(200));
705 }
706
707 #[test]
708 fn test_postgres_offset_snapshot_phase_bypasses_guard() {
709 let snapshot_offset = r#"{
710 "sourcePartition": {"server": "RW_CDC_1001"},
711 "sourceOffset": {
712 "last_snapshot_record": false,
713 "lsn": 150,
714 "txId": 12345,
715 "ts_usec": 1700000000000000
716 },
717 "isHeartbeat": false
718 }"#;
719 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
720
721 split.update_offset(snapshot_offset.to_owned()).unwrap();
724 assert_eq!(split.pg_lsn(), Some(150));
725 }
726
727 #[test]
728 fn test_postgres_offset_lsn_commit_regression_rejected() {
729 let initial = pg_streaming_offset_json_full(200, 200, 200);
733 let mut split = PostgresCdcSplit::new(1, Some(initial));
734
735 let new = pg_streaming_offset_json_full(200, 200, 150);
737 split.update_offset(new).unwrap();
738
739 assert_eq!(
740 split.pg_lsn_commit(),
741 Some(200),
742 "lsn_commit must not regress"
743 );
744 }
745
746 #[test]
747 fn test_postgres_offset_lsn_proc_regression_rejected() {
748 let initial = pg_streaming_offset_json_full(200, 200, 200);
752 let mut split = PostgresCdcSplit::new(1, Some(initial));
753
754 let new = pg_streaming_offset_json_full(200, 150, 200);
756 split.update_offset(new).unwrap();
757
758 assert_eq!(split.pg_lsn_proc(), Some(200), "lsn_proc must not regress");
759 }
760
761 #[test]
762 fn test_postgres_offset_interleaved_tx_lsn_proc_drop_allowed_by_commit_lsn() {
763 let initial = pg_streaming_offset_json_full(33_967_592, 33_967_592, 33_893_392);
774 let mut split = PostgresCdcSplit::new(1, Some(initial));
775
776 let new = pg_streaming_offset_json_full(33_918_336, 33_918_336, 33_969_832);
777 split.update_offset(new).unwrap();
778
779 assert_eq!(split.pg_lsn_commit(), Some(33_969_832));
780 assert_eq!(split.pg_lsn_proc(), Some(33_918_336));
781 }
782
783 #[test]
784 fn test_extract_binlog_file_seq() {
785 assert_eq!(extract_binlog_file_seq("binlog.000123"), Some(123));
787 assert_eq!(extract_binlog_file_seq("mysql-bin.000123"), Some(123));
789 assert_eq!(extract_binlog_file_seq("my-host-bin.000001"), Some(1));
791 assert_eq!(
793 extract_binlog_file_seq("mysql-bin-changelog.037568"),
794 Some(37568)
795 );
796 assert_eq!(extract_binlog_file_seq("binlog.index"), None);
798 assert_eq!(extract_binlog_file_seq("no-extension"), None);
799 }
800
801 #[test]
802 fn test_mysql_binlog_offset() {
803 let offset = r#"{
804 "sourcePartition": {"server": "test"},
805 "sourceOffset": {
806 "file": "mysql-bin-changelog.037568",
807 "pos": 12345
808 },
809 "isHeartbeat": false
810 }"#;
811 let split = MysqlCdcSplit::new(1, Some(offset.to_owned()));
812 assert_eq!(split.mysql_binlog_offset(), Some((37568, 12345)));
813 }
814
815 #[test]
816 fn test_extract_sql_server_lsn_from_offset_str() {
817 let offset = r#"{
818 "sourcePartition": {"server":"RW_CDC_1001"},
819 "sourceOffset": {
820 "change_lsn":"00000027:00000ac0:0001",
821 "commit_lsn":"00000027:00000ac0:0002"
822 },
823 "isHeartbeat": false
824 }"#;
825
826 let change_lsn = extract_sql_server_change_lsn_from_offset_str(offset).unwrap();
827 let commit_lsn = extract_sql_server_commit_lsn_from_offset_str(offset).unwrap();
828 assert!(change_lsn < commit_lsn);
829 }
830}