Skip to main content

risingwave_connector/sink/
postgres.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
42/// Maximum number of bind parameters of a single statement. PostgreSQL itself allows 65535, but
43/// the client encodes the parameter count of a `Bind` message as `i16`, so a larger batch fails
44/// before ever reaching the server.
45const MAX_STATEMENT_PARAMS: usize = i16::MAX as usize;
46
47/// Upper bound of `max_batch_rows`: bigger batches only add buffer memory, since statements are
48/// split at [`MAX_STATEMENT_PARAMS`] anyway.
49const 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, // accept "append-only" or "upsert"
83    #[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        // Verify our sink schema is compatible with Postgres
250        {
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            // Check that names and types match, order of columns doesn't matter.
266            {
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            // check that pk matches
307            {
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
370/// Rows accumulated across chunks, written out on flush.
371enum PendingRows {
372    Insert(Vec<PgRow>),
373    /// Keeps only the last operation per key: PostgreSQL rejects an `INSERT .. ON CONFLICT
374    /// DO UPDATE` affecting the same row twice. Unlike `ChangeBuffer`, insert-then-delete
375    /// still emits the delete: at-least-once replay may regroup a committed batch with new
376    /// chunks, so same-key duplicates are legal and the downstream state is unknown.
377    Upsert {
378        /// Rows absorbed since the last flush; drives the flush threshold so log-store
379        /// truncation keeps pace even when dedup keeps the map small.
380        absorbed: usize,
381        /// Last op per key, tagged with its arrival sequence: keys distinct to RisingWave may
382        /// be equal to PostgreSQL (e.g. `char(n)` padding), so execution order is observable.
383        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    /// Takes all pending rows out, split into deletes (rebuilt from the keys) and upserts, each
427    /// in chronological order.
428    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    /// Columns identifying a downstream row: the sink pk, or all columns when there is no pk.
460    /// Never empty.
461    key_indices: Vec<usize>,
462    key_types: Vec<PgType>,
463    schema_types: Vec<PgType>,
464    max_batch_rows: usize,
465    /// Prepared statements keyed by tuple count; only power-of-two sizes, so the caches stay
466    /// small.
467    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        // Rewrite schema types for serialization
492        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        // validate() rejects out-of-range values at DDL time; clamp here so pre-existing sinks
521        // keep running after an upgrade.
522        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    /// Pairs each sub-batch with a prepared statement of matching tuple count. Batches are split
617    /// into power-of-two sizes so that once warm, every size hits the statement cache.
618    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    /// Writes out all pending rows in a single transaction: all deletes first, then upserts in
637    /// chronological order. Deletes are not interleaved by time because every upsert surviving
638    /// dedup is live in RisingWave and must reach the target even if a PG-equal key was deleted
639    /// after it.
640    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        // Statements are prepared before the transaction; after warm-up every size hits the cache.
647        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        // Deletes are awaited before upserts are sent, so that no delete can land after a
655        // PG-equal upsert and erase a live row; costs one extra round trip on mixed flushes.
656        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            // Retry any failed batch row by row: keys distinct to RisingWave but equal to
663            // PostgreSQL fail a multi-row upsert with SQLSTATE 21000 yet apply cleanly one row at
664            // a time; other errors recover on retry or resurface localized to a single row.
665            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    /// Fallback for a failed batched flush: batched deletes first, then one upsert per statement.
689    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        // Polled concurrently to pipeline; statements still execute in wire order, so the
703        // chronologically last of several PG-equal keys wins.
704        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    /// Barriers bound the sink's visibility latency, so they always flush.
733    async fn commit_on_barrier(&mut self) -> Result<bool> {
734        self.flush().await?;
735        Ok(true)
736    }
737}
738
739/// Number of tuples a single statement may carry, bounded by the parameter limit of the protocol.
740fn 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
746/// Splits `rows` into power-of-two-sized chunks no larger than `cap`, largest first.
747fn 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
758/// Largest power of two not exceeding `n`. `n` must be positive.
759fn 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    // Polling all statements concurrently pipelines them into a single round trip; they execute
768    // in first-poll order, i.e. the order of `batches` — an unstated implementation detail of
769    // `futures` and `tokio-postgres`. If it ever breaks, only the winner among PG-equal upsert
770    // keys can change, which is best-effort anyway.
771    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
780/// `($1, $2), ($3, $4), ...`
781fn 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
863/// Quote an identifier for PostgreSQL.
864fn 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        // All columns in the pk: nothing to update on conflict.
988        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        // The range check fails before any connection is attempted.
1044        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        // Absurd `max_batch_rows` values are capped by the parameter limit.
1058        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        // The largest allowed statement stays within the parameter limit.
1064        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        // Delete then insert on the same key collapses into an upsert, insert then delete into a
1104        // delete, and the last of several upserts wins.
1105        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        // Seven rows absorbed, three distinct keys left after dedup.
1127        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        // Re-upserting a key moves it to the end.
1157        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        // Regression guard: key types and values must follow the declared key order, not schema
1183        // order.
1184        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        // Non-insert ops are dropped by append-only sinks.
1228        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}