StatifierPersistence.Ecto.Migrations.V04 (StatifierPersistence v0.8.0)

Copy Markdown View Source

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

and 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

down(config)

@spec down(StatifierPersistence.Ecto.Config.t()) :: :ok

Leaves V03's index in place - see the moduledoc's rollback section.

up(config)

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.