Versioned migrations for this package's tables, in the shape
statifier_persistence's StatifierPersistence.Ecto.Migrations uses: the
host writes one ordinary migration that delegates here, and later
package versions ship higher-numbered migration modules the same call
picks up.
defmodule MyApp.Repo.Migrations.AddStatifierRouter do
use Ecto.Migration
def up, do: StatifierRouter.Migrations.up(prefix: "routing")
def down, do: StatifierRouter.Migrations.down(prefix: "routing")
endThe options are the two storage options of StatifierRouter.Config,
:table_prefix and :prefix, resolved the same way, :from and
:version, the three layout options statifier_persistence's
migrations helper takes, under the same spellings, and :primary_key:
:table_prefix- a string prefixed to every table name, default"statifier_router_". Pass the same value the host'sStatifierRouter.Configcarries.:prefix- the Postgres schema the tables live in, defaultnil. When it is set,up/1creates the schema if it does not exist anddown/1leaves it in place: dropping a schema the host may share is not this package's call.:fromand:version- where a call starts and where it ends, in both directions.up/1migrates fromfrom:(default: V01) up throughversion:(default: the newest this package knows), anddown/1rolls back fromfrom:(default: the newest) down throughversion:(default: V01, that is, everything).:leading_columns- host-owned columns placed immediately afteridin every table a version creates, in the order given, default[]. A keyword list ofname: {type, opts}, wheretypeandoptsare whatEcto.Migration.add/3takes:leading_columns: [branch_id: {:text, null: true}]puts a nullablebranch_idat ordinal position 2 on all four tables. The package's schemas do not declare the column, so the package never reads or writes it; a default or aNOT NULLbelongs to a later migration of the host's own. A name a table the call creates already declares - any columnStatifierRouter.Migrations.V01orStatifierRouter.Migrations.V02lists for it - raisesArgumentErrornaming the column and those tables, before any DDL runs, where Postgres would otherwise refuse theCREATE TABLEwith a duplicate column. A name only a table the call does not create declares is a host column like any other:up(from: 2)may lead withexpires_at, which only V01's dedupe table has. Without:primary_key, the primary key is the repo's:migration_primary_keyand is not checked: a repo that sets it tofalsemay lead with anidof its own. With:primary_keyset, the package declaresiditself, and a leadingidraises like any other package column.:timestamps_position- whereinserted_atgoes in every table a version creates that has one (the address table, the routing ledger and the subscription table; the dedupe table has none)::trailing(default: the layoutStatifierRouter.Migrations.V01andStatifierRouter.Migrations.V02document) or:leading(immediately afteridand the:leading_columns). The address table'sterminal_seen_atis not a timestamp column in this sense and stays where it is.:column_collations- a collation per package text column, applied wherever a version declares that column in aCREATE TABLE, default[](every column takes the database default). A keyword list ofname: collation, the collation a non-empty string:column_collations: [execution_id: "C"]declaresexecution_idCOLLATE "C"on the address table, the routing ledger and the subscription table. The names are the text columns the versions declare -scope,document,key,execution_id,binding_id,message_id,outcome,reasonandinvoke_id- and the collation must be one the database knows; a host column takes its collation in its own:leading_columnsopts instead.:primary_key- the type and default of theidprimary key of every table a version creates, in place of the repo's:migration_primary_key, default: not set. A keyword list with a:type, required, and a:default, optional, each whatEcto.Migration.add/3takes:primary_key: [type: :text, default: fragment("gen_random_uuid()::text")]buildsidas a text primary key the database fills in, on all four tables. The column is always namedid, the name the schemas inStatifierRouter.Schemaread. The package inserts no id of its own, so the column needs a default the database fills in (abigserialor an identity column has one already); a key without one fails every insert the package makes. The schemas read the id back as the database holds it, an integer or a string (StatifierRouter.Schema.Id), andStatifierRouter.Addresses.reap/2sweeps in the id column's own order. Left out, every table takes the repo's primary key, exactly as before the option existed.
The three layout options and :primary_key apply to a fresh create
only. Each table is laid out by the version that creates it - V01 the
address table, the dedupe table and the routing ledger, V02 the
subscription table - and no version re-places a column or re-types a
key in a table that already exists, so adding an option later changes
nothing in the tables already built: a host that ran V01 under the
repo's key and sets :primary_key for V02 gets the new key on the
subscription table alone, and each table keeps the key it was built
with. Left out, every version builds exactly the tables it built before
the options existed. down/1 accepts them too, so one options list
serves both directions, and ignores them.
A host already running an older version writes its next migration with
from: set to the first version it has not run, rather than re-running
V01's CREATE TABLE against tables that already exist. A host whose
first migration is capped with version: N caps its rollback to match
with from: N.
A fault in the options raises ArgumentError: a migration has no caller
to hand an {:error, reason} to.
StatifierRouter.Migrations.V01 records what the first version creates,
StatifierRouter.Migrations.V02 what the second adds, and
StatifierRouter.Migrations.V03 what the third renames.
:from is inclusive: up(from: 2) runs V02, and a host already on
V01 that writes it gets the subscription table without V01's
CREATE TABLE running a second time (ADR-0007, section 6).
Upgrading to V03
V03 renames the subscription table's unique index, which V02 named past
the 63 bytes Postgres keeps of an identifier, to <table>_invocation_index.
A host that has already run V02 runs one more version, in a new migration
of its own:
def up, do: StatifierRouter.Migrations.up(from: 3)
def down, do: StatifierRouter.Migrations.down(from: 3, version: 3)with the same :table_prefix and :prefix as its earlier migrations.
V03 renames the index in place and nothing is rebuilt. A host whose first
migration calls up/1 with no version: gets V03 from it on a fresh
database, and still writes the migration above for the databases that
ran the first one before V03 existed: on a fresh database the second
run finds the index already renamed and does nothing. On SQLite, which
never cut V02's name, V03 does nothing in either direction and the index
keeps V02's name (StatifierRouter.Migrations.V03).
The location table, V04, is opt-in
A configuration that sets :basichttp keeps each address row's
BasicHTTP location token in a table of its own, which
StatifierRouter.Migrations.V04 creates (ADR-0002, the Amendment of
2026-09-30 on the BasicHTTP location, decision 1). A host that never
sets the key does not need it, so V04 is not in the version walk
(ADR-0002, the Amendment of 2026-09-30 "the location table is opt-in,
outside the version walk"): up/1 and down/1, capped or not, never
create, drop or require it, and every call above answers as it did
before V04 existed. A host that sets the key runs it with its own two
calls, up_locations/1 and down_locations/1, in a migration of its
own after the ones it already has:
def up, do: StatifierRouter.Migrations.up_locations(prefix: "routing")
def down, do: StatifierRouter.Migrations.down_locations(prefix: "routing")They take :table_prefix, :prefix, the three layout options and
:primary_key, and neither :from nor :version. Pass the same
values as the earlier migrations: the table's address_id references
the address table's id and takes the key type :primary_key names.
up_locations/1 creates only what is missing, and down_locations/1
drops the table only if it is there. Because the table references the
address table, the migration that runs up_locations/1 must roll back
before the one that created V01's tables: a host that writes it as a
later migration gets that order from Ecto's rollback, which undoes the
newest migration first.
Index names and a long :table_prefix
Every index name the versions leave is the table's name followed by a
suffix, and Postgres keeps at most 63 bytes of it. Under the default
prefix, "statifier_router_" (17 bytes), every name fits; the longest,
statifier_router_routing_ledger_binding_id_inserted_at_index, is 60
bytes. A :table_prefix longer than 20 bytes takes that name past 63,
and longer ones take more of the names with it. Such a prefix is
accepted: Postgres creates the index under its first 63 bytes and logs a
notice, and the index works as before, since the package's queries name
an index's columns, never its name. What changes is the name a host
reads back - a unique violation's constraint name, or pg_indexes -
which is the truncated one. V03 accounts for that when it renames: it
looks the index up under the name Postgres gave it. A prefix of 49 bytes
or more fails V01 itself: the routing ledger's table name then takes up
all 63 bytes, its index name cut to 63 bytes is the table's own name,
and Postgres refuses the CREATE INDEX because that relation exists.
Summary
Functions
Rolls the tables back from from: (default: the newest version this
package knows) down through version: (default: V01, that is,
everything).
Drops the location table if it is there, and nothing else. Takes the
options up_locations/1 takes and ignores the layout options.
Migrates the tables from from: (default: V01) up through version:
(default: the newest).
Creates the location table StatifierRouter.Migrations.V04 describes,
outside the version walk (see "The location table, V04, is opt-in",
and ADR-0002, the Amendment of 2026-09-30 "the location table is
opt-in, outside the version walk").
Takes the storage options, the layout options and :primary_key; a
:from, a :version or any other key raises ArgumentError, as a
leading column named like one of the table's own columns does, before
any DDL.
Functions
@spec down(keyword()) :: :ok
Rolls the tables back from from: (default: the newest version this
package knows) down through version: (default: V01, that is,
everything).
@spec down_locations(keyword()) :: :ok
Drops the location table if it is there, and nothing else. Takes the
options up_locations/1 takes and ignores the layout options.
@spec up(keyword()) :: :ok
Migrates the tables from from: (default: V01) up through version:
(default: the newest).
@spec up_locations(keyword()) :: :ok
Creates the location table StatifierRouter.Migrations.V04 describes,
outside the version walk (see "The location table, V04, is opt-in",
and ADR-0002, the Amendment of 2026-09-30 "the location table is
opt-in, outside the version walk").
Takes the storage options, the layout options and :primary_key; a
:from, a :version or any other key raises ArgumentError, as a
leading column named like one of the table's own columns does, before
any DDL.