pub async fn migrate(
pool: &PgPool,
options: MigrateOptions<'_>,
) -> Result<(), MigrateError>Expand description
Applies Reliar’s migrations. Never invoked implicitly. pool must reach a PostgreSQL 18
or later server — a hard requirement, with no older-version fallback, checked here as the
first statement on this function’s dedicated connection (ADR 0041 / human decision #47):
a server below the floor returns MigrateError::UnsupportedServerVersion before the schema
is created or any migration file runs.
Creates options.schema if it does not exist, keeps bookkeeping in
<schema>._migrations — never _sqlx_migrations — and serializes concurrent callers with
Reliar’s own advisory lock, acquired by polling (ADR 0040 amendment A; not
sqlx::migrate’s built-in blocking one), so every caller after the first observes Ok(()).
Idempotent.
Self-contained: does not depend on the caller’s search_path (ADR 0018) — create_schema
plus the qualified bookkeeping table name make it work over a pool whose URL never set one.
use reliar_store_postgres::{MigrateOptions, migrate};
migrate(&pool, MigrateOptions::default()).await?;§The lock wait is unbounded, the connection must be a real session, and timing matters
A second concurrent caller can wait for the first for as long as that first run takes —
legitimately minutes for CREATE INDEX CONCURRENTLY on a large table — and this function
never times that wait out on its own; wrap the call in tokio::time::timeout if a bound is
needed. Dropping that future while it is still polling for the lock (before migrator.run
starts) is exactly as clean as it sounds — nothing is held between poll attempts, as noted
below. Dropping it after the lock is acquired, while migrator.run itself is executing
(e.g. mid-CREATE INDEX CONCURRENTLY), is different: the explicit unlock query never runs, so
the advisory lock is released only when the dropped connection’s own teardown ends the
session, not by this function’s normal path — and whatever DDL was in flight is left exactly
as any other interrupted CONCURRENTLY build would be (see the recovery step below).
pool’s connection URL must not point at a transaction-mode pooler: migrate()
needs one real session for the run’s whole duration, both for SET search_path and for the
session-level advisory lock, and a pooler that hands out a different backend per statement
would silently break both (the outbox_pgdog test in this crate’s suite migrates over a
direct connection for exactly this reason, before ever pooling). Finally, CREATE INDEX CONCURRENTLY (in 0002_outbox_claimable_index.sql) must wait for every transaction that was
already open when it started to finish, regardless of what table that transaction touches —
run migrate() when the database has no other long-running transaction in flight.
§Upgrading from 0.3.0
A host that only ever calls this function has nothing to do — migrate() applies
0002/0003 the same way it always applied 0001. A host that instead applies the published
.sql artifact through its own DBA pipeline (Flyway, Liquibase, sqitch, golang-migrate, a raw
psql invocation, …) may not be interchangeable with this function for 0002: see
docs/guides/postgres.md’s “migrate() vs. the release SQL artifact” section for the
per-tool equivalent of “run this one file outside a transaction” that 0002’s CREATE INDEX CONCURRENTLY requires (sqlx’s own -- no-transaction marker means nothing to another
tool), and the same section’s note on 0003’s SET LOCAL lock_timeout, which needs an active
transaction to have any effect.
§0002_outbox_claimable_index.sql runs outside a transaction
That one migration issues CREATE INDEX CONCURRENTLY (ADR 0040 §2), which PostgreSQL refuses
inside a transaction block; sqlx’s -- no-transaction marker keeps it (and only it) out of
one. CONCURRENTLY cannot roll back on failure, so a connection drop or cancellation mid-build
leaves an invalid index rather than undoing itself:
ERROR: relation "ix_outbox_claimable" already existson the next migrate() call means exactly that. Recover with, against the same schema:
DROP INDEX CONCURRENTLY ix_outbox_claimable;then re-run migrate() from the start — it is idempotent and will rebuild the index and
continue into 0003_drop_ix_outbox_pending.sql, which itself refuses to drop the superseded
index unless the new one exists and is valid.
§Errors
Returns MigrateError::InvalidSchema when options.schema is not a valid PostgreSQL
identifier, MigrateError::UnsupportedServerVersion when pool reaches a server older
than crate::MIN_SERVER_VERSION_NUM (PostgreSQL 18), or MigrateError::Sqlx for a
connection failure, a checksum mismatch against an already applied file, or any other failure
sqlx::migrate::Migrator::run reports — including a 0003 run against a missing/invalid
ix_outbox_claimable (see above).