Skip to content
PtahOperator

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.

sh · schema-update
# 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.

StepClaimHeld
1exits 0, prints "customers", never prints "orders"yes
2exits 0, prints "+ signed_up_at timestamptz", prints "+CREATE TABLE orders"yes
3exits 0, reports "Pushed schema as oci://", reports "Digest: sha256:"yes
4exits 0, prints "patched"yes
5ptahschema/storefront: InSync=True (ScopedConverged)yes
5exits 0, prints "storefront"yes
6exits 0, prints "ALTER TABLE \"customers\"", prints "CREATE TABLE \"orders\""yes
7exits 0, prints "FOREIGN KEY (customer_id) REFERENCES customers"yes
7ptahschema/storefront: status.phase=InSync, status.applied.artifactDigest is setyes

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.