Skip to main content

hyperdb_api_core/client/
client.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! High-level synchronous client for Hyper database.
5//!
6//! This module provides [`Client`], the primary synchronous interface for
7//! communicating with a Hyper server. It supports three query execution
8//! modes, each using a different level of the `PostgreSQL` wire protocol:
9//!
10//! - **Simple Query** ([`Client::query`]) — Sends a single `Query` message
11//!   and collects all `DataRow` messages into memory. Results are returned
12//!   in text format. Best for small result sets or DDL/DML commands.
13//!
14//! - **Extended Query / `HyperBinary`** ([`Client::query_fast`]) — Uses the
15//!   Extended Query protocol (`Parse` / `Bind` / `Execute`) with
16//!   [`ColumnFormat::HyperBinary`](super::statement::ColumnFormat::HyperBinary)
17//!   (format code 2) for `LittleEndian` binary results. Returns
18//!   [`StreamRow`]s that compute field offsets
19//!   on-demand, avoiding per-row allocation.
20//!
21//! - **Streaming** ([`Client::query_streaming`]) — Like `query_fast` but
22//!   returns a [`QueryStream`] that yields rows in chunks, keeping memory
23//!   usage constant regardless of result set size. The stream holds the
24//!   connection lock for its lifetime; dropping it triggers a cancel request
25//!   to stop the server from streaming the rest.
26//!
27//! # Bulk Insertion (COPY Protocol)
28//!
29//! [`Client::copy_in`] starts a `COPY ... FROM STDIN WITH (FORMAT HYPERBINARY)`
30//! session and returns a [`CopyInWriter`] for streaming binary data. The
31//! caller is responsible for encoding rows in the correct format (typically
32//! done by the higher-level [`hyperdb_api::Inserter`](https://docs.rs/hyperdb-api)).
33//! See also [`copy_in_with_format`](Client::copy_in_with_format) for
34//! alternative formats (CSV, Arrow IPC) and [`copy_in_raw`](Client::copy_in_raw)
35//! for fully custom COPY statements.
36
37use std::net::TcpStream;
38use std::sync::{Arc, Mutex, MutexGuard};
39use std::time::Duration;
40
41use tracing::{debug, info, trace, warn};
42
43/// Enable TCP keepalive on a connection socket so a half-open peer (laptop
44/// sleep, network blip, a hyperd that vanished without a FIN) is detected in
45/// ~90s instead of blocking a blocking `read()` until the OS default idle
46/// timeout (7200s / 2h on macOS and Linux).
47///
48/// This matters most for long-lived idle connections — e.g. an MCP client
49/// holding a connection to a resident daemon's hyperd across a laptop suspend.
50/// Without keepalive, the next query on a silently-dead socket hangs for hours.
51///
52/// Tuning: 60s idle before the first probe, 10s between probes, 3 probes →
53/// the peer is declared dead ~90s after it goes silent. Probe count is only
54/// honored on platforms whose `socket2` build exposes `with_retries`; macOS
55/// honors idle+interval. All calls are best-effort (`.ok()`): a kernel that
56/// rejects a knob leaves the connection working at OS defaults.
57fn apply_tcp_keepalive(sock: &socket2::SockRef<'_>) {
58    let keepalive = socket2::TcpKeepalive::new()
59        .with_time(Duration::from_secs(60))
60        .with_interval(Duration::from_secs(10));
61    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
62    let keepalive = keepalive.with_retries(3);
63    sock.set_tcp_keepalive(&keepalive).ok();
64}
65
66#[cfg(unix)]
67use std::os::unix::net::UnixStream;
68
69use super::cancel::Cancellable;
70use super::config::Config;
71use super::connection::{RawConnection, parse_error_response};
72use super::endpoint::ConnectionEndpoint;
73use super::error::{Error, Result};
74use super::prepare;
75use super::row::{Row, StreamRow};
76use super::statement::ParamFormat;
77use super::sync_stream::SyncStream;
78
79use crate::protocol::message::Message;
80use crate::types::Oid;
81
82use super::notice::{Notice, NoticeReceiver};
83
84/// A synchronous client for Hyper database.
85///
86/// The client handles connection management and query execution.
87/// It is thread-safe and can be shared between threads using `Arc`.
88///
89/// # Thread Safety
90///
91/// The `Client` is thread-safe and can be shared between threads using `Arc<Client>`.
92/// All methods use internal mutexes to synchronize access to the underlying connection.
93///
94/// # Example
95///
96/// ```no_run
97/// use hyperdb_api_core::client::{Client, Config};
98///
99/// # fn example() -> hyperdb_api_core::client::Result<()> {
100/// let config = Config::new()
101///     .with_host("localhost")
102///     .with_port(7483)
103///     .with_database("test.hyper");
104///
105/// let client = Client::connect(&config)?;
106/// let rows = client.query("SELECT 1")?;
107/// client.close()?;
108/// # Ok(())
109/// # }
110/// ```
111pub struct Client {
112    /// The underlying connection, protected by a mutex for thread safety.
113    connection: Arc<Mutex<RawConnection<SyncStream>>>,
114    /// Backend process ID (for cancel requests).
115    process_id: i32,
116    /// Secret key for authenticating cancel requests.
117    secret_key: i32,
118    /// Connection endpoint for cancel requests and reconnection.
119    endpoint: ConnectionEndpoint,
120    /// Optional notice receiver callback for server notices/warnings.
121    notice_receiver: Option<Arc<NoticeReceiver>>,
122}
123
124// Manual Debug implementation because NoticeReceiver doesn't implement Debug
125impl std::fmt::Debug for Client {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("Client")
128            .field("process_id", &self.process_id)
129            .field("secret_key", &self.secret_key)
130            .field("endpoint", &self.endpoint)
131            .field(
132                "notice_receiver",
133                &self.notice_receiver.as_ref().map(|_| "<callback>"),
134            )
135            .finish_non_exhaustive()
136    }
137}
138
139impl Client {
140    /// Connects to a Hyper server using the given configuration.
141    ///
142    /// Establishes a TCP connection, performs authentication, and initializes
143    /// the client. Returns an error if the connection fails or authentication
144    /// is rejected.
145    ///
146    /// # Arguments
147    ///
148    /// * `config` - Connection configuration (host, port, credentials, etc.)
149    ///
150    /// # Errors
151    ///
152    /// Returns `Error` if:
153    /// - Connection to the server fails
154    /// - Authentication fails
155    /// - Protocol handshake fails
156    ///
157    /// # Example
158    ///
159    /// ```no_run
160    /// # use hyperdb_api_core::client::{Client, Config};
161    /// # fn example() -> hyperdb_api_core::client::Result<()> {
162    /// let config = Config::new()
163    ///     .with_host("localhost")
164    ///     .with_port(7483)
165    ///     .with_user("myuser")
166    ///     .with_password("mypass")
167    ///     .with_database("test.hyper");
168    ///
169    /// let client = Client::connect(&config)?;
170    /// # Ok(())
171    /// # }
172    /// ```
173    pub fn connect(config: &Config) -> Result<Self> {
174        // Log connection parameters (password is intentionally omitted for security)
175        info!(
176            target: "hyperdb_api",
177            host = %config.host(),
178            port = config.port(),
179            user = config.user().unwrap_or("(default)"),
180            database = config.database().unwrap_or("(none)"),
181            "connection-parameters"
182        );
183
184        let endpoint = ConnectionEndpoint::tcp(config.host(), config.port());
185        let addr = format!("{}:{}", config.host(), config.port());
186        let tcp_stream = TcpStream::connect(&addr).map_err(|e| {
187            warn!(target: "hyperdb_api", %addr, error = %e, "connection-failed");
188            Error::connection(format!("failed to connect to {addr}: {e}"))
189        })?;
190
191        // Set TCP options for better performance.
192        //
193        // `TCP_NODELAY` disables Nagle so request bytes flush immediately —
194        // needed for low-latency request/response shapes.
195        //
196        // `SO_RCVBUF` / `SO_SNDBUF` are bumped to 4 MiB. The Windows default
197        // TCP buffers are ~64 KiB, which throttles loopback throughput
198        // because hyperd blocks on `send()` once the kernel buffer fills up.
199        // Linux auto-tunes much higher, so this primarily helps Windows
200        // (and is a marginal win on macOS).
201        //
202        // Empirical knee on Windows i9-10980XE / TCP loopback (100M-row
203        // sync full-scan, single connection):
204        //
205        // |  size | rows/sec |
206        // |------:|---------:|
207        // |  64 K |     2.89 |  (default)
208        // |   1 M |     5.95 |
209        // |   4 M |     6.90 |  <-- knee
210        // |   8 M |     6.68 |  (insert workloads regress 18%)
211        //
212        // 4 MiB hits the throughput plateau without the memory-pressure
213        // regression seen at 8 MiB. We use `.ok()` because the kernel may
214        // clamp to a lower value or refuse the request entirely; either
215        // way the connection still works at the default size.
216        tcp_stream.set_nodelay(true).ok();
217        let sock = socket2::SockRef::from(&tcp_stream);
218        sock.set_recv_buffer_size(4 * 1024 * 1024).ok();
219        sock.set_send_buffer_size(4 * 1024 * 1024).ok();
220        apply_tcp_keepalive(&sock);
221
222        let stream = SyncStream::tcp(tcp_stream);
223        let mut connection = RawConnection::new(stream);
224
225        // Perform startup with authentication
226        let params = config.startup_params();
227        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, *v)).collect();
228        connection.startup(&params_ref, config.password())?;
229
230        let process_id = connection.process_id();
231        let secret_key = connection.secret_key();
232
233        debug!(
234            target: "hyperdb_api",
235            process_id,
236            "connection-established"
237        );
238
239        Ok(Client {
240            connection: Arc::new(Mutex::new(connection)),
241            process_id,
242            secret_key,
243            endpoint,
244            notice_receiver: None,
245        })
246    }
247
248    /// Connects to a Hyper server via Unix Domain Socket (Unix only).
249    ///
250    /// # Example
251    ///
252    /// ```no_run
253    /// # use hyperdb_api_core::client::{Client, Config};
254    /// # use std::path::Path;
255    /// # fn example() -> hyperdb_api_core::client::Result<()> {
256    /// let socket_path = Path::new("/tmp/hyper/.s.PGSQL.12345");
257    /// let config = Config::new().with_database("test.hyper");
258    /// let client = Client::connect_unix(socket_path, &config)?;
259    /// # Ok(())
260    /// # }
261    /// ```
262    ///
263    /// # Errors
264    ///
265    /// - Returns [`Error`] (connection) if the Unix domain socket cannot
266    ///   be connected.
267    /// - Propagates any [`Error`] from the startup handshake
268    ///   (authentication, protocol error, I/O error).
269    #[cfg(unix)]
270    pub fn connect_unix(socket_path: impl AsRef<std::path::Path>, config: &Config) -> Result<Self> {
271        use std::path::Path;
272
273        let path = socket_path.as_ref();
274        info!(
275            target: "hyperdb_api",
276            socket_path = %path.display(),
277            user = config.user().unwrap_or("(default)"),
278            database = config.database().unwrap_or("(none)"),
279            "connection-parameters-unix"
280        );
281
282        let unix_stream = UnixStream::connect(path).map_err(|e| {
283            warn!(target: "hyperdb_api", socket_path = %path.display(), error = %e, "connection-failed");
284            Error::connection(format!("failed to connect to unix socket {}: {}", path.display(), e))
285        })?;
286
287        // Parse endpoint from socket path
288        let directory = path.parent().unwrap_or(Path::new("/"));
289        let name = path
290            .file_name()
291            .and_then(|n| n.to_str())
292            .unwrap_or("socket");
293        let endpoint = ConnectionEndpoint::domain_socket(directory, name);
294
295        let stream = SyncStream::unix(unix_stream);
296        let mut connection = RawConnection::new(stream);
297
298        // Perform startup with authentication
299        let params = config.startup_params();
300        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, *v)).collect();
301        connection.startup(&params_ref, config.password())?;
302
303        let process_id = connection.process_id();
304        let secret_key = connection.secret_key();
305
306        debug!(
307            target: "hyperdb_api",
308            process_id,
309            "connection-established-unix"
310        );
311
312        Ok(Client {
313            connection: Arc::new(Mutex::new(connection)),
314            process_id,
315            secret_key,
316            endpoint,
317            notice_receiver: None,
318        })
319    }
320
321    /// Connects to a Hyper server via Windows Named Pipe (Windows only).
322    ///
323    /// # Arguments
324    ///
325    /// * `pipe_path` - The full pipe path (e.g., `\\.\pipe\hyper-12345`)
326    /// * `config` - Connection configuration
327    ///
328    /// # Errors
329    ///
330    /// Returns an error if the Named Pipe cannot be opened (e.g., pipe does not
331    /// exist, all instances are busy after the retry window, or permission is
332    /// denied) or if the authentication handshake fails.
333    #[cfg(windows)]
334    pub fn connect_named_pipe(pipe_path: &str, config: &Config) -> Result<Self> {
335        use std::fs::OpenOptions;
336        use std::time::Instant;
337
338        info!(
339            target: "hyperdb_api",
340            pipe_path = %pipe_path,
341            user = config.user().unwrap_or("(default)"),
342            database = config.database().unwrap_or("(none)"),
343            "connection-parameters-named-pipe"
344        );
345
346        // Windows named pipes have a finite number of server-side instances
347        // (`MaxInstances` on `CreateNamedPipe`). When all are busy, `CreateFile`
348        // returns `ERROR_PIPE_BUSY` (231). The expected client behavior is to
349        // wait briefly and retry — equivalent to `WaitNamedPipe` from Win32.
350        // We poll with a short sleep up to a reasonable deadline so concurrent
351        // clients don't spuriously fail when the pool is momentarily exhausted.
352        const RETRY_INTERVAL: Duration = Duration::from_millis(20);
353        const MAX_WAIT: Duration = Duration::from_secs(10);
354        const ERROR_PIPE_BUSY: i32 = 231;
355
356        let deadline = Instant::now() + MAX_WAIT;
357        let file = loop {
358            match OpenOptions::new().read(true).write(true).open(pipe_path) {
359                Ok(f) => break f,
360                Err(e)
361                    if e.raw_os_error() == Some(ERROR_PIPE_BUSY) && Instant::now() < deadline =>
362                {
363                    std::thread::sleep(RETRY_INTERVAL);
364                }
365                Err(e) => {
366                    warn!(target: "hyperdb_api", pipe_path = %pipe_path, error = %e, "connection-failed");
367                    return Err(Error::connection(format!(
368                        "failed to connect to named pipe {pipe_path}: {e}"
369                    )));
370                }
371            }
372        };
373
374        // Parse endpoint from pipe path
375        let endpoint = ConnectionEndpoint::parse(&format!(
376            "tab.pipe://{}",
377            pipe_path.trim_start_matches(r"\\").replace('\\', "/")
378        ))
379        .unwrap_or_else(|_| {
380            // Fallback: construct from pipe path directly
381            // Expected format: \\<host>\pipe\<name>
382            let parts: Vec<&str> = pipe_path
383                .trim_start_matches(r"\\")
384                .splitn(3, '\\')
385                .collect();
386            if parts.len() >= 3 {
387                ConnectionEndpoint::named_pipe(parts[0], parts[2])
388            } else {
389                ConnectionEndpoint::named_pipe(".", pipe_path)
390            }
391        });
392
393        let stream = SyncStream::named_pipe(file);
394        let mut connection = RawConnection::new(stream);
395
396        // Perform startup with authentication
397        let params = config.startup_params();
398        let params_ref: Vec<(&str, &str)> = params.iter().map(|(k, v)| (*k, *v)).collect();
399        connection.startup(&params_ref, config.password())?;
400
401        let process_id = connection.process_id();
402        let secret_key = connection.secret_key();
403
404        debug!(
405            target: "hyperdb_api",
406            process_id,
407            "connection-established-named-pipe"
408        );
409
410        Ok(Client {
411            connection: Arc::new(Mutex::new(connection)),
412            process_id,
413            secret_key,
414            endpoint,
415            notice_receiver: None,
416        })
417    }
418
419    /// Connects to a Hyper server using a `ConnectionEndpoint`.
420    ///
421    /// This is a lower-level method that accepts a pre-parsed endpoint.
422    ///
423    /// # Errors
424    ///
425    /// Delegates to [`Client::connect`], [`Client::connect_unix`], or
426    /// `Client::connect_named_pipe` depending on the endpoint variant,
427    /// and propagates their errors unchanged.
428    pub fn connect_endpoint(endpoint: &ConnectionEndpoint, config: &Config) -> Result<Self> {
429        match endpoint {
430            ConnectionEndpoint::Tcp { host, port } => {
431                let mut cfg = config.clone();
432                cfg = cfg.with_host(host.clone()).with_port(*port);
433                Self::connect(&cfg)
434            }
435            #[cfg(unix)]
436            ConnectionEndpoint::DomainSocket { directory, name } => {
437                let socket_path = directory.join(name);
438                Self::connect_unix(&socket_path, config)
439            }
440            #[cfg(windows)]
441            ConnectionEndpoint::NamedPipe { host, name } => {
442                let pipe_path = format!(r"\\{host}\pipe\{name}");
443                Self::connect_named_pipe(&pipe_path, config)
444            }
445        }
446    }
447
448    /// Returns the connection endpoint.
449    #[must_use]
450    pub fn endpoint(&self) -> &ConnectionEndpoint {
451        &self.endpoint
452    }
453
454    /// Returns the server process ID for this connection.
455    #[must_use]
456    pub fn process_id(&self) -> i32 {
457        self.process_id
458    }
459
460    /// Returns the secret key for cancel requests.
461    #[must_use]
462    pub fn secret_key(&self) -> i32 {
463        self.secret_key
464    }
465
466    /// Cancels the currently executing query on this connection.
467    ///
468    /// This method is **thread-safe** and can be called from any thread while
469    /// a query is running on another thread. It works by opening a separate
470    /// TCP connection to the server and sending a cancel request.
471    ///
472    /// # How It Works
473    ///
474    /// 1. Opens a new TCP connection to the same server
475    /// 2. Sends a cancel request containing the process ID and secret key
476    /// 3. The server receives this and cancels the running query
477    /// 4. The original query will fail with error code 57014 (`query_canceled`)
478    ///
479    /// # Thread Safety
480    ///
481    /// This method does NOT acquire the connection mutex, so it can be called
482    /// while another thread is blocked waiting for query results.
483    ///
484    /// # Relation to the [`Cancellable`] trait
485    ///
486    /// This is the **fallible user-facing cancel API**: it returns a
487    /// `Result<()>` so explicit callers can observe transport-level
488    /// failures (network errors, socket issues) and react accordingly —
489    /// e.g. record a metric, show "cancel failed" UX, or retry.
490    ///
491    /// For [`Drop`]-path and other internal cleanup contexts where error
492    /// propagation is impossible, the separate
493    /// [`impl Cancellable for Client`](super::cancel::Cancellable) wraps
494    /// this method and swallows errors (logged via `tracing::warn!`).
495    /// The two coexist by design — each serves a different consumer.
496    ///
497    /// # Example
498    ///
499    /// ```no_run
500    /// use std::thread;
501    /// use std::sync::Arc;
502    /// use std::time::Duration;
503    /// use hyperdb_api_core::client::{Client, Config};
504    ///
505    /// # fn example() -> hyperdb_api_core::client::Result<()> {
506    /// # let config = Config::new().with_host("localhost").with_port(7483);
507    /// let client = Arc::new(Client::connect(&config)?);
508    /// let client_clone = Arc::clone(&client);
509    ///
510    /// // Start a long query in another thread
511    /// let handle = thread::spawn(move || {
512    ///     client_clone.query("SELECT pg_sleep(60)")
513    /// });
514    ///
515    /// // Cancel from the main thread
516    /// thread::sleep(Duration::from_millis(100));
517    /// client.cancel()?;
518    ///
519    /// // The query thread will get a cancellation error
520    /// let result = handle.join().unwrap();
521    /// assert!(result.is_err());
522    /// # Ok(())
523    /// # }
524    /// ```
525    ///
526    /// # Errors
527    ///
528    /// - Returns [`Error`] (connection) if the fresh cancel-side socket
529    ///   cannot be opened (TCP / UDS / named-pipe, depending on
530    ///   [`Self::endpoint`]).
531    /// - Returns [`Error`] (I/O) if writing or flushing the cancel
532    ///   request fails.
533    pub fn cancel(&self) -> Result<()> {
534        use crate::protocol::message::frontend;
535        use bytes::BytesMut;
536        use std::io::Write;
537
538        info!(
539            target: "hyperdb_api",
540            process_id = self.process_id,
541            "query-cancel-request"
542        );
543
544        let endpoint_str = self.endpoint.to_string();
545
546        // Open a new connection specifically for the cancel request
547        match &self.endpoint {
548            ConnectionEndpoint::Tcp { host, port } => {
549                let addr = format!("{host}:{port}");
550                let mut stream = TcpStream::connect(&addr).map_err(|e| {
551                    warn!(
552                        target: "hyperdb_api",
553                        addr = %endpoint_str,
554                        error = %e,
555                        "query-cancel-connect-failed"
556                    );
557                    Error::connection(format!(
558                        "failed to connect for cancel request to {endpoint_str}: {e}"
559                    ))
560                })?;
561                // Cancel is a 16-byte fire-and-forget — disable Nagle so the
562                // request hits the wire without waiting on a coalesce timer.
563                stream.set_nodelay(true).ok();
564
565                // Build and send the cancel request
566                let mut buf = BytesMut::with_capacity(16);
567                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
568
569                stream.write_all(&buf).map_err(|e| {
570                    warn!(
571                        target: "hyperdb_api",
572                        error = %e,
573                        "query-cancel-send-failed"
574                    );
575                    Error::from_io(e)
576                })?;
577
578                stream.flush().map_err(Error::from_io)?;
579            }
580            #[cfg(unix)]
581            ConnectionEndpoint::DomainSocket { directory, name } => {
582                let socket_path = directory.join(name);
583                let mut stream = UnixStream::connect(&socket_path).map_err(|e| {
584                    warn!(
585                        target: "hyperdb_api",
586                        addr = %endpoint_str,
587                        error = %e,
588                        "query-cancel-connect-failed"
589                    );
590                    Error::connection(format!(
591                        "failed to connect for cancel request to {endpoint_str}: {e}"
592                    ))
593                })?;
594
595                // Build and send the cancel request
596                let mut buf = BytesMut::with_capacity(16);
597                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
598
599                stream.write_all(&buf).map_err(|e| {
600                    warn!(
601                        target: "hyperdb_api",
602                        error = %e,
603                        "query-cancel-send-failed"
604                    );
605                    Error::from_io(e)
606                })?;
607
608                stream.flush().map_err(Error::from_io)?;
609            }
610            #[cfg(windows)]
611            ConnectionEndpoint::NamedPipe { host, name } => {
612                let pipe_path = format!(r"\\{host}\pipe\{name}");
613                let mut file = std::fs::OpenOptions::new()
614                    .read(true)
615                    .write(true)
616                    .open(&pipe_path)
617                    .map_err(|e| {
618                        warn!(
619                            target: "hyperdb_api",
620                            addr = %endpoint_str,
621                            error = %e,
622                            "query-cancel-connect-failed"
623                        );
624                        Error::connection(format!(
625                            "failed to connect for cancel request to {endpoint_str}: {e}"
626                        ))
627                    })?;
628
629                // Build and send the cancel request
630                let mut buf = BytesMut::with_capacity(16);
631                frontend::cancel_request(self.process_id, self.secret_key, &mut buf);
632
633                file.write_all(&buf).map_err(|e| {
634                    warn!(
635                        target: "hyperdb_api",
636                        error = %e,
637                        "query-cancel-send-failed"
638                    );
639                    Error::from_io(e)
640                })?;
641
642                file.flush().map_err(Error::from_io)?;
643            }
644        }
645
646        debug!(
647            target: "hyperdb_api",
648            process_id = self.process_id,
649            "query-cancel-sent"
650        );
651
652        Ok(())
653    }
654
655    /// Returns a server parameter value by name.
656    ///
657    /// Server parameters are sent by the server during connection startup.
658    /// Common parameters include:
659    /// - `server_version` - The server version string
660    /// - `server_encoding` - The server's character encoding
661    /// - `client_encoding` - The client's character encoding
662    ///
663    /// # Example
664    ///
665    /// ```no_run
666    /// # use hyperdb_api_core::client::{Client, Config};
667    /// # fn example(client: &Client) {
668    /// if let Some(version) = client.parameter_status("server_version") {
669    ///     println!("Connected to Hyper version: {}", version);
670    /// }
671    /// # }
672    /// ```
673    #[must_use]
674    pub fn parameter_status(&self, name: &str) -> Option<String> {
675        let conn = self.connection.lock().ok()?;
676        conn.parameter_status(name)
677            .map(std::string::ToString::to_string)
678    }
679
680    /// Sets the notice receiver for this connection.
681    ///
682    /// Notice and warning messages generated by the server are not returned by
683    /// query execution functions since they don't indicate failure. Instead,
684    /// they are passed to a notice handling function.
685    ///
686    /// The default behavior is to log notices at the `warn` level.
687    ///
688    /// # Arguments
689    ///
690    /// * `receiver` - The callback function that will be called with each notice.
691    ///   Pass `None` to restore default logging behavior.
692    ///
693    /// # Example
694    ///
695    /// ```no_run
696    /// # use hyperdb_api_core::client::Client;
697    /// # use std::sync::{Arc, Mutex};
698    /// # fn example(client: &mut Client) {
699    /// client.set_notice_receiver(Some(Box::new(|notice| {
700    ///     println!("Server notice: {}", notice);
701    /// })));
702    ///
703    /// // Or capture notices in a Vec
704    /// let notices = Arc::new(Mutex::new(Vec::new()));
705    /// let notices_clone = notices.clone();
706    /// client.set_notice_receiver(Some(Box::new(move |notice| {
707    ///     notices_clone.lock().unwrap().push(notice);
708    /// })));
709    /// # }
710    /// ```
711    pub fn set_notice_receiver(&mut self, receiver: Option<NoticeReceiver>) {
712        self.notice_receiver = receiver.map(Arc::new);
713    }
714
715    /// Processes any notices in a list of messages, calling the notice receiver.
716    ///
717    /// This is called internally after receiving messages from the server.
718    pub(crate) fn process_notices(&self, messages: &[Message]) {
719        for msg in messages {
720            if let Message::NoticeResponse(body) = msg {
721                let notice = Notice::from_response_body(body);
722
723                if let Some(ref receiver) = self.notice_receiver {
724                    receiver(notice);
725                } else {
726                    // Default behavior: log at warn level
727                    warn!(
728                        target: "hyperdb_api",
729                        severity = notice.severity().unwrap_or("NOTICE"),
730                        code = notice.code().unwrap_or(""),
731                        message = %notice.message(),
732                        "server-notice"
733                    );
734                }
735            }
736        }
737    }
738
739    /// Acquires a lock on the connection.
740    fn lock_connection(&self) -> Result<MutexGuard<'_, RawConnection<SyncStream>>> {
741        self.connection
742            .lock()
743            .map_err(|_| Error::connection("connection mutex poisoned"))
744    }
745
746    /// Executes a simple query and returns the rows.
747    ///
748    /// # Errors
749    ///
750    /// - Returns [`Error`] (connection) if the connection mutex is
751    ///   poisoned.
752    /// - Returns [`Error`] (server) for any SQL error the server reports
753    ///   (syntax error, constraint violation, type mismatch).
754    /// - Returns [`Error`] (I/O) on wire-protocol I/O failure.
755    /// - Propagates any [`Error`] from row construction (invalid row
756    ///   description or data row bytes).
757    pub fn query(&self, query: &str) -> Result<Vec<Row>> {
758        let mut conn = self.lock_connection()?;
759        let messages = conn.simple_query(query)?;
760        drop(conn); // Release lock before processing notices
761
762        self.process_notices(&messages);
763
764        let mut rows = Vec::new();
765        let mut columns = None;
766
767        for msg in messages {
768            match msg {
769                crate::protocol::message::Message::RowDescription(desc) => {
770                    // Extract column info including format from protocol
771                    let mut cols = Vec::new();
772                    for f in desc.fields().filter_map(|r| {
773                        r.map_err(|e| trace!(target: "hyperdb_api_core::client", error = %e, "dropped error parsing row description field")).ok()
774                    }) {
775                        cols.push(super::statement::Column::new(
776                            f.name().to_string(),
777                            f.type_oid(),
778                            f.type_modifier(),
779                            super::statement::ColumnFormat::from_code(f.format()),
780                        ));
781                    }
782                    columns = Some(Arc::new(cols));
783                }
784                crate::protocol::message::Message::DataRow(data) => {
785                    if let Some(ref cols) = columns {
786                        rows.push(Row::new(Arc::clone(cols), data)?);
787                    }
788                }
789                _ => {}
790            }
791        }
792
793        Ok(rows)
794    }
795
796    /// Executes a query using `HyperBinary` format for maximum performance.
797    ///
798    /// Returns `StreamRow`s which compute offsets on-demand without pre-allocation,
799    /// making them faster for large result sets where each row is processed once.
800    /// Uses the extended query protocol with `HyperBinary` format (format code 2)
801    /// for direct binary access without text parsing overhead.
802    ///
803    /// # Errors
804    ///
805    /// Same as [`Self::query`]: connection-mutex poisoning, SQL errors
806    /// from the server, and wire-protocol I/O failures all surface as
807    /// [`Error`].
808    pub fn query_fast(&self, query: &str) -> Result<Vec<StreamRow>> {
809        let mut conn = self.lock_connection()?;
810        let messages = conn.query_binary(query)?;
811        drop(conn);
812
813        self.process_notices(&messages);
814
815        let mut rows = Vec::new();
816        for msg in messages {
817            if let crate::protocol::message::Message::DataRow(data) = msg {
818                rows.push(StreamRow::new(data));
819            }
820        }
821        Ok(rows)
822    }
823
824    /// Executes a query with streaming results for minimum memory usage.
825    ///
826    /// Combines `HyperBinary` format with incremental row fetching.
827    ///
828    /// # Errors
829    ///
830    /// - Returns [`Error`] (connection) if the connection mutex is
831    ///   poisoned.
832    /// - Returns [`Error`] (server) or [`Error`] (I/O) if the initial
833    ///   `Parse`/`Bind`/`Execute` sequence for the streaming query
834    ///   fails on the server or on the wire.
835    pub fn query_streaming<'a>(
836        &'a self,
837        query: &str,
838        chunk_size: usize,
839    ) -> Result<QueryStream<'a>> {
840        let mut conn = self.lock_connection()?;
841        conn.start_query_binary(query)?;
842        Ok(QueryStream {
843            conn: Some(conn),
844            // The owning client is the canceller: if the stream is dropped
845            // before being fully drained, its `Drop` impl will call
846            // `self.cancel()` (PG wire `CancelRequest` on a fresh
847            // connection) to stop the server from streaming the rest.
848            canceller: self,
849            finished: false,
850            chunk_size: chunk_size.max(1),
851            schema: None,
852            schema_read: false,
853        })
854    }
855
856    /// Executes a SQL command that doesn't return rows (e.g., INSERT, UPDATE).
857    ///
858    /// # Errors
859    ///
860    /// Same error modes as [`Self::query`] — connection-mutex poisoning,
861    /// server-side SQL errors, and wire-protocol I/O failures all
862    /// surface as [`Error`].
863    pub fn exec(&self, query: &str) -> Result<u64> {
864        let mut conn = self.lock_connection()?;
865        let messages = conn.simple_query(query)?;
866        drop(conn); // Release lock before processing notices
867
868        self.process_notices(&messages);
869
870        let mut affected = 0u64;
871        for msg in messages {
872            if let crate::protocol::message::Message::CommandComplete(body) = msg
873                && let Ok(tag) = body.tag()
874            {
875                // Parse affected row count from tag like "INSERT 0 1"
876                if let Some(count) = parse_affected_rows(tag) {
877                    affected = count;
878                }
879            }
880        }
881
882        Ok(affected)
883    }
884
885    /// Executes a batch of statements separated by semicolons.
886    ///
887    /// # Errors
888    ///
889    /// Same error modes as [`Self::query`] — connection-mutex poisoning,
890    /// server-side SQL errors, and wire-protocol I/O failures.
891    pub fn batch_execute(&self, query: &str) -> Result<()> {
892        let mut conn = self.lock_connection()?;
893        let messages = conn.simple_query(query)?;
894        drop(conn); // Release lock before processing notices
895
896        self.process_notices(&messages);
897        Ok(())
898    }
899
900    /// Prepares a statement for execution with the \[`params!`\] macro.
901    ///
902    /// Returns an [`prepare::OwnedPreparedStatement`] that automatically closes when dropped.
903    ///
904    /// # Example
905    ///
906    /// ```no_run
907    /// # use hyperdb_api_core::{params, client::{Client, Config}};
908    /// # fn example(client: &Client) -> hyperdb_api_core::client::Result<()> {
909    /// let stmt = client.prepare("SELECT * FROM users WHERE id = $1")?;
910    /// let rows = client.execute(&stmt, params![42_i32])?;
911    /// # Ok(())
912    /// # }
913    /// ```
914    ///
915    /// # Errors
916    ///
917    /// Propagates any [`Error`] from [`prepare::prepare_owned`] —
918    /// connection-mutex poisoning, server-side `Parse` failures (SQL
919    /// syntax, type resolution), and wire-protocol I/O failures.
920    pub fn prepare(&self, query: &str) -> Result<prepare::OwnedPreparedStatement> {
921        prepare::prepare_owned(&self.connection, query, &[])
922    }
923
924    /// Prepares a statement with explicit parameter types.
925    ///
926    /// # Errors
927    ///
928    /// Same failure modes as [`Self::prepare`].
929    pub fn prepare_typed(
930        &self,
931        query: &str,
932        param_types: &[Oid],
933    ) -> Result<prepare::OwnedPreparedStatement> {
934        prepare::prepare_owned(&self.connection, query, param_types)
935    }
936
937    /// Executes a prepared statement with parameters.
938    ///
939    /// Use the \[`params!`\] macro for ergonomic parameter encoding:
940    ///
941    /// ```no_run
942    /// # use hyperdb_api_core::{params, client::{Client, Config}};
943    /// # fn example(client: &Client) -> hyperdb_api_core::client::Result<()> {
944    /// let stmt = client.prepare("SELECT * FROM users WHERE id = $1 AND name = $2")?;
945    /// let rows = client.execute(&stmt, params![42_i32, "Alice"])?;
946    /// # Ok(())
947    /// # }
948    /// ```
949    ///
950    /// # Errors
951    ///
952    /// Propagates any [`Error`] from [`prepare::execute_prepared`] —
953    /// parameter-count or type mismatch, server-side SQL errors, and
954    /// wire-protocol I/O failures.
955    pub fn execute<P: AsRef<[Option<Vec<u8>>]>>(
956        &self,
957        statement: &prepare::OwnedPreparedStatement,
958        params: P,
959    ) -> Result<Vec<Row>> {
960        let params_ref: Vec<Option<&[u8]>> = params
961            .as_ref()
962            .iter()
963            .map(|p| p.as_ref().map(std::vec::Vec::as_slice))
964            .collect();
965        prepare::execute_prepared(&self.connection, statement.statement(), &params_ref)
966    }
967
968    /// Executes a prepared statement that doesn't return rows.
969    ///
970    /// # Errors
971    ///
972    /// Same failure modes as [`Self::execute`].
973    pub fn execute_no_result<P: AsRef<[Option<Vec<u8>>]>>(
974        &self,
975        statement: &prepare::OwnedPreparedStatement,
976        params: P,
977    ) -> Result<u64> {
978        self.execute_no_result_with_formats(statement, params, &[])
979    }
980
981    /// Executes a prepared statement that doesn't return rows, choosing the
982    /// wire format per parameter.
983    ///
984    /// `param_formats` must be the same length as `params`, or empty to mean
985    /// "every parameter is binary". See [`ParamFormat`].
986    ///
987    /// # Errors
988    ///
989    /// Same failure modes as [`Self::execute_no_result`].
990    pub fn execute_no_result_with_formats<P: AsRef<[Option<Vec<u8>>]>>(
991        &self,
992        statement: &prepare::OwnedPreparedStatement,
993        params: P,
994        param_formats: &[ParamFormat],
995    ) -> Result<u64> {
996        let params_ref: Vec<Option<&[u8]>> = params
997            .as_ref()
998            .iter()
999            .map(|p| p.as_ref().map(std::vec::Vec::as_slice))
1000            .collect();
1001        prepare::execute_prepared_no_result_with_formats(
1002            &self.connection,
1003            statement.statement(),
1004            &params_ref,
1005            param_formats,
1006        )
1007    }
1008
1009    /// Executes a prepared statement with streaming results.
1010    ///
1011    /// Returns a [`PreparedQueryStream`](super::prepared_stream::PreparedQueryStream)
1012    /// that yields rows in chunks, keeping memory bounded regardless of
1013    /// result size. This is the prepared-statement analog of
1014    /// [`query_streaming`](Self::query_streaming).
1015    ///
1016    /// The connection mutex is held for the duration of iteration;
1017    /// dropping the stream before completion issues a best-effort cancel.
1018    ///
1019    /// # Errors
1020    ///
1021    /// - Returns [`Error`] (connection) if the connection mutex is
1022    ///   poisoned.
1023    /// - Returns [`Error`] (server) or [`Error`] (I/O) if the initial
1024    ///   `Bind`/`Execute` sequence for the prepared statement fails on
1025    ///   the server or on the wire.
1026    pub fn execute_streaming<'a, P: AsRef<[Option<Vec<u8>>]>>(
1027        &'a self,
1028        statement: &prepare::OwnedPreparedStatement,
1029        params: P,
1030        chunk_size: usize,
1031    ) -> Result<super::prepared_stream::PreparedQueryStream<'a>> {
1032        self.execute_streaming_with_formats(statement, params, &[], chunk_size)
1033    }
1034
1035    /// Executes a prepared statement with streaming results, choosing the
1036    /// wire format per parameter.
1037    ///
1038    /// `param_formats` must be the same length as `params`, or empty to mean
1039    /// "every parameter is binary". See [`ParamFormat`].
1040    ///
1041    /// # Errors
1042    ///
1043    /// Same failure modes as [`Self::execute_streaming`].
1044    pub fn execute_streaming_with_formats<'a, P: AsRef<[Option<Vec<u8>>]>>(
1045        &'a self,
1046        statement: &prepare::OwnedPreparedStatement,
1047        params: P,
1048        param_formats: &[ParamFormat],
1049        chunk_size: usize,
1050    ) -> Result<super::prepared_stream::PreparedQueryStream<'a>> {
1051        let params_ref: Vec<Option<&[u8]>> = params
1052            .as_ref()
1053            .iter()
1054            .map(|p| p.as_ref().map(std::vec::Vec::as_slice))
1055            .collect();
1056
1057        let mut conn = self.lock_connection()?;
1058        if param_formats.is_empty() {
1059            conn.start_execute_prepared(statement.name(), &params_ref, statement.columns().len())?;
1060        } else {
1061            conn.start_execute_prepared_with_formats(
1062                statement.name(),
1063                &params_ref,
1064                param_formats,
1065                statement.columns().len(),
1066            )?;
1067        }
1068
1069        let columns = std::sync::Arc::new(statement.columns().to_vec());
1070        Ok(super::prepared_stream::PreparedQueryStream::new(
1071            conn, self, chunk_size, columns,
1072        ))
1073    }
1074
1075    /// Closes the connection.
1076    ///
1077    /// # Errors
1078    ///
1079    /// - Returns [`Error`] (connection) if the connection mutex is
1080    ///   poisoned.
1081    /// - Returns [`Error`] (I/O) if writing the `Terminate` message or
1082    ///   flushing the socket fails.
1083    pub fn close(self) -> Result<()> {
1084        let mut conn = self.lock_connection()?;
1085        conn.terminate()
1086    }
1087
1088    /// Starts a COPY IN operation for bulk data insertion.
1089    ///
1090    /// Returns a `CopyInWriter` that can be used to send data in `HyperBinary` format.
1091    ///
1092    /// # Example
1093    ///
1094    /// ```no_run
1095    /// # use hyperdb_api_core::client::Client;
1096    /// # fn example(client: &Client) -> hyperdb_api_core::client::Result<()> {
1097    /// # let binary_data = &[];
1098    /// let mut writer = client.copy_in("\"my_table\"", &["col1", "col2"])?;
1099    /// writer.send(binary_data)?;
1100    /// let rows = writer.finish()?;
1101    /// # Ok(())
1102    /// # }
1103    /// ```
1104    ///
1105    /// # Errors
1106    ///
1107    /// Delegates to [`Self::copy_in_with_format`]; see that method
1108    /// for the concrete failure modes.
1109    pub fn copy_in(&self, table_name: &str, columns: &[&str]) -> Result<CopyInWriter<'_>> {
1110        self.copy_in_with_format(table_name, columns, "HYPERBINARY")
1111    }
1112
1113    /// Starts a COPY IN operation with a specified data format.
1114    ///
1115    /// Returns a `CopyInWriter` that can be used to send data in the specified format.
1116    ///
1117    /// # Arguments
1118    ///
1119    /// * `table_name` - The target table name (should be properly quoted if needed)
1120    /// * `columns` - Column names to insert into
1121    /// * `format` - The data format string: "HYPERBINARY" or "ARROWSTREAM"
1122    ///
1123    /// # Example
1124    ///
1125    /// ```no_run
1126    /// # use hyperdb_api_core::client::Client;
1127    /// # fn example(client: &Client) -> hyperdb_api_core::client::Result<()> {
1128    /// # let arrow_ipc_data = &[];
1129    /// // For Arrow IPC stream format
1130    /// let mut writer = client.copy_in_with_format("\"my_table\"", &["col1", "col2"], "ARROWSTREAM")?;
1131    /// writer.send(arrow_ipc_data)?;
1132    /// let rows = writer.finish()?;
1133    /// # Ok(())
1134    /// # }
1135    /// ```
1136    ///
1137    /// # Errors
1138    ///
1139    /// - Returns [`Error`] (connection) if the connection mutex is
1140    ///   poisoned.
1141    /// - Returns [`Error`] (server) if the server rejects the generated
1142    ///   `COPY ... FROM STDIN` statement (for example, missing table
1143    ///   or mismatched columns).
1144    /// - Returns [`Error`] (I/O) on wire-protocol I/O failure while
1145    ///   initiating the COPY.
1146    pub fn copy_in_with_format(
1147        &self,
1148        table_name: &str,
1149        columns: &[&str],
1150        format: &str,
1151    ) -> Result<CopyInWriter<'_>> {
1152        let mut conn = self.lock_connection()?;
1153        conn.start_copy_in_with_format(table_name, columns, format)?;
1154        Ok(CopyInWriter { connection: conn })
1155    }
1156
1157    /// Starts a COPY IN operation from a raw SQL query string.
1158    ///
1159    /// The query must be a complete `COPY ... FROM STDIN ...` statement.
1160    /// This is useful for text-format imports (CSV, TSV) where you need
1161    /// full control over the COPY options.
1162    ///
1163    /// # Security
1164    ///
1165    /// The query is validated to start with `COPY` (case-insensitive) as a
1166    /// defense-in-depth measure. Callers are still responsible for proper
1167    /// escaping of table names and other identifiers within the query.
1168    /// Prefer [`copy_in()`](Self::copy_in) or
1169    /// [`copy_in_with_format()`](Self::copy_in_with_format) when possible,
1170    /// as they handle escaping automatically.
1171    ///
1172    /// # Example
1173    ///
1174    /// ```no_run
1175    /// # use hyperdb_api_core::client::Client;
1176    /// # fn example(client: &Client) -> hyperdb_api_core::client::Result<()> {
1177    /// let mut writer = client.copy_in_raw(
1178    ///     "COPY \"my_table\" FROM STDIN WITH (FORMAT csv, HEADER true)"
1179    /// )?;
1180    /// writer.send(b"1,Alice\n2,Bob\n")?;
1181    /// let rows = writer.finish()?;
1182    /// # Ok(())
1183    /// # }
1184    /// ```
1185    ///
1186    /// # Errors
1187    ///
1188    /// - Returns [`Error::Query`] if `query` (trimmed) does not
1189    ///   start with `COPY` (defense-in-depth check against non-COPY
1190    ///   statements).
1191    /// - Returns [`Error`] (connection) if the connection mutex is
1192    ///   poisoned.
1193    /// - Returns [`Error`] (server) or [`Error`] (I/O) if the server rejects
1194    ///   the COPY statement or the wire write fails.
1195    pub fn copy_in_raw(&self, query: &str) -> Result<CopyInWriter<'_>> {
1196        // Defense-in-depth: reject queries that don't look like COPY statements
1197        if !query.trim_start().to_ascii_uppercase().starts_with("COPY") {
1198            return Err(Error::query(
1199                "copy_in_raw() requires a COPY statement. \
1200                 The query must start with 'COPY'.",
1201            ));
1202        }
1203        let mut conn = self.lock_connection()?;
1204        conn.start_copy_in_raw(query)?;
1205        Ok(CopyInWriter { connection: conn })
1206    }
1207
1208    /// Returns true if the connection is alive (no error has occurred).
1209    #[must_use]
1210    pub fn is_alive(&self) -> bool {
1211        self.lock_connection().is_ok()
1212    }
1213
1214    /// Executes a COPY ... TO STDOUT query and returns all output data.
1215    ///
1216    /// This is used for queries like:
1217    /// `COPY (SELECT ...) TO STDOUT WITH (format arrowstream)`
1218    ///
1219    /// # Arguments
1220    ///
1221    /// * `query` - The COPY TO STDOUT query to execute
1222    ///
1223    /// # Returns
1224    ///
1225    /// The raw bytes from all `CopyData` messages concatenated together.
1226    ///
1227    /// # Example
1228    ///
1229    /// ```no_run
1230    /// # use hyperdb_api_core::client::Client;
1231    /// # fn example(client: &Client) -> hyperdb_api_core::client::Result<()> {
1232    /// let arrow_data = client.copy_out(
1233    ///     "COPY (SELECT * FROM my_table) TO STDOUT WITH (format arrowstream)"
1234    /// )?;
1235    /// # Ok(())
1236    /// # }
1237    /// ```
1238    ///
1239    /// # Errors
1240    ///
1241    /// - Returns [`Error`] (connection) if the connection mutex is
1242    ///   poisoned.
1243    /// - Returns [`Error`] (server) if the server rejects the `COPY ... TO
1244    ///   STDOUT` statement.
1245    /// - Returns [`Error`] (I/O) if the wire read fails while collecting
1246    ///   COPY output.
1247    pub fn copy_out(&self, query: &str) -> Result<Vec<u8>> {
1248        let mut conn = self.lock_connection()?;
1249        conn.copy_out(query)
1250    }
1251
1252    /// Executes a COPY ... TO STDOUT query and streams output to a writer.
1253    ///
1254    /// Unlike [`copy_out`](Self::copy_out) which collects all data into memory,
1255    /// this method streams each `CopyData` chunk directly to the provided writer,
1256    /// keeping memory usage constant regardless of result size.
1257    ///
1258    /// Returns the total number of bytes written.
1259    ///
1260    /// # Example
1261    ///
1262    /// ```no_run
1263    /// # use hyperdb_api_core::client::Client;
1264    /// # fn example(client: &Client) -> hyperdb_api_core::client::Result<()> {
1265    /// let mut file = std::fs::File::create("output.csv")?;
1266    /// let bytes_written = client.copy_out_to_writer(
1267    ///     "COPY (SELECT * FROM my_table) TO STDOUT WITH (FORMAT csv, HEADER true)",
1268    ///     &mut file,
1269    /// )?;
1270    /// println!("Wrote {} bytes", bytes_written);
1271    /// # Ok(())
1272    /// # }
1273    /// ```
1274    ///
1275    /// # Errors
1276    ///
1277    /// Same failure modes as [`Self::copy_out`], plus [`Error`] (I/O)
1278    /// when the supplied `writer` returns an error while receiving
1279    /// COPY chunks.
1280    pub fn copy_out_to_writer(&self, query: &str, writer: &mut dyn std::io::Write) -> Result<u64> {
1281        let mut conn = self.lock_connection()?;
1282        conn.copy_out_to_writer(query, writer)
1283    }
1284}
1285
1286impl Cancellable for Client {
1287    /// Fire-and-forget cancel via PG wire protocol `CancelRequest` on a
1288    /// fresh connection. Swallows errors (logged via `tracing::warn!`)
1289    /// because cancellation is a best-effort signal and callers cannot
1290    /// meaningfully recover from a failed cancel.
1291    fn cancel(&self) {
1292        if let Err(e) = Client::cancel(self) {
1293            warn!(
1294                target: "hyperdb_api_core::client",
1295                error = %e,
1296                process_id = self.process_id,
1297                "cancel request failed (best-effort, swallowed)",
1298            );
1299        }
1300    }
1301}
1302
1303/// A writer for COPY IN operations.
1304///
1305/// This struct holds the connection lock while sending data to ensure
1306/// exclusive access during the COPY operation.
1307#[derive(Debug)]
1308pub struct CopyInWriter<'a> {
1309    connection: MutexGuard<'a, RawConnection<SyncStream>>,
1310}
1311
1312impl CopyInWriter<'_> {
1313    /// Sends a chunk of COPY data.
1314    ///
1315    /// The data should be in `HyperBinary` format. For best performance,
1316    /// batch multiple rows into larger chunks (e.g., 1-16 MB).
1317    ///
1318    /// # Errors
1319    ///
1320    /// Returns [`Error`] (I/O) if the wire write of the `CopyData` frame
1321    /// fails.
1322    pub fn send(&mut self, data: &[u8]) -> Result<()> {
1323        self.connection.send_copy_data(data)
1324    }
1325
1326    /// Flushes any buffered data to the server.
1327    ///
1328    /// # Errors
1329    ///
1330    /// Returns [`Error`] (I/O) if flushing the internal write buffer to
1331    /// the transport fails.
1332    pub fn flush(&mut self) -> Result<()> {
1333        self.connection.flush()
1334    }
1335
1336    /// Sends COPY data directly to the stream without internal buffering.
1337    ///
1338    /// This writes data directly to the TCP stream, letting the kernel handle
1339    /// buffering. More efficient for streaming large amounts of data.
1340    /// Call `flush_stream()` periodically to ensure data is sent.
1341    ///
1342    /// # Errors
1343    ///
1344    /// Returns [`Error`] (I/O) if writing to the underlying stream fails.
1345    pub fn send_direct(&mut self, data: &[u8]) -> Result<()> {
1346        self.connection.send_copy_data_direct(data)
1347    }
1348
1349    /// Flushes the TCP stream.
1350    ///
1351    /// Use with `send_direct()` to periodically ensure data reaches the server.
1352    ///
1353    /// # Errors
1354    ///
1355    /// Returns [`Error`] (I/O) if flushing the underlying transport
1356    /// fails.
1357    pub fn flush_stream(&mut self) -> Result<()> {
1358        self.connection.flush_stream()
1359    }
1360
1361    /// Reserves capacity in the write buffer to avoid reallocations.
1362    ///
1363    /// Call this before bulk operations to pre-allocate buffer space.
1364    pub fn reserve_buffer(&mut self, capacity: usize) {
1365        self.connection.reserve_write_buffer(capacity);
1366    }
1367
1368    /// Finishes the COPY operation successfully.
1369    ///
1370    /// Returns the number of rows inserted.
1371    ///
1372    /// # Errors
1373    ///
1374    /// - Returns [`Error`] (I/O) if writing `CopyDone` or flushing the
1375    ///   transport fails.
1376    /// - Returns [`Error`] (server) if the server reports a COPY-side
1377    ///   failure (constraint violation, type coercion error, etc.)
1378    ///   via an `ErrorResponse` after `CopyDone`.
1379    pub fn finish(mut self) -> Result<u64> {
1380        self.connection.finish_copy()
1381    }
1382
1383    /// Cancels the COPY operation.
1384    ///
1385    /// All data sent so far will be discarded.
1386    ///
1387    /// # Errors
1388    ///
1389    /// Returns [`Error`] (I/O) if writing the `CopyFail` frame or
1390    /// flushing the transport fails, or [`Error`] (server) if the server
1391    /// reports an unexpected status after the cancel.
1392    pub fn cancel(mut self, reason: &str) -> Result<()> {
1393        self.connection.cancel_copy(reason)
1394    }
1395}
1396
1397/// Parses affected row count from a command tag.
1398fn parse_affected_rows(tag: &str) -> Option<u64> {
1399    let parts: Vec<&str> = tag.split_whitespace().collect();
1400
1401    match parts.first()? {
1402        &"INSERT" => {
1403            // INSERT oid count
1404            parts.get(2)?.parse().ok()
1405        }
1406        &"UPDATE" | &"DELETE" | &"SELECT" | &"COPY" => {
1407            // UPDATE/DELETE/SELECT/COPY count
1408            parts.get(1)?.parse().ok()
1409        }
1410        _ => None,
1411    }
1412}
1413
1414/// Streaming iterator for query results without materializing all rows.
1415///
1416/// Holding a `QueryStream` keeps the underlying [`RawConnection`] locked
1417/// via a `MutexGuard`. Dropping the stream before fully iterating triggers
1418/// a server-side cancel (see [`Drop`] below) so the connection is returned
1419/// to the pool cleanly.
1420pub struct QueryStream<'a> {
1421    conn: Option<MutexGuard<'a, RawConnection<SyncStream>>>,
1422    /// Best-effort cancel handle, used in [`Drop`] when the stream is
1423    /// abandoned before completion. For the current sync client this is
1424    /// the owning [`Client`] itself (which implements [`Cancellable`] via
1425    /// a PG wire `CancelRequest` on a fresh connection). When a gRPC
1426    /// equivalent lands it will plug in the same trait with a
1427    /// `cancel_query(query_id)` RPC implementation.
1428    canceller: &'a dyn Cancellable,
1429    finished: bool,
1430    chunk_size: usize,
1431    schema: Option<Vec<super::statement::Column>>,
1432    schema_read: bool,
1433}
1434
1435impl std::fmt::Debug for QueryStream<'_> {
1436    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1437        f.debug_struct("QueryStream")
1438            .field("finished", &self.finished)
1439            .field("chunk_size", &self.chunk_size)
1440            .field("schema_read", &self.schema_read)
1441            .finish_non_exhaustive()
1442    }
1443}
1444
1445impl Drop for QueryStream<'_> {
1446    fn drop(&mut self) {
1447        // If the caller exhausted the stream, the connection is already at
1448        // `ReadyForQuery` — nothing to do.
1449        if self.finished {
1450            return;
1451        }
1452
1453        // Otherwise: the server is still happily streaming rows (potentially
1454        // millions of them) for a query we no longer care about. Passively
1455        // draining would waste bandwidth and could block the destructor for
1456        // a very long time. Instead we send a transport-appropriate cancel
1457        // signal — on PG wire this is a `CancelRequest` packet on a fresh
1458        // connection; on gRPC (future) it will be a `cancel_query` RPC on
1459        // the shared channel. `Cancellable::cancel` is fire-and-forget and
1460        // cannot fail.
1461        self.canceller.cancel();
1462
1463        // After cancel, the server stops producing new rows and emits
1464        // `ErrorResponse(QueryCanceled) + ReadyForQuery` promptly. We still
1465        // need to drain those trailing messages off the wire so the
1466        // connection returns to the pool cleanly. The budget here is small
1467        // because we only expect: (a) whatever rows the server had already
1468        // flushed before seeing the cancel, (b) `ErrorResponse`,
1469        // (c) `ReadyForQuery`. A well-behaved server reaches `ReadyForQuery`
1470        // within a handful of messages. If that budget is somehow exceeded
1471        // the bounded drain logs a warning and marks the connection
1472        // desynchronized — downstream users will surface the desync as a
1473        // transport-level failure and reconnect.
1474        const POST_CANCEL_DRAIN_CAP: usize = 1024;
1475        if let Some(ref mut conn) = self.conn {
1476            let _ok = conn.drain_until_ready_bounded(POST_CANCEL_DRAIN_CAP);
1477        }
1478    }
1479}
1480
1481impl QueryStream<'_> {
1482    /// Returns the schema (column metadata) for the result set.
1483    #[must_use]
1484    pub fn schema(&self) -> Option<&[super::statement::Column]> {
1485        self.schema.as_deref()
1486    }
1487
1488    /// Retrieves the next chunk of rows (up to `chunk_size`).
1489    ///
1490    /// # Errors
1491    ///
1492    /// - Returns [`Error`] (I/O) if reading from the underlying transport
1493    ///   fails while awaiting the next protocol message.
1494    /// - Returns [`Error`] (server) when the server sends an `ErrorResponse`
1495    ///   during streaming (for example, a server-side execution failure
1496    ///   encountered partway through the result set).
1497    pub fn next_chunk(&mut self) -> Result<Option<Vec<StreamRow>>> {
1498        if self.finished {
1499            return Ok(None);
1500        }
1501
1502        let Some(conn) = self.conn.as_mut() else {
1503            return Ok(None);
1504        };
1505
1506        let mut rows = Vec::with_capacity(self.chunk_size);
1507        while rows.len() < self.chunk_size {
1508            let msg = conn.read_message()?;
1509            match msg {
1510                Message::RowDescription(desc) if !self.schema_read => {
1511                    let mut cols = Vec::new();
1512                    for f in desc.fields().filter_map(std::result::Result::ok) {
1513                        cols.push(super::statement::Column::new(
1514                            f.name().to_string(),
1515                            f.type_oid(),
1516                            f.type_modifier(),
1517                            super::statement::ColumnFormat::from_code(f.format()),
1518                        ));
1519                    }
1520                    self.schema = Some(cols);
1521                    self.schema_read = true;
1522                }
1523                Message::DataRow(data) => {
1524                    rows.push(StreamRow::new(data));
1525                    if rows.len() >= self.chunk_size {
1526                        return Ok(Some(rows));
1527                    }
1528                }
1529                Message::ReadyForQuery(_) => {
1530                    self.finished = true;
1531                    self.conn = None;
1532                    return if rows.is_empty() {
1533                        Ok(None)
1534                    } else {
1535                        Ok(Some(rows))
1536                    };
1537                }
1538                Message::ErrorResponse(body) => {
1539                    // Mark the stream finished *before* touching the
1540                    // connection so the `Drop` impl's Cancellable-based
1541                    // cleanup path is trivially a no-op regardless of what
1542                    // happens next (normal return, `?`, panic in drain,
1543                    // future refactors that insert early returns, etc).
1544                    //
1545                    // `&mut self` exclusivity means `Drop` can't fire
1546                    // concurrently with this method, so this is purely a
1547                    // defensive-ordering improvement — but it also matches
1548                    // the `ReadyForQuery` arm above, which sets
1549                    // `finished = true` first for the same reason. Keeping
1550                    // both terminal arms in the same order makes the
1551                    // "terminal state is committed before cleanup" rule
1552                    // visible at a glance.
1553                    self.finished = true;
1554                    // Drain through `ReadyForQuery` before releasing the
1555                    // pooled connection so the next user sees a clean wire
1556                    // state. `consume_error` swallows any drain I/O errors
1557                    // via tracing::warn — the caller's original error is
1558                    // more informative than a transport hiccup during
1559                    // cleanup.
1560                    let err = match self.conn {
1561                        Some(ref mut c) => c.consume_error(&body),
1562                        None => parse_error_response(&body),
1563                    };
1564                    self.conn = None;
1565                    return Err(err);
1566                }
1567                _ => {}
1568            }
1569        }
1570        Ok(Some(rows))
1571    }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576    use super::*;
1577
1578    #[test]
1579    fn test_parse_affected_rows() {
1580        assert_eq!(parse_affected_rows("INSERT 0 5"), Some(5));
1581        assert_eq!(parse_affected_rows("UPDATE 10"), Some(10));
1582        assert_eq!(parse_affected_rows("DELETE 3"), Some(3));
1583        assert_eq!(parse_affected_rows("SELECT 100"), Some(100));
1584        assert_eq!(parse_affected_rows("CREATE TABLE"), None);
1585    }
1586
1587    #[test]
1588    fn test_copy_in_raw_rejects_non_copy_query() {
1589        // We can't test with a real connection, but we can verify the prefix guard
1590        // works by checking the error message. Since Client::connect fails without
1591        // a server, we test the validation logic directly.
1592        let query = "SELECT * FROM users";
1593        assert!(
1594            !query.trim_start().to_ascii_uppercase().starts_with("COPY"),
1595            "Non-COPY query should not pass the COPY prefix check"
1596        );
1597
1598        let copy_query = "COPY \"users\" FROM STDIN WITH (FORMAT csv)";
1599        assert!(
1600            copy_query
1601                .trim_start()
1602                .to_ascii_uppercase()
1603                .starts_with("COPY"),
1604            "COPY query should pass the prefix check"
1605        );
1606
1607        // Leading whitespace should be accepted
1608        let padded = "  COPY \"users\" FROM STDIN";
1609        assert!(padded.trim_start().to_ascii_uppercase().starts_with("COPY"));
1610
1611        // Case-insensitive
1612        let lowercase = "copy \"users\" FROM STDIN";
1613        assert!(
1614            lowercase
1615                .trim_start()
1616                .to_ascii_uppercase()
1617                .starts_with("COPY")
1618        );
1619    }
1620}