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