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