DrinkMeAlice.Migrations (drink_me_alice v0.1.0)

Copy Markdown

A checksummed, locked, versioned migration runner; the migration list is supplied by the caller.

Bookkeeping: schema_migration rows record started/applied/failed per version with the SHA-256 of the version's joined statements; a migration_lock:main row makes concurrent ownership a hard conflict. The runner refuses a database that holds a lock it did not take, a started-but-unfinished row, or a checksum that disagrees with its source.

Evidence rules (shared with DrinkMeAlice.Outcome)

Every transaction-shaped operation — lock acquisition, migration application — is classified by DrinkMeAlice.Outcome.classify_tx/3 with the script's positional context, so the runner uses the SAME three-way distinction as the write path:

  • observed commit — full-length reply, well-formed entries, OK BEGIN/COMMIT slots with nil results, mapped decision. Only this is success;
  • qualified abort — the pinned executor's statement-abort silhouette (exactly one real error, NotExecuted markers before it, Cancelled markers after). Only this is a definite failure, and only for it may the failed bookkeeping mark be written;
  • uncertain — the commit-failure rewrite (multiple rewritten Internal errors; the storage commit may already be durable), malformed or unexpected-length replies. The runner writes NOTHING and retains the lock: {:error, {:lock_retained, {:migration_unknown, version, why}}}. Reconcile by reading schema_migration for version (an applied row with the expected checksum means the migration committed) and then removing the lock row; anything else needs investigation before a retry.

Non-transaction operations (bootstrap, the state read, the release DELETE) demand their own complete evidence: exact statement count, well-formed {status, result} entries on every slot, and the expected result shape. An empty or malformed reply is a refusal, never proof of execution. A release that cannot be proven is reported ({:lock_release_unproven, _}), never discarded, and is never attempted through a session that already reported transport loss.

Callbacks (vars, verify_applied) and the caller's query function may crash without leaking the lock: a callback crash becomes a typed error with the lock released; a query-function crash during an operation with durable effect is treated exactly like transport loss.

Summary

Functions

Apply every pending migration over an authenticated root query (see DrinkMeAlice.Admin.migrate/3). owner is a fresh per-run id.

Validate a migration list: contiguous versions from 1, names that are identifiers, non-empty binary statement lists within bounds. Every member must be a map — anything else is a typed refusal, never a raise.

SHA-256 (lowercase hex) of a statement list, joined by newline.

Types

migration()

@type migration() :: %{
  :version => pos_integer(),
  :name => binary(),
  :statements => [binary()],
  optional(:seed) => [binary()],
  optional(:vars) => (keyword() -> {:ok, map()} | {:error, term()}),
  optional(:verify_applied) => (query() -> :ok | {:error, term()})
}

query()

@type query() :: (binary(), map() -> {:ok, list()} | {:error, term()})

Functions

apply_all(query, owner, migrations, opts \\ [])

@spec apply_all(query(), binary(), [migration()], keyword()) :: :ok | {:error, term()}

Apply every pending migration over an authenticated root query (see DrinkMeAlice.Admin.migrate/3). owner is a fresh per-run id.

  • :ok — every migration observed committed (or already applied with matching checksums) AND the lock release was proven;
  • {:error, :locked} — another owner holds migration_lock;
  • {:error, {:dirty, version, detail}} — a started/failed row or a checksum mismatch; refuses until reconciled by hand;
  • {:error, {:lock_state_unknown, detail}} — lock acquisition ended uncertain (transport loss, a crashed query function, or an unqualified reply): the lock may or may not be held. Inspect migration_lock before retrying;
  • {:error, {:lock_retained, {:migration_unknown, version, detail}}} — a migration transaction's outcome is UNCERTAIN (commit-failure rewrite, malformed/unexpected reply, crashed query function). Nothing was overwritten and the lock is still held; reconcile as the module doc describes before retrying;
  • {:error, {:lock_release_unproven, detail}} — the run itself is known ({:run_ok, reason} or {:run_error, run_reason, reason}) but the release DELETE could not be proven; the lock row may remain;
  • {:error, {:callback_crash, class}} — a vars/verify_applied callback crashed before anything was submitted for that migration; the lock WAS released;
  • {:error, {:incomplete_reply, got, expected}} — bootstrap evidence was empty or truncated (bootstrap runs before any lock exists);
  • {:error, {:bad_statement, detail}} — a malformed bootstrap entry;
  • {:error, {:statement_err, msg}} — a QUALIFIED statement abort (per Outcome) in a transaction, or a definite statement error outside one;
  • {:error, {:invalid_migrations, _}} — the list failed check/1;
  • {:error, reason} — transport/statement failure or a verify_applied/vars refusal.

check(migrations)

@spec check([migration()]) :: :ok | {:error, term()}

Validate a migration list: contiguous versions from 1, names that are identifiers, non-empty binary statement lists within bounds. Every member must be a map — anything else is a typed refusal, never a raise.

checksum(statements)

@spec checksum([binary()]) :: binary()

SHA-256 (lowercase hex) of a statement list, joined by newline.