zeph-db
Database abstraction layer for Zeph — unified SQLite and PostgreSQL backends with compile-time backend selection, automatic migrations, dialect-aware SQL helpers, and FTS support.
Important:
Exactly one of the
sqliteorpostgresfeatures must be enabled. The default issqlite, so plaincargo build/cargo build --features full(no--no-default-features) always produce a working sqlite build. Enabling both simultaneously triggers acompile_error!. Using--all-featuresis intentionally unsupported. For a PostgreSQL build, disable default features explicitly —cargo build --no-default-features --features full,postgres— since the default sqlite backend cannot be "overridden" by additively requesting postgres on top of it.
Features
- Compile-time backend selection —
DbPool,DbRow,DbTransaction, andDbQueryResultresolve to the correct sqlx types based on the active feature sql!macro — write?placeholders once; the macro rewrites them to$1, $2, ...for PostgreSQL and is a zero-cost no-op for SQLiteDialecttrait — backend-specific SQL constants (AUTO_PK,INSERT_IGNORE,EPOCH_NOW, etc.) and helpers (ilike,epoch_from_col) via zero-sized marker types- Automatic migrations —
DbConfig::connectrunsmigrations/sqlite/ormigrations/postgres/on startup; WAL checkpoint applied after SQLite migrations FullDriversuper-trait — reduces sqlx bound repetition in generic impl blocks across consumer crates- FTS helpers — backend-aware
WHERE/JOIN/rank fragments for messages and graph entity full-text search limit_clause()helper — cross-backend "0means unlimited"LIMITfragment; omits the clause entirely instead of relying on the SQLite-onlyLIMIT -1sentinel, which PostgreSQL rejects- Safe URL logging —
redact_urlstrips credentials from connection strings before they appear in logs - Write transactions —
begin_writeissuesBEGIN IMMEDIATEon SQLite (preventsSQLITE_BUSY); falls back to standardBEGINon PostgreSQL
Connection URL configuration
The active backend is chosen at compile time by feature flag; the connection URL is resolved at runtime. Set database_url under the [memory] section of config.toml:
[]
= "postgres://user:pass@localhost/zeph"
Because the URL usually embeds credentials, it is also resolvable from the age vault, which takes precedence over the config file:
Important:
The URL scheme must match the compiled feature. A
postgres://URL on asqlitebuild (or a non-postgres URL on apostgresbuild) fails at startup with an explicit error; the URL is redacted before it reaches the message.
CLI migrations
Run pending migrations without starting the agent:
Installation
For a PostgreSQL build, the default sqlite feature must be disabled explicitly — the two backends are mutually exclusive, so postgres cannot be requested additively on top of the default:
[]
= { = "0.22", = false, = ["postgres"] }
Feature Flags
| Feature | Description |
|---|---|
sqlite (default) |
Enables SQLite backend via sqlx/sqlite |
postgres |
Enables PostgreSQL backend via sqlx/postgres |
test-utils |
Enables testcontainers + testcontainers-modules for PostgreSQL integration tests; implies postgres |
Usage
Connect and run migrations
use ;
let config = DbConfig ;
let pool: DbPool = config.connect.await?;
For in-memory SQLite (useful in tests):
let pool = DbConfig
.connect
.await?;
Write portable SQL with the sql! macro
use sql;
let rows = query
.bind
.fetch_all
.await?;
Note:
Do not use the
sql!macro for PostgreSQL JSONB queries that contain?,?|, or?&operators — use$Nplaceholders directly for those.
Dialect-aware SQL fragments
use ;
let ddl = format!;
let insert = format!;
Transactions
use ;
// Standard deferred transaction
let mut tx = begin.await?;
// Write-intent transaction (BEGIN IMMEDIATE on SQLite)
let mut tx = begin_write.await?;
query.bind.execute.await?;
tx.commit.await?;
Cross-backend LIMIT clause
use limit_clause;
let = limit_clause; // 0 => unlimited, omits the clause entirely
let sql = format!;
let mut query = query.bind;
if let Some = bind
FTS helpers
use ;
let q = sanitize_fts_query;
let sql = format!;
Generic consumer crates
Use D: DatabaseDriver + FullDriver as the single generic bound when you need both sqlx pool access and SQL dialect fragments:
use ;
async
Migrations
SQL migration files live in:
migrations/sqlite/— SQLite DDL (FTS5 virtual tables, triggers, indexes)migrations/postgres/— PostgreSQL DDL (tsvector columns, GIN indexes,plainto_tsquerysetup)
Migrations run automatically on first DbConfig::connect call. The active backend's directory is embedded at compile time via sqlx::migrate!.
MSRV
Rust 1.97 (Edition 2024, resolver 3).
License
Licensed under either of MIT or Apache License, Version 2.0 at your option.