Skip to main content

cratefield_adapter_postgres/
lib.rs

1//! `cratefield-adapter-postgres`: the [`Database`] port over `sqlx`
2//! Postgres for the native runtime (ADR 0004, issue #18). Native only —
3//! it must never enter a wasm build.
4//!
5//! # Native-only gate
6//!
7//! The sqlx driver does not compile to wasm, so this crate is impossible
8//! to build for a wasm target: `src/lib.rs` fails compilation with a
9//! clear message on `wasm32`/`wasm64`, and the sqlx dependency itself is
10//! target-gated to non-wasm builds in `Cargo.toml`. The wasm graph of
11//! `examples/venture` must stay free of this crate (CI asserts it with
12//! `cargo tree --target wasm32-unknown-unknown -p venture | grep -c
13//! sqlx` → 0).
14//!
15//! # Statement pipeline
16//!
17//! Modules build queries with sea-query; the port's [`Statement`] carries
18//! them rendered with `SqliteQueryBuilder` (the portable `?`-placeholder
19//! wire form D1 and rusqlite execute directly). This adapter rewrites `?`
20//! to Postgres `$n` placeholders — skipping string literals, quoted
21//! identifiers and comments — and binds the sea-query values positionally
22//! (`src/convert.rs`). The migration runner's own
23//! bookkeeping statements are rendered by sea-query's
24//! `PostgresQueryBuilder` directly.
25//!
26//! # The SQLite → Postgres mapping actually used
27//!
28//! The portable subset (ADR 0004, architecture section 7) is deliberately
29//! the intersection of SQLite and Postgres, so migration files run
30//! verbatim on both engines:
31//!
32//! | Portable concept | SQLite | Postgres | Notes |
33//! |---|---|---|---|
34//! | ULID ids | `TEXT PRIMARY KEY` | `TEXT PRIMARY KEY` | no `AUTOINCREMENT`/`SERIAL` anywhere |
35//! | timestamps | ISO-8601 `TEXT` | ISO-8601 `TEXT` | computed in Rust, never `NOW()`/`datetime()` |
36//! | counters | `INTEGER` | `INTEGER` (`int4`) | `referrals`, `position` |
37//! | booleans | `INTEGER 0/1` by convention | `INTEGER 0/1` by convention | no SQL `BOOLEAN` columns; `SeaValue::Bool` binds as `SMALLINT` 0/1, and rows read back through `Row::get::<bool>` accept both |
38//! | upserts | `INSERT … ON CONFLICT …` | identical syntax | rendered by sea-query for both dialects |
39//! | read-back writes | `INSERT … RETURNING …` | identical syntax | rendered by sea-query for both dialects |
40//!
41//! `batch` runs all statements in one transaction (atomic, ADR 0004).
42//!
43//! ```no_run
44//! # async fn demo() -> Result<(), cratefield_core::DbError> {
45//! use cratefield_adapter_postgres::Postgres;
46//!
47//! let db = Postgres::connect("postgres://user:pass@host:5432/venture").await?;
48//! // db.apply_harness_migrations(&harness).await?;   // library entry point
49//! // … or through Arc<dyn Database> as with any adapter
50//! # Ok(())
51//! # }
52//! ```
53
54#![forbid(unsafe_code)]
55
56// The whole crate is native-only (issue #18): fail a wasm build of this
57// crate fast, with a clear message, before sqlx is even attempted.
58#[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))]
59compile_error!(
60    "cratefield-adapter-postgres is native-only: the sqlx Postgres driver does not \
61     compile to wasm (issue #18, ADR 0004). A wasm target must not depend on this \
62     crate — check the wasm dependency graph of the venture (modules see ports, \
63     never adapters, and runtime-native is the only consumer)."
64);
65
66mod convert;
67mod migrate;
68pub mod testing;
69
70pub use migrate::select_set;
71
72use async_trait::async_trait;
73use cratefield_core::{Database, DbError, Rows, Statement};
74use sqlx::postgres::PgArguments;
75
76/// An open Postgres connection pool (`sqlx` 0.8, `runtime-tokio`,
77/// `tls-rustls`).
78///
79/// One pool per tenant database: ADR 0008 keeps tenants isolated at the
80/// database boundary, and the native runtime (#19) holds one adapter per
81/// tenant.
82pub struct Postgres {
83    pool: sqlx::PgPool,
84}
85
86impl Postgres {
87    /// Opens a pool and establishes at least one connection, failing fast
88    /// on an unreachable server or bad credentials.
89    ///
90    /// # Errors
91    ///
92    /// [`DbError::Execute`] when the pool cannot connect.
93    pub async fn connect(url: &str) -> Result<Self, DbError> {
94        let pool = sqlx::PgPool::connect(url)
95            .await
96            .map_err(|err| DbError::Execute(err.to_string()))?;
97        Ok(Self { pool })
98    }
99
100    /// Prepares a port statement for execution: rewrites `?` placeholders
101    /// to `$n` and binds the sea-query values.
102    fn prepare(stmt: &Statement) -> Result<(String, PgArguments), DbError> {
103        let sql = convert::rebind_placeholders(&stmt.sql);
104        let mut args = PgArguments::default();
105        convert::bind_values(&mut args, &stmt.values.0)?;
106        Ok((sql, args))
107    }
108
109    /// Closes the pool: waits for in-flight queries, then closes every
110    /// connection. Dropping the adapter eventually closes the pool too;
111    /// `close` makes shutdown deterministic for the parity kit and the
112    /// native runtime.
113    ///
114    /// # Errors
115    ///
116    /// [`DbError::Execute`] when the pool cannot be closed.
117    pub async fn close(&self) -> Result<(), DbError> {
118        self.pool.close().await;
119        Ok(())
120    }
121}
122
123#[async_trait]
124impl Database for Postgres {
125    async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
126        let (sql, args) = Self::prepare(stmt)?;
127        let result = sqlx::query_with(sql.as_str(), args)
128            .execute(&self.pool)
129            .await
130            .map_err(|err| DbError::Execute(err.to_string()))?;
131        Ok(result.rows_affected())
132    }
133
134    async fn query(&self, stmt: &Statement) -> Result<Rows, DbError> {
135        let (sql, args) = Self::prepare(stmt)?;
136        let rows = sqlx::query_with(sql.as_str(), args)
137            .fetch_all(&self.pool)
138            .await
139            .map_err(|err| DbError::Query(err.to_string()))?;
140        Ok(Rows::new(rows.iter().map(convert::pg_row_to_row).collect()))
141    }
142
143    /// Runs all statements in one transaction (atomic on Postgres).
144    async fn batch(&self, stmts: &[Statement]) -> Result<(), DbError> {
145        let mut tx = self
146            .pool
147            .begin()
148            .await
149            .map_err(|err| DbError::Batch(err.to_string()))?;
150        for stmt in stmts {
151            let (sql, args) = Self::prepare(stmt)?;
152            sqlx::query_with(sql.as_str(), args)
153                .execute(&mut *tx)
154                .await
155                .map_err(|err| DbError::Batch(err.to_string()))?;
156        }
157        tx.commit()
158            .await
159            .map_err(|err| DbError::Batch(err.to_string()))?;
160        Ok(())
161    }
162}