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-10-10 21:03:15.679951748 +0000+++ demo/schemas/v2.sql 2026-10-10 21:03:15.679951748 +0000@@ -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:22919/schemas/demo:v2-131340Digest: sha256:c5301515761db10b458e08d26a28d4b590d04291713cea2dd9a78db6c2f2ac9cVersion:Tags: [v2-131340] # 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 PRIVILEGES AGEptah-plan-53cd57f5ed02ae538dff14ae storefront sha256:53cd57f5ed02ae538dff14ae9c351181c248d9209927d4dc39c7abd8157fa113 1 false 97sptah-plan-78e6191054fd6817d0131770 storefront sha256:78e6191054fd6817d0131770f89db7068b9b1754cdbefc5d4b6f36a012ccc5f5 3 false 24s # 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 "orders_customer_id_fkey" 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: "orders_customer_id_fkey" 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
- Operatorb98f92bed240
- Ptahv0.13.0
- Executorsha256:f68968558f06
- Recorded2026-10-10
- Scenarios at0f7a06595d8b
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, with the plan approved by a second account that may create neither a schema nor 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.