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