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(¶ms);
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(¶ms);
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}