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-28 15:51:19.710881205 +0000+++ demo/schemas/v2.sql 2026-09-28 15:51:19.710881205 +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:22222/schemas/demo:v2-130176Digest: sha256:6b8d1ae2653d0fb934f3d6d3e57ee54bc8d425aa611dfcd9195790cfdaee552dVersion:Tags: [v2-130176] # 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-3b5e5a45883bd276e3d4bdfb storefront sha256:3b5e5a45883bd276e3d4bdfb754da46f4c580f68f7c97fd3cfbb2728176e16a6 3 false 18sptah-plan-f8d2cf635db1802d0c1aebc3 storefront sha256:f8d2cf635db1802d0c1aebc38a656a9a0f7e2c6f066949c4fda11b45ac859066 1 false 55s # 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
- Operatorc08fc73ebbae
- Ptahf6e562c
- Executorsha256:137662e6db45
- Recorded2026-09-28
- Scenarios atc08fc73ebbae
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.