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