Skip to main content

hyperdb_api/
async_connection.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Async connection to Hyper database.
5//!
6//! This module provides [`AsyncConnection`] the async version of [`Connection`](crate::Connection).
7//! Use this when you're already in an async runtime (tokio).
8
9use std::any::Any;
10use std::sync::{Arc, Mutex};
11
12use crate::async_result::AsyncRowset;
13use crate::async_transport::{AsyncTcpTransport, AsyncTransport};
14use crate::error::{Error, Result};
15use crate::names::escape_sql_path;
16use crate::query_stats::{QueryStats, QueryStatsProvider};
17use crate::result::{Row, RowValue};
18use crate::CreateMode;
19
20/// An async connection to a Hyper database.
21///
22/// This is the async equivalent of [`Connection`](crate::Connection), designed for use
23/// in tokio-based async applications. All I/O operations are non-blocking.
24///
25/// # Example
26///
27/// ```no_run
28/// use hyperdb_api::{AsyncConnection, CreateMode, Result};
29///
30/// #[tokio::main]
31/// async fn main() -> Result<()> {
32///     let conn = AsyncConnection::connect(
33///         "localhost:7483",
34///         "example.hyper",
35///         CreateMode::CreateIfNotExists,
36///     ).await?;
37///
38///     conn.execute_command("CREATE TABLE test (id INT)").await?;
39///     let count: i64 = conn.fetch_scalar("SELECT COUNT(*) FROM test").await?;
40///
41///     conn.close().await?;
42///     Ok(())
43/// }
44/// ```
45pub struct AsyncConnection {
46    transport: AsyncTransport,
47    database: Option<String>,
48    stats_provider: Mutex<Option<Arc<dyn QueryStatsProvider>>>,
49    pending_stats: Mutex<Option<(Box<dyn Any + Send>, String)>>,
50}
51
52impl std::fmt::Debug for AsyncConnection {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("AsyncConnection")
55            .field("database", &self.database)
56            .finish_non_exhaustive()
57    }
58}
59
60impl AsyncConnection {
61    /// Returns a fluent [`AsyncConnectionBuilder`](crate::AsyncConnectionBuilder)
62    /// pointed at `endpoint`.
63    #[must_use]
64    pub fn builder(endpoint: &str) -> crate::AsyncConnectionBuilder {
65        crate::AsyncConnectionBuilder::new(endpoint)
66    }
67
68    /// Connects to a Hyper server (async).
69    ///
70    /// Transport is auto-detected from the endpoint:
71    /// - `https://` or `http://` → gRPC transport
72    /// - Otherwise → TCP transport (`PostgreSQL` wire protocol)
73    ///
74    /// # Errors
75    ///
76    /// - Returns [`Error::Io`] / [`Error::Connection`] if the handshake with
77    ///   the server fails.
78    /// - Returns [`Error::Server`] if the `CreateMode` SQL (`CREATE`
79    ///   / `DROP` / `ATTACH`) is rejected by the server.
80    pub async fn connect(endpoint: &str, database: &str, mode: CreateMode) -> Result<Self> {
81        let transport = AsyncTransport::connect(endpoint, Some(database)).await?;
82        let conn = AsyncConnection {
83            transport,
84            database: Some(database.to_string()),
85            stats_provider: Mutex::new(None),
86            pending_stats: Mutex::new(None),
87        };
88
89        if conn.transport.supports_writes() {
90            conn.handle_creation_mode(database, mode).await?;
91            conn.attach_and_set_path(database).await?;
92        }
93
94        Ok(conn)
95    }
96
97    /// Connects with authentication (async).
98    ///
99    /// # Errors
100    ///
101    /// - Returns [`Error::Authentication`] if authentication is rejected.
102    /// - Returns [`Error::Io`] if the endpoint cannot be reached.
103    /// - Returns [`Error::Server`] if the `CreateMode` SQL is rejected.
104    pub async fn connect_with_auth(
105        endpoint: &str,
106        database: &str,
107        mode: CreateMode,
108        user: &str,
109        password: &str,
110    ) -> Result<Self> {
111        let transport = AsyncTransport::connect_tcp_with_auth(endpoint, user, password).await?;
112        let conn = AsyncConnection {
113            transport,
114            database: Some(database.to_string()),
115            stats_provider: Mutex::new(None),
116            pending_stats: Mutex::new(None),
117        };
118
119        conn.handle_creation_mode(database, mode).await?;
120        conn.attach_and_set_path(database).await?;
121
122        Ok(conn)
123    }
124
125    /// Connects to a server without attaching any database (async).
126    ///
127    /// Useful for running `CREATE DATABASE` / `DROP DATABASE` without an
128    /// active attachment.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`Error::Io`] or [`Error::Connection`] if the TCP handshake
133    /// with `endpoint` fails.
134    pub async fn without_database(endpoint: &str) -> Result<Self> {
135        let transport = AsyncTransport::connect_tcp(endpoint).await?;
136        Ok(AsyncConnection {
137            transport,
138            database: None,
139            stats_provider: Mutex::new(None),
140            pending_stats: Mutex::new(None),
141        })
142    }
143
144    /// Builds an `AsyncConnection` from a pre-existing `AsyncClient` (TCP only).
145    #[must_use]
146    pub fn from_async_client(
147        client: hyperdb_api_core::client::AsyncClient,
148        database: Option<String>,
149    ) -> Self {
150        AsyncConnection {
151            transport: AsyncTransport::Tcp(AsyncTcpTransport { client }),
152            database,
153            stats_provider: Mutex::new(None),
154            pending_stats: Mutex::new(None),
155        }
156    }
157
158    /// Builds an `AsyncConnection` from a pre-constructed transport.
159    ///
160    /// Used by [`AsyncConnectionBuilder`](crate::AsyncConnectionBuilder) to
161    /// stitch together a gRPC transport after its own config construction.
162    pub(crate) fn from_transport(transport: AsyncTransport, database: Option<String>) -> Self {
163        AsyncConnection {
164            transport,
165            database,
166            stats_provider: Mutex::new(None),
167            pending_stats: Mutex::new(None),
168        }
169    }
170
171    /// Runs the configured `CreateMode` as SQL (crate-public for use by
172    /// [`AsyncConnectionBuilder`](crate::AsyncConnectionBuilder)).
173    pub(crate) async fn handle_creation_mode_public(
174        &self,
175        database: &str,
176        mode: CreateMode,
177    ) -> Result<()> {
178        self.handle_creation_mode(database, mode).await
179    }
180
181    /// Attaches the database and sets `search_path` (crate-public for use
182    /// by [`AsyncConnectionBuilder`](crate::AsyncConnectionBuilder)).
183    pub(crate) async fn attach_and_set_path_public(&self, database: &str) -> Result<()> {
184        self.attach_and_set_path(database).await
185    }
186
187    async fn handle_creation_mode(&self, database: &str, mode: CreateMode) -> Result<()> {
188        let escaped_db = escape_sql_path(database);
189        match mode {
190            CreateMode::Create => {
191                self.execute_command(&format!("CREATE DATABASE {escaped_db}"))
192                    .await?;
193            }
194            CreateMode::CreateIfNotExists => {
195                self.execute_command(&format!("CREATE DATABASE IF NOT EXISTS {escaped_db}"))
196                    .await?;
197            }
198            CreateMode::CreateAndReplace => {
199                self.execute_command(&format!("DROP DATABASE IF EXISTS {escaped_db}"))
200                    .await?;
201                self.execute_command(&format!("CREATE DATABASE {escaped_db}"))
202                    .await?;
203            }
204            CreateMode::DoNotCreate => {}
205        }
206        Ok(())
207    }
208
209    async fn attach_and_set_path(&self, database: &str) -> Result<()> {
210        let escaped_db = escape_sql_path(database);
211        let db_alias = std::path::Path::new(database)
212            .file_stem()
213            .and_then(|s| s.to_str())
214            .unwrap_or("db");
215        let escaped_alias = escape_sql_path(db_alias);
216
217        self.execute_command(&format!("ATTACH DATABASE {escaped_db} AS {escaped_alias}"))
218            .await?;
219
220        self.execute_command(&format!("SET search_path TO {escaped_alias}, public"))
221            .await?;
222        Ok(())
223    }
224
225    /// Returns the transport type name (e.g., "TCP", "gRPC").
226    pub fn transport_type(&self) -> &'static str {
227        self.transport.transport_type().as_str()
228    }
229
230    /// Returns true if this connection supports write operations.
231    pub fn supports_writes(&self) -> bool {
232        self.transport.supports_writes()
233    }
234
235    /// Returns the database path.
236    pub fn database(&self) -> Option<&str> {
237        self.database.as_deref()
238    }
239
240    // =========================================================================
241    // Command Execution
242    // =========================================================================
243
244    /// Executes a SQL command that doesn't return rows (async).
245    ///
246    /// Use for DDL statements (CREATE, DROP, ALTER) and DML statements
247    /// (INSERT, UPDATE, DELETE). Returns the number of affected rows (DML)
248    /// or 0 (DDL).
249    ///
250    /// # Errors
251    ///
252    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports that do not yet
253    ///   support write operations.
254    /// - Returns [`Error::Server`] if the SQL fails to parse or execute.
255    /// - Returns [`Error::Io`] on transport-level I/O failures.
256    pub async fn execute_command(&self, sql: &str) -> Result<u64> {
257        let token = self.stats_before_query(sql);
258        let result = self.transport.execute_command(sql).await;
259        self.stats_store_pending(token, sql);
260        result
261    }
262
263    /// Executes multiple SQL statements sequentially (async).
264    ///
265    /// If any statement fails, execution stops and the error is returned
266    /// wrapping the SQL preview for context.
267    ///
268    /// # Errors
269    ///
270    /// Returns an [`Error::Internal`] wrapping the first failing statement's
271    /// error; the wrapping message includes the statement's ordinal and
272    /// an 80-character SQL preview.
273    pub async fn execute_batch(&self, statements: &[&str]) -> Result<u64> {
274        let mut total = 0u64;
275        for (i, stmt) in statements.iter().enumerate() {
276            if !stmt.trim().is_empty() {
277                total += self.execute_command(stmt).await.map_err(|e| {
278                    let preview: String = stmt.chars().take(80).collect();
279                    Error::internal(format!(
280                        "execute_batch failed at statement {} of {}: {}: {}",
281                        i + 1,
282                        statements.len(),
283                        preview,
284                        e,
285                    ))
286                })?;
287            }
288        }
289        Ok(total)
290    }
291
292    // =========================================================================
293    // Query Execution (Streaming)
294    // =========================================================================
295
296    /// Executes a SQL query and returns a streaming [`AsyncRowset`] (async).
297    ///
298    /// Results are streamed in chunks so memory usage stays constant
299    /// regardless of result set size. See [`AsyncRowset`] for the row-level
300    /// API and collectors.
301    ///
302    /// # Errors
303    ///
304    /// - Returns [`Error::Server`] if the SQL is rejected by the server.
305    /// - Returns [`Error::Io`] on transport-level I/O failures while
306    ///   opening the stream.
307    pub async fn execute_query(&self, query: &str) -> Result<AsyncRowset<'_>> {
308        let token = self.stats_before_query(query);
309        let result = self.transport.execute_query_streaming(query).await;
310        self.stats_store_pending(token, query);
311        result
312    }
313
314    /// Fetches a single row, erroring if the query returns zero rows.
315    ///
316    /// # Errors
317    ///
318    /// - Returns the error from [`execute_query`](Self::execute_query) if
319    ///   the query fails.
320    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"` if
321    ///   the query produced zero rows.
322    pub async fn fetch_one<Q: AsRef<str>>(&self, query: Q) -> Result<Row> {
323        self.execute_query(query.as_ref())
324            .await?
325            .require_first_row()
326            .await
327    }
328
329    /// Fetches a single row, returning `None` if the query is empty.
330    ///
331    /// # Errors
332    ///
333    /// Returns the error from [`execute_query`](Self::execute_query) if the
334    /// query fails. An empty result set yields `Ok(None)`, not an error.
335    pub async fn fetch_optional<Q: AsRef<str>>(&self, query: Q) -> Result<Option<Row>> {
336        self.execute_query(query.as_ref()).await?.first_row().await
337    }
338
339    /// Fetches all rows from a query.
340    ///
341    /// # Errors
342    ///
343    /// Returns the error from [`execute_query`](Self::execute_query), or a
344    /// transport error produced while draining every chunk.
345    pub async fn fetch_all<Q: AsRef<str>>(&self, query: Q) -> Result<Vec<Row>> {
346        self.execute_query(query.as_ref())
347            .await?
348            .collect_rows()
349            .await
350    }
351
352    /// Fetches a single row and maps it to a struct using [`crate::FromRow`].
353    ///
354    /// # Errors
355    ///
356    /// - Returns the error from [`fetch_one`](Self::fetch_one).
357    /// - Returns whatever [`FromRow::from_row`](crate::FromRow::from_row)
358    ///   produces when the row cannot be mapped.
359    pub async fn fetch_one_as<T: crate::FromRow>(&self, query: &str) -> Result<T> {
360        let row = self.fetch_one(query).await?;
361        let indices = row
362            .schema()
363            .map(crate::row_accessor::RowAccessor::build_indices)
364            .unwrap_or_default();
365        T::from_row(crate::RowAccessor::new(&row, &indices))
366    }
367
368    /// Fetches all rows and maps them to structs using [`crate::FromRow`].
369    ///
370    /// # Errors
371    ///
372    /// - Returns the error from [`fetch_all`](Self::fetch_all).
373    /// - Returns the first error produced by
374    ///   [`FromRow::from_row`](crate::FromRow::from_row) on any row.
375    pub async fn fetch_all_as<T: crate::FromRow>(&self, query: &str) -> Result<Vec<T>> {
376        let rows = self.fetch_all(query).await?;
377        // Build the column-name → index lookup once from the first
378        // row's schema; reuse for every row.
379        let indices = rows
380            .first()
381            .and_then(crate::result::Row::schema)
382            .map(crate::row_accessor::RowAccessor::build_indices)
383            .unwrap_or_default();
384        rows.iter()
385            .map(|r| T::from_row(crate::RowAccessor::new(r, &indices)))
386            .collect()
387    }
388
389    /// Fetches a single non-NULL scalar value. Errors on empty / NULL.
390    ///
391    /// # Errors
392    ///
393    /// - Returns the error from [`execute_query`](Self::execute_query).
394    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"` if
395    ///   the query is empty.
396    /// - Returns [`Error::Conversion`] with message `"Scalar query returned NULL"`
397    ///   if the first cell is SQL `NULL`.
398    pub async fn fetch_scalar<T, Q>(&self, query: Q) -> Result<T>
399    where
400        T: RowValue,
401        Q: AsRef<str>,
402    {
403        self.execute_query(query.as_ref())
404            .await?
405            .require_scalar()
406            .await
407    }
408
409    /// Fetches a single scalar value, allowing NULL (returns `None`).
410    ///
411    /// # Errors
412    ///
413    /// Returns the error from [`execute_query`](Self::execute_query). An
414    /// empty result still yields an error; SQL `NULL` in the first cell
415    /// yields `Ok(None)`.
416    pub async fn fetch_optional_scalar<T, Q>(&self, query: Q) -> Result<Option<T>>
417    where
418        T: RowValue,
419        Q: AsRef<str>,
420    {
421        self.execute_query(query.as_ref()).await?.scalar().await
422    }
423
424    /// Returns the count from a `SELECT COUNT(*)` style query, defaulting
425    /// to 0 on NULL.
426    ///
427    /// # Errors
428    ///
429    /// Returns the error from [`execute_query`](Self::execute_query) if the
430    /// query itself fails.
431    pub async fn query_count(&self, query: &str) -> Result<i64> {
432        let opt: Option<i64> = self.fetch_optional_scalar(query).await?;
433        Ok(opt.unwrap_or(0))
434    }
435
436    // =========================================================================
437    // Arrow Queries
438    // =========================================================================
439
440    /// Executes a SELECT query and returns results as Arrow IPC stream bytes (async).
441    ///
442    /// TCP uses `COPY ... TO STDOUT WITH (FORMAT ARROWSTREAM)`; gRPC uses
443    /// the native Arrow transport. Both return the same IPC stream shape.
444    ///
445    /// # Errors
446    ///
447    /// Propagates any [`Error::Server`] from the transport when the query
448    /// fails or the server cannot produce Arrow IPC output.
449    pub async fn execute_query_to_arrow(&self, sql: &str) -> Result<bytes::Bytes> {
450        self.transport.execute_query_to_arrow(sql).await
451    }
452
453    /// Exports an entire table to Arrow IPC stream format (async).
454    ///
455    /// # Errors
456    ///
457    /// See [`execute_query_to_arrow`](Self::execute_query_to_arrow).
458    pub async fn export_table_to_arrow(&self, table_name: &str) -> Result<bytes::Bytes> {
459        self.execute_query_to_arrow(&format!("SELECT * FROM {table_name}"))
460            .await
461    }
462
463    /// Executes a SELECT query and returns parsed Arrow `RecordBatch`es (async).
464    ///
465    /// # Errors
466    ///
467    /// - Returns [`Error::Server`] if the query fails.
468    /// - Returns [`Error::Conversion`] if the Arrow IPC payload cannot be
469    ///   decoded into record batches.
470    pub async fn execute_query_to_batches(
471        &self,
472        sql: &str,
473    ) -> Result<Vec<arrow::record_batch::RecordBatch>> {
474        let arrow_data = self.execute_query_to_arrow(sql).await?;
475        crate::arrow_result::parse_arrow_ipc(arrow_data)
476    }
477
478    // =========================================================================
479    // Parameterized Queries
480    // =========================================================================
481
482    /// Executes a parameterized query with safely escaped parameters (async).
483    ///
484    /// Mirrors the sync [`Connection::query_params`](crate::Connection::query_params);
485    /// see that method for the design rationale around text-mode escaping
486    /// vs. future native Bind/Execute support.
487    ///
488    /// # Errors
489    ///
490    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports (prepared statements
491    ///   are TCP-only).
492    /// - Returns [`Error::Server`] if the server rejects the statement at
493    ///   `Parse`, `Bind`, or `Execute` time.
494    /// - Returns [`Error::Io`] on transport-level I/O failures.
495    pub async fn query_params(
496        &self,
497        query: &str,
498        params: &[&dyn crate::params::ToSqlParam],
499    ) -> Result<AsyncRowset<'_>> {
500        // Route through the extended query protocol. See
501        // [`Connection::query_params`] for the sync equivalent and the
502        // rationale behind the statement-guard pattern.
503        let client = match &self.transport {
504            AsyncTransport::Tcp(tcp) => &tcp.client,
505            AsyncTransport::Grpc(_) => {
506                return Err(Error::feature_not_supported(
507                    "prepared statements are not supported over gRPC transport",
508                ));
509            }
510        };
511        let oids: Vec<crate::Oid> = params.iter().map(|p| p.sql_oid()).collect();
512        let stmt = client.prepare_typed(query, &oids).await?;
513        let encoded: Vec<Option<Vec<u8>>> = params.iter().map(|p| p.encode_param()).collect();
514        let stream = client
515            .execute_prepared_streaming(&stmt, encoded, crate::result::DEFAULT_BINARY_CHUNK_SIZE)
516            .await?;
517        Ok(AsyncRowset::from_prepared(stream).with_statement_guard(stmt))
518    }
519
520    /// Executes a parameterized command (INSERT / UPDATE / DELETE) with
521    /// binary-encoded parameters via Parse/Bind/Execute (async).
522    ///
523    /// # Errors
524    ///
525    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports.
526    /// - Returns [`Error::Server`] if the server rejects the statement at
527    ///   `Parse`, `Bind`, or `Execute` time.
528    /// - Returns [`Error::Io`] on transport-level I/O failures.
529    pub async fn command_params(
530        &self,
531        query: &str,
532        params: &[&dyn crate::params::ToSqlParam],
533    ) -> Result<u64> {
534        let client = match &self.transport {
535            AsyncTransport::Tcp(tcp) => &tcp.client,
536            AsyncTransport::Grpc(_) => {
537                return Err(Error::feature_not_supported(
538                    "prepared statements are not supported over gRPC transport",
539                ));
540            }
541        };
542        let oids: Vec<crate::Oid> = params.iter().map(|p| p.sql_oid()).collect();
543        let stmt = client.prepare_typed(query, &oids).await?;
544        let encoded: Vec<Option<Vec<u8>>> = params.iter().map(|p| p.encode_param()).collect();
545        Ok(client.execute_prepared_no_result(&stmt, encoded).await?)
546    }
547
548    // =========================================================================
549    // Catalog / Database Management
550    // =========================================================================
551
552    /// Creates a new database file (async).
553    ///
554    /// # Errors
555    ///
556    /// Returns [`Error::Server`] if the server rejects
557    /// `CREATE DATABASE IF NOT EXISTS` (e.g. the path is not writable).
558    pub async fn create_database(&self, path: &str) -> Result<()> {
559        let sql = format!("CREATE DATABASE IF NOT EXISTS {}", escape_sql_path(path));
560        self.execute_command(&sql).await?;
561        Ok(())
562    }
563
564    /// Drops (deletes) a database file (async).
565    ///
566    /// # Errors
567    ///
568    /// Returns [`Error::Server`] if the server rejects
569    /// `DROP DATABASE IF EXISTS` (e.g. the database is still attached).
570    pub async fn drop_database(&self, path: &str) -> Result<()> {
571        let sql = format!("DROP DATABASE IF EXISTS {}", escape_sql_path(path));
572        self.execute_command(&sql).await?;
573        Ok(())
574    }
575
576    /// Attaches a database file to the connection (async).
577    ///
578    /// # Errors
579    ///
580    /// Returns [`Error::Server`] if the server rejects the
581    /// `ATTACH DATABASE` statement (file missing, permission denied,
582    /// alias conflict).
583    pub async fn attach_database(&self, path: &str, alias: Option<&str>) -> Result<()> {
584        let sql = if let Some(alias) = alias {
585            format!(
586                "ATTACH DATABASE {} AS {}",
587                escape_sql_path(path),
588                escape_sql_path(alias)
589            )
590        } else {
591            format!("ATTACH DATABASE {}", escape_sql_path(path))
592        };
593        self.execute_command(&sql).await?;
594        Ok(())
595    }
596
597    /// Detaches a database alias from this connection (async).
598    ///
599    /// # Errors
600    ///
601    /// Returns [`Error::Server`] if the alias is not attached or the
602    /// server cannot flush pending updates.
603    pub async fn detach_database(&self, alias: &str) -> Result<()> {
604        let sql = format!("DETACH DATABASE {}", escape_sql_path(alias));
605        self.execute_command(&sql).await?;
606        Ok(())
607    }
608
609    /// Detaches all databases from this connection (async).
610    ///
611    /// # Errors
612    ///
613    /// Returns [`Error::Server`] if the server rejects
614    /// `DETACH ALL DATABASES`.
615    pub async fn detach_all_databases(&self) -> Result<()> {
616        self.execute_command("DETACH ALL DATABASES").await?;
617        Ok(())
618    }
619
620    /// Copies a database file to a new path (async).
621    ///
622    /// # Errors
623    ///
624    /// Returns [`Error::Server`] if the server rejects the
625    /// `COPY DATABASE` statement — e.g. the source is not attached or the
626    /// destination path is not writable.
627    pub async fn copy_database(&self, source: &str, destination: &str) -> Result<()> {
628        let sql = format!(
629            "COPY DATABASE {} TO {}",
630            escape_sql_path(source),
631            escape_sql_path(destination)
632        );
633        self.execute_command(&sql).await?;
634        Ok(())
635    }
636
637    /// Creates a schema in the database (async).
638    ///
639    /// # Errors
640    ///
641    /// - Returns an error if `schema_name` cannot be converted to a
642    ///   [`SchemaName`](crate::SchemaName).
643    /// - Returns [`Error::Server`] if the server rejects
644    ///   `CREATE SCHEMA IF NOT EXISTS`.
645    pub async fn create_schema<T>(&self, schema_name: T) -> Result<()>
646    where
647        T: TryInto<crate::SchemaName>,
648        crate::Error: From<T::Error>,
649    {
650        let schema: crate::SchemaName = schema_name.try_into()?;
651        let sql = format!("CREATE SCHEMA IF NOT EXISTS {schema}");
652        self.execute_command(&sql).await?;
653        Ok(())
654    }
655
656    /// Checks whether a schema exists (async).
657    ///
658    /// # Errors
659    ///
660    /// - Returns an error if `schema` cannot be converted to a
661    ///   [`SchemaName`](crate::SchemaName).
662    /// - Returns [`Error::Server`] if the catalog lookup query fails.
663    pub async fn has_schema<T>(&self, schema: T) -> Result<bool>
664    where
665        T: TryInto<crate::SchemaName>,
666        crate::Error: From<T::Error>,
667    {
668        let schema: crate::SchemaName = schema.try_into()?;
669        let db_prefix = if let Some(db) = schema.database() {
670            format!("{db}.")
671        } else {
672            String::new()
673        };
674        let sql = format!(
675            "SELECT 1 FROM {}pg_catalog.pg_namespace WHERE nspname = '{}'",
676            db_prefix,
677            schema.unescaped().replace('\'', "''")
678        );
679        Ok(self.fetch_optional(&sql).await?.is_some())
680    }
681
682    /// Checks whether a table exists (async).
683    ///
684    /// # Errors
685    ///
686    /// - Returns an error if `table_name` cannot be converted to a
687    ///   [`TableName`](crate::TableName).
688    /// - Returns [`Error::Server`] if the catalog lookup query fails.
689    pub async fn has_table<T>(&self, table_name: T) -> Result<bool>
690    where
691        T: TryInto<crate::TableName>,
692        crate::Error: From<T::Error>,
693    {
694        let table: crate::TableName = table_name.try_into()?;
695        let schema = table
696            .schema()
697            .map_or("public", super::names::Name::unescaped);
698        let db_prefix = if let Some(db) = table.database() {
699            format!("{db}.")
700        } else {
701            String::new()
702        };
703        let sql = format!(
704            "SELECT 1 FROM {}pg_catalog.pg_tables WHERE schemaname = '{}' AND tablename = '{}'",
705            db_prefix,
706            schema.replace('\'', "''"),
707            table.table().unescaped().replace('\'', "''")
708        );
709        Ok(self.fetch_optional(&sql).await?.is_some())
710    }
711
712    /// Unloads the database from memory but keeps the session alive (async).
713    ///
714    /// # Errors
715    ///
716    /// Returns [`Error::Server`] if the server rejects `UNLOAD DATABASE`
717    /// (e.g. the database is in use by another session).
718    pub async fn unload_database(&self) -> Result<()> {
719        self.execute_command("UNLOAD DATABASE").await?;
720        Ok(())
721    }
722
723    /// Releases the database completely from the session (async).
724    ///
725    /// # Errors
726    ///
727    /// Returns [`Error::Server`] if the server rejects `UNLOAD RELEASE`,
728    /// most commonly because multiple databases are attached to the same
729    /// session.
730    pub async fn unload_release(&self) -> Result<()> {
731        self.execute_command("UNLOAD RELEASE").await?;
732        Ok(())
733    }
734
735    // =========================================================================
736    // Diagnostics / Explain
737    // =========================================================================
738
739    /// Executes EXPLAIN and returns the plan text (async).
740    ///
741    /// # Errors
742    ///
743    /// Returns [`Error::Server`] if `EXPLAIN <query>` fails to parse or plan.
744    pub async fn explain(&self, query: &str) -> Result<String> {
745        let sql = format!("EXPLAIN {query}");
746        let rows = self.fetch_all(&sql).await?;
747        let lines: Vec<String> = rows.iter().filter_map(|r| r.get::<String>(0)).collect();
748        Ok(lines.join("\n"))
749    }
750
751    /// Executes EXPLAIN ANALYZE and returns the plan with timing (async).
752    ///
753    /// # Errors
754    ///
755    /// Returns [`Error::Server`] if `EXPLAIN ANALYZE <query>` fails — this
756    /// includes any runtime error raised by actually executing `query`.
757    pub async fn explain_analyze(&self, query: &str) -> Result<String> {
758        let sql = format!("EXPLAIN ANALYZE {query}");
759        let rows = self.fetch_all(&sql).await?;
760        let lines: Vec<String> = rows.iter().filter_map(|r| r.get::<String>(0)).collect();
761        Ok(lines.join("\n"))
762    }
763
764    // =========================================================================
765    // Connection Introspection / Lifecycle
766    // =========================================================================
767
768    /// Returns true if the connection is alive (passive check).
769    pub fn is_alive(&self) -> bool {
770        match &self.transport {
771            AsyncTransport::Tcp(tcp) => tcp.client.is_alive(),
772            AsyncTransport::Grpc(_) => true,
773        }
774    }
775
776    /// Actively pings the server with `SELECT 1` (async).
777    ///
778    /// # Errors
779    ///
780    /// Returns [`Error::Server`] or [`Error::Io`] if the `SELECT 1`
781    /// round-trip fails — i.e. the connection is no longer usable.
782    pub async fn ping(&self) -> Result<()> {
783        self.execute_command("SELECT 1").await?;
784        Ok(())
785    }
786
787    /// Returns the backend process ID, or 0 for gRPC transports.
788    pub fn process_id(&self) -> i32 {
789        match &self.transport {
790            AsyncTransport::Tcp(tcp) => tcp.client.process_id(),
791            AsyncTransport::Grpc(_) => 0,
792        }
793    }
794
795    /// Returns the secret key used for cancel requests, or 0 for gRPC.
796    pub fn secret_key(&self) -> i32 {
797        match &self.transport {
798            AsyncTransport::Tcp(tcp) => tcp.client.secret_key(),
799            AsyncTransport::Grpc(_) => 0,
800        }
801    }
802
803    /// Returns a server parameter value by name (async).
804    pub async fn parameter_status(&self, name: &str) -> Option<String> {
805        match &self.transport {
806            AsyncTransport::Tcp(tcp) => tcp.client.parameter_status(name).await,
807            AsyncTransport::Grpc(_) => None,
808        }
809    }
810
811    /// Returns the server version as a parsed struct (async).
812    pub async fn server_version(&self) -> Option<crate::ServerVersion> {
813        let version_str = self.parameter_status("server_version").await?;
814        crate::ServerVersion::parse(&version_str)
815    }
816
817    /// Sets the notice receiver callback for this connection.
818    pub fn set_notice_receiver(
819        &mut self,
820        receiver: Option<Box<dyn Fn(hyperdb_api_core::client::Notice) + Send + Sync>>,
821    ) {
822        match &mut self.transport {
823            AsyncTransport::Tcp(tcp) => tcp.client.set_notice_receiver(receiver),
824            AsyncTransport::Grpc(_) => {}
825        }
826    }
827
828    /// Cancels the currently running query (async).
829    ///
830    /// # Errors
831    ///
832    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports — cancellation is not
833    ///   yet implemented for gRPC.
834    /// - Returns [`Error::Connection`] or [`Error::Io`] if the cancel-request
835    ///   connection to the server fails.
836    pub async fn cancel(&self) -> Result<()> {
837        self.transport.cancel().await
838    }
839
840    /// Closes the connection gracefully, detaching any attached database first (async).
841    ///
842    /// # Errors
843    ///
844    /// - Returns [`Error::Internal`] wrapping the transport close failure if
845    ///   the client cannot be shut down cleanly.
846    /// - Returns [`Error::Internal`] wrapping the detach failure if the
847    ///   attached database could not be detached but the transport close
848    ///   itself succeeded.
849    pub async fn close(self) -> Result<()> {
850        let detach_err = if let Some(ref db_path) = self.database {
851            let db_alias = std::path::Path::new(db_path)
852                .file_stem()
853                .and_then(|s| s.to_str())
854                .unwrap_or("db");
855            self.execute_command(&format!("DETACH DATABASE {}", escape_sql_path(db_alias)))
856                .await
857                .err()
858        } else {
859            None
860        };
861
862        let close_result = self.transport.close().await;
863
864        if let Err(e) = close_result {
865            return Err(Error::internal(format!(
866                "Failed to close async connection: {e}"
867            )));
868        }
869
870        if let Some(e) = detach_err {
871            return Err(Error::internal(format!(
872                "Failed to detach database during close: {e}"
873            )));
874        }
875
876        Ok(())
877    }
878
879    /// Returns a reference to the underlying async TCP client (`None` for gRPC).
880    ///
881    /// Prefer the high-level `AsyncConnection` methods; this escape hatch
882    /// remains for code that needs direct protocol access (e.g. custom
883    /// COPY loops).
884    pub fn async_tcp_client(&self) -> Option<&hyperdb_api_core::client::AsyncClient> {
885        self.transport.async_tcp_client()
886    }
887
888    /// Crate-internal accessor for the transport. Used by
889    /// [`AsyncPreparedStatement`](crate::AsyncPreparedStatement) to reach
890    /// the underlying `hyperdb_api_core::client::AsyncClient`.
891    pub(crate) fn transport(&self) -> &AsyncTransport {
892        &self.transport
893    }
894
895    /// Prepares a SQL statement (async).
896    ///
897    /// See [`Connection::prepare`](crate::Connection::prepare) for
898    /// semantics. The returned
899    /// [`AsyncPreparedStatement`](crate::AsyncPreparedStatement) can be
900    /// executed many times with different parameter values.
901    ///
902    /// # Errors
903    ///
904    /// See [`prepare_typed`](Self::prepare_typed) — this method delegates
905    /// to it with an empty OID list.
906    pub async fn prepare(&self, query: &str) -> Result<crate::AsyncPreparedStatement<'_>> {
907        self.prepare_typed(query, &[]).await
908    }
909
910    /// Prepares a SQL statement with explicit parameter type OIDs (async).
911    ///
912    /// # Errors
913    ///
914    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports (prepared statements
915    ///   are TCP-only).
916    /// - Returns [`Error::Server`] if the server rejects the `Parse`
917    ///   message (SQL syntax error, unknown OID).
918    /// - Returns [`Error::Io`] on transport-level I/O failures.
919    pub async fn prepare_typed(
920        &self,
921        query: &str,
922        param_types: &[crate::Oid],
923    ) -> Result<crate::AsyncPreparedStatement<'_>> {
924        let client = match &self.transport {
925            AsyncTransport::Tcp(tcp) => &tcp.client,
926            AsyncTransport::Grpc(_) => {
927                return Err(Error::feature_not_supported(
928                    "prepared statements are not supported over gRPC transport",
929                ));
930            }
931        };
932        let inner = client.prepare_typed(query, param_types).await?;
933        crate::AsyncPreparedStatement::new(self, inner)
934    }
935
936    /// Owned-handle variant of [`prepare`](Self::prepare). Returns a
937    /// `'static`-lifetime [`AsyncPreparedStatementOwned`](crate::AsyncPreparedStatementOwned)
938    /// that holds an `Arc`-cloned reference to `self`.
939    ///
940    /// Intended for N-API consumers and any other caller that needs
941    /// the prepared statement to outlive the stack frame where the
942    /// connection is held.
943    ///
944    /// # Errors
945    ///
946    /// See [`prepare_typed_arc`](Self::prepare_typed_arc).
947    pub async fn prepare_arc(
948        self: &Arc<Self>,
949        query: &str,
950    ) -> Result<crate::async_prepared::AsyncPreparedStatementOwned> {
951        self.prepare_typed_arc(query, &[]).await
952    }
953
954    /// Owned-handle variant of [`prepare_typed`](Self::prepare_typed).
955    ///
956    /// # Errors
957    ///
958    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports.
959    /// - Returns [`Error::Server`] if the server rejects the `Parse`
960    ///   message.
961    /// - Returns [`Error::Io`] on transport-level I/O failures.
962    pub async fn prepare_typed_arc(
963        self: &Arc<Self>,
964        query: &str,
965        param_types: &[crate::Oid],
966    ) -> Result<crate::async_prepared::AsyncPreparedStatementOwned> {
967        let client = match &self.transport {
968            AsyncTransport::Tcp(tcp) => &tcp.client,
969            AsyncTransport::Grpc(_) => {
970                return Err(Error::feature_not_supported(
971                    "prepared statements are not supported over gRPC transport",
972                ));
973            }
974        };
975        let inner = client.prepare_typed(query, param_types).await?;
976        crate::async_prepared::AsyncPreparedStatementOwned::new(Arc::clone(self), inner)
977    }
978
979    // =========================================================================
980    // Query Statistics
981    // =========================================================================
982
983    /// Enables query statistics collection for this connection.
984    pub fn enable_query_stats(&self, provider: impl QueryStatsProvider + 'static) {
985        if let Ok(mut guard) = self.stats_provider.lock() {
986            *guard = Some(Arc::new(provider));
987        }
988    }
989
990    /// Disables query statistics collection.
991    pub fn disable_query_stats(&self) {
992        if let Ok(mut guard) = self.stats_provider.lock() {
993            *guard = None;
994        }
995        if let Ok(mut guard) = self.pending_stats.lock() {
996            *guard = None;
997        }
998    }
999
1000    /// Returns the stats for the most recent query (if enabled).
1001    pub fn last_query_stats(&self) -> Option<QueryStats> {
1002        let provider = self.stats_provider.lock().ok()?.as_ref().cloned()?;
1003        let mut guard = self.pending_stats.lock().ok()?;
1004        let (token, sql) = guard.take()?;
1005        provider.after_query(token, &sql)
1006    }
1007
1008    fn stats_before_query(&self, sql: &str) -> Option<Box<dyn Any + Send>> {
1009        self.stats_provider
1010            .lock()
1011            .ok()?
1012            .as_ref()
1013            .map(|p| p.before_query(sql))
1014    }
1015
1016    fn stats_store_pending(&self, token: Option<Box<dyn Any + Send>>, sql: &str) {
1017        if let Some(token) = token {
1018            if let Ok(mut guard) = self.pending_stats.lock() {
1019                *guard = Some((token, sql.to_string()));
1020            }
1021        }
1022    }
1023}
1024
1025impl AsyncConnection {
1026    // =========================================================================
1027    // Transaction Control
1028    // =========================================================================
1029
1030    // -------------------------------------------------------------------
1031    // Raw transaction control (internal)
1032    // -------------------------------------------------------------------
1033    //
1034    // The `*_raw` methods below are `pub(crate)` and form the canonical
1035    // implementation of session-level transaction control. The RAII
1036    // guard at `crate::AsyncTransaction` and any internal helper that
1037    // genuinely needs `&self` (rather than the guard's `&mut self`)
1038    // delegate to these.
1039    //
1040    // The matching `pub` methods (`begin_transaction`, `commit`,
1041    // `rollback`) are thin `#[doc(hidden)] #[deprecated]` wrappers
1042    // retained only so any pre-existing downstream caller sees a
1043    // compiler warning rather than a hard break. They will be deleted
1044    // in a future release; the `_raw` methods stay.
1045
1046    /// Issues `BEGIN TRANSACTION`. Crate-internal use only.
1047    pub(crate) async fn begin_transaction_raw(&self) -> Result<()> {
1048        self.execute_command("BEGIN TRANSACTION").await?;
1049        Ok(())
1050    }
1051
1052    /// Issues `COMMIT`. Crate-internal use only.
1053    pub(crate) async fn commit_raw(&self) -> Result<()> {
1054        self.execute_command("COMMIT").await?;
1055        Ok(())
1056    }
1057
1058    /// Issues `ROLLBACK`. Crate-internal use only.
1059    pub(crate) async fn rollback_raw(&self) -> Result<()> {
1060        self.execute_command("ROLLBACK").await?;
1061        Ok(())
1062    }
1063
1064    /// Begins an explicit transaction (async).
1065    ///
1066    /// **Prefer [`transaction()`](Self::transaction)** — the RAII guard
1067    /// auto-rolls back on drop and cannot leak a half-open transaction
1068    /// across error paths. Hidden from generated rustdoc and
1069    /// deprecated; slated for removal in a future release.
1070    ///
1071    /// # Errors
1072    ///
1073    /// Returns [`Error::Server`] if the server rejects `BEGIN TRANSACTION`
1074    /// (e.g. a transaction is already open on this session).
1075    #[doc(hidden)]
1076    #[deprecated(
1077        note = "Use `AsyncConnection::transaction()` for an RAII guard. This method will be \
1078                removed in a future release."
1079    )]
1080    pub async fn begin_transaction(&self) -> Result<()> {
1081        self.begin_transaction_raw().await
1082    }
1083
1084    /// Commits the current transaction (async).
1085    ///
1086    /// **Prefer [`AsyncTransaction::commit`](crate::AsyncTransaction::commit)**
1087    /// on the RAII guard returned by [`transaction()`](Self::transaction).
1088    /// Hidden from generated rustdoc and deprecated; slated for removal.
1089    ///
1090    /// # Errors
1091    ///
1092    /// Returns [`Error::Server`] if the server rejects `COMMIT`.
1093    #[doc(hidden)]
1094    #[deprecated(note = "Use `AsyncTransaction::commit()` on the RAII guard from \
1095                `AsyncConnection::transaction()`. This method will be removed in a future release.")]
1096    pub async fn commit(&self) -> Result<()> {
1097        self.commit_raw().await
1098    }
1099
1100    /// Rolls back the current transaction (async).
1101    ///
1102    /// **Prefer [`AsyncTransaction::rollback`](crate::AsyncTransaction::rollback)**
1103    /// on the RAII guard returned by [`transaction()`](Self::transaction).
1104    /// Hidden from generated rustdoc and deprecated; slated for removal.
1105    ///
1106    /// # Errors
1107    ///
1108    /// Returns [`Error::Server`] if the server rejects `ROLLBACK`.
1109    #[doc(hidden)]
1110    #[deprecated(note = "Use `AsyncTransaction::rollback()` on the RAII guard from \
1111                `AsyncConnection::transaction()`. This method will be removed in a future release.")]
1112    pub async fn rollback(&self) -> Result<()> {
1113        self.rollback_raw().await
1114    }
1115
1116    /// Starts a transaction with an async RAII guard (async).
1117    ///
1118    /// # Errors
1119    ///
1120    /// Returns [`Error::Server`] if the internal `BEGIN` issued by
1121    /// [`AsyncTransaction::new`](crate::AsyncTransaction) fails.
1122    pub async fn transaction(&mut self) -> Result<crate::AsyncTransaction<'_>> {
1123        crate::AsyncTransaction::new(self).await
1124    }
1125}