Versioned migrations for this package's tables, in the Oban.Migration
mold: the host writes one ordinary migration that delegates here, and
later package versions ship higher-numbered migration modules the same
call picks up.
The supported spelling reads the host's compiled configuration, so the DDL cannot drift from the generated schemas (ADR-0002 decision 3):
defmodule MyApp.Repo.Migrations.AddStatifierPersistence do
use Ecto.Migration
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence)
endAlternatively, up/1 and down/1 accept the same literal options
use StatifierPersistence.Ecto takes (:repo, :key, :table_prefix,
:tables, :prefix), funneled through the same
StatifierPersistence.Ecto.Config.new/1 - one resolver, both doors.
The two spellings cannot be mixed in one call.
from: selects where a call starts and version: where it ends, in
both directions: up/1 migrates from from: (default: V01) up through
version: (default: the newest this package knows), and down/1 rolls
back from from: (default: the newest) down through version:
(default: V01, i.e. everything).
from: is what a host
already running an older version writes its next migration with: a
host that ran the migration above when this package shipped only V01
picks up V02 with a second ordinary migration,
defmodule MyApp.Repo.Migrations.AddStatifierPersistenceExecutionMetadata do
use Ecto.Migration
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 2)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, version: 2)
endrather than re-running V01's CREATE TABLE against tables that already
exist. A host migrating a fresh database with the first spelling gets
every version in one call and needs no second migration at all.
Each migration's two calls cover the same span, up/1 upwards and
down/1 downwards, and a capped migration needs both bounds spelled
out. A host that caps its first migration at version: 2 - so that a
fresh clone and an already-migrated database take the same steps in the
same order - caps the rollback to match with from: 2:
defmodule MyApp.Repo.Migrations.AddStatifierPersistence do
use Ecto.Migration
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, version: 2)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, from: 2)
endWithout that ceiling down/1 starts at the newest version this package
knows however far up the migration beside it went, so
mix ecto.rollback --all rolls V03 back twice - once from the later
migration and once from this one - and the second call fails on a
column that is already gone.
V04 is the one version whose full effect depends on how the host's
own migration module is written. It rebuilds V03's metadata GIN
index with CREATE INDEX CONCURRENTLY, which cannot run inside a
transaction, and @disable_ddl_transaction / @disable_migration_lock
are read from the module Ecto.Migrator runs - the host's - not from
a module it delegates to. A host that wants the concurrent build
therefore gives V04 a migration of its own:
defmodule MyApp.Repo.Migrations.RebuildStatifierPersistenceMetadataIndex do
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def up, do: StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 4)
def down, do: StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, version: 4)
endInside a transaction V04 skips the rebuild and leaves V03's index in
place, which is the same index under the same name - so the one-call
recipe above stays correct on a fresh database, where a plain build
on an empty executions table costs nothing. It warns only when that
table already holds rows. StatifierPersistence.Ecto.Migrations.V04
records the whole of it.
V05 needs no recipe of its own: it creates ADR-0010's input log table
and its unique (execution_id, seq) index, on every backend, inside an
ordinary transaction. A host already running V04 picks it up with
up(for: MyApp.Persistence, from: 5).
V06 needs no recipe of its own either, and what it does depends on
which database it finds (ADR-0011 decision 3). On a database this
package built before 0.12.0 it renames the runs table to
executions, both run_id columns to execution_id, and the indexes
over them - in place, copying no data. On a database built at 0.12.0
or later there is nothing to rename, because V01-V05 create the
execution names directly, and V06 is a no-op. Either way it is the
version this package now expects.
Rolling back never renames anything: V06's down/1 is a no-op
(RQ-SF041-25, ruled 2026-09-13). V01-V05 are rewritten to the
execution names and drop the tables under them, on both kinds of
database, so there is nothing a rename back would leave in a better
state - and a downgrade to pre-0.12.0 code is unsupported, by the
record and by this package. StatifierPersistence.Ecto.Migrations.V06
records the whole of it.
One ordering rule comes with it, on the way up only. An install
that still owes V02, V03 or V04 - one capped below version 4 - runs V06
on its own first and the versions it skipped afterwards, because those
three alter the executions table, which on a database built before
0.12.0 carries that name only once V06 has renamed it. For a host
capped at V01:
defmodule MyApp.Repo.Migrations.RenameStatifierPersistenceExecutions do
use Ecto.Migration
def up do
StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 6, version: 6)
StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 2, version: 5)
end
def down do
StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, from: 5, version: 2)
StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence, from: 6, version: 6)
end
endSubstitute your own cap in both spans: a host capped at V02 writes
from: 3 on the second up and version: 3 on the first down, and
one capped at V03 writes from: 4 and version: 4. The two down
calls mirror the two up calls in reverse, and the V06 one does
nothing at all. V02-V05's arms name the executions table under the name
it carries on 0.12.0 code, which is the name V06 gave it - V05 drops
the input log, V03 the metadata index and outcome_blob, V02 the
metadata column - so a rename back would leave every one of them
naming an object that is no longer there. The table itself is dropped
by V01, below the cap, which is the host's own earlier migration's
business; running 0.11.x against a database that was ever upgraded is
unsupported either way (ADR-0011 decision 3).
An install already at V05 needs none of that: up(from: 6) is the whole
upgrade.
expected_version/0 answers what that newest version is. A host that
delegates its migrations here never needs it; a host whose schema is
hand-written DDL has to check for itself that its tables are current,
and that is the number it checks against.
When prefix: names a Postgres schema, up/1 creates the schema if it
does not exist; down/1 leaves the schema in place (dropping a schema
the host may share is not this package's call).
Summary
Functions
Rolls the tables back from from: (default: the newest version this
package knows) down through version: (default: V01, i.e. everything).
The newest migration version this package knows - the newest key of the
version map, the version an up-to-date schema has run through, and
up/1's default target.
Migrates the tables from from: (default: V01) up through version:
(default: the newest).
Functions
@spec down(keyword()) :: :ok
Rolls the tables back from from: (default: the newest version this
package knows) down through version: (default: V01, i.e. everything).
A migration whose up/1 is capped at version: N caps its down/1
with from: N, so the rollback stops at the cap instead of reaching
versions a later migration has already rolled back - see the moduledoc.
Every version's down/1 runs, unconditionally. V06's is a no-op
(RQ-SF041-25, ruled 2026-09-13): V01-V05 are rewritten to the execution
names and drop the tables under them, so there is nothing to rename
back first, and a rollback reaches the same end state under one call or
under one host migration per version.
Takes the same options as up/1.
@spec expected_version() :: pos_integer()
The newest migration version this package knows - the newest key of the
version map, the version an up-to-date schema has run through, and
up/1's default target.
It exists for the host whose schema is not a delegated migration.
A host that writes up(for: MyApp.Persistence) never needs the number:
Ecto.Migrator records that migration and the default target carries it
to the newest version. A host whose DDL is hand-written - the tables
generated by hand, or by another tool, or shared with a schema this
package does not own - has to know which version its hand-written DDL
corresponds to, and that number is not otherwise readable from here:
if MyApp.Schema.statifier_persistence_version() <
StatifierPersistence.Ecto.Migrations.expected_version() do
raise "statifier_persistence schema is behind the package"
endThere is no assert_version!/1, and there cannot be one. Asserting a
version against a repo means reading a marker out of that repo's schema,
and this package records none: it writes no versions table, no marker row
and no version column, and Ecto's own schema_migrations holds the
host's migration timestamps, which say nothing about which of V01..V06
a hand-written schema matches. The only way to grow such an assertion is
to start writing a marker - a new table, in a new migration, carried by
every host including the ones that delegate and already know - which is
new stored surface bought for a comparison the host can make itself.
So the package answers the half it knows, the version it expects, and
the comparison stays the host's.
@spec up(keyword()) :: :ok
Migrates the tables from from: (default: V01) up through version:
(default: the newest).
Takes for: HostModule or the literal options use takes - see the
moduledoc.