StatifierRouter.Migrations (StatifierRouter v0.9.2)

Copy Markdown View Source

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")
end

The 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's StatifierRouter.Config carries.
  • :prefix - the Postgres schema the tables live in, default nil. When it is set, up/1 creates the schema if it does not exist and down/1 leaves it in place: dropping a schema the host may share is not this package's call.
  • :from and :version - where a call starts and 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, that is, everything).
  • :leading_columns - host-owned columns placed immediately after id in every table a version creates, in the order given, default []. A keyword list of name: {type, opts}, where type and opts are what Ecto.Migration.add/3 takes: leading_columns: [branch_id: {:text, null: true}] puts a nullable branch_id at 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 a NOT NULL belongs to a later migration of the host's own. A name a table the call creates already declares - any column StatifierRouter.Migrations.V01 or StatifierRouter.Migrations.V02 lists for it - raises ArgumentError naming the column and those tables, before any DDL runs, where Postgres would otherwise refuse the CREATE TABLE with 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 with expires_at, which only V01's dedupe table has. Without :primary_key, the primary key is the repo's :migration_primary_key and is not checked: a repo that sets it to false may lead with an id of its own. With :primary_key set, the package declares id itself, and a leading id raises like any other package column.
  • :timestamps_position - where inserted_at goes 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 layout StatifierRouter.Migrations.V01 and StatifierRouter.Migrations.V02 document) or :leading (immediately after id and the :leading_columns). The address table's terminal_seen_at is 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 a CREATE TABLE, default [] (every column takes the database default). A keyword list of name: collation, the collation a non-empty string: column_collations: [execution_id: "C"] declares execution_id COLLATE "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, reason and invoke_id - and the collation must be one the database knows; a host column takes its collation in its own :leading_columns opts instead.
  • :primary_key - the type and default of the id primary 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 what Ecto.Migration.add/3 takes: primary_key: [type: :text, default: fragment("gen_random_uuid()::text")] builds id as a text primary key the database fills in, on all four tables. The column is always named id, the name the schemas in StatifierRouter.Schema read. The package inserts no id of its own, so the column needs a default the database fills in (a bigserial or 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), and StatifierRouter.Addresses.reap/2 sweeps 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

down(opts \\ [])

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

down_locations(opts \\ [])

@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.

up(opts \\ [])

@spec up(keyword()) :: :ok

Migrates the tables from from: (default: V01) up through version: (default: the newest).

up_locations(opts \\ [])

@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.