spg_sqlx/lib.rs
1// v7.16.0 — every public item carries a doc-comment.
2#![deny(missing_docs)]
3
4//! # spg-sqlx
5//!
6//! sqlx 0.8 Database driver for [`spg-embedded`]. Lets
7//! in-process callers swap `sqlx::PgPool` for `SpgPool` and keep
8//! the rest of their `sqlx::query` / `sqlx::query_as` /
9//! `pool.begin` cement unchanged — backs mailrs's drop-in
10//! "PgPool → SpgPool" goal from the gap evaluation (E1).
11//!
12//! ## v7.16.0 MVP scope
13//!
14//! - [`Spg`] marker type + the 11 associated types `sqlx::Database`
15//! requires, all wired up to compile.
16//! - [`SpgPool`] / [`SpgConnection`] wrap [`spg_embedded_tokio::AsyncDatabase`]
17//! so a single in-process database is the "pool". No real
18//! pooling — every "connection" handle is a cheap clone of
19//! the underlying `Arc<Mutex<Database>>`.
20//! - Bind-time [`Value`][SpgValue] encoding for the basic scalar
21//! surface: `i32`, `i64`, `bool`, `String`, `Vec<u8>`. Round-trip
22//! verified end-to-end against `sqlx::query("INSERT …").bind(…)`
23//! in the test suite.
24//! - Transactions via the engine's BEGIN/COMMIT/ROLLBACK; the
25//! [`SpgTransactionManager`] wraps that for `pool.begin()`.
26//!
27//! ## v7.16.x / v7.17 follow-up
28//!
29//! - Encode/Decode for the remaining mailrs-side types:
30//! TIMESTAMPTZ (`chrono::DateTime<Utc>`), JSON / JSONB
31//! (`serde_json::Value`), `tsvector`, `VECTOR(N)`,
32//! `INT[]` / `TEXT[]`, `BYTEA` (Vec<u8> beyond the basic path),
33//! numeric.
34//! - `FromRow` derive support — the macro's generated impl reads
35//! columns by index/name via the [`Row`][sqlx_core::row::Row]
36//! trait, so wiring `SpgRow::try_get` is enough for the derive
37//! to "just work" once the per-type Decode lands.
38//! - `sqlx::query!()` compile-time validation via sqlx's offline
39//! mode (`SQLX_OFFLINE=true` + a checked-in `.sqlx/` dir). The
40//! adapter itself doesn't need a DESCRIBE protocol —
41//! `Spg`-shaped offline cache mirrors what mailrs ships
42//! against PG today.
43//!
44//! ## Quick start
45//!
46//! ```no_run
47//! use spg_sqlx::{SpgPool, SpgPoolExt};
48//!
49//! # async fn _f() -> Result<(), Box<dyn std::error::Error>> {
50//! let pool = SpgPool::connect_in_memory().await?;
51//! sqlx::query("CREATE TABLE users (id INT NOT NULL, name TEXT NOT NULL)")
52//! .execute(&pool)
53//! .await?;
54//! sqlx::query("INSERT INTO users VALUES ($1, $2)")
55//! .bind(1_i32)
56//! .bind("alice")
57//! .execute(&pool)
58//! .await?;
59//! # Ok(())
60//! # }
61//! ```
62//!
63//! ## Concurrency, durability, and `Send + Sync` (mailrs round-9 B.4 + v7.18)
64//!
65//! ### `SpgPool: Send + Sync + 'static`
66//!
67//! [`SpgPool`] is `Pool<Spg>` from sqlx-core, which is
68//! `Send + Sync + 'static` by construction. Holding it inside
69//! `Arc<WebState>` for sharing across Axum/Tower handlers,
70//! background workers, and long-lived spawn tasks works the
71//! same as `sqlx::PgPool`. Clones are cheap (Arc bumps).
72//!
73//! ### Pool semantics (v7.18 — drop-in PG-shape)
74//!
75//! `SpgPoolOptions::new().max_connections(N).connect_with(...)`
76//! behaves like its `PgPool` analogue:
77//!
78//! - **Concurrent SELECTs scale**. Every [`SpgConnection`]
79//! lazily attaches an `AsyncReadHandle` on first read-only
80//! statement outside a transaction, then refreshes its
81//! snapshot per statement so each SELECT sees the latest
82//! committed state (PG read-committed default). N pool
83//! connections → N concurrent reads, no writer-lock
84//! serialisation.
85//! - **Writes serialise**. INSERT / UPDATE / DELETE / DDL take
86//! the writer lock — the engine is single-writer at the
87//! storage level and that invariant stays.
88//! - **Transactions stay on the writer path**. Everything
89//! between `BEGIN` and `COMMIT/ROLLBACK` routes to the writer
90//! even for the same-TX SELECTs — that's how the user's
91//! uncommitted writes become visible to subsequent same-TX
92//! reads (the snapshot path would not see them).
93//! - **Cross-connection read-committed**. After one connection
94//! commits, another connection's next SELECT picks up the new
95//! state via its per-statement snapshot refresh — same
96//! visibility window PG users expect.
97//! - **`SpgConnectOptions` shares the underlying engine across
98//! every `connect()`**: one `Arc<RwLock<Database>>` behind a
99//! `tokio::sync::OnceCell` on the options. That's how
100//! `let mut tx = pool.begin().await?;` and a separate
101//! `pool.acquire().await?` reach the same in-process state.
102//!
103//! ### Escape hatch — `read_handle` for SPG-aware code
104//!
105//! For code paths that want to bypass sqlx entirely and hold
106//! an explicit snapshot lifetime (e.g. an IMAP fetch that
107//! shouldn't see writes mid-stream), reach for
108//! [`spg_embedded_tokio::AsyncReadHandle`] directly via
109//! [`SpgConnection::engine()`]. Stock sqlx users do not need
110//! this — the routing inside [`SpgConnection`] already fans
111//! out reads through that exact path under the hood.
112//!
113//! ### Cross-process write semantics
114//!
115//! **Two coexisting processes opening the same `open_path(p)`
116//! are NOT serialised by SPG.** SPG-embedded is single-writer
117//! at the process level: each process gets its own
118//! `Arc<Mutex<Database>>`, and the WAL on disk is not
119//! flock-coordinated across them. If a second process opens
120//! the path while the first is running:
121//!
122//! - the second process replays the WAL as of its open
123//! moment and sees a snapshot of state through the last
124//! completed checkpoint + the WAL it read,
125//! - subsequent writes from the second process land in its
126//! own in-memory catalog and its own WAL append,
127//! - whichever process flushes last wins for the catalog
128//! snapshot on the next checkpoint, and the other process's
129//! writes are silently lost on reopen.
130//!
131//! For an admin-tool + server use case (mailrs round-9 B.4
132//! question 1), the safe pattern is to STOP the server,
133//! run the admin tool, then START the server. The
134//! cross-process locking story (file lock, lease, advisory
135//! lock) is a v7.17+ ask; today the contract is
136//! "single-process owner per database file."
137//!
138//! ### WAL durability under crash
139//!
140//! [`spg_embedded::Database::execute`] fsyncs the WAL append
141//! before returning `Ok`. So at the moment a successful
142//! `execute()` returns, the write is durable across a process
143//! crash AND a host power loss. On reopen,
144//! [`spg_embedded::Database::open_path`] replays every
145//! WAL record produced since the last checkpoint — the
146//! `ZERO-CHANGE CUTOVER VERIFIED` gate (mailrs-spg-embedded
147//! validation harness) covers this end to end.
148//!
149//! What's NOT durable:
150//!
151//! - A `BEGIN`-but-not-yet-`COMMIT` transaction at crash time
152//! rolls back on reopen — the in-tx WAL records aren't replayed.
153//! This is the desired behaviour: SPG's transaction model is
154//! single-writer with explicit COMMIT.
155//! - The catalog snapshot file (the periodic checkpoint output)
156//! is rewritten atomically via temp-file + rename; a crash
157//! during checkpoint leaves the previous snapshot intact.
158//!
159//! The checkpoint threshold defaults to 4 MiB of WAL growth and
160//! is configurable via
161//! [`spg_embedded::Database::set_checkpoint_threshold_bytes`].
162//! Lower thresholds make recovery faster (less WAL to replay)
163//! at the cost of more frequent IO; higher thresholds amortise
164//! IO but extend recovery time.
165
166mod arguments;
167mod column;
168mod connection;
169mod database;
170mod error;
171mod options;
172mod pool;
173mod query_result;
174mod row;
175mod statement;
176mod transaction;
177mod type_info;
178mod types;
179mod value;
180
181pub use crate::arguments::{SpgArgumentValue, SpgArguments};
182pub use crate::column::SpgColumn;
183pub use crate::connection::SpgConnection;
184pub use crate::database::Spg;
185pub use crate::options::SpgConnectOptions;
186pub use crate::pool::{SpgPool, SpgPoolExt, SpgPoolOptions};
187pub use crate::query_result::SpgQueryResult;
188pub use crate::row::SpgRow;
189pub use crate::statement::SpgStatement;
190pub use crate::transaction::SpgTransactionManager;
191pub use crate::type_info::{Kind, SpgTypeInfo};
192pub use crate::value::{SpgValue, SpgValueRef};
193
194// Re-export the embedded engine's owned-value type so consumers
195// don't have to depend on spg-embedded directly to construct or
196// pattern-match values returned from the adapter.
197pub use spg_embedded::Value as EngineValue;