Manage reference data
Rows declared beside the tables that hold them, reconciled through the same plan.
What it shows. What a declared row set owns, how a change to rows alone reaches the database, and why removing a declaration is not a delete.
A replay of a verified run, not a live cluster. 17 checks held while it was recorded. Play types it out in the frame below; the transcript is the same session, already there.
# A declaration names the table, the column that identifies a row, and the file the rows live in.$ sed -n '/ptah:schema:table name="regions"/,/^}/p' demo/reference/v1/catalog.go//ptah:schema:table name="regions"//ptah:schema:data table="regions" key="code" file="regions.yaml"type Region struct { //ptah:schema:field name="code" type="VARCHAR(8)" primary="true" Code string //ptah:schema:field name="name" type="VARCHAR(64)" not_null="true" Name string} # The rows themselves are a list, each one a map from column name to value.$ cat demo/reference/v1/regions.yaml- code: emea name: Europe, Middle East and Africa- code: amer name: Americas # Apply it. The rows are covered by the digest, so nothing later reads the author's working copy.$ APPLY=Always demo/bin/lab manifest catalog "$(cat demo/.lab/catalog-digest)" | kubectl apply -f -ptahschema.operator.ptah.run/catalog created # The tables are created and the declared rows are in them.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "SELECT code, name FROM regions ORDER BY code" code | name------+-------------------------------- amer | Americas emea | Europe, Middle East and Africa(2 rows) # The child table exists and holds nothing. Its rows come in the next revision.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -qAt -c "SELECT count(*) FROM countries"0 # The second revision adds rows and no column. One marker, nothing else.$ diff -u demo/reference/v1/catalog.go demo/reference/v2/catalog.go || true--- demo/reference/v1/catalog.go 2026-09-17 13:40:07.290327094 +0200+++ demo/reference/v2/catalog.go 2026-09-17 13:40:07.290500137 +0200@@ -28,6 +28,7 @@ // parent rows exist, is the sequence that works today. // //ptah:schema:table name="countries"+//ptah:schema:data table="countries" key="code" file="countries.yaml" type Country struct { //ptah:schema:field name="code" type="VARCHAR(2)" primary="true" Code string # Publish it and point the resource at the new digest.$ eval "$(demo/bin/lab credentials)"export PTAH_OCI_USERNAME PTAH_OCI_PASSWORD PTAH_OCI_REGISTRYptah schema push "oci://$PTAH_OCI_REGISTRY/schemas/catalog:v2-$$" \ --root-dir demo/reference/v2 --dialect postgres --plain-http \ | sed -n 's/^Digest: //p' > demo/.lab/catalog-digestkubectl -n "$NAMESPACE" patch ptahschema catalog --type=merge \ -p "{\"spec\":{\"desired\":{\"ociRef\":\"oci://$REGISTRY_IN_CLUSTER/schemas/catalog@$(cat demo/.lab/catalog-digest)\"}}}"ptahschema.operator.ptah.run/catalog patched # An empty schema diff is no reason to skip the rest. The rows are planned and applied.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "SELECT code, name FROM countries ORDER BY code" code | name------+---------------- CA | Canada CZ | Czech Republic US | United States(3 rows) # A value edited outside the operator is drift, not a new declaration.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "UPDATE countries SET name = 'Edited by hand' WHERE code = 'US'"UPDATE 1 # The next observation sees it, plans the difference, and the apply puts the declared value back.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -qAt -c "SELECT name FROM countries WHERE code = 'US'"United States # The third revision removes the declaration on countries and keeps the table.$ diff -u demo/reference/v2/catalog.go demo/reference/v3/catalog.go || true--- demo/reference/v2/catalog.go 2026-09-17 13:40:07.290500137 +0200+++ demo/reference/v3/catalog.go 2026-09-17 13:40:07.290753846 +0200@@ -6,8 +6,7 @@ // testdata/ because a reader publishes it by typing the path, and the path is // part of what the demonstration shows. //-// Two tables joined by a foreign key, because a reference set is usually-// several tables that have to arrive in an order the constraint allows.+// The declaration on countries is gone; the table and its rows are not. package catalog //ptah:schema:table name="regions"@@ -20,15 +19,11 @@ Name string } -// The country rows arrive in the next revision rather than here. Ptah emits-// declared rows grouped by table, in an order that ignores the dependency-// order it uses for the tables themselves, so a child row can be offered-// before the parent row it references and the foreign key refuses it-// (stokaro/ptah#3252). Declaring the parent first, and the child once the-// parent rows exist, is the sequence that works today.+// This revision drops the row declaration on countries and keeps the table.+// Ending management is not deleting rows: what the operator stops doing is+// reconciling them, and what it leaves behind is exactly what was there. // //ptah:schema:table name="countries"-//ptah:schema:data table="countries" key="code" file="countries.yaml" type Country struct { //ptah:schema:field name="code" type="VARCHAR(2)" primary="true" Code string # Publish it and point the resource at it.$ eval "$(demo/bin/lab credentials)"export PTAH_OCI_USERNAME PTAH_OCI_PASSWORD PTAH_OCI_REGISTRYptah schema push "oci://$PTAH_OCI_REGISTRY/schemas/catalog:v3-$$" \ --root-dir demo/reference/v3 --dialect postgres --plain-http \ | sed -n 's/^Digest: //p' > demo/.lab/catalog-digestkubectl -n "$NAMESPACE" patch ptahschema catalog --type=merge \ -p "{\"spec\":{\"desired\":{\"ociRef\":\"oci://$REGISTRY_IN_CLUSTER/schemas/catalog@$(cat demo/.lab/catalog-digest)\"}}}"ptahschema.operator.ptah.run/catalog patched # Ending management is not deleting rows. They stay exactly as they were.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -qAt -c "SELECT count(*) FROM countries"3
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 "ptah:schema:data table=\"regions\"", prints "key=\"code\"", prints "file=\"regions.yaml\"" | yes |
| 2 | exits 0, prints "emea", prints "amer" | yes |
| 3 | exits 0, prints "created" | yes |
| 4 | ptahschema/catalog: InSync=True (ScopedConverged) | yes |
| 4 | exits 0, prints "amer", prints "Americas", prints "emea" | yes |
| 5 | exits 0, prints "0" | yes |
| 6 | exits 0, prints "+//ptah:schema:data table=\"countries\"" | yes |
| 7 | exits 0, prints "patched" | yes |
| 8 | ptahschema/catalog: InSync=True (ScopedConverged) | yes |
| 8 | exits 0, prints "CZ", prints "Czech Republic", prints "US", prints "CA" | yes |
| 9 | exits 0, prints "UPDATE" | yes |
| 10 | exits 0, prints "United States", never prints "Edited by hand" | yes |
| 11 | exits 0, prints "-//ptah:schema:data table=\"countries\"" | yes |
| 12 | ptahschema/catalog: DriftDetected=False (ScopedConverged) | yes |
| 12 | exits 0, prints "patched" | yes |
| 13 | ptahschema/catalog: InSync=True (ScopedConverged) | yes |
| 13 | exits 0, prints "3" | 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/reference-data.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.