Skip to main content

oxisql_postgres/
pipeline.rs

1//! [`PgPipeline`] — batched multi-query execution for PostgreSQL.
2//!
3//! # Background
4//!
5//! PostgreSQL's wire protocol supports pipelining: multiple queries are sent
6//! back-to-back before reading any responses, which keeps both sides of the
7//! connection busy and reduces round-trip latency.
8//!
9//! `tokio-postgres` (0.7.x) achieves this *implicitly*: when multiple query
10//! futures are polled concurrently (e.g. with `futures::future::try_join_all`),
11//! they pipeline their requests over the same TCP connection.  There is no
12//! explicit `pipeline()` API in 0.7.x.
13//!
14//! # Design and limitations
15//!
16//! Because `PgConnection` wraps `tokio_postgres::Client` behind an
17//! `Arc<tokio::sync::Mutex<Client>>`, only one logical operation can hold the
18//! lock at a time.  True parallel in-flight pipelining is therefore not
19//! achievable within a single `PgPipeline` instance — each prepared-statement
20//! compilation and each query execution happens sequentially under the lock.
21//!
22//! What `PgPipeline` *does* provide is a convenient batched-dispatch API:
23//! callers queue all their queries up front, call [`PgPipeline::finish`] once,
24//! and receive all results in one go.  The internal execution is sequential
25//! but fully pipelined at the PostgreSQL wire level where possible (all
26//! prepares are issued first; then all executions are issued in order).
27//!
28//! If true concurrent pipelining is required, use a connection pool with
29//! multiple connections and issue queries on separate handles simultaneously.
30//!
31//! # Example
32//!
33//! ```rust,no_run
34//! # #[tokio::main]
35//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
36//! use oxisql_postgres::{PgConnection, TlsMode};
37//!
38//! let conn = PgConnection::connect(
39//!     "host=localhost user=postgres dbname=testdb",
40//!     TlsMode::Disabled,
41//! ).await?;
42//!
43//! let mut pipeline = conn.pipeline();
44//! pipeline.add_execute("INSERT INTO t (n) VALUES (1)", &[]);
45//! pipeline.add_execute("INSERT INTO t (n) VALUES (2)", &[]);
46//! pipeline.add_query("SELECT n FROM t ORDER BY n", &[]);
47//!
48//! let result = pipeline.finish().await?;
49//! assert_eq!(result.executes.len(), 2);
50//! assert_eq!(result.queries.len(), 1);
51//! # Ok(())
52//! # }
53//! ```
54
55use std::sync::Arc;
56
57use futures::future::try_join_all;
58use tokio::sync::Mutex;
59use tokio_postgres::Statement;
60
61use oxisql_core::{Row, ToSqlValue};
62
63use crate::error::PgError;
64use crate::types::{pg_row_to_row, value_to_param, OwnedParam};
65
66// ── internal query kind ───────────────────────────────────────────────────────
67
68/// Internal representation of a queued pipeline entry.
69enum PipelineQuery {
70    /// A write (DML/DDL) query — returns rows-affected count.
71    Execute {
72        sql: String,
73        params: Vec<OwnedParam>,
74    },
75    /// A read query — returns a row set.
76    Query {
77        sql: String,
78        params: Vec<OwnedParam>,
79    },
80}
81
82// ── PipelineResult ────────────────────────────────────────────────────────────
83
84/// Collected results from a completed [`PgPipeline`].
85///
86/// `executes` and `queries` are ordered to match the sequence in which
87/// [`PgPipeline::add_execute`] and [`PgPipeline::add_query`] were called.
88pub struct PipelineResult {
89    /// Rows-affected count for each [`PgPipeline::add_execute`] call, in order.
90    pub executes: Vec<u64>,
91    /// Row sets for each [`PgPipeline::add_query`] call, in order.
92    pub queries: Vec<Vec<Row>>,
93}
94
95// ── PgPipeline ────────────────────────────────────────────────────────────────
96
97/// A batched-dispatch pipeline for PostgreSQL.
98///
99/// Create via `PgConnection::pipeline`, queue queries with
100/// [`add_execute`][Self::add_execute] and [`add_query`][Self::add_query],
101/// then send all at once with [`finish`][Self::finish].
102///
103/// See the module documentation for design limitations.
104pub struct PgPipeline {
105    client: Arc<Mutex<tokio_postgres::Client>>,
106    queries: Vec<PipelineQuery>,
107}
108
109impl PgPipeline {
110    /// Create a new pipeline backed by `client`.
111    ///
112    /// This is called by [`PgConnection::pipeline`] and is not meant to be
113    /// constructed directly by callers.
114    pub(crate) fn new(client: Arc<Mutex<tokio_postgres::Client>>) -> Self {
115        Self {
116            client,
117            queries: Vec::new(),
118        }
119    }
120
121    /// Queue a write (DML/DDL) statement to be executed when [`finish`][Self::finish] is called.
122    ///
123    /// `params` may be empty for parameter-free SQL.
124    pub fn add_execute(&mut self, sql: &str, params: &[&dyn ToSqlValue]) {
125        let owned: Vec<OwnedParam> = params
126            .iter()
127            .map(|p| value_to_param(&p.to_value()))
128            .collect();
129        self.queries.push(PipelineQuery::Execute {
130            sql: sql.to_string(),
131            params: owned,
132        });
133    }
134
135    /// Queue a read (`SELECT`) statement to be executed when [`finish`][Self::finish] is called.
136    ///
137    /// `params` may be empty for parameter-free SQL.
138    pub fn add_query(&mut self, sql: &str, params: &[&dyn ToSqlValue]) {
139        let owned: Vec<OwnedParam> = params
140            .iter()
141            .map(|p| value_to_param(&p.to_value()))
142            .collect();
143        self.queries.push(PipelineQuery::Query {
144            sql: sql.to_string(),
145            params: owned,
146        });
147    }
148
149    /// Send all queued queries in a single pass and return collected results.
150    ///
151    /// Internally this does two sequential passes under the connection lock:
152    ///
153    /// 1. **Prepare pass** — compile every distinct SQL string once.  All
154    ///    `PREPARE` messages are flushed to the server before any `EXECUTE`
155    ///    messages, allowing the server to start planning while the client
156    ///    sends the remaining requests (wire-level pipelining).
157    ///
158    /// 2. **Execute pass** — run each query in enqueue order, collecting
159    ///    rows-affected counts and row sets respectively.
160    ///
161    /// Returns a [`PipelineResult`] whose `executes` and `queries` vecs are
162    /// in the same order as the corresponding `add_*` calls.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`PgError`] on any preparation, execution, or type-conversion
167    /// failure.  On error, results collected up to that point are discarded.
168    pub async fn finish(self) -> Result<PipelineResult, PgError> {
169        if self.queries.is_empty() {
170            return Ok(PipelineResult {
171                executes: Vec::new(),
172                queries: Vec::new(),
173            });
174        }
175
176        let client = self.client.lock().await;
177
178        // --- Pass 1: prepare all statements concurrently using implicit
179        //             tokio-postgres pipelining (futures polled in parallel
180        //             via try_join_all).  The Mutex guard is held for the
181        //             whole prepare pass; tokio-postgres internally pipelines
182        //             all Prepare messages over the single TCP connection.
183        let prepare_futs: Vec<_> = self
184            .queries
185            .iter()
186            .map(|q| {
187                let sql = match q {
188                    PipelineQuery::Execute { sql, .. } => sql.as_str(),
189                    PipelineQuery::Query { sql, .. } => sql.as_str(),
190                };
191                client.prepare(sql)
192            })
193            .collect();
194
195        let stmts: Vec<Statement> = try_join_all(prepare_futs).await.map_err(PgError::from)?;
196
197        // --- Pass 2: execute in enqueue order, accumulating results.
198        let mut executes: Vec<u64> = Vec::new();
199        let mut queries: Vec<Vec<Row>> = Vec::new();
200
201        for (q, stmt) in self.queries.into_iter().zip(stmts) {
202            match q {
203                PipelineQuery::Execute { params, .. } => {
204                    let refs = owned_refs(&params);
205                    let n = client
206                        .execute(&stmt, refs.as_slice())
207                        .await
208                        .map_err(PgError::from)?;
209                    executes.push(n);
210                }
211                PipelineQuery::Query { params, .. } => {
212                    let refs = owned_refs(&params);
213                    let pg_rows = client
214                        .query(&stmt, refs.as_slice())
215                        .await
216                        .map_err(PgError::from)?;
217                    let rows: Vec<Row> = pg_rows.into_iter().map(pg_row_to_row).collect::<Result<
218                        Vec<_>,
219                        PgError,
220                    >>(
221                    )?;
222                    queries.push(rows);
223                }
224            }
225        }
226
227        Ok(PipelineResult { executes, queries })
228    }
229}
230
231// ── helpers ───────────────────────────────────────────────────────────────────
232
233/// Build a `&[&(dyn ToSql + Sync)]` reference slice from owned params.
234fn owned_refs(owned: &[OwnedParam]) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
235    owned.iter().map(|p| p as _).collect()
236}