What Is a Database Schema Plan? A Ptah Walkthrough
Follow a saved Ptah schema plan from an existing SQLite table through review, approval, application, and stale-plan recovery.
Ptah helps you plan, review, and apply database migrations. Try in your browser
The users table already holds Ada and Lin. A release needs an optional email column, but we want to inspect the database change before it runs. A schema plan gives us the proposed SQL as a file. We can review and sign that file, then apply it separately.
We ran this example with Ptah 0.17.0 and SQLite 3.51.0. The example files contain the complete inputs, commands, recorded output, and a second database for the stale-plan test.
Planning and signing produce files. Apply is the step that changes the database.
Begin with the current and desired schemas
Section titled “Begin with the current and desired schemas”Our current table has a primary key and a display name. It holds two rows:
CREATE TABLE users ( id INTEGER PRIMARY KEY, display_name TEXT NOT NULL);The desired schema adds one nullable column:
CREATE TABLE users ( id INTEGER PRIMARY KEY, display_name TEXT NOT NULL, email TEXT);For a disposable example, we create the starting table with Ptah and insert the rows with SQLite:
ptah schema apply --db-url "sqlite://$PWD/app.db" --schema-file baseline.sql --auto-approvesqlite3 app.db < seed.sqlsqlite3 -header -csv app.db 'SELECT id, display_name FROM users ORDER BY id;'The initial query returned:
id,display_name1,Ada2,LinHere, the current schema is what is in app.db. The desired schema is schema.sql. The two rows are application data, not part of either schema file. The auto-approval flag above is used only to set up this disposable database.
Save the proposed change
Section titled “Save the proposed change”Ptah compares the desired file with app.db and saves the difference:
ptah schema plan --db-url "sqlite://$PWD/app.db" --schema-file schema.sql --name add_email --output add_email.plan.jsonThe command printed:
Planned schema changes:ALTER TABLE "users" ADD COLUMN "email" TEXT;Plan saved to file://add_email.plan.jsonThe plan file records the SQL, the database dialect, a fingerprint of the starting schema, and Ptah’s assessment of the statement:
{ "format_version": 1, "name": "add_email", "dialect": "sqlite", "from_fingerprint": "sha256:d6a6edc8cd51286701cb7b10cd1c55aa8a73ebb9936e1824e751ad98fdb89c9e", "to_fingerprint": "sha256:9b36e1913a0580ae95aa6d113d0c170f073491dc3e1e200aa56553d83169eac2", "destructive": false, "statements": [ { "sql": "ALTER TABLE \"users\" ADD COLUMN \"email\" TEXT", "severity": "safe", "reason": "does not remove data or tighten constraints" } ]}The SQL adds email without rebuilding the table. The plan marks this statement safe because it does not remove data or tighten a constraint. That classification does not establish application compatibility or replace a review of the SQL.
The planned ALTER TABLE has not run yet. We read the table again after saving the plan; its schema and both rows matched the starting records byte for byte. The after-plan schema and after-plan rows are recorded beside the post.
Review and approve the file
Section titled “Review and approve the file”A reviewer should inspect the plan and decide whether its SQL is acceptable for the application. For this disposable run, we made a throwaway SSH key and listed its public half as an allowed signer:
ssh-keygen -q -t ed25519 -N '' -C 'reviewer@example.com' -f reviewer_keymkdir -p .ptahawk '{print "reviewer@example.com namespaces=\"ptah-plan\" " $1 " " $2}' reviewer_key.pub > .ptah/allowed_signersptah schema approve --plan add_email.plan.json --key reviewer_keyptah schema verify-approval --plan add_email.plan.jsonVerification reported:
Approved by reviewer@example.comPlan digest: 1112f32c66f9dd0c14c4475fb5576e8db8a9af71d60f58250964857f63e15940The signature covers the plan file’s bytes. Changing the file after signing would make the approval fail. A signature identifies the allowed key; it does not prove that a person actually read the SQL. The approval guide covers signer management and the separate signature failure cases.
Apply the approved plan, then check the database
Section titled “Apply the approved plan, then check the database”We now run the saved plan, requiring its signature:
ptah schema apply --db-url "sqlite://$PWD/app.db" --plan add_email.plan.json --require-approval --auto-approvePtah verified the signer and executed the one statement:
Plan approved by reviewer@example.comPlanned schema changes:ALTER TABLE "users" ADD COLUMN "email" TEXT;Auto-approval enabled; applying schema changes.Schema apply completed successfully.The auto-approval flag answers the interactive confirmation prompt. It does not bypass the signature check. We compared the resulting schema with schema.sql and read the existing rows:
ptah schema compare --db-url "sqlite://$PWD/app.db" --schema-file schema.sql --exit-codesqlite3 -header -csv app.db 'SELECT id, display_name, email FROM users ORDER BY id;'The comparison exited 0 and reported no difference. The two rows remain; a NULL check confirms both new email values are NULL:
id,display_name,email1,Ada,2,Lin,The plan added a column. It did not backfill email addresses.
If the database changes before apply
Section titled “If the database changes before apply”A saved plan describes a particular starting schema. To test that boundary, we created another disposable database from the same baseline and added a support_note column outside Ptah:
sqlite3 drift.db < baseline.sqlsqlite3 drift.db < seed.sqlsqlite3 drift.db 'ALTER TABLE users ADD COLUMN support_note TEXT;'ptah schema apply --db-url "sqlite://$PWD/drift.db" --plan add_email.plan.json --require-approval --auto-approveThe last command exited 2. The approval still verified, but the database no longer matched the plan’s source fingerprint:
Plan approved by reviewer@example.comerror: pre-planned migration is stale: the target database schema does not match the plan's source fingerprintThe complete refusal records both fingerprints. The email column was not added. This refusal is about the changed SQLite schema. It does not test executable-provider version binding, which is a separate check for provider-backed plans. We then asked Ptah what the old desired file would mean against this changed database:
ptah schema plan --db-url "sqlite://$PWD/drift.db" --schema-file schema.sql --name drift_recheck --dry-runThe dry-run plan was marked destructive. It included a table rebuild and DROP TABLE because schema.sql did not include support_note. Applying it would discard that independently added column. A stale-plan refusal is a reason to investigate the new state, not a reason to approve the next plan without reading it.
In this example, we decided to keep support_note. We put both columns in the desired schema:
CREATE TABLE users ( id INTEGER PRIMARY KEY, display_name TEXT NOT NULL, support_note TEXT, email TEXT);Planning again produced a new, non-destructive plan that only adds email:
ptah schema plan --db-url "sqlite://$PWD/drift.db" --schema-file schema-with-note.sql --name add_email_after_note --output drift.plan.jsonPlanned schema changes:ALTER TABLE "users" ADD COLUMN "email" TEXT;Plan saved to file://drift.plan.jsonWe inspected drift.plan.json, then signed it with the same reviewer key and allowed-signers file used above. We applied that exact plan to drift.db and checked it against the revised desired schema:
ptah schema approve --plan drift.plan.json --key reviewer_keyptah schema apply --db-url "sqlite://$PWD/drift.db" --plan drift.plan.json --require-approval --auto-approveptah schema compare --db-url "sqlite://$PWD/drift.db" --schema-file schema-with-note.sql --exit-codesqlite3 -header -csv drift.db 'SELECT id, display_name, support_note, email FROM users ORDER BY id;'Approval wrote drift.plan.json.sig beside the new plan. Apply reported
Plan approved by reviewer@example.com and completed successfully. The
comparison exited 0 with No schema differences detected. The
recorded apply output,
comparison, and
final rows show the result. The
final schema retains support_note.
The signature on add_email.plan.json covered the old plan’s bytes, so the
new plan required a separate approval.
Where a schema plan fits
Section titled “Where a schema plan fits”A saved schema plan is the proposed change between one desired schema and one observed database state. It is useful when the SQL should be reviewed before a direct schema apply. The fingerprint catches the schema change in this example. It is not a promise that application code, live traffic, or every data value will remain compatible. The direct-change guide explains the workflow and its limits.
A versioned migration is a different delivery choice: reviewed SQL lives in ordered migration files with recorded history. See our guide to reviewed migrations for that path. Ptah Operator has its own artifact and reconciliation lifecycle; our operator walkthrough follows it separately.
For this SQLite run, the practical result is specific: one approved ALTER TABLE added email, both rows remained, and a stale copy refused the old plan before we created and approved a new one.
Put Ptah to work
Example files
- baseline.sql
- diagrams/before-after.svg
- diagrams/lifecycle.svg
- measured/add_email.plan.json
- measured/add_email.plan.json.sig.txt
- measured/after-plan-rows.txt
- measured/after-plan-schema.sql
- measured/allowed_signers.txt
- measured/apply.txt
- measured/approve.txt
- measured/before-rows.txt
- measured/before-schema.sql
- measured/commands.json
- measured/compare.txt
- measured/drift-apply.txt
- measured/drift-approve.txt
- measured/drift-compare.txt
- measured/drift-final-rows.txt
- measured/drift-final-schema.sql
- measured/drift-refusal.excerpt.txt
- measured/drift-refusal.txt
- measured/drift-replan.txt
- measured/drift-revised-plan.txt
- measured/drift.plan.json
- measured/drift.plan.json.sig.txt
- measured/final-rows.txt
- measured/final-schema.sql
- measured/go-version.txt
- measured/null-check.txt
- measured/plan.txt
- measured/reviewer_key.pub.txt
- measured/setup.txt
- measured/sqlite-version.txt
- measured/verify-approval.txt
- measured/version.txt
- README.md
- schema-with-note.sql
- schema.sql
- seed.sql
- verified.json