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