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