1pub mod enumerator;
16pub mod external;
17pub mod jni_source;
18pub mod mongodb;
19pub mod mysql;
20pub mod oracle;
21pub mod postgres;
22pub mod source;
23pub mod split;
24pub mod sql_server;
25
26use std::collections::{BTreeMap, HashMap};
27use std::marker::PhantomData;
28
29pub use enumerator::*;
30use itertools::Itertools;
31use risingwave_common::bail;
32use risingwave_common::id::{ActorId, SourceId};
33use risingwave_common::license::FeatureNotAvailable;
34use risingwave_pb::catalog::PbSource;
35use risingwave_pb::connector_service::{PbSourceType, PbTableSchema, SourceType, TableSchema};
36use risingwave_pb::plan_common::column_desc::GeneratedOrDefaultColumn;
37use risingwave_pb::plan_common::{ExternalTableDesc, PbEncodeType, PbFormatType};
38use risingwave_pb::secret::PbSecretRef;
39use risingwave_pb::source::{PbCdcTableSnapshotSplit, PbCdcTableSnapshotSplitsWithGeneration};
40use risingwave_pb::stream_plan::StreamCdcScanOptions;
41use simd_json::prelude::ArrayTrait;
42pub use source::*;
43
44use crate::enforce_secret::EnforceSecret;
45use crate::error::ConnectorResult;
46use crate::source::{CdcTableSnapshotSplitRaw, SourceProperties, SplitImpl, TryFromBTreeMap};
47use crate::{for_all_classified_sources, impl_cdc_source_type};
48
49pub const CDC_CONNECTOR_NAME_SUFFIX: &str = "-cdc";
50pub const CDC_SNAPSHOT_MODE_KEY: &str = "debezium.snapshot.mode";
51pub const CDC_SNAPSHOT_BACKFILL: &str = "rw_cdc_backfill";
52pub const CDC_SHARING_MODE_KEY: &str = "rw.sharing.mode.enable";
53pub const CDC_BACKFILL_ENABLE_KEY: &str = "snapshot";
55pub const CDC_BACKFILL_SNAPSHOT_INTERVAL_KEY: &str = "snapshot.interval";
56pub const CDC_BACKFILL_SNAPSHOT_BATCH_SIZE_KEY: &str = "snapshot.batch_size";
57pub const CDC_BACKFILL_PARALLELISM: &str = "backfill.parallelism";
58pub const CDC_BACKFILL_NUM_ROWS_PER_SPLIT: &str = "backfill.num_rows_per_split";
59pub const CDC_BACKFILL_AS_EVEN_SPLITS: &str = "backfill.as_even_splits";
60pub const CDC_BACKFILL_SPLIT_PK_COLUMN_INDEX: &str = "backfill.split_pk_column_index";
61pub const CDC_TRANSACTIONAL_KEY: &str = "transactional";
63pub const CDC_WAIT_FOR_STREAMING_START_TIMEOUT: &str = "cdc.source.wait.streaming.start.timeout";
64pub const CDC_BACKFILL_MAX_PARALLELISM: u32 = 256;
65
66pub const CDC_MONGODB_STRONG_SCHEMA_KEY: &str = "strong_schema";
68
69pub const MYSQL_CDC_CONNECTOR: &str = Mysql::CDC_CONNECTOR_NAME;
70pub const POSTGRES_CDC_CONNECTOR: &str = Postgres::CDC_CONNECTOR_NAME;
71pub const MONGODB_CDC_CONNECTOR: &str = Mongodb::CDC_CONNECTOR_NAME;
72pub const SQL_SERVER_CDC_CONNECTOR: &str = SqlServer::CDC_CONNECTOR_NAME;
73pub const ORACLE_CDC_CONNECTOR: &str = Oracle::CDC_CONNECTOR_NAME;
74
75pub fn build_cdc_table_id(source_id: SourceId, external_table_name: &str) -> String {
77 format!("{}.{}", source_id, external_table_name)
78}
79
80pub fn normalize_simple_postgres_quoted_table_name(table_name: &str) -> Option<String> {
81 let (schema_name, table_name) = table_name.split_once('.')?;
82 let table_name = table_name.strip_prefix('"')?.strip_suffix('"')?;
83 if schema_name.is_empty()
84 || table_name.is_empty()
85 || table_name.contains('.')
86 || table_name.contains('"')
87 || table_name.contains('\\')
88 || schema_name.contains('"')
89 {
90 return None;
91 }
92
93 Some(format!("{schema_name}.{table_name}"))
94}
95
96pub trait CdcSourceTypeTrait: Send + Sync + Clone + std::fmt::Debug + 'static {
97 const CDC_CONNECTOR_NAME: &'static str;
98 fn source_type() -> CdcSourceType;
99}
100
101pub trait CdcSourceRuntimeFlags: CdcSourceTypeTrait {
103 const CAN_BACKFILL: bool = false;
105 const CAN_BACKFILL_IN_PARALLEL: bool = false;
107 const CAN_USE_TRANSACTION_METADATA: bool = false;
110 const NEEDS_NON_EMPTY_INITIAL_OFFSET: bool = false;
113 const NEEDS_OFFSET_COMMIT_ON_CHECKPOINT: bool = false;
116}
117
118pub trait CdcSourceDdlValidator: CdcSourceTypeTrait {
120 fn check_format_encode(format: PbFormatType, encode: PbEncodeType) -> ConnectorResult<()> {
122 check_format_encode_inner(
123 Self::CDC_CONNECTOR_NAME,
124 format,
125 encode,
126 &[
127 (PbFormatType::Debezium, &[PbEncodeType::Json]),
128 (PbFormatType::Plain, &[PbEncodeType::Json]),
130 ],
131 )
132 }
133
134 fn check_include_column(additional_column_type: &str) -> ConnectorResult<()> {
137 bail!(
138 "additional column type {} is not supported for connector {}, which accepts no additional column",
139 additional_column_type,
140 Self::CDC_CONNECTOR_NAME
141 )
142 }
143
144 fn check_and_fill_create_with_options(
150 props: &mut BTreeMap<String, String>,
151 ) -> ConnectorResult<()> {
152 check_common_with_options(props)
153 }
154
155 fn check_license() -> Result<(), FeatureNotAvailable> {
157 Ok(())
158 }
159
160 fn check_alter_with_options(
163 props: &BTreeMap<String, String>,
164 _secret_refs: &BTreeMap<String, PbSecretRef>,
165 ) -> ConnectorResult<()> {
166 check_common_with_options(props)
167 }
168}
169
170pub fn check_common_with_options(props: &BTreeMap<String, String>) -> ConnectorResult<()> {
172 if let Some(mode) = props.get("debezium.decimal.handling.mode")
176 && mode != "string"
177 {
178 bail!("'debezium.decimal.handling.mode' must be 'string', got: '{mode}'");
179 }
180 for key in [
181 CDC_WAIT_FOR_STREAMING_START_TIMEOUT,
182 "debezium.max.queue.size",
183 ] {
184 if let Some(value) = props.get(key)
185 && value.parse::<u32>().is_err()
186 {
187 bail!("Invalid value `{value}` for `{key}`");
188 }
189 }
190 if let Some(value) = props.get("debezium.heartbeat.interval.ms")
193 && !value.parse::<i32>().is_ok_and(|interval| interval > 0)
194 {
195 bail!("'debezium.heartbeat.interval.ms' must be a positive integer, got: '{value}'");
196 }
197 Ok(())
198}
199
200pub fn check_format_encode_inner(
201 connector_name: &str,
202 format: PbFormatType,
203 encode: PbEncodeType,
204 compatible_formats: &[(PbFormatType, &[PbEncodeType])],
205) -> ConnectorResult<()> {
206 let Some((_, compatible_encodes)) = compatible_formats.iter().find(|(f, _)| *f == format)
207 else {
208 bail!(
209 "connector {} does not support format {:?}",
210 connector_name,
211 format
212 );
213 };
214 if !compatible_encodes.contains(&encode) {
215 bail!(
216 "connector {} does not support format {:?} with encode {:?}",
217 connector_name,
218 format,
219 encode
220 );
221 }
222 Ok(())
223}
224
225pub fn check_include_column_inner(
226 connector_name: &str,
227 additional_column_type: &str,
228 compatible_additional_column_types: &[&str],
229) -> ConnectorResult<()> {
230 if !compatible_additional_column_types.contains(&additional_column_type) {
231 bail!(
232 "additional column type {} is not supported for connector {}, acceptable column types: {:?}",
233 additional_column_type,
234 connector_name,
235 compatible_additional_column_types
236 );
237 }
238 Ok(())
239}
240
241pub fn check_alter_with_options_inner(
242 connector_name: &str,
243 props: &BTreeMap<String, String>,
244 secret_refs: &BTreeMap<String, PbSecretRef>,
245 allowed_keys: &[&str],
246) -> ConnectorResult<()> {
247 if let Some(key) = props
248 .keys()
249 .chain(secret_refs.keys())
250 .find(|key| !allowed_keys.contains(&key.as_str()))
251 {
252 bail!(
253 "Field '{}' is not allowed to be altered on the fly for connector: {}",
254 key,
255 connector_name
256 );
257 }
258 Ok(())
259}
260
261for_all_classified_sources!(impl_cdc_source_type);
262
263impl<'a> From<&'a str> for CdcSourceType {
264 fn from(name: &'a str) -> Self {
265 match name {
266 MYSQL_CDC_CONNECTOR => CdcSourceType::Mysql,
267 POSTGRES_CDC_CONNECTOR => CdcSourceType::Postgres,
268 MONGODB_CDC_CONNECTOR => CdcSourceType::Mongodb,
269 SQL_SERVER_CDC_CONNECTOR => CdcSourceType::SqlServer,
270 ORACLE_CDC_CONNECTOR => CdcSourceType::Oracle,
271 _ => CdcSourceType::Unspecified,
272 }
273 }
274}
275
276impl CdcSourceType {
277 pub fn as_str_name(&self) -> &str {
278 match self {
279 CdcSourceType::Mysql => "MySQL",
280 CdcSourceType::Postgres => "Postgres",
281 CdcSourceType::Mongodb => "MongoDB",
282 CdcSourceType::SqlServer => "SQL Server",
283 CdcSourceType::Oracle => "Oracle",
284 CdcSourceType::Unspecified => "Unspecified",
285 }
286 }
287}
288
289#[derive(Clone, Debug, Default)]
290pub struct CdcProperties<T: CdcSourceTypeTrait> {
291 pub properties: BTreeMap<String, String>,
293
294 pub table_schema: TableSchema,
296
297 pub is_cdc_source_job: bool,
299
300 pub is_backfill_table: bool,
302
303 pub _phantom: PhantomData<T>,
304}
305
306pub fn table_schema_exclude_additional_columns(table_schema: &TableSchema) -> TableSchema {
307 TableSchema {
308 columns: table_schema
309 .columns
310 .iter()
311 .filter(|col| {
312 col.additional_column
313 .as_ref()
314 .is_some_and(|val| val.column_type.is_none())
315 })
316 .cloned()
317 .collect(),
318 pk_indices: table_schema.pk_indices.clone(),
319 }
320}
321
322impl<T: CdcSourceTypeTrait> TryFromBTreeMap for CdcProperties<T> {
323 fn try_from_btreemap(
324 properties: BTreeMap<String, String>,
325 _deny_unknown_fields: bool,
326 ) -> ConnectorResult<Self> {
327 let is_share_source: bool = properties
328 .get(CDC_SHARING_MODE_KEY)
329 .is_some_and(|v| v == "true");
330 Ok(CdcProperties {
331 properties,
332 table_schema: Default::default(),
333 is_cdc_source_job: is_share_source,
335 is_backfill_table: false,
336 _phantom: PhantomData,
337 })
338 }
339}
340
341impl<T: CdcSourceTypeTrait> EnforceSecret for CdcProperties<T> {} impl<T: CdcSourceTypeTrait> SourceProperties for CdcProperties<T>
344where
345 DebeziumCdcSplit<T>: TryFrom<SplitImpl, Error = crate::error::ConnectorError> + Into<SplitImpl>,
346 DebeziumSplitEnumerator<T>: ListCdcSplits<CdcSourceType = T> + enumerator::CdcMonitor,
347{
348 type Split = DebeziumCdcSplit<T>;
349 type SplitEnumerator = DebeziumSplitEnumerator<T>;
350 type SplitReader = CdcSplitReader<T>;
351
352 const SOURCE_NAME: &'static str = T::CDC_CONNECTOR_NAME;
353
354 fn init_from_pb_source(&mut self, source: &PbSource) {
355 let pk_indices = source
356 .pk_column_ids
357 .iter()
358 .map(|&id| {
359 source
360 .columns
361 .iter()
362 .position(|col| col.column_desc.as_ref().unwrap().column_id == id)
363 .unwrap() as u32
364 })
365 .collect_vec();
366
367 let table_schema = PbTableSchema {
368 columns: source
369 .columns
370 .iter()
371 .flat_map(|col| &col.column_desc)
372 .filter(|col| {
373 !matches!(
374 col.generated_or_default_column,
375 Some(GeneratedOrDefaultColumn::GeneratedColumn(_))
376 )
377 })
378 .cloned()
379 .collect(),
380 pk_indices,
381 };
382 self.table_schema = table_schema;
383 if let Some(info) = source.info.as_ref() {
384 self.is_cdc_source_job = info.is_shared();
385 }
386 }
387
388 fn init_from_pb_cdc_table_desc(&mut self, table_desc: &ExternalTableDesc) {
389 let table_schema = TableSchema {
390 columns: table_desc
391 .columns
392 .iter()
393 .filter(|col| {
394 !matches!(
395 col.generated_or_default_column,
396 Some(GeneratedOrDefaultColumn::GeneratedColumn(_))
397 )
398 })
399 .cloned()
400 .collect(),
401 pk_indices: table_desc.stream_key.clone(),
402 };
403
404 self.table_schema = table_schema;
405 self.is_cdc_source_job = false;
406 self.is_backfill_table = true;
407 }
408}
409
410impl<T: CdcSourceTypeTrait> crate::source::UnknownFields for CdcProperties<T> {
411 fn unknown_fields(&self) -> HashMap<String, String> {
412 HashMap::new()
414 }
415}
416
417impl<T: CdcSourceTypeTrait> CdcProperties<T> {
418 pub fn get_source_type_pb(&self) -> SourceType {
419 SourceType::from(T::source_type())
420 }
421}
422
423pub const INVALID_CDC_SPLIT_ASSIGNMENT_GENERATION_ID: u64 = 0;
424pub const INITIAL_CDC_SPLIT_ASSIGNMENT_GENERATION_ID: u64 = 1;
425
426#[derive(Clone, Debug, PartialEq, Default)]
427pub struct CdcTableSnapshotSplitAssignmentWithGeneration {
428 pub splits: HashMap<ActorId, (Vec<CdcTableSnapshotSplitRaw>, u64)>,
429}
430
431impl CdcTableSnapshotSplitAssignmentWithGeneration {
432 pub fn new(splits: HashMap<ActorId, (Vec<CdcTableSnapshotSplitRaw>, u64)>) -> Self {
433 Self { splits }
434 }
435
436 pub fn empty() -> Self {
437 Self {
438 splits: HashMap::default(),
439 }
440 }
441}
442
443pub fn build_cdc_table_snapshot_split(s: &CdcTableSnapshotSplitRaw) -> PbCdcTableSnapshotSplit {
444 PbCdcTableSnapshotSplit {
445 split_id: s.split_id,
446 left_bound_inclusive: s.left_bound_inclusive.clone(),
447 right_bound_exclusive: s.right_bound_exclusive.clone(),
448 }
449}
450
451pub fn build_actor_cdc_table_snapshot_splits_with_generation(
452 pb_cdc_table_snapshot_split_assignment: PbCdcTableSnapshotSplitsWithGeneration,
453) -> CdcTableSnapshotSplitAssignmentWithGeneration {
454 let splits = pb_cdc_table_snapshot_split_assignment
455 .splits
456 .into_iter()
457 .map(|(actor_id, splits)| {
458 let generation = splits.generation;
459 let splits = splits
460 .splits
461 .into_iter()
462 .map(|s| CdcTableSnapshotSplitRaw {
463 split_id: s.split_id,
464 left_bound_inclusive: s.left_bound_inclusive,
465 right_bound_exclusive: s.right_bound_exclusive,
466 })
467 .collect();
468 (actor_id, (splits, generation))
469 })
470 .collect();
471 CdcTableSnapshotSplitAssignmentWithGeneration { splits }
472}
473
474#[derive(Debug, Clone, Hash, PartialEq)]
475pub struct CdcScanOptions {
476 pub disable_backfill: bool,
478 pub snapshot_barrier_interval: u32,
480 pub snapshot_batch_size: u32,
482 pub backfill_parallelism: u32,
484 pub backfill_num_rows_per_split: u64,
486 pub backfill_as_even_splits: bool,
488 pub backfill_split_pk_column_index: u32,
490}
491
492impl Default for CdcScanOptions {
493 fn default() -> Self {
494 Self {
495 disable_backfill: false,
496 snapshot_barrier_interval: 10,
497 snapshot_batch_size: 1000,
498 backfill_parallelism: 0,
500 backfill_num_rows_per_split: 100_000,
501 backfill_as_even_splits: true,
502 backfill_split_pk_column_index: 0,
503 }
504 }
505}
506
507impl CdcScanOptions {
508 pub fn to_proto(&self) -> StreamCdcScanOptions {
509 StreamCdcScanOptions {
510 disable_backfill: self.disable_backfill,
511 snapshot_barrier_interval: self.snapshot_barrier_interval,
512 snapshot_batch_size: self.snapshot_batch_size,
513 backfill_parallelism: self.backfill_parallelism,
514 backfill_num_rows_per_split: self.backfill_num_rows_per_split,
515 backfill_as_even_splits: self.backfill_as_even_splits,
516 backfill_split_pk_column_index: self.backfill_split_pk_column_index,
517 }
518 }
519
520 pub fn from_proto(proto: &StreamCdcScanOptions) -> Self {
521 Self {
522 disable_backfill: proto.disable_backfill,
523 snapshot_barrier_interval: proto.snapshot_barrier_interval,
524 snapshot_batch_size: proto.snapshot_batch_size,
525 backfill_parallelism: proto.backfill_parallelism,
526 backfill_num_rows_per_split: proto.backfill_num_rows_per_split,
527 backfill_as_even_splits: proto.backfill_as_even_splits,
528 backfill_split_pk_column_index: proto.backfill_split_pk_column_index,
529 }
530 }
531
532 pub fn is_parallelized_backfill(&self) -> bool {
533 !self.disable_backfill
534 && self.backfill_num_rows_per_split > 0
535 && self.backfill_parallelism > 0
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_normalize_simple_postgres_quoted_table_name() {
545 assert_eq!(
546 normalize_simple_postgres_quoted_table_name(r#"public."TableName""#).as_deref(),
547 Some("public.TableName")
548 );
549 assert_eq!(
550 normalize_simple_postgres_quoted_table_name(r#"public."pg_my_table""#).as_deref(),
551 Some("public.pg_my_table")
552 );
553 assert_eq!(
554 normalize_simple_postgres_quoted_table_name("public.table"),
555 None
556 );
557 assert_eq!(
558 normalize_simple_postgres_quoted_table_name(r#""Mixed.Schema"."TableName""#),
559 None
560 );
561 assert_eq!(
562 normalize_simple_postgres_quoted_table_name(r#"public."table.name""#),
563 None
564 );
565 assert_eq!(
566 normalize_simple_postgres_quoted_table_name(r#"public."table\name""#),
567 None
568 );
569 }
570}