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}