Skip to main content

hyperdb_api_core/client/
async_client.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! High-level asynchronous client for Hyper database.
5//!
6//! This module provides [`AsyncClient`], the async version of [`Client`](crate::client::Client).
7//! It uses tokio for async I/O operations.
8
9use std::sync::Arc;
10
11use tokio::net::TcpStream;
12use tokio::sync::Mutex;
13use tracing::{debug, info, warn};
14
15#[cfg(unix)]
16use tokio::net::UnixStream;
17
18use super::async_connection::AsyncRawConnection;
19use super::async_stream::AsyncStream;
20use super::async_stream_query::AsyncQueryStream;
21use super::cancel::Cancellable;
22use super::config::Config;
23use super::endpoint::ConnectionEndpoint;
24use super::error::{Error, Result};
25use super::notice::{Notice, NoticeReceiver};
26use super::row::{Row, StreamRow};
27use super::statement::ParamFormat;
28
29use crate::protocol::message::Message;
30
31/// An asynchronous client for Hyper database.
32///
33/// This is the async equivalent of [`Client`](crate::client::Client), designed for use
34/// in tokio-based async applications. All I/O operations are non-blocking.
35///
36/// # Example
37///
38/// ```no_run
39/// use hyperdb_api_core::client::{AsyncClient, Config};
40///
41/// #[tokio::main]
42/// async fn main() -> hyperdb_api_core::client::Result<()> {
43///     let config = Config::new()
44///         .with_host("localhost")
45///         .with_port(7483)
46///         .with_database("test.hyper");
47///
48///     let client = AsyncClient::connect(&config).await?;
49///     let rows = client.query("SELECT 1").await?;
50///     client.close().await?;
51///     Ok(())
52/// }
53/// ```
54pub struct AsyncClient {
55    /// The underlying async connection, protected by a mutex for concurrent access.
56    connection: Arc<Mutex<AsyncRawConnection<AsyncStream>>>,
57    /// Backend process ID (for cancel requests).
58    process_id: i32,
59    /// Secret key for authenticating cancel requests.
60    secret_key: i32,
61    /// Connection endpoint for cancel requests and reconnection.
62    endpoint: ConnectionEndpoint,
63    /// Optional notice receiver callback for server notices/warnings.
64    notice_receiver: Option<Arc<NoticeReceiver>>,
65}
66
67impl std::fmt::Debug for AsyncClient {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("AsyncClient")
70            .field("process_id", &self.process_id)
71            .field("secret_key", &self.secret_key)
72            .field("endpoint", &self.endpoint)
73            .field(
74                "notice_receiver",
75                &self.notice_receiver.as_ref().map(|_| "<callback>"),
76            )
77            .finish_non_exhaustive()
78    }
79}
80
81impl AsyncClient {
82    /// Connects to a Hyper server using the given configuration (async).
83    ///
84    /// # Example
85    ///
86    /// ```no_run
87    /// # use hyperdb_api_core::client::{AsyncClient, Config};
88    /// # async fn example() -> hyperdb_api_core::client::Result<()> {
89    /// let config = Config::new()
90    ///     .with_host("localhost")
91    ///     .with_port(7483)
92    ///     .with_database("test.hyper");
93    ///
94    /// let client = AsyncClient::connect(&config).await?;
95    /// # Ok(())
96    /// # }
97    /// ```
98    ///
99    /// # Errors
100    ///
101    /// - Returns [`Error`] (connection) if the TCP connection cannot be
102    ///   established to `config.host():config.port()`.
103    /// - Propagates any [`Error`] from the startup handshake —
104    ///   [`Error`] (auth) for missing/wrong credentials,
105    ///   [`Error`] (server) for server-side startup errors, [`Error`] (protocol)
106    ///   for out-of-sequence messages, or [`Error`] (I/O) for wire
107    ///   failure.
108    pub async fn connect(config: &Config) -> Result<Self> {
109        info!(
110            target: "hyperdb_api",
111            host = %config.host(),
112            port = config.port(),
113            user = config.user().unwrap_or("(default)"),
114            database = config.database().unwrap_or("(none)"),
115            "connection-parameters"
116        );
117
118        let endpoint = ConnectionEndpoint::tcp(config.host(), config.port());
119        let addr = format!("{}:{}", config.host(), config.port());
120        let tcp_stream = TcpStream::connect(&addr).await.map_err(|e| {
121            warn!(target: "hyperdb_api", %addr, error = %e, "connection-failed");
122            Error::connection(format!("failed to connect to {addr}: {e}"))
123        })?;
124
125        // Set TCP options. See the sync mirror in
126        // [`super::client::Client::connect`] for the full rationale and
127        // empirical knee analysis. We bump `SO_RCVBUF` / `SO_SNDBUF` to
128        // 4 MiB (Windows default ~64 KiB throttles loopback throughput;
129        // Linux auto-tunes higher).
130        tcp_stream.set_nodelay(true).ok();
131        let sock = socket2::SockRef::from(&tcp_stream);
132        sock.set_recv_buffer_size(4 * 1024 * 1024).ok();
133        sock.set_send_buffer_size(4 * 1024 * 1024).ok();
134        // TCP keepalive: detect a half-open peer (laptop sleep, network blip,
135        // a hyperd that vanished without a FIN) in ~90s instead of the 2h OS
136        // idle default. See the rationale on `super::client::apply_tcp_keepalive`
137        // (the sync mirror). Best-effort: a rejected knob leaves OS defaults.
138        {
139            let keepalive = socket2::TcpKeepalive::new()
140                .with_time(std::time::Duration::from_secs(60))
141                .with_interval(std::time::Duration::from_secs(10));
142            #[cfg(not(any(target_os = "macos", target_os = "windows")))]
143            let keepalive = keepalive.with_retries(3);
144            sock.set_tcp_keepalive(&keepalive).ok();
145        }
146
147        let stream = AsyncStream::tcp(tcp_stream);
148        let mut connection = AsyncRawConnection::new(stream);
149
150        // Perform startup with authentication
151        let params = config.startup_params();
152        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, *v)).collect();
153        connection.startup(&params_ref, config.password()).await?;
154
155        let process_id = connection.process_id();
156        let secret_key = connection.secret_key();
157
158        debug!(
159            target: "hyperdb_api",
160            process_id,
161            "connection-established"
162        );
163
164        Ok(AsyncClient {
165            connection: Arc::new(Mutex::new(connection)),
166            process_id,
167            secret_key,
168            endpoint,
169            notice_receiver: None,
170        })
171    }
172
173    /// Connects to a Hyper server via Unix Domain Socket (async, Unix only).
174    ///
175    /// # Example
176    ///
177    /// ```no_run
178    /// # use hyperdb_api_core::client::{AsyncClient, Config};
179    /// # use std::path::Path;
180    /// # async fn example() -> hyperdb_api_core::client::Result<()> {
181    /// let socket_path = Path::new("/tmp/hyper/.s.PGSQL.12345");
182    /// let config = Config::new().with_database("test.hyper");
183    /// let client = AsyncClient::connect_unix(socket_path, &config).await?;
184    /// # Ok(())
185    /// # }
186    /// ```
187    ///
188    /// # Errors
189    ///
190    /// - Returns [`Error`] (connection) if the Unix domain socket cannot
191    ///   be connected.
192    /// - Propagates any error from the startup handshake (see
193    ///   [`Self::connect`]).
194    #[cfg(unix)]
195    pub async fn connect_unix(
196        socket_path: impl AsRef<std::path::Path>,
197        config: &Config,
198    ) -> Result<Self> {
199        use std::path::Path;
200
201        let path = socket_path.as_ref();
202        info!(
203            target: "hyperdb_api",
204            socket_path = %path.display(),
205            user = config.user().unwrap_or("(default)"),
206            database = config.database().unwrap_or("(none)"),
207            "connection-parameters-unix"
208        );
209
210        let unix_stream = UnixStream::connect(path).await.map_err(|e| {
211            warn!(target: "hyperdb_api", socket_path = %path.display(), error = %e, "connection-failed");
212            Error::connection(format!("failed to connect to unix socket {}: {}", path.display(), e))
213        })?;
214
215        // Parse endpoint from socket path
216        let directory = path.parent().unwrap_or(Path::new("/"));
217        let name = path
218            .file_name()
219            .and_then(|n| n.to_str())
220            .unwrap_or("socket");
221        let endpoint = ConnectionEndpoint::domain_socket(directory, name);
222
223        let stream = AsyncStream::unix(unix_stream);
224        let mut connection = AsyncRawConnection::new(stream);
225
226        // Perform startup with authentication
227        let params = config.startup_params();
228        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, *v)).collect();
229        connection.startup(&params_ref, config.password()).await?;
230
231        let process_id = connection.process_id();
232        let secret_key = connection.secret_key();
233
234        debug!(
235            target: "hyperdb_api",
236            process_id,
237            "connection-established-unix"
238        );
239
240        Ok(AsyncClient {
241            connection: Arc::new(Mutex::new(connection)),
242            process_id,
243            secret_key,
244            endpoint,
245            notice_receiver: None,
246        })
247    }
248
249    /// Connects to a Hyper server via Windows Named Pipe (async, Windows only).
250    ///
251    /// # Arguments
252    ///
253    /// * `pipe_path` - The full pipe path (e.g., `\\.\pipe\hyper-12345`)
254    /// * `config` - Connection configuration
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the Named Pipe cannot be opened (e.g., pipe does not
259    /// exist, all instances are busy after the retry window, or permission is
260    /// denied) or if the authentication handshake fails.
261    #[cfg(windows)]
262    pub async fn connect_named_pipe(pipe_path: &str, config: &Config) -> Result<Self> {
263        use std::time::{Duration, Instant};
264        use tokio::net::windows::named_pipe::ClientOptions;
265
266        info!(
267            target: "hyperdb_api",
268            pipe_path = %pipe_path,
269            user = config.user().unwrap_or("(default)"),
270            database = config.database().unwrap_or("(none)"),
271            "connection-parameters-named-pipe"
272        );
273
274        // Retry on `ERROR_PIPE_BUSY` (231) — Windows named pipes have a finite
275        // number of server-side instances and concurrent clients can hit the
276        // cap. See the sync mirror in [`super::client::Client::connect_named_pipe`]
277        // for the full rationale.
278        const RETRY_INTERVAL: Duration = Duration::from_millis(20);
279        const MAX_WAIT: Duration = Duration::from_secs(10);
280        const ERROR_PIPE_BUSY: i32 = 231;
281
282        let deadline = Instant::now() + MAX_WAIT;
283        let client = loop {
284            match ClientOptions::new().open(pipe_path) {
285                Ok(c) => break c,
286                Err(e)
287                    if e.raw_os_error() == Some(ERROR_PIPE_BUSY) && Instant::now() < deadline =>
288                {
289                    tokio::time::sleep(RETRY_INTERVAL).await;
290                }
291                Err(e) => {
292                    warn!(target: "hyperdb_api", pipe_path = %pipe_path, error = %e, "connection-failed");
293                    return Err(Error::connection(format!(
294                        "failed to connect to named pipe {pipe_path}: {e}"
295                    )));
296                }
297            }
298        };
299
300        // Parse endpoint from pipe path
301        let endpoint = ConnectionEndpoint::parse(&format!(
302            "tab.pipe://{}",
303            pipe_path.trim_start_matches(r"\\").replace('\\', "/")
304        ))
305        .unwrap_or_else(|_| {
306            let parts: Vec<&str> = pipe_path
307                .trim_start_matches(r"\\")
308                .splitn(3, '\\')
309                .collect();
310            if parts.len() >= 3 {
311                ConnectionEndpoint::named_pipe(parts[0], parts[2])
312            } else {
313                ConnectionEndpoint::named_pipe(".", pipe_path)
314            }
315        });
316
317        let stream = AsyncStream::named_pipe(client);
318        let mut connection = AsyncRawConnection::new(stream);
319
320        // Perform startup with authentication
321        let params = config.startup_params();
322        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, *v)).collect();
323        connection.startup(&params_ref, config.password()).await?;
324
325        let process_id = connection.process_id();
326        let secret_key = connection.secret_key();
327
328        debug!(
329            target: "hyperdb_api",
330            process_id,
331            "connection-established-named-pipe"
332        );
333
334        Ok(AsyncClient {
335            connection: Arc::new(Mutex::new(connection)),
336            process_id,
337            secret_key,
338            endpoint,
339            notice_receiver: None,
340        })
341    }
342
343    /// Connects to a Hyper server using a `ConnectionEndpoint` (async).
344    ///
345    /// This is a lower-level method that accepts a pre-parsed endpoint.
346    ///
347    /// # Errors
348    ///
349    /// Delegates to [`Self::connect`], [`Self::connect_unix`], or
350    /// `Self::connect_named_pipe` depending on the endpoint variant,
351    /// and propagates their errors unchanged.
352    pub async fn connect_endpoint(endpoint: &ConnectionEndpoint, config: &Config) -> Result<Self> {
353        match endpoint {
354            ConnectionEndpoint::Tcp { host, port } => {
355                let mut cfg = config.clone();
356                cfg = cfg.with_host(host.clone()).with_port(*port);
357                Self::connect(&cfg).await
358            }
359            #[cfg(unix)]
360            ConnectionEndpoint::DomainSocket { directory, name } => {
361                let socket_path = directory.join(name);
362                Self::connect_unix(&socket_path, config).await
363            }
364            #[cfg(windows)]
365            ConnectionEndpoint::NamedPipe { host, name } => {
366                let pipe_path = format!(r"\\{host}\pipe\{name}");
367                Self::connect_named_pipe(&pipe_path, config).await
368            }
369        }
370    }
371
372    /// Returns the connection endpoint.
373    #[must_use]
374    pub fn endpoint(&self) -> &ConnectionEndpoint {
375        &self.endpoint
376    }
377
378    /// Returns the server process ID for this connection.
379    #[must_use]
380    pub fn process_id(&self) -> i32 {
381        self.process_id
382    }
383
384    /// Returns the secret key for cancel requests.
385    #[must_use]
386    pub fn secret_key(&self) -> i32 {
387        self.secret_key
388    }
389
390    /// Cancels the currently executing query on this connection (async).
391    ///
392    /// This method opens a separate connection to send a cancel request.
393    /// For TCP endpoints, it opens a new TCP connection.
394    /// For Unix domain sockets, it connects to the same socket path.
395    ///
396    /// # Errors
397    ///
398    /// - Returns [`Error`] (connection) if a fresh cancel-side socket
399    ///   (TCP / UDS / named-pipe) cannot be opened to
400    ///   [`Self::endpoint`].
401    /// - Returns [`Error`] (I/O) if writing the cancel request fails.
402    pub async fn cancel(&self) -> Result<()> {
403        use crate::protocol::message::frontend;
404        use bytes::BytesMut;
405        use tokio::io::AsyncWriteExt;
406
407        info!(
408            target: "hyperdb_api",
409            process_id = self.process_id,
410            "query-cancel-request"
411        );
412
413        let endpoint_str = self.endpoint.to_string();
414
415        match &self.endpoint {
416            ConnectionEndpoint::Tcp { host, port } => {
417                let addr = format!("{host}:{port}");
418                let mut stream = TcpStream::connect(&addr).await.map_err(|e| {
419                    warn!(
420                        target: "hyperdb_api",
421                        addr = %endpoint_str,
422                        error = %e,
423                        "query-cancel-connect-failed"
424                    );
425                    Error::connection(format!(
426                        "failed to connect for cancel request to {endpoint_str}: {e}"
427                    ))
428                })?;
429                // Cancel is a 16-byte fire-and-forget — disable Nagle so the
430                // request hits the wire without waiting on a coalesce timer.
431                stream.set_nodelay(true).ok();
432
433                let mut buf = BytesMut::new();
434                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
435
436                stream.write_all(&buf).await.map_err(|e| {
437                    warn!(
438                        target: "hyperdb_api",
439                        error = %e,
440                        "query-cancel-send-failed"
441                    );
442                    Error::from_io(e)
443                })?;
444            }
445            #[cfg(unix)]
446            ConnectionEndpoint::DomainSocket { directory, name } => {
447                let socket_path = directory.join(name);
448                let mut stream = UnixStream::connect(&socket_path).await.map_err(|e| {
449                    warn!(
450                        target: "hyperdb_api",
451                        addr = %endpoint_str,
452                        error = %e,
453                        "query-cancel-connect-failed"
454                    );
455                    Error::connection(format!(
456                        "failed to connect for cancel request to {endpoint_str}: {e}"
457                    ))
458                })?;
459
460                let mut buf = BytesMut::new();
461                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
462
463                stream.write_all(&buf).await.map_err(|e| {
464                    warn!(
465                        target: "hyperdb_api",
466                        error = %e,
467                        "query-cancel-send-failed"
468                    );
469                    Error::from_io(e)
470                })?;
471            }
472            #[cfg(windows)]
473            ConnectionEndpoint::NamedPipe { host, name } => {
474                let pipe_path = format!(r"\\{host}\pipe\{name}");
475                // Use sync file I/O for cancel (short-lived connection)
476                let mut file = std::fs::OpenOptions::new()
477                    .read(true)
478                    .write(true)
479                    .open(&pipe_path)
480                    .map_err(|e| {
481                        warn!(
482                            target: "hyperdb_api",
483                            addr = %endpoint_str,
484                            error = %e,
485                            "query-cancel-connect-failed"
486                        );
487                        Error::connection(format!(
488                            "failed to connect for cancel request to {endpoint_str}: {e}"
489                        ))
490                    })?;
491
492                let mut buf = BytesMut::new();
493                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
494
495                use std::io::Write;
496                file.write_all(&buf).map_err(|e| {
497                    warn!(
498                        target: "hyperdb_api",
499                        error = %e,
500                        "query-cancel-send-failed"
501                    );
502                    Error::from_io(e)
503                })?;
504
505                file.flush().map_err(Error::from_io)?;
506            }
507        }
508
509        debug!(target: "hyperdb_api", "query-cancel-sent");
510        Ok(())
511    }
512
513    /// Executes a query and returns all result rows (async).
514    ///
515    /// # Errors
516    ///
517    /// Propagates any [`Error`] from the underlying connection's
518    /// [`AsyncRawConnection::simple_query`] — [`Error`] (server) for
519    /// server-side SQL errors, [`Error`] (I/O) / [`Error`] (closed) for
520    /// transport failures, and [`Error`] (connection) if the connection
521    /// is unhealthy. Row construction may also raise an [`Error`] when
522    /// a `DataRow` cannot be decoded against its `RowDescription`.
523    pub async fn query(&self, sql: &str) -> Result<Vec<Row>> {
524        let mut conn = self.connection.lock().await;
525        let messages = conn.simple_query(sql).await?;
526        Self::process_query_messages(messages, self.notice_receiver.as_ref())
527    }
528
529    /// Executes a query with `HyperBinary` format for better performance (async).
530    ///
531    /// # Errors
532    ///
533    /// Same failure modes as [`Self::query`].
534    pub async fn query_fast(&self, sql: &str) -> Result<Vec<StreamRow>> {
535        let mut conn = self.connection.lock().await;
536        let messages = conn.query_binary(sql).await?;
537        Ok(Self::process_binary_messages(
538            messages,
539            self.notice_receiver.as_ref(),
540        ))
541    }
542
543    /// Executes a query with `HyperBinary` format and returns a streaming
544    /// result reader (async).
545    ///
546    /// This is the async mirror of
547    /// [`Client::query_streaming`](super::client::Client::query_streaming).
548    /// The returned [`AsyncQueryStream`] yields rows in chunks so callers
549    /// can process arbitrarily large result sets with constant memory. The
550    /// connection mutex is held for the duration of iteration; dropping the
551    /// stream before completion issues a best-effort cancel and marks the
552    /// connection desynchronized.
553    ///
554    /// # Errors
555    ///
556    /// - Returns [`Error`] (connection) if the connection is unhealthy.
557    /// - Returns [`Error`] (I/O) if writing the Parse/Bind/Execute/Sync
558    ///   sequence fails on the transport.
559    pub async fn query_streaming(
560        &self,
561        sql: &str,
562        chunk_size: usize,
563    ) -> Result<AsyncQueryStream<'_>> {
564        let mut conn = self.connection.lock().await;
565        conn.start_query_binary(sql).await?;
566        Ok(AsyncQueryStream::new(conn, self, chunk_size))
567    }
568
569    /// Sends a best-effort `CancelRequest` using *synchronous* I/O so it
570    /// is usable from [`Drop`] impls (notably
571    /// [`AsyncQueryStream::drop`](super::async_stream_query::AsyncQueryStream)).
572    ///
573    /// Cancellation opens a short-lived TCP / UDS / Named-Pipe connection,
574    /// writes the cancel packet, and drops it — the server recognizes the
575    /// (`process_id`, `secret_key`) tuple and signals the long-running query
576    /// to abort. No response is expected.
577    fn cancel_sync(&self) -> Result<()> {
578        use crate::protocol::message::frontend;
579        use bytes::BytesMut;
580        use std::io::Write;
581
582        info!(
583            target: "hyperdb_api",
584            process_id = self.process_id,
585            "query-cancel-request"
586        );
587
588        let endpoint_str = self.endpoint.to_string();
589
590        match &self.endpoint {
591            ConnectionEndpoint::Tcp { host, port } => {
592                let addr = format!("{host}:{port}");
593                let mut stream = std::net::TcpStream::connect(&addr).map_err(|e| {
594                    warn!(
595                        target: "hyperdb_api",
596                        addr = %endpoint_str,
597                        error = %e,
598                        "query-cancel-connect-failed"
599                    );
600                    Error::connection(format!(
601                        "failed to connect for cancel request to {endpoint_str}: {e}"
602                    ))
603                })?;
604                // Cancel is a 16-byte fire-and-forget — disable Nagle so the
605                // request hits the wire without waiting on a coalesce timer.
606                stream.set_nodelay(true).ok();
607
608                let mut buf = BytesMut::with_capacity(16);
609                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
610
611                stream.write_all(&buf).map_err(Error::from_io)?;
612                stream.flush().map_err(Error::from_io)?;
613            }
614            #[cfg(unix)]
615            ConnectionEndpoint::DomainSocket { directory, name } => {
616                let socket_path = directory.join(name);
617                let mut stream =
618                    std::os::unix::net::UnixStream::connect(&socket_path).map_err(|e| {
619                        warn!(
620                            target: "hyperdb_api",
621                            addr = %endpoint_str,
622                            error = %e,
623                            "query-cancel-connect-failed"
624                        );
625                        Error::connection(format!(
626                            "failed to connect for cancel request to {endpoint_str}: {e}"
627                        ))
628                    })?;
629
630                let mut buf = BytesMut::with_capacity(16);
631                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
632
633                stream.write_all(&buf).map_err(Error::from_io)?;
634                stream.flush().map_err(Error::from_io)?;
635            }
636            #[cfg(windows)]
637            ConnectionEndpoint::NamedPipe { host, name } => {
638                let pipe_path = format!(r"\\{host}\pipe\{name}");
639                let mut file = std::fs::OpenOptions::new()
640                    .read(true)
641                    .write(true)
642                    .open(&pipe_path)
643                    .map_err(|e| {
644                        warn!(
645                            target: "hyperdb_api",
646                            addr = %endpoint_str,
647                            error = %e,
648                            "query-cancel-connect-failed"
649                        );
650                        Error::connection(format!(
651                            "failed to connect for cancel request to {endpoint_str}: {e}"
652                        ))
653                    })?;
654
655                let mut buf = BytesMut::with_capacity(16);
656                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
657
658                file.write_all(&buf).map_err(Error::from_io)?;
659                file.flush().map_err(Error::from_io)?;
660            }
661        }
662
663        debug!(target: "hyperdb_api", "query-cancel-sent");
664        Ok(())
665    }
666
667    /// Executes a command (INSERT/UPDATE/DELETE/DDL) and returns affected row count (async).
668    ///
669    /// # Errors
670    ///
671    /// Same failure modes as [`Self::query`] — server-side SQL errors,
672    /// transport failures, and unhealthy-connection state all surface
673    /// as [`Error`].
674    pub async fn exec(&self, sql: &str) -> Result<u64> {
675        let mut conn = self.connection.lock().await;
676        let messages = conn.simple_query(sql).await?;
677        Ok(Self::extract_row_count(&messages))
678    }
679
680    /// Returns a server parameter value by name.
681    pub async fn parameter_status(&self, name: &str) -> Option<String> {
682        let conn = self.connection.lock().await;
683        conn.parameter_status(name)
684            .map(std::string::ToString::to_string)
685    }
686
687    /// Sets the notice receiver callback.
688    pub fn set_notice_receiver(&mut self, receiver: Option<Box<dyn Fn(Notice) + Send + Sync>>) {
689        self.notice_receiver = receiver.map(Arc::from);
690    }
691
692    /// Closes the connection gracefully (async).
693    ///
694    /// # Errors
695    ///
696    /// Returns [`Error`] (I/O) if writing the `Terminate` frame or
697    /// flushing the async transport fails.
698    pub async fn close(self) -> Result<()> {
699        let mut conn = self.connection.lock().await;
700        conn.terminate().await
701    }
702
703    /// Executes a batch of statements separated by semicolons (async).
704    ///
705    /// # Errors
706    ///
707    /// Same failure modes as [`Self::query`].
708    pub async fn batch_execute(&self, sql: &str) -> Result<()> {
709        let mut conn = self.connection.lock().await;
710        let _messages = conn.simple_query(sql).await?;
711        Ok(())
712    }
713
714    /// Starts a COPY IN operation for bulk data insertion (async).
715    ///
716    /// # Errors
717    ///
718    /// Delegates to [`Self::copy_in_with_format`]; see that method
719    /// for concrete failure modes.
720    pub async fn copy_in(
721        &self,
722        table_name: &str,
723        columns: &[&str],
724    ) -> Result<AsyncCopyInWriter<'_>> {
725        self.copy_in_with_format(table_name, columns, "HYPERBINARY")
726            .await
727    }
728
729    /// Starts a COPY IN operation and returns an owned-handle writer
730    /// whose lifetime is independent of this client. The writer holds an
731    /// `Arc`-cloned reference to the underlying connection mutex, so it
732    /// can be stored in structs that need a `'static`-lifetime writer —
733    /// e.g. N-API classes that can't carry borrowed references across
734    /// JS callbacks.
735    ///
736    /// # Errors
737    ///
738    /// Same failure modes as [`Self::copy_in_with_format`].
739    pub async fn copy_in_arc_with_format(
740        &self,
741        table_name: &str,
742        columns: &[&str],
743        format: &str,
744    ) -> Result<AsyncCopyInWriterOwned> {
745        let mut conn = self.connection.lock().await;
746        conn.start_copy_in_with_format(table_name, columns, format)
747            .await?;
748        drop(conn);
749        Ok(AsyncCopyInWriterOwned::new(Arc::clone(&self.connection)))
750    }
751
752    /// Starts a COPY IN operation with a specified data format (async).
753    ///
754    /// # Errors
755    ///
756    /// - Returns [`Error`] (connection) if the connection is unhealthy.
757    /// - Returns [`Error`] (server) if the server rejects the generated
758    ///   `COPY ... FROM STDIN` statement.
759    /// - Returns [`Error`] (I/O) on transport read/write failure.
760    pub async fn copy_in_with_format(
761        &self,
762        table_name: &str,
763        columns: &[&str],
764        format: &str,
765    ) -> Result<AsyncCopyInWriter<'_>> {
766        let mut conn = self.connection.lock().await;
767        conn.start_copy_in_with_format(table_name, columns, format)
768            .await?;
769        drop(conn);
770        Ok(AsyncCopyInWriter::new(&self.connection))
771    }
772
773    /// Executes a COPY ... TO STDOUT query and returns all output data (async).
774    ///
775    /// # Errors
776    ///
777    /// - Returns [`Error`] (connection) if the connection is unhealthy.
778    /// - Returns [`Error`] (server) when the server rejects the statement.
779    /// - Returns [`Error`] (I/O) / [`Error`] (closed) on transport
780    ///   read/write failure.
781    pub async fn copy_out(&self, query: &str) -> Result<Vec<u8>> {
782        let mut conn = self.connection.lock().await;
783        conn.copy_out(query).await
784    }
785
786    /// Returns true if the connection is alive.
787    #[must_use]
788    pub fn is_alive(&self) -> bool {
789        // Try to acquire the lock - if we can, connection is alive
790        self.connection.try_lock().is_ok()
791    }
792
793    /// Prepares a statement for execution (async).
794    ///
795    /// # Errors
796    ///
797    /// Delegates to [`Self::prepare_typed`]; see that method for the
798    /// failure modes.
799    pub async fn prepare(&self, query: &str) -> Result<AsyncPreparedStatement> {
800        self.prepare_typed(query, &[]).await
801    }
802
803    /// Prepares a statement with explicit parameter types (async).
804    ///
805    /// # Errors
806    ///
807    /// - Returns [`Error`] (connection) if the connection is unhealthy.
808    /// - Returns [`Error`] (server) if the server rejects the `Parse`
809    ///   request (SQL syntax, unknown parameter OIDs, etc.).
810    /// - Returns [`Error`] (I/O) on transport read/write failure.
811    pub async fn prepare_typed(
812        &self,
813        query: &str,
814        param_types: &[crate::types::Oid],
815    ) -> Result<AsyncPreparedStatement> {
816        use std::sync::atomic::{AtomicU64, Ordering};
817        static COUNTER: AtomicU64 = AtomicU64::new(0);
818
819        let name = format!(
820            "__hyper_async_stmt_{}",
821            COUNTER.fetch_add(1, Ordering::Relaxed)
822        );
823        let mut conn = self.connection.lock().await;
824        let (params, columns) = conn.prepare(&name, query, param_types).await?;
825
826        Ok(AsyncPreparedStatement {
827            name,
828            query: query.to_string(),
829            param_types: params,
830            columns,
831            connection: Arc::downgrade(&self.connection),
832            closed: false,
833        })
834    }
835
836    /// Closes a prepared statement on the server (async).
837    ///
838    /// Prefer the RAII [`AsyncPreparedStatement::close`] method on the
839    /// statement itself — it consumes the statement and prevents the
840    /// auto-close Drop path from double-closing.
841    ///
842    /// # Errors
843    ///
844    /// Propagates any error from
845    /// [`AsyncRawConnection::close_statement`] — unhealthy connection,
846    /// server-side error during `Close`/`Sync`, or transport failure.
847    pub async fn close_statement(&self, statement: &AsyncPreparedStatement) -> Result<()> {
848        let mut conn = self.connection.lock().await;
849        conn.close_statement(&statement.name).await
850    }
851
852    /// Executes a prepared statement with parameters (async).
853    ///
854    /// # Errors
855    ///
856    /// Propagates any error from
857    /// [`AsyncRawConnection::execute_prepared`] — unhealthy connection,
858    /// parameter/type mismatch, server-side execution failure, or
859    /// transport failure. Row construction may also raise an [`Error`]
860    /// when a `DataRow` cannot be decoded.
861    pub async fn execute_prepared<P: AsRef<[Option<Vec<u8>>]>>(
862        &self,
863        statement: &AsyncPreparedStatement,
864        params: P,
865    ) -> Result<Vec<Row>> {
866        let params_ref: Vec<Option<&[u8]>> = params
867            .as_ref()
868            .iter()
869            .map(|p| p.as_ref().map(std::vec::Vec::as_slice))
870            .collect();
871
872        let mut conn = self.connection.lock().await;
873        conn.execute_prepared(&statement.name, &params_ref, statement.columns.len())
874            .await
875    }
876
877    /// Executes a prepared statement that doesn't return rows (async).
878    ///
879    /// # Errors
880    ///
881    /// Same failure modes as [`Self::execute_prepared`] (excluding
882    /// row-construction errors — this path never builds rows).
883    pub async fn execute_prepared_no_result<P: AsRef<[Option<Vec<u8>>]>>(
884        &self,
885        statement: &AsyncPreparedStatement,
886        params: P,
887    ) -> Result<u64> {
888        self.execute_prepared_no_result_with_formats(statement, params, &[])
889            .await
890    }
891
892    /// Executes a prepared statement that doesn't return rows, choosing the
893    /// wire format per parameter (async).
894    ///
895    /// `param_formats` must be the same length as `params`, or empty to mean
896    /// "every parameter is binary". See [`ParamFormat`].
897    ///
898    /// # Errors
899    ///
900    /// Same failure modes as [`Self::execute_prepared_no_result`].
901    pub async fn execute_prepared_no_result_with_formats<P: AsRef<[Option<Vec<u8>>]>>(
902        &self,
903        statement: &AsyncPreparedStatement,
904        params: P,
905        param_formats: &[ParamFormat],
906    ) -> Result<u64> {
907        let params_ref: Vec<Option<&[u8]>> = params
908            .as_ref()
909            .iter()
910            .map(|p| p.as_ref().map(std::vec::Vec::as_slice))
911            .collect();
912
913        let mut conn = self.connection.lock().await;
914        conn.execute_prepared_no_result_with_formats(&statement.name, &params_ref, param_formats)
915            .await
916    }
917
918    /// Executes a prepared statement with streaming results (async).
919    ///
920    /// Returns an [`AsyncPreparedQueryStream`](super::async_prepared_stream::AsyncPreparedQueryStream)
921    /// that yields rows in chunks, keeping memory bounded regardless of
922    /// result size. Async mirror of
923    /// [`Client::execute_streaming`](crate::client::Client::execute_streaming).
924    ///
925    /// # Errors
926    ///
927    /// - Returns [`Error`] (connection) if the connection is unhealthy.
928    /// - Returns [`Error`] (I/O) if writing the initial Bind/Execute/Sync
929    ///   sequence fails on the transport.
930    pub async fn execute_prepared_streaming<'a, P: AsRef<[Option<Vec<u8>>]>>(
931        &'a self,
932        statement: &AsyncPreparedStatement,
933        params: P,
934        chunk_size: usize,
935    ) -> Result<super::async_prepared_stream::AsyncPreparedQueryStream<'a>> {
936        self.execute_prepared_streaming_with_formats(statement, params, &[], chunk_size)
937            .await
938    }
939
940    /// Executes a prepared statement with streaming results, choosing the
941    /// wire format per parameter (async).
942    ///
943    /// `param_formats` must be the same length as `params`, or empty to mean
944    /// "every parameter is binary". See [`ParamFormat`].
945    ///
946    /// # Errors
947    ///
948    /// Same failure modes as [`Self::execute_prepared_streaming`].
949    pub async fn execute_prepared_streaming_with_formats<'a, P: AsRef<[Option<Vec<u8>>]>>(
950        &'a self,
951        statement: &AsyncPreparedStatement,
952        params: P,
953        param_formats: &[ParamFormat],
954        chunk_size: usize,
955    ) -> Result<super::async_prepared_stream::AsyncPreparedQueryStream<'a>> {
956        let params_ref: Vec<Option<&[u8]>> = params
957            .as_ref()
958            .iter()
959            .map(|p| p.as_ref().map(std::vec::Vec::as_slice))
960            .collect();
961
962        let mut conn = self.connection.lock().await;
963        if param_formats.is_empty() {
964            conn.start_execute_prepared(&statement.name, &params_ref, statement.columns.len())
965                .await?;
966        } else {
967            conn.start_execute_prepared_with_formats(
968                &statement.name,
969                &params_ref,
970                param_formats,
971                statement.columns.len(),
972            )
973            .await?;
974        }
975
976        let columns = std::sync::Arc::new(statement.columns.clone());
977        Ok(super::async_prepared_stream::AsyncPreparedQueryStream::new(
978            conn, self, chunk_size, columns,
979        ))
980    }
981}
982
983impl Cancellable for AsyncClient {
984    /// Fire-and-forget cancel via PG wire protocol `CancelRequest` on a
985    /// fresh connection. Uses synchronous I/O so it is callable from
986    /// `Drop` impls. Errors are logged and swallowed because cancellation
987    /// is best-effort and callers cannot meaningfully recover.
988    fn cancel(&self) {
989        if let Err(e) = AsyncClient::cancel_sync(self) {
990            warn!(
991                target: "hyperdb_api_core::client",
992                error = %e,
993                process_id = self.process_id,
994                "cancel request failed (best-effort, swallowed)",
995            );
996        }
997    }
998}
999
1000impl AsyncClient {
1001    fn process_query_messages(
1002        messages: Vec<Message>,
1003        notice_receiver: Option<&Arc<NoticeReceiver>>,
1004    ) -> Result<Vec<Row>> {
1005        use super::statement::{Column, ColumnFormat};
1006
1007        let mut rows = Vec::new();
1008        let mut columns: Option<Arc<Vec<Column>>> = None;
1009
1010        for msg in messages {
1011            match msg {
1012                Message::RowDescription(desc) => {
1013                    let mut cols = Vec::new();
1014                    for field in desc.fields().filter_map(std::result::Result::ok) {
1015                        cols.push(Column::new(
1016                            field.name().to_string(),
1017                            field.type_oid(),
1018                            field.type_modifier(),
1019                            ColumnFormat::from_code(field.format()),
1020                        ));
1021                    }
1022                    columns = Some(Arc::new(cols));
1023                }
1024                Message::DataRow(data) => {
1025                    if let Some(ref cols) = columns {
1026                        rows.push(Row::new(Arc::clone(cols), data)?);
1027                    }
1028                }
1029                Message::NoticeResponse(body) => {
1030                    if let Some(receiver) = notice_receiver {
1031                        let notice = Notice::from_response_body(&body);
1032                        receiver(notice);
1033                    }
1034                }
1035                _ => {}
1036            }
1037        }
1038        Ok(rows)
1039    }
1040
1041    fn process_binary_messages(
1042        messages: Vec<Message>,
1043        notice_receiver: Option<&Arc<NoticeReceiver>>,
1044    ) -> Vec<StreamRow> {
1045        let mut rows = Vec::new();
1046
1047        for msg in messages {
1048            match msg {
1049                Message::DataRow(data) => {
1050                    rows.push(StreamRow::new(data));
1051                }
1052                Message::NoticeResponse(body) => {
1053                    if let Some(receiver) = notice_receiver {
1054                        let notice = Notice::from_response_body(&body);
1055                        receiver(notice);
1056                    }
1057                }
1058                _ => {}
1059            }
1060        }
1061        rows
1062    }
1063
1064    fn extract_row_count(messages: &[Message]) -> u64 {
1065        for msg in messages {
1066            if let Message::CommandComplete(body) = msg
1067                && let Ok(tag) = body.tag()
1068            {
1069                // Parse formats like "INSERT 0 5", "UPDATE 10", "DELETE 3"
1070                let parts: Vec<&str> = tag.split_whitespace().collect();
1071                if let Some(last) = parts.last()
1072                    && let Ok(count) = last.parse()
1073                {
1074                    return count;
1075                }
1076            }
1077        }
1078        0
1079    }
1080}
1081
1082/// An async prepared statement.
1083///
1084/// Represents a server-side prepared statement that can be executed
1085/// multiple times with different parameters. **Auto-closes on `Drop`**
1086/// via a best-effort `tokio::spawn` task — if no tokio runtime is
1087/// available at drop time we log a warning and flag the connection
1088/// desynchronized rather than silently leaking the server-side
1089/// statement slot.
1090///
1091/// For callers who need confirmed close with error propagation, use
1092/// [`AsyncPreparedStatement::close`] (explicit async close).
1093#[derive(Debug)]
1094pub struct AsyncPreparedStatement {
1095    /// Statement name on the server.
1096    pub(crate) name: String,
1097    /// Original SQL query string.
1098    query: String,
1099    /// Parameter type OIDs.
1100    param_types: Vec<crate::types::Oid>,
1101    /// Result column descriptions.
1102    pub(crate) columns: Vec<super::statement::Column>,
1103    /// Weak handle to the owning connection for the Drop path. `Weak`
1104    /// so that a lingering statement never keeps the connection alive
1105    /// past the `AsyncClient` it was prepared against.
1106    connection: std::sync::Weak<Mutex<AsyncRawConnection<AsyncStream>>>,
1107    /// Flipped by `close(self)` to suppress the Drop-path auto-close.
1108    closed: bool,
1109}
1110
1111impl AsyncPreparedStatement {
1112    /// Returns the statement name.
1113    #[must_use]
1114    pub fn name(&self) -> &str {
1115        &self.name
1116    }
1117
1118    /// Returns the original query.
1119    #[must_use]
1120    pub fn query(&self) -> &str {
1121        &self.query
1122    }
1123
1124    /// Returns the parameter types.
1125    #[must_use]
1126    pub fn param_types(&self) -> &[crate::types::Oid] {
1127        &self.param_types
1128    }
1129
1130    /// Returns the number of parameters.
1131    #[must_use]
1132    pub fn param_count(&self) -> usize {
1133        self.param_types.len()
1134    }
1135
1136    /// Returns the result column descriptions.
1137    #[must_use]
1138    pub fn columns(&self) -> &[super::statement::Column] {
1139        &self.columns
1140    }
1141
1142    /// Returns the number of result columns.
1143    #[must_use]
1144    pub fn column_count(&self) -> usize {
1145        self.columns.len()
1146    }
1147
1148    /// Explicitly closes the prepared statement on the server (async).
1149    ///
1150    /// Consumes the statement — no further `execute_prepared` /
1151    /// `execute_prepared_streaming` calls are possible — and suppresses
1152    /// the Drop-path auto-close. Returns the `close_statement` result so
1153    /// callers can observe any transport errors.
1154    ///
1155    /// If you don't need error propagation, simply dropping the
1156    /// statement has the same effect (best-effort auto-close via
1157    /// `tokio::spawn`).
1158    ///
1159    /// # Errors
1160    ///
1161    /// Propagates any error from
1162    /// [`AsyncClient::close_statement`] — unhealthy connection,
1163    /// server-side error during `Close`/`Sync`, or transport failure.
1164    pub async fn close(mut self, client: &AsyncClient) -> Result<()> {
1165        self.closed = true;
1166        client.close_statement(&self).await
1167    }
1168}
1169
1170impl Drop for AsyncPreparedStatement {
1171    fn drop(&mut self) {
1172        // Explicit close already ran — nothing to do.
1173        if self.closed {
1174            return;
1175        }
1176
1177        // Best-effort close. Try to grab a tokio handle; if we're being
1178        // dropped outside a runtime (e.g. the runtime has already shut
1179        // down or the caller never had one), fall back to a warning and
1180        // flag the connection desynchronized so the next operation fails
1181        // loudly rather than racing with a lingering statement.
1182        let Some(conn) = self.connection.upgrade() else {
1183            // Connection has already been dropped — nothing to close.
1184            return;
1185        };
1186
1187        let name = std::mem::take(&mut self.name);
1188
1189        if let Ok(handle) = tokio::runtime::Handle::try_current() {
1190            handle.spawn(async move {
1191                let mut c = conn.lock().await;
1192                if let Err(e) = c.close_statement(&name).await {
1193                    warn!(
1194                        target: "hyperdb_api_core::client",
1195                        statement = %name,
1196                        error = %e,
1197                        "AsyncPreparedStatement drop-close failed (best-effort, swallowed)"
1198                    );
1199                }
1200            });
1201        } else {
1202            // No runtime — can't do async I/O from here. Mark the
1203            // connection desynchronized so the next caller gets a
1204            // clear error instead of a latent leaked statement.
1205            if let Ok(mut c) = conn.try_lock() {
1206                c.mark_desynchronized();
1207            }
1208            warn!(
1209                target: "hyperdb_api_core::client",
1210                statement = %name,
1211                "AsyncPreparedStatement dropped outside of a tokio runtime; \
1212                 server-side statement slot leaked and connection marked \
1213                 desynchronized — call statement.close(&client) explicitly \
1214                 for deterministic cleanup"
1215            );
1216        }
1217    }
1218}
1219
1220/// Async COPY IN writer for bulk data insertion.
1221///
1222/// # Drop Safety
1223///
1224/// If this writer is dropped without calling [`finish()`](Self::finish) or
1225/// [`cancel()`](Self::cancel), it will attempt a best-effort synchronous cancel
1226/// by queuing a `CopyFail` message in the connection's write buffer. The next
1227/// async operation on the connection will flush and drain the cancel response,
1228/// restoring the connection to a usable state.
1229#[derive(Debug)]
1230pub struct AsyncCopyInWriter<'a> {
1231    connection: &'a Mutex<AsyncRawConnection<AsyncStream>>,
1232    /// Set to `true` after `finish()` or `cancel()` consumes the writer.
1233    /// Checked in `Drop` to avoid queuing a spurious `CopyFail`.
1234    finished: bool,
1235}
1236
1237/// Owned-handle variant of [`AsyncCopyInWriter`] that holds an
1238/// `Arc<Mutex<_>>` to the underlying connection instead of a borrow.
1239/// Used by callers that need a `'static`-lifetime writer — e.g. N-API
1240/// classes that can't carry borrowed references across JS callbacks.
1241///
1242/// Semantics are identical to [`AsyncCopyInWriter`]; the only
1243/// difference is lifetime.
1244#[derive(Debug)]
1245pub struct AsyncCopyInWriterOwned {
1246    connection: Arc<Mutex<AsyncRawConnection<AsyncStream>>>,
1247    finished: bool,
1248}
1249
1250impl AsyncCopyInWriterOwned {
1251    /// Creates a new owned-handle COPY IN writer.
1252    pub(crate) fn new(connection: Arc<Mutex<AsyncRawConnection<AsyncStream>>>) -> Self {
1253        AsyncCopyInWriterOwned {
1254            connection,
1255            finished: false,
1256        }
1257    }
1258
1259    /// Sends data to the server.
1260    ///
1261    /// # Errors
1262    ///
1263    /// Currently infallible — frame construction is pure. The `Result`
1264    /// return type is preserved for forward compatibility.
1265    pub async fn send(&mut self, data: &[u8]) -> Result<()> {
1266        let mut conn = self.connection.lock().await;
1267        conn.send_copy_data(data)?;
1268        Ok(())
1269    }
1270
1271    /// Flushes any buffered data to the server.
1272    ///
1273    /// # Errors
1274    ///
1275    /// Returns [`Error`] (I/O) if flushing the async transport fails.
1276    pub async fn flush(&mut self) -> Result<()> {
1277        let mut conn = self.connection.lock().await;
1278        conn.flush().await
1279    }
1280
1281    /// Sends COPY data directly to the stream without internal buffering.
1282    ///
1283    /// # Errors
1284    ///
1285    /// - Returns [`Error`] (protocol) if `data.len() + 4` exceeds
1286    ///   `u32::MAX`.
1287    /// - Returns [`Error`] (I/O) on transport write failure.
1288    pub async fn send_direct(&mut self, data: &[u8]) -> Result<()> {
1289        let mut conn = self.connection.lock().await;
1290        conn.send_copy_data_direct(data).await
1291    }
1292
1293    /// Flushes the TCP stream.
1294    ///
1295    /// # Errors
1296    ///
1297    /// Returns [`Error`] (I/O) if flushing the async transport fails.
1298    pub async fn flush_stream(&mut self) -> Result<()> {
1299        let mut conn = self.connection.lock().await;
1300        conn.flush_stream().await
1301    }
1302
1303    /// Finishes the COPY operation and returns the number of rows inserted.
1304    ///
1305    /// # Errors
1306    ///
1307    /// Returns [`Error`] (server) if the server reports an `ErrorResponse`
1308    /// (e.g. constraint violation), or [`Error`] (I/O) / [`Error`] (closed)
1309    /// on transport failure.
1310    pub async fn finish(mut self) -> Result<u64> {
1311        self.finished = true;
1312        let mut conn = self.connection.lock().await;
1313        conn.finish_copy().await
1314    }
1315
1316    /// Cancels the COPY operation.
1317    ///
1318    /// # Errors
1319    ///
1320    /// Returns [`Error`] (I/O) on transport write failure, or
1321    /// [`Error`] (closed) if the server drops the connection before
1322    /// acknowledging the cancel.
1323    pub async fn cancel(mut self, reason: &str) -> Result<()> {
1324        self.finished = true;
1325        let mut conn = self.connection.lock().await;
1326        conn.cancel_copy(reason).await
1327    }
1328}
1329
1330impl Drop for AsyncCopyInWriterOwned {
1331    fn drop(&mut self) {
1332        if self.finished {
1333            return;
1334        }
1335        // Mirror AsyncCopyInWriter's Drop — queue a CopyFail via
1336        // try_lock; the next async operation on the connection will
1337        // flush and drain.
1338        if let Ok(mut conn) = self.connection.try_lock() {
1339            conn.queue_copy_fail("AsyncCopyInWriterOwned dropped without finish/cancel");
1340        }
1341    }
1342}
1343
1344impl<'a> AsyncCopyInWriter<'a> {
1345    /// Creates a new COPY IN writer.
1346    pub(crate) fn new(connection: &'a Mutex<AsyncRawConnection<AsyncStream>>) -> Self {
1347        AsyncCopyInWriter {
1348            connection,
1349            finished: false,
1350        }
1351    }
1352
1353    /// Sends data to the server.
1354    ///
1355    /// # Errors
1356    ///
1357    /// Currently infallible — frame construction is pure. The `Result`
1358    /// return type is preserved for forward compatibility.
1359    pub async fn send(&mut self, data: &[u8]) -> Result<()> {
1360        let mut conn = self.connection.lock().await;
1361        conn.send_copy_data(data)?;
1362        Ok(())
1363    }
1364
1365    /// Flushes any buffered data to the server.
1366    ///
1367    /// # Errors
1368    ///
1369    /// Returns [`Error`] (I/O) if flushing the async transport fails.
1370    pub async fn flush(&mut self) -> Result<()> {
1371        let mut conn = self.connection.lock().await;
1372        conn.flush().await
1373    }
1374
1375    /// Sends COPY data directly to the stream without internal buffering.
1376    ///
1377    /// This writes data directly to the TCP stream, letting the kernel handle
1378    /// buffering. More efficient for streaming large amounts of data.
1379    /// Call `flush_stream()` periodically to ensure data is sent.
1380    ///
1381    /// # Errors
1382    ///
1383    /// - Returns [`Error`] (protocol) if `data.len() + 4` exceeds
1384    ///   `u32::MAX`.
1385    /// - Returns [`Error`] (I/O) on transport write failure.
1386    pub async fn send_direct(&mut self, data: &[u8]) -> Result<()> {
1387        let mut conn = self.connection.lock().await;
1388        conn.send_copy_data_direct(data).await
1389    }
1390
1391    /// Flushes the TCP stream.
1392    ///
1393    /// Use with `send_direct()` to periodically ensure data reaches the server.
1394    ///
1395    /// # Errors
1396    ///
1397    /// Returns [`Error`] (I/O) if flushing the async transport fails.
1398    pub async fn flush_stream(&mut self) -> Result<()> {
1399        let mut conn = self.connection.lock().await;
1400        conn.flush_stream().await
1401    }
1402
1403    /// Finishes the COPY operation and returns the number of rows inserted.
1404    ///
1405    /// # Errors
1406    ///
1407    /// Returns [`Error`] (server) if the server reports an `ErrorResponse`,
1408    /// or [`Error`] (I/O) / [`Error`] (closed) on transport failure.
1409    pub async fn finish(mut self) -> Result<u64> {
1410        self.finished = true;
1411        let mut conn = self.connection.lock().await;
1412        conn.finish_copy().await
1413    }
1414
1415    /// Cancels the COPY operation.
1416    ///
1417    /// # Errors
1418    ///
1419    /// Returns [`Error`] (I/O) on transport write failure, or
1420    /// [`Error`] (closed) if the server drops the connection before
1421    /// acknowledging the cancel.
1422    pub async fn cancel(mut self, reason: &str) -> Result<()> {
1423        self.finished = true;
1424        let mut conn = self.connection.lock().await;
1425        conn.cancel_copy(reason).await
1426    }
1427}
1428
1429impl Drop for AsyncCopyInWriter<'_> {
1430    fn drop(&mut self) {
1431        if self.finished {
1432            return;
1433        }
1434        // Best-effort cancel: try to acquire the mutex synchronously and
1435        // queue a CopyFail message. We cannot do async I/O from Drop, so
1436        // the message is only written to the buffer — not flushed. The next
1437        // async operation will call drain_pending_copy_cancel() to flush it
1438        // and restore the connection to ReadyForQuery state.
1439        if let Ok(mut conn) = self.connection.try_lock() {
1440            conn.queue_copy_fail("COPY writer dropped without finish or cancel");
1441            warn!(
1442                target: "hyperdb_api_core::client",
1443                "AsyncCopyInWriter dropped without finish() or cancel(). \
1444                 Queued best-effort CopyFail — connection will self-heal on next operation."
1445            );
1446        } else {
1447            warn!(
1448                target: "hyperdb_api_core::client",
1449                "AsyncCopyInWriter dropped without finish() or cancel(), \
1450                 and the connection mutex was locked. The connection may be \
1451                 left in an unusable COPY-IN state."
1452            );
1453        }
1454    }
1455}