From OCI Artifacts to Database Plans in Ptah Operator
Follow a schema or migration update from an OCI registry to a verified database change, with real Ptah Console plans and approval decisions.
Ptah helps you plan, review, and apply database migrations. Try in your browser
An engineer adds shipment tracking to orders. CI publishes the change, but
the database still has the old columns. Publishing an artifact and changing
a database are separate events. Ptah Operator connects them through a
Kubernetes resource, verified inputs, and a plan.
We followed the same change through a desired schema and prepared migrations
with Ptah 0.13.0, Ptah Operator 0.2.4, and PostgreSQL 17.11.
The example files use two disposable copies of the
same orders database so each workflow manages its own target. Real
Ptah Console views show the plans
and the result of verification.
Publish the desired schema or migration files
Section titled “Publish the desired schema or migration files”The delivery path starts in Git. CI publishes an OCI artifact. A Kubernetes resource selects that artifact and a database target. The operator then reconciles the resource.
The reference selects the input. The artifact bytes travel from the registry to operation Jobs. Open any diagram or screenshot to read it at full size.
The resource kind determines what those bytes mean:
| Resource | Source field | Artifact contents | What the plan compares |
|---|---|---|---|
PtahSchema |
spec.desired.ociRef |
Desired schema and declared reference data | The live database against desired state |
PtahMigration |
spec.artifact.ociRef |
Prepared, ordered migration files | Recorded revision history against the selected files |
Our desired schema adds tracking_code, shipped_at, and a tracking index:
CREATE TABLE orders ( id bigint PRIMARY KEY, status text NOT NULL, tracking_code text, shipped_at timestamp with time zone);CREATE INDEX orders_tracking_idx ON orders (tracking_code);The migration directory expresses those additions as versions 011, 012,
and 013, after version 010 created orders. The filenames pad the version
numbers with leading zeros. Both copies start with the old table and a row
whose status is queued; the migration copy has version 010 recorded.
These are the publication commands for the updated inputs:
ptah schema push "oci://$REGISTRY/orders-schema:v13" --schema-file schema.sql --dialect postgres --version v13 --tag stable --plain-httpptah migrations hash --dir migrationsptah migrations push "oci://$REGISTRY/orders-migrations:v13" --migrations-dir migrations --dir-format ptah --verify-sum --version v13 --plain-httpThe fixture uses an isolated HTTP registry, hence --plain-http. Its publishing
address is $REGISTRY; Jobs reach the same registry through
registry.delivery.svc.cluster.local:5000.
The commands use --version for write-once release tags. --tag stable
deliberately moves the schema alias.
Schema publication resolves the supplied source formats into the schema artifact’s canonical HCL. SQL, YAML, HCL, DBML, and annotated Go input are publication choices. The operator consumes the published artifact. Migration publication retains the prepared files and their checksum manifest.
OCI is the source transport exposed by both operator resources. A GitOps tool can deliver or update the Kubernetes resource. The operator does not clone Git or accept a ConfigMap, PVC, or arbitrary HTTP URL as its SQL source.
Select the input and its verification policy
Section titled “Select the input and its verification policy”Our schema resource selects the movable stable tag. This is the source
portion of the complete manifest:
spec: desired: ociRef: oci://registry.delivery.svc.cluster.local:5000/orders-schema:stable registryAuthFrom: name: registry-access mode: Environment verificationPolicyFrom: name: schema-verification key: policy.yaml transport: plainHTTP: trueThe migration resource selects the digest returned by publication. Its source
portion uses artifact, with the same separation of credentials and policy:
spec: artifact: ociRef: oci://registry.delivery.svc.cluster.local:5000/orders-migrations@sha256:572692f033d25eb6d8cc68a90ce375655796652ed9097f7c02abad96009cdb56 registryAuthFrom: name: registry-access mode: Environment verificationPolicyFrom: name: migrations-verification key: policy.yaml transport: plainHTTP: trueThe complete migration manifest also names its
target. Both resources get the database URL from spec.target.urlFrom, a
Secret reference separate from registry access.
registryAuthFrom supplies access to the selected registry. The immutable
ConfigMap named by verificationPolicyFrom says which artifacts are accepted.
Our schema policy is:
version: 1artifact_types: - application/vnd.stokaro.ptah.schema.v1The migration policy accepts
application/vnd.stokaro.ptah.migrations.v1 instead. These ConfigMaps contain
verification rules; the SQL input remains in the OCI artifact. The fixture’s
policies check artifact type. Publisher authenticity can be enforced with a
signature policy and registry access controls.
Choose how an update is discovered
Section titled “Choose how an update is discovered”Publishing a release does not make it the selected release. The reference strategy determines what needs to change:
| Selected reference | CI publishes an update | How the operator discovers it |
|---|---|---|
Mutable tag, such as :stable |
Move that tag to the new artifact | Periodic Resolve finds its new digest |
Release tag, such as :v12 |
Publish :v13 |
Update the resource’s ociRef; publishing v13 alone leaves v12 selected |
Digest, @sha256:… |
Publish another artifact | Update ociRef to select the new digest |
The operator resolves the reference it was given. It neither scans for the highest version tag nor receives registry-push notifications.
A spec update changes the Kubernetes generation and triggers reconciliation.
A tag move leaves the spec unchanged. spec.interval controls periodic
refresh: its default is 10m, and its permitted range is 10s–24h. Our
fixture uses 1m. This is a cadence; scheduling, active work, and retries
also affect when an update is discovered.
In our run, the schema detected the moved tag without a generation change.
Publishing migration v13 alone left the pinned resource on v10, with only
version 010 recorded. Updating its ociRef increased the generation and
selected the new artifact.
Resolve pins one digest. Verification and subsequent operations use those
bytes. A tag move cannot replace the SQL inside an already dispatched Apply.
When a verification policy sets require_digest_pin, the original reference
must itself name a digest; resolving a tag does not satisfy that requirement.
The target is another input. An unchanged artifact can still produce a new finding when somebody changes the database schema or revision history.
Read the schema plan
Section titled “Read the schema plan”For PtahSchema, the path is Resolve → Verify → Observe → Plan. Each step
runs as a Job. Observe uses native ptah schema drift; Plan uses native
ptah schema plan to calculate the transition from the live database now to
the selected desired state.
The runner requires independent native plans to return identical outcomes
and bytes. When there are changes, it validates the saved plan through
schema apply --plan ... --dry-run before publication. That validates the
plan through the native execution path; it does not execute the SQL against
a shadow database.
The managed scope, including exclusions, governs planning and Apply.
InSync describes convergence within that configured scope.
The schema plan contains SQL computed for the current database. Console reads and checks the stored bytes before displaying them.
PtahSchemaPlan identifies the immutable plan. Immutable
PtahSchemaPlanChunk objects hold its SQL bytes. The operator can plan
directly from the live database to a later desired-state snapshot; it does
not require the database to visit each earlier published snapshot.
Read the migration plan
Section titled “Read the migration plan”For PtahMigration, the path is Resolve → Verify → History → Plan.
The History Job runs native ptah migrations status --migrations-dir ... --json.
Ptah reports recorded history and the pending selection. The manager builds
an ordered PtahMigrationPlan from that report. This path has no separate
Plan Job and no operator-managed shadow replay.
Schema planning stores SQL. Migration planning stores the selected sequence and leaves its SQL in the verified artifact.
Our database was at 010. The selected cumulative artifact carried files
through 013. The plan selected 011 → 012 → 013:
The migration plan records versions and checksums. It does not provide a migration SQL preview.
The plan also records checkpoint and transaction metadata where applicable, the selected artifact, and the history against which the sequence was chosen. The SQL stays in that artifact.
If CI publishes several intermediate releases before reconciliation, the operator can observe the latest cumulative artifact and plan all required pending files. Skipping an unobserved artifact release does not skip a required migration. Those files must be present in the selected artifact; the operator does not retrieve missing files from older release tags.
This differs from planning directly to a later desired-schema snapshot. Prepared migrations preserve their authored execution order. Incompatible history can refuse that path; the versioned migration guide covers dirty revisions, modified files, ordering, adoption, and recovery.
Approve the bound work
Section titled “Approve the bound work”Both workflows publish a plan before considering Apply:
| Apply policy | What happens |
|---|---|
OnApproval, the default |
Wait for an approval naming this exact plan |
Always |
Proceed when the plan and other policy gates permit it |
Never |
Publish the plan without executing it |
A plan binds its content or sequence to the artifact, target, observed state or history, policies, and execution semantics. An approval authorizes that plan. Manager image and revision are recorded rather than bound, so changing only the manager image does not itself invalidate an approval.
Schema plans have additional gates. Destructive changes are disabled by
default and still require an exact-plan approval when enabled. Privilege
changes require an exact-plan approval even under Always. Those schema-plan
classification rules do not classify arbitrary SQL in prepared migrations.
Periodic refresh continues while approval is pending. A due refresh updates the evidence before Apply is considered. Changed bindings can retire the old plan and approval; unchanged bindings can retain the same immutable plan. During our Console review, a due refresh temporarily prevented approval until the resource was awaiting a decision again. The unchanged plan was retained.
We reviewed and approved each plan in Console. Schema Apply consumed the
stored plan bytes. A single migration Apply Job passed the approved sequence
to Ptah with --expect-sequence, which checks the selection under Ptah’s
migration lock before execution.
Verify the database after Apply
Section titled “Verify the database after Apply”A successful Apply process is followed by an independent database read. For the schema, fresh observation and planning establish convergence. For migrations, a fresh history read establishes what is applied and still pending.
The schema’s result is a fresh read showing no differences.
The migration’s result comes from reading its revision history again.
A separate native CLI read confirmed that history:
ptah migrations status --db-url "$MIGRATION_DB_URL" --migrations-dir migrationsExcerpt:
Current Version: 13Total Migrations: 4Applied Migrations: 4Pending Migrations: 0Out-of-order Migrations: 0Status: ✅ Database is up to dateThe direct database query recorded
010, 011, 012, and 013 as applied. Independent schema comparisons
found no differences in either copy, and the original queued row remained
with NULL values in the new columns.
Choose the resource from the input your team owns: desired state or prepared migration files. Then choose how the resource selects new artifacts. The published plan makes that selected input reviewable; the fresh database read establishes the result. The reconciliation reference and plans and approvals guide cover the lifecycle and binding rules.
For accumulated releases, stale approvals, and interrupted runs, continue with When Migration Releases Arrive Faster Than Reconciliation.
Put Ptah to work
Example files
- diagrams/delivery.svg
- diagrams/discovery.svg
- diagrams/planning.svg
- diagrams/README.md
- history.sql
- measured/approvals.json
- measured/commands.json
- measured/configure-orders-migrations-runtime-pull.stderr.txt
- measured/configure-orders-migrations-runtime-pull.txt
- measured/configure-orders-schema-runtime-pull.stderr.txt
- measured/configure-orders-schema-runtime-pull.txt
- measured/console-migration-plan.txt
- measured/console-migration-summary.txt
- measured/console-migration-verified.txt
- measured/console-schema-plan.txt
- measured/console-schema-summary.txt
- measured/console-schema-verified.txt
- measured/console-verified.txt
- measured/create-resources.stderr.txt
- measured/create-resources.txt
- measured/history-after-publication-before-reference-update.txt
- measured/history-after.txt
- measured/history-before.txt
- measured/migration-after-publication-before-reference-update.json
- measured/migration-awaiting.json
- measured/migration-baseline-hash.stderr.txt
- measured/migration-baseline-hash.txt
- measured/migration-baseline.stderr.txt
- measured/migration-baseline.txt
- measured/migration-final-history.stderr.txt
- measured/migration-final-history.txt
- measured/migration-final.json
- measured/migration-hash.stderr.txt
- measured/migration-hash.txt
- measured/migration-initial.json
- measured/migration-plan.json
- measured/migration-status-readable.stderr.txt
- measured/migration-status-readable.txt
- measured/migrations-compare.stderr.txt
- measured/migrations-compare.txt
- measured/migrations-rows.txt
- measured/migrations-v10.stderr.txt
- measured/migrations-v10.txt
- measured/migrations-v13.stderr.txt
- measured/migrations-v13.txt
- measured/operation-jobs.json
- measured/postgres-version.txt
- measured/ptah-version.stderr.txt
- measured/ptah-version.txt
- measured/schema-awaiting.json
- measured/schema-compare.stderr.txt
- measured/schema-compare.txt
- measured/schema-final.json
- measured/schema-initial.json
- measured/schema-plan-read.stderr.txt
- measured/schema-plan-read.txt
- measured/schema-plan.json
- measured/schema-rows.txt
- measured/schema-v10.stderr.txt
- measured/schema-v10.txt
- measured/schema-v13.stderr.txt
- measured/schema-v13.txt
- measured/status-excerpt.txt
- measured/update-reference.stderr.txt
- measured/update-reference.txt
- measured/verification-policies.stderr.txt
- measured/verification-policies.txt
- migration-policy.yaml
- migration-source.yaml
- migration-v10.yaml
- migration.yaml
- migrations-v10/0000000010_create_orders.down.sql
- migrations-v10/0000000010_create_orders.up.sql
- migrations-v10/ptah.sum
- migrations/0000000010_create_orders.down.sql
- migrations/0000000010_create_orders.up.sql
- migrations/0000000011_add_tracking.down.sql
- migrations/0000000011_add_tracking.up.sql
- migrations/0000000012_add_shipped_at.down.sql
- migrations/0000000012_add_shipped_at.up.sql
- migrations/0000000013_index_tracking.down.sql
- migrations/0000000013_index_tracking.up.sql
- migrations/ptah.sum
- README.md
- schema-policy.yaml
- schema-source.yaml
- schema-v10.sql
- schema.sql
- schema.yaml
- screenshots/capture.json
- screenshots/migration-plan.png
- screenshots/migration-verified-summary.png
- screenshots/migration-verified.png
- screenshots/schema-plan.png
- screenshots/schema-verified-summary.png
- screenshots/schema-verified.png
- screenshots/verified.png
- seed.sql
- source-review.md
- verification-policies.yaml
- verification.json
- verified.json



