Prisma Models, Ptah Deployment Checks
Keep Prisma models and its typed client, export the desired PostgreSQL schema, and use Ptah to plan, verify, and deploy a new ticket-priority field and index.
Ptah helps you plan, review, and apply database migrations. Try in your browser
A support application uses Prisma to describe tickets and query PostgreSQL. The next release adds ticket priority and an index for the urgent queue. We can keep the Prisma model and client while using Ptah to review and execute the database change.
The connection between the tools is a SQL schema exported by Prisma’s native CLI. Ptah compares that desired state with the database, generates migrations, and checks the result after deployment. No model-conversion script is needed.
This run used Prisma 7.10.0, Ptah 0.9.0, PostgreSQL 18.6, and Node.js 26.0.0. The example files include pinned dependencies, both model versions, client code, migrations, and captured output.
Export a desired schema from the model
Section titled “Export a desired schema from the model”The initial model has an ID, subject, and status. We deployed that first schema through Ptah, generated the Prisma client, and inserted two tickets through the client. One was open; the other was closed.
The model change adds a default priority and an index:
priority String @default("normal")
@@index([status, priority], map: "tickets_status_priority_idx")Export the complete desired schema:
npx prisma migrate diff --from-empty --to-schema schema.prisma --script > schema.sql--from-empty is deliberate. This SQL describes everything the model wants,
including the table that already exists. It is input to Ptah’s comparison;
we do not execute that creation script against the existing application.
Let Ptah plan the change against the deployed database
Section titled “Let Ptah plan the change against the deployed database”DB_URL names the existing database, and SHADOW_URL names a separate,
disposable database for migration verification.
ptah migrations generate --db-url "$DB_URL" \ --schema-file schema.sql --schemas public \ --migrations-dir migrations --name ticket_priority \ --shadow-db "$SHADOW_URL"Ptah generated two migration pairs. The first adds the column in a transaction:
ALTER TABLE "tickets" ADD COLUMN "priority" TEXT NOT NULL DEFAULT 'normal';The second creates the index concurrently and declares that it must run outside a transaction:
-- +ptah no_transactionCREATE INDEX CONCURRENTLY IF NOT EXISTS "tickets_status_priority_idx" ON "tickets" ("status", "priority");The complete files include their generated metadata. The statements above are the parts to review: existing tickets receive the default, and the index has its own execution mode. The shadow check replayed the history, applied both new files, rolled them back, and reapplied them successfully.
Review, lint, and apply both files
Section titled “Review, lint, and apply both files”After reviewing the generated pairs, update the directory checksum and lint both new versions:
ptah migrations hash --dir migrationsptah migrations lint --dir migrations --dialect postgres \ --latest 2 --fail-on error --dev-url "$SHADOW_URL"ptah migrations up --db-url "$DB_URL" \ --migrations-dir migrations --verify-sumptah schema compare --db-url "$DB_URL" \ --schema-file schema.sql --schemas public --exit-codeLint replayed the directory on the shadow database, so its checks read the schema each migration starts from. It reported no findings. Ptah applied the column change in a transaction and the concurrent index outside one. The final comparison found no schema differences. A separate empty database replayed all three versions, including the initial table migration, and reached the same schema.
Use the updated Prisma client
Section titled “Use the updated Prisma client”Regenerate the client after changing the model:
npx prisma generateOur application then marked the existing open ticket as urgent and created another urgent ticket. This query returned both:
await prisma.ticket.findMany({ where: { status: "open", priority: "urgent" }, orderBy: { id: "asc" },});The results were ticket 1, “Invoice missing,” and ticket 3, “Export failed.”
The original closed ticket remained with the default normal priority.
These are actual Prisma client writes and reads against the Ptah-migrated
database.
Compare state even when history is current
Section titled “Compare state even when history is current”To test the deployment check, we deliberately dropped the index from the disposable application database. Ptah’s status still reported three applied migrations and zero pending files. Those migrations had run; their history was accurate.
Running the schema comparison again returned exit code 1 and identified
tickets_status_priority_idx as missing. Restoring the index made the same
comparison pass. A CI job can use that exit code to catch drift before an
application release.
For a deployment, compare with the currently deployed model’s exported schema before applying a new change, then compare with the incoming model’s export afterward. Comparing an old database with tomorrow’s model would also report a difference, even when nobody changed the database outside migrations.
What this adds to a Prisma application
Section titled “What this adds to a Prisma application”Prisma Migrate already provides migration generation and execution. Choosing Ptah here gives this application the same SQL review, migration linting, shadow verification, and live-schema checks used by services with other ORMs. In this run, Ptah also split the planned PostgreSQL change into transactional and concurrent-index files without a wrapper.
Prisma remains responsible for the model and typed client. Ptah owns migration execution for this database. Commit the model, exported schema, generated migrations, and checksum together so the reviewed database change travels with the client that uses it.
Put Ptah to work
Example files
- application.ts
- expected/add-index.sql
- expected/add-priority.sql
- expected/model-change.prisma
- expected/query.ts
- measured/all-rows.txt
- measured/application.txt
- measured/client-v1.txt
- measured/client-v2.txt
- measured/commands.json
- measured/compare-drift.txt
- measured/compare-fresh.txt
- measured/compare-live.txt
- measured/compare-restored.txt
- measured/drift-inject.txt
- measured/drift-restore.txt
- measured/export-v1.txt
- measured/export-v2.txt
- measured/generate-v1.txt
- measured/generate-v2.txt
- measured/hash-v1.txt
- measured/hash-v2.txt
- measured/lint-both-new.txt
- measured/lint-v2.txt
- measured/locked-application.txt
- measured/locked-client-v1.txt
- measured/locked-client-v2.txt
- measured/locked-compare.txt
- measured/locked-prisma-version.txt
- measured/locked-seed.txt
- measured/locked-up-v1.txt
- measured/locked-up-v2.txt
- measured/prisma-version.txt
- measured/seed-client.txt
- measured/status-after-drift.txt
- measured/up-fresh.txt
- measured/up-v1.txt
- measured/up-v2.txt
- migrations/1790524001_tickets.down.sql
- migrations/1790524001_tickets.up.sql
- migrations/1790524013_ticket_priority_transactional.down.sql
- migrations/1790524013_ticket_priority_transactional.up.sql
- migrations/1790524014_ticket_priority_concurrent_indexes.down.sql
- migrations/1790524014_ticket_priority_concurrent_indexes.up.sql
- migrations/ptah.sum
- package-lock.json
- package.json
- prisma.config.ts
- README.md
- schema-v1.prisma
- schema-v1.sql
- schema.prisma
- schema.sql
- seed.ts
- verified.json