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::CreateMode;
13use crate::async_result::AsyncRowset;
14use crate::async_transport::{AsyncTcpTransport, AsyncTransport};
15use crate::error::{Error, Result};
16use crate::names::escape_sql_path;
17use crate::query_stats::{QueryStats, QueryStatsProvider};
18use crate::result::{Row, RowValue};
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    /// Returns a lazy `Stream` over rows, mapping each to `T` via
390    /// [`FromRow`].
391    ///
392    /// This is the streaming variant of [`fetch_all_as`](Self::fetch_all_as):
393    /// memory usage is bounded by the chunk size (default 64K rows), not by
394    /// the total row count. Use this for large result sets where collecting
395    /// all rows into a `Vec` would exceed memory limits.
396    ///
397    /// The column-name → index lookup table is built exactly once (on the
398    /// first non-empty chunk) and reused for all rows, so per-row mapping is
399    /// O(1) in column count.
400    ///
401    /// # Example
402    ///
403    /// ```no_run
404    /// # use hyperdb_api::{AsyncConnection, CreateMode, FromRow, RowAccessor, Result};
405    /// # use futures::StreamExt;
406    /// # struct User { id: i32, name: String }
407    /// # impl FromRow for User {
408    /// #     fn from_row(row: RowAccessor<'_>) -> Result<Self> {
409    /// #         Ok(User { id: row.get("id")?, name: row.get("name")? })
410    /// #     }
411    /// # }
412    /// # async fn example(conn: &AsyncConnection) -> Result<()> {
413    /// let stream = conn.stream_as::<User>("SELECT id, name FROM users");
414    /// tokio::pin!(stream);
415    /// while let Some(row_result) = stream.next().await {
416    ///     let user = row_result?;
417    ///     println!("{}: {}", user.id, user.name);
418    /// }
419    /// # Ok(())
420    /// # }
421    /// ```
422    ///
423    /// # Errors
424    ///
425    /// Each yielded item is a `Result<T>`:
426    /// - The first item will be `Err(e)` if query submission fails (parse
427    ///   failures, server errors, transport failures). The stream is lazy and
428    ///   does not execute the query until first polled.
429    /// - Subsequent items are `Ok(T)` if the row was successfully mapped via
430    ///   `FromRow`, or `Err(e)` if mapping failed (missing column, type
431    ///   mismatch, NULL in a non-optional field). These errors surface lazily
432    ///   during iteration.
433    ///
434    /// [`FromRow`]: crate::FromRow
435    pub fn stream_as<'a, T: crate::FromRow + 'a>(
436        &'a self,
437        query: &str,
438    ) -> impl futures_core::Stream<Item = Result<T>> + 'a + use<'a, T> {
439        // Own the query string so the stream doesn't borrow the &str arg
440        // across await points.
441        let query = query.to_owned();
442        async_stream::try_stream! {
443            let mut rs = self.execute_query(&query).await?;   // submit err → first Err item
444            let mut indices: Option<std::collections::HashMap<String, usize>> = None;
445            while let Some(chunk) = rs.next_chunk().await? {
446                // Build the name→index map once, on the first chunk, after
447                // next_chunk() has materialized the schema (TCP sends the
448                // RowDescription as the first stream message). If the schema is
449                // somehow unavailable, fall back to an empty map so per-row
450                // lookups surface a `Missing` error — matching `fetch_all_as`'s
451                // `unwrap_or_default()` and the sync `stream_as`, rather than
452                // silently skipping the chunk.
453                if indices.is_none() {
454                    let map = rs
455                        .schema()
456                        .map(|schema| crate::RowAccessor::build_owned_indices(&schema))
457                        .unwrap_or_default();
458                    indices = Some(map);
459                }
460                let idx = indices.get_or_insert_with(Default::default);
461                for row in &chunk {
462                    yield T::from_row(crate::RowAccessor::new_owned(row, idx))?;
463                }
464            }
465        }
466    }
467
468    /// Fetches a single row from a **parameterized** query and maps it to a
469    /// struct using [`FromRow`](crate::FromRow) (async).
470    ///
471    /// Parameterized counterpart to [`fetch_one_as`](Self::fetch_one_as): binds
472    /// `$1`, `$2`, … placeholders from `params` (via
473    /// [`ToSqlParam`](crate::params::ToSqlParam), exactly as
474    /// [`query_params`](Self::query_params)) and maps the first result row into
475    /// `T`.
476    ///
477    /// # Errors
478    ///
479    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports (prepared
480    ///   statements are TCP-only).
481    /// - Returns the error from [`query_params`](Self::query_params) if the
482    ///   server rejects the statement, or on transport-level I/O failures.
483    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"`
484    ///   if the query produced zero rows.
485    /// - Returns whatever [`FromRow::from_row`](crate::FromRow::from_row)
486    ///   produces when the row cannot be mapped.
487    pub async fn fetch_one_as_params<T: crate::FromRow>(
488        &self,
489        query: &str,
490        params: &[&dyn crate::params::ToSqlParam],
491    ) -> Result<T> {
492        let row = self
493            .query_params(query, params)
494            .await?
495            .require_first_row()
496            .await?;
497        let indices = row
498            .schema()
499            .map(crate::row_accessor::RowAccessor::build_indices)
500            .unwrap_or_default();
501        T::from_row(crate::RowAccessor::new(&row, &indices))
502    }
503
504    /// Fetches all rows from a **parameterized** query and maps them to structs
505    /// using [`FromRow`](crate::FromRow) (async).
506    ///
507    /// Parameterized counterpart to [`fetch_all_as`](Self::fetch_all_as): binds
508    /// `$1`, `$2`, … placeholders from `params` (see
509    /// [`query_params`](Self::query_params)) and maps every result row into `T`.
510    ///
511    /// # Errors
512    ///
513    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports.
514    /// - Returns the error from [`query_params`](Self::query_params) if the
515    ///   server rejects the statement, or on transport-level I/O failures.
516    /// - Returns the first error produced by
517    ///   [`FromRow::from_row`](crate::FromRow::from_row) on any row.
518    pub async fn fetch_all_as_params<T: crate::FromRow>(
519        &self,
520        query: &str,
521        params: &[&dyn crate::params::ToSqlParam],
522    ) -> Result<Vec<T>> {
523        let rows = self
524            .query_params(query, params)
525            .await?
526            .collect_rows()
527            .await?;
528        // Build the column-name → index lookup once from the first row's
529        // schema; reuse for every row. See `fetch_all_as`.
530        let indices = rows
531            .first()
532            .and_then(crate::result::Row::schema)
533            .map(crate::row_accessor::RowAccessor::build_indices)
534            .unwrap_or_default();
535        rows.iter()
536            .map(|r| T::from_row(crate::RowAccessor::new(r, &indices)))
537            .collect()
538    }
539
540    /// Returns a lazy `Stream` over the rows of a **parameterized** query,
541    /// mapping each to `T` via [`FromRow`] (async).
542    ///
543    /// Parameterized counterpart to [`stream_as`](Self::stream_as): binds `$1`,
544    /// `$2`, … placeholders from `params` and streams the result with O(chunk)
545    /// memory, mapping each row into `T`. The column-index map is built once on
546    /// the first chunk and reused.
547    ///
548    /// # Errors
549    ///
550    /// Like [`stream_as`](Self::stream_as), this returns the `Stream` directly
551    /// (no outer `Result`) — the query is lazy and does not execute until first
552    /// polled. Each yielded item is a `Result<T>`:
553    /// - The **first** item is `Err(e)` if statement submission fails:
554    ///   [`Error::FeatureNotSupported`] on gRPC transport, a `Parse`/`Bind`
555    ///   rejection, or a transport failure. These surface as the first item,
556    ///   *not* eagerly.
557    /// - Subsequent items are `Ok(T)` on a clean per-row mapping, or `Err(e)`
558    ///   for a mapping failure (missing column, type mismatch, NULL in a
559    ///   non-`Option` field) or a transport error hit on a later chunk.
560    ///
561    /// [`FromRow`]: crate::FromRow
562    pub fn stream_as_params<'a, T: crate::FromRow + 'a>(
563        &'a self,
564        query: &str,
565        params: &[&dyn crate::params::ToSqlParam],
566    ) -> impl futures_core::Stream<Item = Result<T>> + 'a + use<'a, T> {
567        // `&[&dyn ToSqlParam]` can't cross the `try_stream!` await points, so
568        // own the query string and encode params up front (encoding needs no
569        // connection). The prepare+execute sequence below mirrors
570        // `query_params` (see that method) — keep the two in sync if its
571        // Parse/Bind/Execute handling ever changes.
572        let query = query.to_owned();
573        let oids: Vec<crate::Oid> = params.iter().map(|p| p.sql_oid()).collect();
574        let (encoded, formats) = crate::async_prepared::encode_params(params);
575        async_stream::try_stream! {
576            let client = match &self.transport {
577                AsyncTransport::Tcp(tcp) => &tcp.client,
578                AsyncTransport::Grpc(_) => {
579                    // `?` inside try_stream! yields Err(e) and terminates the
580                    // generator, so `unreachable!()` is dead code — its `!`
581                    // type just satisfies the arm's need for a `&AsyncClient`
582                    // (same type the Tcp arm produces).
583                    Err(Error::feature_not_supported(
584                        "prepared statements are not supported over gRPC transport",
585                    ))?;
586                    unreachable!()
587                }
588            };
589            let stmt = client.prepare_typed(&query, &oids).await?;
590            let stream = client
591                .execute_prepared_streaming_with_formats(
592                    &stmt,
593                    encoded,
594                    &formats,
595                    crate::result::DEFAULT_BINARY_CHUNK_SIZE,
596                )
597                .await?;
598            let mut rs = AsyncRowset::from_prepared(stream).with_statement_guard(stmt);
599            // The Prepared path captures the schema at prepare time, so the
600            // column-name → index map is available immediately — build it once
601            // up front rather than deferring to the first chunk (the empty-map
602            // fallback matches `stream_as` / `fetch_all_as` if it is somehow
603            // unavailable, surfacing a per-row `Missing` error).
604            let idx = rs
605                .schema()
606                .map(|schema| crate::RowAccessor::build_owned_indices(&schema))
607                .unwrap_or_default();
608            while let Some(chunk) = rs.next_chunk().await? {
609                for row in &chunk {
610                    yield T::from_row(crate::RowAccessor::new_owned(row, &idx))?;
611                }
612            }
613        }
614    }
615
616    /// Fetches a single non-NULL scalar value. Errors on empty / NULL.
617    ///
618    /// # Errors
619    ///
620    /// - Returns the error from [`execute_query`](Self::execute_query).
621    /// - Returns [`Error::Conversion`] with message `"Query returned no rows"` if
622    ///   the query is empty.
623    /// - Returns [`Error::Conversion`] with message `"Scalar query returned NULL"`
624    ///   if the first cell is SQL `NULL`.
625    pub async fn fetch_scalar<T, Q>(&self, query: Q) -> Result<T>
626    where
627        T: RowValue,
628        Q: AsRef<str>,
629    {
630        self.execute_query(query.as_ref())
631            .await?
632            .require_scalar()
633            .await
634    }
635
636    /// Fetches a single scalar value, allowing NULL (returns `None`).
637    ///
638    /// # Errors
639    ///
640    /// Returns the error from [`execute_query`](Self::execute_query). An
641    /// empty result still yields an error; SQL `NULL` in the first cell
642    /// yields `Ok(None)`.
643    pub async fn fetch_optional_scalar<T, Q>(&self, query: Q) -> Result<Option<T>>
644    where
645        T: RowValue,
646        Q: AsRef<str>,
647    {
648        self.execute_query(query.as_ref()).await?.scalar().await
649    }
650
651    /// Returns the count from a `SELECT COUNT(*)` style query, defaulting
652    /// to 0 on NULL.
653    ///
654    /// # Errors
655    ///
656    /// Returns the error from [`execute_query`](Self::execute_query) if the
657    /// query itself fails.
658    pub async fn query_count(&self, query: &str) -> Result<i64> {
659        let opt: Option<i64> = self.fetch_optional_scalar(query).await?;
660        Ok(opt.unwrap_or(0))
661    }
662
663    // =========================================================================
664    // Arrow Queries
665    // =========================================================================
666
667    /// Executes a SELECT query and returns results as Arrow IPC stream bytes (async).
668    ///
669    /// TCP uses `COPY ... TO STDOUT WITH (FORMAT ARROWSTREAM)`; gRPC uses
670    /// the native Arrow transport. Both return the same IPC stream shape.
671    ///
672    /// # Errors
673    ///
674    /// Propagates any [`Error::Server`] from the transport when the query
675    /// fails or the server cannot produce Arrow IPC output.
676    pub async fn execute_query_to_arrow(&self, sql: &str) -> Result<bytes::Bytes> {
677        self.transport.execute_query_to_arrow(sql).await
678    }
679
680    /// Exports an entire table to Arrow IPC stream format (async).
681    ///
682    /// # Errors
683    ///
684    /// See [`execute_query_to_arrow`](Self::execute_query_to_arrow).
685    pub async fn export_table_to_arrow(&self, table_name: &str) -> Result<bytes::Bytes> {
686        self.execute_query_to_arrow(&format!("SELECT * FROM {table_name}"))
687            .await
688    }
689
690    /// Executes a SELECT query and returns parsed Arrow `RecordBatch`es (async).
691    ///
692    /// # Errors
693    ///
694    /// - Returns [`Error::Server`] if the query fails.
695    /// - Returns [`Error::Conversion`] if the Arrow IPC payload cannot be
696    ///   decoded into record batches.
697    pub async fn execute_query_to_batches(
698        &self,
699        sql: &str,
700    ) -> Result<Vec<arrow::record_batch::RecordBatch>> {
701        let arrow_data = self.execute_query_to_arrow(sql).await?;
702        crate::arrow_result::parse_arrow_ipc(arrow_data)
703    }
704
705    // =========================================================================
706    // Parameterized Queries
707    // =========================================================================
708
709    /// Executes a parameterized query with binary-encoded parameters (async).
710    ///
711    /// Mirrors the sync [`Connection::query_params`](crate::Connection::query_params);
712    /// see that method for the design rationale. Parameters travel through the
713    /// extended query protocol (Parse/Bind/Execute) in HyperBinary format — no
714    /// SQL escaping, full SQL-injection safety regardless of parameter content.
715    ///
716    /// # Errors
717    ///
718    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports (prepared statements
719    ///   are TCP-only).
720    /// - Returns [`Error::Server`] if the server rejects the statement at
721    ///   `Parse`, `Bind`, or `Execute` time.
722    /// - Returns [`Error::Io`] on transport-level I/O failures.
723    pub async fn query_params(
724        &self,
725        query: &str,
726        params: &[&dyn crate::params::ToSqlParam],
727    ) -> Result<AsyncRowset<'_>> {
728        // Route through the extended query protocol. See
729        // [`Connection::query_params`] for the sync equivalent and the
730        // rationale behind the statement-guard pattern.
731        let client = match &self.transport {
732            AsyncTransport::Tcp(tcp) => &tcp.client,
733            AsyncTransport::Grpc(_) => {
734                return Err(Error::feature_not_supported(
735                    "prepared statements are not supported over gRPC transport",
736                ));
737            }
738        };
739        let oids: Vec<crate::Oid> = params.iter().map(|p| p.sql_oid()).collect();
740        let stmt = client.prepare_typed(query, &oids).await?;
741        let (encoded, formats) = crate::async_prepared::encode_params(params);
742        let stream = client
743            .execute_prepared_streaming_with_formats(
744                &stmt,
745                encoded,
746                &formats,
747                crate::result::DEFAULT_BINARY_CHUNK_SIZE,
748            )
749            .await?;
750        Ok(AsyncRowset::from_prepared(stream).with_statement_guard(stmt))
751    }
752
753    /// Executes a parameterized command (INSERT / UPDATE / DELETE) with
754    /// binary-encoded parameters via Parse/Bind/Execute (async).
755    ///
756    /// # Errors
757    ///
758    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports.
759    /// - Returns [`Error::Server`] if the server rejects the statement at
760    ///   `Parse`, `Bind`, or `Execute` time.
761    /// - Returns [`Error::Io`] on transport-level I/O failures.
762    pub async fn command_params(
763        &self,
764        query: &str,
765        params: &[&dyn crate::params::ToSqlParam],
766    ) -> Result<u64> {
767        let client = match &self.transport {
768            AsyncTransport::Tcp(tcp) => &tcp.client,
769            AsyncTransport::Grpc(_) => {
770                return Err(Error::feature_not_supported(
771                    "prepared statements are not supported over gRPC transport",
772                ));
773            }
774        };
775        let oids: Vec<crate::Oid> = params.iter().map(|p| p.sql_oid()).collect();
776        let stmt = client.prepare_typed(query, &oids).await?;
777        let (encoded, formats) = crate::async_prepared::encode_params(params);
778        Ok(client
779            .execute_prepared_no_result_with_formats(&stmt, encoded, &formats)
780            .await?)
781    }
782
783    // =========================================================================
784    // Catalog / Database Management
785    // =========================================================================
786
787    /// Creates a new database file (async).
788    ///
789    /// # Errors
790    ///
791    /// Returns [`Error::Server`] if the server rejects
792    /// `CREATE DATABASE IF NOT EXISTS` (e.g. the path is not writable).
793    pub async fn create_database(&self, path: &str) -> Result<()> {
794        let sql = format!("CREATE DATABASE IF NOT EXISTS {}", escape_sql_path(path));
795        self.execute_command(&sql).await?;
796        Ok(())
797    }
798
799    /// Drops (deletes) a database file (async).
800    ///
801    /// # Errors
802    ///
803    /// Returns [`Error::Server`] if the server rejects
804    /// `DROP DATABASE IF EXISTS` (e.g. the database is still attached).
805    pub async fn drop_database(&self, path: &str) -> Result<()> {
806        let sql = format!("DROP DATABASE IF EXISTS {}", escape_sql_path(path));
807        self.execute_command(&sql).await?;
808        Ok(())
809    }
810
811    /// Attaches a database file to the connection (async).
812    ///
813    /// # Errors
814    ///
815    /// Returns [`Error::Server`] if the server rejects the
816    /// `ATTACH DATABASE` statement (file missing, permission denied,
817    /// alias conflict).
818    pub async fn attach_database(&self, path: &str, alias: Option<&str>) -> Result<()> {
819        let sql = if let Some(alias) = alias {
820            format!(
821                "ATTACH DATABASE {} AS {}",
822                escape_sql_path(path),
823                escape_sql_path(alias)
824            )
825        } else {
826            format!("ATTACH DATABASE {}", escape_sql_path(path))
827        };
828        self.execute_command(&sql).await?;
829        Ok(())
830    }
831
832    /// Detaches a database alias from this connection (async).
833    ///
834    /// # Errors
835    ///
836    /// Returns [`Error::Server`] if the alias is not attached or the
837    /// server cannot flush pending updates.
838    pub async fn detach_database(&self, alias: &str) -> Result<()> {
839        let sql = format!("DETACH DATABASE {}", escape_sql_path(alias));
840        self.execute_command(&sql).await?;
841        Ok(())
842    }
843
844    /// Detaches all databases from this connection (async).
845    ///
846    /// # Errors
847    ///
848    /// Returns [`Error::Server`] if the server rejects
849    /// `DETACH ALL DATABASES`.
850    pub async fn detach_all_databases(&self) -> Result<()> {
851        self.execute_command("DETACH ALL DATABASES").await?;
852        Ok(())
853    }
854
855    /// Copies a database file to a new path (async).
856    ///
857    /// # Errors
858    ///
859    /// Returns [`Error::Server`] if the server rejects the
860    /// `COPY DATABASE` statement — e.g. the source is not attached or the
861    /// destination path is not writable.
862    pub async fn copy_database(&self, source: &str, destination: &str) -> Result<()> {
863        let sql = format!(
864            "COPY DATABASE {} TO {}",
865            escape_sql_path(source),
866            escape_sql_path(destination)
867        );
868        self.execute_command(&sql).await?;
869        Ok(())
870    }
871
872    /// Creates a schema in the database (async).
873    ///
874    /// # Errors
875    ///
876    /// - Returns an error if `schema_name` cannot be converted to a
877    ///   [`SchemaName`](crate::SchemaName).
878    /// - Returns [`Error::Server`] if the server rejects
879    ///   `CREATE SCHEMA IF NOT EXISTS`.
880    pub async fn create_schema<T>(&self, schema_name: T) -> Result<()>
881    where
882        T: TryInto<crate::SchemaName>,
883        crate::Error: From<T::Error>,
884    {
885        let schema: crate::SchemaName = schema_name.try_into()?;
886        let sql = format!("CREATE SCHEMA IF NOT EXISTS {schema}");
887        self.execute_command(&sql).await?;
888        Ok(())
889    }
890
891    /// Checks whether a schema exists (async).
892    ///
893    /// # Errors
894    ///
895    /// - Returns an error if `schema` cannot be converted to a
896    ///   [`SchemaName`](crate::SchemaName).
897    /// - Returns [`Error::Server`] if the catalog lookup query fails.
898    pub async fn has_schema<T>(&self, schema: T) -> Result<bool>
899    where
900        T: TryInto<crate::SchemaName>,
901        crate::Error: From<T::Error>,
902    {
903        let schema: crate::SchemaName = schema.try_into()?;
904        let db_prefix = if let Some(db) = schema.database() {
905            format!("{db}.")
906        } else {
907            String::new()
908        };
909        let sql = format!(
910            "SELECT 1 FROM {}pg_catalog.pg_namespace WHERE nspname = '{}'",
911            db_prefix,
912            schema.unescaped().replace('\'', "''")
913        );
914        Ok(self.fetch_optional(&sql).await?.is_some())
915    }
916
917    /// Checks whether a table exists (async).
918    ///
919    /// # Errors
920    ///
921    /// - Returns an error if `table_name` cannot be converted to a
922    ///   [`TableName`](crate::TableName).
923    /// - Returns [`Error::Server`] if the catalog lookup query fails.
924    pub async fn has_table<T>(&self, table_name: T) -> Result<bool>
925    where
926        T: TryInto<crate::TableName>,
927        crate::Error: From<T::Error>,
928    {
929        let table: crate::TableName = table_name.try_into()?;
930        let schema = table
931            .schema()
932            .map_or("public", super::names::Name::unescaped);
933        let db_prefix = if let Some(db) = table.database() {
934            format!("{db}.")
935        } else {
936            String::new()
937        };
938        let sql = format!(
939            "SELECT 1 FROM {}pg_catalog.pg_tables WHERE schemaname = '{}' AND tablename = '{}'",
940            db_prefix,
941            schema.replace('\'', "''"),
942            table.table().unescaped().replace('\'', "''")
943        );
944        Ok(self.fetch_optional(&sql).await?.is_some())
945    }
946
947    /// Unloads the database from memory but keeps the session alive (async).
948    ///
949    /// # Errors
950    ///
951    /// Returns [`Error::Server`] if the server rejects `UNLOAD DATABASE`
952    /// (e.g. the database is in use by another session).
953    pub async fn unload_database(&self) -> Result<()> {
954        self.execute_command("UNLOAD DATABASE").await?;
955        Ok(())
956    }
957
958    /// Releases the database completely from the session (async).
959    ///
960    /// # Errors
961    ///
962    /// Returns [`Error::Server`] if the server rejects `UNLOAD RELEASE`,
963    /// most commonly because multiple databases are attached to the same
964    /// session.
965    pub async fn unload_release(&self) -> Result<()> {
966        self.execute_command("UNLOAD RELEASE").await?;
967        Ok(())
968    }
969
970    // =========================================================================
971    // Diagnostics / Explain
972    // =========================================================================
973
974    /// Executes EXPLAIN and returns the plan text (async).
975    ///
976    /// # Errors
977    ///
978    /// Returns [`Error::Server`] if `EXPLAIN <query>` fails to parse or plan.
979    pub async fn explain(&self, query: &str) -> Result<String> {
980        let sql = format!("EXPLAIN {query}");
981        let rows = self.fetch_all(&sql).await?;
982        let lines: Vec<String> = rows.iter().filter_map(|r| r.get::<String>(0)).collect();
983        Ok(lines.join("\n"))
984    }
985
986    /// Executes EXPLAIN ANALYZE and returns the plan with timing (async).
987    ///
988    /// # Errors
989    ///
990    /// Returns [`Error::Server`] if `EXPLAIN ANALYZE <query>` fails — this
991    /// includes any runtime error raised by actually executing `query`.
992    pub async fn explain_analyze(&self, query: &str) -> Result<String> {
993        let sql = format!("EXPLAIN ANALYZE {query}");
994        let rows = self.fetch_all(&sql).await?;
995        let lines: Vec<String> = rows.iter().filter_map(|r| r.get::<String>(0)).collect();
996        Ok(lines.join("\n"))
997    }
998
999    // =========================================================================
1000    // Connection Introspection / Lifecycle
1001    // =========================================================================
1002
1003    /// Returns true if the connection is alive (passive check).
1004    pub fn is_alive(&self) -> bool {
1005        match &self.transport {
1006            AsyncTransport::Tcp(tcp) => tcp.client.is_alive(),
1007            AsyncTransport::Grpc(_) => true,
1008        }
1009    }
1010
1011    /// Actively pings the server with `SELECT 1` (async).
1012    ///
1013    /// # Errors
1014    ///
1015    /// Returns [`Error::Server`] or [`Error::Io`] if the `SELECT 1`
1016    /// round-trip fails — i.e. the connection is no longer usable.
1017    pub async fn ping(&self) -> Result<()> {
1018        self.execute_command("SELECT 1").await?;
1019        Ok(())
1020    }
1021
1022    /// Returns the backend process ID, or 0 for gRPC transports.
1023    pub fn process_id(&self) -> i32 {
1024        match &self.transport {
1025            AsyncTransport::Tcp(tcp) => tcp.client.process_id(),
1026            AsyncTransport::Grpc(_) => 0,
1027        }
1028    }
1029
1030    /// Returns the secret key used for cancel requests, or 0 for gRPC.
1031    pub fn secret_key(&self) -> i32 {
1032        match &self.transport {
1033            AsyncTransport::Tcp(tcp) => tcp.client.secret_key(),
1034            AsyncTransport::Grpc(_) => 0,
1035        }
1036    }
1037
1038    /// Returns a server parameter value by name (async).
1039    pub async fn parameter_status(&self, name: &str) -> Option<String> {
1040        match &self.transport {
1041            AsyncTransport::Tcp(tcp) => tcp.client.parameter_status(name).await,
1042            AsyncTransport::Grpc(_) => None,
1043        }
1044    }
1045
1046    /// Returns the server version as a parsed struct (async).
1047    pub async fn server_version(&self) -> Option<crate::ServerVersion> {
1048        let version_str = self.parameter_status("server_version").await?;
1049        crate::ServerVersion::parse(&version_str)
1050    }
1051
1052    /// Sets the notice receiver callback for this connection.
1053    pub fn set_notice_receiver(
1054        &mut self,
1055        receiver: Option<Box<dyn Fn(hyperdb_api_core::client::Notice) + Send + Sync>>,
1056    ) {
1057        match &mut self.transport {
1058            AsyncTransport::Tcp(tcp) => tcp.client.set_notice_receiver(receiver),
1059            AsyncTransport::Grpc(_) => {}
1060        }
1061    }
1062
1063    /// Cancels the currently running query (async).
1064    ///
1065    /// # Errors
1066    ///
1067    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports — cancellation is not
1068    ///   yet implemented for gRPC.
1069    /// - Returns [`Error::Connection`] or [`Error::Io`] if the cancel-request
1070    ///   connection to the server fails.
1071    pub async fn cancel(&self) -> Result<()> {
1072        self.transport.cancel().await
1073    }
1074
1075    /// Closes the connection gracefully, detaching any attached database first (async).
1076    ///
1077    /// # Errors
1078    ///
1079    /// - Returns [`Error::Internal`] wrapping the transport close failure if
1080    ///   the client cannot be shut down cleanly.
1081    /// - Returns [`Error::Internal`] wrapping the detach failure if the
1082    ///   attached database could not be detached but the transport close
1083    ///   itself succeeded.
1084    pub async fn close(self) -> Result<()> {
1085        let detach_err = if let Some(ref db_path) = self.database {
1086            let db_alias = std::path::Path::new(db_path)
1087                .file_stem()
1088                .and_then(|s| s.to_str())
1089                .unwrap_or("db");
1090            self.execute_command(&format!("DETACH DATABASE {}", escape_sql_path(db_alias)))
1091                .await
1092                .err()
1093        } else {
1094            None
1095        };
1096
1097        let close_result = self.transport.close().await;
1098
1099        if let Err(e) = close_result {
1100            return Err(Error::internal(format!(
1101                "Failed to close async connection: {e}"
1102            )));
1103        }
1104
1105        if let Some(e) = detach_err {
1106            return Err(Error::internal(format!(
1107                "Failed to detach database during close: {e}"
1108            )));
1109        }
1110
1111        Ok(())
1112    }
1113
1114    /// Returns a reference to the underlying async TCP client (`None` for gRPC).
1115    ///
1116    /// Prefer the high-level `AsyncConnection` methods; this escape hatch
1117    /// remains for code that needs direct protocol access (e.g. custom
1118    /// COPY loops).
1119    pub fn async_tcp_client(&self) -> Option<&hyperdb_api_core::client::AsyncClient> {
1120        self.transport.async_tcp_client()
1121    }
1122
1123    /// Crate-internal accessor for the transport. Used by
1124    /// [`AsyncPreparedStatement`](crate::AsyncPreparedStatement) to reach
1125    /// the underlying `hyperdb_api_core::client::AsyncClient`.
1126    pub(crate) fn transport(&self) -> &AsyncTransport {
1127        &self.transport
1128    }
1129
1130    /// Prepares a SQL statement (async).
1131    ///
1132    /// See [`Connection::prepare`](crate::Connection::prepare) for
1133    /// semantics. The returned
1134    /// [`AsyncPreparedStatement`](crate::AsyncPreparedStatement) can be
1135    /// executed many times with different parameter values.
1136    ///
1137    /// # Errors
1138    ///
1139    /// See [`prepare_typed`](Self::prepare_typed) — this method delegates
1140    /// to it with an empty OID list.
1141    pub async fn prepare(&self, query: &str) -> Result<crate::AsyncPreparedStatement<'_>> {
1142        self.prepare_typed(query, &[]).await
1143    }
1144
1145    /// Prepares a SQL statement with explicit parameter type OIDs (async).
1146    ///
1147    /// # Errors
1148    ///
1149    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports (prepared statements
1150    ///   are TCP-only).
1151    /// - Returns [`Error::Server`] if the server rejects the `Parse`
1152    ///   message (SQL syntax error, unknown OID).
1153    /// - Returns [`Error::Io`] on transport-level I/O failures.
1154    pub async fn prepare_typed(
1155        &self,
1156        query: &str,
1157        param_types: &[crate::Oid],
1158    ) -> Result<crate::AsyncPreparedStatement<'_>> {
1159        let client = match &self.transport {
1160            AsyncTransport::Tcp(tcp) => &tcp.client,
1161            AsyncTransport::Grpc(_) => {
1162                return Err(Error::feature_not_supported(
1163                    "prepared statements are not supported over gRPC transport",
1164                ));
1165            }
1166        };
1167        let inner = client.prepare_typed(query, param_types).await?;
1168        crate::AsyncPreparedStatement::new(self, inner)
1169    }
1170
1171    /// Owned-handle variant of [`prepare`](Self::prepare). Returns a
1172    /// `'static`-lifetime [`AsyncPreparedStatementOwned`](crate::AsyncPreparedStatementOwned)
1173    /// that holds an `Arc`-cloned reference to `self`.
1174    ///
1175    /// Intended for N-API consumers and any other caller that needs
1176    /// the prepared statement to outlive the stack frame where the
1177    /// connection is held.
1178    ///
1179    /// # Errors
1180    ///
1181    /// See [`prepare_typed_arc`](Self::prepare_typed_arc).
1182    pub async fn prepare_arc(
1183        self: &Arc<Self>,
1184        query: &str,
1185    ) -> Result<crate::async_prepared::AsyncPreparedStatementOwned> {
1186        self.prepare_typed_arc(query, &[]).await
1187    }
1188
1189    /// Owned-handle variant of [`prepare_typed`](Self::prepare_typed).
1190    ///
1191    /// # Errors
1192    ///
1193    /// - Returns [`Error::FeatureNotSupported`] on gRPC transports.
1194    /// - Returns [`Error::Server`] if the server rejects the `Parse`
1195    ///   message.
1196    /// - Returns [`Error::Io`] on transport-level I/O failures.
1197    pub async fn prepare_typed_arc(
1198        self: &Arc<Self>,
1199        query: &str,
1200        param_types: &[crate::Oid],
1201    ) -> Result<crate::async_prepared::AsyncPreparedStatementOwned> {
1202        let client = match &self.transport {
1203            AsyncTransport::Tcp(tcp) => &tcp.client,
1204            AsyncTransport::Grpc(_) => {
1205                return Err(Error::feature_not_supported(
1206                    "prepared statements are not supported over gRPC transport",
1207                ));
1208            }
1209        };
1210        let inner = client.prepare_typed(query, param_types).await?;
1211        crate::async_prepared::AsyncPreparedStatementOwned::new(Arc::clone(self), inner)
1212    }
1213
1214    // =========================================================================
1215    // Query Statistics
1216    // =========================================================================
1217
1218    /// Enables query statistics collection for this connection.
1219    pub fn enable_query_stats(&self, provider: impl QueryStatsProvider + 'static) {
1220        if let Ok(mut guard) = self.stats_provider.lock() {
1221            *guard = Some(Arc::new(provider));
1222        }
1223    }
1224
1225    /// Disables query statistics collection.
1226    pub fn disable_query_stats(&self) {
1227        if let Ok(mut guard) = self.stats_provider.lock() {
1228            *guard = None;
1229        }
1230        if let Ok(mut guard) = self.pending_stats.lock() {
1231            *guard = None;
1232        }
1233    }
1234
1235    /// Returns the stats for the most recent query (if enabled).
1236    pub fn last_query_stats(&self) -> Option<QueryStats> {
1237        let provider = self.stats_provider.lock().ok()?.as_ref().cloned()?;
1238        let mut guard = self.pending_stats.lock().ok()?;
1239        let (token, sql) = guard.take()?;
1240        provider.after_query(token, &sql)
1241    }
1242
1243    fn stats_before_query(&self, sql: &str) -> Option<Box<dyn Any + Send>> {
1244        self.stats_provider
1245            .lock()
1246            .ok()?
1247            .as_ref()
1248            .map(|p| p.before_query(sql))
1249    }
1250
1251    fn stats_store_pending(&self, token: Option<Box<dyn Any + Send>>, sql: &str) {
1252        if let Some(token) = token
1253            && let Ok(mut guard) = self.pending_stats.lock()
1254        {
1255            *guard = Some((token, sql.to_string()));
1256        }
1257    }
1258}
1259
1260impl AsyncConnection {
1261    // =========================================================================
1262    // Transaction Control
1263    // =========================================================================
1264
1265    // -------------------------------------------------------------------
1266    // Raw transaction control (internal)
1267    // -------------------------------------------------------------------
1268    //
1269    // The `*_raw` methods below are `pub(crate)` and form the canonical
1270    // implementation of session-level transaction control. The RAII
1271    // guard at `crate::AsyncTransaction` and any internal helper that
1272    // genuinely needs `&self` (rather than the guard's `&mut self`)
1273    // delegate to these.
1274    //
1275    // They are public because a `&self` helper cannot use the guard at all.
1276    // They replaced the `#[doc(hidden)] #[deprecated]`
1277    // `begin_transaction`/`commit`/`rollback` wrappers, removed in 1.0.0.
1278
1279    /// Issues `BEGIN TRANSACTION` without returning a guard.
1280    ///
1281    /// **Prefer [`transaction()`](Self::transaction).** The RAII guard cannot
1282    /// leak a half-open transaction across an error path, and rolls back on
1283    /// drop. Reach for this only when the guard's `&mut self` borrow is
1284    /// impossible — for example inside a helper that holds `&self` and so
1285    /// cannot borrow the connection mutably.
1286    ///
1287    /// Pairing is the caller's responsibility: every call must be matched by
1288    /// [`commit_unguarded`](Self::commit_unguarded) or
1289    /// [`rollback_unguarded`](Self::rollback_unguarded) on **every** path,
1290    /// including panics and cancelled futures. Leaving one open wedges the
1291    /// session — subsequent statements fail with "transaction already in
1292    /// progress" on a connection that is otherwise healthy, so reconnect
1293    /// logic will not recover it.
1294    ///
1295    /// # Errors
1296    ///
1297    /// Returns [`Error::Server`] if the server rejects `BEGIN TRANSACTION`
1298    /// (e.g. a transaction is already open on this session).
1299    pub async fn begin_transaction_unguarded(&self) -> Result<()> {
1300        self.execute_command("BEGIN TRANSACTION").await?;
1301        Ok(())
1302    }
1303
1304    /// Issues `COMMIT` for a transaction opened with
1305    /// [`begin_transaction_unguarded`](Self::begin_transaction_unguarded).
1306    ///
1307    /// **Prefer [`AsyncTransaction::commit`](crate::AsyncTransaction::commit)**
1308    /// on the guard returned by [`transaction()`](Self::transaction).
1309    ///
1310    /// # Errors
1311    ///
1312    /// Returns [`Error::Server`] if the server rejects `COMMIT`.
1313    pub async fn commit_unguarded(&self) -> Result<()> {
1314        self.execute_command("COMMIT").await?;
1315        Ok(())
1316    }
1317
1318    /// Issues `ROLLBACK` for a transaction opened with
1319    /// [`begin_transaction_unguarded`](Self::begin_transaction_unguarded).
1320    ///
1321    /// **Prefer [`AsyncTransaction::rollback`](crate::AsyncTransaction::rollback)**
1322    /// on the guard returned by [`transaction()`](Self::transaction).
1323    ///
1324    /// # Errors
1325    ///
1326    /// Returns [`Error::Server`] if the server rejects `ROLLBACK`.
1327    pub async fn rollback_unguarded(&self) -> Result<()> {
1328        self.execute_command("ROLLBACK").await?;
1329        Ok(())
1330    }
1331
1332    /// Starts a transaction with an async RAII guard (async).
1333    ///
1334    /// # Errors
1335    ///
1336    /// Returns [`Error::Server`] if the internal `BEGIN` issued by
1337    /// [`AsyncTransaction::new`](crate::AsyncTransaction) fails.
1338    pub async fn transaction(&mut self) -> Result<crate::AsyncTransaction<'_>> {
1339        crate::AsyncTransaction::new(self).await
1340    }
1341}