Skip to content

When Migration Releases Arrive Faster Than Reconciliation

Follow cumulative migration releases through one ordered plan, exact approval, Apply, and fresh history verification in Ptah Operator and Console.

Ptah helps you plan, review, and apply database migrations. Try in your browser

The orders database has migration 010. CI publishes v11, v12, and v13 before Ptah Operator reads the updated artifact reference. Each release adds a migration. The database must apply 011, 012, and 013 in order, even if the operator never observes the intermediate releases.

We follow that sequence with Ptah 0.13.0, Ptah Operator 0.2.4, and PostgreSQL 17.11. Real Ptah Console views show the decision, execution, and recovery states. The example files retain the SQL, manifests, recorded history, and reproduction steps.

A PtahMigration selects an OCI artifact reference and a database. It reads the artifact’s migration files against the database’s revision history. It does not keep a FIFO queue of CI releases.

Our v11 artifact contains 010 and 011. The next release contains 010–012, and v13 contains 010–013. Once stable points to v13, that artifact has everything needed to advance the database from 010.

CI publishes cumulative v11, v12, and v13 while the database remains at 010. The operator observes v13 and plans 011 through 013 together.

The operator can miss intermediate artifact releases. Their required migration files still belong in the artifact it observes. Open any figure for the full-size version.

The files add a tracking column, a shipment timestamp, and an index:

Migration Change Present in
010 Create orders v10–v13
011 Add tracking_code v11–v13
012 Add shipped_at v12–v13
013 Create orders_tracking_idx v13

For example, migration 011 contains:

examples/releases/v13/0000000011_add_tracking.up.sql
ALTER TABLE orders ADD COLUMN tracking_code text;

We hash and publish each cumulative directory. Here is the final publication:

Terminal window
ptah migrations hash --dir releases/v13
ptah migrations push "oci://$REGISTRY/orders-migrations:v13" --migrations-dir releases/v13 --dir-format ptah --verify-sum --version v13 --tag stable --plain-http

--version writes the release tag; --tag stable moves the alias. The fixture’s isolated registry uses HTTP, hence --plain-http. Registry access and the artifact verification policy are separate inputs in the complete resource.

The source reference decides which publication can be discovered:

Reference What a new publication changes
orders-migrations:stable A later Resolve can discover the moved tag without a CR spec update.
orders-migrations@sha256:… The selected bytes stay fixed. Publishing another tag does not change the CR; update its reference to select another digest.

The operator pins the resolved digest before verification and history reads. A tag move does not rewrite the bytes of a plan or an active Job. The delivery and planning guide explains the artifact and resource setup in more detail.

Before publishing v11–v13, Console shows that the database matches v10:

The orders migration resource has completed its history verification against the initial artifact.

This is the starting state. The selected artifact contains 010; the direct revision-table read below confirms that no later version has run.

Our before-and-after query is:

examples/history.sql
SELECT version, state, applied, total
FROM schema_migrations
ORDER BY version;

At the start, the result contains version 10 alone. After the tag moves, the operator resolves and verifies v13, reads history, and publishes one immutable PtahMigrationPlan with the ordered sequence 011 → 012 → 013.

Resolve and Verify Jobs lead to a History Job, manager-side planning, an exact-plan decision, one Apply Job, and a fresh History Job before InSync.

Planning uses the history already read. There is no separate migration Plan Job. Approval records a decision against the published plan.

Console’s Approver view presents that single plan:

One migration plan lists versions 11, 12, and 13 in order, their checksums, and the artifact, history version, apply policy, and fingerprint.

The plan carries versions and checksums. Migration SQL remains in the artifact; Console states that the operator’s migration-plan API does not expose it.

The plan’s identity binds the observed history and ordered sequence to the artifact digest, database target, coordination identity, verification policy, apply policy, and execution binding. An approval names that exact plan and fingerprint. It does not approve a tag or future migrations.

spec.policy.apply controls the decision:

Policy What happens to the pending sequence
OnApproval (default) Publish the plan and wait for an exact-plan approval.
Always Publish the plan and proceed without a human approval.
Never Publish the plan, report ApplyDisabled, and execute nothing.

In Console, choose Approve this plan…, review the confirmation, and confirm. One decision authorizes all three migrations in this plan.

One Apply, followed by a fresh history read

Section titled “One Apply, followed by a fresh history read”

The operator starts one Apply Job for the approved sequence. Ptah takes its native migration lock, checks the selected versions with --expect-sequence, and executes the files in order. It does not create one Apply Job per CI release.

Console shows the Apply stage active for orders while fresh history verification has not completed.

Execution and verification are separate stages. The recorded plan and Job identities are retained in the supporting files.

Once Apply finishes, another History Job reads the database. That read establishes InSync:

Console reports that the database has every migration in the artifact, with Verify history complete.

The current Console view summarizes verification. It does not yet show the revision table; the migration detail work tracks that view. We read the actual table separately.

Revision Before publication After fresh verification
010 applied applied
011 absent applied
012 absent applied
013 absent applied

The original orders row remains queued. An independent CLI read confirms the final version and pending count:

Terminal window
ptah migrations status --db-url "$DATABASE_URL" --migrations-dir releases/v13

Recorded output excerpt:

examples/measured/finished-status-excerpt.txt
Current Version: 13
Total Migrations: 4
Applied Migrations: 4
Pending Migrations: 0
Out-of-order Migrations: 0
Status: ✅ Database is up to date

A successful Job exit alone would not establish this result. The database’s history must account for the selected sequence.

A newer release can replace a waiting plan

Section titled “A newer release can replace a waiting plan”

In a second copy of the database, orders-waiting first discovers v11. Its plan contains 011, and an approver opens the confirmation. Before they confirm, CI publishes cumulative v12 and v13 to the same mutable tag.

The next interval refreshes Resolve, Verify, and History before the operator consults an approval. The new digest and pending sequence produce a new plan for 011–013. Confirming the older dialog does not approve it:

Console refuses the old confirmation with It changed: a newer plan replaced the reviewed one and nothing was approved.

The dialog held the plan the person reviewed. A later plan requires a separate decision.

Following Review the new plan shows the replacement. Approval stays disabled until the person chooses I am reading the new plan:

The replacement plan contains 11, 12, and 13; Console announces The plan changed and requires the reviewer to read it before approval.

A previously admitted approval also cannot authorize a replacement plan. Admission and dispatch check the plan’s bindings, rather than treating an approval object as continuing permission.

Refresh timing matters. Publishing a new tag does not notify an already pinned plan instantly. An approval that is still valid may dispatch the older digest before the next refresh discovers the new publication. That Apply accounts for its own sequence; the later reconciliation handles what remains.

The orders-running copy approves 011 from v11. While its Apply is active, we publish v12 and v13. The active operation retains its Job and pinned digest. No SQL is appended and no second Apply starts alongside it.

After 011 is accounted for, a later Resolve observes v13. Fresh history now finds only 012 and 013 pending, so the next plan contains those versions and needs its own approval.

Before Resolve a release joins the observed batch; during approval a refresh can replace the plan; during Apply a moved tag waits for the current operation to finish.

These examples move a registry tag while the CR spec stays unchanged. Changing the CR spec during Apply is a different input change and can make result attribution uncertain. Let the active run finish before changing its resource.

The timing evidence records the active operation, and the next plan selects 012–013. The operator accounts for an active operation before starting another.

A third copy uses a deliberately broken 012: it adds shipped_at, then tries to write to a table that does not exist. PostgreSQL runs each file in a transaction. Migration 011 stays committed; the changes in 012 roll back; 013 does not run.

The revision table records the attempt as failed, with zero of two statements committed. That leaves history dirty. Console shows the refusal and the evidence retained for the failed Apply:

The failed orders run reports a dirty revision and offers database assessment rather than a retry or rollback action.

Transactional SQL rollback does not erase the failed revision record. The next Apply remains blocked until the history is repaired. The Job completed its reporting work, but its structured result still reports a failed migration; a green Job does not override that result.

Migration Recorded result Database effect
010 applied Existing orders table remains.
011 applied tracking_code exists.
012 failed / dirty shipped_at is absent after rollback.
013 absent The index was not created.

MySQL and MariaDB DDL commits as it goes. Their migrations use an explicit transactionMode: none; a file can leave earlier statements committed when a later statement fails. A partial run needs assessment of those effects, not an assumption that the file rolled back.

Account for an unknown run before continuing

Section titled “Account for an unknown run before continuing”

An interrupted result is a different problem. In orders-unknown, migration 011 sleeps before adding its column. We remove its active Apply Job while it sleeps. The operator records Unknown and an unresolved run. It does not take the missing Job as proof that SQL succeeded or rolled back.

Console shows an unresolved Unknown run and the exact-run acknowledgment action for orders-unknown.

No automatic replay follows this state. First establish what reached the database and whether any executor can still write.

For this disposable example, we confirm that the sleeping session is gone, that tracking_code was not created, and that the original row is unchanged. Only the unfinished 011 revision remains. We remove that specific unfinished record after assessment; the recovery evidence records the checks. This is a decision about this fixture’s effects, not a general repair recipe.

Console then offers Acknowledge this run…. Its confirmation makes the scope explicit:

The acknowledgment dialog names one unresolved run and states that the decision is not evidence that SQL effects were verified.

Acknowledgment settles the run record. It neither repairs SQL nor approves the next plan.

After admission records the decision, the operator reads history again. The unresolved record clears, and OnApproval leaves the pending sequence waiting for a fresh approval:

After assessment and acknowledgment, the plan for 11 through 13 waits for a fresh approval.

Here the bound inputs match the original plan, so its immutable identity is reused. The earlier approval is consumed. Another Apply still needs a fresh decision.

A clean history that accounts for every migration can also settle an unresolved run on the same database. Pending work after an uncertain run requires a person’s decision; the operator does not guess which SQL to replay.

The reason beside a stopped resource explains what prevents execution. These boundaries come from the operator’s migration workflow, plan bindings, and recovery contract.

Situation Visible state or reason Does Apply run? Next supported action
Clean pending sequence, OnApproval AwaitingApproval After approval of that exact plan. Review its sequence and bindings.
Clean pending sequence, Always Planning, then Applying Automatically for the selected plan. Follow Apply and fresh history verification.
Clean pending sequence, Never Planning / ApplyDisabled No. Change policy deliberately if execution is wanted; review the replacement binding.
Same migration files, a new build Compare the resolved digest and current plan. Only while the selected plan remains authorized. Unchanged bindings reuse the plan. A changed artifact digest changes its fingerprint even if SQL is identical; review the replacement. A Git commit outside the published artifact alone changes nothing.
012 fails after 011 Failed outcome; fresh history may report HistoryDirty 013 does not run. Assess the failed file and database, then repair the cause and revision history.
Dirty or partially applied file HistoryDirty; a partial run also retains an unresolved record No blind replay. Assess committed effects, repair deliberately, and settle any unresolved run.
Lost Job or unreadable result Unknown, unresolved run; the latest history can add another refusal No automatic replay. Establish effects and remaining writers. Acknowledge the exact run only after assessment, or let fully matched history settle it.
Late 011 after 012 HistoryOutOfOrder, named versions No. Deliberately resolve the ordering; publish a valid linear history.
Applied file checksum changed HistoryModified, named versions No. Restore the applied files or correct the artifact to match recorded history.
Database newer than the artifact HistoryAhead No rollback. Select an artifact that accounts for the database’s versions.
Existing schema, empty history Every file appears pending. Approval would execute the files again. Suspend execution and use native ptah migrations baseline with the exact files and a disposable shadow database.
Artifact contains a checkpoint History accounts for checkpoint coverage. The selected pending sequence decides. Empty databases bootstrap at the applicable checkpoint. Already migrated databases retain their history; inspect versions, not counts alone.
Another actor advances or restores history after planning Replacement history binding; an execution-time sequence mismatch reports Failed No unapproved selection executes. Read fresh history and review the new plan. Ptah checks --expect-sequence under its migration lock.
More than 256 pending migrations Plan publication refused as an operation failure No. Bring history forward through reviewed cumulative artifacts with bounded pending sequences.
Several resources address one database Shared-target refusal unless explicitly allowed; coordination Lease serializes allowed claimants. Only an authorized claimant may execute. Use a consistent coordination key or realm and explicit shared-target opt-in. Separate CRs do not merge their files or create a global version queue.

Clean history permits planning. Dirty, modified, out-of-order, or ahead history stops Apply. Partial and unknown outcomes require assessment before any acknowledgment or later plan.

An acknowledgment cannot make a dirty or mismatched history safe. The next history read still has to pass the normal guards.

Cumulative releases preserve the required migration files. The operator plans the sequence missing from the observed database and binds the decision to those inputs. Review the exact plan, let its Apply finish, and use fresh history to decide what remains.

Put Ptah to work

Example files