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}