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::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 pub lsn: u64,
50 #[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
80impl 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 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 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 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 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 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 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 let client = create_pg_client(&config.pg_connection_config()?, None).await?;
246
247 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 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, ¶ms).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, ¶ms).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 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 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, ¶ms).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, ¶ms).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 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, ¶ms).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 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 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 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
837fn 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 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), "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 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), "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 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 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 assert!(off1 < off2);
1061
1062 let off3 = PostgresOffset {
1064 txid: 3,
1065 lsn: 500,
1066 lsn_commit: Some(200), lsn_proc: Some(160), };
1069
1070 assert!(off1 < off3);
1072
1073 let off4 = PostgresOffset {
1075 txid: 4,
1076 lsn: 400,
1077 lsn_commit: Some(100), lsn_proc: None, };
1080
1081 assert!(off1 < off4);
1083
1084 let off5 = PostgresOffset {
1086 txid: 5,
1087 lsn: 50, lsn_commit: None, lsn_proc: Some(300), };
1091
1092 assert!(off5 < off1);
1094
1095 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), lsn_proc: Some(400), };
1108
1109 assert!(off6 < off7);
1111
1112 let off8 = PostgresOffset {
1114 txid: 8,
1115 lsn: 800,
1116 lsn_commit: Some(500), lsn_proc: Some(200), };
1119
1120 assert!(off8 < off6);
1121 assert!(off8 < off7);
1122
1123 let off9 = PostgresOffset {
1125 txid: 9,
1126 lsn: 900,
1127 lsn_commit: Some(500), lsn_proc: Some(300), };
1130
1131 assert_eq!(off6.partial_cmp(&off9), Some(Ordering::Equal));
1133 }
1134
1135 #[test]
1136 fn test_debezium_offset_parsing() {
1137 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 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 #[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}