Change the schema
A second revision, a new digest, and the difference applied as one plan.
What it shows. How a change reaches a database, and why the plan is the thing to review.
A replay of a verified run, not a live cluster. 9 checks held while it was recorded. Play types it out in the frame below; the transcript is the same session, already there.
# Where it starts -- one table, converged.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\dt" List of relations Schema | Name | Type | Owner--------+-----------+-------+--------------- public | customers | table | ptah_external(1 row) # The next revision adds a column and a table that references the first.$ diff -u demo/schemas/v1.sql demo/schemas/v2.sql || true--- demo/schemas/v1.sql 2026-09-17 13:40:07.292405935 +0200+++ demo/schemas/v2.sql 2026-09-17 13:40:07.292461477 +0200@@ -1,4 +1,11 @@ CREATE TABLE customers ( id bigint NOT NULL PRIMARY KEY,- email text NOT NULL+ email text NOT NULL,+ signed_up_at timestamptz+);++CREATE TABLE orders (+ id bigint NOT NULL PRIMARY KEY,+ customer_id bigint NOT NULL REFERENCES customers (id),+ total_cents bigint NOT NULL ); # Publish the next revision, with the same command as the first.$ eval "$(demo/bin/lab credentials)"export PTAH_OCI_USERNAME PTAH_OCI_PASSWORD PTAH_OCI_REGISTRYptah schema push "oci://$PTAH_OCI_REGISTRY/schemas/demo:v2-$$" \ --schema-file demo/schemas/v2.sql --dialect postgres --plain-http \ | tee /dev/stderr | sed -n 's/^Digest: //p' > demo/.lab/digestPushed schema as oci://127.0.0.1:24793/schemas/demo:v2-28280Digest: sha256:7a5811202614f1ef56a4875bb023db943c6c6ae1e94b72469f5bc608811196ecVersion:Tags: [v2-28280] # Point the same resource at the new digest. One field changes.$ kubectl -n "$NAMESPACE" patch ptahschema storefront --type=merge \ -p "{\"spec\":{\"desired\":{\"ociRef\":\"oci://$REGISTRY_IN_CLUSTER/schemas/demo@$(cat demo/.lab/digest)\"}}}"ptahschema.operator.ptah.run/storefront patched # A new plan, against the database as it actually is rather than as v1 left it.$ kubectl -n "$NAMESPACE" get ptahschemaplanNAME SCHEMA FINGERPRINT STATEMENTS DESTRUCTIVE AGEptah-plan-796c655f8407b565b8425f31 storefront sha256:796c655f8407b565b8425f31eb3efca67d23c76f1392983681345fb7fc802f15 1 false 64sptah-plan-8f02a03d479b50aaac989947 storefront sha256:8f02a03d479b50aaac9899476a70357d077e1b4466a067b204afba88e485784a 3 false 21s # The SQL it applied. An ALTER and a CREATE, not a second CREATE TABLE customers.$ kubectl ptah plan storefront --applied -n "$NAMESPACE" -o sql-- POSTGRES TABLE: orders --CREATE TABLE "orders" ( "id" bigint PRIMARY KEY NOT NULL, "customer_id" bigint NOT NULL, "total_cents" bigint NOT NULL);-- Add/modify columns for table: customers ---- ALTER statements: --ALTER TABLE "customers" ADD COLUMN "signed_up_at" timestamptz;-- ALTER statements: --ALTER TABLE "orders" ADD CONSTRAINT "fk_orders_customer_id" FOREIGN KEY ("customer_id") REFERENCES "customers"("id"); # And the database has both, with the foreign key the declaration asked for.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\d orders" Table "public.orders" Column | Type | Collation | Nullable | Default-------------+--------+-----------+----------+--------- id | bigint | | not null | customer_id | bigint | | not null | total_cents | bigint | | not null |Indexes: "orders_pkey" PRIMARY KEY, btree (id)Foreign-key constraints: "fk_orders_customer_id" FOREIGN KEY (customer_id) REFERENCES customers(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.
| Step | Claim | Held |
|---|---|---|
| 1 | exits 0, prints "customers", never prints "orders" | yes |
| 2 | exits 0, prints "+ signed_up_at timestamptz", prints "+CREATE TABLE orders" | yes |
| 3 | exits 0, reports "Pushed schema as oci://", reports "Digest: sha256:" | yes |
| 4 | exits 0, prints "patched" | yes |
| 5 | ptahschema/storefront: InSync=True (ScopedConverged) | yes |
| 5 | exits 0, prints "storefront" | yes |
| 6 | exits 0, prints "ALTER TABLE \"customers\"", prints "CREATE TABLE \"orders\"" | yes |
| 7 | exits 0, prints "FOREIGN KEY (customer_id) REFERENCES customers" | yes |
| 7 | ptahschema/storefront: status.phase=InSync, status.applied.artifactDigest is set | yes |
What it ran against
- Kubernetes1.37.0
- Operator8c7fe187707f
- Ptahv0.6.0
- Executorsha256:91ec0a6fa27f
- Recorded2026-09-17
- Scenarios at8c7fe187707f
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 read-only client.
- KUBECONFIGthe cluster the commands run against
- NAMESPACEthe namespace the schema and its database credentials live in
- OPERATOR_NAMESPACEwhere the chart installed the operator
- CONTROLLERthe controller Deployment’s name, from that release
- 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
- PTAH_OCI_PASSWORDits password, which no command prints
A session also uses demo/bin/lab in two places, and both are the lab handing over what it generated rather than doing the demonstration’s work:lab credentials prints the three registry values above, and lab manifestfills the published template with values the step states. Rundemo/acceptance/reproduce.sh to watch the same scenario repeated from a directory where neither exists, by an account that may not create a Job.
The scenario is demo/scenarios/schema-update.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.