V04 of the package DDL: rebuilds V03's metadata GIN index with
CREATE INDEX CONCURRENTLY.
V03 creates that index with a plain CREATE INDEX, which takes a
SHARE lock on the runs table for the whole build and blocks every
INSERT, UPDATE and DELETE against it until the build finishes.
For a host stepping runs durably that is every step of every run. On
a small or idle table the build is imperceptible; on a large one it
is an outage, and the README's answer used to be a hand-written
migration the host maintained itself (sp-ajz).
This version ships that answer instead. It drops the index V03 built
and creates the same index again - same name, same expression, same
jsonb_path_ops opclass - with concurrently: true on both
statements, so neither blocks writes.
The host's migration module is what disables the transaction
CREATE INDEX CONCURRENTLY cannot run inside a transaction block,
and @disable_ddl_transaction / @disable_migration_lock are read
by Ecto.Migrator from the module it is running - the host's own
migration - not from this module. A package migration module cannot
turn its caller's transaction off. So a host that wants the
concurrent build writes:
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)
endand up/1 skips when it finds itself inside a transaction after
all. Skipping rather than raising is deliberate: the end state of a
skipped V04 is the index V03 already built, which is correct and
complete, differing only in how it was built. Raising would break the
one-call recipe (up(for: MyApp.Persistence)) that every fresh
database and every test harness uses, to no benefit - on an empty
runs table a plain build costs nothing.
The skip warns through Logger.warning/1 only when the runs table
already holds rows, which is exactly the case where the plain build
blocked writes and the host has something to do about it. A fresh
database migrates in silence.
Rolling back
down/1 does nothing, on every adapter, and that is the whole
rollback. What V04 leaves behind is not a new object: it is V03's
index, under V03's name, with V03's definition, and V03's down/1
is what drops it. Rebuilding it plainly on the way down would take
exactly the SHARE lock this version exists to avoid, in order to
reach a state no reader can tell apart from the one it started in.
Failure part-way through
A non-transactional migration has no rollback, so an interrupted
up/1 can leave the runs table with no metadata index or with an
invalid one. Re-running the migration is the repair: the drop is
drop_if_exists, so it clears either leftover, and the create then
builds the index once more. Nothing in the package's DDL depends on
the index existing; what depends on it is the cost of the two
containment queries V03's moduledoc names.
Adapters other than Postgres
up/1 and down/1 are both no-ops off Ecto.Adapters.Postgres,
under the same exact-match adapter check V03 uses and for the same
reason: GIN and jsonb_path_ops are Postgres spellings, so on any
other adapter there is no index to rebuild - V03 skipped creating one
(sp-11w), and docs/non-postgres-backends.md describes what that
costs such a host. The check runs before the transaction check, so a
SQLite host needs neither attribute.
Summary
Functions
Leaves V03's index in place - see the moduledoc's rollback section.
Rebuilds the metadata GIN index concurrently per config.
Functions
@spec down(StatifierPersistence.Ecto.Config.t()) :: :ok
Leaves V03's index in place - see the moduledoc's rollback section.
@spec up(StatifierPersistence.Ecto.Config.t()) :: :ok
Rebuilds the metadata GIN index concurrently per config.
A no-op off Postgres, and a no-op with a warning when the host's migration module has not disabled the DDL transaction - see the moduledoc.