Skip to main content

hyperdb_api/
lib.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Pure Rust API for Hyper database.
5//!
6//! This crate provides a safe, idiomatic Rust interface for working with
7//! Hyper database files (.hyper). It is a pure-Rust implementation using
8//! the `PostgreSQL` wire protocol with Hyper-specific extensions.
9//!
10//! # Architecture
11//!
12//! This is a layered API built from four crates:
13//! - `hyper-types` — Type definitions with `LittleEndian` encoding
14//! - `hyper-protocol` — Wire protocol with `HyperBinary` COPY support
15//! - `hyper-client` — Sync/async TCP and gRPC clients
16//! - `hyperdb-api` — High-level API (this crate)
17//!
18//! Optional companion crates:
19//! - `sea-query-hyperdb` — `HyperDB` SQL dialect backend for `sea-query`
20//! - `hyperdb-api-salesforce` — Salesforce Data Cloud OAuth authentication
21//! - `hyperdb-api-derive` — Proc-macro `#[derive(FromRow)]` (re-exported by this crate)
22//!
23//! # Quick Start
24//!
25//! ```no_run
26//! use hyperdb_api::{HyperProcess, Connection, CreateMode, Result};
27//!
28//! fn main() -> Result<()> {
29//!     let hyper = HyperProcess::new(None, None)?;
30//!     let conn = Connection::new(&hyper, "example.hyper", CreateMode::CreateIfNotExists)?;
31//!
32//!     conn.execute_command("CREATE TABLE test (id INT, name TEXT)")?;
33//!     conn.execute_command("INSERT INTO test VALUES (1, 'Hello')")?;
34//!
35//!     let mut result = conn.execute_query("SELECT * FROM test")?;
36//!     while let Some(chunk) = result.next_chunk()? {
37//!         for row in &chunk {
38//!             let id: Option<i32> = row.get(0);
39//!             let name: Option<String> = row.get(1);
40//!             println!("id: {:?}, name: {:?}", id, name);
41//!         }
42//!     }
43//!     Ok(())
44//! }
45//! ```
46//!
47//! # Lifetime Safety
48//!
49//! The API uses lifetime annotations to provide compile-time guarantees that
50//! resources are used correctly. All dependent types ([`Inserter`],
51//! [`Catalog`], [`Rowset`], [`Transaction`]) carry a `'conn` lifetime
52//! parameter tying them to the [`Connection`] they borrow:
53//!
54//! ```text
55//! Connection (owns underlying client)
56//! ├── Inserter<'conn>
57//! │   └── CopyInWriter<'conn>
58//! ├── Catalog<'conn>
59//! ├── KvStore<'conn>
60//! ├── Rowset<'conn>
61//! └── Transaction<'conn>
62//! ```
63//!
64//! This is a **simple hierarchical design**, not a complex lifetime web:
65//! - **Single root owner**: `Connection` owns the underlying client
66//! - **Simple borrows**: All dependent types borrow `&'conn Connection`
67//! - **No circular references**: `Inserter` doesn't reference `Catalog`, etc.
68//! - **Single lifetime parameter**: Just one `'conn` — no multi-lifetime bounds
69//!
70//! The Rust borrow checker enforces that you cannot drop or move a `Connection`
71//! while any dependent type holds a reference to it:
72//!
73//! ```compile_fail
74//! # use hyperdb_api::{Connection, Inserter, CreateMode};
75//! # fn example() -> hyperdb_api::Result<()> {
76//! let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
77//! let inserter = Inserter::new(&conn, /* ... */)?;
78//! drop(conn);  // ERROR: cannot move `conn` because it is borrowed by `inserter`
79//! # Ok(())
80//! # }
81//! ```
82//!
83//! The same guarantee holds for a [`KvStore`], which borrows its `Connection`
84//! for the handle's lifetime:
85//!
86//! ```compile_fail
87//! # use hyperdb_api::{Connection, CreateMode};
88//! # fn example() -> hyperdb_api::Result<()> {
89//! let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
90//! let kv = conn.kv_store("s")?;
91//! drop(conn);  // ERROR: cannot move `conn` because it is borrowed by `kv`
92//! let _ = kv.get("k")?;
93//! # Ok(())
94//! # }
95//! ```
96//!
97//! The `execute(self)` method on [`Inserter`] takes ownership (`self`), which
98//! automatically ends the borrow when the insert completes — no manual cleanup
99//! needed.
100//!
101//! # Key Types
102//!
103//! - [`Connection`] / [`AsyncConnection`] — Sync and async database connections
104//! - [`HyperProcess`] — Manage a local `hyperd` server process
105//! - [`Inserter`] / [`MappedInserter`] / [`AsyncInserter`] — Bulk row insertion (`HyperBinary` COPY)
106//! - [`ArrowInserter`] / [`AsyncArrowInserter`] — Arrow `RecordBatch` insertion
107//! - [`Catalog`] — Schema/table introspection
108//! - [`TableDefinition`] — Define table schemas
109//! - [`Transaction`] / [`AsyncTransaction`] — RAII transaction guards
110//! - [`KvStore`] / [`AsyncKvStore`] — String-native key-value store over a shared backing table
111//!
112//! # Public Modules
113//!
114//! - [`copy`] — CSV/text export and import via COPY protocol
115//! - [`pool`] — Async connection pooling (deadpool-based)
116//! - [`grpc`] — gRPC transport types for Arrow IPC queries
117//!
118//! # Bulk Data Loading
119//!
120//! Several inserter APIs are available depending on your data format and runtime model:
121//! - [`Inserter`] / [`MappedInserter`] — Sync `HyperBinary` row-by-row
122//! - [`AsyncInserter`] — Async `HyperBinary` row-by-row (mirrors [`Inserter`])
123//! - [`ArrowInserter`] — Sync Arrow IPC (batch or streaming `RecordBatch`)
124//! - [`AsyncArrowInserter`] — Async Arrow IPC
125//! - [`copy`] module — CSV/TSV/delimited text import & export
126//!
127//! # Authentication
128//!
129//! The client supports multiple authentication methods (Trust, Cleartext, MD5, SCRAM-SHA-256):
130//!
131//! ```no_run
132//! use hyperdb_api::{Connection, CreateMode, Result};
133//!
134//! fn main() -> Result<()> {
135//!     let conn = Connection::connect_with_auth(
136//!         "localhost:7483",
137//!         "example.hyper",
138//!         CreateMode::CreateIfNotExists,
139//!         "myuser",
140//!         "mypassword",
141//!     )?;
142//!     Ok(())
143//! }
144//! ```
145
146#![warn(missing_docs, rust_2018_idioms, clippy::all)]
147// `must_use_candidate` is `allow` workspace-wide because it measures
148// *public API* ergonomics, and 140 of its 141 workspace-wide sites are in
149// places where that does not apply: `hyperdb-api-core` (explicitly not a
150// public API), the `hyperdb-mcp` binary's internal daemon helpers, and
151// prost-generated protobuf code. This crate *is* the public API, so it opts
152// back in.
153#![warn(clippy::must_use_candidate)]
154
155mod arrow_inserter;
156/// Semantic version of this crate, resolved at compile time from
157/// `Cargo.toml`. Used by downstream tools (notably `hyperdb-mcp`) to
158/// surface the library version in their own status output without
159/// duplicating the version string.
160pub const VERSION: &str = env!("CARGO_PKG_VERSION");
161
162mod arrow_reader;
163mod arrow_result;
164mod async_arrow_inserter;
165mod async_connection;
166mod async_connection_builder;
167mod async_inserter;
168mod async_kv_store;
169mod async_prepared;
170mod async_result;
171mod async_transaction;
172mod async_transport;
173mod catalog;
174mod connection;
175mod connection_builder;
176pub mod copy;
177mod data_format;
178mod error;
179mod inserter;
180mod kv_store;
181mod names;
182mod params;
183pub mod pool;
184mod prepared;
185mod process;
186mod query_as;
187mod query_result;
188pub(crate) mod query_stats;
189mod result;
190mod row_accessor;
191mod server_version;
192mod table;
193mod table_copy;
194mod table_definition;
195mod transaction;
196mod transport;
197
198mod grpc_connection;
199#[cfg(kani)]
200mod proofs;
201
202pub use arrow_inserter::ArrowInserter;
203pub use arrow_reader::ArrowReader;
204pub use arrow_result::{
205    ArrowChunk, ArrowRow, ArrowRowset, ChunkSource, FromArrowValue, parse_arrow_ipc,
206};
207pub use async_arrow_inserter::{AsyncArrowInserter, AsyncArrowInserterOwned};
208pub use async_connection::AsyncConnection;
209pub use async_connection_builder::AsyncConnectionBuilder;
210pub use async_inserter::AsyncInserter;
211pub use async_kv_store::AsyncKvStore;
212pub use async_prepared::{AsyncPreparedStatement, AsyncPreparedStatementOwned};
213pub use async_result::AsyncRowset;
214pub use catalog::Catalog;
215pub use connection::{Connection, CreateMode, ScalarValue};
216pub use connection_builder::ConnectionBuilder;
217pub use error::{ColumnErrorKind, Error, Result};
218pub use params::{ParamFormat, ToSqlParam};
219pub use prepared::PreparedStatement;
220// Re-export Notice for callback registrants. `hyperdb-api-core`'s
221// `client::Error` is intentionally NOT re-exported — callers match
222// directly on the flat `Error` enum this crate defines.
223pub use async_transaction::AsyncTransaction;
224pub use hyperdb_api_core::client::{Notice, NoticeReceiver};
225pub use inserter::{ChunkSender, ColumnMapping, InsertChunk, Inserter, IntoValue, MappedInserter};
226pub use kv_store::{BatchGuardOutcome, BatchSetOutcome, KvStore, SetOutcome};
227pub use names::{
228    DatabaseName, Name, SchemaName, TableName, escape_name, escape_sql_path, escape_string_literal,
229};
230pub use process::{HyperProcess, ListenMode, Parameters, TransportMode};
231pub use query_stats::{LogFileStatsProvider, QueryStats, QueryStatsProvider};
232pub use result::{FromRow, ResultColumn, ResultSchema, Row, RowIterator, RowValue, Rowset};
233pub use row_accessor::RowAccessor;
234
235// NOTE: proc-macro re-exports (FromRow, Table, query_as!) are intentionally
236// absent from hyperdb-api. Re-exporting them creates a dependency cycle:
237//   hyperdb-api → hyperdb-api-derive → hyperdb-compile-check → hyperdb-api
238// Users add `hyperdb-api-derive` directly with the features they need.
239pub use query_as::{QueryAs, QueryScalar};
240pub use server_version::ServerVersion;
241pub use table::Table;
242pub use table_copy::{CopyTableReport, UnpreservedItem, UnpreservedReason};
243pub use table_definition::{ColumnDefinition, Persistence, TableConstraint, TableDefinition};
244pub use transaction::Transaction;
245
246// Re-export types from hyperdb-api-core's types layer.
247pub use hyperdb_api_core::types::{
248    Date, Geography, Interval, Nullability, Numeric, OffsetTimestamp, Oid, SqlType, Time,
249    Timestamp, Type,
250};
251
252/// Re-export of `GeoError` from hyperdb-api-core::types.
253pub use hyperdb_api_core::types::GeoError;
254
255/// Re-export of the PostgreSQL OID constants. Access as `hyperdb_api::oids::INT4` etc.
256pub use hyperdb_api_core::types::oids;
257
258// Re-export gRPC types (always available)
259pub mod grpc {
260    //! gRPC transport types for Hyper database access.
261    //!
262    //! This module provides two ways to use gRPC:
263    //!
264    //! 1. **Unified Connection** (recommended): Use `Connection::connect()` with an
265    //!    `https://` or `http://` URL - transport is auto-detected.
266    //!
267    //! 2. **Direct gRPC**: Use `GrpcConnection` or `GrpcConnectionAsync` for
268    //!    explicit gRPC access with full control over transfer modes and async.
269    //!
270    //! # Transfer Modes
271    //!
272    //! - `TransferMode::Sync` - All results in one response (simple, 100s timeout)
273    //! - `TransferMode::Async` - Header only, fetch results via `GetQueryResult`
274    //! - `TransferMode::Adaptive` - First chunk inline, rest streamed (default, recommended)
275
276    // Re-export connection types from grpc_connection module
277    pub use crate::grpc_connection::{GrpcConnection, GrpcConnectionAsync};
278
279    // Re-export types from hyperdb_api_core::client::grpc
280    pub use hyperdb_api_core::client::grpc::{
281        GrpcClient, GrpcClientSync, GrpcConfig, GrpcError, GrpcQueryResult, GrpcResultChunk,
282        TransferMode,
283    };
284}
285
286/// Macro for creating table definitions with a fluent syntax.
287///
288/// This macro simplifies the common pattern of creating table definitions
289/// with multiple columns by providing a more compact syntax.
290///
291/// # Syntax
292///
293/// ```text
294/// table! {
295///     "table_name" {
296///         "column_name": SqlType::type_name(), NULLABLE | NOT_NULL,
297///         // ... more columns
298///     }
299/// }
300/// ```
301///
302/// # Example
303///
304/// ```no_run
305/// # use hyperdb_api::{table, TableDefinition, SqlType, Result};
306/// # fn example() -> Result<()> {
307/// let orders = table! {
308///     "Orders" {
309///         "Address ID": SqlType::small_int(), NOT_NULL,
310///         "Customer ID": SqlType::text(), NOT_NULL,
311///         "Order Date": SqlType::date(), NOT_NULL,
312///         "Order ID": SqlType::text(), NOT_NULL,
313///         "Ship Date": SqlType::date(), NULLABLE,
314///         "Ship Mode": SqlType::text(), NULLABLE,
315///     }
316/// };
317///
318/// // Equivalent to:
319/// let orders_manual = TableDefinition::new("Orders")
320///     .add_required_column("Address ID", SqlType::small_int())
321///     .add_required_column("Customer ID", SqlType::text())
322///     .add_required_column("Order Date", SqlType::date())
323///     .add_required_column("Order ID", SqlType::text())
324///     .add_nullable_column("Ship Date", SqlType::date())
325///     .add_nullable_column("Ship Mode", SqlType::text());
326/// # Ok(())
327/// # }
328/// ```
329#[macro_export]
330macro_rules! table {
331    // Match table with schema.table syntax
332    ($schema:literal.$table:literal {
333        $($col_name:literal: $col_type:expr, $nullability:ident),* $(,)?
334    }) => {{
335        #[allow(unused_mut)]
336        let mut table_def = $crate::TableDefinition::new($table).with_schema($schema);
337        $(
338            table_def = table!(@add_column table_def, $col_name, $col_type, $nullability);
339        )*
340        table_def
341    }};
342
343    // Match simple table name
344    ($table:literal {
345        $($col_name:literal: $col_type:expr, $nullability:ident),* $(,)?
346    }) => {{
347        #[allow(unused_mut)]
348        let mut table_def = $crate::TableDefinition::new($table);
349        $(
350            table_def = table!(@add_column table_def, $col_name, $col_type, $nullability);
351        )*
352        table_def
353    }};
354
355    // Helper to add column based on nullability
356    (@add_column $table_def:expr, $col_name:literal, $col_type:expr, NULLABLE) => {
357        $table_def.add_nullable_column($col_name, $col_type)
358    };
359    (@add_column $table_def:expr, $col_name:literal, $col_type:expr, NOT_NULL) => {
360        $table_def.add_required_column($col_name, $col_type)
361    };
362}