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