Skip to main content

hyperdb_api/
connection.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Database connection management.
5//!
6//! The [`Connection`] type provides a unified interface for connecting to Hyper
7//! databases via either TCP (`PostgreSQL` wire protocol) or gRPC transport.
8//! The transport is automatically detected from the endpoint URL:
9//!
10//! - `https://` or `http://` → gRPC transport
11//! - Otherwise → TCP transport (e.g., `localhost:7483`)
12
13use std::path::Path;
14
15use hyperdb_api_core::client::Client;
16
17use crate::error::{Error, Result};
18use crate::names::escape_sql_path;
19use crate::process::HyperProcess;
20use crate::result::{DEFAULT_BINARY_CHUNK_SIZE, Row, Rowset};
21use crate::transport::Transport;
22
23use std::any::Any;
24use std::sync::{Arc, Mutex};
25
26use crate::query_stats::{QueryStats, QueryStatsProvider};
27
28/// Trait for types that can be extracted from a scalar query result.
29///
30/// This trait enables the generic [`Connection::execute_scalar_query`] method,
31/// similar to C++'s `executeScalarQuery<T>()` template.
32///
33/// # Implementing Custom Types
34///
35/// You can implement this trait for custom types to use them with `execute_scalar_query`:
36///
37/// ```no_run
38/// # use hyperdb_api::{Row, ScalarValue};
39/// # struct MyType;
40/// # impl MyType { fn parse(s: &str) -> Self { MyType } }
41/// impl ScalarValue for MyType {
42///     fn from_row(row: &Row, col: usize) -> Option<Self> {
43///         row.get_string(col).map(|s| MyType::parse(&s))
44///     }
45/// }
46/// ```
47pub trait ScalarValue: Sized {
48    /// Extracts a value of this type from a row at the given column.
49    fn from_row(row: &Row, col: usize) -> Option<Self>;
50}
51
52impl ScalarValue for i64 {
53    fn from_row(row: &Row, col: usize) -> Option<Self> {
54        row.get_i64(col)
55    }
56}
57
58impl ScalarValue for i32 {
59    fn from_row(row: &Row, col: usize) -> Option<Self> {
60        row.get_i32(col)
61    }
62}
63
64impl ScalarValue for i16 {
65    fn from_row(row: &Row, col: usize) -> Option<Self> {
66        row.get_i16(col)
67    }
68}
69
70impl ScalarValue for f64 {
71    fn from_row(row: &Row, col: usize) -> Option<Self> {
72        row.get_f64(col)
73    }
74}
75
76impl ScalarValue for bool {
77    fn from_row(row: &Row, col: usize) -> Option<Self> {
78        row.get_bool(col)
79    }
80}
81
82impl ScalarValue for String {
83    fn from_row(row: &Row, col: usize) -> Option<Self> {
84        row.get_string(col)
85    }
86}
87
88/// Database creation mode when connecting.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
90pub enum CreateMode {
91    /// Do not create the database. Method will fail if database doesn't exist.
92    #[default]
93    DoNotCreate,
94    /// Create the database. Method will fail if the database already exists.
95    Create,
96    /// Create the database if it doesn't exist.
97    CreateIfNotExists,
98    /// Create the database. If it already exists, drop the old one first.
99    CreateAndReplace,
100}
101
102/// A connection to a Hyper database.
103///
104/// This struct represents an active connection to a Hyper server and optionally
105/// an attached database. The connection is automatically closed when dropped.
106///
107/// # Transport Auto-Detection
108///
109/// The transport is automatically detected from the endpoint URL:
110/// - `https://` or `http://` → gRPC transport (read-only until server supports writes)
111/// - Otherwise → TCP transport (full read/write support)
112///
113/// # CSV / Text Import & Export
114///
115/// For CSV, TSV, and other delimited-text formats, see the [`copy`](crate::copy)
116/// module which provides [`export_csv()`](Self::export_csv),
117/// [`import_csv()`](Self::import_csv), and related methods on this struct.
118///
119/// # Example
120///
121/// ```no_run
122/// use hyperdb_api::{Connection, CreateMode, Result};
123///
124/// fn main() -> Result<()> {
125///     // TCP connection (full read/write)
126///     let conn = Connection::connect("localhost:7483", "example.hyper", CreateMode::CreateIfNotExists)?;
127///
128///     // Execute SQL commands
129///     conn.execute_command("CREATE TABLE test (id INT, name TEXT)")?;
130///     conn.execute_command("INSERT INTO test VALUES (1, 'Hello')")?;
131///
132///     Ok(())
133/// }
134/// ```
135///
136/// ```no_run
137/// # use hyperdb_api::{Connection, CreateMode, Result};
138/// # fn example() -> Result<()> {
139/// // gRPC connection (read-only, auto-detected from URL)
140/// let conn = Connection::connect(
141///     "https://hyper-server.example.com:443",
142///     "example.hyper",
143///     CreateMode::DoNotCreate,  // Must be DoNotCreate for gRPC
144/// )?;
145/// # Ok(())
146/// # }
147/// ```
148pub struct Connection {
149    transport: Transport,
150    database: Option<String>,
151    stats_provider: Option<Arc<dyn QueryStatsProvider>>,
152    /// Pending stats token + SQL from the most recent query, resolved lazily.
153    pending_stats: Mutex<Option<(Box<dyn Any + Send>, String)>>,
154}
155
156impl std::fmt::Debug for Connection {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.debug_struct("Connection")
159            .field("database", &self.database)
160            .finish_non_exhaustive()
161    }
162}
163
164impl Connection {
165    /// Creates a new connection to a Hyper instance with a database.
166    ///
167    /// This is the primary way to connect to a running [`HyperProcess`].
168    ///
169    /// # Arguments
170    ///
171    /// * `instance` - The Hyper server instance to connect to.
172    /// * `database_path` - Path to the database file.
173    /// * `create_mode` - How to handle database creation.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the connection could not be established.
178    ///
179    /// # Example
180    ///
181    /// ```no_run
182    /// use hyperdb_api::{HyperProcess, Connection, CreateMode, Result};
183    ///
184    /// fn main() -> Result<()> {
185    ///     let hyper = HyperProcess::new(None, None)?;
186    ///     let conn = Connection::new(&hyper, "database.hyper", CreateMode::CreateIfNotExists)?;
187    ///     Ok(())
188    /// }
189    /// ```
190    pub fn new(
191        instance: &HyperProcess,
192        database_path: impl AsRef<Path>,
193        create_mode: CreateMode,
194    ) -> Result<Self> {
195        // Prefer using the connection_endpoint which properly handles UDS/Named Pipes
196        if let Some(conn_endpoint) = instance.connection_endpoint() {
197            return Self::connect_with_endpoint(
198                conn_endpoint,
199                &database_path.as_ref().to_string_lossy(),
200                create_mode,
201            );
202        }
203
204        // Fall back to string endpoint (TCP)
205        let endpoint = instance.require_endpoint()?;
206        Self::connect(
207            endpoint,
208            &database_path.as_ref().to_string_lossy(),
209            create_mode,
210        )
211    }
212
213    /// Connects using a `ConnectionEndpoint` (supports TCP, UDS, and Named Pipes).
214    fn connect_with_endpoint(
215        endpoint: &hyperdb_api_core::client::ConnectionEndpoint,
216        database_path: &str,
217        create_mode: CreateMode,
218    ) -> Result<Self> {
219        let db_path_str = Some(database_path.to_string());
220
221        let config = hyperdb_api_core::client::Config::new().with_user("tableau_internal_user");
222
223        let client = hyperdb_api_core::client::Client::connect_endpoint(endpoint, &config)?;
224
225        let conn = Connection::from_client(client, db_path_str.clone());
226
227        // Handle database creation
228        if let Some(db_path) = db_path_str {
229            conn.handle_creation_mode(&db_path, create_mode)?;
230            conn.attach_and_set_path(&db_path)?;
231        }
232
233        Ok(conn)
234    }
235
236    /// Connects to a Hyper server and optionally attaches a database.
237    ///
238    /// # Arguments
239    ///
240    /// * `endpoint` - The server endpoint (host:port).
241    /// * `database_path` - Path to the database file.
242    /// * `create_mode` - How to handle database creation.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if the connection could not be established.
247    pub fn connect(endpoint: &str, database_path: &str, create_mode: CreateMode) -> Result<Self> {
248        crate::ConnectionBuilder::new(endpoint)
249            .database(database_path)
250            .create_mode(create_mode)
251            .build()
252    }
253
254    /// Returns a connection builder for advanced configuration.
255    ///
256    /// This is useful when you need to set authentication, timeouts, or
257    /// other advanced options before connecting.
258    #[must_use]
259    pub fn builder(endpoint: &str) -> crate::ConnectionBuilder {
260        crate::ConnectionBuilder::new(endpoint)
261    }
262
263    /// Creates a Connection from a low-level Client (internal use, TCP only).
264    pub(crate) fn from_client(client: Client, database: Option<String>) -> Self {
265        Connection {
266            transport: Transport::Tcp(Box::new(crate::transport::TcpTransport { client })),
267            database,
268            stats_provider: None,
269            pending_stats: Mutex::new(None),
270        }
271    }
272
273    /// Creates a Connection from a Transport (internal use).
274    #[allow(
275        dead_code,
276        reason = "used by ConnectionBuilder for the gRPC path; not reached under non-gRPC feature builds"
277    )]
278    pub(crate) fn from_transport(transport: Transport, database: Option<String>) -> Self {
279        Connection {
280            transport,
281            database,
282            stats_provider: None,
283            pending_stats: Mutex::new(None),
284        }
285    }
286
287    /// Returns the transport type name (e.g., "TCP", "gRPC", "Unix Socket").
288    pub fn transport_type(&self) -> &'static str {
289        self.transport.transport_type().as_str()
290    }
291
292    /// Returns true if this connection supports write operations.
293    ///
294    /// Currently, only TCP connections support writes. gRPC connections are
295    /// read-only until the server supports write operations over gRPC.
296    pub fn supports_writes(&self) -> bool {
297        self.transport.supports_writes()
298    }
299
300    /// Handles database creation logic (internal use).
301    pub(crate) fn handle_creation_mode(
302        &self,
303        database_path: &str,
304        create_mode: CreateMode,
305    ) -> Result<()> {
306        match create_mode {
307            CreateMode::DoNotCreate => {}
308            CreateMode::Create => {
309                self.execute_command(&format!(
310                    "CREATE DATABASE {}",
311                    escape_sql_path(database_path)
312                ))?;
313            }
314            CreateMode::CreateIfNotExists => {
315                if let Err(e) = self.execute_command(&format!(
316                    "CREATE DATABASE IF NOT EXISTS {}",
317                    escape_sql_path(database_path)
318                )) && !is_already_exists_error(&e)
319                {
320                    return Err(Error::internal(format!(
321                        "Failed to create database '{database_path}': {e}"
322                    )));
323                }
324            }
325            CreateMode::CreateAndReplace => {
326                let _ = self.execute_command(&format!(
327                    "DROP DATABASE IF EXISTS {}",
328                    escape_sql_path(database_path)
329                ));
330                self.execute_command(&format!(
331                    "CREATE DATABASE {}",
332                    escape_sql_path(database_path)
333                ))?;
334            }
335        }
336        Ok(())
337    }
338
339    /// Attaches and sets the database path (internal use).
340    pub(crate) fn attach_and_set_path(&self, database_path: &str) -> Result<()> {
341        let db_alias = std::path::Path::new(database_path)
342            .file_stem()
343            .and_then(|s| s.to_str())
344            .unwrap_or("db");
345
346        self.execute_command(&format!(
347            "ATTACH DATABASE {} AS {}",
348            escape_sql_path(database_path),
349            escape_sql_path(db_alias)
350        ))?;
351
352        self.execute_command(&format!(
353            "SET search_path TO {}, public",
354            escape_sql_path(db_alias)
355        ))?;
356
357        Ok(())
358    }
359
360    /// Connects to a Hyper server with authentication.
361    ///
362    /// # Arguments
363    ///
364    /// * `endpoint` - The server endpoint (host:port).
365    /// * `database_path` - Path to the database file.
366    /// * `create_mode` - How to handle database creation.
367    /// * `user` - Username for authentication.
368    /// * `password` - Password for authentication.
369    ///
370    /// # Errors
371    ///
372    /// Returns an error if the connection or authentication fails.
373    pub fn connect_with_auth(
374        endpoint: &str,
375        database_path: &str,
376        create_mode: CreateMode,
377        user: &str,
378        password: &str,
379    ) -> Result<Self> {
380        crate::ConnectionBuilder::new(endpoint)
381            .database(database_path)
382            .create_mode(create_mode)
383            .user(user.to_string())
384            .password(password)
385            .build()
386    }
387
388    /// Creates a connection to a Hyper server without attaching a database.
389    ///
390    /// # Errors
391    ///
392    /// Returns [`Error::Connection`] if the TCP or gRPC handshake fails, and
393    /// [`Error::Io`] if the endpoint cannot be reached.
394    pub fn without_database(endpoint: &str) -> Result<Self> {
395        crate::ConnectionBuilder::new(endpoint).build()
396    }
397
398    /// Executes a SQL command that doesn't return results.
399    ///
400    /// Use this for DDL statements (CREATE, ALTER, DROP) and DML statements
401    /// (INSERT, UPDATE, DELETE).
402    ///
403    /// # Arguments
404    ///
405    /// * `command` - The SQL command to execute.
406    ///
407    /// # Returns
408    ///
409    /// The number of affected rows, or 0 if not applicable.
410    ///
411    /// # Errors
412    ///
413    /// Returns an error if:
414    /// - The connection is using gRPC transport (write operations not yet supported)
415    /// - The command fails to execute
416    pub fn execute_command(&self, command: &str) -> Result<u64> {
417        let token = self.stats_before_query(command);
418
419        let result = self.transport.execute_command(command);
420
421        // For commands, the query is fully executed synchronously, so we
422        // can store the pending token immediately for lazy resolution.
423        self.stats_store_pending(token, command);
424
425        result
426    }
427
428    /// Executes a SQL query and returns a streaming result set.
429    ///
430    /// Results are streamed in chunks (default 64K rows), keeping memory usage
431    /// constant regardless of result set size. This makes it safe for any
432    /// result size, from a single row to billions of rows.
433    ///
434    /// # Example
435    ///
436    /// ```no_run
437    /// # use hyperdb_api::{Connection, Result};
438    /// # fn example(conn: &Connection) -> Result<()> {
439    /// let mut result = conn.execute_query("SELECT id, value FROM measurements")?;
440    /// while let Some(chunk) = result.next_chunk()? {
441    ///     for row in &chunk {
442    ///         // Generic typed access (like C++ row.get<T>())
443    ///         let id: Option<i32> = row.get(0);
444    ///         let value: Option<f64> = row.get(1);
445    ///
446    ///         // Or direct accessors
447    ///         let id = row.get_i32(0);
448    ///         let value = row.get_f64(1);
449    ///     }
450    /// }
451    /// # Ok(())
452    /// # }
453    /// ```
454    ///
455    /// # Memory Behavior
456    ///
457    /// - Only one chunk is held in memory at a time (~few MB for 64K rows)
458    /// - Safe for result sets of any size (millions/billions of rows)
459    /// - Memory usage is `O(chunk_size)`, not `O(total_rows)`
460    ///
461    /// # Errors
462    ///
463    /// - Returns [`Error::Server`] wrapping a `hyperdb_api_core::client::Error` if the
464    ///   SQL fails to parse, execute, or if the server reports an error
465    ///   while streaming.
466    /// - Returns [`Error::Io`] on transport-level I/O failures.
467    pub fn execute_query(&self, query: &str) -> Result<Rowset<'_>> {
468        let token = self.stats_before_query(query);
469
470        let result = match &self.transport {
471            Transport::Tcp(tcp) => {
472                let stream = tcp
473                    .client
474                    .query_streaming(query, DEFAULT_BINARY_CHUNK_SIZE)?;
475                Ok(Rowset::new(stream))
476            }
477            Transport::Grpc(grpc) => {
478                // gRPC streaming: pull chunks lazily so peak memory is
479                // bounded by one gRPC message (tonic default 64 MB), not
480                // by the full result size. Matches TCP's
481                // constant-memory streaming shape.
482                //
483                // The transport module already creates a fresh gRPC client
484                // per query (gRPC client needs &mut self to execute), so we
485                // do the same here: connect, start the stream, wrap as a
486                // `ChunkSource`. The stream keeps the channel and runtime
487                // alive via refcounted handles inside `GrpcChunkStreamSync`.
488                let mut client =
489                    hyperdb_api_core::client::grpc::GrpcClientSync::connect(grpc.config.clone())?;
490                let stream = client.execute_query_stream(query)?;
491                let source = Box::new(crate::grpc_connection::GrpcChunkStreamSource::new(stream));
492                let arrow_rowset = crate::arrow_result::ArrowRowset::from_stream(source)?;
493                Ok(Rowset::from_arrow(arrow_rowset))
494            }
495        };
496
497        // Store the pending token — Hyper logs the execution stats after the
498        // result is consumed (streamed), so we defer resolution until
499        // last_query_stats() is called.
500        self.stats_store_pending(token, query);
501
502        result
503    }
504
505    // =========================================================================
506    // Arrow Format Queries
507    // =========================================================================
508
509    /// Executes a SELECT query and returns results as Arrow IPC stream bytes.
510    ///
511    /// # Example
512    ///
513    /// ```no_run
514    /// use hyperdb_api::{Connection, CreateMode, Result};
515    ///
516    /// fn main() -> Result<()> {
517    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
518    ///
519    ///     // Create and populate a table
520    ///     conn.execute_command("CREATE TABLE data (id INT, value DOUBLE PRECISION)")?;
521    ///     conn.execute_command("INSERT INTO data VALUES (1, 1.5), (2, 2.5)")?;
522    ///
523    ///     // Get results as Arrow IPC stream
524    ///     let arrow_data = conn.execute_query_to_arrow("SELECT * FROM data")?;
525    ///     println!("Got {} bytes of Arrow IPC data", arrow_data.len());
526    ///
527    ///     Ok(())
528    /// }
529    /// ```
530    ///
531    /// # Errors
532    ///
533    /// Propagates any [`Error::Server`] from the TCP or gRPC transport when
534    /// the query fails or the server cannot produce Arrow IPC output.
535    pub fn execute_query_to_arrow(&self, select_query: &str) -> Result<bytes::Bytes> {
536        self.transport.execute_query_to_arrow(select_query)
537    }
538
539    /// Exports an entire table to Arrow IPC stream format.
540    ///
541    /// This is a convenience method equivalent to
542    /// `execute_query_to_arrow("SELECT * FROM table_name")`.
543    ///
544    /// # Arguments
545    ///
546    /// * `table_name` - The table name
547    ///
548    /// # Returns
549    ///
550    /// Raw Arrow IPC stream bytes containing all rows from the table.
551    ///
552    /// # Example
553    ///
554    /// ```no_run
555    /// use hyperdb_api::{Connection, CreateMode, Result};
556    ///
557    /// fn main() -> Result<()> {
558    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
559    ///     let arrow_data = conn.export_table_to_arrow("my_table")?;
560    ///     Ok(())
561    /// }
562    /// ```
563    ///
564    /// # Errors
565    ///
566    /// Returns whatever [`execute_query_to_arrow`](Self::execute_query_to_arrow)
567    /// would return for `SELECT * FROM <table_name>` — typically
568    /// [`Error::Server`] if the table does not exist or the query is rejected.
569    pub fn export_table_to_arrow(&self, table_name: &str) -> Result<bytes::Bytes> {
570        self.execute_query_to_arrow(&format!("SELECT * FROM {table_name}"))
571    }
572
573    /// Executes a SELECT query and returns results as Arrow `RecordBatch`es.
574    ///
575    /// This is the recommended method for Arrow-native workflows (`DataFusion`,
576    /// Polars, etc.) where you want direct `RecordBatch` access without going
577    /// through the `Row` abstraction.
578    ///
579    /// # Example
580    ///
581    /// ```no_run
582    /// use hyperdb_api::{Connection, CreateMode, Result};
583    /// use arrow::record_batch::RecordBatch;
584    ///
585    /// fn main() -> Result<()> {
586    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
587    ///
588    ///     let batches: Vec<RecordBatch> = conn.execute_query_to_batches("SELECT * FROM data")?;
589    ///     for batch in &batches {
590    ///         println!("batch: {} rows x {} cols", batch.num_rows(), batch.num_columns());
591    ///     }
592    ///     Ok(())
593    /// }
594    /// ```
595    ///
596    /// # Errors
597    ///
598    /// - Returns [`Error::Server`] if the query itself fails.
599    /// - Returns [`Error::Conversion`] if the Arrow IPC payload returned by the
600    ///   server is malformed and cannot be decoded into record batches.
601    pub fn execute_query_to_batches(
602        &self,
603        select_query: &str,
604    ) -> Result<Vec<arrow::record_batch::RecordBatch>> {
605        let arrow_data = self.execute_query_to_arrow(select_query)?;
606        crate::arrow_result::parse_arrow_ipc(arrow_data)
607    }
608
609    /// Fetches a single row from a query.
610    ///
611    /// Returns an error if the query returns no rows.
612    ///
613    /// # Example
614    ///
615    /// ```no_run
616    /// use hyperdb_api::{Connection, CreateMode, Result};
617    ///
618    /// fn main() -> Result<()> {
619    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
620    ///     let row = conn.fetch_one("SELECT * FROM users WHERE id = 1")?;
621    ///     let id: Option<i32> = row.get(0);
622    ///     let name: Option<String> = row.get(1);
623    ///     Ok(())
624    /// }
625    /// ```
626    ///
627    /// # Errors
628    ///
629    /// - Returns the error from [`execute_query`](Self::execute_query) if
630    ///   the query itself fails.
631    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"` if
632    ///   the query produced zero rows.
633    pub fn fetch_one<Q>(&self, query: Q) -> Result<crate::Row>
634    where
635        Q: AsRef<str>,
636    {
637        let query = query.as_ref();
638        let result = self.execute_query(query)?;
639        result.require_first_row()
640    }
641
642    /// Fetches an optional single row from a query.
643    ///
644    /// Returns `None` if the query returns no rows.
645    ///
646    /// # Example
647    ///
648    /// ```no_run
649    /// use hyperdb_api::{Connection, CreateMode, Result};
650    ///
651    /// fn main() -> Result<()> {
652    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
653    ///     if let Some(row) = conn.fetch_optional("SELECT * FROM users WHERE id = 999")? {
654    ///         let name: Option<String> = row.get(1);
655    ///         println!("Found user: {:?}", name);
656    ///     }
657    ///     Ok(())
658    /// }
659    /// ```
660    ///
661    /// # Errors
662    ///
663    /// Returns the error from [`execute_query`](Self::execute_query) if the
664    /// query itself fails. An empty result set is not an error — it yields
665    /// `Ok(None)`.
666    pub fn fetch_optional<Q>(&self, query: Q) -> Result<Option<crate::Row>>
667    where
668        Q: AsRef<str>,
669    {
670        let query = query.as_ref();
671        let result = self.execute_query(query)?;
672        result.first_row()
673    }
674
675    /// Fetches all rows from a query.
676    ///
677    /// # Example
678    ///
679    /// ```no_run
680    /// use hyperdb_api::{Connection, CreateMode, Result};
681    ///
682    /// fn main() -> Result<()> {
683    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
684    ///     let rows = conn.fetch_all("SELECT * FROM users WHERE active = true ORDER BY name")?;
685    ///     for row in rows {
686    ///         let id: Option<i32> = row.get(0);
687    ///         let name: Option<String> = row.get(1);
688    ///         println!("User {}: {:?}", id.unwrap_or(-1), name);
689    ///     }
690    ///     Ok(())
691    /// }
692    /// ```
693    ///
694    /// # Errors
695    ///
696    /// Returns the error from [`execute_query`](Self::execute_query), or a
697    /// transport error produced while draining every chunk of the streamed
698    /// result set.
699    pub fn fetch_all<Q>(&self, query: Q) -> Result<Vec<crate::Row>>
700    where
701        Q: AsRef<str>,
702    {
703        let query = query.as_ref();
704        let result = self.execute_query(query)?;
705        result.collect_rows()
706    }
707
708    /// Fetches a single row and maps it to a struct using [`FromRow`](crate::FromRow).
709    ///
710    /// Returns an error if the query returns no rows or if mapping fails.
711    ///
712    /// # Example
713    ///
714    /// ```no_run
715    /// use hyperdb_api::{Connection, CreateMode, FromRow, RowAccessor, Result};
716    ///
717    /// struct User { id: i32, name: String }
718    ///
719    /// impl FromRow for User {
720    ///     fn from_row(row: RowAccessor<'_>) -> Result<Self> {
721    ///         Ok(User {
722    ///             id: row.get("id")?,
723    ///             name: row.get_opt("name")?.unwrap_or_default(),
724    ///         })
725    ///     }
726    /// }
727    ///
728    /// fn main() -> Result<()> {
729    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
730    ///     let user: User = conn.fetch_one_as("SELECT id, name FROM users WHERE id = 1")?;
731    ///     Ok(())
732    /// }
733    /// ```
734    ///
735    /// # Errors
736    ///
737    /// - Returns the error from [`fetch_one`](Self::fetch_one) if the query
738    ///   fails or returns no rows.
739    /// - Returns whatever error [`FromRow::from_row`](crate::FromRow::from_row)
740    ///   produces when the row cannot be mapped into `T`.
741    pub fn fetch_one_as<T: crate::FromRow>(&self, query: &str) -> Result<T> {
742        let row = self.fetch_one(query)?;
743        let indices = row
744            .schema()
745            .map(crate::row_accessor::RowAccessor::build_indices)
746            .unwrap_or_default();
747        T::from_row(crate::RowAccessor::new(&row, &indices))
748    }
749
750    /// Fetches all rows and maps them to structs using [`FromRow`](crate::FromRow).
751    ///
752    /// # Example
753    ///
754    /// ```no_run
755    /// # use hyperdb_api::{Connection, FromRow, RowAccessor, Result};
756    /// # struct User { id: i32, name: String }
757    /// # impl FromRow for User {
758    /// #     fn from_row(row: RowAccessor<'_>) -> Result<Self> {
759    /// #         Ok(User { id: row.get("id")?, name: row.get_opt("name")?.unwrap_or_default() })
760    /// #     }
761    /// # }
762    /// # fn example(conn: &Connection) -> Result<()> {
763    /// let users: Vec<User> = conn.fetch_all_as("SELECT id, name FROM users")?;
764    /// # Ok(())
765    /// # }
766    /// ```
767    ///
768    /// # Errors
769    ///
770    /// - Returns the error from [`fetch_all`](Self::fetch_all) if the query
771    ///   fails.
772    /// - Returns the first error produced by
773    ///   [`FromRow::from_row`](crate::FromRow::from_row) on any of the rows.
774    pub fn fetch_all_as<T: crate::FromRow>(&self, query: &str) -> Result<Vec<T>> {
775        let rows = self.fetch_all(query)?;
776        // Build the column-name → index lookup once from the first
777        // row's schema; reuse for every row. All rows in a result set
778        // share the same `Arc<ResultSchema>`, so this is safe.
779        let indices = rows
780            .first()
781            .and_then(crate::result::Row::schema)
782            .map(crate::row_accessor::RowAccessor::build_indices)
783            .unwrap_or_default();
784        rows.iter()
785            .map(|r| T::from_row(crate::RowAccessor::new(r, &indices)))
786            .collect()
787    }
788
789    /// Returns a lazy iterator over rows, mapping each to `T` via
790    /// [`FromRow`].
791    ///
792    /// This is the streaming variant of [`fetch_all_as`](Self::fetch_all_as):
793    /// memory usage is bounded by the chunk size (default 64K rows), not by
794    /// the total row count. Use this for large result sets where collecting
795    /// all rows into a `Vec` would exceed memory limits.
796    ///
797    /// The column-name → index lookup table is built exactly once (on the
798    /// first non-empty chunk) and reused for all rows, so per-row mapping is
799    /// O(1) in column count.
800    ///
801    /// # Example
802    ///
803    /// ```no_run
804    /// # use hyperdb_api::{Connection, CreateMode, FromRow, RowAccessor, Result};
805    /// # struct User { id: i32, name: String }
806    /// # impl FromRow for User {
807    /// #     fn from_row(row: RowAccessor<'_>) -> Result<Self> {
808    /// #         Ok(User { id: row.get("id")?, name: row.get("name")? })
809    /// #     }
810    /// # }
811    /// # fn example(conn: &Connection) -> Result<()> {
812    /// for row_result in conn.stream_as::<User>("SELECT id, name FROM users")? {
813    ///     let user = row_result?;
814    ///     println!("{}: {}", user.id, user.name);
815    /// }
816    /// # Ok(())
817    /// # }
818    /// ```
819    ///
820    /// # Errors
821    ///
822    /// - The returned `Result` wraps errors detected while *opening* the
823    ///   result stream — transport/connection failures, and (on the gRPC
824    ///   transport, which establishes the query stream eagerly) SQL parse and
825    ///   server errors. On the default TCP transport the query is streamed
826    ///   lazily, so SQL errors such as a missing table are typically reported
827    ///   as the **first yielded item** rather than by this outer `Result`.
828    /// - Each yielded item is itself a `Result<T>`:
829    ///   - `Ok(T)` if the row was successfully mapped via `FromRow`.
830    ///   - `Err(e)` for a server/transport error encountered while streaming a
831    ///     later chunk, or for a per-row mapping failure (missing column, type
832    ///     mismatch, NULL in a non-optional field).
833    ///
834    /// In short: always handle errors *both* on the outer `Result` and on each
835    /// item — do not assume a successfully-returned iterator means the query
836    /// succeeded.
837    ///
838    /// [`FromRow`]: crate::FromRow
839    pub fn stream_as<'a, T>(
840        &'a self,
841        query: &str,
842    ) -> Result<impl Iterator<Item = Result<T>> + 'a + use<'a, T>>
843    where
844        T: crate::FromRow + 'a,
845    {
846        let rowset = self.execute_query(query)?;
847        Ok(crate::result::TypedRowIterator::<T>::new(rowset))
848    }
849
850    /// Fetches a single row from a **parameterized** query and maps it to a
851    /// struct using [`FromRow`](crate::FromRow).
852    ///
853    /// This is the parameterized counterpart to
854    /// [`fetch_one_as`](Self::fetch_one_as): it binds `$1`, `$2`, … placeholders
855    /// from `params` (via [`ToSqlParam`](crate::params::ToSqlParam), exactly as
856    /// [`query_params`](Self::query_params) does) and maps the first result row
857    /// into `T`. Use it when a parameterized `SELECT` should yield a typed
858    /// struct rather than a raw [`Row`](crate::Row).
859    ///
860    /// # Example
861    ///
862    /// ```no_run
863    /// # use hyperdb_api::{Connection, FromRow, RowAccessor, Result};
864    /// # struct User { id: i32, name: String }
865    /// # impl FromRow for User {
866    /// #     fn from_row(row: RowAccessor<'_>) -> Result<Self> {
867    /// #         Ok(User { id: row.get("id")?, name: row.get("name")? })
868    /// #     }
869    /// # }
870    /// # fn example(conn: &Connection) -> Result<()> {
871    /// let user: User = conn.fetch_one_as_params(
872    ///     "SELECT id, name FROM users WHERE id = $1",
873    ///     &[&1i32],
874    /// )?;
875    /// # Ok(())
876    /// # }
877    /// ```
878    ///
879    /// # Errors
880    ///
881    /// - Returns [`Error::FeatureNotSupported`] if the connection is using gRPC
882    ///   transport (prepared statements are TCP-only).
883    /// - Returns the error from [`query_params`](Self::query_params) if the
884    ///   server rejects the statement at `Parse`, `Bind`, or `Execute` time, or
885    ///   on transport-level I/O failures.
886    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"`
887    ///   if the query produced zero rows.
888    /// - Returns whatever [`FromRow::from_row`](crate::FromRow::from_row)
889    ///   produces when the row cannot be mapped into `T`.
890    pub fn fetch_one_as_params<T: crate::FromRow>(
891        &self,
892        query: &str,
893        params: &[&dyn crate::params::ToSqlParam],
894    ) -> Result<T> {
895        let row = self.query_params(query, params)?.require_first_row()?;
896        let indices = row
897            .schema()
898            .map(crate::row_accessor::RowAccessor::build_indices)
899            .unwrap_or_default();
900        T::from_row(crate::RowAccessor::new(&row, &indices))
901    }
902
903    /// Fetches all rows from a **parameterized** query and maps them to structs
904    /// using [`FromRow`](crate::FromRow).
905    ///
906    /// This is the parameterized counterpart to
907    /// [`fetch_all_as`](Self::fetch_all_as): it binds `$1`, `$2`, … placeholders
908    /// from `params` (see [`query_params`](Self::query_params)) and maps every
909    /// result row into `T`.
910    ///
911    /// # Example
912    ///
913    /// ```no_run
914    /// # use hyperdb_api::{Connection, FromRow, RowAccessor, Result};
915    /// # struct User { id: i32, name: String }
916    /// # impl FromRow for User {
917    /// #     fn from_row(row: RowAccessor<'_>) -> Result<Self> {
918    /// #         Ok(User { id: row.get("id")?, name: row.get("name")? })
919    /// #     }
920    /// # }
921    /// # fn example(conn: &Connection) -> Result<()> {
922    /// let users: Vec<User> = conn.fetch_all_as_params(
923    ///     "SELECT id, name FROM users WHERE org_id = $1",
924    ///     &[&42i32],
925    /// )?;
926    /// # Ok(())
927    /// # }
928    /// ```
929    ///
930    /// # Errors
931    ///
932    /// - Returns [`Error::FeatureNotSupported`] if the connection is using gRPC
933    ///   transport.
934    /// - Returns the error from [`query_params`](Self::query_params) if the
935    ///   server rejects the statement, or on transport-level I/O failures.
936    /// - Returns the first error produced by
937    ///   [`FromRow::from_row`](crate::FromRow::from_row) on any of the rows.
938    pub fn fetch_all_as_params<T: crate::FromRow>(
939        &self,
940        query: &str,
941        params: &[&dyn crate::params::ToSqlParam],
942    ) -> Result<Vec<T>> {
943        let rows = self.query_params(query, params)?.collect_rows()?;
944        // Build the column-name → index lookup once from the first row's
945        // schema; reuse for every row. See `fetch_all_as`.
946        let indices = rows
947            .first()
948            .and_then(crate::result::Row::schema)
949            .map(crate::row_accessor::RowAccessor::build_indices)
950            .unwrap_or_default();
951        rows.iter()
952            .map(|r| T::from_row(crate::RowAccessor::new(r, &indices)))
953            .collect()
954    }
955
956    /// Returns a lazy iterator over the rows of a **parameterized** query,
957    /// mapping each to `T` via [`FromRow`].
958    ///
959    /// This is the parameterized counterpart to
960    /// [`stream_as`](Self::stream_as): it binds `$1`, `$2`, … placeholders from
961    /// `params` (see [`query_params`](Self::query_params)) and streams the
962    /// result, mapping each row into `T` while holding only one transport chunk
963    /// in memory at a time. The column-index map is built once on the first
964    /// chunk and reused, so per-row mapping is O(1) in the column count.
965    ///
966    /// # Example
967    ///
968    /// ```no_run
969    /// # use hyperdb_api::{Connection, FromRow, RowAccessor, Result};
970    /// # struct User { id: i32, name: String }
971    /// # impl FromRow for User {
972    /// #     fn from_row(row: RowAccessor<'_>) -> Result<Self> {
973    /// #         Ok(User { id: row.get("id")?, name: row.get("name")? })
974    /// #     }
975    /// # }
976    /// # fn example(conn: &Connection) -> Result<()> {
977    /// for row_result in conn.stream_as_params::<User>(
978    ///     "SELECT id, name FROM users WHERE org_id = $1",
979    ///     &[&42i32],
980    /// )? {
981    ///     let user = row_result?;
982    ///     println!("{}: {}", user.id, user.name);
983    /// }
984    /// # Ok(())
985    /// # }
986    /// ```
987    ///
988    /// # Errors
989    ///
990    /// - The returned outer `Result` wraps errors detected while *opening* the
991    ///   stream: [`Error::FeatureNotSupported`] on gRPC transport, and any
992    ///   `Parse`/`Bind` rejection or transport failure surfaced by
993    ///   [`query_params`](Self::query_params).
994    /// - Each yielded item is itself a `Result<T>`: `Ok(T)` when the row mapped
995    ///   cleanly, or `Err(e)` for a per-row mapping failure (missing column,
996    ///   type mismatch, NULL in a non-optional field) or a server/transport
997    ///   error hit while streaming a later chunk.
998    ///
999    /// As with [`stream_as`](Self::stream_as), always handle errors *both* on
1000    /// the outer `Result` and on each item.
1001    ///
1002    /// [`FromRow`]: crate::FromRow
1003    pub fn stream_as_params<'a, T>(
1004        &'a self,
1005        query: &str,
1006        params: &[&dyn crate::params::ToSqlParam],
1007    ) -> Result<impl Iterator<Item = Result<T>> + 'a + use<'a, T>>
1008    where
1009        T: crate::FromRow + 'a,
1010    {
1011        // `query_params` returns a Rowset that already carries the prepared
1012        // statement guard, so Drop ordering (close_statement after the rowset
1013        // releases its connection lock) is preserved with no extra work here.
1014        let rowset = self.query_params(query, params)?;
1015        Ok(crate::result::TypedRowIterator::<T>::new(rowset))
1016    }
1017
1018    /// Fetches a single scalar value from a query.
1019    ///
1020    /// Returns an error if the query returns no rows or NULL.
1021    ///
1022    /// # Example
1023    ///
1024    /// ```no_run
1025    /// use hyperdb_api::{Connection, CreateMode, Result};
1026    ///
1027    /// fn main() -> Result<()> {
1028    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
1029    ///     let count: i64 = conn.fetch_scalar("SELECT COUNT(*) FROM users")?;
1030    ///     println!("User count: {}", count);
1031    ///     Ok(())
1032    /// }
1033    /// ```
1034    ///
1035    /// # Errors
1036    ///
1037    /// - Returns the error from [`execute_query`](Self::execute_query) if
1038    ///   the query itself fails.
1039    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"` if
1040    ///   the query produced zero rows.
1041    /// - Returns [`Error::Conversion`] with message `"Scalar query returned NULL"`
1042    ///   if the single cell is SQL `NULL`.
1043    pub fn fetch_scalar<T, Q>(&self, query: Q) -> Result<T>
1044    where
1045        T: crate::connection::ScalarValue + crate::result::RowValue,
1046        Q: AsRef<str>,
1047    {
1048        let query = query.as_ref();
1049        let result = self.execute_query(query)?;
1050        result.require_scalar()
1051    }
1052
1053    /// Fetches an optional scalar value from a query.
1054    ///
1055    /// Returns `None` if the query returns no rows or NULL.
1056    ///
1057    /// # Example
1058    ///
1059    /// ```no_run
1060    /// use hyperdb_api::{Connection, CreateMode, Result};
1061    ///
1062    /// fn main() -> Result<()> {
1063    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
1064    ///     let max_id: Option<i32> = conn.fetch_optional_scalar("SELECT MAX(id) FROM users")?;
1065    ///     println!("Max ID: {:?}", max_id);
1066    ///     Ok(())
1067    /// }
1068    /// ```
1069    ///
1070    /// # Errors
1071    ///
1072    /// - Returns the error from [`execute_query`](Self::execute_query) if
1073    ///   the query itself fails.
1074    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"` if
1075    ///   the query produced zero rows. (An empty result is treated as an
1076    ///   error here because we need at least one row to inspect; SQL `NULL`
1077    ///   in the single cell yields `Ok(None)`.)
1078    pub fn fetch_optional_scalar<T, Q>(&self, query: Q) -> Result<Option<T>>
1079    where
1080        T: crate::connection::ScalarValue + crate::result::RowValue,
1081        Q: AsRef<str>,
1082    {
1083        let query = query.as_ref();
1084        let result = self.execute_query(query)?;
1085        result.scalar()
1086    }
1087
1088    /// Executes a scalar query and returns a single value of type `T`.
1089    ///
1090    /// Alias for [`fetch_optional_scalar`](Self::fetch_optional_scalar) for C++ API compatibility.
1091    ///
1092    /// # Errors
1093    ///
1094    /// See [`fetch_optional_scalar`](Self::fetch_optional_scalar).
1095    #[inline]
1096    pub fn execute_scalar_query<T>(&self, query: &str) -> Result<Option<T>>
1097    where
1098        T: ScalarValue + crate::result::RowValue,
1099    {
1100        self.fetch_optional_scalar(query)
1101    }
1102
1103    /// Queries for a count value, defaulting to 0 if NULL.
1104    ///
1105    /// This is optimized for COUNT queries which typically return 0
1106    /// instead of NULL when there are no matching rows.
1107    ///
1108    /// # Example
1109    ///
1110    /// ```no_run
1111    /// use hyperdb_api::{Connection, CreateMode, Result};
1112    ///
1113    /// fn main() -> Result<()> {
1114    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
1115    ///     let count = conn.query_count("SELECT COUNT(*) FROM users WHERE active = true")?;
1116    ///     println!("Active users: {}", count);
1117    ///     Ok(())
1118    /// }
1119    /// ```
1120    ///
1121    /// # Errors
1122    ///
1123    /// Returns the error from [`execute_query`](Self::execute_query) if the
1124    /// query fails or produces no rows. SQL `NULL` is mapped to `0`, not an
1125    /// error.
1126    pub fn query_count(&self, query: &str) -> Result<i64> {
1127        self.fetch_optional_scalar::<i64, _>(query)
1128            .map(|opt| opt.unwrap_or(0))
1129    }
1130
1131    // =========================================================================
1132    // Parameterized Queries (SQL Injection Safe)
1133    // =========================================================================
1134
1135    /// Executes a parameterized query, returning streaming results.
1136    ///
1137    /// This is safe to use with untrusted user input: parameters travel
1138    /// through the extended query protocol (Parse/Bind/Execute) as
1139    /// length-prefixed values and are never interpolated into the SQL
1140    /// string. Most bind as PostgreSQL binary (format `1`); a scaled
1141    /// [`Numeric`](crate::Numeric) and a [`Geography`](crate::Geography)
1142    /// bind as PostgreSQL text (format `0`), because Hyper has no binary
1143    /// input function for either — see
1144    /// [`ToSqlParam::param_format`](crate::ToSqlParam::param_format).
1145    /// (`HyperBinary`, format `2`, is a *result* encoding only; it is never
1146    /// used for parameters.)
1147    ///
1148    /// For repeated executions of the same SQL with different values, prefer
1149    /// the explicit [`prepare_typed`](Self::prepare_typed) API — it returns a
1150    /// reusable [`PreparedStatement`](crate::PreparedStatement) that skips
1151    /// the Parse round-trip on every call. Note that a prepared statement
1152    /// fixes its parameter OIDs up front, so it cannot accept both whole and
1153    /// scaled `NUMERIC` values; `query_params` re-parses per call and can.
1154    /// See [`Numeric::sql_oid`](crate::ToSqlParam::sql_oid) for the detail.
1155    ///
1156    /// Under the hood, `query_params` is a one-shot
1157    /// prepare+execute+close: it prepares an unnamed statement, binds
1158    /// the parameters, starts streaming, and closes the statement when
1159    /// the returned [`Rowset`] is dropped.
1160    ///
1161    /// # Arguments
1162    ///
1163    /// * `query` - The SQL query with parameter placeholders (`$1`, `$2`, etc.)
1164    /// * `params` - Parameter values matching the placeholders
1165    ///
1166    /// # SQL Injection Prevention
1167    ///
1168    /// ```no_run
1169    /// use hyperdb_api::{Connection, CreateMode, Result};
1170    ///
1171    /// fn search_users(conn: &Connection, user_input: &str) -> Result<()> {
1172    ///     // DANGEROUS - vulnerable to SQL injection:
1173    ///     // let query = format!("SELECT * FROM users WHERE name = '{}'", user_input);
1174    ///
1175    ///     // SAFE - parameterized query:
1176    ///     let mut result = conn.query_params(
1177    ///         "SELECT * FROM users WHERE name = $1",
1178    ///         &[&user_input],
1179    ///     )?;
1180    ///
1181    ///     while let Some(chunk) = result.next_chunk()? {
1182    ///         for row in &chunk {
1183    ///             let id: Option<i32> = row.get(0);
1184    ///             let name: Option<String> = row.get(1);
1185    ///             println!("Found: {:?} - {:?}", id, name);
1186    ///         }
1187    ///     }
1188    ///     Ok(())
1189    /// }
1190    /// ```
1191    ///
1192    /// # Multiple Parameters
1193    ///
1194    /// ```no_run
1195    /// use hyperdb_api::{Connection, CreateMode, Result};
1196    ///
1197    /// fn main() -> Result<()> {
1198    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
1199    ///
1200    ///     // Multiple parameters of different types
1201    ///     let result = conn.query_params(
1202    ///         "SELECT * FROM orders WHERE customer_id = $1 AND total > $2",
1203    ///         &[&42i32, &100.0f64],
1204    ///     )?;
1205    ///     Ok(())
1206    /// }
1207    /// ```
1208    ///
1209    /// # Errors
1210    ///
1211    /// - Returns [`Error::FeatureNotSupported`] if the connection is using gRPC transport
1212    ///   (prepared statements are TCP-only).
1213    /// - Returns [`Error::Server`] if the server rejects the statement at
1214    ///   `Parse`, `Bind`, or `Execute` time, including on type-mismatch
1215    ///   between `params` and the inferred OIDs.
1216    /// - Returns [`Error::Io`] on transport-level I/O failures.
1217    pub fn query_params(
1218        &self,
1219        query: &str,
1220        params: &[&dyn crate::params::ToSqlParam],
1221    ) -> Result<Rowset<'_>> {
1222        // Implementation note: routes through the extended query protocol
1223        // via Parse/Bind/Execute so parameters travel as length-prefixed
1224        // Bind values (PG binary, or PG text for the types Hyper has no
1225        // binary input function for) — no SQL escaping, full SQL-injection
1226        // safety regardless of parameter content. The statement handle is stashed inside the
1227        // returned Rowset so its Drop-time close_statement fires *after*
1228        // the rowset releases its connection lock (otherwise the close
1229        // would deadlock on the still-held mutex).
1230        let client = match &self.transport {
1231            Transport::Tcp(tcp) => &tcp.client,
1232            Transport::Grpc(_) => {
1233                return Err(Error::feature_not_supported(
1234                    "prepared statements are not supported over gRPC transport",
1235                ));
1236            }
1237        };
1238        let oids: Vec<crate::Oid> = params.iter().map(|p| p.sql_oid()).collect();
1239        let stmt = client.prepare_typed(query, &oids)?;
1240        let (encoded, formats) = crate::prepared::encode_params(params);
1241        let stream = client.execute_streaming_with_formats(
1242            &stmt,
1243            encoded,
1244            &formats,
1245            crate::result::DEFAULT_BINARY_CHUNK_SIZE,
1246        )?;
1247        Ok(Rowset::from_prepared(stream).with_statement_guard(stmt))
1248    }
1249
1250    /// Executes a parameterized command that doesn't return rows.
1251    ///
1252    /// Use this for INSERT, UPDATE, DELETE, or DDL statements with parameters.
1253    /// Returns the number of affected rows.
1254    ///
1255    /// See [`query_params`](Self::query_params) for details on parameter
1256    /// handling and SQL injection prevention.
1257    ///
1258    /// # Example
1259    ///
1260    /// ```no_run
1261    /// use hyperdb_api::{Connection, CreateMode, Result};
1262    ///
1263    /// fn delete_user(conn: &Connection, user_id: i32) -> Result<u64> {
1264    ///     // Safe from SQL injection
1265    ///     conn.command_params("DELETE FROM users WHERE id = $1", &[&user_id])
1266    /// }
1267    /// ```
1268    ///
1269    /// # Errors
1270    ///
1271    /// - Returns [`Error::FeatureNotSupported`] if the connection is using gRPC transport.
1272    /// - Returns [`Error::Server`] if the server rejects the statement at
1273    ///   `Parse`, `Bind`, or `Execute` time.
1274    /// - Returns [`Error::Io`] on transport-level I/O failures.
1275    pub fn command_params(
1276        &self,
1277        query: &str,
1278        params: &[&dyn crate::params::ToSqlParam],
1279    ) -> Result<u64> {
1280        // One-shot prepare+execute with explicit OIDs — see `query_params`
1281        // for why we collect OIDs from each parameter.
1282        let client = match &self.transport {
1283            Transport::Tcp(tcp) => &tcp.client,
1284            Transport::Grpc(_) => {
1285                return Err(Error::feature_not_supported(
1286                    "prepared statements are not supported over gRPC transport",
1287                ));
1288            }
1289        };
1290        let oids: Vec<crate::Oid> = params.iter().map(|p| p.sql_oid()).collect();
1291        let stmt = client.prepare_typed(query, &oids)?;
1292        let (encoded, formats) = crate::prepared::encode_params(params);
1293        Ok(client.execute_no_result_with_formats(&stmt, encoded, &formats)?)
1294    }
1295
1296    /// Executes multiple SQL statements in a single call.
1297    ///
1298    /// Each statement is executed sequentially. If any statement fails,
1299    /// execution stops and the error is returned. Returns the total number
1300    /// of affected rows across all statements.
1301    ///
1302    /// This is more efficient than calling `execute_command` in a loop
1303    /// because it reduces round-trips for DDL scripts and multi-statement setup.
1304    ///
1305    /// # Example
1306    ///
1307    /// ```no_run
1308    /// use hyperdb_api::{Connection, CreateMode, Result};
1309    ///
1310    /// fn main() -> Result<()> {
1311    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
1312    ///     let total = conn.execute_batch(&[
1313    ///         "CREATE TABLE users (id INT, name TEXT)",
1314    ///         "INSERT INTO users VALUES (1, 'Alice')",
1315    ///         "INSERT INTO users VALUES (2, 'Bob')",
1316    ///     ])?;
1317    ///     println!("Total affected: {}", total);
1318    ///     Ok(())
1319    /// }
1320    /// ```
1321    ///
1322    /// # Errors
1323    ///
1324    /// Returns a wrapped [`Error::Internal`] on the first statement that fails;
1325    /// its `source` is the original [`Error::Server`] from
1326    /// [`execute_command`](Self::execute_command). The error message
1327    /// includes the failing statement's ordinal and an 80-character preview
1328    /// of its SQL.
1329    pub fn execute_batch(&self, statements: &[&str]) -> Result<u64> {
1330        let mut total = 0u64;
1331        for (i, stmt) in statements.iter().enumerate() {
1332            if !stmt.trim().is_empty() {
1333                total += self.execute_command(stmt).map_err(|e| {
1334                    let preview: String = stmt.chars().take(80).collect();
1335                    Error::internal(format!(
1336                        "execute_batch failed at statement {} of {}: {}: {}",
1337                        i + 1,
1338                        statements.len(),
1339                        preview,
1340                        e,
1341                    ))
1342                })?;
1343            }
1344        }
1345        Ok(total)
1346    }
1347
1348    /// Returns the attached database path, if any.
1349    pub fn database(&self) -> Option<&str> {
1350        self.database.as_deref()
1351    }
1352
1353    /// Creates a new database file.
1354    ///
1355    /// # Example
1356    ///
1357    /// ```no_run
1358    /// use hyperdb_api::{Connection, Result};
1359    ///
1360    /// fn main() -> Result<()> {
1361    ///     let conn = Connection::without_database("localhost:7483")?;
1362    ///     conn.create_database("new_database.hyper")?;
1363    ///     Ok(())
1364    /// }
1365    /// ```
1366    ///
1367    /// # Errors
1368    ///
1369    /// Returns [`Error::Server`] if the server rejects the
1370    /// `CREATE DATABASE IF NOT EXISTS` statement (e.g. the path is not
1371    /// writable on the server).
1372    pub fn create_database(&self, path: &str) -> Result<()> {
1373        let sql = format!("CREATE DATABASE IF NOT EXISTS {}", escape_sql_path(path));
1374        self.execute_command(&sql)?;
1375        Ok(())
1376    }
1377
1378    /// Drops (deletes) a database file.
1379    ///
1380    /// # Example
1381    ///
1382    /// ```no_run
1383    /// use hyperdb_api::{Connection, Result};
1384    ///
1385    /// fn main() -> Result<()> {
1386    ///     let conn = Connection::without_database("localhost:7483")?;
1387    ///     conn.drop_database("old_database.hyper")?;
1388    ///     Ok(())
1389    /// }
1390    /// ```
1391    ///
1392    /// # Errors
1393    ///
1394    /// Returns [`Error::Server`] if the server rejects the
1395    /// `DROP DATABASE IF EXISTS` statement (e.g. the database is still
1396    /// attached or permissions deny deletion).
1397    pub fn drop_database(&self, path: &str) -> Result<()> {
1398        let sql = format!("DROP DATABASE IF EXISTS {}", escape_sql_path(path));
1399        self.execute_command(&sql)?;
1400        Ok(())
1401    }
1402
1403    /// Attaches a database file to the connection.
1404    ///
1405    /// Once attached, the database can be queried and modified.
1406    /// The database is identified by its alias (or by its path if no alias is provided).
1407    ///
1408    /// # Arguments
1409    ///
1410    /// * `path` - The path to the database file to attach.
1411    /// * `alias` - Optional alias for the database. If `None`, the database is
1412    ///   attached without an explicit alias (typically using its filename).
1413    ///
1414    /// # Errors
1415    ///
1416    /// Returns an error if the database file doesn't exist or if attachment fails.
1417    ///
1418    /// # Example
1419    ///
1420    /// ```no_run
1421    /// use hyperdb_api::{Connection, Result};
1422    ///
1423    /// fn main() -> Result<()> {
1424    ///     let conn = Connection::without_database("localhost:7483")?;
1425    ///
1426    ///     // Attach with an alias
1427    ///     conn.attach_database("data.hyper", Some("mydata"))?;
1428    ///
1429    ///     // Attach without an alias
1430    ///     conn.attach_database("other.hyper", None)?;
1431    ///     Ok(())
1432    /// }
1433    /// ```
1434    pub fn attach_database(&self, path: &str, alias: Option<&str>) -> Result<()> {
1435        let sql = if let Some(alias) = alias {
1436            format!(
1437                "ATTACH DATABASE {} AS {}",
1438                escape_sql_path(path),
1439                escape_sql_path(alias)
1440            )
1441        } else {
1442            format!("ATTACH DATABASE {}", escape_sql_path(path))
1443        };
1444        self.execute_command(&sql)?;
1445        Ok(())
1446    }
1447
1448    /// Detaches a database from this connection.
1449    ///
1450    /// After detaching, the database file is released and can be accessed
1451    /// externally (e.g., copied, moved, etc.). All pending updates are
1452    /// written to disk before detaching.
1453    ///
1454    /// # Arguments
1455    ///
1456    /// * `alias` - The alias of the database to detach.
1457    ///
1458    /// # Errors
1459    ///
1460    /// Returns an error if the database is not attached or if detachment fails.
1461    ///
1462    /// # Example
1463    ///
1464    /// ```no_run
1465    /// use hyperdb_api::{Connection, Result};
1466    ///
1467    /// fn main() -> Result<()> {
1468    ///     let conn = Connection::without_database("localhost:7483")?;
1469    ///     conn.attach_database("data.hyper", Some("mydata"))?;
1470    ///     // ... work with the database ...
1471    ///     conn.detach_database("mydata")?;
1472    ///     Ok(())
1473    /// }
1474    /// ```
1475    pub fn detach_database(&self, alias: &str) -> Result<()> {
1476        let sql = format!("DETACH DATABASE {}", escape_sql_path(alias));
1477        self.execute_command(&sql)?;
1478        Ok(())
1479    }
1480
1481    /// Detaches all databases from this connection.
1482    ///
1483    /// This is useful for cleanup before closing a connection or when
1484    /// you need to release all database files.
1485    ///
1486    /// # Errors
1487    ///
1488    /// Returns [`Error::Server`] if the server rejects the
1489    /// `DETACH ALL DATABASES` statement (e.g. a database is still in use by
1490    /// another session).
1491    pub fn detach_all_databases(&self) -> Result<()> {
1492        self.execute_command("DETACH ALL DATABASES")?;
1493        Ok(())
1494    }
1495
1496    /// Creates a schema in the database.
1497    ///
1498    /// # Errors
1499    ///
1500    /// - Returns an error if `schema_name` cannot be converted into a
1501    ///   [`SchemaName`](crate::SchemaName) (invalid identifier).
1502    /// - Returns [`Error::Server`] if the server rejects the
1503    ///   `CREATE SCHEMA` statement (e.g. the schema already exists).
1504    pub fn create_schema<T>(&self, schema_name: T) -> Result<()>
1505    where
1506        T: TryInto<crate::SchemaName>,
1507        crate::Error: From<T::Error>,
1508    {
1509        crate::catalog::Catalog::new(self).create_schema(schema_name)
1510    }
1511
1512    /// Checks whether a schema exists.
1513    ///
1514    /// # Arguments
1515    ///
1516    /// * `schema` - The schema name (can include database qualifier).
1517    ///
1518    /// # Example
1519    ///
1520    /// ```no_run
1521    /// use hyperdb_api::{Connection, Result};
1522    ///
1523    /// fn main() -> Result<()> {
1524    ///     let conn = Connection::without_database("localhost:7483")?;
1525    ///     if conn.has_schema("public")? {
1526    ///         println!("Schema 'public' exists");
1527    ///     }
1528    ///     Ok(())
1529    /// }
1530    /// ```
1531    ///
1532    /// # Errors
1533    ///
1534    /// - Returns an error if `schema` cannot be converted into a
1535    ///   [`SchemaName`](crate::SchemaName).
1536    /// - Returns [`Error::Server`] if the catalog lookup query fails.
1537    pub fn has_schema<T>(&self, schema: T) -> Result<bool>
1538    where
1539        T: TryInto<crate::SchemaName>,
1540        crate::Error: From<T::Error>,
1541    {
1542        use crate::catalog::Catalog;
1543        Catalog::new(self).has_schema(schema)
1544    }
1545
1546    /// Checks whether a table exists.
1547    ///
1548    /// # Arguments
1549    ///
1550    /// * `table_name` - The table name (can include database and schema qualifiers).
1551    ///
1552    /// # Example
1553    ///
1554    /// ```no_run
1555    /// use hyperdb_api::{Connection, Result};
1556    ///
1557    /// fn main() -> Result<()> {
1558    ///     let conn = Connection::without_database("localhost:7483")?;
1559    ///     if conn.has_table("public.users")? {
1560    ///         println!("Table 'users' exists");
1561    ///     }
1562    ///     Ok(())
1563    /// }
1564    /// ```
1565    ///
1566    /// # Errors
1567    ///
1568    /// - Returns an error if `table_name` cannot be converted into a
1569    ///   [`TableName`](crate::TableName).
1570    /// - Returns [`Error::Server`] if the catalog lookup query fails.
1571    pub fn has_table<T>(&self, table_name: T) -> Result<bool>
1572    where
1573        T: TryInto<crate::TableName>,
1574        crate::Error: From<T::Error>,
1575    {
1576        use crate::catalog::Catalog;
1577        Catalog::new(self).has_table(table_name)
1578    }
1579
1580    /// Returns the server version as a parsed struct.
1581    ///
1582    /// Returns `None` if the version cannot be determined (e.g., gRPC connection).
1583    ///
1584    /// # Example
1585    ///
1586    /// ```no_run
1587    /// use hyperdb_api::{Connection, CreateMode, HyperProcess, ServerVersion, Result};
1588    ///
1589    /// fn main() -> Result<()> {
1590    ///     let hyper = HyperProcess::new(None, None)?;
1591    ///     let conn = Connection::new(&hyper, "test.hyper", CreateMode::CreateIfNotExists)?;
1592    ///     if let Some(version) = conn.server_version() {
1593    ///         println!("Hyper {}", version);
1594    ///         if version >= ServerVersion::new(0, 1, 0) {
1595    ///             println!("Has feature X");
1596    ///         }
1597    ///     }
1598    ///     Ok(())
1599    /// }
1600    /// ```
1601    pub fn server_version(&self) -> Option<crate::ServerVersion> {
1602        let version_str = self.parameter_status("server_version")?;
1603        crate::ServerVersion::parse(&version_str)
1604    }
1605
1606    /// Copies a database file to a new path.
1607    ///
1608    /// The source database must be attached to this connection.
1609    ///
1610    /// # Example
1611    ///
1612    /// ```no_run
1613    /// use hyperdb_api::{Connection, CreateMode, HyperProcess, Result};
1614    ///
1615    /// fn main() -> Result<()> {
1616    ///     let hyper = HyperProcess::new(None, None)?;
1617    ///     let conn = Connection::new(&hyper, "source.hyper", CreateMode::DoNotCreate)?;
1618    ///     conn.copy_database("source.hyper", "backup.hyper")?;
1619    ///     Ok(())
1620    /// }
1621    /// ```
1622    ///
1623    /// # Errors
1624    ///
1625    /// Returns [`Error::Server`] if the server rejects the
1626    /// `COPY DATABASE` statement — e.g. the source is not attached, the
1627    /// destination path is not writable, or it already exists.
1628    pub fn copy_database(&self, source: &str, destination: &str) -> Result<()> {
1629        let sql = format!(
1630            "COPY DATABASE {} TO {}",
1631            escape_sql_path(source),
1632            escape_sql_path(destination)
1633        );
1634        self.execute_command(&sql)?;
1635        Ok(())
1636    }
1637
1638    /// Executes EXPLAIN on a query and returns the plan as a string.
1639    ///
1640    /// # Example
1641    ///
1642    /// ```no_run
1643    /// use hyperdb_api::{Connection, CreateMode, Result};
1644    ///
1645    /// fn main() -> Result<()> {
1646    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
1647    ///     let plan = conn.explain("SELECT * FROM users WHERE id = 1")?;
1648    ///     println!("{}", plan);
1649    ///     Ok(())
1650    /// }
1651    /// ```
1652    ///
1653    /// # Errors
1654    ///
1655    /// Returns [`Error::Server`] if `EXPLAIN <query>` fails to parse or
1656    /// plan, or if the streamed result cannot be consumed.
1657    pub fn explain(&self, query: &str) -> Result<String> {
1658        let explain_sql = format!("EXPLAIN {query}");
1659        let result = self.execute_query(&explain_sql)?;
1660        let mut lines = Vec::new();
1661        for row in result.rows() {
1662            let row = row?;
1663            if let Some(line) = row.get::<String>(0) {
1664                lines.push(line);
1665            }
1666        }
1667        Ok(lines.join("\n"))
1668    }
1669
1670    /// Executes EXPLAIN ANALYZE on a query and returns the plan with timing info.
1671    ///
1672    /// **Note:** This actually executes the query to collect timing information.
1673    ///
1674    /// # Errors
1675    ///
1676    /// Returns [`Error::Server`] if `EXPLAIN ANALYZE <query>` fails — this
1677    /// includes any runtime error raised by actually executing `query`.
1678    pub fn explain_analyze(&self, query: &str) -> Result<String> {
1679        let explain_sql = format!("EXPLAIN ANALYZE {query}");
1680        let result = self.execute_query(&explain_sql)?;
1681        let mut lines = Vec::new();
1682        for row in result.rows() {
1683            let row = row?;
1684            if let Some(line) = row.get::<String>(0) {
1685                lines.push(line);
1686            }
1687        }
1688        Ok(lines.join("\n"))
1689    }
1690
1691    /// Returns a reference to the underlying TCP client.
1692    ///
1693    /// # Panics
1694    ///
1695    /// This method returns `None` if the connection is using gRPC transport.
1696    pub fn tcp_client(&self) -> Option<&Client> {
1697        match &self.transport {
1698            Transport::Tcp(tcp) => Some(&tcp.client),
1699            Transport::Grpc(_) => None,
1700        }
1701    }
1702
1703    /// Crate-internal accessor for the transport. Used by
1704    /// [`PreparedStatement`](crate::PreparedStatement) to reach the
1705    /// underlying `hyperdb_api_core::client::Client`.
1706    pub(crate) fn transport(&self) -> &Transport {
1707        &self.transport
1708    }
1709
1710    /// Prepares a SQL statement with automatic parameter type inference.
1711    ///
1712    /// The returned [`PreparedStatement`](crate::PreparedStatement) can
1713    /// be executed many times with different parameter values; the
1714    /// server caches the parsed plan. This is the preferred way to
1715    /// execute a statement repeatedly inside a loop.
1716    ///
1717    /// For explicit parameter types (necessary when `$N` placeholders
1718    /// would otherwise be ambiguous), use
1719    /// [`prepare_typed`](Self::prepare_typed).
1720    ///
1721    /// # Example
1722    ///
1723    /// ```no_run
1724    /// # use hyperdb_api::{Connection, CreateMode, Result};
1725    /// # fn example(conn: &Connection) -> Result<()> {
1726    /// let stmt = conn.prepare("SELECT name FROM users WHERE id = $1")?;
1727    /// for id in [1_i32, 2, 3] {
1728    ///     let name: String = stmt.fetch_scalar(&[&id])?;
1729    ///     println!("{id}: {name}");
1730    /// }
1731    /// # Ok(())
1732    /// # }
1733    /// ```
1734    ///
1735    /// # Errors
1736    ///
1737    /// See [`prepare_typed`](Self::prepare_typed) — this method delegates
1738    /// to it with an empty OID list.
1739    pub fn prepare(&self, query: &str) -> Result<crate::PreparedStatement<'_>> {
1740        self.prepare_typed(query, &[])
1741    }
1742
1743    /// Prepares a SQL statement with explicit parameter type OIDs.
1744    ///
1745    /// Use this when the server cannot infer parameter types from the
1746    /// SQL alone (e.g. a bare `$1` in a `WHERE v > $1` clause with no
1747    /// other context). Constants for common types live in
1748    /// [`hyperdb_api_core::types::oids`].
1749    ///
1750    /// In practice this is the *only* way to prepare a statement that binds
1751    /// parameters: [`prepare`](Self::prepare) passes an empty OID list, and
1752    /// Hyper then rejects any `$N` in the SQL with `42601` ("unexpected
1753    /// parameter '$1', expected to have 0 parameter(s)") — it does not infer
1754    /// parameter types at Parse time.
1755    ///
1756    /// # Scaled `NUMERIC` parameters
1757    ///
1758    /// Parameter OIDs are fixed here, before any value exists, so a single
1759    /// prepared statement cannot accept both whole and scaled
1760    /// [`Numeric`](crate::Numeric) values: `oids::NUMERIC` rejects scaled
1761    /// values with `22003`, and `Oid::new(0)` rejects whole numbers with
1762    /// `0A000`. Use [`query_params`](Self::query_params) when the scale
1763    /// varies across calls — it re-parses per statement and picks the OID
1764    /// from the value. [`Geography`](crate::Geography) is unaffected and
1765    /// works normally here.
1766    ///
1767    /// # Errors
1768    ///
1769    /// - Returns [`Error::FeatureNotSupported`] if the connection is using gRPC transport
1770    ///   (prepared statements are TCP-only).
1771    /// - Returns [`Error::Server`] if the server rejects the `Parse`
1772    ///   message, e.g. SQL syntax error or unknown OID.
1773    /// - Returns [`Error::Io`] on transport-level I/O failures.
1774    pub fn prepare_typed(
1775        &self,
1776        query: &str,
1777        param_types: &[crate::Oid],
1778    ) -> Result<crate::PreparedStatement<'_>> {
1779        let client = match &self.transport {
1780            Transport::Tcp(tcp) => &tcp.client,
1781            Transport::Grpc(_) => {
1782                return Err(Error::feature_not_supported(
1783                    "prepared statements are not supported over gRPC transport",
1784                ));
1785            }
1786        };
1787        let inner = client.prepare_typed(query, param_types)?;
1788        crate::PreparedStatement::new(self, inner)
1789    }
1790
1791    /// Returns true if the connection is alive (passive check).
1792    ///
1793    /// This is a lightweight check that does not send any data to the server.
1794    /// For an active health check, use [`ping`](Self::ping).
1795    pub fn is_alive(&self) -> bool {
1796        match &self.transport {
1797            Transport::Tcp(tcp) => tcp.client.is_alive(),
1798            Transport::Grpc(_) => true, // gRPC connections are stateless
1799        }
1800    }
1801
1802    /// Actively checks that the connection is healthy by executing a trivial query.
1803    ///
1804    /// Unlike [`is_alive`](Self::is_alive) which only checks local state,
1805    /// this method sends `SELECT 1` to the server and verifies a response.
1806    ///
1807    /// # Example
1808    ///
1809    /// ```no_run
1810    /// # use hyperdb_api::{Connection, CreateMode, Result};
1811    /// # fn example(conn: &Connection) -> Result<()> {
1812    /// if conn.ping().is_ok() {
1813    ///     println!("Connection is healthy");
1814    /// }
1815    /// # Ok(())
1816    /// # }
1817    /// ```
1818    ///
1819    /// # Errors
1820    ///
1821    /// Returns [`Error::Server`] or [`Error::Io`] if the `SELECT 1`
1822    /// round-trip fails — i.e. the connection is no longer usable.
1823    pub fn ping(&self) -> Result<()> {
1824        self.execute_command("SELECT 1")?;
1825        Ok(())
1826    }
1827
1828    /// Returns the process ID of the backend server connection.
1829    ///
1830    /// Returns 0 for gRPC connections (not applicable).
1831    pub fn process_id(&self) -> i32 {
1832        match &self.transport {
1833            Transport::Tcp(tcp) => tcp.client.process_id(),
1834            Transport::Grpc(_) => 0,
1835        }
1836    }
1837
1838    /// Returns the secret key for the backend server connection.
1839    ///
1840    /// This is used for cancellation requests.
1841    /// Returns 0 for gRPC connections (not applicable).
1842    pub fn secret_key(&self) -> i32 {
1843        match &self.transport {
1844            Transport::Tcp(tcp) => tcp.client.secret_key(),
1845            Transport::Grpc(_) => 0,
1846        }
1847    }
1848
1849    /// Returns a server parameter value by name.
1850    ///
1851    /// Server parameters are sent by the server during connection startup.
1852    /// Common parameters include:
1853    /// - `server_version` - The server version string
1854    /// - `server_encoding` - The server's character encoding
1855    /// - `client_encoding` - The client's character encoding
1856    /// - `DateStyle` - Date display format
1857    /// - `TimeZone` - Server timezone
1858    /// - `session_identifier` - Session ID for connection migration (if routing enabled)
1859    ///
1860    /// Returns `None` if the parameter is not known.
1861    ///
1862    /// # Example
1863    ///
1864    /// ```no_run
1865    /// use hyperdb_api::{Connection, CreateMode, HyperProcess, Result};
1866    ///
1867    /// fn main() -> Result<()> {
1868    ///     let hyper = HyperProcess::new(None, None)?;
1869    ///     let conn = Connection::new(&hyper, "test.hyper", CreateMode::CreateIfNotExists)?;
1870    ///
1871    ///     if let Some(version) = conn.parameter_status("server_version") {
1872    ///         println!("Connected to Hyper version: {}", version);
1873    ///     }
1874    ///     Ok(())
1875    /// }
1876    /// ```
1877    pub fn parameter_status(&self, name: &str) -> Option<String> {
1878        match &self.transport {
1879            Transport::Tcp(tcp) => tcp.client.parameter_status(name),
1880            Transport::Grpc(_) => None, // gRPC doesn't have server parameters
1881        }
1882    }
1883
1884    /// Sets the notice receiver for this connection.
1885    ///
1886    /// Server notices and warnings are passed to this callback instead of being
1887    /// logged. Pass `None` to restore default logging behavior.
1888    pub fn set_notice_receiver(
1889        &mut self,
1890        receiver: Option<hyperdb_api_core::client::NoticeReceiver>,
1891    ) {
1892        match &mut self.transport {
1893            Transport::Tcp(tcp) => tcp.client.set_notice_receiver(receiver),
1894            Transport::Grpc(_) => {} // gRPC doesn't support notice receivers
1895        }
1896    }
1897
1898    /// Cancels the currently executing query (thread-safe).
1899    ///
1900    /// # Errors
1901    ///
1902    /// - Returns [`Error::FeatureNotSupported`] on gRPC connections — cancellation is not
1903    ///   yet implemented for gRPC transport.
1904    /// - Returns [`Error::Connection`] or [`Error::Io`] if the separate
1905    ///   cancel-request connection to the server fails.
1906    pub fn cancel(&self) -> Result<()> {
1907        match &self.transport {
1908            Transport::Tcp(tcp) => tcp.client.cancel().map_err(Error::from),
1909            Transport::Grpc(_) => Err(Error::feature_not_supported(
1910                "Query cancellation is not yet supported for gRPC connections.",
1911            )),
1912        }
1913    }
1914
1915    /// Closes the connection, detaching all databases first.
1916    ///
1917    /// # Errors
1918    ///
1919    /// - Returns [`Error::Internal`] wrapping the underlying close failure
1920    ///   (its `source` is the transport error) if the client cannot be
1921    ///   shut down cleanly.
1922    /// - Returns [`Error::Internal`] wrapping the detach failure if the
1923    ///   attached database could not be detached but close itself
1924    ///   succeeded.
1925    pub fn close(self) -> Result<()> {
1926        // Detach the attached database to ensure files are flushed and released.
1927        // Always attempt close, even if detach fails.
1928        let detach_err = if let Some(ref db_path) = self.database {
1929            let db_alias = std::path::Path::new(db_path)
1930                .file_stem()
1931                .and_then(|s| s.to_str())
1932                .unwrap_or("db");
1933            self.execute_command(&format!("DETACH DATABASE {}", escape_sql_path(db_alias)))
1934                .err()
1935        } else {
1936            None
1937        };
1938
1939        // Always attempt to close the client to release the connection.
1940        let close_result = match self.transport {
1941            Transport::Tcp(tcp) => tcp.client.close(),
1942            Transport::Grpc(_) => Ok(()), // gRPC connections are stateless
1943        };
1944
1945        if let Err(e) = close_result {
1946            return Err(Error::internal(format!("Failed to close connection: {e}")));
1947        }
1948
1949        if let Some(e) = detach_err {
1950            // Detach failed but close succeeded; surface the detach error.
1951            return Err(Error::internal(format!(
1952                "Failed to detach database during close: {e}"
1953            )));
1954        }
1955
1956        Ok(())
1957    }
1958
1959    /// Unloads the database from memory while keeping the connection active.
1960    ///
1961    /// This executes the `UNLOAD DATABASE` command, which releases the database
1962    /// from memory but keeps the session and connection open. The database can
1963    /// be accessed again by subsequent queries that will automatically reload it.
1964    ///
1965    /// This is useful for releasing memory locks when switching between databases
1966    /// or when working with multiple database files.
1967    ///
1968    /// # Example
1969    ///
1970    /// ```no_run
1971    /// use hyperdb_api::{Connection, CreateMode, HyperProcess, Result};
1972    ///
1973    /// fn main() -> Result<()> {
1974    ///     let hyper = HyperProcess::new(None, None)?;
1975    ///     let conn = Connection::new(&hyper, "test.hyper", CreateMode::Create)?;
1976    ///     
1977    ///     // Do some work with the database
1978    ///     conn.execute_command("CREATE TABLE test (id INT)")?;
1979    ///     
1980    ///     // Unload from memory (but keep connection)
1981    ///     conn.unload_database()?;
1982    ///     
1983    ///     // Database can still be accessed (will be reloaded automatically)
1984    ///     let count: i64 = conn.fetch_scalar("SELECT COUNT(*) FROM test")?;
1985    ///     println!("Count: {}", count);
1986    ///     
1987    ///     Ok(())
1988    /// }
1989    /// ```
1990    ///
1991    /// # Errors
1992    ///
1993    /// Returns [`Error::Server`] if the server rejects the `UNLOAD DATABASE`
1994    /// command (e.g. the database is still in use by another session).
1995    pub fn unload_database(&self) -> Result<()> {
1996        self.execute_command("UNLOAD DATABASE")?;
1997        Ok(())
1998    }
1999
2000    /// Releases the database completely from the session.
2001    ///
2002    /// This executes the `UNLOAD RELEASE` command, which completely releases
2003    /// the database from the session. After this call, the database cannot
2004    /// be accessed until a new connection is established.
2005    ///
2006    /// This is useful for completely freeing database resources when you're
2007    /// done with a database and want to ensure no locks are held.
2008    ///
2009    /// **Note:** This should only be used when the session has exactly one
2010    /// database attached. Hyper does not support `UNLOAD RELEASE` with
2011    /// multiple databases attached to the same session.
2012    ///
2013    /// # Example
2014    ///
2015    /// ```no_run
2016    /// use hyperdb_api::{Connection, CreateMode, HyperProcess, Result};
2017    ///
2018    /// fn main() -> Result<()> {
2019    ///     let hyper = HyperProcess::new(None, None)?;
2020    ///     let conn = Connection::new(&hyper, "test.hyper", CreateMode::Create)?;
2021    ///     
2022    ///     // Do some work with the database
2023    ///     conn.execute_command("CREATE TABLE test (id INT)")?;
2024    ///     
2025    ///     // Release database completely from session
2026    ///     conn.unload_release()?;
2027    ///     
2028    ///     // Database cannot be accessed after this point without new connection
2029    ///     // conn.execute_command("SELECT * FROM test")?; // This would fail
2030    ///     
2031    ///     Ok(())
2032    /// }
2033    /// ```
2034    ///
2035    /// # Errors
2036    ///
2037    /// Returns [`Error::Server`] if the server rejects `UNLOAD RELEASE`, most
2038    /// commonly because multiple databases are attached to the same session
2039    /// (Hyper only supports `UNLOAD RELEASE` with exactly one attached DB).
2040    pub fn unload_release(&self) -> Result<()> {
2041        self.execute_command("UNLOAD RELEASE")?;
2042        Ok(())
2043    }
2044
2045    // =========================================================================
2046    // Query Statistics
2047    // =========================================================================
2048
2049    /// Enables query statistics collection for this connection.
2050    ///
2051    /// After enabling, each `execute_command()` or `execute_query()` call will
2052    /// capture detailed performance metrics from Hyper. Retrieve them via
2053    /// [`last_query_stats()`](Self::last_query_stats).
2054    ///
2055    /// The provider determines how stats are collected. Use
2056    /// [`LogFileStatsProvider`](crate::LogFileStatsProvider) to parse Hyper's log file (requires local
2057    /// `hyperd.log`), or implement a custom [`QueryStatsProvider`](crate::QueryStatsProvider).
2058    ///
2059    /// # Example
2060    ///
2061    /// ```no_run
2062    /// # use hyperdb_api::{Connection, CreateMode, HyperProcess, Result};
2063    /// # fn main() -> Result<()> {
2064    /// # let hyper = HyperProcess::new(None, None)?;
2065    /// # let mut conn = Connection::new(&hyper, "test.hyper", CreateMode::CreateIfNotExists)?;
2066    /// use hyperdb_api::LogFileStatsProvider;
2067    ///
2068    /// // Auto-detect log path from HyperProcess
2069    /// conn.enable_query_stats(LogFileStatsProvider::from_process(&hyper));
2070    ///
2071    /// // Or specify an explicit log path
2072    /// // conn.enable_query_stats(LogFileStatsProvider::new("/path/to/hyperd.log"));
2073    /// # Ok(())
2074    /// # }
2075    /// ```
2076    pub fn enable_query_stats(&mut self, provider: impl QueryStatsProvider + 'static) {
2077        self.stats_provider = Some(Arc::new(provider));
2078    }
2079
2080    /// Disables query statistics collection.
2081    ///
2082    /// After calling this, `last_query_stats()` will return `None`.
2083    pub fn disable_query_stats(&mut self) {
2084        self.stats_provider = None;
2085        if let Ok(mut guard) = self.pending_stats.lock() {
2086            *guard = None;
2087        }
2088    }
2089
2090    /// Returns the query statistics from the most recent query execution.
2091    ///
2092    /// Stats are resolved **lazily** — the log file is read when this method
2093    /// is called, not when the query executes. This is important for streaming
2094    /// queries (`execute_query`), where Hyper writes the execution stats only
2095    /// after the result set is fully consumed.
2096    ///
2097    /// **Call this after consuming the result set** (e.g., after `collect_rows()`,
2098    /// iterating all chunks, or dropping the `Rowset`).
2099    ///
2100    /// Returns `None` if:
2101    /// - Query stats collection is not enabled
2102    /// - No query has been executed yet
2103    /// - Stats could not be found for the last query (e.g., log entry not matched)
2104    ///
2105    /// # Example
2106    ///
2107    /// ```no_run
2108    /// # use hyperdb_api::{Connection, CreateMode, HyperProcess, Result};
2109    /// # fn main() -> Result<()> {
2110    /// # let hyper = HyperProcess::new(None, None)?;
2111    /// # let mut conn = Connection::new(&hyper, "test.hyper", CreateMode::CreateIfNotExists)?;
2112    /// # use hyperdb_api::LogFileStatsProvider;
2113    /// # conn.enable_query_stats(LogFileStatsProvider::from_process(&hyper));
2114    /// conn.execute_command("CREATE TABLE t (id INT)")?;
2115    ///
2116    /// if let Some(stats) = conn.last_query_stats() {
2117    ///     println!("Total: {}s", stats.elapsed_s);
2118    ///     if let Some(ref pre) = stats.pre_execution {
2119    ///         println!("  Parse: {:?}s", pre.parsing_time_s);
2120    ///         println!("  Compile: {:?}s", pre.compilation_time_s);
2121    ///     }
2122    ///     if let Some(ref exec) = stats.execution {
2123    ///         println!("  Execute: {:?}s", exec.elapsed_s);
2124    ///         println!("  Peak mem: {:?} MB", exec.peak_memory_mb);
2125    ///     }
2126    /// }
2127    /// # Ok(())
2128    /// # }
2129    /// ```
2130    pub fn last_query_stats(&self) -> Option<QueryStats> {
2131        let provider = self.stats_provider.as_ref()?;
2132        let mut guard = self.pending_stats.lock().ok()?;
2133        let (token, sql) = guard.take()?;
2134        provider.after_query(token, &sql)
2135    }
2136
2137    /// Internal: call provider's `before_query` if stats are enabled.
2138    fn stats_before_query(&self, sql: &str) -> Option<Box<dyn Any + Send>> {
2139        self.stats_provider.as_ref().map(|p| p.before_query(sql))
2140    }
2141
2142    /// Internal: store the pending token+sql for lazy resolution.
2143    fn stats_store_pending(&self, token: Option<Box<dyn Any + Send>>, sql: &str) {
2144        if let Some(token) = token
2145            && let Ok(mut guard) = self.pending_stats.lock()
2146        {
2147            *guard = Some((token, sql.to_string()));
2148        }
2149    }
2150}
2151
2152impl Connection {
2153    // =========================================================================
2154    // Transaction Control
2155    // =========================================================================
2156
2157    // -------------------------------------------------------------------
2158    // Raw transaction control (internal)
2159    // -------------------------------------------------------------------
2160    //
2161    // The `*_unguarded` methods below are the canonical implementation of
2162    // session-level transaction control. The RAII guard at
2163    // `crate::Transaction` and any helper that genuinely needs `&self`
2164    // (rather than the guard's `&mut self`) delegate to these.
2165    //
2166    // They are public because a `&self` helper cannot use the guard at all,
2167    // and `hyperdb-mcp`'s engine is exactly that case. They replaced the
2168    // `#[doc(hidden)] #[deprecated]` `begin_transaction`/`commit`/`rollback`
2169    // wrappers, which were removed in 1.0.0.
2170
2171    /// Issues `BEGIN TRANSACTION` without returning a guard.
2172    ///
2173    /// **Prefer [`transaction()`](Self::transaction).** The RAII guard cannot
2174    /// leak a half-open transaction across an error path, and rolls back on
2175    /// drop. Reach for this only when the guard's `&mut self` borrow is
2176    /// impossible — for example inside a helper that holds `&self` and so
2177    /// cannot borrow the connection mutably.
2178    ///
2179    /// Pairing is the caller's responsibility: every call must be matched by
2180    /// [`commit_unguarded`](Self::commit_unguarded) or
2181    /// [`rollback_unguarded`](Self::rollback_unguarded) on **every** path,
2182    /// including panics. Leaving one open wedges the session — subsequent
2183    /// statements fail with "transaction already in progress" on a connection
2184    /// that is otherwise healthy, so reconnect logic will not recover it.
2185    ///
2186    /// # Errors
2187    ///
2188    /// Returns [`Error::Server`] if the server rejects `BEGIN TRANSACTION`
2189    /// (e.g. a transaction is already open on this session).
2190    pub fn begin_transaction_unguarded(&self) -> Result<()> {
2191        self.execute_command("BEGIN TRANSACTION")?;
2192        Ok(())
2193    }
2194
2195    /// Issues `COMMIT` for a transaction opened with
2196    /// [`begin_transaction_unguarded`](Self::begin_transaction_unguarded).
2197    ///
2198    /// **Prefer [`Transaction::commit`](crate::Transaction::commit)** on the
2199    /// guard returned by [`transaction()`](Self::transaction).
2200    ///
2201    /// # Errors
2202    ///
2203    /// Returns [`Error::Server`] if the server rejects `COMMIT`.
2204    pub fn commit_unguarded(&self) -> Result<()> {
2205        self.execute_command("COMMIT")?;
2206        Ok(())
2207    }
2208
2209    /// Issues `ROLLBACK` for a transaction opened with
2210    /// [`begin_transaction_unguarded`](Self::begin_transaction_unguarded).
2211    ///
2212    /// **Prefer [`Transaction::rollback`](crate::Transaction::rollback)** on
2213    /// the guard returned by [`transaction()`](Self::transaction).
2214    ///
2215    /// # Errors
2216    ///
2217    /// Returns [`Error::Server`] if the server rejects `ROLLBACK`.
2218    pub fn rollback_unguarded(&self) -> Result<()> {
2219        self.execute_command("ROLLBACK")?;
2220        Ok(())
2221    }
2222
2223    /// Starts a transaction and returns an RAII guard that auto-rolls back on drop.
2224    ///
2225    /// The returned [`Transaction`](crate::Transaction) exclusively borrows this connection,
2226    /// preventing any other use of the connection while the transaction is active.
2227    /// This is enforced at compile time by Rust's borrow checker. The guard provides
2228    /// `commit()` and `rollback()` methods. If dropped without calling either, the
2229    /// transaction is automatically rolled back.
2230    ///
2231    /// # Example
2232    ///
2233    /// ```no_run
2234    /// # use hyperdb_api::{Connection, CreateMode, Result};
2235    /// # fn main() -> Result<()> {
2236    /// # let mut conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::DoNotCreate)?;
2237    /// let txn = conn.transaction()?;
2238    /// txn.execute_command("INSERT INTO users VALUES (1, 'Alice')")?;
2239    /// txn.commit()?; // or drop `txn` to auto-rollback
2240    /// # Ok(())
2241    /// # }
2242    /// ```
2243    ///
2244    /// # Errors
2245    ///
2246    /// Returns [`Error::Server`] if the server rejects the `BEGIN`
2247    /// statement issued internally by
2248    /// [`Transaction::new`](crate::Transaction).
2249    pub fn transaction(&mut self) -> Result<crate::Transaction<'_>> {
2250        crate::Transaction::new(self)
2251    }
2252}
2253
2254/// Checks if an error indicates an "already exists" condition based on SQLSTATE codes.
2255///
2256/// This function uses `PostgreSQL` SQLSTATE codes to reliably detect duplicate object errors
2257/// regardless of server locale or message formatting. The codes checked are:
2258/// - `42P04`: Database already exists
2259/// - `42710`: Duplicate object
2260/// - `42P06`: Duplicate schema
2261/// - `42P07`: Duplicate table
2262///
2263/// See: <https://www.postgresql.org/docs/current/errcodes-appendix.html>
2264fn is_already_exists_error(err: &Error) -> bool {
2265    err.sqlstate()
2266        .is_some_and(|code| matches!(code, "42P04" | "42710" | "42P06" | "42P07"))
2267}