Skip to main content

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)
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//! ### Single-process write semantics
74//!
75//! Every connection acquired from one [`SpgPool`] shares the
76//! same underlying [`spg_embedded_tokio::AsyncDatabase`] (one
77//! `Arc<Mutex<Database>>` behind a `tokio::sync::OnceCell` on
78//! [`SpgConnectOptions`]). That's how
79//! `let mut tx = pool.begin().await?;` and a separate
80//! `pool.acquire().await?` see the same committed state.
81//! The single-writer invariant of the underlying spg-engine is
82//! upheld by the `tokio::sync::Mutex` inside `AsyncDatabase`:
83//! every `execute`/`query` serialises against every other call
84//! on the same pool, and `tx.commit().await?` is what makes
85//! the in-tx writes visible to subsequent reads.
86//!
87//! ### Cross-process write semantics
88//!
89//! **Two coexisting processes opening the same `open_path(p)`
90//! are NOT serialised by SPG.** SPG-embedded is single-writer
91//! at the process level: each process gets its own
92//! `Arc<Mutex<Database>>`, and the WAL on disk is not
93//! flock-coordinated across them. If a second process opens
94//! the path while the first is running:
95//!
96//! - the second process replays the WAL as of its open
97//!   moment and sees a snapshot of state through the last
98//!   completed checkpoint + the WAL it read,
99//! - subsequent writes from the second process land in its
100//!   own in-memory catalog and its own WAL append,
101//! - whichever process flushes last wins for the catalog
102//!   snapshot on the next checkpoint, and the other process's
103//!   writes are silently lost on reopen.
104//!
105//! For an admin-tool + server use case (mailrs round-9 B.4
106//! question 1), the safe pattern is to STOP the server,
107//! run the admin tool, then START the server. The
108//! cross-process locking story (file lock, lease, advisory
109//! lock) is a v7.17+ ask; today the contract is
110//! "single-process owner per database file."
111//!
112//! ### WAL durability under crash
113//!
114//! [`spg_embedded::Database::execute`] fsyncs the WAL append
115//! before returning `Ok`. So at the moment a successful
116//! `execute()` returns, the write is durable across a process
117//! crash AND a host power loss. On reopen,
118//! [`spg_embedded::Database::open_path`] replays every
119//! WAL record produced since the last checkpoint — the
120//! `ZERO-CHANGE CUTOVER VERIFIED` gate (mailrs-spg-embedded
121//! validation harness) covers this end to end.
122//!
123//! What's NOT durable:
124//!
125//! - A `BEGIN`-but-not-yet-`COMMIT` transaction at crash time
126//!   rolls back on reopen — the in-tx WAL records aren't replayed.
127//!   This is the desired behaviour: SPG's transaction model is
128//!   single-writer with explicit COMMIT.
129//! - The catalog snapshot file (the periodic checkpoint output)
130//!   is rewritten atomically via temp-file + rename; a crash
131//!   during checkpoint leaves the previous snapshot intact.
132//!
133//! The checkpoint threshold defaults to 4 MiB of WAL growth and
134//! is configurable via
135//! [`spg_embedded::Database::set_checkpoint_threshold_bytes`].
136//! Lower thresholds make recovery faster (less WAL to replay)
137//! at the cost of more frequent IO; higher thresholds amortise
138//! IO but extend recovery time.
139
140mod arguments;
141mod column;
142mod connection;
143mod database;
144mod error;
145mod options;
146mod pool;
147mod query_result;
148mod row;
149mod statement;
150mod transaction;
151mod type_info;
152mod types;
153mod value;
154
155pub use crate::arguments::{SpgArgumentValue, SpgArguments};
156pub use crate::column::SpgColumn;
157pub use crate::connection::SpgConnection;
158pub use crate::database::Spg;
159pub use crate::options::SpgConnectOptions;
160pub use crate::pool::{SpgPool, SpgPoolExt, SpgPoolOptions};
161pub use crate::query_result::SpgQueryResult;
162pub use crate::row::SpgRow;
163pub use crate::statement::SpgStatement;
164pub use crate::transaction::SpgTransactionManager;
165pub use crate::type_info::SpgTypeInfo;
166pub use crate::value::{SpgValue, SpgValueRef};
167
168// Re-export the embedded engine's owned-value type so consumers
169// don't have to depend on spg-embedded directly to construct or
170// pattern-match values returned from the adapter.
171pub use spg_embedded::Value as EngineValue;