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