Skip to main content

migrate

Function migrate 

Source
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 exists

on 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).