PostgreSQL
Point an umbral app at Postgres - connection URL, migrations, queries, and the Postgres-only field types.
PostgreSQL is umbral's first-class production backend. The plain Article::objects().fetch() you write against SQLite runs unchanged against Postgres; the migration engine emits Postgres-flavoured DDL; and a set of Postgres-only field types (arrays, INET/CIDR/MACADDR, JSONB operators, full-text search) opens up the engine where the workload calls for it.
Configure
Point DATABASE_URL (env or umbral.toml) at a postgres:// (or postgresql://) URL. umbral::db::connect dispatches on the URL scheme and returns the matching [DbPool] variant.
use umbral::prelude::*; #[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let settings = Settings::from_env()?; let pool = umbral::db::connect(&settings.database_url).await?; let app = App::builder() .settings(settings) .database("default", pool) .plugin(blog::BlogPlugin) .build()?; app.serve("127.0.0.1:8000".parse::<std::net::SocketAddr>()?).await?; Ok(())}App::build() cross-checks settings.database_url against the pool's runtime backend. A mismatch (URL says postgres:// but the pool is DbPool::Sqlite, or vice versa) is a hard BuildError::DatabaseBackendMismatch at boot, not a silent failure on the first query.
Pooling. connect returns a sqlx PgPool wrapped in the DbPool enum, with umbral's pool configuration already applied - max_connections, min_connections, a bounded acquire_timeout (so a saturated pool fails fast instead of blocking forever), idle_timeout, max_lifetime, and test_before_acquire. Tune all of them from settings (UMBRAL_DB_* env vars or umbral.toml) - see Connection pooling for the full knob list and defaults. The pool is process-wide, shared between every handler, and emits one boot-log line with its effective configuration.
Migrations
cargo run -- makemigrations # writes a JSON migration under migrations/<plugin>/cargo run -- migrate # applies pending migrations against $DATABASE_URLThe engine renders Postgres DDL automatically:
BIGSERIALfori64primary keys (vsINTEGER PRIMARY KEY AUTOINCREMENTon SQLite).ALTER TABLE ... ALTER COLUMN SET / DROP NOT NULLfor nullability changes (vs SQLite's table-recreation dance).$1..$Nplaceholders andON CONFLICT DO NOTHINGon the migration apply path.- Native types from the Type catalogue below -
JSONB,UUID,TIMESTAMPTZ, arrays, INET/CIDR/MACADDR, TSVECTOR - instead of theTEXT/BLOBfallbacks SQLite uses.
The migration files themselves are backend-agnostic JSON; the same file applies to both backends. The engine renders the backend-specific SQL at migrate time from the active pool's backend.
Querying
let articles = Article::objects().fetch().await?;The ambient pool() reads through whichever DbPool variant App::build() registered. No per-backend code in the handler.
If you hold a PgPool directly (integration tests, scripts that don't run through App::build()):
let articles = Article::objects().on_pg(&pg_pool).fetch().await?;.on_pg(&pool) is the Postgres counterpart of .on(&sqlite_pool). The _pg variants on the QuerySet (fetch_pg, first_pg, get_pg, count_pg, exists_pg) take an explicit PgPool and skip the dispatch entirely - useful for models whose fields are Postgres-only and so don't satisfy the dual FromRow bound.
Postgres-only field types
Some workloads benefit enough from native PG features to make the SQLite story irrelevant. Umbral exposes them as first-class field types; the boot-time system check refuses to start if you register one against a SQLite pool, with a clear error pointing at the offending model.
| Type | Rust field | Postgres column | Notes |
|---|---|---|---|
| Array | Vec<T> / Option<Vec<T>> | T[] | Element type is one of i32, i64, String, Uuid, bool. See orm/models - Array columns. |
| JSONB | serde_json::Value (M3 Json SqlType) | JSONB | Indexed ->, ->> operator surface; the QuerySet exposes them as .has_key("key") and .path_text(&["a", "b"]).eq("v"). See orm/querying - JSON column ops. |
| INET / CIDR / MACADDR | IpNet-style newtypes | INET, CIDR, MACADDR | Networking fields. orm/models - Network address columns. |
| TSVECTOR | umbral::orm::TsVector | TSVECTOR | Full-text search. Populate via trigger or GENERATED ALWAYS AS (to_tsvector(...)). orm/models - Full-text search columns. |
Plus the standard cross-backend types (Boolean, Uuid, Timestamptz, etc.) all map to their native Postgres equivalents (BOOLEAN, UUID, TIMESTAMP WITH TIME ZONE) on this backend.
inspectdb (Postgres)
inspectdb walks pg_catalog and information_schema to produce umbral Model definitions for every table in an existing database:
cargo run -- inspectdb --output src/legacy# -> Wrote src/legacy/models.rs# -> Wrote src/legacy/migrations/app/0001_initial.json--output is a directory: inspectdb writes a models.rs with one #[derive(Model)] struct per table plus a 0001_initial.json migration, ready to drop into a module and register from a plugin (or via .model::<T>()). Round-trip integration coverage lives in crates/umbral-core/tests/postgres_inspect.rs. See the inspectdb page for the full flag set (--mark-applied, adopting a populated DB).
Row-Level Security
Postgres's Row-Level Security is the right tool for per-row access control that the database enforces regardless of how the row is queried. The umbral-rls plugin (Row-Level Security (umbral-rls)) wraps the CREATE POLICY shape and runs at Plugin::on_ready time. On a SQLite backend the plugin logs and skips, so RLS-using apps stay dev-portable.
Type catalogue
The complete SqlType → Postgres column-type mapping lives in crates/umbral-core/src/backend.rs. Highlights:
SqlType | Postgres column |
|---|---|
SmallInt / Integer / BigInt | SMALLINT / INTEGER / BIGINT |
Real / Double | FLOAT / DOUBLE PRECISION |
Boolean | BOOLEAN |
Text | TEXT |
Date / Time / Timestamptz | DATE / TIME / TIMESTAMP WITH TIME ZONE |
Uuid | UUID |
Json | JSONB (always JSONB, never JSON - the operator + index story is the whole point) |
Array(T) | T[] (recursively mapped) |
Inet / Cidr / MacAddr | INET / CIDR / MACADDR |
FullText | TSVECTOR (via ColumnType::custom) |
See also
- SQLite backend - the dev / test counterpart.
- Row-Level Security (umbral-rls) - Postgres-only policy plugin.
- orm/models - Postgres-only column types - array / JSON / network / full-text field declarations and operator surface.