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(¶ms_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(¶ms_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(¶ms_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(), ¶ms_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 ¶ms_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(), ¶ms_ref, statement.columns().len())?;
1060 } else {
1061 conn.start_execute_prepared_with_formats(
1062 statement.name(),
1063 ¶ms_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}