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, Mariadb, 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 MariadbCdcSplit {
86 pub inner: CdcSplitBase,
87}
88
89#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
90pub struct PostgresCdcSplit {
91 pub inner: CdcSplitBase,
92}
93
94#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
95pub struct MongodbCdcSplit {
96 pub inner: CdcSplitBase,
97}
98
99#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
100pub struct SqlServerCdcSplit {
101 pub inner: CdcSplitBase,
102}
103
104#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
105pub struct OracleCdcSplit {
106 pub inner: CdcSplitBase,
107}
108
109impl MysqlCdcSplit {
110 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
111 let split = CdcSplitBase {
112 split_id,
113 start_offset,
114 snapshot_done: false,
115 };
116 Self { inner: split }
117 }
118
119 pub fn mysql_binlog_offset(&self) -> Option<(u64, u64)> {
137 let offset_str = self.inner.start_offset.as_ref()?;
138 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
139 let source_offset = offset.get("sourceOffset")?;
140
141 let file = source_offset.get("file")?.as_str()?;
142 let pos = source_offset.get("pos")?.as_u64()?;
143
144 let file_seq = extract_binlog_file_seq(file)?;
145
146 Some((file_seq, pos))
147 }
148}
149
150impl MariadbCdcSplit {
151 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
152 Self {
153 inner: CdcSplitBase::new(split_id, start_offset),
154 }
155 }
156
157 pub fn mariadb_binlog_offset(&self) -> Option<(u64, u64)> {
158 binlog_offset(&self.inner)
159 }
160}
161
162impl CdcSplitTrait for MariadbCdcSplit {
163 fn split_id(&self) -> u32 {
164 self.inner.split_id
165 }
166
167 fn start_offset(&self) -> &Option<String> {
168 &self.inner.start_offset
169 }
170
171 fn is_snapshot_done(&self) -> bool {
172 self.inner.snapshot_done
173 }
174
175 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
176 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
177 self.inner.start_offset = Some(last_seen_offset);
178 Ok(())
179 }
180}
181
182impl CdcSplitTrait for MysqlCdcSplit {
183 fn split_id(&self) -> u32 {
184 self.inner.split_id
185 }
186
187 fn start_offset(&self) -> &Option<String> {
188 &self.inner.start_offset
189 }
190
191 fn is_snapshot_done(&self) -> bool {
192 self.inner.snapshot_done
193 }
194
195 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
196 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
198 self.inner.start_offset = Some(last_seen_offset);
199 Ok(())
200 }
201}
202
203impl PostgresCdcSplit {
204 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
205 let split = CdcSplitBase {
206 split_id,
207 start_offset,
208 snapshot_done: false,
209 };
210 Self { inner: split }
211 }
212
213 pub fn pg_lsn(&self) -> Option<u64> {
218 let offset_str = self.inner.start_offset.as_ref()?;
219 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
220 let source_offset = offset.get("sourceOffset")?;
221 let lsn = source_offset.get("lsn")?;
222 lsn.as_u64()
223 }
224
225 pub fn pg_lsn_commit(&self) -> Option<u64> {
227 let offset_str = self.inner.start_offset.as_ref()?;
228 extract_postgres_lsn_commit_from_offset_str(offset_str)
229 }
230
231 pub fn pg_lsn_proc(&self) -> Option<u64> {
233 let offset_str = self.inner.start_offset.as_ref()?;
234 extract_postgres_lsn_proc_from_offset_str(offset_str)
235 }
236}
237
238impl CdcSplitTrait for PostgresCdcSplit {
239 fn split_id(&self) -> u32 {
240 self.inner.split_id
241 }
242
243 fn start_offset(&self) -> &Option<String> {
244 &self.inner.start_offset
245 }
246
247 fn is_snapshot_done(&self) -> bool {
248 self.inner.snapshot_done
249 }
250
251 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
252 let new_snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
253
254 if let (Some(old_offset), Some(new_offset)) = (
289 self.inner
290 .start_offset
291 .as_deref()
292 .and_then(parse_postgres_offset_from_offset_str),
293 parse_postgres_offset_from_offset_str(&last_seen_offset),
294 ) && new_offset < old_offset
295 {
296 tracing::warn!(
297 split_id = self.inner.split_id,
298 ?old_offset,
299 ?new_offset,
300 "Rejecting backward Postgres CDC offset update; \
301 keeping current state to prevent state-table regression."
302 );
303 return Ok(());
304 }
305
306 self.inner.snapshot_done = new_snapshot_done;
307 self.inner.start_offset = Some(last_seen_offset);
308 Ok(())
309 }
310
311 fn extract_snapshot_flag(&self, start_offset: &str) -> ConnectorResult<bool> {
312 let mut snapshot_done = self.is_snapshot_done();
314 if snapshot_done {
315 return Ok(snapshot_done);
316 }
317
318 let dbz_offset: DebeziumOffset = serde_json::from_str(start_offset).with_context(|| {
319 format!(
320 "invalid postgres offset: {}, split: {}",
321 start_offset, self.inner.split_id
322 )
323 })?;
324
325 if !dbz_offset.is_heartbeat {
327 snapshot_done = dbz_offset
328 .source_offset
329 .last_snapshot_record
330 .unwrap_or(false);
331 }
332 Ok(snapshot_done)
333 }
334}
335
336impl MongodbCdcSplit {
337 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
338 let split = CdcSplitBase {
339 split_id,
340 start_offset,
341 snapshot_done: false,
342 };
343 Self { inner: split }
344 }
345}
346
347impl CdcSplitTrait for MongodbCdcSplit {
348 fn split_id(&self) -> u32 {
349 self.inner.split_id
350 }
351
352 fn start_offset(&self) -> &Option<String> {
353 &self.inner.start_offset
354 }
355
356 fn is_snapshot_done(&self) -> bool {
357 self.inner.snapshot_done
358 }
359
360 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
361 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
363 self.inner.start_offset = Some(last_seen_offset);
364 Ok(())
365 }
366}
367
368impl SqlServerCdcSplit {
369 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
370 let split = CdcSplitBase {
371 split_id,
372 start_offset,
373 snapshot_done: false,
374 };
375 Self { inner: split }
376 }
377
378 pub fn sql_server_change_lsn(&self) -> Option<u128> {
380 let offset_str = self.inner.start_offset.as_ref()?;
381 extract_sql_server_change_lsn_from_offset_str(offset_str)
382 }
383
384 pub fn sql_server_commit_lsn(&self) -> Option<u128> {
386 let offset_str = self.inner.start_offset.as_ref()?;
387 extract_sql_server_commit_lsn_from_offset_str(offset_str)
388 }
389}
390
391impl CdcSplitTrait for SqlServerCdcSplit {
392 fn split_id(&self) -> u32 {
393 self.inner.split_id
394 }
395
396 fn start_offset(&self) -> &Option<String> {
397 &self.inner.start_offset
398 }
399
400 fn is_snapshot_done(&self) -> bool {
401 self.inner.snapshot_done
402 }
403
404 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
405 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
407 self.inner.start_offset = Some(last_seen_offset);
408 Ok(())
409 }
410}
411
412impl OracleCdcSplit {
413 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
414 Self {
415 inner: CdcSplitBase::new(split_id, start_offset),
416 }
417 }
418}
419
420impl CdcSplitTrait for OracleCdcSplit {
421 fn split_id(&self) -> u32 {
422 self.inner.split_id
423 }
424
425 fn start_offset(&self) -> &Option<String> {
426 &self.inner.start_offset
427 }
428
429 fn is_snapshot_done(&self) -> bool {
430 self.inner.snapshot_done
431 }
432
433 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
434 self.inner.snapshot_done = self.extract_snapshot_flag(last_seen_offset.as_str())?;
435 self.inner.start_offset = Some(last_seen_offset);
436 Ok(())
437 }
438}
439
440#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
442pub struct DebeziumCdcSplit<T: CdcSourceTypeTrait> {
443 pub mysql_split: Option<MysqlCdcSplit>,
444 pub mariadb_split: Option<MariadbCdcSplit>,
445
446 #[serde(rename = "pg_split")] pub postgres_split: Option<PostgresCdcSplit>,
448 pub mongodb_split: Option<MongodbCdcSplit>,
449 pub sql_server_split: Option<SqlServerCdcSplit>,
450 pub oracle_split: Option<OracleCdcSplit>,
451
452 #[serde(skip)]
453 pub _phantom: PhantomData<T>,
454}
455
456macro_rules! dispatch_cdc_split_inner {
457 ($dbz_split:expr, $as_type:tt, {$({$cdc_source_type:tt, $cdc_source_split:tt}),*}, $body:expr) => {
458 match T::source_type() {
459 $(
460 CdcSourceType::$cdc_source_type => {
461 $crate::paste! {
462 $dbz_split.[<$cdc_source_split>]
463 .[<as_ $as_type>]()
464 .expect(concat!(stringify!([<$cdc_source_type:lower>]), " split must exist"))
465 .$body
466 }
467 }
468 )*
469 CdcSourceType::Unspecified => {
470 unreachable!("invalid debezium split");
471 }
472 }
473 }
474}
475
476macro_rules! dispatch_cdc_split {
478 ($dbz_split:expr, $as_type:tt, $body:expr) => {
479 dispatch_cdc_split_inner!($dbz_split, $as_type, {
480 {Mysql, mysql_split},
481 {Mariadb, mariadb_split},
482 {Postgres, postgres_split},
483 {Mongodb, mongodb_split},
484 {SqlServer, sql_server_split},
485 {Oracle, oracle_split}
486 }, $body)
487 }
488}
489
490impl<T: CdcSourceTypeTrait> SplitMetaData for DebeziumCdcSplit<T> {
491 fn id(&self) -> SplitId {
492 format!("{}", self.split_id()).into()
493 }
494
495 fn encode_to_json(&self) -> JsonbVal {
496 serde_json::to_value(self.clone()).unwrap().into()
497 }
498
499 fn restore_from_json(value: JsonbVal) -> ConnectorResult<Self> {
500 serde_json::from_value(value.take()).map_err(Into::into)
501 }
502
503 fn update_offset(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
504 self.update_offset_inner(last_seen_offset)
505 }
506}
507
508impl<T: CdcSourceTypeTrait> DebeziumCdcSplit<T> {
509 pub fn new(split_id: u32, start_offset: Option<String>) -> Self {
510 let mut ret = Self {
511 mysql_split: None,
512 mariadb_split: None,
513 postgres_split: None,
514 mongodb_split: None,
515 sql_server_split: None,
516 oracle_split: None,
517 _phantom: PhantomData,
518 };
519 match T::source_type() {
520 CdcSourceType::Mysql => {
521 let split = MysqlCdcSplit::new(split_id, start_offset);
522 ret.mysql_split = Some(split);
523 }
524 CdcSourceType::Mariadb => {
525 let split = MariadbCdcSplit::new(split_id, start_offset);
526 ret.mariadb_split = Some(split);
527 }
528 CdcSourceType::Postgres => {
529 let split = PostgresCdcSplit::new(split_id, start_offset);
530 ret.postgres_split = Some(split);
531 }
532 CdcSourceType::Mongodb => {
533 let split = MongodbCdcSplit::new(split_id, start_offset);
534 ret.mongodb_split = Some(split);
535 }
536 CdcSourceType::SqlServer => {
537 let split = SqlServerCdcSplit::new(split_id, start_offset);
538 ret.sql_server_split = Some(split);
539 }
540 CdcSourceType::Oracle => {
541 let split = OracleCdcSplit::new(split_id, start_offset);
542 ret.oracle_split = Some(split);
543 }
544 CdcSourceType::Unspecified => {
545 unreachable!("invalid debezium split")
546 }
547 }
548 ret
549 }
550
551 pub fn split_id(&self) -> u32 {
552 dispatch_cdc_split!(self, ref, split_id())
553 }
554
555 pub fn start_offset(&self) -> &Option<String> {
556 dispatch_cdc_split!(self, ref, start_offset())
557 }
558
559 pub fn snapshot_done(&self) -> bool {
560 dispatch_cdc_split!(self, ref, is_snapshot_done())
561 }
562
563 pub fn update_offset_inner(&mut self, last_seen_offset: String) -> ConnectorResult<()> {
564 dispatch_cdc_split!(self, mut, update_offset(last_seen_offset)?);
565 Ok(())
566 }
567}
568
569impl DebeziumCdcSplit<Postgres> {
570 pub fn pg_lsn(&self) -> Option<u64> {
574 self.postgres_split.as_ref()?.pg_lsn()
575 }
576}
577
578impl DebeziumCdcSplit<Mysql> {
579 pub fn mysql_binlog_offset(&self) -> Option<(u64, u64)> {
585 self.mysql_split.as_ref()?.mysql_binlog_offset()
586 }
587}
588
589impl DebeziumCdcSplit<Mariadb> {
590 pub fn mariadb_binlog_offset(&self) -> Option<(u64, u64)> {
591 self.mariadb_split.as_ref()?.mariadb_binlog_offset()
592 }
593}
594
595fn binlog_offset(split: &CdcSplitBase) -> Option<(u64, u64)> {
596 let offset_str = split.start_offset.as_ref()?;
597 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
598 let source_offset = offset.get("sourceOffset")?;
599 let file = source_offset.get("file")?.as_str()?;
600 let pos = source_offset.get("pos")?.as_u64()?;
601 Some((extract_binlog_file_seq(file)?, pos))
602}
603
604impl DebeziumCdcSplit<SqlServer> {
605 pub fn sql_server_change_lsn(&self) -> Option<u128> {
607 self.sql_server_split.as_ref()?.sql_server_change_lsn()
608 }
609
610 pub fn sql_server_commit_lsn(&self) -> Option<u128> {
612 self.sql_server_split.as_ref()?.sql_server_commit_lsn()
613 }
614}
615
616pub fn extract_binlog_file_seq(file_name: &str) -> Option<u64> {
621 file_name.rsplit('.').next()?.parse::<u64>().ok()
622}
623
624pub fn extract_postgres_lsn_from_offset_str(offset_str: &str) -> Option<u64> {
631 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
632 let source_offset = offset.get("sourceOffset")?;
633 let lsn = source_offset.get("lsn")?;
634 lsn.as_u64()
635}
636
637fn parse_postgres_offset_from_offset_str(offset_str: &str) -> Option<PostgresOffset> {
638 PostgresOffset::parse_debezium_offset(offset_str).ok()
639}
640
641pub fn extract_postgres_lsn_commit_from_offset_str(offset_str: &str) -> Option<u64> {
650 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
651 let source_offset = offset.get("sourceOffset")?;
652 let lsn = source_offset.get("lsn_commit")?;
653 lsn.as_u64()
654}
655
656pub fn extract_postgres_lsn_proc_from_offset_str(offset_str: &str) -> Option<u64> {
664 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
665 let source_offset = offset.get("sourceOffset")?;
666 let lsn = source_offset.get("lsn_proc")?;
667 lsn.as_u64()
668}
669
670pub fn parse_sql_server_lsn_str(lsn: &str) -> Option<u128> {
672 let mut parts = lsn.split(':');
673 let part0 = u32::from_str_radix(parts.next()?, 16).ok()? as u128;
674 let part1 = u32::from_str_radix(parts.next()?, 16).ok()? as u128;
675 let part2 = u16::from_str_radix(parts.next()?, 16).ok()? as u128;
676 if parts.next().is_some() {
677 return None;
678 }
679
680 Some((part0 << 48) | (part1 << 16) | part2)
681}
682
683pub fn extract_sql_server_change_lsn_from_offset_str(offset_str: &str) -> Option<u128> {
685 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
686 let source_offset = offset.get("sourceOffset")?;
687 let lsn = source_offset.get("change_lsn")?.as_str()?;
688 parse_sql_server_lsn_str(lsn)
689}
690
691pub fn extract_sql_server_commit_lsn_from_offset_str(offset_str: &str) -> Option<u128> {
693 let offset = serde_json::from_str::<serde_json::Value>(offset_str).ok()?;
694 let source_offset = offset.get("sourceOffset")?;
695 let lsn = source_offset.get("commit_lsn")?.as_str()?;
696 parse_sql_server_lsn_str(lsn)
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn mariadb_split_round_trip_and_binlog_offset() {
705 let offset = r#"{"sourcePartition":{"server":"RW_CDC_1"},"sourceOffset":{"file":"mariadb-bin.000123","pos":456},"isHeartbeat":false}"#;
706 let split = DebeziumCdcSplit::<Mariadb>::new(1, Some(offset.to_owned()));
707 assert_eq!(split.mariadb_binlog_offset(), Some((123, 456)));
708
709 let json = split.encode_to_json();
710 let restored = DebeziumCdcSplit::<Mariadb>::restore_from_json(json).unwrap();
711 assert_eq!(restored, split);
712 }
713
714 #[test]
715 fn test_parse_sql_server_lsn_str() {
716 let lsn = "00000027:00000ac0:0002";
717 let parsed = parse_sql_server_lsn_str(lsn).unwrap();
718 let expected = ((0x00000027_u128) << 48) | ((0x00000ac0_u128) << 16) | (0x0002_u128);
719 assert_eq!(parsed, expected);
720 }
721
722 fn pg_streaming_offset_json(lsn: u64) -> String {
726 pg_streaming_offset_json_full(lsn, lsn, lsn)
727 }
728
729 fn pg_streaming_offset_json_full(lsn: u64, lsn_proc: u64, lsn_commit: u64) -> String {
733 format!(
734 r#"{{
735 "sourcePartition": {{"server": "RW_CDC_1001"}},
736 "sourceOffset": {{
737 "last_snapshot_record": false,
738 "lsn": {lsn},
739 "lsn_proc": {lsn_proc},
740 "lsn_commit": {lsn_commit},
741 "txId": 12345,
742 "ts_usec": 1700000000000000
743 }},
744 "isHeartbeat": false
745 }}"#
746 )
747 }
748
749 #[test]
750 fn test_postgres_offset_monotonic_guard_rejects_backward_lsn() {
751 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
752
753 split
755 .update_offset(pg_streaming_offset_json(150))
756 .expect("update_offset must not error on rejection");
757 assert_eq!(split.pg_lsn(), Some(200));
758 }
759
760 #[test]
761 fn test_postgres_offset_monotonic_guard_allows_forward_lsn() {
762 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
763
764 split.update_offset(pg_streaming_offset_json(250)).unwrap();
765 assert_eq!(split.pg_lsn(), Some(250));
766 }
767
768 #[test]
769 fn test_postgres_offset_monotonic_guard_allows_equal_lsn() {
770 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
771
772 split.update_offset(pg_streaming_offset_json(200)).unwrap();
774 assert_eq!(split.pg_lsn(), Some(200));
775 }
776
777 #[test]
778 fn test_postgres_offset_snapshot_phase_bypasses_guard() {
779 let snapshot_offset = r#"{
780 "sourcePartition": {"server": "RW_CDC_1001"},
781 "sourceOffset": {
782 "last_snapshot_record": false,
783 "lsn": 150,
784 "txId": 12345,
785 "ts_usec": 1700000000000000
786 },
787 "isHeartbeat": false
788 }"#;
789 let mut split = PostgresCdcSplit::new(1, Some(pg_streaming_offset_json(200)));
790
791 split.update_offset(snapshot_offset.to_owned()).unwrap();
794 assert_eq!(split.pg_lsn(), Some(150));
795 }
796
797 #[test]
798 fn test_postgres_offset_lsn_commit_regression_rejected() {
799 let initial = pg_streaming_offset_json_full(200, 200, 200);
803 let mut split = PostgresCdcSplit::new(1, Some(initial));
804
805 let new = pg_streaming_offset_json_full(200, 200, 150);
807 split.update_offset(new).unwrap();
808
809 assert_eq!(
810 split.pg_lsn_commit(),
811 Some(200),
812 "lsn_commit must not regress"
813 );
814 }
815
816 #[test]
817 fn test_postgres_offset_lsn_proc_regression_rejected() {
818 let initial = pg_streaming_offset_json_full(200, 200, 200);
822 let mut split = PostgresCdcSplit::new(1, Some(initial));
823
824 let new = pg_streaming_offset_json_full(200, 150, 200);
826 split.update_offset(new).unwrap();
827
828 assert_eq!(split.pg_lsn_proc(), Some(200), "lsn_proc must not regress");
829 }
830
831 #[test]
832 fn test_postgres_offset_interleaved_tx_lsn_proc_drop_allowed_by_commit_lsn() {
833 let initial = pg_streaming_offset_json_full(33_967_592, 33_967_592, 33_893_392);
844 let mut split = PostgresCdcSplit::new(1, Some(initial));
845
846 let new = pg_streaming_offset_json_full(33_918_336, 33_918_336, 33_969_832);
847 split.update_offset(new).unwrap();
848
849 assert_eq!(split.pg_lsn_commit(), Some(33_969_832));
850 assert_eq!(split.pg_lsn_proc(), Some(33_918_336));
851 }
852
853 #[test]
854 fn test_extract_binlog_file_seq() {
855 assert_eq!(extract_binlog_file_seq("binlog.000123"), Some(123));
857 assert_eq!(extract_binlog_file_seq("mysql-bin.000123"), Some(123));
859 assert_eq!(extract_binlog_file_seq("my-host-bin.000001"), Some(1));
861 assert_eq!(
863 extract_binlog_file_seq("mysql-bin-changelog.037568"),
864 Some(37568)
865 );
866 assert_eq!(extract_binlog_file_seq("binlog.index"), None);
868 assert_eq!(extract_binlog_file_seq("no-extension"), None);
869 }
870
871 #[test]
872 fn test_mysql_binlog_offset() {
873 let offset = r#"{
874 "sourcePartition": {"server": "test"},
875 "sourceOffset": {
876 "file": "mysql-bin-changelog.037568",
877 "pos": 12345
878 },
879 "isHeartbeat": false
880 }"#;
881 let split = MysqlCdcSplit::new(1, Some(offset.to_owned()));
882 assert_eq!(split.mysql_binlog_offset(), Some((37568, 12345)));
883 }
884
885 #[test]
886 fn test_extract_sql_server_lsn_from_offset_str() {
887 let offset = r#"{
888 "sourcePartition": {"server":"RW_CDC_1001"},
889 "sourceOffset": {
890 "change_lsn":"00000027:00000ac0:0001",
891 "commit_lsn":"00000027:00000ac0:0002"
892 },
893 "isHeartbeat": false
894 }"#;
895
896 let change_lsn = extract_sql_server_change_lsn_from_offset_str(offset).unwrap();
897 let commit_lsn = extract_sql_server_commit_lsn_from_offset_str(offset).unwrap();
898 assert!(change_lsn < commit_lsn);
899 }
900}