Skip to main content

hyperdb_api_core/client/
async_connection.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Async low-level connection handling.
5//!
6//! This module provides [`AsyncRawConnection`], the async version of [`RawConnection`](super::connection::RawConnection).
7//! It uses tokio's async I/O traits for non-blocking network operations.
8
9use std::collections::HashMap;
10
11use bytes::BytesMut;
12use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
13use tracing::{debug, info, warn};
14
15use crate::protocol::message::{backend::Message, frontend};
16
17use super::auth::{self, AuthState};
18use super::error::{Error, Result};
19use super::statement::{ParamFormat, bind_format_codes};
20
21/// An async raw connection to a Hyper server.
22///
23/// This is the async equivalent of [`RawConnection`](super::connection::RawConnection),
24/// using tokio's async I/O traits instead of std's sync I/O.
25///
26/// The connection is generic over the stream type `S`, allowing it to work
27/// with different transport mechanisms (`TcpStream`, `TlsStream`, etc.) as long as they
28/// implement `AsyncRead + AsyncWrite + Unpin`.
29#[derive(Debug)]
30pub struct AsyncRawConnection<S> {
31    /// The underlying async I/O stream.
32    stream: S,
33    /// Buffer for reading incoming messages from the server.
34    read_buf: BytesMut,
35    /// Buffer for writing outgoing messages to the server.
36    write_buf: BytesMut,
37    /// Backend process ID (for cancel requests).
38    process_id: i32,
39    /// Secret key for authenticating cancel requests.
40    secret_key: i32,
41    /// Server parameters received during startup.
42    server_params: HashMap<String, String>,
43    /// Set by `AsyncCopyInWriter::Drop` when a COPY session is abandoned.
44    /// The `CopyFail` message has been written to `write_buf` but not flushed.
45    /// The next async operation must flush and drain the server response
46    /// (`ErrorResponse` + `ReadyForQuery`) before proceeding.
47    pending_copy_cancel: bool,
48    /// Sticky flag mirroring
49    /// [`RawConnection`](super::connection::RawConnection)'s
50    /// `desynchronized` field. Set when a bounded drain exhausts its
51    /// budget or hits a mid-drain I/O error; never cleared. See
52    /// [`Self::is_healthy`] and [`Self::ensure_healthy`] for the
53    /// consumer-facing API.
54    desynchronized: bool,
55}
56
57impl<S> AsyncRawConnection<S>
58where
59    S: AsyncRead + AsyncWrite + Unpin,
60{
61    /// Creates a new async raw connection from a stream.
62    ///
63    /// Initializes read and write buffers with default capacity (64 KB each).
64    /// The connection is not yet authenticated - call `startup()` to begin
65    /// the connection handshake.
66    pub fn new(stream: S) -> Self {
67        AsyncRawConnection {
68            stream,
69            read_buf: BytesMut::with_capacity(64 * 1024),
70            write_buf: BytesMut::with_capacity(64 * 1024),
71            process_id: 0,
72            secret_key: 0,
73            server_params: HashMap::new(),
74            pending_copy_cancel: false,
75            desynchronized: false,
76        }
77    }
78
79    /// Returns `true` if this connection is still in a known-good state
80    /// and safe to use for new requests. See
81    /// [`super::connection::RawConnection::is_healthy`] for the full
82    /// semantics — this is the async mirror with identical behavior.
83    pub fn is_healthy(&self) -> bool {
84        !self.desynchronized
85    }
86
87    /// Marks this connection as desynchronized.
88    ///
89    /// Used by async result streams that are dropped mid-iteration: the
90    /// [`Drop`] impl cannot `await` to drain trailing `ErrorResponse +
91    /// ReadyForQuery` messages after sending a cancel, so it flags the
92    /// connection so the next operation short-circuits with a clear error
93    /// rather than hanging or misinterpreting stale server output.
94    pub fn mark_desynchronized(&mut self) {
95        self.desynchronized = true;
96    }
97
98    /// Async mirror of
99    /// [`super::connection::RawConnection::ensure_healthy`]. Called from
100    /// the entry point of every `pub async fn` that initiates a new
101    /// server request to short-circuit operations on a desynchronized
102    /// connection before any bytes hit the wire.
103    pub(crate) fn ensure_healthy(&self) -> Result<()> {
104        if self.desynchronized {
105            return Err(Error::connection(
106                "connection is desynchronized from the server and cannot be reused; \
107                 discard it and open a new one",
108            ));
109        }
110        Ok(())
111    }
112
113    /// Returns the process ID assigned by the server.
114    pub fn process_id(&self) -> i32 {
115        self.process_id
116    }
117
118    /// Returns the secret key for cancel requests.
119    pub fn secret_key(&self) -> i32 {
120        self.secret_key
121    }
122
123    /// Returns a reference to the underlying stream.
124    pub fn stream(&self) -> &S {
125        &self.stream
126    }
127
128    /// Returns a mutable reference to the underlying stream.
129    pub fn stream_mut(&mut self) -> &mut S {
130        &mut self.stream
131    }
132
133    /// Returns a server parameter value by name.
134    pub fn parameter_status(&self, name: &str) -> Option<&str> {
135        self.server_params
136            .get(name)
137            .map(std::string::String::as_str)
138    }
139
140    /// Queues a `CopyFail` message in the write buffer (synchronous).
141    ///
142    /// Called from `AsyncCopyInWriter::Drop` when a COPY session is abandoned
143    /// without `finish()` or `cancel()`. The `CopyFail` is written to the buffer
144    /// but NOT flushed (we can't do async I/O from `Drop`). The next async
145    /// operation will call [`drain_pending_copy_cancel`](Self::drain_pending_copy_cancel) to flush and drain
146    /// the server's `ErrorResponse` + `ReadyForQuery` before proceeding.
147    pub fn queue_copy_fail(&mut self, reason: &str) {
148        frontend::copy_fail(reason, &mut self.write_buf);
149        self.pending_copy_cancel = true;
150    }
151
152    /// Drains a pending COPY cancel that was queued by `queue_copy_fail()`.
153    ///
154    /// If `pending_copy_cancel` is set, this flushes the `CopyFail` message to
155    /// the server and reads messages until `ReadyForQuery`, restoring the
156    /// connection to a usable state. Called automatically at the start of
157    /// new operations (`simple_query`, `query_binary`, `start_copy_in*`).
158    ///
159    /// # Errors
160    ///
161    /// Returns [`Error`] (I/O) if flushing the queued `CopyFail` or
162    /// reading the server's drain responses fails. A successful drain
163    /// clears `pending_copy_cancel`.
164    pub async fn drain_pending_copy_cancel(&mut self) -> Result<()> {
165        if !self.pending_copy_cancel {
166            return Ok(());
167        }
168
169        // Flush the queued CopyFail message
170        self.flush().await?;
171
172        // Drain messages until the connection is back in ReadyForQuery state
173        loop {
174            let msg = self.read_message().await?;
175            match msg {
176                Message::ReadyForQuery(_) => {
177                    self.pending_copy_cancel = false;
178                    debug!(
179                        target: "hyperdb_api_core::client",
180                        "drained pending COPY cancel — connection restored"
181                    );
182                    return Ok(());
183                }
184                Message::ErrorResponse(_) => {
185                    // Expected — server confirms the cancel
186                }
187                _ => {
188                    // Ignore other messages (e.g., NoticeResponse)
189                }
190            }
191        }
192    }
193
194    /// Sends a startup message and performs initial handshake (async).
195    ///
196    /// # Errors
197    ///
198    /// - Returns [`Error`] (auth) when the server requests an
199    ///   auth method and no password is supplied, when the offered
200    ///   SASL mechanisms exclude SCRAM-SHA-256, or when SCRAM state
201    ///   is missing at the SASL-continue / SASL-final step.
202    /// - Returns [`Error`] (server) when the server sends an `ErrorResponse`
203    ///   during startup (unknown user, unknown database, etc.).
204    /// - Returns [`Error`] (protocol) if a message arrives out of
205    ///   sequence.
206    /// - Returns [`Error`] (I/O) on transport read/write failure.
207    pub async fn startup(&mut self, params: &[(&str, &str)], password: Option<&str>) -> Result<()> {
208        // Send startup message
209        frontend::startup_message(params, &mut self.write_buf)?;
210        self.flush().await?;
211
212        // Handle authentication
213        let mut auth_state: Option<AuthState> = None;
214
215        loop {
216            let msg = self.read_message().await?;
217            match msg {
218                Message::AuthenticationOk => {
219                    info!(target: "hyperdb_api", "connection-auth-success");
220                }
221                Message::AuthenticationCleartextPassword => {
222                    debug!(target: "hyperdb_api", method = "cleartext", "connection-auth-method");
223                    let password = password.ok_or_else(|| {
224                        Error::authentication(
225                            "server requested cleartext password but none provided",
226                        )
227                    })?;
228                    frontend::password_message(password, &mut self.write_buf)?;
229                    self.flush().await?;
230                }
231                Message::AuthenticationMd5Password(body) => {
232                    debug!(target: "hyperdb_api", method = "MD5", "connection-auth-method");
233                    let password = password.ok_or_else(|| {
234                        Error::authentication("server requested MD5 password but none provided")
235                    })?;
236                    let user = params
237                        .iter()
238                        .find(|(k, _)| *k == "user")
239                        .map_or("", |(_, v)| *v);
240
241                    let md5_response = auth::compute_md5_password(user, password, &body.salt());
242                    frontend::password_message(&md5_response, &mut self.write_buf)?;
243                    self.flush().await?;
244                }
245                Message::AuthenticationSasl(body) => {
246                    debug!(target: "hyperdb_api", method = "SCRAM-SHA-256", "connection-auth-method");
247                    let password = password.ok_or_else(|| {
248                        Error::authentication(
249                            "server requested SASL authentication but no password provided",
250                        )
251                    })?;
252
253                    let mechanisms: Vec<&str> = body.mechanisms().collect();
254                    if !mechanisms.contains(&"SCRAM-SHA-256") {
255                        return Err(Error::authentication(format!(
256                            "server offered unsupported SASL mechanisms: {mechanisms:?}"
257                        )));
258                    }
259
260                    let (state, client_first) = auth::scram_client_first(password)?;
261                    auth_state = Some(state);
262
263                    frontend::sasl_initial_response(
264                        "SCRAM-SHA-256",
265                        &client_first,
266                        &mut self.write_buf,
267                    )?;
268                    self.flush().await?;
269                }
270                Message::AuthenticationSaslContinue(body) => {
271                    let state = auth_state.take().ok_or_else(|| {
272                        Error::authentication("received SASL continue without initial state")
273                    })?;
274
275                    let server_first = body.data();
276                    let (new_state, client_final) = auth::scram_client_final(state, server_first)?;
277                    auth_state = Some(new_state);
278
279                    frontend::sasl_response(&client_final, &mut self.write_buf)?;
280                    self.flush().await?;
281                }
282                Message::AuthenticationSaslFinal(body) => {
283                    let state = auth_state.take().ok_or_else(|| {
284                        Error::authentication("received SASL final without state")
285                    })?;
286                    auth::scram_verify_server(state, body.data())?;
287                }
288                Message::BackendKeyData(data) => {
289                    self.process_id = data.process_id();
290                    self.secret_key = data.secret_key();
291                }
292                Message::ParameterStatus(body) => {
293                    if let (Ok(name), Ok(value)) = (body.name(), body.value()) {
294                        self.server_params
295                            .insert(name.to_string(), value.to_string());
296                    }
297                }
298                Message::ReadyForQuery(_) => {
299                    return Ok(());
300                }
301                Message::ErrorResponse(body) => {
302                    return Err(self.consume_error(&body).await);
303                }
304                _ => {
305                    return Err(Error::protocol("unexpected message during startup"));
306                }
307            }
308        }
309    }
310
311    /// Sends a simple query and returns all messages until `ReadyForQuery` (async).
312    ///
313    /// # Errors
314    ///
315    /// - Returns [`Error`] (connection) if the connection has been
316    ///   marked unhealthy.
317    /// - Returns [`Error`] (server) when the server emits an
318    ///   `ErrorResponse` (SQL error, constraint violation, etc.).
319    /// - Returns [`Error`] (I/O) / [`Error`] (closed) on transport
320    ///   read/write failure.
321    /// - Propagates any error from
322    ///   [`Self::drain_pending_copy_cancel`] when a queued `CopyFail`
323    ///   needs to be flushed first.
324    pub async fn simple_query(&mut self, query: &str) -> Result<Vec<Message>> {
325        self.ensure_healthy()?;
326        self.drain_pending_copy_cancel().await?;
327        frontend::query(query, &mut self.write_buf)?;
328        self.flush().await?;
329
330        let mut messages = Vec::new();
331        loop {
332            let msg = self.read_message().await?;
333            match &msg {
334                Message::ReadyForQuery(_) => {
335                    messages.push(msg);
336                    return Ok(messages);
337                }
338                Message::ErrorResponse(body) => {
339                    return Err(self.consume_error(body).await);
340                }
341                _ => {
342                    messages.push(msg);
343                }
344            }
345        }
346    }
347
348    /// Sends a query using extended protocol with binary format results (async).
349    ///
350    /// # Errors
351    ///
352    /// Same failure modes as [`Self::simple_query`].
353    pub async fn query_binary(&mut self, query: &str) -> Result<Vec<Message>> {
354        self.ensure_healthy()?;
355        self.drain_pending_copy_cancel().await?;
356        const HYPER_BINARY_FORMAT: i16 = 2;
357
358        frontend::parse("", query, &[], &mut self.write_buf)?;
359        frontend::bind(
360            "",
361            "",
362            &[],
363            &[],
364            &[HYPER_BINARY_FORMAT],
365            &mut self.write_buf,
366        )?;
367        frontend::describe(b'P', "", &mut self.write_buf)?;
368        frontend::execute("", 0, &mut self.write_buf)?;
369        frontend::sync(&mut self.write_buf);
370
371        self.flush().await?;
372
373        let mut messages = Vec::new();
374        loop {
375            let msg = self.read_message().await?;
376            match &msg {
377                Message::ReadyForQuery(_) => {
378                    messages.push(msg);
379                    return Ok(messages);
380                }
381                Message::ErrorResponse(body) => {
382                    return Err(self.consume_error(body).await);
383                }
384                _ => {
385                    messages.push(msg);
386                }
387            }
388        }
389    }
390
391    /// Starts a binary query but leaves result consumption to the caller (async).
392    ///
393    /// # Errors
394    ///
395    /// - Returns [`Error`] (connection) if the connection is unhealthy.
396    /// - Returns [`Error`] (I/O) on transport write failure.
397    /// - Propagates any error from [`Self::drain_pending_copy_cancel`].
398    pub async fn start_query_binary(&mut self, query: &str) -> Result<()> {
399        self.ensure_healthy()?;
400        // Drain any CopyFail queued by `AsyncCopyInWriter::Drop` before
401        // writing the extended-query bytes. Without this, the flush at
402        // the end of this method would send [CopyFail | Parse | Bind |
403        // Describe | Execute | Sync] in a single buffer and the server
404        // would answer with CopyFail's ErrorResponse+ReadyForQuery
405        // interleaved with our query's responses — the read loop would
406        // then misattribute the COPY error to this query.
407        self.drain_pending_copy_cancel().await?;
408        const HYPER_BINARY_FORMAT: i16 = 2;
409
410        frontend::parse("", query, &[], &mut self.write_buf)?;
411        frontend::bind(
412            "",
413            "",
414            &[],
415            &[],
416            &[HYPER_BINARY_FORMAT],
417            &mut self.write_buf,
418        )?;
419        frontend::describe(b'P', "", &mut self.write_buf)?;
420        frontend::execute("", 0, &mut self.write_buf)?;
421        frontend::sync(&mut self.write_buf);
422
423        self.flush().await
424    }
425
426    /// Starts a simple query but leaves result consumption to the caller (async).
427    ///
428    /// # Errors
429    ///
430    /// Same failure modes as [`Self::start_query_binary`].
431    pub async fn start_simple_query(&mut self, query: &str) -> Result<()> {
432        self.ensure_healthy()?;
433        // See `start_query_binary` for why the pending-copy-cancel drain
434        // is required before writing any new query bytes.
435        self.drain_pending_copy_cancel().await?;
436        frontend::query(query, &mut self.write_buf)?;
437        self.flush().await
438    }
439
440    /// Starts an **execute** of a prepared statement but leaves result
441    /// consumption to the caller (async).
442    ///
443    /// Async mirror of
444    /// [`super::connection::RawConnection::start_execute_prepared`]. See
445    /// that method's docs for the split format-code rationale (params
446    /// use `1` = PG binary/BE, results use `2` = HyperBinary/LE).
447    ///
448    /// # Errors
449    ///
450    /// - Returns [`Error`] (connection) if the connection is unhealthy.
451    /// - Returns [`Error`] (I/O) on transport write failure.
452    /// - Propagates any error from [`Self::drain_pending_copy_cancel`].
453    pub async fn start_execute_prepared(
454        &mut self,
455        statement_name: &str,
456        params: &[Option<&[u8]>],
457        column_count: usize,
458    ) -> Result<()> {
459        self.start_execute_prepared_with_formats(statement_name, params, &[], column_count)
460            .await
461    }
462
463    /// Same as [`Self::start_execute_prepared`], but with a caller-chosen
464    /// wire format per parameter.
465    ///
466    /// Async mirror of
467    /// [`super::connection::RawConnection::start_execute_prepared_with_formats`].
468    /// `param_formats` is either empty — meaning **every parameter is
469    /// binary** — or the same length as `params`.
470    ///
471    /// # Errors
472    ///
473    /// - Returns [`Error`] (protocol) if `param_formats` is non-empty and its
474    ///   length differs from `params`.
475    /// - Otherwise the same failure modes as
476    ///   [`Self::start_execute_prepared`].
477    pub async fn start_execute_prepared_with_formats(
478        &mut self,
479        statement_name: &str,
480        params: &[Option<&[u8]>],
481        param_formats: &[ParamFormat],
482        column_count: usize,
483    ) -> Result<()> {
484        let codes = bind_format_codes(param_formats, params.len())?;
485        self.bind_execute_sync(statement_name, params, &codes, column_count)
486            .await
487    }
488
489    async fn bind_execute_sync(
490        &mut self,
491        statement_name: &str,
492        params: &[Option<&[u8]>],
493        param_format_codes: &[i16],
494        column_count: usize,
495    ) -> Result<()> {
496        self.ensure_healthy()?;
497        // Same rationale as `start_query_binary` for draining a pending
498        // CopyFail before writing new extended-query bytes.
499        self.drain_pending_copy_cancel().await?;
500
501        const HYPER_BINARY_FORMAT: i16 = 2;
502        let result_formats: Vec<i16> = vec![HYPER_BINARY_FORMAT; column_count];
503
504        frontend::bind(
505            "", // unnamed portal
506            statement_name,
507            param_format_codes,
508            params,
509            &result_formats,
510            &mut self.write_buf,
511        )?;
512        frontend::execute("", 0, &mut self.write_buf)?;
513        frontend::sync(&mut self.write_buf);
514
515        self.flush().await
516    }
517
518    /// Reads a single message from the server (async).
519    ///
520    /// # Errors
521    ///
522    /// - Returns [`Error`] (I/O) if reading from the transport fails or
523    ///   if [`Message::parse`] reports a malformed frame.
524    /// - Returns [`Error`] (closed) when the transport reaches EOF
525    ///   (server closed the connection).
526    pub async fn read_message(&mut self) -> Result<Message> {
527        loop {
528            if let Some(msg) = Message::parse(&mut self.read_buf).map_err(Error::from_io)? {
529                return Ok(msg);
530            }
531
532            // Need more data — read directly into the spare capacity of
533            // `read_buf`, no temporary buffer or `extend_from_slice` memcpy.
534            // See the sync mirror in
535            // [`super::connection::RawConnection::read_message`] for the
536            // full rationale on the 64 KiB ceiling and Windows-loopback
537            // syscall amplification.
538            let prev_len = self.read_buf.len();
539            self.read_buf.resize(prev_len + 64 * 1024, 0);
540            let n = self.stream.read(&mut self.read_buf[prev_len..]).await?;
541            if n == 0 {
542                self.read_buf.truncate(prev_len);
543                warn!(target: "hyperdb_api", "connection-closed");
544                return Err(Error::closed("connection closed"));
545            }
546            self.read_buf.truncate(prev_len + n);
547        }
548    }
549
550    /// Async equivalent of
551    /// [`super::connection::RawConnection::drain_until_ready`]. Unbounded;
552    /// prefer [`drain_until_ready_bounded`](Self::drain_until_ready_bounded)
553    /// in destructors and other code paths where blocking indefinitely is
554    /// unacceptable. Drain errors are logged via `tracing::warn!` and then
555    /// swallowed.
556    pub async fn drain_until_ready(&mut self) {
557        let _ = self.drain_until_ready_bounded(usize::MAX).await;
558    }
559
560    /// Async equivalent of
561    /// [`super::connection::RawConnection::drain_until_ready_bounded`].
562    /// See that function's docs for the full semantics, including why we do
563    /// **not** send a `Sync` before draining (it would produce an extra
564    /// `ReadyForQuery` on the wire and corrupt the next query's response).
565    pub async fn drain_until_ready_bounded(&mut self, max_messages: usize) -> bool {
566        for i in 0..max_messages {
567            match self.read_message().await {
568                Ok(Message::ReadyForQuery(_)) => return true,
569                Ok(_) => {}
570                Err(e) => {
571                    warn!(
572                        target: "hyperdb_api_core::client",
573                        error = %e,
574                        messages_read = i,
575                        "drain_until_ready: read error mid-drain (likely closed connection); \
576                         connection marked desynchronized",
577                    );
578                    // Mirror of sync path: any mid-drain read error leaves
579                    // the connection in unknown state. See
580                    // `super::connection::RawConnection::drain_until_ready_bounded`
581                    // for the full rationale.
582                    self.desynchronized = true;
583                    return false;
584                }
585            }
586        }
587        warn!(
588            target: "hyperdb_api_core::client",
589            max_messages,
590            "drain_until_ready_bounded: exhausted budget without seeing ReadyForQuery; \
591             connection marked desynchronized and should not be reused",
592        );
593        self.desynchronized = true;
594        false
595    }
596
597    /// Async equivalent of
598    /// [`super::connection::RawConnection::consume_error`]. Parse the error
599    /// body and drain the rest of the response in one call. Semantics are
600    /// identical to the sync version, including the
601    /// [`POST_ERROR_DRAIN_CAP`](super::connection::POST_ERROR_DRAIN_CAP)
602    /// safety valve — see that function's docs for the rationale. Unbounded
603    /// drain would be particularly dangerous here because a stalled read
604    /// on the underlying async stream would hang the caller's future
605    /// indefinitely with no observable symptom; the bounded drain turns
606    /// that into a loud `tracing::warn!` plus a connection marked for
607    /// reconnect on next use.
608    pub async fn consume_error(
609        &mut self,
610        body: &crate::protocol::message::backend::ErrorResponseBody,
611    ) -> Error {
612        let err = super::connection::parse_error_response(body);
613        let _ = self
614            .drain_until_ready_bounded(super::connection::POST_ERROR_DRAIN_CAP)
615            .await;
616        err
617    }
618
619    /// Flushes the write buffer to the server (async).
620    ///
621    /// # Errors
622    ///
623    /// Returns [`Error`] (I/O) if writing the buffered bytes or flushing
624    /// the underlying async transport fails.
625    pub async fn flush(&mut self) -> Result<()> {
626        if !self.write_buf.is_empty() {
627            self.stream.write_all(&self.write_buf).await?;
628            self.stream.flush().await?;
629            self.write_buf.clear();
630        }
631        Ok(())
632    }
633
634    /// Sends a terminate message and closes the connection (async).
635    ///
636    /// # Errors
637    ///
638    /// Returns [`Error`] (I/O) if writing the `Terminate` frame or
639    /// flushing the async transport fails.
640    pub async fn terminate(&mut self) -> Result<()> {
641        frontend::terminate(&mut self.write_buf);
642        self.flush().await
643    }
644
645    /// Returns a mutable reference to the write buffer.
646    pub fn write_buf(&mut self) -> &mut BytesMut {
647        &mut self.write_buf
648    }
649
650    /// Initiates a COPY IN operation with `HyperBinary` format (async).
651    ///
652    /// # Errors
653    ///
654    /// Same failure modes as [`Self::start_copy_in_with_format`].
655    pub async fn start_copy_in(&mut self, table_name: &str, columns: &[&str]) -> Result<()> {
656        self.start_copy_in_with_format(table_name, columns, "HYPERBINARY")
657            .await
658    }
659
660    /// Initiates a COPY IN operation with a specified format (async).
661    ///
662    /// # Errors
663    ///
664    /// - Returns [`Error`] (connection) if the connection has been
665    ///   marked unhealthy.
666    /// - Returns [`Error`] (server) if the server rejects the generated
667    ///   `COPY ... FROM STDIN` statement.
668    /// - Returns [`Error`] (I/O) on transport read/write failure.
669    /// - Propagates any error from [`Self::drain_pending_copy_cancel`].
670    pub async fn start_copy_in_with_format(
671        &mut self,
672        table_name: &str,
673        columns: &[&str],
674        format: &str,
675    ) -> Result<()> {
676        self.ensure_healthy()?;
677        self.drain_pending_copy_cancel().await?;
678        let column_list = if columns.is_empty() {
679            String::new()
680        } else {
681            format!(
682                " ({})",
683                columns
684                    .iter()
685                    .map(|c| format!("\"{}\"", c.replace('"', "\"\"")))
686                    .collect::<Vec<_>>()
687                    .join(", ")
688            )
689        };
690
691        let query = format!("COPY {table_name}{column_list} FROM STDIN WITH (FORMAT {format})");
692
693        frontend::query(&query, &mut self.write_buf)?;
694        self.flush().await?;
695
696        loop {
697            let msg = self.read_message().await?;
698            match msg {
699                Message::CopyInResponse(_) => {
700                    return Ok(());
701                }
702                Message::ErrorResponse(body) => {
703                    return Err(self.consume_error(&body).await);
704                }
705                _ => {}
706            }
707        }
708    }
709
710    /// Sends COPY data to the server (sync - just buffers).
711    ///
712    /// # Errors
713    ///
714    /// Currently infallible — frame construction is pure. The `Result`
715    /// return type is preserved for forward compatibility.
716    pub fn send_copy_data(&mut self, data: &[u8]) -> Result<()> {
717        frontend::copy_data(data, &mut self.write_buf);
718        Ok(())
719    }
720
721    /// Sends COPY data directly to the stream without internal buffering (async).
722    ///
723    /// This writes the `CopyData` message directly to the TCP stream, letting
724    /// the kernel's TCP stack handle buffering. Use `flush_stream()` periodically
725    /// to ensure data is sent.
726    ///
727    /// # Errors
728    ///
729    /// - Returns [`Error`] (protocol) if `data.len() + 4` exceeds
730    ///   `u32::MAX` (PostgreSQL's per-message length cap).
731    /// - Returns [`Error`] (I/O) if flushing buffered bytes or writing
732    ///   the header / payload to the async transport fails.
733    pub async fn send_copy_data_direct(&mut self, data: &[u8]) -> Result<()> {
734        // First flush any pending buffered data
735        if !self.write_buf.is_empty() {
736            self.stream.write_all(&self.write_buf).await?;
737            self.write_buf.clear();
738        }
739
740        // Write CopyData message header + data directly to stream
741        // Message format: 'd' (1 byte) + length (4 bytes BigEndian) + data
742        let msg_len = u32::try_from(4 + data.len())
743            .map_err(|_| Error::protocol("CopyData payload exceeds u32::MAX bytes"))?;
744        let len_be = msg_len.to_be_bytes();
745        let header = [b'd', len_be[0], len_be[1], len_be[2], len_be[3]];
746        self.stream.write_all(&header).await?;
747        self.stream.write_all(data).await?;
748        Ok(())
749    }
750
751    /// Flushes the TCP stream without clearing the write buffer (async).
752    ///
753    /// Use this with `send_copy_data_direct()` to periodically ensure
754    /// data is sent to the server.
755    ///
756    /// # Errors
757    ///
758    /// Returns [`Error`] (I/O) if flushing the underlying async transport
759    /// fails.
760    pub async fn flush_stream(&mut self) -> Result<()> {
761        self.stream.flush().await?;
762        Ok(())
763    }
764
765    /// Finishes a COPY IN operation successfully (async).
766    ///
767    /// # Errors
768    ///
769    /// - Returns [`Error`] (server) when the server emits an
770    ///   `ErrorResponse` during finalization (for example, a
771    ///   constraint violation from the accumulated rows).
772    /// - Returns [`Error`] (I/O) / [`Error`] (closed) on transport
773    ///   read/write failure.
774    pub async fn finish_copy(&mut self) -> Result<u64> {
775        self.flush().await?;
776
777        frontend::copy_done(&mut self.write_buf);
778        self.flush().await?;
779
780        let mut row_count = 0u64;
781        loop {
782            let msg = self.read_message().await?;
783            match msg {
784                Message::CommandComplete(body) => {
785                    if let Ok(tag) = body.tag()
786                        && let Some(count_str) = tag.strip_prefix("COPY ")
787                        && let Ok(count) = count_str.trim().parse()
788                    {
789                        row_count = count;
790                    }
791                }
792                Message::ReadyForQuery(_) => {
793                    return Ok(row_count);
794                }
795                Message::ErrorResponse(body) => {
796                    return Err(self.consume_error(&body).await);
797                }
798                _ => {}
799            }
800        }
801    }
802
803    /// Cancels a COPY IN operation (async).
804    ///
805    /// # Errors
806    ///
807    /// Returns [`Error`] (I/O) if flushing the buffer or writing the
808    /// `CopyFail` frame fails, or [`Error`] (closed) if the server
809    /// drops the connection before returning `ReadyForQuery`.
810    pub async fn cancel_copy(&mut self, reason: &str) -> Result<()> {
811        self.flush().await?;
812
813        frontend::copy_fail(reason, &mut self.write_buf);
814        self.flush().await?;
815
816        loop {
817            let msg = self.read_message().await?;
818            match msg {
819                Message::ReadyForQuery(_) => {
820                    return Ok(());
821                }
822                Message::ErrorResponse(_) => {}
823                _ => {}
824            }
825        }
826    }
827
828    /// Executes a COPY ... TO STDOUT query and returns all output data (async).
829    ///
830    /// # Errors
831    ///
832    /// - Returns [`Error`] (connection) if the connection is unhealthy.
833    /// - Returns [`Error`] (server) when the server rejects the COPY TO
834    ///   STDOUT statement via `ErrorResponse`.
835    /// - Returns [`Error`] (I/O) / [`Error`] (closed) on transport
836    ///   read/write failure.
837    pub async fn copy_out(&mut self, query: &str) -> Result<Vec<u8>> {
838        self.ensure_healthy()?;
839        self.drain_pending_copy_cancel().await?;
840        frontend::query(query, &mut self.write_buf)?;
841        self.flush().await?;
842
843        let mut data = Vec::new();
844        let mut in_copy_out = false;
845
846        loop {
847            let msg = self.read_message().await?;
848            match msg {
849                Message::CopyOutResponse(_) => {
850                    in_copy_out = true;
851                }
852                Message::CopyData(body) if in_copy_out => {
853                    data.extend_from_slice(body.data());
854                }
855                Message::CopyDone => {
856                    in_copy_out = false;
857                }
858                Message::CommandComplete(_) => {}
859                Message::ReadyForQuery(_) => {
860                    return Ok(data);
861                }
862                Message::ErrorResponse(body) => {
863                    return Err(self.consume_error(&body).await);
864                }
865                _ => {}
866            }
867        }
868    }
869
870    /// Prepares a statement using the extended query protocol (async).
871    ///
872    /// # Errors
873    ///
874    /// - Returns [`Error`] (connection) if the connection is unhealthy.
875    /// - Returns [`Error`] (server) if the server rejects the `Parse`
876    ///   request (SQL syntax error, unknown type OIDs, etc.).
877    /// - Returns [`Error`] (I/O) on transport read/write failure.
878    pub async fn prepare(
879        &mut self,
880        name: &str,
881        query: &str,
882        param_types: &[crate::types::Oid],
883    ) -> Result<(Vec<crate::types::Oid>, Vec<super::statement::Column>)> {
884        use super::statement::{Column, ColumnFormat};
885
886        self.ensure_healthy()?;
887        self.drain_pending_copy_cancel().await?;
888
889        // Send Parse message
890        frontend::parse(name, query, param_types, &mut self.write_buf)?;
891
892        // Send Describe message for the statement
893        frontend::describe(b'S', name, &mut self.write_buf)?;
894
895        // Send Sync to get responses
896        frontend::sync(&mut self.write_buf);
897        self.flush().await?;
898
899        // Process responses
900        let mut parsed_params = Vec::new();
901        let mut parsed_columns = Vec::new();
902
903        loop {
904            let msg = self.read_message().await?;
905            match msg {
906                Message::ParseComplete => {}
907                Message::ParameterDescription(desc) => {
908                    for oid in desc.parameters().filter_map(std::result::Result::ok) {
909                        parsed_params.push(oid);
910                    }
911                }
912                Message::RowDescription(desc) => {
913                    for f in desc.fields().filter_map(std::result::Result::ok) {
914                        parsed_columns.push(Column::new(
915                            f.name().to_string(),
916                            f.type_oid(),
917                            f.type_modifier(),
918                            ColumnFormat::from_code(f.format()),
919                        ));
920                    }
921                }
922                Message::NoData => {}
923                Message::ReadyForQuery(_) => {
924                    break;
925                }
926                Message::ErrorResponse(body) => {
927                    return Err(self.consume_error(&body).await);
928                }
929                _ => {}
930            }
931        }
932
933        Ok((parsed_params, parsed_columns))
934    }
935
936    /// Executes a prepared statement with parameters (async), collecting all
937    /// rows.
938    ///
939    /// **Every parameter is bound as PostgreSQL binary.** Values whose
940    /// [`ParamFormat`] is [`ParamFormat::Text`] — a scaled `NUMERIC`, a
941    /// `geography` — will be rejected by the server, because the bytes
942    /// `ToSqlParam::encode_param` produced for them are text. That
943    /// combination is unreachable from `hyperdb-api`, which streams through
944    /// [`Self::start_execute_prepared_with_formats`] instead; if you are
945    /// calling this directly and need mixed formats, use
946    /// [`Self::execute_prepared_no_result_with_formats`] or the streaming
947    /// path.
948    ///
949    /// # Errors
950    ///
951    /// - Returns [`Error`] (connection) if the connection is unhealthy.
952    /// - Returns [`Error`] (server) if `Bind` / `Execute` fails on the
953    ///   server (parameter type mismatch, constraint violation, etc.).
954    /// - Returns [`Error`] (I/O) / [`Error`] (closed) on transport
955    ///   read/write failure.
956    /// - Propagates row-construction errors from
957    ///   `super::row::Row::new` if a `DataRow` cannot be decoded
958    ///   against the reported `RowDescription`.
959    pub async fn execute_prepared(
960        &mut self,
961        statement_name: &str,
962        params: &[Option<&[u8]>],
963        column_count: usize,
964    ) -> Result<Vec<super::row::Row>> {
965        use super::statement::Column;
966        use std::sync::Arc;
967
968        self.ensure_healthy()?;
969        // Prepared-statement execution writes Bind/Execute/Sync into the
970        // buffer and flushes at the end; a pending CopyFail would be
971        // flushed together with our bind bytes and corrupt the response
972        // stream. See `start_query_binary` for the full argument.
973        self.drain_pending_copy_cancel().await?;
974        // `&[]` means all-binary; see `bind_format_codes`. Infallible here.
975        let param_formats = bind_format_codes(&[], params.len())?;
976        let result_formats: Vec<i16> = vec![1; column_count];
977
978        frontend::bind(
979            "",
980            statement_name,
981            &param_formats,
982            params,
983            &result_formats,
984            &mut self.write_buf,
985        )?;
986
987        frontend::execute("", 0, &mut self.write_buf)?;
988        frontend::sync(&mut self.write_buf);
989        self.flush().await?;
990
991        let mut rows = Vec::new();
992        let mut columns: Option<Arc<Vec<Column>>> = None;
993
994        loop {
995            let msg = self.read_message().await?;
996            match msg {
997                Message::BindComplete => {}
998                Message::RowDescription(desc) => {
999                    let mut cols = Vec::new();
1000                    for f in desc.fields().filter_map(std::result::Result::ok) {
1001                        cols.push(Column::new(
1002                            f.name().to_string(),
1003                            f.type_oid(),
1004                            f.type_modifier(),
1005                            super::statement::ColumnFormat::from_code(f.format()),
1006                        ));
1007                    }
1008                    columns = Some(Arc::new(cols));
1009                }
1010                Message::DataRow(data) => {
1011                    if let Some(ref cols) = columns {
1012                        rows.push(super::row::Row::new(Arc::clone(cols), data)?);
1013                    }
1014                }
1015                Message::CommandComplete(_) => {}
1016                Message::EmptyQueryResponse => {}
1017                Message::ReadyForQuery(_) => {
1018                    break;
1019                }
1020                Message::ErrorResponse(body) => {
1021                    return Err(self.consume_error(&body).await);
1022                }
1023                _ => {}
1024            }
1025        }
1026
1027        Ok(rows)
1028    }
1029
1030    /// Executes a prepared statement that doesn't return rows (async).
1031    ///
1032    /// # Errors
1033    ///
1034    /// Same failure modes as [`Self::execute_prepared`] (excluding
1035    /// row-construction errors — this path never builds rows).
1036    pub async fn execute_prepared_no_result(
1037        &mut self,
1038        statement_name: &str,
1039        params: &[Option<&[u8]>],
1040    ) -> Result<u64> {
1041        self.execute_prepared_no_result_with_formats(statement_name, params, &[])
1042            .await
1043    }
1044
1045    /// Same as [`Self::execute_prepared_no_result`], but with a
1046    /// caller-chosen wire format per parameter.
1047    ///
1048    /// `param_formats` must be the same length as `params`, or empty to mean
1049    /// "every parameter is binary".
1050    ///
1051    /// # Errors
1052    ///
1053    /// - Returns [`Error`] (protocol) if `param_formats` is non-empty and its
1054    ///   length differs from `params`.
1055    /// - Otherwise the same failure modes as
1056    ///   [`Self::execute_prepared_no_result`].
1057    pub async fn execute_prepared_no_result_with_formats(
1058        &mut self,
1059        statement_name: &str,
1060        params: &[Option<&[u8]>],
1061        param_formats: &[ParamFormat],
1062    ) -> Result<u64> {
1063        self.ensure_healthy()?;
1064        // See `execute_prepared` and `start_query_binary` for why we must
1065        // drain any pending COPY cancel before writing new bytes.
1066        self.drain_pending_copy_cancel().await?;
1067        let param_format_codes = bind_format_codes(param_formats, params.len())?;
1068        let result_formats: Vec<i16> = vec![];
1069
1070        frontend::bind(
1071            "",
1072            statement_name,
1073            &param_format_codes,
1074            params,
1075            &result_formats,
1076            &mut self.write_buf,
1077        )?;
1078
1079        frontend::execute("", 0, &mut self.write_buf)?;
1080        frontend::sync(&mut self.write_buf);
1081        self.flush().await?;
1082
1083        let mut affected_rows = 0u64;
1084
1085        loop {
1086            let msg = self.read_message().await?;
1087            match msg {
1088                Message::BindComplete => {}
1089                Message::CommandComplete(body) => {
1090                    if let Ok(tag) = body.tag() {
1091                        // Parse formats like "INSERT 0 5", "UPDATE 10", "DELETE 3"
1092                        let parts: Vec<&str> = tag.split_whitespace().collect();
1093                        match parts.first() {
1094                            Some(&"INSERT") => {
1095                                if let Some(count) = parts.get(2) {
1096                                    affected_rows = count.parse().unwrap_or(0);
1097                                }
1098                            }
1099                            Some(&"UPDATE" | &"DELETE" | &"SELECT" | &"COPY") => {
1100                                if let Some(count) = parts.get(1) {
1101                                    affected_rows = count.parse().unwrap_or(0);
1102                                }
1103                            }
1104                            _ => {}
1105                        }
1106                    }
1107                }
1108                Message::EmptyQueryResponse => {}
1109                Message::ReadyForQuery(_) => {
1110                    break;
1111                }
1112                Message::ErrorResponse(body) => {
1113                    return Err(self.consume_error(&body).await);
1114                }
1115                _ => {}
1116            }
1117        }
1118
1119        Ok(affected_rows)
1120    }
1121
1122    /// Closes a prepared statement (async).
1123    ///
1124    /// # Errors
1125    ///
1126    /// - Returns [`Error`] (connection) if the connection is unhealthy.
1127    /// - Returns [`Error`] (server) if the server reports an `ErrorResponse`
1128    ///   during `Close`/`Sync`.
1129    /// - Returns [`Error`] (I/O) / [`Error`] (closed) on transport
1130    ///   read/write failure.
1131    /// - Propagates any error from [`Self::drain_pending_copy_cancel`].
1132    pub async fn close_statement(&mut self, statement_name: &str) -> Result<()> {
1133        self.ensure_healthy()?;
1134        // Close + Sync get flushed together; a pending CopyFail would
1135        // share the flush and corrupt the response stream. See
1136        // `start_query_binary` for the full argument.
1137        self.drain_pending_copy_cancel().await?;
1138        frontend::close(b'S', statement_name, &mut self.write_buf)?;
1139        frontend::sync(&mut self.write_buf);
1140        self.flush().await?;
1141
1142        loop {
1143            let msg = self.read_message().await?;
1144            match msg {
1145                Message::CloseComplete => {}
1146                Message::ReadyForQuery(_) => {
1147                    return Ok(());
1148                }
1149                Message::ErrorResponse(body) => {
1150                    return Err(self.consume_error(&body).await);
1151                }
1152                _ => {}
1153            }
1154        }
1155    }
1156}