Skip to main content

risingwave_connector/source/cdc/external/
postgres.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
15use std::cmp::Ordering;
16use std::sync::LazyLock;
17
18use anyhow::Context;
19use futures::stream::BoxStream;
20use futures::{StreamExt, pin_mut};
21use futures_async_stream::{for_await, try_stream};
22use itertools::Itertools;
23use risingwave_common::catalog::{Field, Schema};
24use risingwave_common::log::LogSuppressor;
25use risingwave_common::row::{OwnedRow, Row};
26use risingwave_common::types::{DataType, Datum, ScalarImpl, ToOwnedDatum};
27use risingwave_common::util::iter_util::ZipEqFast;
28use serde::{Deserialize, Serialize};
29use thiserror_ext::AsReport;
30use tokio_postgres::types::{PgLsn, Type as PgType};
31
32use crate::connector_common::create_pg_client;
33use crate::error::{ConnectorError, ConnectorResult};
34use crate::parser::scalar_adapter::ScalarAdapter;
35use crate::parser::{postgres_cell_to_scalar_impl, postgres_row_to_owned_row};
36use crate::source::CdcTableSnapshotSplit;
37use crate::source::cdc::external::{
38    CDC_TABLE_SPLIT_ID_START, CdcOffset, CdcOffsetParseFunc, CdcTableSnapshotSplitOption,
39    DebeziumOffset, ExternalTableConfig, ExternalTableReader, SchemaTableName,
40};
41
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct PostgresOffset {
44    pub txid: i64,
45    // In postgres, an LSN is a 64-bit integer, representing a byte position in the write-ahead log stream.
46    // It is printed as two hexadecimal numbers of up to 8 digits each, separated by a slash; for example, 16/B374D848
47    pub lsn: u64,
48    // Additional LSN fields for improved tracking
49    #[serde(default)]
50    pub lsn_commit: Option<u64>,
51    #[serde(default)]
52    pub lsn_proc: Option<u64>,
53}
54
55impl PartialOrd for PostgresOffset {
56    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
57        Some(self.cmp(other))
58    }
59}
60
61impl Eq for PostgresOffset {}
62impl PartialEq for PostgresOffset {
63    fn eq(&self, other: &Self) -> bool {
64        match (
65            self.lsn_commit,
66            self.lsn_proc,
67            other.lsn_commit,
68            other.lsn_proc,
69        ) {
70            (_, Some(_), _, Some(_)) => {
71                self.lsn_commit == other.lsn_commit && self.lsn_proc == other.lsn_proc
72            }
73            _ => self.lsn == other.lsn,
74        }
75    }
76}
77
78// only compare the lsn field, prefer lsn_commit and lsn_proc if both available
79impl Ord for PostgresOffset {
80    fn cmp(&self, other: &Self) -> Ordering {
81        match (
82            self.lsn_commit,
83            self.lsn_proc,
84            other.lsn_commit,
85            other.lsn_proc,
86        ) {
87            (_, Some(self_proc), _, Some(other_proc)) => {
88                // if both have `lsn_commit` and `lsn_proc`, compare `lsn_commit` first, then `lsn_proc`
89                // if `lsn_commit` is None, fall back to `lsn_proc`
90                match self.lsn_commit.cmp(&other.lsn_commit) {
91                    Ordering::Equal => self_proc.cmp(&other_proc),
92                    other_result => other_result,
93                }
94            }
95            _ => {
96                // Fall back to lsn comparison when either lsn_commit or lsn_proc is missing
97                static LOG_SUPPRESSOR: LazyLock<LogSuppressor> =
98                    LazyLock::new(LogSuppressor::default);
99                if let Ok(suppressed_count) = LOG_SUPPRESSOR.check() {
100                    tracing::warn!(
101                        suppressed_count,
102                        self_lsn = self.lsn,
103                        other_lsn = other.lsn,
104                        "lsn_commit and lsn_proc are missing, fall back to lsn comparison"
105                    );
106                }
107                self.lsn.cmp(&other.lsn)
108            }
109        }
110    }
111}
112
113impl PostgresOffset {
114    pub fn parse_debezium_offset(offset: &str) -> ConnectorResult<Self> {
115        let dbz_offset: DebeziumOffset = serde_json::from_str(offset)
116            .with_context(|| format!("invalid upstream offset: {}", offset))?;
117
118        let lsn = dbz_offset
119            .source_offset
120            .lsn
121            .context("invalid postgres lsn")?;
122
123        // `lsn_commit` may not be present in the offset for the first tx.
124        let lsn_commit = dbz_offset.source_offset.lsn_commit;
125
126        let lsn_proc = dbz_offset
127            .source_offset
128            .lsn_proc
129            .context("invalid postgres lsn_proc")?;
130
131        Ok(Self {
132            txid: dbz_offset
133                .source_offset
134                .txid
135                .context("invalid postgres txid")?,
136            lsn,
137            lsn_commit,
138            lsn_proc: Some(lsn_proc),
139        })
140    }
141}
142
143pub struct PostgresExternalTableReader {
144    rw_schema: Schema,
145    field_names: String,
146    pk_indices: Vec<usize>,
147    client: tokio::sync::Mutex<tokio_postgres::Client>,
148    schema_table_name: SchemaTableName,
149}
150
151impl ExternalTableReader for PostgresExternalTableReader {
152    async fn current_cdc_offset(&self) -> ConnectorResult<CdcOffset> {
153        let mut client = self.client.lock().await;
154        // start a transaction to read current lsn and txid
155        let trxn = client.transaction().await?;
156        let row = trxn.query_one("SELECT pg_current_wal_lsn()", &[]).await?;
157        let mut pg_offset = PostgresOffset::default();
158        let pg_lsn = row.get::<_, PgLsn>(0);
159        tracing::debug!("current lsn: {}", pg_lsn);
160        pg_offset.lsn = pg_lsn.into();
161
162        let txid_row = trxn.query_one("SELECT txid_current()", &[]).await?;
163        let txid: i64 = txid_row.get::<_, i64>(0);
164        pg_offset.txid = txid;
165
166        // commit the transaction
167        trxn.commit().await?;
168
169        Ok(CdcOffset::Postgres(pg_offset))
170    }
171
172    fn snapshot_read(
173        &self,
174        table_name: SchemaTableName,
175        start_pk: Option<OwnedRow>,
176        primary_keys: Vec<String>,
177        limit: u32,
178    ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
179        assert_eq!(table_name, self.schema_table_name);
180        self.snapshot_read_inner(table_name, start_pk, primary_keys, limit)
181    }
182
183    #[try_stream(boxed, ok = CdcTableSnapshotSplit, error = ConnectorError)]
184    async fn get_parallel_cdc_splits(&self, options: CdcTableSnapshotSplitOption) {
185        let backfill_num_rows_per_split = options.backfill_num_rows_per_split;
186        if backfill_num_rows_per_split == 0 {
187            return Err(anyhow::anyhow!(
188                "invalid backfill_num_rows_per_split, must be greater than 0"
189            )
190            .into());
191        }
192        if options.backfill_split_pk_column_index as usize >= self.pk_indices.len() {
193            return Err(anyhow::anyhow!(format!(
194                "invalid backfill_split_pk_column_index {}, out of bound",
195                options.backfill_split_pk_column_index
196            ))
197            .into());
198        }
199        let split_column = self.split_column(&options);
200        let row_stream = if options.backfill_as_even_splits
201            && is_supported_even_split_data_type(&split_column.data_type)
202        {
203            // For certain types, use evenly-sized partition to optimize performance.
204            tracing::info!(?self.schema_table_name, ?self.rw_schema, ?self.pk_indices, ?split_column, "Get parallel cdc table snapshot even splits.");
205            self.as_even_splits(options)
206        } else {
207            tracing::info!(?self.schema_table_name, ?self.rw_schema, ?self.pk_indices, ?split_column, "Get parallel cdc table snapshot uneven splits.");
208            self.as_uneven_splits(options)
209        };
210        pin_mut!(row_stream);
211        #[for_await]
212        for row in row_stream {
213            let row = row?;
214            yield row;
215        }
216    }
217
218    fn split_snapshot_read(
219        &self,
220        table_name: SchemaTableName,
221        left: OwnedRow,
222        right: OwnedRow,
223        split_columns: Vec<Field>,
224    ) -> BoxStream<'_, ConnectorResult<OwnedRow>> {
225        assert_eq!(table_name, self.schema_table_name);
226        self.split_snapshot_read_inner(table_name, left, right, split_columns)
227    }
228}
229
230impl PostgresExternalTableReader {
231    pub async fn new(
232        config: ExternalTableConfig,
233        rw_schema: Schema,
234        pk_indices: Vec<usize>,
235        schema_table_name: SchemaTableName,
236    ) -> ConnectorResult<Self> {
237        tracing::info!(
238            ?rw_schema,
239            ?pk_indices,
240            "create postgres external table reader"
241        );
242        // No TCP keepalive for CDC source
243        let client = create_pg_client(&config.pg_connection_config()?, None).await?;
244
245        // Discover user-defined composite columns. tokio-postgres cannot decode
246        // composite values natively, so for these columns we cast to text in the
247        // snapshot SELECT to get the `(a,b,c)` textual representation. Other
248        // varchar columns stay as-is to preserve RW's own text rendering
249        // (e.g. numeric -> "NaN"/"POSITIVE_INFINITY").
250        let composite_columns = client
251            .query(
252                "SELECT a.attname \
253                 FROM pg_attribute a \
254                 JOIN pg_class c ON a.attrelid = c.oid \
255                 JOIN pg_namespace n ON c.relnamespace = n.oid \
256                 JOIN pg_type t ON a.atttypid = t.oid \
257                 WHERE n.nspname = $1 \
258                   AND c.relname = $2 \
259                   AND a.attnum > 0 \
260                   AND NOT a.attisdropped \
261                   AND t.typtype = 'c'",
262                &[
263                    &schema_table_name.schema_name,
264                    &schema_table_name.table_name,
265                ],
266            )
267            .await
268            .map(|rows| {
269                rows.into_iter()
270                    .map(|row| row.get::<_, String>(0))
271                    .collect::<std::collections::HashSet<_>>()
272            })
273            .unwrap_or_else(|err| {
274                tracing::warn!(
275                    error = %err.as_report(),
276                    schema = %schema_table_name.schema_name,
277                    table = %schema_table_name.table_name,
278                    "failed to discover postgres composite columns; falling back to no text cast"
279                );
280                std::collections::HashSet::new()
281            });
282
283        let field_names = rw_schema
284            .fields
285            .iter()
286            .map(|f| {
287                let quoted = Self::quote_column(&f.name);
288                if matches!(f.data_type, DataType::Varchar) && composite_columns.contains(&f.name) {
289                    format!("{quoted}::text AS {quoted}")
290                } else {
291                    quoted
292                }
293            })
294            .join(",");
295
296        Ok(Self {
297            rw_schema,
298            field_names,
299            pk_indices,
300            client: tokio::sync::Mutex::new(client),
301            schema_table_name,
302        })
303    }
304
305    pub fn get_normalized_table_name(table_name: &SchemaTableName) -> String {
306        format!(
307            "\"{}\".\"{}\"",
308            table_name.schema_name, table_name.table_name
309        )
310    }
311
312    pub fn get_cdc_offset_parser() -> CdcOffsetParseFunc {
313        Box::new(move |offset| {
314            Ok(CdcOffset::Postgres(PostgresOffset::parse_debezium_offset(
315                offset,
316            )?))
317        })
318    }
319
320    #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
321    async fn snapshot_read_inner(
322        &self,
323        table_name: SchemaTableName,
324        start_pk_row: Option<OwnedRow>,
325        primary_keys: Vec<String>,
326        scan_limit: u32,
327    ) {
328        let order_key = Self::get_order_key(&primary_keys);
329        let client = self.client.lock().await;
330        client.execute("set time zone '+00:00'", &[]).await?;
331
332        let stream = match start_pk_row {
333            Some(ref pk_row) => {
334                // prepare the scan statement, since we may need to convert the RW data type to postgres data type
335                // e.g. varchar to uuid
336                let prepared_scan_stmt = {
337                    let primary_keys = self
338                        .pk_indices
339                        .iter()
340                        .map(|i| self.rw_schema.fields[*i].name.clone())
341                        .collect_vec();
342
343                    let order_key = Self::get_order_key(&primary_keys);
344                    let scan_sql = format!(
345                        "SELECT {} FROM {} WHERE {} ORDER BY {} LIMIT {scan_limit}",
346                        self.field_names,
347                        Self::get_normalized_table_name(&table_name),
348                        Self::filter_expression(&primary_keys),
349                        order_key,
350                    );
351                    client.prepare(&scan_sql).await?
352                };
353
354                let params: Vec<Option<ScalarAdapter>> = pk_row
355                    .iter()
356                    .zip_eq_fast(prepared_scan_stmt.params())
357                    .map(|(datum, ty)| {
358                        datum
359                            .map(|scalar| ScalarAdapter::from_scalar(scalar, ty))
360                            .transpose()
361                    })
362                    .try_collect()?;
363
364                client.query_raw(&prepared_scan_stmt, &params).await?
365            }
366            None => {
367                let sql = format!(
368                    "SELECT {} FROM {} ORDER BY {} LIMIT {scan_limit}",
369                    self.field_names,
370                    Self::get_normalized_table_name(&table_name),
371                    order_key,
372                );
373                let params: Vec<Option<ScalarAdapter>> = vec![];
374                client.query_raw(&sql, &params).await?
375            }
376        };
377
378        let row_stream = stream.map(|row| {
379            let row = row?;
380            Ok::<_, crate::error::ConnectorError>(postgres_row_to_owned_row(row, &self.rw_schema))
381        });
382
383        pin_mut!(row_stream);
384        #[for_await]
385        for row in row_stream {
386            let row = row?;
387            yield row;
388        }
389    }
390
391    // row filter expression: (v1, v2, v3) > ($1, $2, $3)
392    fn filter_expression(columns: &[String]) -> String {
393        let mut col_expr = String::new();
394        let mut arg_expr = String::new();
395        for (i, column) in columns.iter().enumerate() {
396            if i > 0 {
397                col_expr.push_str(", ");
398                arg_expr.push_str(", ");
399            }
400            col_expr.push_str(&Self::quote_column(column));
401            arg_expr.push_str(format!("${}", i + 1).as_str());
402        }
403        format!("({}) > ({})", col_expr, arg_expr)
404    }
405
406    // row filter expression: (v1, v2, v3) >= ($1, $2, $3) AND (v1, v2, v3) < ($1, $2, $3)
407    fn split_filter_expression(
408        columns: &[String],
409        is_first_split: bool,
410        is_last_split: bool,
411    ) -> String {
412        let mut left_col_expr = String::new();
413        let mut left_arg_expr = String::new();
414        let mut right_col_expr = String::new();
415        let mut right_arg_expr = String::new();
416        let mut c = 1;
417        if !is_first_split {
418            for (i, column) in columns.iter().enumerate() {
419                if i > 0 {
420                    left_col_expr.push_str(", ");
421                    left_arg_expr.push_str(", ");
422                }
423                left_col_expr.push_str(&Self::quote_column(column));
424                left_arg_expr.push_str(format!("${}", c).as_str());
425                c += 1;
426            }
427        }
428        if !is_last_split {
429            for (i, column) in columns.iter().enumerate() {
430                if i > 0 {
431                    right_col_expr.push_str(", ");
432                    right_arg_expr.push_str(", ");
433                }
434                right_col_expr.push_str(&Self::quote_column(column));
435                right_arg_expr.push_str(format!("${}", c).as_str());
436                c += 1;
437            }
438        }
439        if is_first_split && is_last_split {
440            "1 = 1".to_owned()
441        } else if is_first_split {
442            format!("({}) < ({})", right_col_expr, right_arg_expr,)
443        } else if is_last_split {
444            format!("({}) >= ({})", left_col_expr, left_arg_expr,)
445        } else {
446            format!(
447                "({}) >= ({}) AND ({}) < ({})",
448                left_col_expr, left_arg_expr, right_col_expr, right_arg_expr,
449            )
450        }
451    }
452
453    fn get_order_key(primary_keys: &Vec<String>) -> String {
454        primary_keys
455            .iter()
456            .map(|col| Self::quote_column(col))
457            .join(",")
458    }
459
460    fn quote_column(column: &str) -> String {
461        format!("\"{}\"", column)
462    }
463
464    async fn min_and_max(
465        &self,
466        split_column: &Field,
467    ) -> ConnectorResult<Option<(ScalarImpl, ScalarImpl)>> {
468        let sql = format!(
469            "SELECT MIN({}), MAX({}) FROM {}",
470            split_column.name,
471            split_column.name,
472            Self::get_normalized_table_name(&self.schema_table_name),
473        );
474        let client = self.client.lock().await;
475        let rows = client.query(&sql, &[]).await?;
476        if rows.is_empty() {
477            Ok(None)
478        } else {
479            let row = &rows[0];
480            let min =
481                postgres_cell_to_scalar_impl(row, &split_column.data_type, 0, &split_column.name);
482            let max =
483                postgres_cell_to_scalar_impl(row, &split_column.data_type, 1, &split_column.name);
484            match (min, max) {
485                (Some(min), Some(max)) => Ok(Some((min, max))),
486                _ => Ok(None),
487            }
488        }
489    }
490
491    async fn next_split_right_bound_exclusive(
492        &self,
493        left_value: &ScalarImpl,
494        max_value: &ScalarImpl,
495        max_split_size: u64,
496        split_column: &Field,
497    ) -> ConnectorResult<Option<Datum>> {
498        let sql = format!(
499            "WITH t as (SELECT {} FROM {} WHERE {} >= $1 ORDER BY {} ASC LIMIT {}) SELECT CASE WHEN MAX({}) < $2 THEN MAX({}) ELSE NULL END FROM t",
500            Self::quote_column(&split_column.name),
501            Self::get_normalized_table_name(&self.schema_table_name),
502            Self::quote_column(&split_column.name),
503            Self::quote_column(&split_column.name),
504            max_split_size,
505            Self::quote_column(&split_column.name),
506            Self::quote_column(&split_column.name),
507        );
508        let client = self.client.lock().await;
509        let prepared_stmt = client.prepare(&sql).await?;
510        let params: Vec<Option<ScalarAdapter>> = vec![
511            Some(ScalarAdapter::from_scalar(
512                left_value.as_scalar_ref_impl(),
513                &prepared_stmt.params()[0],
514            )?),
515            Some(ScalarAdapter::from_scalar(
516                max_value.as_scalar_ref_impl(),
517                &prepared_stmt.params()[1],
518            )?),
519        ];
520        let stream = client.query_raw(&prepared_stmt, &params).await?;
521        let datum_stream = stream.map(|row| {
522            let row = row?;
523            Ok::<_, ConnectorError>(postgres_cell_to_scalar_impl(
524                &row,
525                &split_column.data_type,
526                0,
527                &split_column.name,
528            ))
529        });
530        pin_mut!(datum_stream);
531        #[for_await]
532        for datum in datum_stream {
533            let right = datum?;
534            return Ok(Some(right.to_owned_datum()));
535        }
536        Ok(None)
537    }
538
539    async fn next_greater_bound(
540        &self,
541        start_offset: &ScalarImpl,
542        max_value: &ScalarImpl,
543        split_column: &Field,
544    ) -> ConnectorResult<Option<Datum>> {
545        let sql = format!(
546            "SELECT MIN({}) FROM {} WHERE {} > $1 AND {} <$2",
547            Self::quote_column(&split_column.name),
548            Self::get_normalized_table_name(&self.schema_table_name),
549            Self::quote_column(&split_column.name),
550            Self::quote_column(&split_column.name),
551        );
552        let client = self.client.lock().await;
553        let prepared_stmt = client.prepare(&sql).await?;
554        let params: Vec<Option<ScalarAdapter>> = vec![
555            Some(ScalarAdapter::from_scalar(
556                start_offset.as_scalar_ref_impl(),
557                &prepared_stmt.params()[0],
558            )?),
559            Some(ScalarAdapter::from_scalar(
560                max_value.as_scalar_ref_impl(),
561                &prepared_stmt.params()[1],
562            )?),
563        ];
564        let stream = client.query_raw(&prepared_stmt, &params).await?;
565        let datum_stream = stream.map(|row| {
566            let row = row?;
567            Ok::<_, ConnectorError>(postgres_cell_to_scalar_impl(
568                &row,
569                &split_column.data_type,
570                0,
571                &split_column.name,
572            ))
573        });
574        pin_mut!(datum_stream);
575        #[for_await]
576        for datum in datum_stream {
577            let right = datum?;
578            return Ok(Some(right));
579        }
580        Ok(None)
581    }
582
583    #[try_stream(boxed, ok = OwnedRow, error = ConnectorError)]
584    async fn split_snapshot_read_inner(
585        &self,
586        table_name: SchemaTableName,
587        left: OwnedRow,
588        right: OwnedRow,
589        split_columns: Vec<Field>,
590    ) {
591        assert_eq!(
592            split_columns.len(),
593            1,
594            "multiple split columns is not supported yet"
595        );
596        assert_eq!(left.len(), 1, "multiple split columns is not supported yet");
597        assert_eq!(
598            right.len(),
599            1,
600            "multiple split columns is not supported yet"
601        );
602        let is_first_split = left[0].is_none();
603        let is_last_split = right[0].is_none();
604        let split_column_names = split_columns.iter().map(|c| c.name.clone()).collect_vec();
605        let client = self.client.lock().await;
606        client.execute("set time zone '+00:00'", &[]).await?;
607        // prepare the scan statement, since we may need to convert the RW data type to postgres data type
608        // e.g. varchar to uuid
609        let prepared_scan_stmt = {
610            let scan_sql = format!(
611                "SELECT {} FROM {} WHERE {}",
612                self.field_names,
613                Self::get_normalized_table_name(&table_name),
614                Self::split_filter_expression(&split_column_names, is_first_split, is_last_split),
615            );
616            client.prepare(&scan_sql).await?
617        };
618
619        let mut params: Vec<Option<ScalarAdapter>> = vec![];
620        if !is_first_split {
621            let left_params: Vec<Option<ScalarAdapter>> = left
622                .iter()
623                .zip_eq_fast(prepared_scan_stmt.params().iter().take(left.len()))
624                .map(|(datum, ty)| {
625                    datum
626                        .map(|scalar| ScalarAdapter::from_scalar(scalar, ty))
627                        .transpose()
628                })
629                .try_collect()?;
630            params.extend(left_params);
631        }
632        if !is_last_split {
633            let right_params: Vec<Option<ScalarAdapter>> = right
634                .iter()
635                .zip_eq_fast(prepared_scan_stmt.params().iter().skip(params.len()))
636                .map(|(datum, ty)| {
637                    datum
638                        .map(|scalar| ScalarAdapter::from_scalar(scalar, ty))
639                        .transpose()
640                })
641                .try_collect()?;
642            params.extend(right_params);
643        }
644
645        let stream = client.query_raw(&prepared_scan_stmt, &params).await?;
646        let row_stream = stream.map(|row| {
647            let row = row?;
648            Ok::<_, crate::error::ConnectorError>(postgres_row_to_owned_row(row, &self.rw_schema))
649        });
650
651        pin_mut!(row_stream);
652        #[for_await]
653        for row in row_stream {
654            let row = row?;
655            yield row;
656        }
657    }
658
659    #[try_stream(boxed, ok = CdcTableSnapshotSplit, error = ConnectorError)]
660    async fn as_uneven_splits(&self, options: CdcTableSnapshotSplitOption) {
661        let split_column = self.split_column(&options);
662        let mut split_id = CDC_TABLE_SPLIT_ID_START;
663        let Some((min_value, max_value)) = self.min_and_max(&split_column).await? else {
664            let left_bound_row = OwnedRow::new(vec![None]);
665            let right_bound_row = OwnedRow::new(vec![None]);
666            let split = CdcTableSnapshotSplit {
667                split_id,
668                left_bound_inclusive: left_bound_row,
669                right_bound_exclusive: right_bound_row,
670            };
671            yield split;
672            return Ok(());
673        };
674        // left bound will never be NULL value.
675        let mut next_left_bound_inclusive = min_value.clone();
676        loop {
677            let left_bound_inclusive: Datum = if next_left_bound_inclusive == min_value {
678                None
679            } else {
680                Some(next_left_bound_inclusive.clone())
681            };
682            let right_bound_exclusive;
683            let mut next_right = self
684                .next_split_right_bound_exclusive(
685                    &next_left_bound_inclusive,
686                    &max_value,
687                    options.backfill_num_rows_per_split,
688                    &split_column,
689                )
690                .await?;
691            if let Some(Some(ref inner)) = next_right
692                && *inner == next_left_bound_inclusive
693            {
694                next_right = self
695                    .next_greater_bound(&next_left_bound_inclusive, &max_value, &split_column)
696                    .await?;
697            }
698            if let Some(next_right) = next_right {
699                match next_right {
700                    None => {
701                        // NULL found.
702                        right_bound_exclusive = None;
703                    }
704                    Some(next_right) => {
705                        next_left_bound_inclusive = next_right.clone();
706                        right_bound_exclusive = Some(next_right);
707                    }
708                }
709            } else {
710                // Not found.
711                right_bound_exclusive = None;
712            };
713            let is_completed = right_bound_exclusive.is_none();
714            if is_completed && left_bound_inclusive.is_none() {
715                assert_eq!(split_id, 1);
716            }
717            tracing::info!(
718                split_id,
719                ?left_bound_inclusive,
720                ?right_bound_exclusive,
721                "New CDC table snapshot split."
722            );
723            let left_bound_row = OwnedRow::new(vec![left_bound_inclusive]);
724            let right_bound_row = OwnedRow::new(vec![right_bound_exclusive]);
725            let split = CdcTableSnapshotSplit {
726                split_id,
727                left_bound_inclusive: left_bound_row,
728                right_bound_exclusive: right_bound_row,
729            };
730            try_increase_split_id(&mut split_id)?;
731            yield split;
732            if is_completed {
733                break;
734            }
735        }
736    }
737
738    #[try_stream(boxed, ok = CdcTableSnapshotSplit, error = ConnectorError)]
739    async fn as_even_splits(&self, options: CdcTableSnapshotSplitOption) {
740        let split_column = self.split_column(&options);
741        let mut split_id = 1;
742        let Some((min_value, max_value)) = self.min_and_max(&split_column).await? else {
743            let left_bound_row = OwnedRow::new(vec![None]);
744            let right_bound_row = OwnedRow::new(vec![None]);
745            let split = CdcTableSnapshotSplit {
746                split_id,
747                left_bound_inclusive: left_bound_row,
748                right_bound_exclusive: right_bound_row,
749            };
750            yield split;
751            return Ok(());
752        };
753        let min_value = min_value.as_integral();
754        let max_value = max_value.as_integral();
755        let saturated_split_max_size = options
756            .backfill_num_rows_per_split
757            .try_into()
758            .unwrap_or(i64::MAX);
759        let mut left = None;
760        let mut right = Some(min_value.saturating_add(saturated_split_max_size));
761        loop {
762            let mut is_completed = false;
763            if right.as_ref().map(|r| *r >= max_value).unwrap_or(true) {
764                right = None;
765                is_completed = true;
766            }
767            let split = CdcTableSnapshotSplit {
768                split_id,
769                left_bound_inclusive: OwnedRow::new(vec![
770                    left.map(|l| to_int_scalar(l, &split_column.data_type)),
771                ]),
772                right_bound_exclusive: OwnedRow::new(vec![
773                    right.map(|r| to_int_scalar(r, &split_column.data_type)),
774                ]),
775            };
776            try_increase_split_id(&mut split_id)?;
777            yield split;
778            if is_completed {
779                break;
780            }
781            left = right;
782            right = left.map(|l| l.saturating_add(saturated_split_max_size));
783        }
784    }
785
786    fn split_column(&self, options: &CdcTableSnapshotSplitOption) -> Field {
787        self.rw_schema.fields[self.pk_indices[options.backfill_split_pk_column_index as usize]]
788            .clone()
789    }
790}
791
792fn to_int_scalar(i: i64, data_type: &DataType) -> ScalarImpl {
793    match data_type {
794        DataType::Int16 => ScalarImpl::Int16(i.try_into().unwrap()),
795        DataType::Int32 => ScalarImpl::Int32(i.try_into().unwrap()),
796        DataType::Int64 => ScalarImpl::Int64(i),
797        _ => {
798            panic!("Can't convert int {} to ScalarImpl::{}", i, data_type)
799        }
800    }
801}
802
803fn try_increase_split_id(split_id: &mut i64) -> ConnectorResult<()> {
804    match split_id.checked_add(1) {
805        Some(s) => {
806            *split_id = s;
807            Ok(())
808        }
809        None => Err(anyhow::anyhow!("too many CDC snapshot splits").into()),
810    }
811}
812
813/// Use the first column of primary keys to split table.
814fn is_supported_even_split_data_type(data_type: &DataType) -> bool {
815    matches!(
816        data_type,
817        DataType::Int16 | DataType::Int32 | DataType::Int64
818    )
819}
820
821pub fn type_name_to_pg_type(ty_name: &str) -> Option<PgType> {
822    let ty_name_lower = ty_name.to_lowercase();
823    // Handle array types (prefixed with _)
824    if let Some(base_type) = ty_name_lower.strip_prefix('_') {
825        match base_type {
826            "int2" => Some(PgType::INT2_ARRAY),
827            "int4" => Some(PgType::INT4_ARRAY),
828            "int8" => Some(PgType::INT8_ARRAY),
829            "bit" => Some(PgType::BIT_ARRAY),
830            "float4" => Some(PgType::FLOAT4_ARRAY),
831            "float8" => Some(PgType::FLOAT8_ARRAY),
832            "numeric" => Some(PgType::NUMERIC_ARRAY),
833            "bool" => Some(PgType::BOOL_ARRAY),
834            "xml" | "macaddr" | "macaddr8" | "cidr" | "inet" | "int4range" | "int8range"
835            | "numrange" | "tsrange" | "tstzrange" | "daterange" | "citext" => {
836                Some(PgType::VARCHAR_ARRAY)
837            }
838            "varchar" => Some(PgType::VARCHAR_ARRAY),
839            "text" => Some(PgType::TEXT_ARRAY),
840            "bytea" => Some(PgType::BYTEA_ARRAY),
841            "geometry" => Some(PgType::BYTEA_ARRAY), // PostGIS geometry array
842            "date" => Some(PgType::DATE_ARRAY),
843            "time" => Some(PgType::TIME_ARRAY),
844            "timetz" => Some(PgType::TIMETZ_ARRAY),
845            "timestamp" => Some(PgType::TIMESTAMP_ARRAY),
846            "timestamptz" => Some(PgType::TIMESTAMPTZ_ARRAY),
847            "interval" => Some(PgType::INTERVAL_ARRAY),
848            "json" => Some(PgType::JSON_ARRAY),
849            "jsonb" => Some(PgType::JSONB_ARRAY),
850            "uuid" => Some(PgType::UUID_ARRAY),
851            "point" => Some(PgType::POINT_ARRAY),
852            "oid" => Some(PgType::OID_ARRAY),
853            "money" => Some(PgType::MONEY_ARRAY),
854            _ => None,
855        }
856    } else {
857        // Handle non-array types
858        match ty_name_lower.as_str() {
859            "int2" => Some(PgType::INT2),
860            "bit" => Some(PgType::BIT),
861            "int" | "int4" => Some(PgType::INT4),
862            "int8" => Some(PgType::INT8),
863            "float4" => Some(PgType::FLOAT4),
864            "float8" => Some(PgType::FLOAT8),
865            "numeric" => Some(PgType::NUMERIC),
866            "money" => Some(PgType::MONEY),
867            "boolean" | "bool" => Some(PgType::BOOL),
868            "inet" | "xml" | "varchar" | "character varying" | "int4range" | "int8range"
869            | "numrange" | "tsrange" | "tstzrange" | "daterange" | "macaddr" | "macaddr8"
870            | "cidr" => Some(PgType::VARCHAR),
871            "char" | "character" | "bpchar" => Some(PgType::BPCHAR),
872            "citext" | "text" => Some(PgType::TEXT),
873            "bytea" => Some(PgType::BYTEA),
874            "geometry" => Some(PgType::BYTEA), // PostGIS geometry type
875            "date" => Some(PgType::DATE),
876            "time" => Some(PgType::TIME),
877            "timetz" => Some(PgType::TIMETZ),
878            "timestamp" => Some(PgType::TIMESTAMP),
879            "timestamptz" => Some(PgType::TIMESTAMPTZ),
880            "interval" => Some(PgType::INTERVAL),
881            "json" => Some(PgType::JSON),
882            "jsonb" => Some(PgType::JSONB),
883            "uuid" => Some(PgType::UUID),
884            "point" => Some(PgType::POINT),
885            "oid" => Some(PgType::OID),
886            _ => None,
887        }
888    }
889}
890
891pub fn pg_type_to_rw_type(pg_type: &PgType) -> ConnectorResult<DataType> {
892    let data_type = match *pg_type {
893        PgType::BOOL => DataType::Boolean,
894        PgType::BIT => DataType::Boolean,
895        PgType::INT2 => DataType::Int16,
896        PgType::INT4 => DataType::Int32,
897        PgType::INT8 => DataType::Int64,
898        PgType::FLOAT4 => DataType::Float32,
899        PgType::FLOAT8 => DataType::Float64,
900        PgType::NUMERIC | PgType::MONEY => DataType::Decimal,
901        PgType::DATE => DataType::Date,
902        PgType::TIME => DataType::Time,
903        PgType::TIMETZ => DataType::Time,
904        PgType::POINT => DataType::Struct(risingwave_common::types::StructType::new(vec![
905            ("x", DataType::Float32),
906            ("y", DataType::Float32),
907        ])),
908        PgType::TIMESTAMP => DataType::Timestamp,
909        PgType::TIMESTAMPTZ => DataType::Timestamptz,
910        PgType::INTERVAL => DataType::Interval,
911        PgType::VARCHAR | PgType::TEXT | PgType::BPCHAR | PgType::UUID => DataType::Varchar,
912        PgType::BYTEA => DataType::Bytea,
913        PgType::JSON | PgType::JSONB => DataType::Jsonb,
914        // Array types
915        PgType::BOOL_ARRAY => DataType::Boolean.list(),
916        PgType::BIT_ARRAY => DataType::Boolean.list(),
917        PgType::INT2_ARRAY => DataType::Int16.list(),
918        PgType::INT4_ARRAY => DataType::Int32.list(),
919        PgType::INT8_ARRAY => DataType::Int64.list(),
920        PgType::FLOAT4_ARRAY => DataType::Float32.list(),
921        PgType::FLOAT8_ARRAY => DataType::Float64.list(),
922        PgType::NUMERIC_ARRAY => DataType::Decimal.list(),
923        PgType::VARCHAR_ARRAY => DataType::Varchar.list(),
924        PgType::TEXT_ARRAY => DataType::Varchar.list(),
925        PgType::BYTEA_ARRAY => DataType::Bytea.list(),
926        PgType::DATE_ARRAY => DataType::Date.list(),
927        PgType::TIME_ARRAY => DataType::Time.list(),
928        PgType::TIMESTAMP_ARRAY => DataType::Timestamp.list(),
929        PgType::TIMESTAMPTZ_ARRAY => DataType::Timestamptz.list(),
930        PgType::INTERVAL_ARRAY => DataType::Interval.list(),
931        PgType::JSON_ARRAY => DataType::Jsonb.list(),
932        PgType::JSONB_ARRAY => DataType::Jsonb.list(),
933        PgType::UUID_ARRAY => DataType::Varchar.list(),
934        PgType::OID => DataType::Int64,
935        PgType::OID_ARRAY => DataType::Int64.list(),
936        PgType::MONEY_ARRAY => DataType::Decimal.list(),
937        PgType::POINT_ARRAY => {
938            DataType::list(DataType::Struct(risingwave_common::types::StructType::new(
939                vec![("x", DataType::Float32), ("y", DataType::Float32)],
940            )))
941        }
942        _ => {
943            return Err(anyhow::anyhow!("unsupported postgres type: {}", pg_type).into());
944        }
945    };
946    Ok(data_type)
947}
948
949#[cfg(test)]
950mod tests {
951    use std::cmp::Ordering;
952    use std::collections::HashMap;
953
954    use futures::pin_mut;
955    use futures_async_stream::for_await;
956    use maplit::{convert_args, hashmap};
957    use risingwave_common::catalog::{ColumnDesc, ColumnId, Field, Schema};
958    use risingwave_common::row::OwnedRow;
959    use risingwave_common::types::{DataType, ScalarImpl};
960
961    use crate::connector_common::PostgresExternalTable;
962    use crate::source::cdc::external::postgres::{PostgresExternalTableReader, PostgresOffset};
963    use crate::source::cdc::external::{ExternalTableConfig, ExternalTableReader, SchemaTableName};
964
965    #[ignore]
966    #[tokio::test]
967    async fn test_postgres_schema() {
968        let config = ExternalTableConfig {
969            connector: "postgres-cdc".to_owned(),
970            host: "localhost".to_owned(),
971            port: "8432".to_owned(),
972            username: "myuser".to_owned(),
973            password: "123456".to_owned(),
974            database: "mydb".to_owned(),
975            schema: "public".to_owned(),
976            table: "mytest".to_owned(),
977            ssl_mode: Default::default(),
978            ssl_root_cert: None,
979            encrypt: "false".to_owned(),
980        };
981
982        let table = PostgresExternalTable::connect(
983            &config.pg_connection_config().unwrap(),
984            &config.schema,
985            &config.table,
986            false,
987        )
988        .await
989        .unwrap();
990
991        println!("columns: {:?}", table.column_descs());
992        println!("primary keys: {:?}", table.pk_names());
993    }
994
995    #[test]
996    fn test_postgres_offset() {
997        let off1 = PostgresOffset {
998            txid: 4,
999            lsn: 2,
1000            ..Default::default()
1001        };
1002        let off2 = PostgresOffset {
1003            txid: 1,
1004            lsn: 3,
1005            ..Default::default()
1006        };
1007        let off3 = PostgresOffset {
1008            txid: 5,
1009            lsn: 1,
1010            ..Default::default()
1011        };
1012
1013        assert!(off1 < off2);
1014        assert!(off3 < off1);
1015        assert!(off2 > off3);
1016    }
1017
1018    #[test]
1019    fn test_postgres_offset_partial_ord_with_lsn_commit() {
1020        // Test comparison with both lsn_commit and lsn_proc fields
1021        let off1 = PostgresOffset {
1022            txid: 1,
1023            lsn: 100,
1024            lsn_commit: Some(200),
1025            lsn_proc: Some(150),
1026        };
1027        let off2 = PostgresOffset {
1028            txid: 2,
1029            lsn: 300,
1030            lsn_commit: Some(250),
1031            lsn_proc: Some(200),
1032        };
1033
1034        // Should compare using lsn_commit first when both have both fields
1035        assert!(off1 < off2);
1036
1037        // Test with same lsn_commit but different lsn_proc
1038        let off3 = PostgresOffset {
1039            txid: 3,
1040            lsn: 500,
1041            lsn_commit: Some(200), // same as off1
1042            lsn_proc: Some(160),   // higher than off1
1043        };
1044
1045        // Should compare lsn_proc when lsn_commit is equal
1046        assert!(off1 < off3);
1047
1048        // Test with missing lsn_proc - should fall back to lsn comparison
1049        let off4 = PostgresOffset {
1050            txid: 4,
1051            lsn: 400,
1052            lsn_commit: Some(100), // lower than off1's lsn_commit
1053            lsn_proc: None,        // missing lsn_proc
1054        };
1055
1056        // Should fall back to lsn comparison (off1.lsn=100 < off4.lsn=400)
1057        assert!(off1 < off4);
1058
1059        // Test with missing lsn_commit - should fall back to lsn comparison
1060        let off5 = PostgresOffset {
1061            txid: 5,
1062            lsn: 50,             // lower than off1.lsn
1063            lsn_commit: None,    // missing lsn_commit
1064            lsn_proc: Some(300), // higher than off1's lsn_proc
1065        };
1066
1067        // Should fall back to lsn comparison (off5.lsn=50 < off1.lsn=100)
1068        assert!(off5 < off1);
1069
1070        // Additional test cases: equal lsn_commit values with different lsn_proc
1071        let off6 = PostgresOffset {
1072            txid: 6,
1073            lsn: 600,
1074            lsn_commit: Some(500),
1075            lsn_proc: Some(300),
1076        };
1077        let off7 = PostgresOffset {
1078            txid: 7,
1079            lsn: 700,
1080            lsn_commit: Some(500), // same as off6
1081            lsn_proc: Some(400),   // higher than off6
1082        };
1083
1084        // Should compare lsn_proc since lsn_commit is equal
1085        assert!(off6 < off7);
1086
1087        // Test reverse order
1088        let off8 = PostgresOffset {
1089            txid: 8,
1090            lsn: 800,
1091            lsn_commit: Some(500), // same as others
1092            lsn_proc: Some(200),   // lower than off6
1093        };
1094
1095        assert!(off8 < off6);
1096        assert!(off8 < off7);
1097
1098        // Test equal lsn_commit and lsn_proc
1099        let off9 = PostgresOffset {
1100            txid: 9,
1101            lsn: 900,
1102            lsn_commit: Some(500), // same as off6
1103            lsn_proc: Some(300),   // same as off6
1104        };
1105
1106        // Should be equal
1107        assert_eq!(off6.partial_cmp(&off9), Some(Ordering::Equal));
1108    }
1109
1110    #[test]
1111    fn test_debezium_offset_parsing() {
1112        // Test parsing with all required fields present
1113        let debezium_offset_with_fields = r#"{
1114            "sourcePartition": {"server": "RW_CDC_1004"},
1115            "sourceOffset": {
1116                "last_snapshot_record": false,
1117                "lsn": 29973552,
1118                "txId": 1046,
1119                "ts_usec": 1670826189008456,
1120                "snapshot": true,
1121                "lsn_commit": 29973600,
1122                "lsn_proc": 29973580
1123            },
1124            "isHeartbeat": false
1125        }"#;
1126
1127        let offset = PostgresOffset::parse_debezium_offset(debezium_offset_with_fields).unwrap();
1128        assert_eq!(offset.txid, 1046);
1129        assert_eq!(offset.lsn, 29973552);
1130        assert_eq!(offset.lsn_commit, Some(29973600));
1131        assert_eq!(offset.lsn_proc, Some(29973580));
1132
1133        // Test parsing should fail when required fields are missing
1134        let debezium_offset_missing_fields = r#"{
1135            "sourcePartition": {"server": "RW_CDC_1004"},
1136            "sourceOffset": {
1137                "last_snapshot_record": false,
1138                "lsn": 29973552,
1139                "txId": 1046,
1140                "ts_usec": 1670826189008456,
1141                "snapshot": true
1142            },
1143            "isHeartbeat": false
1144        }"#;
1145
1146        let result = PostgresOffset::parse_debezium_offset(debezium_offset_missing_fields);
1147        assert!(result.is_err());
1148        let error_msg = result.unwrap_err().to_string();
1149        assert!(error_msg.contains("invalid postgres lsn_proc"));
1150    }
1151
1152    #[test]
1153    fn test_filter_expression() {
1154        let cols = vec!["v1".to_owned()];
1155        let expr = PostgresExternalTableReader::filter_expression(&cols);
1156        assert_eq!(expr, "(\"v1\") > ($1)");
1157
1158        let cols = vec!["v1".to_owned(), "v2".to_owned()];
1159        let expr = PostgresExternalTableReader::filter_expression(&cols);
1160        assert_eq!(expr, "(\"v1\", \"v2\") > ($1, $2)");
1161
1162        let cols = vec!["v1".to_owned(), "v2".to_owned(), "v3".to_owned()];
1163        let expr = PostgresExternalTableReader::filter_expression(&cols);
1164        assert_eq!(expr, "(\"v1\", \"v2\", \"v3\") > ($1, $2, $3)");
1165    }
1166
1167    #[test]
1168    fn test_split_filter_expression() {
1169        let cols = vec!["v1".to_owned()];
1170        let expr = PostgresExternalTableReader::split_filter_expression(&cols, true, true);
1171        assert_eq!(expr, "1 = 1");
1172
1173        let expr = PostgresExternalTableReader::split_filter_expression(&cols, true, false);
1174        assert_eq!(expr, "(\"v1\") < ($1)");
1175
1176        let expr = PostgresExternalTableReader::split_filter_expression(&cols, false, true);
1177        assert_eq!(expr, "(\"v1\") >= ($1)");
1178
1179        let expr = PostgresExternalTableReader::split_filter_expression(&cols, false, false);
1180        assert_eq!(expr, "(\"v1\") >= ($1) AND (\"v1\") < ($2)");
1181    }
1182
1183    // manual test
1184    #[ignore]
1185    #[tokio::test]
1186    async fn test_pg_table_reader() {
1187        let columns = [
1188            ColumnDesc::named("v1", ColumnId::new(1), DataType::Int32),
1189            ColumnDesc::named("v2", ColumnId::new(2), DataType::Varchar),
1190            ColumnDesc::named("v3", ColumnId::new(3), DataType::Decimal),
1191            ColumnDesc::named("v4", ColumnId::new(4), DataType::Date),
1192        ];
1193        let rw_schema = Schema {
1194            fields: columns.iter().map(Field::from).collect(),
1195        };
1196
1197        let props: HashMap<String, String> = convert_args!(hashmap!(
1198                "hostname" => "localhost",
1199                "port" => "8432",
1200                "username" => "myuser",
1201                "password" => "123456",
1202                "database.name" => "mydb",
1203                "schema.name" => "public",
1204                "table.name" => "t1"));
1205
1206        let config =
1207            serde_json::from_value::<ExternalTableConfig>(serde_json::to_value(props).unwrap())
1208                .unwrap();
1209        let schema_table_name = SchemaTableName {
1210            schema_name: "public".to_owned(),
1211            table_name: "t1".to_owned(),
1212        };
1213        let reader = PostgresExternalTableReader::new(
1214            config,
1215            rw_schema,
1216            vec![0, 1],
1217            schema_table_name.clone(),
1218        )
1219        .await
1220        .unwrap();
1221
1222        let offset = reader.current_cdc_offset().await.unwrap();
1223        println!("CdcOffset: {:?}", offset);
1224
1225        let start_pk = OwnedRow::new(vec![Some(ScalarImpl::from(3)), Some(ScalarImpl::from("c"))]);
1226        let stream = reader.snapshot_read(
1227            schema_table_name,
1228            Some(start_pk),
1229            vec!["v1".to_owned(), "v2".to_owned()],
1230            1000,
1231        );
1232
1233        pin_mut!(stream);
1234        #[for_await]
1235        for row in stream {
1236            println!("OwnedRow: {:?}", row);
1237        }
1238    }
1239}