<p align="center">
<a href="https://github.com/Cratefield/harness">
<img src="https://raw.githubusercontent.com/Cratefield/harness/main/assets/banners/cratefield-adapter-postgres.png" alt="cratefield-adapter-postgres — Database, at size." width="100%">
</a>
</p>
<p align="center">
<a href="https://crates.io/crates/cratefield-adapter-postgres"><img src="https://img.shields.io/crates/v/cratefield-adapter-postgres.svg?style=flat-square&labelColor=0A0A0B&color=4C6FFF" alt="cratefield-adapter-postgres on crates.io"></a>
<a href="https://docs.rs/cratefield-adapter-postgres"><img src="https://img.shields.io/docsrs/cratefield-adapter-postgres?style=flat-square&labelColor=0A0A0B&color=EDEBE6" alt="cratefield-adapter-postgres documentation"></a>
<a href="https://github.com/Cratefield/harness/blob/main/LICENSE"><img src="https://img.shields.io/badge/LICENSE-MIT-4C6FFF?style=flat-square&labelColor=0A0A0B" alt="MIT"></a>
</p>
# cratefield-adapter-postgres
The [`Database`] port over `sqlx` Postgres 16 for the [Cratefield
harness](https://github.com/Cratefield/harness) native runtime (ADR 0004,
issue #18). **Native only** — the crate fails compilation on any wasm
target with a clear message, and the sqlx dependency is target-gated to
non-wasm builds, so it can never slip into a Worker.
```rust
use cratefield_adapter_postgres::Postgres;
let db = Postgres::connect("postgres://user:pass@host:5432/venture").await?;
db.apply_harness_migrations(&harness).await?; // see below
// … through Arc<dyn Database> as with any adapter
```
- `Postgres::connect(url)` opens a pool (`sqlx` 0.8, `runtime-tokio`,
`tls-rustls`); `batch` runs all statements in one transaction (atomic).
- Port statements arrive in the portable `?`-placeholder form rendered by
sea-query's `SqliteQueryBuilder`; the adapter rewrites them to `$n`
(skipping string literals, quoted identifiers and comments) and binds
the values positionally. The migration runner's own bookkeeping is
rendered by sea-query's `PostgresQueryBuilder`.
- `apply_harness_migrations(&Harness)` applies every module's migrations
in lock order (module config order, then zero-padded migration id —
the order `fz migrations collect` pins). Per module it applies the
`postgres` migration set when the module ships one, else the `sqlite`
set when it passes the portable-SQL lint (`cratefield-core`'s
`lint_portable_sql`, the same predicate `fz doctor` enforces).
- `apply_migrations(module, &[SqlMigration])` is the per-module entry
point: idempotent, each migration applied in its own transaction and
tracked under `<module>/<id>` in `harness_migrations(id, applied_at)`.
- CLI: `fz migrations apply --dialect postgres --url …` (the `fz` binary
must be built with cratefield-cli's `postgres` feature).
## The SQLite → Postgres mapping actually used
The portable subset is the intersection of both engines, so the same
migration files run verbatim:
| ULID ids | `TEXT PRIMARY KEY` | `TEXT PRIMARY KEY` | no `AUTOINCREMENT`/`SERIAL` |
| timestamps | ISO-8601 `TEXT` | ISO-8601 `TEXT` | computed in Rust, never `NOW()`/`datetime()` |
| counters | `INTEGER` | `INTEGER` (`int4`) | |
| booleans | `INTEGER 0/1` convention | `INTEGER 0/1` convention | no SQL `BOOLEAN` columns; `SeaValue::Bool` binds as `SMALLINT` 0/1 |
| upserts | `INSERT … ON CONFLICT …` | identical syntax | sea-query renders both |
| read-back writes | `INSERT … RETURNING …` | identical syntax | sea-query renders both |
## Tests
Tests that need a server are gated on `FZ_TEST_POSTGRES_URL` and skipped
with a printed reason when it is unset (CI provides a `postgres:16`
service container). Locally:
```sh
docker run --rm -e POSTGRES_PASSWORD=postgres -p 5433:5432 postgres:16
export FZ_TEST_POSTGRES_URL=postgres://postgres:postgres@127.0.0.1:5433/postgres
cargo test -p cratefield-adapter-postgres
```
---
MIT. Built in the open for [Cratefield](https://cratefield.com), a [Factory Zero](https://factory0.ventures) venture.