Skip to content

Start a new database from a checkpoint

A fresh database runs the snapshot and what came after it, and ends up where the long way ends.

What it shows. What a checkpoint replaces, who selects it, and how the operator proves the shortcut arrived at the same place.

Where it starts. An empty database with no migration history, and a namespace with the lab's Secrets, as Stand up the lab leaves them.

A replay of a verified run, not a live cluster. 8 checks held while it was recorded. Play types it out in the frame below; the transcript is the same session, already there.

sh · checkpoint-bootstrap
# Four versions, each with its down file, and one is not a migration. The name is the contract.$ ls demo/migrations-checkpoint0000000001_create_shipments.down.sql0000000001_create_shipments.up.sql0000000002_add_carrier.down.sql0000000002_add_carrier.up.sql0000000003_snapshot.checkpoint.down.sql0000000003_snapshot.checkpoint.up.sql0000000004_add_delivered_at.down.sql0000000004_add_delivered_at.up.sql # The checkpoint records where migration 2 left the column, not where migration 1 created it.$ cat demo/migrations-checkpoint/0000000003_snapshot.checkpoint.up.sql-- What migrations 1 and 2 produce between them, as one file.---- The name is the contract: `.checkpoint.` is what makes Ptah treat this as the-- floor a fresh database starts from rather than a third migration. A database-- that has already run 1 and 2 ignores it; a database that has run nothing runs-- this and then 4, and ends up indistinguishable from one that replayed-- everything.---- The column is NOT NULL here because that is where migration 2 left it. A-- checkpoint that recorded the column as it was created, before the backfill-- and the constraint, would start a new database in a state the old one had-- already left.CREATE TABLE shipments (    id INTEGER PRIMARY KEY,    reference TEXT NOT NULL,    carrier TEXT NOT NULL); # Publish it, and point a resource at the whole artifact. Nothing in it names the checkpoint.$ REF=$(ptah migrations push "oci://$PTAH_OCI_REGISTRY/migrations/demo:ckpt-$$" \  --migrations-dir demo/migrations-checkpoint --dir-format ptah --version "ckpt-$$" --plain-http --format reference)PULL="oci://$REGISTRY_IN_CLUSTER/${REF#"oci://$PTAH_OCI_REGISTRY/"}"kubectl apply -n "$NAMESPACE" -f - <<EOFapiVersion: operator.ptah.run/v1alpha1kind: PtahMigrationmetadata:  name: shipmentsspec:  target:    engine: PostgreSQL    coordinationKey: demo/storefront/primary    urlFrom: {name: demo-database, key: url}  artifact:    ociRef: $PULL    registryAuthFrom: {name: demo-registry}    verificationPolicyFrom: {name: demo-migration-verification-policy, key: policy.yaml}    transport: {plainHTTP: true}  policy:    apply: Always    lockTimeout: 30s  interval: 1mEOFptahmigration.operator.ptah.run/shipments created # Against a database with no history, under apply Always, it runs without waiting for anyone.$ kubectl ptah wait migration/shipments -n "$NAMESPACE" --for in-syncptahmigration/shipments in namespace demo reached in-sync.  Phase:      InSync  Generation: 1, observed by the operator  Database:   last read at <time>kubectl ptah migration shipments -n demo shows where it stands. # An empty history let the checkpoint stand in for 1 and 2, and 4 ran after it. Nothing is left.$ kubectl ptah migration shipments -n "$NAMESPACE"Migration:            demo/shipmentsPhase:                InSyncReady:                True, HistoryMatched: The database has every migration this artifact carriesPolicy:               apply AlwaysArtifact:             oci://e2e-registry.demo.svc.cluster.local:5000/migrations/demo@sha256:30879e709767555792d703a16b5c20dba156f9f8eaf6add0f6479f55ad54577bHistory read:         <time>Current version:      4Checkpoint:           3Applied:              4Pending:              0 Last run:             Applied, started <time>, finished <time>  Job:                ptah-m-apply-shipments-5d8bbe7ac5dfe03b, UID <uid>  Versions:           3, 4  Message:            2 migrations are recorded applied No plan is published. Approvals: none Run acknowledgments: none Conditions:  TYPE              STATUS  REASON           SINCE                 MESSAGE  Progressing       False   HistoryMatched   <time>                Nothing is pending  ArtifactVerified  True    PolicySatisfied  <time>                The resolved artifact satisfied its verification policy  Blocked           False   HistoryMatched   <time>                The history continues this artifact  Ready             True    HistoryMatched   <time>                The database has every migration this artifact carries  ApprovalRequired  False   Satisfied        <time>                The plan's approval requirements are satisfied kubectl events -n demo --for ptahmigration/shipments prints its Events. # The table is what replaying all four would have built, down to the constraint.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\d shipments"                         Table "public.shipments"    Column    |           Type           | Collation | Nullable | Default--------------+--------------------------+-----------+----------+--------- id           | integer                  |           | not null | reference    | text                     |           | not null | carrier      | text                     |           | not null | delivered_at | timestamp with time zone |           |          |Indexes:    "shipments_pkey" PRIMARY KEY, btree (id)

What was checked

Every step states what has to hold before its output may be published. A condition is read as its type, status and reason on the live object, never as a phrase in a message.

StepClaimHeld
1exits 0, prints "0000000003_snapshot.checkpoint.up.sql", prints "0000000004_add_delivered_at.up.sql"yes
2exits 0, prints "carrier TEXT NOT NULL"yes
3exits 0, prints "shipments created"yes
3ptahmigration/shipments: spec.policy.apply=Alwaysyes
4exits 0, prints "reached in-sync"yes
4ptahmigration/shipments: Ready=True (HistoryMatched)yes
5exits 0, prints "InSync", prints "Current version: 4", prints "Pending: 0", prints "Versions: 3, 4", prints "No plan is published.", never prints "CREATE TABLE", never prints "ALTER TABLE"yes
6exits 0, prints "carrier", prints "delivered_at", prints "not null"yes

What it ran against

  • Kubernetes1.37.0
  • Operator42522be3fd5a
  • Ptahv0.17.0
  • Executorsha256:181e27972091
  • Recorded2026-11-02
  • Scenarios at42522be3fd5a

What the commands read

A session is repeated, not admired, so every name in it is one your own environment can carry. There is nothing else: no helper of ours stands between a command and the cluster, and the recorder refuses a step that reads a name this list does not hold. The commands arekubectl, ptah and kubectl ptah, each of which you install once; the last one is the operator’s own client. They run from the root of a clone of this repository, which holds the files they name: the SQL under demo/schemas, demo/migrations,demo/migrations-checkpoint and demo/mysql, the reference data underdemo/reference, and the manifests under demo/manifests.

  • KUBECONFIGthe cluster the commands run against
  • NAMESPACEthe namespace the resources and their database credentials live in
  • OPERATOR_NAMESPACEwhere the chart installed the operator
  • CONTROLLERthe controller Deployment’s name, from that release
  • EXECUTOR_IMAGEthe executor image the chart runs, its executorImage value
  • REGISTRY_IN_CLUSTERthe registry address a Pod in the cluster resolves
  • PTAH_OCI_REGISTRYthe registry address the push goes to
  • PTAH_OCI_USERNAMEthe account that push uses, read by ptah itself
  • PTAH_OCI_PASSWORDits password, which no command prints

The last two are how a CI job hands ptah a registry credential. On a workstation,ptah oci login stores it once instead, reading the password from a prompt or from standard input rather than from the command line.

The registry has two addresses, the way a registry in an ordinary deployment does: a push goes to PTAH_OCI_REGISTRY, and the resource names REGISTRY_IN_CLUSTER with the same repository and digest. The registry Secret a resource names says which address its credential is for, and the operator sends it to no other.

The lab itself appears in one command, demo/bin/lab versions in Stand up the lab, which says what the lab is made of. What a run starts from is its preparation, which is not recorded; Where it starts above says what that is and which run ends there. Rundemo/acceptance/reproduce.sh to watch Approve a plan repeated from a directory where the lab’s scripts do not exist, by an account that may not create a Job, with the plan approved by a second account that may create neither a schema nor a Job, and then the same schema published with ptah schema push and moved to its next revision withkubectl ptah set-source.

The same changes, as edits

Each kubectl ptah command that changes a resource changes one field of it, so the same change is an ordinary edit, with kubectl edit ptahschema storefront -n "$NAMESPACE"or in a manifest you apply; the runs that keep their resources as files underdemo/manifests make their changes that way. What the commands add is checking: set-source takes only a reference pinned by digest, changes nothing while an Apply may be running, and changes nothing another writer changed since it read the resource.

CommandThe line it changes
kubectl ptah suspendspec.suspend: true
kubectl ptah resumespec.suspend: false
kubectl ptah set-source --ocispec.desired.ociRef of a schema, spec.artifact.ociRef of a migration
kubectl ptah refreshthe annotation operator.ptah.run/read-requested-at, set to the time of the request

An approval is not one of them. It is an object of its own, created once by the person who decided: kubectl ptah approve --dry-run client prints it as YAML for a repository, to be created with kubectl create, and never edited afterwards.

The scenario is demo/scenarios/checkpoint-bootstrap.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.