1pub mod mock_external_table;
16pub mod postgres;
17pub mod sql_server;
18
19pub mod mysql;
20
21use std::collections::{BTreeMap, HashMap};
22
23use anyhow::{Context, anyhow};
24use futures::pin_mut;
25use futures::stream::BoxStream;
26use futures_async_stream::try_stream;
27use risingwave_common::bail;
28use risingwave_common::catalog::{ColumnDesc, Field, Schema};
29use risingwave_common::row::OwnedRow;
30use risingwave_common::secret::LocalSecretManager;
31use risingwave_pb::catalog::table::CdcTableType as PbCdcTableType;
32use risingwave_pb::secret::PbSecretRef;
33use serde::{Deserialize, Serialize};
34
35use crate::WithPropertiesExt;
36use crate::connector_common::{PgConnectionConfig, PostgresExternalTable, SslMode};
37use crate::error::{ConnectorError, ConnectorResult};
38use crate::source::CdcTableSnapshotSplit;
39use crate::source::cdc::CdcSourceType;
40use crate::source::cdc::external::mock_external_table::MockExternalTableReader;
41use crate::source::cdc::external::mysql::{
42 MySqlExternalTable, MySqlExternalTableReader, MySqlOffset,
43};
44use crate::source::cdc::external::postgres::{PostgresExternalTableReader, PostgresOffset};
45use crate::source::cdc::external::sql_server::{
46 SqlServerExternalTable, SqlServerExternalTableReader, SqlServerOffset,
47};
48
49#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub enum ExternalCdcTableType {
51 Undefined,
52 Mock,
53 MySql,
54 Postgres,
55 SqlServer,
56 Citus,
57 Mongo,
58}
59
60impl ExternalCdcTableType {
61 pub fn from_properties(with_properties: &impl WithPropertiesExt) -> Self {
62 let connector = with_properties.get_connector().unwrap_or_default();
63 match connector.as_str() {
64 "mysql-cdc" => Self::MySql,
65 "postgres-cdc" => Self::Postgres,
66 "citus-cdc" => Self::Citus,
67 "sqlserver-cdc" => Self::SqlServer,
68 "mongodb-cdc" => Self::Mongo,
69 _ => Self::Undefined,
70 }
71 }
72
73 pub fn can_backfill(&self) -> bool {
74 matches!(self, Self::MySql | Self::Postgres | Self::SqlServer)
75 }
76
77 pub fn enable_transaction_metadata(&self) -> bool {
78 matches!(self, Self::MySql | Self::Postgres)
82 }
83
84 pub async fn create_table_reader(
85 &self,
86 config: ExternalTableConfig,
87 schema: Schema,
88 pk_indices: Vec<usize>,
89 schema_table_name: SchemaTableName,
90 table_id: u32,
91 ) -> ConnectorResult<ExternalTableReaderImpl> {
92 match self {
93 Self::MySql => Ok(ExternalTableReaderImpl::MySql(
94 MySqlExternalTableReader::new(config, schema, pk_indices).await?,
95 )),
96 Self::Postgres => Ok(ExternalTableReaderImpl::Postgres(
97 PostgresExternalTableReader::new(
98 config,
99 schema,
100 pk_indices,
101 schema_table_name,
102 table_id,
103 )
104 .await?,
105 )),
106 Self::SqlServer => Ok(ExternalTableReaderImpl::SqlServer(
107 SqlServerExternalTableReader::new(config, schema, pk_indices).await?,
108 )),
109 Self::Mock => Ok(ExternalTableReaderImpl::Mock(MockExternalTableReader::new())),
111 _ => bail!("invalid external table type: {:?}", *self),
112 }
113 }
114}
115
116impl From<ExternalCdcTableType> for PbCdcTableType {
117 fn from(cdc_table_type: ExternalCdcTableType) -> Self {
118 match cdc_table_type {
119 ExternalCdcTableType::Postgres => Self::Postgres,
120 ExternalCdcTableType::MySql => Self::Mysql,
121 ExternalCdcTableType::SqlServer => Self::Sqlserver,
122
123 ExternalCdcTableType::Citus => Self::Citus,
124 ExternalCdcTableType::Mongo => Self::Mongo,
125 ExternalCdcTableType::Undefined | ExternalCdcTableType::Mock => Self::Unspecified,
126 }
127 }
128}
129
130impl From<PbCdcTableType> for ExternalCdcTableType {
131 fn from(cdc_table_type: PbCdcTableType) -> Self {
132 match cdc_table_type {
133 PbCdcTableType::Postgres => Self::Postgres,
134 PbCdcTableType::Mysql => Self::MySql,
135 PbCdcTableType::Sqlserver => Self::SqlServer,
136 PbCdcTableType::Mongo => Self::Mongo,
137 PbCdcTableType::Citus => Self::Citus,
138 PbCdcTableType::Unspecified => Self::Undefined,
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq)]
144pub struct SchemaTableName {
145 pub schema_name: String,
147 pub table_name: String,
148}
149
150pub const TABLE_NAME_KEY: &str = "table.name";
151pub const SCHEMA_NAME_KEY: &str = "schema.name";
152pub const DATABASE_NAME_KEY: &str = "database.name";
153
154impl SchemaTableName {
155 pub fn from_properties(properties: &BTreeMap<String, String>) -> Self {
156 let table_type = ExternalCdcTableType::from_properties(properties);
157 let table_name = properties.get(TABLE_NAME_KEY).cloned().unwrap_or_default();
158
159 let schema_name = match table_type {
160 ExternalCdcTableType::MySql => properties
161 .get(DATABASE_NAME_KEY)
162 .cloned()
163 .unwrap_or_default(),
164 ExternalCdcTableType::Postgres | ExternalCdcTableType::Citus => {
165 properties.get(SCHEMA_NAME_KEY).cloned().unwrap_or_default()
166 }
167 ExternalCdcTableType::SqlServer => {
168 properties.get(SCHEMA_NAME_KEY).cloned().unwrap_or_default()
169 }
170 _ => {
171 unreachable!("invalid external table type: {:?}", table_type);
172 }
173 };
174
175 Self {
176 schema_name,
177 table_name,
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
183pub enum CdcOffset {
184 MySql(MySqlOffset),
185 Postgres(PostgresOffset),
186 SqlServer(SqlServerOffset),
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct DebeziumOffset {
206 #[serde(rename = "sourcePartition")]
207 pub source_partition: HashMap<String, String>,
208 #[serde(rename = "sourceOffset")]
209 pub source_offset: DebeziumSourceOffset,
210 #[serde(rename = "isHeartbeat")]
211 pub is_heartbeat: bool,
212}
213
214#[derive(Debug, Default, Clone, Serialize, Deserialize)]
215pub struct DebeziumSourceOffset {
216 pub last_snapshot_record: Option<bool>,
218 pub snapshot: Option<bool>,
220
221 pub file: Option<String>,
223 pub pos: Option<u64>,
224
225 pub lsn: Option<u64>,
227 #[serde(rename = "txId")]
228 pub txid: Option<i64>,
229 pub tx_usec: Option<u64>,
230 pub lsn_commit: Option<u64>,
231 pub lsn_proc: Option<u64>,
232
233 pub commit_lsn: Option<String>,
235 pub change_lsn: Option<String>,
236}
237
238pub type CdcOffsetParseFunc = Box<dyn Fn(&str) -> ConnectorResult<CdcOffset> + Send>;
239
240pub trait ExternalTableReader: Sized {
241 async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset>;
242
243 async fn disconnect(self) -> ConnectorResult<()> {
246 Ok(())
247 }
248
249 fn snapshot_read(
250 &self,
251 table_name: SchemaTableName,
252 start_pk: Option<OwnedRow>,
253 primary_keys: Vec<String>,
254 limit: u32,
255 ) -> BoxStream<'_, ConnectorResult<OwnedRow>>;
256
257 fn get_parallel_cdc_splits(
258 &self,
259 options: CdcTableSnapshotSplitOption,
260 ) -> BoxStream<'_, ConnectorResult<CdcTableSnapshotSplit>>;
261
262 fn split_snapshot_read(
263 &self,
264 table_name: SchemaTableName,
265 left: OwnedRow,
266 right: OwnedRow,
267 split_columns: Vec<Field>,
268 ) -> BoxStream<'_, ConnectorResult<OwnedRow>>;
269}
270
271pub struct CdcTableSnapshotSplitOption {
272 pub backfill_num_rows_per_split: u64,
273 pub backfill_as_even_splits: bool,
274 pub backfill_split_pk_column_index: u32,
275}
276
277pub enum ExternalTableReaderImpl {
278 MySql(MySqlExternalTableReader),
279 Postgres(PostgresExternalTableReader),
280 SqlServer(SqlServerExternalTableReader),
281 Mock(MockExternalTableReader),
282}
283
284#[derive(Debug, Default, Clone, Deserialize)]
285pub struct ExternalTableConfig {
286 pub connector: String,
287
288 #[serde(rename = "hostname")]
289 pub host: String,
290 pub port: String,
291 pub username: String,
292 pub password: String,
293 #[serde(rename = "database.name")]
294 pub database: String,
295 #[serde(rename = "schema.name", default = "Default::default")]
296 pub schema: String,
297 #[serde(rename = "table.name")]
298 pub table: String,
299 #[serde(rename = "ssl.mode", default = "postgres_ssl_mode_default")]
303 #[serde(alias = "debezium.database.sslmode")]
304 pub ssl_mode: SslMode,
305
306 #[serde(rename = "ssl.root.cert")]
307 #[serde(alias = "debezium.database.sslrootcert")]
308 pub ssl_root_cert: Option<String>,
309
310 #[serde(rename = "database.encrypt", default = "Default::default")]
313 pub encrypt: String,
314}
315
316fn postgres_ssl_mode_default() -> SslMode {
317 SslMode::Disabled
319}
320
321impl ExternalTableConfig {
322 pub fn try_from_btreemap(
323 connect_properties: BTreeMap<String, String>,
324 secret_refs: BTreeMap<String, PbSecretRef>,
325 ) -> ConnectorResult<Self> {
326 let options_with_secret =
327 LocalSecretManager::global().fill_secrets(connect_properties, secret_refs)?;
328 let json_value = serde_json::to_value(options_with_secret)?;
329 let config = serde_json::from_value::<ExternalTableConfig>(json_value)?;
330 Ok(config)
331 }
332
333 pub fn pg_connection_config(&self) -> ConnectorResult<PgConnectionConfig> {
337 let port = self
338 .port
339 .parse::<u16>()
340 .with_context(|| format!("invalid postgres port `{}`", self.port))?;
341 Ok(PgConnectionConfig {
342 host: self.host.clone(),
343 port,
344 user: self.username.clone(),
345 password: self.password.clone(),
346 database: self.database.clone(),
347 ssl_mode: self.ssl_mode.clone(),
348 ssl_root_cert: self.ssl_root_cert.clone(),
349 })
350 }
351}
352
353impl ExternalTableReader for ExternalTableReaderImpl {
354 async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset> {
355 match self {
356 ExternalTableReaderImpl::MySql(mysql) => mysql.current_cdc_offset().await,
357 ExternalTableReaderImpl::Postgres(postgres) => postgres.current_cdc_offset().await,
358 ExternalTableReaderImpl::SqlServer(sql_server) => sql_server.current_cdc_offset().await,
359 ExternalTableReaderImpl::Mock(mock) => mock.current_cdc_offset().await,
360 }
361 }
362
363 fn snapshot_read(
364 &self,
365 table_name: SchemaTableName,
366 start_pk: Option<OwnedRow>,
367 primary_keys: Vec<String>,
368 limit: u32,
369 ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
370 self.snapshot_read_inner(table_name, start_pk, primary_keys, limit)
371 }
372
373 fn get_parallel_cdc_splits(
374 &self,
375 options: CdcTableSnapshotSplitOption,
376 ) -> BoxStream<'_, ConnectorResult<CdcTableSnapshotSplit>> {
377 self.get_parallel_cdc_splits_inner(options)
378 }
379
380 fn split_snapshot_read(
381 &self,
382 table_name: SchemaTableName,
383 left: OwnedRow,
384 right: OwnedRow,
385 split_columns: Vec<Field>,
386 ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
387 self.split_snapshot_read_inner(table_name, left, right, split_columns)
388 }
389}
390
391impl ExternalTableReaderImpl {
392 pub fn pk_column_unsigned_i64_compare_flags(
396 &self,
397 pk_names: &[String],
398 ) -> ConnectorResult<Vec<bool>> {
399 match self {
400 ExternalTableReaderImpl::MySql(mysql) => {
401 mysql.pk_column_unsigned_i64_compare_flags(pk_names)
402 }
403 _ => Ok(vec![false; pk_names.len()]),
404 }
405 }
406
407 pub fn get_cdc_offset_parser(&self) -> CdcOffsetParseFunc {
408 match self {
409 ExternalTableReaderImpl::MySql(_) => MySqlExternalTableReader::get_cdc_offset_parser(),
410 ExternalTableReaderImpl::Postgres(_) => {
411 PostgresExternalTableReader::get_cdc_offset_parser()
412 }
413 ExternalTableReaderImpl::SqlServer(_) => {
414 SqlServerExternalTableReader::get_cdc_offset_parser()
415 }
416 ExternalTableReaderImpl::Mock(_) => MockExternalTableReader::get_cdc_offset_parser(),
417 }
418 }
419
420 #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
421 async fn snapshot_read_inner(
422 &self,
423 table_name: SchemaTableName,
424 start_pk: Option<OwnedRow>,
425 primary_keys: Vec<String>,
426 limit: u32,
427 ) {
428 let stream = match self {
429 ExternalTableReaderImpl::MySql(mysql) => {
430 mysql.snapshot_read(table_name, start_pk, primary_keys, limit)
431 }
432 ExternalTableReaderImpl::Postgres(postgres) => {
433 postgres.snapshot_read(table_name, start_pk, primary_keys, limit)
434 }
435 ExternalTableReaderImpl::SqlServer(sql_server) => {
436 sql_server.snapshot_read(table_name, start_pk, primary_keys, limit)
437 }
438 ExternalTableReaderImpl::Mock(mock) => {
439 mock.snapshot_read(table_name, start_pk, primary_keys, limit)
440 }
441 };
442
443 pin_mut!(stream);
444 #[for_await]
445 for row in stream {
446 let row = row?;
447 yield row;
448 }
449 }
450
451 #[try_stream(boxed, ok = CdcTableSnapshotSplit, error = ConnectorError)]
452 async fn get_parallel_cdc_splits_inner(&self, options: CdcTableSnapshotSplitOption) {
453 let stream = match self {
454 ExternalTableReaderImpl::MySql(e) => e.get_parallel_cdc_splits(options),
455 ExternalTableReaderImpl::Postgres(e) => e.get_parallel_cdc_splits(options),
456 ExternalTableReaderImpl::SqlServer(e) => e.get_parallel_cdc_splits(options),
457 ExternalTableReaderImpl::Mock(e) => e.get_parallel_cdc_splits(options),
458 };
459 pin_mut!(stream);
460 #[for_await]
461 for row in stream {
462 let row = row?;
463 yield row;
464 }
465 }
466
467 #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
468 async fn split_snapshot_read_inner(
469 &self,
470 table_name: SchemaTableName,
471 left: OwnedRow,
472 right: OwnedRow,
473 split_columns: Vec<Field>,
474 ) {
475 let stream = match self {
476 ExternalTableReaderImpl::MySql(mysql) => {
477 mysql.split_snapshot_read(table_name, left, right, split_columns)
478 }
479 ExternalTableReaderImpl::Postgres(postgres) => {
480 postgres.split_snapshot_read(table_name, left, right, split_columns)
481 }
482 ExternalTableReaderImpl::SqlServer(sql_server) => {
483 sql_server.split_snapshot_read(table_name, left, right, split_columns)
484 }
485 ExternalTableReaderImpl::Mock(mock) => {
486 mock.split_snapshot_read(table_name, left, right, split_columns)
487 }
488 };
489
490 pin_mut!(stream);
491 #[for_await]
492 for row in stream {
493 let row = row?;
494 yield row;
495 }
496 }
497}
498
499pub enum ExternalTableImpl {
500 MySql(MySqlExternalTable),
501 Postgres(PostgresExternalTable),
502 SqlServer(SqlServerExternalTable),
503}
504
505impl ExternalTableImpl {
506 pub async fn connect(config: ExternalTableConfig) -> ConnectorResult<Self> {
507 let cdc_source_type = CdcSourceType::from(config.connector.as_str());
508 match cdc_source_type {
509 CdcSourceType::Mysql => Ok(ExternalTableImpl::MySql(
510 MySqlExternalTable::connect(config).await?,
511 )),
512 CdcSourceType::Postgres => {
513 let pg_conn = config.pg_connection_config()?;
514 Ok(ExternalTableImpl::Postgres(
515 PostgresExternalTable::connect(
516 &pg_conn,
517 &config.schema,
518 &config.table,
519 false,
520 Some("SELECT"),
521 )
522 .await?,
523 ))
524 }
525 CdcSourceType::SqlServer => Ok(ExternalTableImpl::SqlServer(
526 SqlServerExternalTable::connect(config).await?,
527 )),
528 _ => Err(anyhow!("Unsupported cdc connector type: {}", config.connector).into()),
529 }
530 }
531
532 pub fn column_descs(&self) -> &Vec<ColumnDesc> {
533 match self {
534 ExternalTableImpl::MySql(mysql) => mysql.column_descs(),
535 ExternalTableImpl::Postgres(postgres) => postgres.column_descs(),
536 ExternalTableImpl::SqlServer(sql_server) => sql_server.column_descs(),
537 }
538 }
539
540 pub fn pk_names(&self) -> &Vec<String> {
541 match self {
542 ExternalTableImpl::MySql(mysql) => mysql.pk_names(),
543 ExternalTableImpl::Postgres(postgres) => postgres.pk_names(),
544 ExternalTableImpl::SqlServer(sql_server) => sql_server.pk_names(),
545 }
546 }
547}
548
549pub const CDC_TABLE_SPLIT_ID_START: i64 = 1;