Skip to main content

hyperdb_api_core/client/
connection.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Low-level synchronous connection handling over the `PostgreSQL` wire protocol.
5//!
6//! [`RawConnection`] provides the message-level interface to a Hyper server:
7//! startup/authentication handshake, simple and extended query execution, and
8//! the COPY data path. It is generic over the transport stream `S` (TCP,
9//! Unix domain socket, named pipe, or TLS-wrapped variants).
10//!
11//! # Wire Protocol Overview
12//!
13//! Communication follows the `PostgreSQL` v3 message protocol. Each message has a
14//! 1-byte type tag, a 4-byte length (including itself), and a variable-length
15//! body. The connection maintains separate read and write `BytesMut` buffers to
16//! amortize syscall overhead.
17//!
18//! ## Query Protocols
19//!
20//! - **Simple Query**: A single `Query('Q')` message; the server responds with
21//!   `RowDescription`, zero or more `DataRow`, `CommandComplete`, and
22//!   `ReadyForQuery`. Results are in text format.
23//!
24//! - **Extended Query (`HyperBinary`)**: `Parse` / `Bind` / `Describe` /
25//!   `Execute` / `Sync` sequence requesting format code 2 (`HyperBinary`,
26//!   little-endian binary). Results are zero-copy-friendly `DataRow` messages.
27//!   Used by [`Client::query_fast`](crate::client::Client::query_fast) and
28//!   [`Client::query_streaming`](crate::client::Client::query_streaming).
29//!
30//! ## Connection Health
31//!
32//! A `desynchronized` flag tracks whether the wire protocol has fallen out of
33//! sync (e.g., a bounded drain exceeded its budget). Once set, all subsequent
34//! operations fast-fail. The only recovery is to drop the connection and
35//! open a new one. Pool layers should check [`RawConnection::is_healthy`]
36//! during recycle.
37
38use std::io::{Read, Write};
39
40use bytes::BytesMut;
41use tracing::{debug, info, trace, warn};
42
43use crate::protocol::message::{backend::Message, frontend};
44
45use super::auth::{self, AuthState};
46use super::error::{Error, Result};
47use super::statement::{ParamFormat, bind_format_codes};
48
49/// Maximum number of messages [`RawConnection::consume_error`] (and its
50/// async sibling) will read while draining the tail of a failed request.
51///
52/// A well-behaved server emits only a handful of messages after
53/// `ErrorResponse` before `ReadyForQuery` (typically just the error +
54/// `Z`, with the occasional `NoticeResponse` interleaved). This cap is
55/// therefore orders of magnitude above anything a legitimate error path
56/// produces; it exists purely as a defensive safety valve against broken
57/// server implementations and stalled network paths. When exceeded, the
58/// drain logs a `tracing::warn!` and returns — the next operation on the
59/// connection will see the resulting desync and trigger a reconnect.
60///
61/// See [`RawConnection::consume_error`] for the full rationale.
62pub const POST_ERROR_DRAIN_CAP: usize = 1024;
63
64/// A raw connection to a Hyper server.
65///
66/// This handles the low-level protocol communication, including:
67/// - Message framing (reading/writing `PostgreSQL` wire protocol messages)
68/// - Authentication handshake
69/// - Query execution (simple and extended query protocols)
70/// - COPY protocol support
71///
72/// The connection is generic over the stream type `S`, allowing it to work
73/// with different transport mechanisms (TCP, TLS, etc.) as long as they
74/// implement `Read + Write`.
75///
76/// # Buffering
77///
78/// The connection maintains separate read and write buffers for efficient
79/// I/O. Messages are buffered before being sent, and incoming data is
80/// buffered until complete messages can be parsed.
81#[derive(Debug)]
82pub struct RawConnection<S> {
83    /// The underlying I/O stream.
84    stream: S,
85    /// Buffer for reading incoming messages from the server.
86    read_buf: BytesMut,
87    /// Buffer for writing outgoing messages to the server.
88    write_buf: BytesMut,
89    /// Backend process ID (for cancel requests).
90    process_id: i32,
91    /// Secret key for authenticating cancel requests.
92    secret_key: i32,
93    /// Server parameters received during startup (e.g., `server_version`, `session_identifier`).
94    server_params: std::collections::HashMap<String, String>,
95    /// Sticky flag set when the wire protocol has fallen out of sync with
96    /// the server (e.g. a bounded drain exhausted its budget before seeing
97    /// `ReadyForQuery`). Once set it is never cleared — the only valid
98    /// recovery is to discard this connection and open a new one.
99    ///
100    /// Every public method that initiates a new server request checks
101    /// this flag via [`Self::ensure_healthy`] and fast-fails with a
102    /// clear error instead of sending bytes into a known-poisoned
103    /// channel. Pool layers should call [`Self::is_healthy`] during
104    /// recycle to skip the health-probe roundtrip for connections that
105    /// are already known-bad.
106    desynchronized: bool,
107}
108
109impl<S> RawConnection<S>
110where
111    S: Read + Write,
112{
113    /// Creates a new raw connection from a stream.
114    ///
115    /// Initializes read and write buffers with default capacity (64 KB each).
116    /// The connection is not yet authenticated - call `startup()` to begin
117    /// the connection handshake.
118    ///
119    /// # Arguments
120    ///
121    /// * `stream` - The I/O stream (must implement `Read + Write`)
122    pub fn new(stream: S) -> Self {
123        RawConnection {
124            stream,
125            read_buf: BytesMut::with_capacity(64 * 1024),
126            write_buf: BytesMut::with_capacity(64 * 1024),
127            process_id: 0,
128            secret_key: 0,
129            server_params: std::collections::HashMap::new(),
130            desynchronized: false,
131        }
132    }
133
134    /// Returns `true` if this connection is still in a known-good state
135    /// and safe to use for new requests.
136    ///
137    /// Once the wire protocol falls out of sync with the server (see the
138    /// `desynchronized` field on [`RawConnection`]), this returns `false`
139    /// permanently — the only recovery is to drop this connection and
140    /// open a new one. Pool implementations should consult this before
141    /// running a recycle health probe to avoid spending a roundtrip on a
142    /// connection that is already known to be bad.
143    pub fn is_healthy(&self) -> bool {
144        !self.desynchronized
145    }
146
147    /// Fast-fails with an explicit [`Error::Connection`] error if the
148    /// wire has fallen out of sync with the server, before any bytes are
149    /// written to the stream. Called from the entry point of every public
150    /// method that initiates a new server request — simple queries,
151    /// streaming queries, prepared statement execution, COPY in/out,
152    /// etc. — so that a desynchronized connection produces a clear
153    /// "connection unusable" diagnostic at the API boundary rather than a
154    /// cryptic protocol-parse error deep inside the message loop of the
155    /// *next* unrelated operation.
156    pub(crate) fn ensure_healthy(&self) -> Result<()> {
157        if self.desynchronized {
158            return Err(Error::connection(
159                "connection is desynchronized from the server and cannot be reused; \
160                 discard it and open a new one",
161            ));
162        }
163        Ok(())
164    }
165
166    /// Reserves capacity in the write buffer to avoid reallocations.
167    ///
168    /// Call this before bulk operations to pre-allocate buffer space.
169    /// This is useful for high-throughput scenarios where buffer growth
170    /// would cause performance overhead.
171    pub fn reserve_write_buffer(&mut self, additional: usize) {
172        self.write_buf.reserve(additional);
173    }
174
175    /// Returns the process ID assigned by the server.
176    pub fn process_id(&self) -> i32 {
177        self.process_id
178    }
179
180    /// Returns the secret key for cancel requests.
181    pub fn secret_key(&self) -> i32 {
182        self.secret_key
183    }
184
185    /// Returns a reference to the underlying stream.
186    pub fn stream(&self) -> &S {
187        &self.stream
188    }
189
190    /// Returns a mutable reference to the underlying stream.
191    pub fn stream_mut(&mut self) -> &mut S {
192        &mut self.stream
193    }
194
195    /// Returns a server parameter value by name.
196    ///
197    /// Server parameters are sent by the server during connection startup.
198    /// Common parameters include:
199    /// - `server_version` - The server version string
200    /// - `server_encoding` - The server's character encoding
201    /// - `client_encoding` - The client's character encoding
202    pub fn parameter_status(&self, name: &str) -> Option<&str> {
203        self.server_params
204            .get(name)
205            .map(std::string::String::as_str)
206    }
207
208    /// Sends a startup message and performs initial handshake.
209    ///
210    /// # Errors
211    ///
212    /// - Returns [`Error`] (auth) when the server requests an
213    ///   auth method (cleartext, MD5, SASL) and no password is supplied,
214    ///   when the offered SASL mechanisms exclude SCRAM-SHA-256, or when
215    ///   SCRAM state is missing at the SASL-continue / SASL-final step.
216    /// - Returns [`Error`] (server) when the server sends an `ErrorResponse`
217    ///   during startup (for example, unknown user or database).
218    /// - Returns [`Error`] (protocol) if a message arrives out of the
219    ///   expected startup sequence.
220    /// - Returns [`Error`] (I/O) on wire-protocol read/write failure.
221    pub fn startup(&mut self, params: &[(&str, &str)], password: Option<&str>) -> Result<()> {
222        // Send startup message
223        frontend::startup_message(params, &mut self.write_buf)?;
224        self.flush()?;
225
226        // Handle authentication
227        let mut auth_state: Option<AuthState> = None;
228
229        loop {
230            let msg = self.read_message()?;
231            match msg {
232                Message::AuthenticationOk => {
233                    info!(target: "hyperdb_api", "connection-auth-success");
234                }
235                Message::AuthenticationCleartextPassword => {
236                    debug!(target: "hyperdb_api", method = "cleartext", "connection-auth-method");
237                    let password = password.ok_or_else(|| {
238                        Error::authentication(
239                            "server requested cleartext password but none provided",
240                        )
241                    })?;
242                    frontend::password_message(password, &mut self.write_buf)?;
243                    self.flush()?;
244                }
245                Message::AuthenticationMd5Password(body) => {
246                    debug!(target: "hyperdb_api", method = "MD5", "connection-auth-method");
247                    let password = password.ok_or_else(|| {
248                        Error::authentication("server requested MD5 password but none provided")
249                    })?;
250                    let user = params
251                        .iter()
252                        .find(|(k, _)| *k == "user")
253                        .map_or("", |(_, v)| *v);
254
255                    let md5_response = auth::compute_md5_password(user, password, &body.salt());
256                    frontend::password_message(&md5_response, &mut self.write_buf)?;
257                    self.flush()?;
258                }
259                Message::AuthenticationSasl(body) => {
260                    debug!(target: "hyperdb_api", method = "SCRAM-SHA-256", "connection-auth-method");
261                    let password = password.ok_or_else(|| {
262                        Error::authentication(
263                            "server requested SASL authentication but no password provided",
264                        )
265                    })?;
266
267                    // Check for SCRAM-SHA-256
268                    let mechanisms: Vec<&str> = body.mechanisms().collect();
269                    if !mechanisms.contains(&"SCRAM-SHA-256") {
270                        return Err(Error::authentication(format!(
271                            "server offered unsupported SASL mechanisms: {mechanisms:?}"
272                        )));
273                    }
274
275                    // Start SCRAM-SHA-256 exchange
276                    let (state, client_first) = auth::scram_client_first(password)?;
277                    auth_state = Some(state);
278
279                    frontend::sasl_initial_response(
280                        "SCRAM-SHA-256",
281                        &client_first,
282                        &mut self.write_buf,
283                    )?;
284                    self.flush()?;
285                }
286                Message::AuthenticationSaslContinue(body) => {
287                    let state = auth_state.take().ok_or_else(|| {
288                        Error::authentication("received SASL continue without initial state")
289                    })?;
290
291                    let server_first = body.data();
292                    let (new_state, client_final) = auth::scram_client_final(state, server_first)?;
293                    auth_state = Some(new_state);
294
295                    frontend::sasl_response(&client_final, &mut self.write_buf)?;
296                    self.flush()?;
297                }
298                Message::AuthenticationSaslFinal(body) => {
299                    let state = auth_state.take().ok_or_else(|| {
300                        Error::authentication("received SASL final without state")
301                    })?;
302
303                    // Verify server signature
304                    auth::scram_verify_server(state, body.data())?;
305                }
306                Message::BackendKeyData(data) => {
307                    self.process_id = data.process_id();
308                    self.secret_key = data.secret_key();
309                }
310                Message::ParameterStatus(body) => {
311                    // Store server parameters
312                    if let (Ok(name), Ok(value)) = (body.name(), body.value()) {
313                        self.server_params
314                            .insert(name.to_string(), value.to_string());
315                    }
316                }
317                Message::ReadyForQuery(_) => {
318                    // Connection is ready
319                    return Ok(());
320                }
321                Message::ErrorResponse(body) => {
322                    // Startup typically fails the connection outright, but we
323                    // still drain in case the server sent any trailing
324                    // messages so parity with the async startup path is
325                    // preserved. The drain is bounded implicitly: a post-
326                    // startup-error server either sends ReadyForQuery or
327                    // closes immediately, so drain_until_ready returns fast.
328                    return Err(self.consume_error(&body));
329                }
330                _ => {
331                    return Err(Error::protocol("unexpected message during startup"));
332                }
333            }
334        }
335    }
336
337    /// Sends a simple query and returns all messages until `ReadyForQuery`.
338    ///
339    /// # Errors
340    ///
341    /// - Returns [`Error`] (server) when the server sends an `ErrorResponse`
342    ///   (SQL error, constraint violation, etc.).
343    /// - Returns [`Error`] (I/O) on transport read/write failure.
344    /// - Returns [`Error`] (closed) if the server closes the connection
345    ///   mid-query.
346    /// - Returns [`Error`] (connection) if the connection has already
347    ///   been marked unhealthy by a prior failure.
348    pub fn simple_query(&mut self, query: &str) -> Result<Vec<Message>> {
349        self.ensure_healthy()?;
350        frontend::query(query, &mut self.write_buf)?;
351        self.flush()?;
352
353        let mut messages = Vec::new();
354        loop {
355            let msg = self.read_message()?;
356            match &msg {
357                Message::ReadyForQuery(_) => {
358                    messages.push(msg);
359                    return Ok(messages);
360                }
361                Message::ErrorResponse(body) => {
362                    return Err(self.consume_error(body));
363                }
364                _ => {
365                    messages.push(msg);
366                }
367            }
368        }
369    }
370
371    /// Sends a query using extended protocol with binary format results.
372    ///
373    /// This uses the `PostgreSQL` extended query protocol (Parse/Bind/Execute/Sync)
374    /// with `HyperBinary` format (format code 2) for maximum performance.
375    ///
376    /// Returns all messages until `ReadyForQuery`.
377    ///
378    /// # Errors
379    ///
380    /// Same failure modes as [`Self::simple_query`] — server-side SQL
381    /// errors surface as [`Error`] (server), transport failures as
382    /// [`Error`] (I/O) / [`Error`] (closed), and an unhealthy prior state
383    /// as [`Error`] (connection).
384    pub fn query_binary(&mut self, query: &str) -> Result<Vec<Message>> {
385        self.ensure_healthy()?;
386        // HyperBinary format code
387        const HYPER_BINARY_FORMAT: i16 = 2;
388
389        // Parse: prepare an unnamed statement
390        frontend::parse("", query, &[], &mut self.write_buf)?;
391
392        // Bind: bind unnamed portal with HyperBinary result format
393        // Empty arrays for param_formats and params (no parameters)
394        // Single result_format of 2 (HyperBinary) applies to all columns
395        frontend::bind(
396            "",
397            "",
398            &[],
399            &[],
400            &[HYPER_BINARY_FORMAT],
401            &mut self.write_buf,
402        )?;
403
404        // Describe: get column metadata (optional but useful)
405        frontend::describe(b'P', "", &mut self.write_buf)?;
406
407        // Execute: run the unnamed portal with no row limit
408        frontend::execute("", 0, &mut self.write_buf)?;
409
410        // Sync: end the extended query sequence
411        frontend::sync(&mut self.write_buf);
412
413        self.flush()?;
414
415        let mut messages = Vec::new();
416        loop {
417            let msg = self.read_message()?;
418            match &msg {
419                Message::ReadyForQuery(_) => {
420                    messages.push(msg);
421                    return Ok(messages);
422                }
423                Message::ErrorResponse(body) => {
424                    return Err(self.consume_error(body));
425                }
426                _ => {
427                    messages.push(msg);
428                }
429            }
430        }
431    }
432
433    /// Starts a binary query but leaves result consumption to the caller.
434    ///
435    /// This is useful for streaming scenarios where you want to pull messages
436    /// incrementally instead of materializing the full result set up front.
437    ///
438    /// # Errors
439    ///
440    /// - Returns [`Error`] (connection) if the connection has been
441    ///   marked unhealthy.
442    /// - Returns [`Error`] (I/O) if writing the Parse/Bind/Execute/Sync
443    ///   sequence to the transport fails.
444    pub fn start_query_binary(&mut self, query: &str) -> Result<()> {
445        self.ensure_healthy()?;
446        const HYPER_BINARY_FORMAT: i16 = 2;
447
448        frontend::parse("", query, &[], &mut self.write_buf)?;
449        frontend::bind(
450            "",
451            "",
452            &[],
453            &[],
454            &[HYPER_BINARY_FORMAT],
455            &mut self.write_buf,
456        )?;
457        frontend::describe(b'P', "", &mut self.write_buf)?;
458        frontend::execute("", 0, &mut self.write_buf)?;
459        frontend::sync(&mut self.write_buf);
460
461        self.flush()
462    }
463
464    /// Starts a simple query but leaves result consumption to the caller.
465    ///
466    /// This is useful for streaming scenarios where you want to pull messages
467    /// incrementally instead of materializing the full result set up front.
468    ///
469    /// # Errors
470    ///
471    /// Same failure modes as [`Self::start_query_binary`] —
472    /// [`Error`] (connection) for unhealthy state, [`Error`] (I/O) for
473    /// transport failure.
474    pub fn start_simple_query(&mut self, query: &str) -> Result<()> {
475        self.ensure_healthy()?;
476        frontend::query(query, &mut self.write_buf)?;
477        self.flush()
478    }
479
480    /// Starts an **execute** of a prepared statement but leaves result
481    /// consumption to the caller.
482    ///
483    /// Sends `Bind` + `Execute(unnamed_portal, 0)` + `Sync`, then
484    /// returns. The caller drives a message loop that reads
485    /// `BindComplete`, any `DataRow`s, then `CommandComplete` +
486    /// `ReadyForQuery` — the same shape used by
487    /// [`Self::start_query_binary`].
488    ///
489    /// Format codes (`PostgreSQL` wire protocol):
490    /// - **Parameters**: format `1` (standard PG binary, big-endian). The
491    ///   caller is responsible for supplying parameter bytes in BE. Use
492    ///   [`Self::start_execute_prepared_with_formats`] to send some or all
493    ///   parameters as text instead.
494    /// - **Results**: format `2` (`HyperBinary`, little-endian). Hyper
495    ///   supports this as a separate protocol extension; the row
496    ///   decoders in [`super::row::StreamRow`] and the hyperdb-api `Row`
497    ///   type all expect LE, so requesting LE at Bind time avoids an
498    ///   extra conversion pass.
499    ///
500    /// `max_rows = 0` means "send all rows" — we pace on the client side
501    /// by reading `DataRows` in chunks from the read buffer.
502    ///
503    /// # Errors
504    ///
505    /// - Returns [`Error`] (connection) if the connection has been
506    ///   marked unhealthy.
507    /// - Returns [`Error`] (I/O) if writing the Bind/Execute/Sync
508    ///   sequence to the transport fails.
509    pub fn start_execute_prepared(
510        &mut self,
511        statement_name: &str,
512        params: &[Option<&[u8]>],
513        column_count: usize,
514    ) -> Result<()> {
515        self.start_execute_prepared_with_formats(statement_name, params, &[], column_count)
516    }
517
518    /// Same as [`Self::start_execute_prepared`], but with a caller-chosen
519    /// wire format per parameter.
520    ///
521    /// `param_formats` is either empty — meaning **every parameter is
522    /// binary**, exactly like [`Self::start_execute_prepared`] — or the same
523    /// length as `params`. Hyper accepts a mixed format-code array, so binary
524    /// stays the fast path and only the parameters that need it — scaled
525    /// `NUMERIC`, `geography` — degrade to text. See [`ParamFormat`] for why
526    /// those two types have no binary input function.
527    ///
528    /// # Errors
529    ///
530    /// - Returns [`Error`] (protocol) if `param_formats` is non-empty and its
531    ///   length differs from `params`.
532    /// - Otherwise the same failure modes as
533    ///   [`Self::start_execute_prepared`].
534    pub fn start_execute_prepared_with_formats(
535        &mut self,
536        statement_name: &str,
537        params: &[Option<&[u8]>],
538        param_formats: &[ParamFormat],
539        column_count: usize,
540    ) -> Result<()> {
541        let codes = bind_format_codes(param_formats, params.len())?;
542        self.bind_execute_sync(statement_name, params, &codes, column_count)
543    }
544
545    fn bind_execute_sync(
546        &mut self,
547        statement_name: &str,
548        params: &[Option<&[u8]>],
549        param_format_codes: &[i16],
550        column_count: usize,
551    ) -> Result<()> {
552        self.ensure_healthy()?;
553
554        const HYPER_BINARY_FORMAT: i16 = 2;
555        let result_formats: Vec<i16> = vec![HYPER_BINARY_FORMAT; column_count];
556
557        frontend::bind(
558            "", // unnamed portal
559            statement_name,
560            param_format_codes,
561            params,
562            &result_formats,
563            &mut self.write_buf,
564        )?;
565        frontend::execute("", 0, &mut self.write_buf)?;
566        frontend::sync(&mut self.write_buf);
567
568        self.flush()
569    }
570
571    /// Reads a single message from the server.
572    ///
573    /// # Errors
574    ///
575    /// - Returns [`Error`] (I/O) if reading from the transport fails or
576    ///   if [`Message::parse`] reports a malformed frame.
577    /// - Returns [`Error`] (closed) when the transport reaches EOF
578    ///   (server closed the connection).
579    pub fn read_message(&mut self) -> Result<Message> {
580        loop {
581            if let Some(msg) = Message::parse(&mut self.read_buf).map_err(Error::from_io)? {
582                return Ok(msg);
583            }
584
585            // Need more data — read directly into the spare capacity of
586            // `read_buf`, no temporary buffer or `extend_from_slice` memcpy.
587            //
588            // The 64 KiB read-window matches the typical TCP loopback
589            // segment size and the default DataRow streaming chunk. On
590            // Windows TCP loopback the per-`WSARecv` overhead is several
591            // times higher than Linux/macOS `recv`, so a tight ceiling here
592            // dominates wall time on long scans. The previous 8 KiB ceiling
593            // forced an 8× syscall amplification on this path.
594            //
595            // Implementation: `resize` extends the buffer with zeroed bytes
596            // (single memset, ~50 GB/s on modern CPUs), `read` writes into
597            // the new tail, then `truncate` shrinks to the actual byte
598            // count. This is safe Rust and results in exactly one memset
599            // per syscall — no heap alloc, no extra memcpy.
600            let prev_len = self.read_buf.len();
601            self.read_buf.resize(prev_len + 64 * 1024, 0);
602            let n = self.stream.read(&mut self.read_buf[prev_len..])?;
603            if n == 0 {
604                self.read_buf.truncate(prev_len);
605                warn!(target: "hyperdb_api", "connection-closed");
606                return Err(Error::closed("connection closed"));
607            }
608            self.read_buf.truncate(prev_len + n);
609        }
610    }
611
612    /// Drains messages from the server until a [`Message::ReadyForQuery`] is
613    /// seen, discarding them. Call this after receiving an
614    /// [`Message::ErrorResponse`] to stay in sync with the wire protocol.
615    ///
616    /// Per the `PostgreSQL` wire protocol, every query (simple or extended) ends
617    /// with `ReadyForQuery`, even if the statement failed. Without this drain,
618    /// the `ReadyForQuery` (and any other trailing messages) remain in the
619    /// read buffer and get consumed by the next operation's response parser,
620    /// which misinterprets them — classic wire desync.
621    ///
622    /// This is the **unbounded** variant, safe to use in the standard
623    /// error path where the server has already sent `ErrorResponse` and
624    /// `ReadyForQuery` is guaranteed to arrive within a few messages. In
625    /// exceptional cases where the drain might take arbitrarily long —
626    /// most notably the `Drop` path for a streaming result that the caller
627    /// abandoned mid-way — prefer
628    /// [`drain_until_ready_bounded`](Self::drain_until_ready_bounded) to
629    /// avoid blocking indefinitely on an unresponsive server.
630    ///
631    /// Drain errors (connection already closed, I/O failure mid-drain) are
632    /// logged via `tracing::warn!` and then swallowed. The caller's original
633    /// error is more informative to surface, and a dead connection will be
634    /// reported on the next real operation anyway.
635    pub fn drain_until_ready(&mut self) {
636        let _ = self.drain_until_ready_bounded(usize::MAX);
637    }
638
639    /// Bounded version of [`drain_until_ready`](Self::drain_until_ready) that
640    /// stops after reading at most `max_messages` messages. Returns `true`
641    /// when `ReadyForQuery` was observed within that budget; `false` if the
642    /// budget was exhausted first or an I/O error occurred before reaching it.
643    ///
644    /// # Why we do not send `Sync` before draining
645    ///
646    /// A natural question is whether to send a `Sync` message first to prompt
647    /// the server to emit `ReadyForQuery` sooner. The answer for Hyper is
648    /// **no** — it would actively corrupt the next query.
649    ///
650    /// Per the Hyper server state machine (see `LibpqConnection::handleSync`
651    /// and `handleQueryDone`), every query — simple or extended — already
652    /// ends with exactly one `ReadyForQuery` emission. After an error or
653    /// normal completion the server returns to its main loop. If we then
654    /// send a `Sync`, `handleSync` would emit an **additional**
655    /// `ReadyForQuery` that no current operation is reading, and the next
656    /// query's response parser would consume that stale `ReadyForQuery`
657    /// as its own terminator — the symptom is that query returning an
658    /// empty result with "Query returned no rows".
659    ///
660    /// For the abandoned-stream case (a long-running query that the client
661    /// stopped reading), `Sync` also does not help: Hyper processes the
662    /// incoming byte stream in order, so `Sync` is only handled *after*
663    /// the in-flight `Execute` finishes emitting all its `DataRow`s plus
664    /// its own `CommandComplete` and `ReadyForQuery`. By that point the
665    /// drain has already reached `ReadyForQuery`, and the `Sync` produces
666    /// the same extra `ReadyForQuery` contamination described above.
667    ///
668    /// The canonical way to abort a running query is to open a *separate*
669    /// connection and send `CancelRequest` with the original connection's
670    /// process id and secret. That is exactly what
671    /// [`QueryStream`](super::client::QueryStream)'s `Drop` impl does
672    /// (via the [`Cancellable`](super::cancel::Cancellable) trait)
673    /// before calling this bounded drain. Cancel-then-drain converges on
674    /// `ReadyForQuery` within a handful of messages because the server
675    /// stops producing new `DataRow`s once it observes the cancel.
676    ///
677    /// # Poisoned connections
678    ///
679    /// When this returns `false` the connection is in an indeterminate
680    /// state. Callers should treat it as poisoned and not return it to a
681    /// connection pool — the next operation will see residual bytes from
682    /// whatever was still streaming. The bounded variant exists precisely
683    /// to prevent indefinite blocking in contexts like `Drop` impls where
684    /// we don't own the thread's time and can't afford to wait for a
685    /// multi-million-row query result to finish before returning from a
686    /// destructor.
687    ///
688    /// All drain errors are logged via `tracing::warn!` so state-related
689    /// issues are observable in logs even though they don't interrupt the
690    /// caller's control flow.
691    pub fn drain_until_ready_bounded(&mut self, max_messages: usize) -> bool {
692        for i in 0..max_messages {
693            match self.read_message() {
694                Ok(Message::ReadyForQuery(_)) => return true,
695                Ok(_) => {}
696                Err(e) => {
697                    warn!(
698                        target: "hyperdb_api_core::client",
699                        error = %e,
700                        messages_read = i,
701                        "drain_until_ready: read error mid-drain (likely closed connection); \
702                         connection marked desynchronized",
703                    );
704                    // Whether the underlying error is a closed socket, a
705                    // partial read, or a corrupt frame, any subsequent
706                    // `read_message` on this connection is operating on
707                    // unknown state. Mark it so pool layers and upper APIs
708                    // can short-circuit instead of piling another failed
709                    // operation on top.
710                    self.desynchronized = true;
711                    return false;
712                }
713            }
714        }
715        warn!(
716            target: "hyperdb_api_core::client",
717            max_messages,
718            "drain_until_ready_bounded: exhausted budget without seeing ReadyForQuery; \
719             connection marked desynchronized and should not be reused",
720        );
721        // Budget exhausted — residual messages still on the wire. The
722        // next read on this connection will almost certainly misparse
723        // them as belonging to an unrelated operation. Mark it so the
724        // failure surfaces at a well-defined API boundary instead.
725        self.desynchronized = true;
726        false
727    }
728
729    /// Convenience: parse a server [`Message::ErrorResponse`] body into an
730    /// [`Error`] and drain the rest of the response through the trailing
731    /// [`Message::ReadyForQuery`] so the connection is safe to reuse.
732    ///
733    /// Callers should almost always prefer this over calling
734    /// [`drain_until_ready`](Self::drain_until_ready) or
735    /// [`drain_until_ready_bounded`](Self::drain_until_ready_bounded) by
736    /// hand, because forgetting the drain is exactly the bug it exists to
737    /// prevent.
738    ///
739    /// # Drain budget
740    ///
741    /// Uses a bounded drain with a [`POST_ERROR_DRAIN_CAP`]-message budget
742    /// rather than the unbounded [`drain_until_ready`](Self::drain_until_ready).
743    /// A well-behaved server emits only a handful of messages after
744    /// `ErrorResponse` before `ReadyForQuery` — typically just the error
745    /// itself plus the `Z`, occasionally with a few `NoticeResponse`
746    /// messages interleaved — so the cap is orders of magnitude above
747    /// anything a legitimate error path produces. The cap exists purely
748    /// as a defensive safety valve against pathological server behavior
749    /// (a broken backend that never emits `ReadyForQuery`) and
750    /// misbehaved network paths (stalled reads that would otherwise hang
751    /// the caller indefinitely, particularly visible in async contexts).
752    ///
753    /// If the cap is exceeded, `drain_until_ready_bounded` logs a
754    /// `tracing::warn!` and marks the connection desynchronized; the
755    /// next operation on it will surface a transport-level failure and
756    /// trigger reconnect higher up. That is strictly better than
757    /// blocking forever with no observable symptom.
758    ///
759    /// # Example
760    ///
761    /// ```ignore
762    /// match msg {
763    ///     Message::ErrorResponse(body) => {
764    ///         return Err(self.consume_error(&body));
765    ///     }
766    ///     // ...
767    /// }
768    /// ```
769    pub fn consume_error(
770        &mut self,
771        body: &crate::protocol::message::backend::ErrorResponseBody,
772    ) -> Error {
773        let err = parse_error_response(body);
774        let _ = self.drain_until_ready_bounded(POST_ERROR_DRAIN_CAP);
775        err
776    }
777
778    /// Flushes the write buffer to the server.
779    ///
780    /// # Errors
781    ///
782    /// Returns [`Error`] (I/O) if writing the buffered bytes or flushing
783    /// the underlying transport fails.
784    pub fn flush(&mut self) -> Result<()> {
785        if !self.write_buf.is_empty() {
786            self.stream.write_all(&self.write_buf)?;
787            self.stream.flush()?;
788            self.write_buf.clear();
789        }
790        Ok(())
791    }
792
793    /// Sends a terminate message and closes the connection.
794    ///
795    /// # Errors
796    ///
797    /// Returns [`Error`] (I/O) if writing the `Terminate` frame or
798    /// flushing the transport fails.
799    pub fn terminate(&mut self) -> Result<()> {
800        frontend::terminate(&mut self.write_buf);
801        self.flush()
802    }
803
804    /// Returns a mutable reference to the write buffer.
805    pub fn write_buf(&mut self) -> &mut BytesMut {
806        &mut self.write_buf
807    }
808
809    /// Initiates a COPY IN operation with `HyperBinary` format.
810    ///
811    /// This sends a COPY ... FROM STDIN query and waits for `CopyInResponse`.
812    /// After this returns successfully, the caller should send data using
813    /// `send_copy_data` and then call `finish_copy` or `cancel_copy`.
814    ///
815    /// # Errors
816    ///
817    /// Same failure modes as [`Self::start_copy_in_with_format`].
818    pub fn start_copy_in(&mut self, table_name: &str, columns: &[&str]) -> Result<()> {
819        self.start_copy_in_with_format(table_name, columns, "HYPERBINARY")
820    }
821
822    /// Initiates a COPY IN operation with a specified format.
823    ///
824    /// This sends a COPY ... FROM STDIN query and waits for `CopyInResponse`.
825    /// After this returns successfully, the caller should send data using
826    /// `send_copy_data` and then call `finish_copy` or `cancel_copy`.
827    ///
828    /// # Arguments
829    ///
830    /// * `table_name` - The target table name (should be properly quoted if needed)
831    /// * `columns` - Column names to insert into
832    /// * `format` - The data format string: "HYPERBINARY" or "ARROWSTREAM"
833    ///
834    /// # Example
835    ///
836    /// ```no_run
837    /// # use hyperdb_api_core::client::connection::RawConnection;
838    /// # use std::net::TcpStream;
839    /// # fn example(conn: &mut RawConnection<TcpStream>) -> hyperdb_api_core::client::Result<()> {
840    /// // For HyperBinary format (default)
841    /// conn.start_copy_in_with_format("my_table", &["col1", "col2"], "HYPERBINARY")?;
842    ///
843    /// // For Arrow IPC stream format
844    /// conn.start_copy_in_with_format("my_table", &["col1", "col2"], "ARROWSTREAM")?;
845    /// # Ok(())
846    /// # }
847    /// ```
848    ///
849    /// # Errors
850    ///
851    /// - Returns [`Error`] (connection) if the connection has been
852    ///   marked unhealthy by a prior failure.
853    /// - Returns [`Error`] (server) if the server rejects the generated
854    ///   `COPY ... FROM STDIN` statement (missing table, column
855    ///   mismatch, etc.).
856    /// - Returns [`Error`] (I/O) on wire-protocol I/O failure.
857    pub fn start_copy_in_with_format(
858        &mut self,
859        table_name: &str,
860        columns: &[&str],
861        format: &str,
862    ) -> Result<()> {
863        self.ensure_healthy()?;
864        // Build COPY command with specified format
865        let column_list = if columns.is_empty() {
866            String::new()
867        } else {
868            format!(
869                " ({})",
870                columns
871                    .iter()
872                    .map(|c| format!("\"{}\"", c.replace('"', "\"\"")))
873                    .collect::<Vec<_>>()
874                    .join(", ")
875            )
876        };
877
878        let query = format!("COPY {table_name}{column_list} FROM STDIN WITH (FORMAT {format})");
879
880        frontend::query(&query, &mut self.write_buf)?;
881        self.flush()?;
882
883        // Wait for CopyInResponse
884        loop {
885            let msg = self.read_message()?;
886            match msg {
887                Message::CopyInResponse(_) => {
888                    // Ready to receive data
889                    return Ok(());
890                }
891                Message::ErrorResponse(body) => {
892                    return Err(self.consume_error(&body));
893                }
894                _ => {
895                    // Ignore other messages (like NoticeResponse)
896                }
897            }
898        }
899    }
900
901    /// Initiates a COPY IN operation from a raw SQL query string.
902    ///
903    /// The query must be a complete `COPY ... FROM STDIN ...` statement.
904    ///
905    /// # Errors
906    ///
907    /// Same failure modes as [`Self::start_copy_in_with_format`]: unhealthy
908    /// connection, server-side SQL rejection, or transport I/O failure.
909    pub fn start_copy_in_raw(&mut self, query: &str) -> Result<()> {
910        self.ensure_healthy()?;
911        frontend::query(query, &mut self.write_buf)?;
912        self.flush()?;
913
914        loop {
915            let msg = self.read_message()?;
916            match msg {
917                Message::CopyInResponse(_) => {
918                    return Ok(());
919                }
920                Message::ErrorResponse(body) => {
921                    return Err(self.consume_error(&body));
922                }
923                _ => {}
924            }
925        }
926    }
927
928    /// Sends COPY data to the server.
929    ///
930    /// The data should be in `HyperBinary` format.
931    ///
932    /// # Errors
933    ///
934    /// Currently infallible — frame construction is pure. The `Result`
935    /// return type is preserved for forward compatibility.
936    pub fn send_copy_data(&mut self, data: &[u8]) -> Result<()> {
937        frontend::copy_data(data, &mut self.write_buf);
938        // Don't flush immediately for better batching
939        // Caller can call flush() explicitly if needed
940        Ok(())
941    }
942
943    /// Sends COPY data directly to the stream without internal buffering.
944    ///
945    /// This writes the `CopyData` message directly to the TCP stream, letting
946    /// the kernel's TCP stack handle buffering. Use `flush_stream()` periodically
947    /// to ensure data is sent.
948    ///
949    /// This is more efficient for streaming large amounts of data as it avoids
950    /// copying data into an intermediate buffer.
951    ///
952    /// # Errors
953    ///
954    /// - Returns [`Error`] (protocol) if `data.len() + 4` exceeds
955    ///   `u32::MAX` (the PostgreSQL per-message length cap).
956    /// - Returns [`Error`] (I/O) if flushing buffered bytes or writing
957    ///   the header/payload directly to the transport fails.
958    pub fn send_copy_data_direct(&mut self, data: &[u8]) -> Result<()> {
959        // First flush any pending buffered data
960        if !self.write_buf.is_empty() {
961            self.stream.write_all(&self.write_buf)?;
962            self.write_buf.clear();
963        }
964
965        // Write CopyData message header + data directly to stream
966        // Message format: 'd' (1 byte) + length (4 bytes BigEndian) + data
967        let msg_len = u32::try_from(4 + data.len())
968            .map_err(|_| Error::protocol("CopyData payload exceeds u32::MAX bytes"))?;
969        let len_be = msg_len.to_be_bytes();
970        let header = [b'd', len_be[0], len_be[1], len_be[2], len_be[3]];
971        self.stream.write_all(&header)?;
972        self.stream.write_all(data)?;
973        Ok(())
974    }
975
976    /// Flushes the TCP stream without clearing the write buffer.
977    ///
978    /// Use this with `send_copy_data_direct()` to periodically ensure
979    /// data is sent to the server.
980    ///
981    /// # Errors
982    ///
983    /// Returns [`Error`] (I/O) if flushing the underlying transport
984    /// fails.
985    pub fn flush_stream(&mut self) -> Result<()> {
986        self.stream.flush()?;
987        Ok(())
988    }
989
990    /// Finishes a COPY IN operation successfully.
991    ///
992    /// This sends `CopyDone` and waits for `CommandComplete`.
993    /// Returns the number of rows inserted.
994    ///
995    /// # Errors
996    ///
997    /// - Returns [`Error`] (server) if the server emits an `ErrorResponse`
998    ///   during finalization (e.g. constraint violation from the
999    ///   accumulated rows).
1000    /// - Returns [`Error`] (I/O) on wire-protocol read/write failure.
1001    pub fn finish_copy(&mut self) -> Result<u64> {
1002        // Ensure all data is sent
1003        self.flush()?;
1004
1005        // Send CopyDone
1006        frontend::copy_done(&mut self.write_buf);
1007        self.flush()?;
1008
1009        // Wait for CommandComplete and ReadyForQuery
1010        let mut row_count = 0u64;
1011        loop {
1012            let msg = self.read_message()?;
1013            match msg {
1014                Message::CommandComplete(body) => {
1015                    if let Ok(tag) = body.tag() {
1016                        // Parse row count from tag like "COPY 1234"
1017                        if let Some(count_str) = tag.strip_prefix("COPY ")
1018                            && let Ok(count) = count_str.trim().parse()
1019                        {
1020                            row_count = count;
1021                        }
1022                    }
1023                }
1024                Message::ReadyForQuery(_) => {
1025                    return Ok(row_count);
1026                }
1027                Message::ErrorResponse(body) => {
1028                    return Err(self.consume_error(&body));
1029                }
1030                _ => {
1031                    // Ignore other messages
1032                }
1033            }
1034        }
1035    }
1036
1037    /// Cancels a COPY IN operation.
1038    ///
1039    /// This sends `CopyFail` and waits for the error response.
1040    ///
1041    /// # Errors
1042    ///
1043    /// Returns [`Error`] (I/O) if flushing the buffer or writing the
1044    /// `CopyFail` frame fails, or [`Error`] (closed) if the server
1045    /// drops the connection before returning `ReadyForQuery`.
1046    pub fn cancel_copy(&mut self, reason: &str) -> Result<()> {
1047        // Ensure buffer is clear
1048        self.flush()?;
1049
1050        // Send CopyFail
1051        frontend::copy_fail(reason, &mut self.write_buf);
1052        self.flush()?;
1053
1054        // Wait for ErrorResponse and ReadyForQuery
1055        loop {
1056            let msg = self.read_message()?;
1057            match msg {
1058                Message::ReadyForQuery(_) => {
1059                    return Ok(());
1060                }
1061                Message::ErrorResponse(_) => {
1062                    // Expected - the server confirms the cancel
1063                }
1064                _ => {
1065                    // Ignore other messages
1066                }
1067            }
1068        }
1069    }
1070
1071    /// Executes a COPY ... TO STDOUT query and returns all output data.
1072    ///
1073    /// This is used for queries like:
1074    /// `COPY (SELECT ...) TO STDOUT WITH (format arrowstream)`
1075    ///
1076    /// The method:
1077    /// 1. Sends the query
1078    /// 2. Waits for `CopyOutResponse`
1079    /// 3. Collects all `CopyData` messages
1080    /// 4. Waits for `CopyDone`, `CommandComplete`, and `ReadyForQuery`
1081    ///
1082    /// # Arguments
1083    ///
1084    /// * `query` - The COPY TO STDOUT query to execute
1085    ///
1086    /// # Returns
1087    ///
1088    /// The raw bytes from all `CopyData` messages concatenated together.
1089    ///
1090    /// # Example
1091    ///
1092    /// ```no_run
1093    /// # use hyperdb_api_core::client::connection::RawConnection;
1094    /// # use std::net::TcpStream;
1095    /// # fn example(conn: &mut RawConnection<TcpStream>) -> hyperdb_api_core::client::Result<()> {
1096    /// let arrow_data = conn.copy_out(
1097    ///     "COPY (SELECT * FROM my_table) TO STDOUT WITH (format arrowstream)"
1098    /// )?;
1099    /// # Ok(())
1100    /// # }
1101    /// ```
1102    ///
1103    /// # Errors
1104    ///
1105    /// - Returns [`Error`] (connection) if the connection has been
1106    ///   marked unhealthy.
1107    /// - Returns [`Error`] (server) when the server rejects the COPY TO
1108    ///   STDOUT statement via `ErrorResponse`.
1109    /// - Returns [`Error`] (I/O) / [`Error`] (closed) on transport
1110    ///   read/write failure.
1111    pub fn copy_out(&mut self, query: &str) -> Result<Vec<u8>> {
1112        self.ensure_healthy()?;
1113        // Send the query
1114        frontend::query(query, &mut self.write_buf)?;
1115        self.flush()?;
1116
1117        let mut data = Vec::new();
1118        let mut in_copy_out = false;
1119
1120        // Process messages
1121        loop {
1122            let msg = self.read_message()?;
1123            match msg {
1124                Message::CopyOutResponse(_) => {
1125                    // Server is ready to send COPY data
1126                    in_copy_out = true;
1127                }
1128                Message::CopyData(body) if in_copy_out => {
1129                    // Accumulate the copy data
1130                    data.extend_from_slice(body.data());
1131                }
1132                Message::CopyDone => {
1133                    // COPY data transfer complete
1134                    in_copy_out = false;
1135                }
1136                Message::CommandComplete(_) => {
1137                    // Command finished
1138                }
1139                Message::ReadyForQuery(_) => {
1140                    // Connection is ready for next command
1141                    return Ok(data);
1142                }
1143                Message::ErrorResponse(body) => {
1144                    return Err(self.consume_error(&body));
1145                }
1146                _ => {
1147                    // Ignore other messages (like NoticeResponse)
1148                }
1149            }
1150        }
1151    }
1152
1153    /// Streams COPY OUT data directly to a writer without buffering all data in memory.
1154    ///
1155    /// Returns the total number of bytes written.
1156    ///
1157    /// # Errors
1158    ///
1159    /// Same failure modes as [`Self::copy_out`], plus [`Error`] (I/O)
1160    /// wrapping any error from `writer.write_all` when the target
1161    /// writer cannot accept a COPY chunk.
1162    pub fn copy_out_to_writer(
1163        &mut self,
1164        query: &str,
1165        writer: &mut dyn std::io::Write,
1166    ) -> Result<u64> {
1167        self.ensure_healthy()?;
1168        frontend::query(query, &mut self.write_buf)?;
1169        self.flush()?;
1170
1171        let mut total_bytes: u64 = 0;
1172        let mut in_copy_out = false;
1173
1174        loop {
1175            let msg = self.read_message()?;
1176            match msg {
1177                Message::CopyOutResponse(_) => {
1178                    in_copy_out = true;
1179                }
1180                Message::CopyData(body) if in_copy_out => {
1181                    let chunk = body.data();
1182                    writer
1183                        .write_all(chunk)
1184                        .map_err(|e| Error::io(format!("Failed to write COPY data: {e}")))?;
1185                    total_bytes += chunk.len() as u64;
1186                }
1187                Message::CopyDone => {
1188                    in_copy_out = false;
1189                }
1190                Message::CommandComplete(_) => {}
1191                Message::ReadyForQuery(_) => {
1192                    return Ok(total_bytes);
1193                }
1194                Message::ErrorResponse(body) => {
1195                    return Err(self.consume_error(&body));
1196                }
1197                _ => {}
1198            }
1199        }
1200    }
1201}
1202
1203/// Parses an error response into an Error.
1204pub(crate) fn parse_error_response(
1205    body: &crate::protocol::message::backend::ErrorResponseBody,
1206) -> Error {
1207    let mut severity = String::from("ERROR");
1208    let mut code = String::from("00000");
1209    let mut message = String::from("unknown error");
1210
1211    for field in body.fields().filter_map(|r| {
1212        r.map_err(|e| trace!(target: "hyperdb_api_core::client", error = %e, "dropped error parsing error response field")).ok()
1213    }) {
1214        match field.type_() {
1215            b'S' | b'V' => {
1216                if let Ok(s) = field.value() {
1217                    severity = s.to_string();
1218                }
1219            }
1220            b'C' => {
1221                if let Ok(s) = field.value() {
1222                    code = s.to_string();
1223                }
1224            }
1225            b'M' => {
1226                if let Ok(s) = field.value() {
1227                    message = s.to_string();
1228                }
1229            }
1230            _ => {}
1231        }
1232    }
1233
1234    Error::db(&severity, &code, &message)
1235}
1236
1237#[cfg(test)]
1238mod tests {
1239    use super::*;
1240    use std::io::Cursor;
1241
1242    /// Minimal `Read + Write` harness. Discards all writes and hands back
1243    /// empty reads so we can construct a `RawConnection` without touching
1244    /// the network. Adequate for exercising pure-state logic like the
1245    /// `desynchronized` flag and `ensure_healthy` — not for anything that
1246    /// actually needs a live server response.
1247    struct NullStream;
1248    impl std::io::Read for NullStream {
1249        fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
1250            Ok(0)
1251        }
1252    }
1253    impl std::io::Write for NullStream {
1254        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1255            Ok(buf.len())
1256        }
1257        fn flush(&mut self) -> std::io::Result<()> {
1258            Ok(())
1259        }
1260    }
1261
1262    /// Fresh connections are healthy and `ensure_healthy` is a no-op.
1263    #[test]
1264    fn fresh_connection_is_healthy() {
1265        let conn = RawConnection::new(NullStream);
1266        assert!(conn.is_healthy());
1267        assert!(conn.ensure_healthy().is_ok());
1268    }
1269
1270    /// Once `desynchronized` is set, `is_healthy` reports false and
1271    /// `ensure_healthy` returns a `Connection`-kind error whose message
1272    /// explicitly names the desync so log consumers can grep for it.
1273    #[test]
1274    fn desynchronized_connection_fails_health_check() {
1275        let mut conn = RawConnection::new(NullStream);
1276        conn.desynchronized = true;
1277        assert!(!conn.is_healthy());
1278        let err = conn.ensure_healthy().expect_err("must fail-fast");
1279        assert!(matches!(err, Error::Connection { .. }));
1280        assert!(
1281            err.to_string().to_lowercase().contains("desynchron"),
1282            "error message should mention desynchronization; got: {err}",
1283        );
1284    }
1285
1286    /// `drain_until_ready_bounded` with budget `0` returns false without
1287    /// reading anything, and marks the connection desynchronized. This is
1288    /// the cheapest way to exercise the "budget exhausted" code path
1289    /// without a live protocol stream. Uses a `Cursor` over an empty
1290    /// buffer so the underlying stream is well-defined.
1291    #[test]
1292    fn zero_budget_drain_marks_desynchronized() {
1293        let mut conn = RawConnection::new(Cursor::new(Vec::<u8>::new()));
1294        assert!(conn.is_healthy());
1295        let ok = conn.drain_until_ready_bounded(0);
1296        assert!(!ok, "zero-budget drain must return false");
1297        assert!(
1298            !conn.is_healthy(),
1299            "drain failure must mark connection desynchronized",
1300        );
1301    }
1302
1303    /// Once desynchronized, the main public request APIs all fail-fast
1304    /// with the `ensure_healthy` error instead of sending bytes into a
1305    /// known-poisoned wire. Spot-check one sync query method here; the
1306    /// check itself (`self.ensure_healthy()?`) is a trivial first line
1307    /// at every entry point so extending coverage to every API wouldn't
1308    /// catch additional bug classes.
1309    #[test]
1310    fn desynchronized_connection_fast_fails_simple_query() {
1311        let mut conn = RawConnection::new(Cursor::new(Vec::<u8>::new()));
1312        conn.desynchronized = true;
1313        // `Message` doesn't implement `Debug`, so we can't use
1314        // `expect_err`; match the result directly instead.
1315        let Err(err) = conn.simple_query("SELECT 1") else {
1316            panic!("desynced simple_query must fail-fast")
1317        };
1318        assert!(matches!(err, Error::Connection { .. }));
1319        assert!(err.to_string().to_lowercase().contains("desynchron"));
1320    }
1321}