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