This version is in beta. Some features may change before release.

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.

Code
rust
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.

Info

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

Code
bash
cargo run -- makemigrations # writes a JSON migration under migrations/<plugin>/
cargo run -- migrate # applies pending migrations against $DATABASE_URL

The engine renders Postgres DDL automatically:

  • BIGSERIAL for i64 primary keys (vs INTEGER PRIMARY KEY AUTOINCREMENT on SQLite).
  • ALTER TABLE ... ALTER COLUMN SET / DROP NOT NULL for nullability changes (vs SQLite's table-recreation dance).
  • $1..$N placeholders and ON CONFLICT DO NOTHING on the migration apply path.
  • Native types from the Type catalogue below - JSONB, UUID, TIMESTAMPTZ, arrays, INET/CIDR/MACADDR, TSVECTOR - instead of the TEXT/BLOB fallbacks 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

Code
rust
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()):

Code
rust
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.

TypeRust fieldPostgres columnNotes
ArrayVec<T> / Option<Vec<T>>T[]Element type is one of i32, i64, String, Uuid, bool. See orm/models - Array columns.
JSONBserde_json::Value (M3 Json SqlType)JSONBIndexed ->, ->> 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 / MACADDRIpNet-style newtypesINET, CIDR, MACADDRNetworking fields. orm/models - Network address columns.
TSVECTORumbral::orm::TsVectorTSVECTORFull-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:

Code
bash
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:

SqlTypePostgres column
SmallInt / Integer / BigIntSMALLINT / INTEGER / BIGINT
Real / DoubleFLOAT / DOUBLE PRECISION
BooleanBOOLEAN
TextTEXT
Date / Time / TimestamptzDATE / TIME / TIMESTAMP WITH TIME ZONE
UuidUUID
JsonJSONB (always JSONB, never JSON - the operator + index story is the whole point)
Array(T)T[] (recursively mapped)
Inet / Cidr / MacAddrINET / CIDR / MACADDR
FullTextTSVECTOR (via ColumnType::custom)

See also

postgresbackendsdatabase