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 ) -> ConnectorResult<ExternalTableReaderImpl> {
91 match self {
92 Self::MySql => Ok(ExternalTableReaderImpl::MySql(
93 MySqlExternalTableReader::new(config, schema, pk_indices).await?,
94 )),
95 Self::Postgres => Ok(ExternalTableReaderImpl::Postgres(
96 PostgresExternalTableReader::new(config, schema, pk_indices, schema_table_name)
97 .await?,
98 )),
99 Self::SqlServer => Ok(ExternalTableReaderImpl::SqlServer(
100 SqlServerExternalTableReader::new(config, schema, pk_indices).await?,
101 )),
102 Self::Mock => Ok(ExternalTableReaderImpl::Mock(MockExternalTableReader::new())),
104 _ => bail!("invalid external table type: {:?}", *self),
105 }
106 }
107}
108
109impl From<ExternalCdcTableType> for PbCdcTableType {
110 fn from(cdc_table_type: ExternalCdcTableType) -> Self {
111 match cdc_table_type {
112 ExternalCdcTableType::Postgres => Self::Postgres,
113 ExternalCdcTableType::MySql => Self::Mysql,
114 ExternalCdcTableType::SqlServer => Self::Sqlserver,
115
116 ExternalCdcTableType::Citus => Self::Citus,
117 ExternalCdcTableType::Mongo => Self::Mongo,
118 ExternalCdcTableType::Undefined | ExternalCdcTableType::Mock => Self::Unspecified,
119 }
120 }
121}
122
123impl From<PbCdcTableType> for ExternalCdcTableType {
124 fn from(cdc_table_type: PbCdcTableType) -> Self {
125 match cdc_table_type {
126 PbCdcTableType::Postgres => Self::Postgres,
127 PbCdcTableType::Mysql => Self::MySql,
128 PbCdcTableType::Sqlserver => Self::SqlServer,
129 PbCdcTableType::Mongo => Self::Mongo,
130 PbCdcTableType::Citus => Self::Citus,
131 PbCdcTableType::Unspecified => Self::Undefined,
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub struct SchemaTableName {
138 pub schema_name: String,
140 pub table_name: String,
141}
142
143pub const TABLE_NAME_KEY: &str = "table.name";
144pub const SCHEMA_NAME_KEY: &str = "schema.name";
145pub const DATABASE_NAME_KEY: &str = "database.name";
146
147impl SchemaTableName {
148 pub fn from_properties(properties: &BTreeMap<String, String>) -> Self {
149 let table_type = ExternalCdcTableType::from_properties(properties);
150 let table_name = properties.get(TABLE_NAME_KEY).cloned().unwrap_or_default();
151
152 let schema_name = match table_type {
153 ExternalCdcTableType::MySql => properties
154 .get(DATABASE_NAME_KEY)
155 .cloned()
156 .unwrap_or_default(),
157 ExternalCdcTableType::Postgres | ExternalCdcTableType::Citus => {
158 properties.get(SCHEMA_NAME_KEY).cloned().unwrap_or_default()
159 }
160 ExternalCdcTableType::SqlServer => {
161 properties.get(SCHEMA_NAME_KEY).cloned().unwrap_or_default()
162 }
163 _ => {
164 unreachable!("invalid external table type: {:?}", table_type);
165 }
166 };
167
168 Self {
169 schema_name,
170 table_name,
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
176pub enum CdcOffset {
177 MySql(MySqlOffset),
178 Postgres(PostgresOffset),
179 SqlServer(SqlServerOffset),
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct DebeziumOffset {
199 #[serde(rename = "sourcePartition")]
200 pub source_partition: HashMap<String, String>,
201 #[serde(rename = "sourceOffset")]
202 pub source_offset: DebeziumSourceOffset,
203 #[serde(rename = "isHeartbeat")]
204 pub is_heartbeat: bool,
205}
206
207#[derive(Debug, Default, Clone, Serialize, Deserialize)]
208pub struct DebeziumSourceOffset {
209 pub last_snapshot_record: Option<bool>,
211 pub snapshot: Option<bool>,
213
214 pub file: Option<String>,
216 pub pos: Option<u64>,
217
218 pub lsn: Option<u64>,
220 #[serde(rename = "txId")]
221 pub txid: Option<i64>,
222 pub tx_usec: Option<u64>,
223 pub lsn_commit: Option<u64>,
224 pub lsn_proc: Option<u64>,
225
226 pub commit_lsn: Option<String>,
228 pub change_lsn: Option<String>,
229}
230
231pub type CdcOffsetParseFunc = Box<dyn Fn(&str) -> ConnectorResult<CdcOffset> + Send>;
232
233pub trait ExternalTableReader: Sized {
234 async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset>;
235
236 async fn disconnect(self) -> ConnectorResult<()> {
239 Ok(())
240 }
241
242 fn snapshot_read(
243 &self,
244 table_name: SchemaTableName,
245 start_pk: Option<OwnedRow>,
246 primary_keys: Vec<String>,
247 limit: u32,
248 ) -> BoxStream<'_, ConnectorResult<OwnedRow>>;
249
250 fn get_parallel_cdc_splits(
251 &self,
252 options: CdcTableSnapshotSplitOption,
253 ) -> BoxStream<'_, ConnectorResult<CdcTableSnapshotSplit>>;
254
255 fn split_snapshot_read(
256 &self,
257 table_name: SchemaTableName,
258 left: OwnedRow,
259 right: OwnedRow,
260 split_columns: Vec<Field>,
261 ) -> BoxStream<'_, ConnectorResult<OwnedRow>>;
262}
263
264pub struct CdcTableSnapshotSplitOption {
265 pub backfill_num_rows_per_split: u64,
266 pub backfill_as_even_splits: bool,
267 pub backfill_split_pk_column_index: u32,
268}
269
270pub enum ExternalTableReaderImpl {
271 MySql(MySqlExternalTableReader),
272 Postgres(PostgresExternalTableReader),
273 SqlServer(SqlServerExternalTableReader),
274 Mock(MockExternalTableReader),
275}
276
277#[derive(Debug, Default, Clone, Deserialize)]
278pub struct ExternalTableConfig {
279 pub connector: String,
280
281 #[serde(rename = "hostname")]
282 pub host: String,
283 pub port: String,
284 pub username: String,
285 pub password: String,
286 #[serde(rename = "database.name")]
287 pub database: String,
288 #[serde(rename = "schema.name", default = "Default::default")]
289 pub schema: String,
290 #[serde(rename = "table.name")]
291 pub table: String,
292 #[serde(rename = "ssl.mode", default = "postgres_ssl_mode_default")]
296 #[serde(alias = "debezium.database.sslmode")]
297 pub ssl_mode: SslMode,
298
299 #[serde(rename = "ssl.root.cert")]
300 #[serde(alias = "debezium.database.sslrootcert")]
301 pub ssl_root_cert: Option<String>,
302
303 #[serde(rename = "database.encrypt", default = "Default::default")]
306 pub encrypt: String,
307}
308
309fn postgres_ssl_mode_default() -> SslMode {
310 SslMode::Disabled
312}
313
314impl ExternalTableConfig {
315 pub fn try_from_btreemap(
316 connect_properties: BTreeMap<String, String>,
317 secret_refs: BTreeMap<String, PbSecretRef>,
318 ) -> ConnectorResult<Self> {
319 let options_with_secret =
320 LocalSecretManager::global().fill_secrets(connect_properties, secret_refs)?;
321 let json_value = serde_json::to_value(options_with_secret)?;
322 let config = serde_json::from_value::<ExternalTableConfig>(json_value)?;
323 Ok(config)
324 }
325
326 pub fn pg_connection_config(&self) -> ConnectorResult<PgConnectionConfig> {
330 let port = self
331 .port
332 .parse::<u16>()
333 .with_context(|| format!("invalid postgres port `{}`", self.port))?;
334 Ok(PgConnectionConfig {
335 host: self.host.clone(),
336 port,
337 user: self.username.clone(),
338 password: self.password.clone(),
339 database: self.database.clone(),
340 ssl_mode: self.ssl_mode.clone(),
341 ssl_root_cert: self.ssl_root_cert.clone(),
342 })
343 }
344}
345
346impl ExternalTableReader for ExternalTableReaderImpl {
347 async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset> {
348 match self {
349 ExternalTableReaderImpl::MySql(mysql) => mysql.current_cdc_offset().await,
350 ExternalTableReaderImpl::Postgres(postgres) => postgres.current_cdc_offset().await,
351 ExternalTableReaderImpl::SqlServer(sql_server) => sql_server.current_cdc_offset().await,
352 ExternalTableReaderImpl::Mock(mock) => mock.current_cdc_offset().await,
353 }
354 }
355
356 fn snapshot_read(
357 &self,
358 table_name: SchemaTableName,
359 start_pk: Option<OwnedRow>,
360 primary_keys: Vec<String>,
361 limit: u32,
362 ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
363 self.snapshot_read_inner(table_name, start_pk, primary_keys, limit)
364 }
365
366 fn get_parallel_cdc_splits(
367 &self,
368 options: CdcTableSnapshotSplitOption,
369 ) -> BoxStream<'_, ConnectorResult<CdcTableSnapshotSplit>> {
370 self.get_parallel_cdc_splits_inner(options)
371 }
372
373 fn split_snapshot_read(
374 &self,
375 table_name: SchemaTableName,
376 left: OwnedRow,
377 right: OwnedRow,
378 split_columns: Vec<Field>,
379 ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
380 self.split_snapshot_read_inner(table_name, left, right, split_columns)
381 }
382}
383
384impl ExternalTableReaderImpl {
385 pub fn pk_column_unsigned_i64_compare_flags(
389 &self,
390 pk_names: &[String],
391 ) -> ConnectorResult<Vec<bool>> {
392 match self {
393 ExternalTableReaderImpl::MySql(mysql) => {
394 mysql.pk_column_unsigned_i64_compare_flags(pk_names)
395 }
396 _ => Ok(vec![false; pk_names.len()]),
397 }
398 }
399
400 pub fn get_cdc_offset_parser(&self) -> CdcOffsetParseFunc {
401 match self {
402 ExternalTableReaderImpl::MySql(_) => MySqlExternalTableReader::get_cdc_offset_parser(),
403 ExternalTableReaderImpl::Postgres(_) => {
404 PostgresExternalTableReader::get_cdc_offset_parser()
405 }
406 ExternalTableReaderImpl::SqlServer(_) => {
407 SqlServerExternalTableReader::get_cdc_offset_parser()
408 }
409 ExternalTableReaderImpl::Mock(_) => MockExternalTableReader::get_cdc_offset_parser(),
410 }
411 }
412
413 #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
414 async fn snapshot_read_inner(
415 &self,
416 table_name: SchemaTableName,
417 start_pk: Option<OwnedRow>,
418 primary_keys: Vec<String>,
419 limit: u32,
420 ) {
421 let stream = match self {
422 ExternalTableReaderImpl::MySql(mysql) => {
423 mysql.snapshot_read(table_name, start_pk, primary_keys, limit)
424 }
425 ExternalTableReaderImpl::Postgres(postgres) => {
426 postgres.snapshot_read(table_name, start_pk, primary_keys, limit)
427 }
428 ExternalTableReaderImpl::SqlServer(sql_server) => {
429 sql_server.snapshot_read(table_name, start_pk, primary_keys, limit)
430 }
431 ExternalTableReaderImpl::Mock(mock) => {
432 mock.snapshot_read(table_name, start_pk, primary_keys, limit)
433 }
434 };
435
436 pin_mut!(stream);
437 #[for_await]
438 for row in stream {
439 let row = row?;
440 yield row;
441 }
442 }
443
444 #[try_stream(boxed, ok = CdcTableSnapshotSplit, error = ConnectorError)]
445 async fn get_parallel_cdc_splits_inner(&self, options: CdcTableSnapshotSplitOption) {
446 let stream = match self {
447 ExternalTableReaderImpl::MySql(e) => e.get_parallel_cdc_splits(options),
448 ExternalTableReaderImpl::Postgres(e) => e.get_parallel_cdc_splits(options),
449 ExternalTableReaderImpl::SqlServer(e) => e.get_parallel_cdc_splits(options),
450 ExternalTableReaderImpl::Mock(e) => e.get_parallel_cdc_splits(options),
451 };
452 pin_mut!(stream);
453 #[for_await]
454 for row in stream {
455 let row = row?;
456 yield row;
457 }
458 }
459
460 #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
461 async fn split_snapshot_read_inner(
462 &self,
463 table_name: SchemaTableName,
464 left: OwnedRow,
465 right: OwnedRow,
466 split_columns: Vec<Field>,
467 ) {
468 let stream = match self {
469 ExternalTableReaderImpl::MySql(mysql) => {
470 mysql.split_snapshot_read(table_name, left, right, split_columns)
471 }
472 ExternalTableReaderImpl::Postgres(postgres) => {
473 postgres.split_snapshot_read(table_name, left, right, split_columns)
474 }
475 ExternalTableReaderImpl::SqlServer(sql_server) => {
476 sql_server.split_snapshot_read(table_name, left, right, split_columns)
477 }
478 ExternalTableReaderImpl::Mock(mock) => {
479 mock.split_snapshot_read(table_name, left, right, split_columns)
480 }
481 };
482
483 pin_mut!(stream);
484 #[for_await]
485 for row in stream {
486 let row = row?;
487 yield row;
488 }
489 }
490}
491
492pub enum ExternalTableImpl {
493 MySql(MySqlExternalTable),
494 Postgres(PostgresExternalTable),
495 SqlServer(SqlServerExternalTable),
496}
497
498impl ExternalTableImpl {
499 pub async fn connect(config: ExternalTableConfig) -> ConnectorResult<Self> {
500 let cdc_source_type = CdcSourceType::from(config.connector.as_str());
501 match cdc_source_type {
502 CdcSourceType::Mysql => Ok(ExternalTableImpl::MySql(
503 MySqlExternalTable::connect(config).await?,
504 )),
505 CdcSourceType::Postgres => {
506 let pg_conn = config.pg_connection_config()?;
507 Ok(ExternalTableImpl::Postgres(
508 PostgresExternalTable::connect(
509 &pg_conn,
510 &config.schema,
511 &config.table,
512 false,
513 Some("SELECT"),
514 )
515 .await?,
516 ))
517 }
518 CdcSourceType::SqlServer => Ok(ExternalTableImpl::SqlServer(
519 SqlServerExternalTable::connect(config).await?,
520 )),
521 _ => Err(anyhow!("Unsupported cdc connector type: {}", config.connector).into()),
522 }
523 }
524
525 pub fn column_descs(&self) -> &Vec<ColumnDesc> {
526 match self {
527 ExternalTableImpl::MySql(mysql) => mysql.column_descs(),
528 ExternalTableImpl::Postgres(postgres) => postgres.column_descs(),
529 ExternalTableImpl::SqlServer(sql_server) => sql_server.column_descs(),
530 }
531 }
532
533 pub fn pk_names(&self) -> &Vec<String> {
534 match self {
535 ExternalTableImpl::MySql(mysql) => mysql.pk_names(),
536 ExternalTableImpl::Postgres(postgres) => postgres.pk_names(),
537 ExternalTableImpl::SqlServer(sql_server) => sql_server.pk_names(),
538 }
539 }
540}
541
542pub const CDC_TABLE_SPLIT_ID_START: i64 = 1;