Skip to main content

wasi_pg_client/query/
pipeline.rs

1//! Pipelined extended query execution.
2//!
3//! A [`Pipeline`] batches multiple parameterized queries into a single
4//! round-trip, reducing latency when multiple independent queries need to
5//! execute.
6//!
7//! # Example
8//! ```ignore
9//! let results = conn.pipeline()
10//!     .query("SELECT $1", &[&1i32])
11//!     .query("SELECT $1", &[&2i32])
12//!     .finish()
13//!     .await?;
14//! ```
15
16use std::sync::Arc;
17
18use crate::protocol::{BackendMessage, FrontendMessage, TransactionStatus};
19
20use crate::connection::{Connection, ConnectionState};
21use crate::error::{PgError, PgServerError, Result};
22use crate::query::params::encode_params_text;
23use crate::query::result::{CommandTag, QueryResult};
24use crate::query::row::{FieldDescription, Row};
25use crate::query::{read_data_row, read_row_description};
26use crate::transport::AsyncTransport;
27
28// ---------------------------------------------------------------------------
29// Pipeline types
30// ---------------------------------------------------------------------------
31
32/// A single operation in a pipeline.
33#[derive(Debug)]
34pub(crate) enum PipelineOp {
35    /// A query that returns rows.
36    Query {
37        sql: String,
38        params: Vec<Option<Vec<u8>>>,
39    },
40    /// A statement that does not return rows.
41    Execute {
42        sql: String,
43        params: Vec<Option<Vec<u8>>>,
44    },
45}
46
47/// The result of a single pipeline operation.
48#[derive(Debug, Clone)]
49#[non_exhaustive]
50pub enum PipelineResult {
51    /// A query returned rows.
52    Query(QueryResult),
53    /// A statement completed without returning rows.
54    Execute(CommandTag),
55}
56
57// ---------------------------------------------------------------------------
58// Pipeline builder
59// ---------------------------------------------------------------------------
60
61/// A builder for pipelined extended-query operations.
62///
63/// Created via [`Connection::pipeline`]. All operations are buffered locally
64/// until [`Pipeline::finish`] is called, at which point they are sent to the
65/// server in a single batch followed by one `Sync`.
66#[non_exhaustive]
67pub struct Pipeline<'a> {
68    conn: &'a mut Connection,
69    ops: Vec<PipelineOp>,
70}
71
72impl<'a> Pipeline<'a> {
73    pub(crate) fn new(conn: &'a mut Connection) -> Self {
74        Self {
75            conn,
76            ops: Vec::new(),
77        }
78    }
79
80    /// Add a query operation that returns rows.
81    pub fn query(mut self, sql: &str, params: &[&dyn crate::types::ToSql]) -> Result<Self> {
82        let values = encode_params_text(params)?;
83        self.ops.push(PipelineOp::Query {
84            sql: sql.to_string(),
85            params: values,
86        });
87        Ok(self)
88    }
89
90    /// Add an execute operation that does not return rows.
91    pub fn execute(mut self, sql: &str, params: &[&dyn crate::types::ToSql]) -> Result<Self> {
92        let values = encode_params_text(params)?;
93        self.ops.push(PipelineOp::Execute {
94            sql: sql.to_string(),
95            params: values,
96        });
97        Ok(self)
98    }
99
100    /// Send all buffered operations and collect results.
101    ///
102    /// Operations are sent as a single pipeline:
103    /// ```text
104    /// Parse | Bind | Execute | ... | Parse | Bind | Execute | Sync
105    /// ```
106    #[must_use = "pipeline errors should be checked"]
107    pub async fn finish(self) -> Result<Vec<PipelineResult>> {
108        let conn = self.conn;
109        conn.transition(ConnectionState::ActiveExtendedQuery)?;
110
111        // Send all operations as a batch
112        for op in &self.ops {
113            match op {
114                PipelineOp::Query { sql, params } | PipelineOp::Execute { sql, params } => {
115                    conn.codec
116                        .encode_and_write(
117                            &mut conn.transport,
118                            &FrontendMessage::Parse {
119                                name: String::new(),
120                                sql: sql.clone(),
121                                param_types: vec![],
122                            },
123                        )
124                        .await?;
125
126                    conn.codec
127                        .encode_and_write(
128                            &mut conn.transport,
129                            &FrontendMessage::Bind {
130                                portal: String::new(),
131                                statement: String::new(),
132                                param_formats: vec![crate::protocol::FormatCode::Text],
133                                params: params.clone(),
134                                result_formats: vec![crate::protocol::FormatCode::Binary],
135                            },
136                        )
137                        .await?;
138
139                    // Describe the unnamed portal so the server sends
140                    // RowDescription before any DataRow.
141                    conn.codec
142                        .encode_and_write(
143                            &mut conn.transport,
144                            &FrontendMessage::Describe {
145                                variant: b'P',
146                                name: String::new(),
147                            },
148                        )
149                        .await?;
150
151                    conn.codec
152                        .encode_and_write(
153                            &mut conn.transport,
154                            &FrontendMessage::Execute {
155                                portal: String::new(),
156                                max_rows: 0,
157                            },
158                        )
159                        .await?;
160                }
161            }
162        }
163
164        // Single Sync at the end
165        conn.codec
166            .encode_and_write(&mut conn.transport, &FrontendMessage::Sync)
167            .await?;
168
169        // Flush the entire batch
170        conn.transport.flush().await.map_err(PgError::Transport)?;
171
172        // Read results
173        let mut results = Vec::with_capacity(self.ops.len());
174        let mut current_op = 0;
175        let mut current_columns: Option<Arc<Vec<FieldDescription>>> = None;
176        let mut current_rows: Vec<Row> = Vec::new();
177
178        loop {
179            let msg = conn.codec.read_message(&mut conn.transport).await?;
180            if conn.handle_async_message(&msg) {
181                continue;
182            }
183            match msg {
184                BackendMessage::ParseComplete => {}
185                BackendMessage::BindComplete => {}
186                BackendMessage::NoData => {
187                    // Statement doesn't return rows (INSERT, UPDATE, etc.)
188                }
189                BackendMessage::RowDescription(body) => {
190                    current_columns = Some(Arc::new(read_row_description(body)?));
191                }
192                BackendMessage::DataRow(body) => {
193                    let values = read_data_row(body)?;
194                    current_rows.push(Row::new(
195                        current_columns.clone().unwrap_or_default(),
196                        values,
197                    ));
198                }
199                BackendMessage::CommandComplete(body) => {
200                    let tag = CommandTag::new(body.tag().unwrap_or("").into());
201
202                    // Determine if this was a Query or Execute op
203                    match self.ops.get(current_op) {
204                        Some(PipelineOp::Query { .. }) => {
205                            results.push(PipelineResult::Query(QueryResult::new(
206                                std::mem::take(&mut current_rows),
207                                tag,
208                                current_columns.take().unwrap_or_default(),
209                            )));
210                        }
211                        Some(PipelineOp::Execute { .. }) => {
212                            results.push(PipelineResult::Execute(tag));
213                        }
214                        None => {}
215                    }
216                    current_op += 1;
217                }
218                BackendMessage::EmptyQueryResponse => {
219                    match self.ops.get(current_op) {
220                        Some(PipelineOp::Query { .. }) => {
221                            results.push(PipelineResult::Query(QueryResult::new(
222                                Vec::new(),
223                                CommandTag::new("".into()),
224                                Arc::new(Vec::new()),
225                            )));
226                        }
227                        Some(PipelineOp::Execute { .. }) => {
228                            results.push(PipelineResult::Execute(CommandTag::new("".into())));
229                        }
230                        None => {}
231                    }
232                    current_op += 1;
233                }
234                BackendMessage::ErrorResponse(body) => {
235                    let server_err = PgServerError::from_error_body(&body).map_err(PgError::Io)?;
236                    conn.read_until_ready().await?;
237                    conn.state = ConnectionState::Idle;
238                    return Err(PgError::Server(Box::new(server_err)));
239                }
240                BackendMessage::ReadyForQuery(body) => {
241                    conn.transaction_status = TransactionStatus::from_u8(body.status())
242                        .unwrap_or(TransactionStatus::Idle);
243                    conn.state = ConnectionState::Idle;
244                    break;
245                }
246                _ => {}
247            }
248        }
249
250        Ok(results)
251    }
252}
253
254// ---------------------------------------------------------------------------
255// Connection method
256// ---------------------------------------------------------------------------
257
258impl Connection {
259    /// Start building a pipeline of extended-query operations.
260    ///
261    /// See [`Pipeline`] for details.
262    pub fn pipeline(&mut self) -> Pipeline<'_> {
263        Pipeline::new(self)
264    }
265}