Migrations

Failures

What a failed migration step leaves in okm_history and okm_steps, and how to resume.

okm migrate apply records a finished step in okm_history and an in-flight step in okm_steps. A step that can share a transaction rolls back with its history row. A step that committed outside that transaction is probed or left for a person. The command that records the person's decision is migrate resolve. Status lines are migrate status.

Postgres: native. Proof passes (concurrent index create and drop run outside the transaction, and validate stays in it).

Two tables

okm_history is the only record of a step that finished.

Column
migration_idThe migration file.
step_index0-based.
classexpand, contract, or unclassified.
catalog_hashThe catalog hash recorded for that step.
applied_atWhen the step was recorded.

okm_steps exists only while a step that already committed is still in flight. A step that rolls back with its transaction leaves no row here.

Column
migration_id, step_indexThe same pair as history.
kindThe step kind, or raw-sql.
sql_hashSHA-256 of the statement text, LF line endings, trimmed.
statestarted, failed, or retry.
attemptStarts at 1.
started_at, finished_atfinished_at is null until the step fails or is marked retry.
runnerThe host that started the attempt.
error_code, error_texterror_text is at most 500 characters.

The primary key of both tables is (migration_id, step_index).

The step id is migrationId:stepIndex. 0002_add_orders:2 is step index 2 of that migration. okm migrate status prints that id and says which command to run. failed at step N in that line is 0-based, the same N as the id. Apply's own Migration <id> failed at step N is 1-based.

Renaming okm_steps or okm_history in the schema is OKM1899. The name is the ledger.

Where the runner stops

What happenedWhat is recordedWhat you do
The statement was inside the migration transaction and it failed.Nothing. The transaction rolls back, and so does the history row.Fix the migration and run okm migrate apply again.
The statement committed, and this kind has a built-in probe.An okm_steps row.Run okm migrate apply again. The probe decides.
The statement committed, and the kind has no probe.An okm_steps row. The next apply stops.Finish the work, then okm migrate resolve.
The connection dropped after the server committed and before the client heard.The same as a committed statement.Probe if the kind has one. Otherwise resolve.

A built-in probe reads the catalog. It does not re-run the statement to find out.

KindWhat the probe reads
create-index with CONCURRENTLYpg_index.indisvalid and pg_stat_progress_create_index. Valid is applied. In progress is unknown. Absent is not applied.
drop-index with CONCURRENTLYpg_class. Absent is applied. Still listed is not applied.
add-constraintpg_constraint and convalidated. Valid is applied.
validate-constraintpg_constraint.convalidated.
add-enum-valuepg_enum.

Any other kind is unknown unless the step declares -- probe:.

The SQL text is part of the decision for a concurrent index. A hash that does not match the row is OKM1556. Apply does not guess.

An index build still running in another session is unknown. The message is Index <name> is being built by another session. Wait for it to finish, then apply again.

The step table

The table is generated from STEP_KINDS, postgresHasProbe, and unitMode. Do not edit by hand.

KindTransactionalBuilt-in probeResumeCode
create-tableyesnorolls back with the history row; the next apply runs the step again—
drop-tableyesnorolls back with the history row; the next apply runs the step again—
add-columnyesnorolls back with the history row; the next apply runs the step again—
add-column-requiredyesnorolls back with the history row; the next apply runs the step again—
drop-columnyesnorolls back with the history row; the next apply runs the step again—
set-column-typeyesnorolls back with the history row; the next apply runs the step again—
set-not-nullyesnorolls back with the history row; the next apply runs the step again—
drop-not-nullyesnorolls back with the history row; the next apply runs the step again—
set-defaultyesnorolls back with the history row; the next apply runs the step again—
drop-defaultyesnorolls back with the history row; the next apply runs the step again—
add-identityyesnorolls back with the history row; the next apply runs the step again—
drop-identityyesnorolls back with the history row; the next apply runs the step again—
set-identity-alwaysyesnorolls back with the history row; the next apply runs the step again—
set-identity-by-defaultyesnorolls back with the history row; the next apply runs the step again—
add-constraintyesyesrolls back with the history row; a committed copy follows the probeOKM1553
validate-constraintyesyesrolls back with the history row; a committed copy follows the probeOKM1553
drop-constraintyesnorolls back with the history row; the next apply runs the step again—
drop-not-null-checkyesnorolls back with the history row; the next apply runs the step again—
rename-constraintyesnorolls back with the history row; the next apply runs the step again—
widen-checkyesnorolls back with the history row; the next apply runs the step again—
narrow-checkyesnorolls back with the history row; the next apply runs the step again—
create-indexno when CONCURRENTLYwhen CONCURRENTLYa plain statement rolls back; CONCURRENTLY follows the probe, and partial drops the index and rebuildsOKM1553, OKM1556
drop-indexno when CONCURRENTLYwhen CONCURRENTLYa plain statement rolls back; CONCURRENTLY follows the probeOKM1553, OKM1556
rename-indexyesnorolls back with the history row; the next apply runs the step again—
create-sequenceyesnorolls back with the history row; the next apply runs the step again—
drop-sequenceyesnorolls back with the history row; the next apply runs the step again—
create-enumyesnorolls back with the history row; the next apply runs the step again—
recreate-enumyesnorolls back with the history row; the next apply runs the step again—
add-enum-valuenoyesfollows the probe: applied writes history, absent runs the statement, unknown stopsOKM1553, OKM1556
rename-enumyesnorolls back with the history row; the next apply runs the step again—
drop-enumyesnorolls back with the history row; the next apply runs the step again—
set-enum-columnyesnorolls back with the history row; the next apply runs the step again—
create-domainyesnorolls back with the history row; the next apply runs the step again—
drop-domainyesnorolls back with the history row; the next apply runs the step again—
add-domain-checkyesnorolls back with the history row; the next apply runs the step again—
validate-domain-checkyesnorolls back with the history row; the next apply runs the step again—
drop-domain-checkyesnorolls back with the history row; the next apply runs the step again—
rename-domain-checkyesnorolls back with the history row; the next apply runs the step again—
create-extensionyesnorolls back with the history row; the next apply runs the step again—
drop-extensionyesnorolls back with the history row; the next apply runs the step again—
update-extensionyesnorolls back with the history row; the next apply runs the step again—
move-extensionyesnorolls back with the history row; the next apply runs the step again—
create-functionyesnorolls back with the history row; the next apply runs the step again—
drop-functionyesnorolls back with the history row; the next apply runs the step again—
replace-functionyesnorolls back with the history row; the next apply runs the step again—
create-triggeryesnorolls back with the history row; the next apply runs the step again—
drop-triggeryesnorolls back with the history row; the next apply runs the step again—
create-viewyesnorolls back with the history row; the next apply runs the step again—
drop-viewyesnorolls back with the history row; the next apply runs the step again—
replace-viewyesnorolls back with the history row; the next apply runs the step again—
create-matviewyesnorolls back with the history row; the next apply runs the step again—
drop-matviewyesnorolls back with the history row; the next apply runs the step again—
refresh-matviewyesnorolls back with the history row; the next apply runs the step again—
create-roleyesnorolls back with the history row; the next apply runs the step again—
alter-roleyesnorolls back with the history row; the next apply runs the step again—
grantyesnorolls back with the history row; the next apply runs the step again—
revokeyesnorolls back with the history row; the next apply runs the step again—
grant-defaultyesnorolls back with the history row; the next apply runs the step again—
revoke-defaultyesnorolls back with the history row; the next apply runs the step again—
rename-tableyesnorolls back with the history row; the next apply runs the step again—
rename-columnyesnorolls back with the history row; the next apply runs the step again—
rename-sequenceyesnorolls back with the history row; the next apply runs the step again—
backfill-expandnonocontinues from the last key in okm_backfill; no okm_steps row—
backfill-contractnonocontinues from the last key in okm_backfill; no okm_steps row—
enable-rlsyesnorolls back with the history row; the next apply runs the step again—
create-policyyesnorolls back with the history row; the next apply runs the step again—
drop-policyyesnorolls back with the history row; the next apply runs the step again—
disable-rlsyesnorolls back with the history row; the next apply runs the step again—
raw-sqlyes, unless the statement cannot share a transactionwhen CONCURRENTLYrolls back when the statement can share a transaction; a concurrent index follows the probe; any other committed statement stops until a declared probe or a person marks retryOKM1553, OKM1556, OKM1557

create-index and drop-index are transactional when the statement is not CONCURRENTLY. raw-sql is transactional unless the statement cannot share a transaction. The resume cell is what the next apply does.

A declared probe

A step with no built-in probe can name one in its header.

probe.sql
-- probe: SELECT to_regclass('public.orders') IS NOT NULL
-- transactional: false
CREATE TABLE public.orders (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY);

The probe is one read-only SELECT (or WITH … SELECT). It returns exactly one boolean. True means the step has already been applied. False means it has not. NULL, more than one row, a non-boolean, or an error is unknown, never applied. Apply runs it inside begin read only and rolls the transaction back. The text is not part of the statement hash.

A built-in probe for the step's kind still wins. The declared line is not consulted.

There is no cleanup for a declared probe. A raw step has to be atomic. If a crash can leave a partial result, the author repairs that through okm migrate resolve after looking at the database. okm migrate resolve --applied uses the declared probe, so that step no longer needs --accept-unverified. A probe that is not applied is still OKM1554.

OKM1557 refuses a committed step that has no built-in probe and no -- probe:. OKM1558 refuses a probe that is empty, is not one read-only SELECT, has more than one statement, or sits on a step that stays inside a transaction. -- okm-allow OKM1557: reason records that a failure of that step needs a person. An empty reason does not silence the finding.

Resolve

okm migrate resolve <migration>:<step> --applied writes the history row and deletes the okm_steps row. When the kind has a probe, or the step declares one, a probe that says the step is not applied is OKM1554. When no probe exists, the command is OKM1555 until --accept-unverified.

okm migrate resolve <migration>:<step> --retry sets the row to retry. The next apply probes first when a probe exists, and otherwise runs the statement again.

There is no skip. A protected target is a history repair and needs --allow-protected. The invocation and the JSON fields are on migrate resolve.

NextYou use it for
LintCatch a replay failure before apply.
BackfillA step that resumes from okm_backfill instead of okm_steps.
migrate resolve--applied, --retry, and --accept-unverified.