Skip to main content

risingwave_connector/source/cdc/external/
mod.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub 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::parser::mysql_row_to_owned_row;
39use crate::source::CdcTableSnapshotSplit;
40use crate::source::cdc::CdcSourceType;
41use crate::source::cdc::external::mock_external_table::MockExternalTableReader;
42use crate::source::cdc::external::mysql::{
43    MySqlExternalTable, MySqlExternalTableReader, MySqlOffset,
44};
45use crate::source::cdc::external::postgres::{PostgresExternalTableReader, PostgresOffset};
46use crate::source::cdc::external::sql_server::{
47    SqlServerExternalTable, SqlServerExternalTableReader, SqlServerOffset,
48};
49
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
51pub enum ExternalCdcTableType {
52    Undefined,
53    Mock,
54    MySql,
55    Postgres,
56    SqlServer,
57    Citus,
58    Mongo,
59}
60
61impl ExternalCdcTableType {
62    pub fn from_properties(with_properties: &impl WithPropertiesExt) -> Self {
63        let connector = with_properties.get_connector().unwrap_or_default();
64        match connector.as_str() {
65            "mysql-cdc" => Self::MySql,
66            "postgres-cdc" => Self::Postgres,
67            "citus-cdc" => Self::Citus,
68            "sqlserver-cdc" => Self::SqlServer,
69            "mongodb-cdc" => Self::Mongo,
70            _ => Self::Undefined,
71        }
72    }
73
74    pub fn can_backfill(&self) -> bool {
75        matches!(self, Self::MySql | Self::Postgres | Self::SqlServer)
76    }
77
78    pub fn enable_transaction_metadata(&self) -> bool {
79        // In Debezium, transactional metadata cause delay of the newest events, as the `END` message is never sent unless a new transaction starts.
80        // So we only allow transactional metadata for MySQL and Postgres.
81        // See more in https://debezium.io/documentation/reference/2.6/connectors/sqlserver.html#sqlserver-transaction-metadata
82        matches!(self, Self::MySql | Self::Postgres)
83    }
84
85    pub async fn create_table_reader(
86        &self,
87        config: ExternalTableConfig,
88        schema: Schema,
89        pk_indices: Vec<usize>,
90        schema_table_name: SchemaTableName,
91    ) -> ConnectorResult<ExternalTableReaderImpl> {
92        match self {
93            Self::MySql => Ok(ExternalTableReaderImpl::MySql(
94                MySqlExternalTableReader::new(config, schema).await?,
95            )),
96            Self::Postgres => Ok(ExternalTableReaderImpl::Postgres(
97                PostgresExternalTableReader::new(config, schema, pk_indices, schema_table_name)
98                    .await?,
99            )),
100            Self::SqlServer => Ok(ExternalTableReaderImpl::SqlServer(
101                SqlServerExternalTableReader::new(config, schema, pk_indices).await?,
102            )),
103            // citus is never supported for cdc backfill (create source + create table).
104            Self::Mock => Ok(ExternalTableReaderImpl::Mock(MockExternalTableReader::new())),
105            _ => bail!("invalid external table type: {:?}", *self),
106        }
107    }
108}
109
110impl From<ExternalCdcTableType> for PbCdcTableType {
111    fn from(cdc_table_type: ExternalCdcTableType) -> Self {
112        match cdc_table_type {
113            ExternalCdcTableType::Postgres => Self::Postgres,
114            ExternalCdcTableType::MySql => Self::Mysql,
115            ExternalCdcTableType::SqlServer => Self::Sqlserver,
116
117            ExternalCdcTableType::Citus => Self::Citus,
118            ExternalCdcTableType::Mongo => Self::Mongo,
119            ExternalCdcTableType::Undefined | ExternalCdcTableType::Mock => Self::Unspecified,
120        }
121    }
122}
123
124impl From<PbCdcTableType> for ExternalCdcTableType {
125    fn from(cdc_table_type: PbCdcTableType) -> Self {
126        match cdc_table_type {
127            PbCdcTableType::Postgres => Self::Postgres,
128            PbCdcTableType::Mysql => Self::MySql,
129            PbCdcTableType::Sqlserver => Self::SqlServer,
130            PbCdcTableType::Mongo => Self::Mongo,
131            PbCdcTableType::Citus => Self::Citus,
132            PbCdcTableType::Unspecified => Self::Undefined,
133        }
134    }
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub struct SchemaTableName {
139    // namespace of the table, e.g. database in mysql, schema in postgres
140    pub schema_name: String,
141    pub table_name: String,
142}
143
144pub const TABLE_NAME_KEY: &str = "table.name";
145pub const SCHEMA_NAME_KEY: &str = "schema.name";
146pub const DATABASE_NAME_KEY: &str = "database.name";
147
148impl SchemaTableName {
149    pub fn from_properties(properties: &BTreeMap<String, String>) -> Self {
150        let table_type = ExternalCdcTableType::from_properties(properties);
151        let table_name = properties.get(TABLE_NAME_KEY).cloned().unwrap_or_default();
152
153        let schema_name = match table_type {
154            ExternalCdcTableType::MySql => properties
155                .get(DATABASE_NAME_KEY)
156                .cloned()
157                .unwrap_or_default(),
158            ExternalCdcTableType::Postgres | ExternalCdcTableType::Citus => {
159                properties.get(SCHEMA_NAME_KEY).cloned().unwrap_or_default()
160            }
161            ExternalCdcTableType::SqlServer => {
162                properties.get(SCHEMA_NAME_KEY).cloned().unwrap_or_default()
163            }
164            _ => {
165                unreachable!("invalid external table type: {:?}", table_type);
166            }
167        };
168
169        Self {
170            schema_name,
171            table_name,
172        }
173    }
174}
175
176#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
177pub enum CdcOffset {
178    MySql(MySqlOffset),
179    Postgres(PostgresOffset),
180    SqlServer(SqlServerOffset),
181}
182
183// Example debezium offset for Postgres:
184// {
185//     "sourcePartition":
186//     {
187//         "server": "RW_CDC_1004"
188//     },
189//     "sourceOffset":
190//     {
191//         "last_snapshot_record": false,
192//         "lsn": 29973552,
193//         "txId": 1046,
194//         "ts_usec": 1670826189008456,
195//         "snapshot": true
196//     }
197// }
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct DebeziumOffset {
200    #[serde(rename = "sourcePartition")]
201    pub source_partition: HashMap<String, String>,
202    #[serde(rename = "sourceOffset")]
203    pub source_offset: DebeziumSourceOffset,
204    #[serde(rename = "isHeartbeat")]
205    pub is_heartbeat: bool,
206}
207
208#[derive(Debug, Default, Clone, Serialize, Deserialize)]
209pub struct DebeziumSourceOffset {
210    // postgres snapshot progress
211    pub last_snapshot_record: Option<bool>,
212    // mysql snapshot progress
213    pub snapshot: Option<bool>,
214
215    // mysql binlog offset
216    pub file: Option<String>,
217    pub pos: Option<u64>,
218
219    // postgres offset
220    pub lsn: Option<u64>,
221    #[serde(rename = "txId")]
222    pub txid: Option<i64>,
223    pub tx_usec: Option<u64>,
224    pub lsn_commit: Option<u64>,
225    pub lsn_proc: Option<u64>,
226
227    // sql server offset
228    pub commit_lsn: Option<String>,
229    pub change_lsn: Option<String>,
230}
231
232pub type CdcOffsetParseFunc = Box<dyn Fn(&str) -> ConnectorResult<CdcOffset> + Send>;
233
234pub trait ExternalTableReader: Sized {
235    async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset>;
236
237    // Currently, MySQL cdc uses a connection pool to manage connections to MySQL, and other CDC processes do not require the disconnect step for now.
238
239    async fn disconnect(self) -> ConnectorResult<()> {
240        Ok(())
241    }
242
243    fn snapshot_read(
244        &self,
245        table_name: SchemaTableName,
246        start_pk: Option<OwnedRow>,
247        primary_keys: Vec<String>,
248        limit: u32,
249    ) -> BoxStream<'_, ConnectorResult<OwnedRow>>;
250
251    fn get_parallel_cdc_splits(
252        &self,
253        options: CdcTableSnapshotSplitOption,
254    ) -> BoxStream<'_, ConnectorResult<CdcTableSnapshotSplit>>;
255
256    fn split_snapshot_read(
257        &self,
258        table_name: SchemaTableName,
259        left: OwnedRow,
260        right: OwnedRow,
261        split_columns: Vec<Field>,
262    ) -> BoxStream<'_, ConnectorResult<OwnedRow>>;
263}
264
265pub struct CdcTableSnapshotSplitOption {
266    pub backfill_num_rows_per_split: u64,
267    pub backfill_as_even_splits: bool,
268    pub backfill_split_pk_column_index: u32,
269}
270
271pub enum ExternalTableReaderImpl {
272    MySql(MySqlExternalTableReader),
273    Postgres(PostgresExternalTableReader),
274    SqlServer(SqlServerExternalTableReader),
275    Mock(MockExternalTableReader),
276}
277
278#[derive(Debug, Default, Clone, Deserialize)]
279pub struct ExternalTableConfig {
280    pub connector: String,
281
282    #[serde(rename = "hostname")]
283    pub host: String,
284    pub port: String,
285    pub username: String,
286    pub password: String,
287    #[serde(rename = "database.name")]
288    pub database: String,
289    #[serde(rename = "schema.name", default = "Default::default")]
290    pub schema: String,
291    #[serde(rename = "table.name")]
292    pub table: String,
293    /// `ssl.mode` specifies the SSL/TLS encryption level for secure communication with Postgres.
294    /// Choices include `disabled`, `preferred`, and `required`.
295    /// This field is optional.
296    #[serde(rename = "ssl.mode", default = "postgres_ssl_mode_default")]
297    #[serde(alias = "debezium.database.sslmode")]
298    pub ssl_mode: SslMode,
299
300    #[serde(rename = "ssl.root.cert")]
301    #[serde(alias = "debezium.database.sslrootcert")]
302    pub ssl_root_cert: Option<String>,
303
304    /// `encrypt` specifies whether connect to SQL Server using SSL.
305    /// Only "true" means using SSL. All other values are treated as "false".
306    #[serde(rename = "database.encrypt", default = "Default::default")]
307    pub encrypt: String,
308}
309
310fn postgres_ssl_mode_default() -> SslMode {
311    // NOTE(StrikeW): Default to `disabled` for backward compatibility
312    SslMode::Disabled
313}
314
315impl ExternalTableConfig {
316    pub fn try_from_btreemap(
317        connect_properties: BTreeMap<String, String>,
318        secret_refs: BTreeMap<String, PbSecretRef>,
319    ) -> ConnectorResult<Self> {
320        let options_with_secret =
321            LocalSecretManager::global().fill_secrets(connect_properties, secret_refs)?;
322        let json_value = serde_json::to_value(options_with_secret)?;
323        let config = serde_json::from_value::<ExternalTableConfig>(json_value)?;
324        Ok(config)
325    }
326
327    /// Project the Postgres-specific subset of this config so it can drive the
328    /// shared `create_pg_client` / `PostgresExternalTable` helpers. Only meaningful
329    /// for the Postgres CDC connector; other connectors ignore the fields.
330    pub fn pg_connection_config(&self) -> ConnectorResult<PgConnectionConfig> {
331        let port = self
332            .port
333            .parse::<u16>()
334            .with_context(|| format!("invalid postgres port `{}`", self.port))?;
335        Ok(PgConnectionConfig {
336            host: self.host.clone(),
337            port,
338            user: self.username.clone(),
339            password: self.password.clone(),
340            database: self.database.clone(),
341            ssl_mode: self.ssl_mode.clone(),
342            ssl_root_cert: self.ssl_root_cert.clone(),
343        })
344    }
345}
346
347impl ExternalTableReader for ExternalTableReaderImpl {
348    async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset> {
349        match self {
350            ExternalTableReaderImpl::MySql(mysql) => mysql.current_cdc_offset().await,
351            ExternalTableReaderImpl::Postgres(postgres) => postgres.current_cdc_offset().await,
352            ExternalTableReaderImpl::SqlServer(sql_server) => sql_server.current_cdc_offset().await,
353            ExternalTableReaderImpl::Mock(mock) => mock.current_cdc_offset().await,
354        }
355    }
356
357    fn snapshot_read(
358        &self,
359        table_name: SchemaTableName,
360        start_pk: Option<OwnedRow>,
361        primary_keys: Vec<String>,
362        limit: u32,
363    ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
364        self.snapshot_read_inner(table_name, start_pk, primary_keys, limit)
365    }
366
367    fn get_parallel_cdc_splits(
368        &self,
369        options: CdcTableSnapshotSplitOption,
370    ) -> BoxStream<'_, ConnectorResult<CdcTableSnapshotSplit>> {
371        self.get_parallel_cdc_splits_inner(options)
372    }
373
374    fn split_snapshot_read(
375        &self,
376        table_name: SchemaTableName,
377        left: OwnedRow,
378        right: OwnedRow,
379        split_columns: Vec<Field>,
380    ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
381        self.split_snapshot_read_inner(table_name, left, right, split_columns)
382    }
383}
384
385impl ExternalTableReaderImpl {
386    pub fn get_cdc_offset_parser(&self) -> CdcOffsetParseFunc {
387        match self {
388            ExternalTableReaderImpl::MySql(_) => MySqlExternalTableReader::get_cdc_offset_parser(),
389            ExternalTableReaderImpl::Postgres(_) => {
390                PostgresExternalTableReader::get_cdc_offset_parser()
391            }
392            ExternalTableReaderImpl::SqlServer(_) => {
393                SqlServerExternalTableReader::get_cdc_offset_parser()
394            }
395            ExternalTableReaderImpl::Mock(_) => MockExternalTableReader::get_cdc_offset_parser(),
396        }
397    }
398
399    #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
400    async fn snapshot_read_inner(
401        &self,
402        table_name: SchemaTableName,
403        start_pk: Option<OwnedRow>,
404        primary_keys: Vec<String>,
405        limit: u32,
406    ) {
407        let stream = match self {
408            ExternalTableReaderImpl::MySql(mysql) => {
409                mysql.snapshot_read(table_name, start_pk, primary_keys, limit)
410            }
411            ExternalTableReaderImpl::Postgres(postgres) => {
412                postgres.snapshot_read(table_name, start_pk, primary_keys, limit)
413            }
414            ExternalTableReaderImpl::SqlServer(sql_server) => {
415                sql_server.snapshot_read(table_name, start_pk, primary_keys, limit)
416            }
417            ExternalTableReaderImpl::Mock(mock) => {
418                mock.snapshot_read(table_name, start_pk, primary_keys, limit)
419            }
420        };
421
422        pin_mut!(stream);
423        #[for_await]
424        for row in stream {
425            let row = row?;
426            yield row;
427        }
428    }
429
430    #[try_stream(boxed, ok = CdcTableSnapshotSplit, error = ConnectorError)]
431    async fn get_parallel_cdc_splits_inner(&self, options: CdcTableSnapshotSplitOption) {
432        let stream = match self {
433            ExternalTableReaderImpl::MySql(e) => e.get_parallel_cdc_splits(options),
434            ExternalTableReaderImpl::Postgres(e) => e.get_parallel_cdc_splits(options),
435            ExternalTableReaderImpl::SqlServer(e) => e.get_parallel_cdc_splits(options),
436            ExternalTableReaderImpl::Mock(e) => e.get_parallel_cdc_splits(options),
437        };
438        pin_mut!(stream);
439        #[for_await]
440        for row in stream {
441            let row = row?;
442            yield row;
443        }
444    }
445
446    #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
447    async fn split_snapshot_read_inner(
448        &self,
449        table_name: SchemaTableName,
450        left: OwnedRow,
451        right: OwnedRow,
452        split_columns: Vec<Field>,
453    ) {
454        let stream = match self {
455            ExternalTableReaderImpl::MySql(mysql) => {
456                mysql.split_snapshot_read(table_name, left, right, split_columns)
457            }
458            ExternalTableReaderImpl::Postgres(postgres) => {
459                postgres.split_snapshot_read(table_name, left, right, split_columns)
460            }
461            ExternalTableReaderImpl::SqlServer(sql_server) => {
462                sql_server.split_snapshot_read(table_name, left, right, split_columns)
463            }
464            ExternalTableReaderImpl::Mock(mock) => {
465                mock.split_snapshot_read(table_name, left, right, split_columns)
466            }
467        };
468
469        pin_mut!(stream);
470        #[for_await]
471        for row in stream {
472            let row = row?;
473            yield row;
474        }
475    }
476}
477
478pub enum ExternalTableImpl {
479    MySql(MySqlExternalTable),
480    Postgres(PostgresExternalTable),
481    SqlServer(SqlServerExternalTable),
482}
483
484impl ExternalTableImpl {
485    pub async fn connect(config: ExternalTableConfig) -> ConnectorResult<Self> {
486        let cdc_source_type = CdcSourceType::from(config.connector.as_str());
487        match cdc_source_type {
488            CdcSourceType::Mysql => Ok(ExternalTableImpl::MySql(
489                MySqlExternalTable::connect(config).await?,
490            )),
491            CdcSourceType::Postgres => {
492                let pg_conn = config.pg_connection_config()?;
493                Ok(ExternalTableImpl::Postgres(
494                    PostgresExternalTable::connect(&pg_conn, &config.schema, &config.table, false)
495                        .await?,
496                ))
497            }
498            CdcSourceType::SqlServer => Ok(ExternalTableImpl::SqlServer(
499                SqlServerExternalTable::connect(config).await?,
500            )),
501            _ => Err(anyhow!("Unsupported cdc connector type: {}", config.connector).into()),
502        }
503    }
504
505    pub fn column_descs(&self) -> &Vec<ColumnDesc> {
506        match self {
507            ExternalTableImpl::MySql(mysql) => mysql.column_descs(),
508            ExternalTableImpl::Postgres(postgres) => postgres.column_descs(),
509            ExternalTableImpl::SqlServer(sql_server) => sql_server.column_descs(),
510        }
511    }
512
513    pub fn pk_names(&self) -> &Vec<String> {
514        match self {
515            ExternalTableImpl::MySql(mysql) => mysql.pk_names(),
516            ExternalTableImpl::Postgres(postgres) => postgres.pk_names(),
517            ExternalTableImpl::SqlServer(sql_server) => sql_server.pk_names(),
518        }
519    }
520}
521
522pub const CDC_TABLE_SPLIT_ID_START: i64 = 1;