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(¶ms_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(¶ms_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(¶ms_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, ¶ms_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, ¶ms_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, ¶ms_ref, statement.columns.len())
965 .await?;
966 } else {
967 conn.start_execute_prepared_with_formats(
968 &statement.name,
969 ¶ms_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}