Skip to main content

ferrule_sql/
lib.rs

1//! `ferrule-sql` — the embeddable, synchronous, bounded-memory SQL core.
2//!
3//! This crate owns the unified neutral [`Value`]/[`Row`] types, the
4//! [`DatabaseUrl`] parser, the [`Connection`] trait and its per-backend
5//! drivers, the connect dispatcher (direct, HTTP-proxy, and SSH-tunnel
6//! transports), the transaction helpers, and the cross-backend copy /
7//! bulk-load write path. It carries no rendering (`tabled`) or
8//! credential-resolution (`ferrule-config`) dependency, so it can be
9//! embedded by callers that supply already-resolved connection details.
10//!
11//! Backends are feature-gated (`postgres`, `mysql`, `mssql`, `sqlite`,
12//! `oracle`); the SSH tunnel transport is behind `ssh`. The `default`
13//! feature set is empty — enable the backends you need.
14//!
15//! # Why these properties
16//!
17//! - **Synchronous public API.** No `async fn` / `Future` crosses the
18//!   crate boundary, so a host with no runtime of its own can call
19//!   straight through. The async network drivers are still used — each
20//!   [`Connection`] owns a private current-thread `tokio` runtime and
21//!   blocks on it — but that runtime is an implementation detail. (SQLite
22//!   and Oracle are natively synchronous and call through directly.)
23//! - **Bounded memory.** [`Connection::query_cursor`] streams from a
24//!   native database cursor one back-pressured batch at a time, and
25//!   [`write_rows`] flushes an arbitrarily large row iterator in
26//!   fixed-size batches — both stay `O(batch)` regardless of total row
27//!   count. The eager [`Connection::query`] still materializes a full
28//!   `Vec<Row>`, but it is capped by per-cell / per-row / per-result
29//!   [`SizeGuards`] so a pathological result fails fast instead of OOMing.
30//! - **Caller-resolved credentials.** [`connect`] takes the password as a
31//!   [`secrecy::SecretString`] on [`ConnectOptions`]; the crate performs
32//!   no credential resolution and depends on no keyring / prompt library.
33//!
34//! # Embedding flow
35//!
36//! The three steps a host follows — **connect → streaming read → batched
37//! write** — line up with the runnable
38//! [`examples/embed.rs`](https://github.com/rustpunk/ferrule/blob/main/ferrule-sql/examples/embed.rs)
39//! (`cargo run -p ferrule-sql --example embed --features sqlite`):
40//!
41//! ### 1. Connect with a resolved secret
42//!
43//! Parse a [`DatabaseUrl`], hand the host-resolved credential to
44//! [`ConnectOptions::password`], and call [`connect`]. The returned
45//! [`Connection`] owns its private runtime and blocks on every call.
46//!
47//! ### 2. Streaming read (the bounded cursor)
48//!
49//! [`Connection::query_cursor`] returns a [`RowCursor`]. Pull rows with
50//! [`RowCursor::next_batch(n)`](RowCursor::next_batch) (a bounded chunk)
51//! or by iterating; either way the driver only fetches more from the
52//! server as you consume, so peak memory is `O(batch)`, never the whole
53//! result. The cursor borrows the connection for its lifetime, so scope
54//! it before issuing the next statement.
55//!
56//! ### 3. Batched write (back-pressured, structured report)
57//!
58//! [`write_rows`] consumes any `IntoIterator<Item = Row>` and flushes it
59//! in fixed-size batches ([`WriteOptions::batch_size`]), buffering one
60//! batch at a time. It reuses the cross-DB copy path's SQL generation and
61//! transaction control and returns a [`WriteReport`] naming exactly which
62//! batches / rows landed and which were rejected. Pair it with the
63//! cursor from step 2 for an end-to-end bounded-memory pipe.
64//!
65//! ```no_run
66//! # #[cfg(feature = "sqlite")]
67//! # fn demo() -> Result<(), Box<dyn std::error::Error>> {
68//! use ferrule_sql::{
69//!     connect, write_rows, Backend, ColumnInfo, ConnectOptions, DatabaseUrl,
70//!     Row, TypeHint, Value, WriteOptions,
71//! };
72//! use secrecy::SecretString;
73//!
74//! // 1. Connect. The password is a caller-resolved `SecretString`;
75//! //    SQLite ignores it, a networked backend would consume it.
76//! let url = DatabaseUrl::parse("sqlite:///tmp/embed-demo.db")?;
77//! let opts = ConnectOptions {
78//!     insecure: false,
79//!     password: Some(SecretString::from("resolved-by-the-host")),
80//! };
81//! let mut conn = connect(&url, &opts, None)?;
82//! conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")?;
83//!
84//! // 2. Streaming read at bounded memory: two rows per pull.
85//! {
86//!     let mut cursor = conn.query_cursor("SELECT id, name FROM t ORDER BY id")?;
87//!     while !cursor.next_batch(2)?.is_empty() {
88//!         // process this bounded batch, then pull the next one
89//!     }
90//! }
91//!
92//! // 3. Batched write: one batch buffered at a time.
93//! let columns = [
94//!     ColumnInfo { name: "id".into(), type_hint: TypeHint::Int64, nullable: false },
95//!     ColumnInfo { name: "name".into(), type_hint: TypeHint::String, nullable: true },
96//! ];
97//! let rows: Vec<Row> = (1..=1000)
98//!     .map(|i| vec![Value::Int64(i), Value::String(format!("n{i}"))])
99//!     .collect();
100//! let report = write_rows(
101//!     &mut *conn,
102//!     Backend::Sqlite,
103//!     "t",
104//!     &columns,
105//!     rows,
106//!     &WriteOptions { batch_size: 200, ..Default::default() },
107//! )?;
108//! assert!(report.is_complete());
109//! # Ok(())
110//! # }
111//! ```
112//!
113//! # Reentrancy
114//!
115//! The private runtime is current-thread, so a [`Connection`] (and its
116//! [`RowCursor`]) must not be driven from inside another `block_on` on the
117//! same thread. An async host hops to a blocking thread
118//! (`tokio::task::spawn_blocking` or a dedicated OS thread) first.
119
120#![allow(dead_code, unused_variables, unused_imports)]
121
122pub mod backend;
123pub mod connection;
124pub mod copy;
125pub mod dialect;
126pub mod error;
127pub mod guard;
128pub mod proxy;
129pub mod query_builder;
130pub mod render;
131pub mod stream;
132pub mod sync;
133pub mod transaction;
134pub mod tunnel;
135pub mod url;
136pub mod value;
137pub mod write;
138
139/// Per-backend driver modules, one feature-gated submodule per backend.
140///
141/// The module is `pub` so the per-backend concrete connection types and
142/// their inline integration tests are reachable, but the connection
143/// *constructors* are `pub(crate)`: every caller establishes connections
144/// through the synchronous URL-scheme dispatcher [`connect`], which is
145/// the only blocking entry point and the one that owns the private
146/// runtime. Embedders never touch a driver's async constructor directly.
147pub mod backends;
148
149#[cfg(feature = "ssh")]
150pub use backend::connect_with_tunnel;
151pub use backend::{Backend, connect};
152pub use connection::{
153    BulkInsert, ConnectOptions, Connection, ExecutionSummary, ForeignKey, QueryResult, SchemaInfo,
154    StatementResult,
155};
156pub use copy::{
157    AllTablesOptions, BulkMode, CopyFormat, CopyOptions, CopySource, CycleError, IfExists,
158    copy_all_tables, copy_rows, discover_tables, quote_identifier, topo_sort, translate_ddl,
159    translate_type,
160};
161pub use dialect::Dialect;
162pub use error::SqlError;
163pub use guard::SizeGuards;
164pub use proxy::{ProxiedConnection, ProxyConfig, is_no_proxy, resolve_proxy_from_env};
165pub use query_builder::apply_paging;
166pub use render::{quote_string, render_value};
167pub use stream::{BoxRowStream, DEFAULT_CURSOR_CAPACITY, RowCursor};
168pub use sync::SyncConnection;
169pub use transaction::{begin_transaction, commit_transaction, rollback_transaction};
170pub use tunnel::SshConfig;
171#[cfg(feature = "ssh")]
172pub use tunnel::{
173    KeySource, SshSession, TunnelError, TunnelHandle, TunnelStream, TunnelTransport,
174    TunnelTransportResult, TunneledConnection,
175};
176pub use url::DatabaseUrl;
177pub use value::{ColumnInfo, Row, TypeHint, Value};
178pub use write::{
179    BatchOutcome, DEFAULT_WRITE_BATCH, RejectedBatch, RejectedRow, WriteMode, WriteOptions,
180    WriteReport, write_rows,
181};