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}