1use std::collections::{BTreeMap, HashMap, HashSet};
16
17use anyhow::{Context, anyhow};
18use async_trait::async_trait;
19use itertools::Itertools;
20use phf::phf_set;
21use risingwave_common::array::{Op, StreamChunk};
22use risingwave_common::catalog::Schema;
23use risingwave_common::row::{OwnedRow, Row, RowExt};
24use serde::Deserialize;
25use serde_with::{DisplayFromStr, serde_as};
26use simd_json::prelude::ArrayTrait;
27use thiserror_ext::AsReport;
28use tokio_postgres::types::Type as PgType;
29use with_options::WithOptions;
30
31use super::{SINK_TYPE_APPEND_ONLY, SINK_TYPE_OPTION, SINK_TYPE_UPSERT, SinkError};
32use crate::connector_common::{
33 PgConnectionConfig, PostgresExternalTable, SslMode, TcpKeepaliveConfig, create_pg_client,
34};
35use crate::enforce_secret::EnforceSecret;
36use crate::parser::scalar_adapter::{ScalarAdapter, validate_pg_type_to_rw_type};
37use crate::sink::batching_log_sink::{BatchingLogSinker, BatchingSinkWriter};
38use crate::sink::{Result, Sink, SinkParam, SinkWriterParam};
39
40pub const POSTGRES_SINK: &str = "postgres";
41
42const MAX_STATEMENT_PARAMS: usize = i16::MAX as usize;
46
47const MAX_BATCH_ROWS_LIMIT: usize = 65536;
50
51const CHECK_FOREIGN_KEY_SQL: &str = r#"
52 SELECT EXISTS (
53 SELECT 1
54 FROM pg_constraint c
55 JOIN pg_class t ON t.oid = c.conrelid
56 JOIN pg_namespace n ON n.oid = t.relnamespace
57 WHERE n.nspname = $1
58 AND t.relname = $2
59 AND c.contype = 'f'
60 )
61"#;
62
63#[serde_as]
64#[derive(Clone, Debug, Deserialize, WithOptions)]
65pub struct PostgresConfig {
66 pub host: String,
67 #[serde_as(as = "DisplayFromStr")]
68 pub port: u16,
69 pub user: String,
70 pub password: String,
71 pub database: String,
72 pub table: String,
73 #[serde(default = "default_schema")]
74 pub schema: String,
75 #[serde(default = "Default::default")]
76 pub ssl_mode: SslMode,
77 #[serde(rename = "ssl.root.cert")]
78 pub ssl_root_cert: Option<String>,
79 #[serde(default = "default_max_batch_rows")]
80 #[serde_as(as = "DisplayFromStr")]
81 pub max_batch_rows: usize,
82 pub r#type: String, #[serde(default, rename = "tcp.keepalive.enable")]
84 #[serde_as(as = "DisplayFromStr")]
85 pub tcp_keepalive_enable: bool,
86
87 #[serde(flatten)]
88 pub tcp_keepalive: TcpKeepaliveConfig,
89
90 #[serde(flatten)]
91 pub unknown_fields: std::collections::HashMap<String, String>,
92}
93
94crate::impl_sink_unknown_fields!(PostgresConfig);
95
96impl EnforceSecret for PostgresConfig {
97 const ENFORCE_SECRET_PROPERTIES: phf::Set<&'static str> = phf_set! {
98 "password", "ssl.root.cert"
99 };
100}
101
102fn default_max_batch_rows() -> usize {
103 1024
104}
105
106fn default_schema() -> String {
107 "public".to_owned()
108}
109
110fn tcp_keepalive_from_config(config: &PostgresConfig) -> Option<TcpKeepaliveConfig> {
111 config
112 .tcp_keepalive_enable
113 .then(|| config.tcp_keepalive.clone())
114}
115
116async fn ensure_no_foreign_key(config: &PostgresConfig) -> Result<()> {
117 let pg_conn = config.pg_connection_config();
118 let client = create_pg_client(
119 &pg_conn,
120 tcp_keepalive_from_config(config),
121 Some("risingwave-postgres-sink-validator"),
122 )
123 .await?;
124
125 ensure_no_foreign_key_with_client(&client, &config.schema, &config.table).await
126}
127
128async fn ensure_no_foreign_key_with_client(
129 client: &tokio_postgres::Client,
130 schema: &str,
131 table: &str,
132) -> Result<()> {
133 let has_foreign_key = client
134 .query_one(CHECK_FOREIGN_KEY_SQL, &[&schema, &table])
135 .await
136 .context("failed to check foreign key constraints")?
137 .get::<_, bool>(0);
138
139 if has_foreign_key {
140 return Err(SinkError::Config(anyhow!(
141 "Postgres sink does not support target table \"{}\".\"{}\" with foreign key constraints. Please remove foreign key constraints from the target table or choose a different sink table.",
142 schema,
143 table,
144 )));
145 }
146
147 Ok(())
148}
149
150impl PostgresConfig {
151 pub fn from_btreemap(properties: BTreeMap<String, String>) -> Result<Self> {
152 let config =
153 serde_json::from_value::<PostgresConfig>(serde_json::to_value(properties).unwrap())
154 .map_err(|e| SinkError::Config(anyhow!(e)))?;
155 if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {
156 return Err(SinkError::Config(anyhow!(
157 "`{}` must be {}, or {}",
158 SINK_TYPE_OPTION,
159 SINK_TYPE_APPEND_ONLY,
160 SINK_TYPE_UPSERT
161 )));
162 }
163 Ok(config)
164 }
165
166 pub fn pg_connection_config(&self) -> PgConnectionConfig {
167 PgConnectionConfig {
168 host: self.host.clone(),
169 port: self.port,
170 user: self.user.clone(),
171 password: self.password.clone(),
172 database: self.database.clone(),
173 ssl_mode: self.ssl_mode.clone(),
174 ssl_root_cert: self.ssl_root_cert.clone(),
175 }
176 }
177}
178
179#[derive(Debug)]
180pub struct PostgresSink {
181 pub config: PostgresConfig,
182 schema: Schema,
183 pk_indices: Vec<usize>,
184 is_append_only: bool,
185}
186
187impl PostgresSink {
188 pub fn new(
189 config: PostgresConfig,
190 schema: Schema,
191 pk_indices: Vec<usize>,
192 is_append_only: bool,
193 ) -> Result<Self> {
194 Ok(Self {
195 config,
196 schema,
197 pk_indices,
198 is_append_only,
199 })
200 }
201}
202
203impl EnforceSecret for PostgresSink {
204 fn enforce_secret<'a>(
205 prop_iter: impl Iterator<Item = &'a str>,
206 ) -> crate::error::ConnectorResult<()> {
207 for prop in prop_iter {
208 PostgresConfig::enforce_one(prop)?;
209 }
210 Ok(())
211 }
212}
213
214impl TryFrom<SinkParam> for PostgresSink {
215 type Error = SinkError;
216
217 fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
218 let schema = param.schema();
219 let pk_indices = param.downstream_pk_or_empty();
220 let config = PostgresConfig::from_btreemap(param.properties)?;
221 PostgresSink::new(config, schema, pk_indices, param.sink_type.is_append_only())
222 }
223}
224
225impl Sink for PostgresSink {
226 type LogSinker = BatchingLogSinker<PostgresSinkWriter>;
227
228 const SINK_NAME: &'static str = POSTGRES_SINK;
229
230 crate::impl_validate_sink_unknown_fields!();
231
232 async fn validate(&self) -> Result<()> {
233 if !(1..=MAX_BATCH_ROWS_LIMIT).contains(&self.config.max_batch_rows) {
234 return Err(SinkError::Config(anyhow!(
235 "`max_batch_rows` must be between 1 and {}, got {}",
236 MAX_BATCH_ROWS_LIMIT,
237 self.config.max_batch_rows
238 )));
239 }
240
241 if !self.is_append_only && self.pk_indices.is_empty() {
242 return Err(SinkError::Config(anyhow!(
243 "Primary key not defined for upsert Postgres sink (please define in `primary_key` field)"
244 )));
245 }
246
247 ensure_no_foreign_key(&self.config).await?;
248
249 {
251 let pg_conn = self.config.pg_connection_config();
252 let pg_table = PostgresExternalTable::connect(
253 &pg_conn,
254 &self.config.schema,
255 &self.config.table,
256 self.is_append_only,
257 None,
258 )
259 .await
260 .context(format!(
261 "failed to connect to database: {}, schema: {}, table: {}",
262 self.config.database, self.config.schema, self.config.table
263 ))?;
264
265 {
267 let pg_columns = pg_table.column_descs();
268 let sink_columns = self.schema.fields();
269 if pg_columns.len() < sink_columns.len() {
270 return Err(SinkError::Config(anyhow!(
271 "Column count mismatch: Postgres table has {} columns, but sink schema has {} columns, sink should have less or equal columns to the Postgres table",
272 pg_columns.len(),
273 sink_columns.len()
274 )));
275 }
276
277 let pg_columns_lookup = pg_columns
278 .iter()
279 .map(|c| (c.name.clone(), c.data_type.clone()))
280 .collect::<BTreeMap<_, _>>();
281 for sink_column in sink_columns {
282 let pg_column = pg_columns_lookup.get(&sink_column.name);
283 match pg_column {
284 None => {
285 return Err(SinkError::Config(anyhow!(
286 "Column `{}` not found in Postgres table `{}`",
287 sink_column.name,
288 self.config.table
289 )));
290 }
291 Some(pg_column) => {
292 if !validate_pg_type_to_rw_type(pg_column, &sink_column.data_type()) {
293 return Err(SinkError::Config(anyhow!(
294 "Column `{}` in Postgres table `{}` has type `{}`, but sink schema defines it as type `{}`",
295 sink_column.name,
296 self.config.table,
297 pg_column,
298 sink_column.data_type()
299 )));
300 }
301 }
302 }
303 }
304 }
305
306 {
308 let pg_pk_names = pg_table.pk_names();
309 let sink_pk_names = self
310 .pk_indices
311 .iter()
312 .map(|i| &self.schema.fields()[*i].name)
313 .collect::<HashSet<_>>();
314 if pg_pk_names.len() != sink_pk_names.len() {
315 return Err(SinkError::Config(anyhow!(
316 "Primary key mismatch: Postgres table has primary key on columns {:?}, but sink schema defines primary key on columns {:?}",
317 pg_pk_names,
318 sink_pk_names
319 )));
320 }
321 for name in pg_pk_names {
322 if !sink_pk_names.contains(name) {
323 return Err(SinkError::Config(anyhow!(
324 "Primary key mismatch: Postgres table has primary key on column `{}`, but sink schema does not define it as a primary key",
325 name
326 )));
327 }
328 }
329 }
330 }
331
332 Ok(())
333 }
334
335 async fn new_log_sinker(&self, writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
336 let writer = PostgresSinkWriter::new(
337 self.config.clone(),
338 self.schema.clone(),
339 self.pk_indices.clone(),
340 self.is_append_only,
341 &writer_param,
342 )
343 .await?;
344 Ok(BatchingLogSinker::new(writer))
345 }
346}
347
348#[derive(Clone, Copy)]
349enum StatementKind {
350 Insert,
351 Upsert,
352 Delete,
353}
354
355impl StatementKind {
356 fn as_str(self) -> &'static str {
357 match self {
358 StatementKind::Insert => "insert",
359 StatementKind::Upsert => "upsert",
360 StatementKind::Delete => "delete",
361 }
362 }
363}
364
365enum PendingOp {
366 Upsert(PgRow),
367 Delete,
368}
369
370enum PendingRows {
372 Insert(Vec<PgRow>),
373 Upsert {
378 absorbed: usize,
381 rows: HashMap<OwnedRow, (usize, PendingOp)>,
384 },
385}
386
387impl PendingRows {
388 fn absorbed(&self) -> usize {
389 match self {
390 PendingRows::Insert(rows) => rows.len(),
391 PendingRows::Upsert { absorbed, .. } => *absorbed,
392 }
393 }
394
395 fn absorb(&mut self, chunk: &StreamChunk, key_indices: &[usize], schema_types: &[PgType]) {
396 match self {
397 PendingRows::Insert(rows) => {
398 rows.reserve(chunk.cardinality());
399 for (op, row) in chunk.rows() {
400 if op == Op::Insert {
401 rows.push(convert_row_to_pg_row(row, schema_types));
402 } else {
403 tracing::error!(
404 "row ignored, append-only sink should not receive update insert, update delete and delete operations"
405 );
406 }
407 }
408 }
409 PendingRows::Upsert { absorbed, rows } => {
410 rows.reserve(chunk.cardinality());
411 for (op, row) in chunk.rows() {
412 let key = row.project(key_indices).into_owned_row();
413 let pending_op = match op {
414 Op::Insert | Op::UpdateInsert => {
415 PendingOp::Upsert(convert_row_to_pg_row(row, schema_types))
416 }
417 Op::Delete | Op::UpdateDelete => PendingOp::Delete,
418 };
419 rows.insert(key, (*absorbed, pending_op));
420 *absorbed += 1;
421 }
422 }
423 }
424 }
425
426 fn take(&mut self, key_types: &[PgType]) -> (Vec<PgRow>, Vec<PgRow>) {
429 match self {
430 PendingRows::Insert(rows) => (vec![], std::mem::take(rows)),
431 PendingRows::Upsert { absorbed, rows } => {
432 *absorbed = 0;
433 let mut deletes = Vec::with_capacity(rows.len());
434 let mut upserts = Vec::with_capacity(rows.len());
435 for (key, (seq, op)) in rows.drain() {
436 match op {
437 PendingOp::Upsert(row) => upserts.push((seq, row)),
438 PendingOp::Delete => {
439 deletes.push((seq, convert_row_to_pg_row(&key, key_types)))
440 }
441 }
442 }
443 deletes.sort_unstable_by_key(|(seq, _)| *seq);
444 upserts.sort_unstable_by_key(|(seq, _)| *seq);
445 (
446 deletes.into_iter().map(|(_, row)| row).collect(),
447 upserts.into_iter().map(|(_, row)| row).collect(),
448 )
449 }
450 }
451 }
452}
453
454pub struct PostgresSinkWriter {
455 client: tokio_postgres::Client,
456 schema: Schema,
457 schema_name: String,
458 table_name: String,
459 key_indices: Vec<usize>,
462 key_types: Vec<PgType>,
463 schema_types: Vec<PgType>,
464 max_batch_rows: usize,
465 write_statements: HashMap<usize, tokio_postgres::Statement>,
468 delete_statements: HashMap<usize, tokio_postgres::Statement>,
469 pending: PendingRows,
470}
471
472impl PostgresSinkWriter {
473 async fn new(
474 config: PostgresConfig,
475 schema: Schema,
476 pk_indices: Vec<usize>,
477 is_append_only: bool,
478 writer_param: &SinkWriterParam,
479 ) -> Result<Self> {
480 let tcp_keepalive = tcp_keepalive_from_config(&config);
481
482 let pg_conn = config.pg_connection_config();
483 let application_name = format!(
484 "risingwave-postgres-sink-{}-{}",
485 writer_param.sink_id, writer_param.actor_id
486 );
487 let client = create_pg_client(&pg_conn, tcp_keepalive, Some(&application_name)).await?;
488
489 ensure_no_foreign_key_with_client(&client, &config.schema, &config.table).await?;
490
491 let schema_types = {
493 let name_to_type = PostgresExternalTable::type_mapping(
494 &pg_conn,
495 &config.schema,
496 &config.table,
497 is_append_only,
498 )
499 .await?;
500 let mut schema_types = Vec::with_capacity(schema.fields.len());
501 for field in &schema.fields {
502 let actual_data_type = name_to_type.get(&field.name).cloned().ok_or_else(|| {
503 SinkError::Config(anyhow!("Column `{}` not found in sink schema", field.name))
504 })?;
505 schema_types.push(actual_data_type);
506 }
507 schema_types
508 };
509
510 let key_indices = if pk_indices.is_empty() {
511 (0..schema.len()).collect_vec()
512 } else {
513 pk_indices
514 };
515 let key_types = key_indices
516 .iter()
517 .map(|i| schema_types[*i].clone())
518 .collect_vec();
519
520 let max_batch_rows = config.max_batch_rows.clamp(1, MAX_BATCH_ROWS_LIMIT);
523 if max_batch_rows != config.max_batch_rows {
524 tracing::warn!(
525 configured = config.max_batch_rows,
526 effective = max_batch_rows,
527 "max_batch_rows out of range, clamped"
528 );
529 }
530
531 let pending = if is_append_only {
532 PendingRows::Insert(Vec::new())
533 } else {
534 PendingRows::Upsert {
535 absorbed: 0,
536 rows: HashMap::new(),
537 }
538 };
539
540 let writer = Self {
541 client,
542 schema,
543 schema_name: config.schema,
544 table_name: config.table,
545 key_indices,
546 key_types,
547 schema_types,
548 max_batch_rows,
549 write_statements: HashMap::new(),
550 delete_statements: HashMap::new(),
551 pending,
552 };
553 Ok(writer)
554 }
555
556 fn write_kind(&self) -> StatementKind {
557 match &self.pending {
558 PendingRows::Insert(_) => StatementKind::Insert,
559 PendingRows::Upsert { .. } => StatementKind::Upsert,
560 }
561 }
562
563 fn create_sql(&self, kind: StatementKind, n_tuples: usize) -> String {
564 match kind {
565 StatementKind::Insert => {
566 create_insert_sql(&self.schema, &self.schema_name, &self.table_name, n_tuples)
567 }
568 StatementKind::Upsert => create_upsert_sql(
569 &self.schema,
570 &self.schema_name,
571 &self.table_name,
572 &self.key_indices,
573 n_tuples,
574 ),
575 StatementKind::Delete => create_delete_sql(
576 &self.schema,
577 &self.schema_name,
578 &self.table_name,
579 &self.key_indices,
580 n_tuples,
581 ),
582 }
583 }
584
585 fn statement_cache(
586 &mut self,
587 kind: StatementKind,
588 ) -> &mut HashMap<usize, tokio_postgres::Statement> {
589 match kind {
590 StatementKind::Insert | StatementKind::Upsert => &mut self.write_statements,
591 StatementKind::Delete => &mut self.delete_statements,
592 }
593 }
594
595 async fn cached_statement(
596 &mut self,
597 kind: StatementKind,
598 n_tuples: usize,
599 ) -> Result<tokio_postgres::Statement> {
600 if let Some(statement) = self.statement_cache(kind).get(&n_tuples) {
601 return Ok(statement.clone());
602 }
603 let sql = self.create_sql(kind, n_tuples);
604 let statement = self.client.prepare(&sql).await.with_context(|| {
605 format!(
606 "failed to prepare {} statement for {} rows",
607 kind.as_str(),
608 n_tuples
609 )
610 })?;
611 self.statement_cache(kind)
612 .insert(n_tuples, statement.clone());
613 Ok(statement)
614 }
615
616 async fn prepare_batches<'a>(
619 &mut self,
620 kind: StatementKind,
621 rows: &'a [PgRow],
622 ) -> Result<Vec<(tokio_postgres::Statement, &'a [PgRow])>> {
623 let params_per_tuple = match kind {
624 StatementKind::Insert | StatementKind::Upsert => self.schema.len(),
625 StatementKind::Delete => self.key_indices.len(),
626 };
627 let cap = tuples_per_statement(self.max_batch_rows, params_per_tuple);
628 let mut batches = Vec::new();
629 for tuples in split_power_of_two(rows, cap) {
630 let statement = self.cached_statement(kind, tuples.len()).await?;
631 batches.push((statement, tuples));
632 }
633 Ok(batches)
634 }
635
636 async fn flush(&mut self) -> Result<()> {
641 let (deletes, upserts) = self.pending.take(&self.key_types);
642 if deletes.is_empty() && upserts.is_empty() {
643 return Ok(());
644 }
645
646 let write_kind = self.write_kind();
648 let delete_batches = self
649 .prepare_batches(StatementKind::Delete, &deletes)
650 .await?;
651 let upsert_batches = self.prepare_batches(write_kind, &upserts).await?;
652
653 let transaction = self.client.transaction().await?;
654 let result = async {
657 execute_batches(&transaction, &delete_batches).await?;
658 execute_batches(&transaction, &upsert_batches).await
659 }
660 .await;
661 if let Err(e) = result {
662 let context = || {
666 format!(
667 "failed to execute batched {} statements ({} delete rows, {} write rows)",
668 write_kind.as_str(),
669 deletes.len(),
670 upserts.len()
671 )
672 };
673 if let Err(rollback_err) = transaction.rollback().await {
674 tracing::warn!(
675 error = %rollback_err.as_report(),
676 "failed to roll back failed batch"
677 );
678 return Err(anyhow::Error::new(e).context(context()).into());
679 }
680 tracing::warn!(error = %e.as_report(), "{}, retrying row by row", context());
681 return self.flush_row_by_row(&deletes, &upserts).await;
682 }
683 transaction.commit().await?;
684
685 Ok(())
686 }
687
688 async fn flush_row_by_row(&mut self, deletes: &[PgRow], upserts: &[PgRow]) -> Result<()> {
690 let delete_batches = self.prepare_batches(StatementKind::Delete, deletes).await?;
691 let statement = self.cached_statement(self.write_kind(), 1).await?;
692
693 let transaction = self.client.transaction().await?;
694 execute_batches(&transaction, &delete_batches)
695 .await
696 .with_context(|| {
697 format!(
698 "failed to execute delete statements on {} rows",
699 deletes.len()
700 )
701 })?;
702 let executions = upserts
705 .iter()
706 .map(|row| transaction.execute_raw(&statement, row));
707 futures::future::try_join_all(executions)
708 .await
709 .context("failed to execute single-row write statements")?;
710 transaction.commit().await?;
711 Ok(())
712 }
713}
714
715#[async_trait]
716impl BatchingSinkWriter for PostgresSinkWriter {
717 async fn write_batch(&mut self, chunk: StreamChunk) -> Result<()> {
718 self.pending
719 .absorb(&chunk, &self.key_indices, &self.schema_types);
720 Ok(())
721 }
722
723 async fn try_commit(&mut self) -> Result<bool> {
724 if self.pending.absorbed() >= self.max_batch_rows {
725 self.flush().await?;
726 Ok(true)
727 } else {
728 Ok(false)
729 }
730 }
731
732 async fn commit_on_barrier(&mut self) -> Result<bool> {
734 self.flush().await?;
735 Ok(true)
736 }
737}
738
739fn tuples_per_statement(max_batch_rows: usize, params_per_tuple: usize) -> usize {
741 max_batch_rows
742 .min(MAX_STATEMENT_PARAMS / params_per_tuple.max(1))
743 .max(1)
744}
745
746fn split_power_of_two<T>(rows: &[T], cap: usize) -> Vec<&[T]> {
748 let mut chunks = Vec::new();
749 let mut rest = rows;
750 while !rest.is_empty() {
751 let (chunk, tail) = rest.split_at(prev_power_of_two(rest.len().min(cap)));
752 chunks.push(chunk);
753 rest = tail;
754 }
755 chunks
756}
757
758fn prev_power_of_two(n: usize) -> usize {
760 1 << (usize::BITS - 1 - n.leading_zeros())
761}
762
763async fn execute_batches(
764 transaction: &tokio_postgres::Transaction<'_>,
765 batches: &[(tokio_postgres::Statement, &[PgRow])],
766) -> std::result::Result<(), tokio_postgres::Error> {
767 let executions = batches.iter().map(|(statement, tuples)| {
772 let mut params = Vec::with_capacity(tuples.len() * tuples.first().map_or(0, |t| t.len()));
773 params.extend(tuples.iter().flatten());
774 transaction.execute_raw(statement, params)
775 });
776 futures::future::try_join_all(executions).await?;
777 Ok(())
778}
779
780fn create_parameter_tuples(params_per_tuple: usize, n_tuples: usize) -> String {
782 (0..n_tuples)
783 .map(|tuple| {
784 let parameters = (0..params_per_tuple)
785 .map(|i| format!("${}", tuple * params_per_tuple + i + 1))
786 .join(", ");
787 format!("({parameters})")
788 })
789 .join(", ")
790}
791
792fn create_insert_sql(
793 schema: &Schema,
794 schema_name: &str,
795 table_name: &str,
796 n_tuples: usize,
797) -> String {
798 let normalized_table_name = format!(
799 "{}.{}",
800 quote_identifier(schema_name),
801 quote_identifier(table_name)
802 );
803 let columns: String = schema
804 .fields()
805 .iter()
806 .map(|field| quote_identifier(&field.name))
807 .join(", ");
808 let values = create_parameter_tuples(schema.len(), n_tuples);
809 format!("INSERT INTO {normalized_table_name} ({columns}) VALUES {values}")
810}
811
812fn create_delete_sql(
813 schema: &Schema,
814 schema_name: &str,
815 table_name: &str,
816 key_indices: &[usize],
817 n_tuples: usize,
818) -> String {
819 let normalized_table_name = format!(
820 "{}.{}",
821 quote_identifier(schema_name),
822 quote_identifier(table_name)
823 );
824 let pk = {
825 let pk_symbols = key_indices
826 .iter()
827 .map(|key_index| quote_identifier(&schema.fields()[*key_index].name))
828 .join(", ");
829 format!("({})", pk_symbols)
830 };
831 let parameters = create_parameter_tuples(key_indices.len(), n_tuples);
832 format!("DELETE FROM {normalized_table_name} WHERE {pk} in ({parameters})")
833}
834
835fn create_upsert_sql(
836 schema: &Schema,
837 schema_name: &str,
838 table_name: &str,
839 key_indices: &[usize],
840 n_tuples: usize,
841) -> String {
842 let insert_sql = create_insert_sql(schema, schema_name, table_name, n_tuples);
843 let pk_columns = key_indices
844 .iter()
845 .map(|key_index| quote_identifier(&schema.fields()[*key_index].name))
846 .collect_vec()
847 .join(", ");
848 let update_parameters: String = (0..schema.len())
849 .filter(|i| !key_indices.contains(i))
850 .map(|i| {
851 let column = quote_identifier(&schema.fields()[i].name);
852 format!("{column} = EXCLUDED.{column}")
853 })
854 .collect_vec()
855 .join(", ");
856 if update_parameters.is_empty() {
857 format!("{insert_sql} on conflict ({pk_columns}) do nothing")
858 } else {
859 format!("{insert_sql} on conflict ({pk_columns}) do update set {update_parameters}")
860 }
861}
862
863fn quote_identifier(identifier: &str) -> String {
865 format!("\"{}\"", identifier.replace("\"", "\"\""))
866}
867
868type PgDatum = Option<ScalarAdapter>;
869type PgRow = Vec<PgDatum>;
870
871fn convert_row_to_pg_row(row: impl Row, schema_types: &[PgType]) -> PgRow {
872 let mut buffer = Vec::with_capacity(row.len());
873 for (i, datum_ref) in row.iter().enumerate() {
874 let pg_datum = datum_ref.map(|s| {
875 match ScalarAdapter::from_scalar(s, &schema_types[i]) {
876 Ok(scalar) => Some(scalar),
877 Err(e) => {
878 tracing::error!(error=%e.as_report(), scalar=?s, "Failed to convert scalar to pg value");
879 None
880 }
881 }
882 });
883 buffer.push(pg_datum.flatten());
884 }
885 buffer
886}
887
888#[cfg(test)]
889mod tests {
890 use std::fmt::Display;
891
892 use expect_test::{Expect, expect};
893 use risingwave_common::catalog::Field;
894 use risingwave_common::test_prelude::StreamChunkTestExt;
895 use risingwave_common::types::DataType;
896
897 use super::*;
898
899 fn check(actual: impl Display, expect: Expect) {
900 let actual = actual.to_string();
901 expect.assert_eq(&actual);
902 }
903
904 fn test_schema() -> Schema {
905 Schema::new(vec![
906 Field {
907 data_type: DataType::Int32,
908 name: "a".to_owned(),
909 },
910 Field {
911 data_type: DataType::Int32,
912 name: "b".to_owned(),
913 },
914 ])
915 }
916
917 #[test]
918 fn test_create_insert_sql() {
919 let schema = test_schema();
920 let schema_name = "test_schema";
921 let table_name = "test_table";
922 let sql = create_insert_sql(&schema, schema_name, table_name, 3);
923 check(
924 sql,
925 expect![[
926 r#"INSERT INTO "test_schema"."test_table" ("a", "b") VALUES ($1, $2), ($3, $4), ($5, $6)"#
927 ]],
928 );
929 }
930
931 #[test]
932 fn test_create_delete_sql() {
933 let schema = test_schema();
934 let schema_name = "test_schema";
935 let table_name = "test_table";
936 let sql = create_delete_sql(&schema, schema_name, table_name, &[1], 3);
937 check(
938 sql,
939 expect![[
940 r#"DELETE FROM "test_schema"."test_table" WHERE ("b") in (($1), ($2), ($3))"#
941 ]],
942 );
943 let sql = create_delete_sql(&schema, schema_name, table_name, &[0, 1], 3);
944 check(
945 sql,
946 expect![[
947 r#"DELETE FROM "test_schema"."test_table" WHERE ("a", "b") in (($1, $2), ($3, $4), ($5, $6))"#
948 ]],
949 );
950 }
951
952 #[test]
953 fn test_create_upsert_sql() {
954 let schema = test_schema();
955 let schema_name = "test_schema";
956 let table_name = "test_table";
957 let sql = create_upsert_sql(&schema, schema_name, table_name, &[1], 3);
958 check(
959 sql,
960 expect![[
961 r#"INSERT INTO "test_schema"."test_table" ("a", "b") VALUES ($1, $2), ($3, $4), ($5, $6) on conflict ("b") do update set "a" = EXCLUDED."a""#
962 ]],
963 );
964
965 let composite = Schema::new(vec![
966 Field {
967 data_type: DataType::Int32,
968 name: "user_id".to_owned(),
969 },
970 Field {
971 data_type: DataType::Int32,
972 name: "client_id".to_owned(),
973 },
974 Field {
975 data_type: DataType::Int32,
976 name: "value".to_owned(),
977 },
978 ]);
979 let sql = create_upsert_sql(&composite, schema_name, table_name, &[0, 1], 2);
980 check(
981 sql,
982 expect![[
983 r#"INSERT INTO "test_schema"."test_table" ("user_id", "client_id", "value") VALUES ($1, $2, $3), ($4, $5, $6) on conflict ("user_id", "client_id") do update set "value" = EXCLUDED."value""#
984 ]],
985 );
986
987 let all_pk = Schema::new(vec![
989 Field {
990 data_type: DataType::Int32,
991 name: "user_id".to_owned(),
992 },
993 Field {
994 data_type: DataType::Int32,
995 name: "client_id".to_owned(),
996 },
997 ]);
998 let sql = create_upsert_sql(&all_pk, schema_name, table_name, &[0, 1], 2);
999 check(
1000 sql,
1001 expect![[
1002 r#"INSERT INTO "test_schema"."test_table" ("user_id", "client_id") VALUES ($1, $2), ($3, $4) on conflict ("user_id", "client_id") do nothing"#
1003 ]],
1004 );
1005 }
1006
1007 #[test]
1008 fn test_split_power_of_two() {
1009 let check_split = |len: usize, cap: usize, expect: &[usize]| {
1010 let rows = vec![(); len];
1011 let sizes = split_power_of_two(&rows, cap)
1012 .iter()
1013 .map(|c| c.len())
1014 .collect_vec();
1015 assert_eq!(sizes, expect, "len={len} cap={cap}");
1016 };
1017 check_split(1024, 1024, &[1024]);
1018 check_split(922, 1024, &[512, 256, 128, 16, 8, 2]);
1019 check_split(37, 1024, &[32, 4, 1]);
1020 check_split(1, 1, &[1]);
1021 check_split(1000, 327, &[256, 256, 256, 128, 64, 32, 8]);
1022
1023 assert_eq!(prev_power_of_two(1), 1);
1024 assert_eq!(prev_power_of_two(3), 2);
1025 assert_eq!(prev_power_of_two(1023), 512);
1026 assert_eq!(prev_power_of_two(1024), 1024);
1027 }
1028
1029 #[tokio::test]
1030 async fn test_validate_max_batch_rows_range() {
1031 let properties = BTreeMap::from(
1032 [
1033 ("host", "localhost"),
1034 ("port", "5432"),
1035 ("user", "u"),
1036 ("password", "p"),
1037 ("database", "d"),
1038 ("table", "t"),
1039 ("type", "upsert"),
1040 ]
1041 .map(|(k, v)| (k.to_owned(), v.to_owned())),
1042 );
1043 for bad in ["0", "65537"] {
1045 let mut properties = properties.clone();
1046 properties.insert("max_batch_rows".to_owned(), bad.to_owned());
1047 let config = PostgresConfig::from_btreemap(properties).unwrap();
1048 let sink = PostgresSink::new(config, test_schema(), vec![0], false).unwrap();
1049 let err = sink.validate().await.unwrap_err();
1050 assert!(err.to_string().contains("max_batch_rows"), "{}", err);
1051 }
1052 }
1053
1054 #[test]
1055 fn test_tuples_per_statement() {
1056 assert_eq!(tuples_per_statement(1024, 2), 1024);
1057 assert_eq!(tuples_per_statement(usize::MAX, 1), 32767);
1059 assert_eq!(tuples_per_statement(usize::MAX, 2), 16383);
1060 assert_eq!(tuples_per_statement(1_000_000, 3), 10922);
1061 assert_eq!(tuples_per_statement(0, 2), 1);
1062
1063 let schema = test_schema();
1065 let n_tuples = tuples_per_statement(usize::MAX, schema.len());
1066 assert_eq!(n_tuples * schema.len(), 32766);
1067 assert!(n_tuples * schema.len() <= MAX_STATEMENT_PARAMS);
1068 let sql = create_insert_sql(&schema, "test_schema", "test_table", n_tuples);
1069 assert!(sql.ends_with("($32765, $32766)"));
1070 }
1071
1072 fn render_pending(pending: &PendingRows) -> String {
1073 match pending {
1074 PendingRows::Insert(rows) => rows
1075 .iter()
1076 .map(|row| format!("insert {:?}", row))
1077 .join("\n"),
1078 PendingRows::Upsert { rows, .. } => rows
1079 .iter()
1080 .map(|(key, (seq, op))| match op {
1081 PendingOp::Upsert(row) => format!("{:?} => #{} upsert {:?}", key, seq, row),
1082 PendingOp::Delete => format!("{:?} => #{} delete", key, seq),
1083 })
1084 .sorted()
1085 .join("\n"),
1086 }
1087 }
1088
1089 fn first_columns(rows: &[PgRow]) -> Vec<String> {
1090 rows.iter().map(|row| format!("{:?}", row[0])).collect()
1091 }
1092
1093 #[test]
1094 fn test_pending_upsert_keep_last() {
1095 let types = vec![PgType::INT4, PgType::INT4];
1096 let key_indices = [0];
1097 let key_types = vec![PgType::INT4];
1098 let mut pending = PendingRows::Upsert {
1099 absorbed: 0,
1100 rows: HashMap::new(),
1101 };
1102
1103 pending.absorb(
1106 &StreamChunk::from_pretty(
1107 " i i
1108 - 1 10
1109 + 1 11
1110 + 2 20
1111 - 2 20
1112 + 3 30",
1113 ),
1114 &key_indices,
1115 &types,
1116 );
1117 pending.absorb(
1118 &StreamChunk::from_pretty(
1119 " i i
1120 U- 3 30
1121 U+ 3 31",
1122 ),
1123 &key_indices,
1124 &types,
1125 );
1126 assert_eq!(pending.absorbed(), 7);
1128 check(
1129 render_pending(&pending),
1130 expect![[r#"
1131 OwnedRow([Some(Int32(1))]) => #1 upsert [Some(Builtin(Int32(1))), Some(Builtin(Int32(11)))]
1132 OwnedRow([Some(Int32(2))]) => #3 delete
1133 OwnedRow([Some(Int32(3))]) => #6 upsert [Some(Builtin(Int32(3))), Some(Builtin(Int32(31)))]"#]],
1134 );
1135
1136 let (deletes, upserts) = pending.take(&key_types);
1137 assert_eq!(deletes.len(), 1);
1138 assert_eq!(upserts.len(), 2);
1139 assert_eq!(pending.absorbed(), 0);
1140 }
1141
1142 #[test]
1143 fn test_pending_take_chronological() {
1144 let types = vec![PgType::INT4, PgType::INT4];
1145 let mut pending = PendingRows::Upsert {
1146 absorbed: 0,
1147 rows: HashMap::new(),
1148 };
1149
1150 let inserts = (1..=64).map(|k| format!("+ {k} {k}")).join("\n");
1151 pending.absorb(
1152 &StreamChunk::from_pretty(&format!(" i i\n{inserts}")),
1153 &[0],
1154 &types,
1155 );
1156 pending.absorb(
1158 &StreamChunk::from_pretty(
1159 " i i
1160 - 3 3
1161 + 1 100
1162 - 2 2",
1163 ),
1164 &[0],
1165 &types,
1166 );
1167
1168 let (deletes, upserts) = pending.take(&[PgType::INT4]);
1169 let expected_upserts = (4..=64)
1170 .chain([1])
1171 .map(|k| format!("Some(Builtin(Int32({k})))"))
1172 .collect_vec();
1173 assert_eq!(first_columns(&upserts), expected_upserts);
1174 assert_eq!(
1175 first_columns(&deletes),
1176 ["Some(Builtin(Int32(3)))", "Some(Builtin(Int32(2)))"]
1177 );
1178 }
1179
1180 #[test]
1181 fn test_permuted_key_indices() {
1182 let schema = Schema::new(vec![
1185 Field {
1186 data_type: DataType::Int32,
1187 name: "a".to_owned(),
1188 },
1189 Field {
1190 data_type: DataType::Varchar,
1191 name: "b".to_owned(),
1192 },
1193 ]);
1194 let sql = create_delete_sql(&schema, "test_schema", "test_table", &[1, 0], 2);
1195 check(
1196 sql,
1197 expect![[
1198 r#"DELETE FROM "test_schema"."test_table" WHERE ("b", "a") in (($1, $2), ($3, $4))"#
1199 ]],
1200 );
1201
1202 let schema_types = vec![PgType::INT4, PgType::TEXT];
1203 let mut pending = PendingRows::Upsert {
1204 absorbed: 0,
1205 rows: HashMap::new(),
1206 };
1207 pending.absorb(
1208 &StreamChunk::from_pretty(
1209 " i T
1210 - 1 x",
1211 ),
1212 &[1, 0],
1213 &schema_types,
1214 );
1215 let (deletes, upserts) = pending.take(&[PgType::TEXT, PgType::INT4]);
1216 assert!(upserts.is_empty());
1217 check(
1218 format!("{:?}", deletes),
1219 expect![[r#"[[Some(Builtin(Utf8("x"))), Some(Builtin(Int32(1)))]]"#]],
1220 );
1221 }
1222
1223 #[test]
1224 fn test_pending_insert_no_dedup() {
1225 let types = vec![PgType::INT4, PgType::INT4];
1226 let mut pending = PendingRows::Insert(Vec::new());
1227 pending.absorb(
1229 &StreamChunk::from_pretty(
1230 " i i
1231 + 1 10
1232 + 1 10
1233 - 1 10",
1234 ),
1235 &[0],
1236 &types,
1237 );
1238 check(
1239 render_pending(&pending),
1240 expect![[r#"
1241 insert [Some(Builtin(Int32(1))), Some(Builtin(Int32(10)))]
1242 insert [Some(Builtin(Int32(1))), Some(Builtin(Int32(10)))]"#]],
1243 );
1244 let (deletes, upserts) = pending.take(&[PgType::INT4]);
1245 assert!(deletes.is_empty());
1246 assert_eq!(upserts.len(), 2);
1247 }
1248}