@soartec-lab/prisma-strong-migrations
v0.2.0
Published
Detect dangerous operations in Prisma migrations
Downloads
192
Maintainers
Readme
prisma-strong-migrations
Catch unsafe migrations in development for Prisma + PostgreSQL
✓ Detects potentially dangerous operations
✓ Prevents them from being applied by default
✓ Provides instructions on safer ways to do what you want
Inspired by strong_migrations for Ruby on Rails.
Installation
npm install prisma-strong-migrations --save-dev
# or
yarn add prisma-strong-migrations --dev
# or
pnpm add prisma-strong-migrations --save-dev
# or
bun add prisma-strong-migrations --dev
# or (vite-plus)
vp add -D prisma-strong-migrationsHow It Works
When you create a migration that's potentially dangerous, you'll see an error message like:
prisma/migrations/20240320_remove_user_name/migration.sql
error [removeColumn] line 1
Removing column "name" from table "users"
❌ Bad: Removing a column may cause application errors.
If you deploy the migration before updating your application code,
requests handled by old instances will fail.
✅ Good: Follow these steps:
1. Remove all usages of 'name' field from your code
2. Run 'npx prisma generate' to update Prisma Client
3. Deploy the code changes
4. Then apply this migration with a disable comment
To skip this check, add above the statement:
-- prisma-strong-migrations-disable-next-line removeColumn
────────────────────────────────────────────────────────────
✗ 1 error
❌ Migration check failed.Comparison
| Feature | prisma-strong-migrations | squawk | Prisma built-in |
| -------------------------------------------- | ------------------------ | ------ | ------------------ |
| Prisma-specific rules | ✅ 13 rules | ❌ | ❌ |
| Auto-fix (--fix) | ✅ 6 rules | ❌ | ❌ |
| Custom rules (JS/TS) | ✅ | ❌ | ❌ |
| migrate dev / migrate deploy integration | ✅ | ❌ | ✅ |
| Inline skip with audit trail | ✅ | ❌ | ❌ |
| Total rules | 38 | ~26 | syntax errors only |
squawk is a general-purpose PostgreSQL SQL linter. It catches common dangerous patterns but has no awareness of Prisma's migration conventions — such as the implicit transaction wrapper, CONCURRENTLY requirements, or Prisma-managed tables like _AToB join tables.
Usage
Recommended Workflow
# 1. Modify your schema
vim prisma/schema.prisma
# 2. Create migration without applying
npx prisma migrate dev --create-only --name add_feature
# 3. Check the generated SQL
npx prisma-strong-migrations check
# 4a. If safe, apply the migration
npx prisma migrate dev
# 4b. If issues found:
# - Fix using the suggested safe approach, OR
# - Add disable comment if intentionalAdopting in an Existing Project
When introducing this tool to an already-running application, the first local environment setup can be painful: all existing migration files will be pending in the fresh database, and they'll trigger a flood of errors even though those migrations are already safely running in production.
Use --force to skip safety checks and apply all migrations as-is:
# Apply all existing migrations without safety checks (local setup only)
npx prisma-strong-migrations migrate dev --force
# or
npx prisma-strong-migrations migrate deploy --forceWarning:
--forcedisables all safety checks. Use it only for local development environment setup, never in production CI/CD pipelines.
Commands
All commands are available as prisma-strong-migrations <command> or the psm shorthand (e.g. npx psm migrate dev).
check [migration]
Check migration files for dangerous operations.
# Check all migrations
npx prisma-strong-migrations check
# Check a specific file
npx prisma-strong-migrations check prisma/migrations/20240320_add_index/migration.sql| Option | Description |
| ----------------------- | ------------------------------------------------------------------- |
| -f, --format <format> | Output format: console (default) or json |
| -c, --config <path> | Path to config file (default: prisma-strong-migrations.config.js) |
| --no-fail | Always exit with code 0, even if errors are found |
| --fix | Automatically rewrite auto-fixable issues in the SQL files |
The --format json output shape:
{
"errors": [
{
"ruleName": "addIndex",
"severity": "error",
"migrationPath": "prisma/migrations/...",
"line": 3,
"message": "...",
"suggestion": "..."
}
],
"warnings": [],
"totalErrors": 1,
"totalWarnings": 0
}migrate dev
Create a migration with --create-only, check it, then apply if safe. Wraps prisma migrate dev.
npx prisma-strong-migrations migrate dev| Option | Description |
| --------------------- | ---------------------------------------------------------- |
| --name <name> | Migration name, passed to Prisma |
| --schema <path> | Path to schema.prisma, passed to Prisma |
| -c, --config <path> | Path to config file |
| --fix | Auto-fix issues and exit — re-run without --fix to apply |
| --force | Skip all safety checks (local dev setup only) |
--fix workflow: --fix rewrites SQL files only — it does not apply the migration. Re-run without --fix to apply after reviewing the changes.
# Step 1: auto-fix the SQL
npx psm migrate dev --fix
# ✔ Auto-fixed 1 issue(s) in prisma/migrations/.../migration.sql
# ✅ Auto-fix applied. Run the same command again (without --fix) to apply the migration.
# Step 2: review the rewritten SQL, then apply
npx psm migrate devmigrate deploy
Check all migrations, then run prisma migrate deploy if all checks pass. Wraps prisma migrate deploy.
npx prisma-strong-migrations migrate deploy| Option | Description |
| --------------------- | --------------------------------------------- |
| -c, --config <path> | Path to config file |
| --force | Skip all safety checks (local dev setup only) |
migrate dev vs migrate deploy:
| | migrate dev | migrate deploy |
| ---------------------- | ------------------------------- | ------------------------------ |
| Intended environment | Local development | Production / staging |
| Creates migration file | Yes (via --create-only) | No (apply only) |
| Interactive prompts | Yes | No |
| Checks | Newly generated migration | All pending migrations |
| Typical usage | npx psm migrate dev --name … | npx psm migrate deploy |
init
Interactive setup wizard. Run once when introducing the tool to a project.
npx prisma-strong-migrations init- Creates
prisma-strong-migrations.config.jswith all options and defaults - Scans
package.jsonscripts and interactively offers to replaceprisma migrate dev/prisma migrate deploywith the wrapped commands
init-rule <name>
Generate a custom rule template in ./prisma-strong-migrations-rules/<name>.js.
npx prisma-strong-migrations init-rule my-ruleWhy Prisma-Specific Rules?
General-purpose SQL linters catch many dangerous patterns, but Prisma has conventions that a generic tool cannot know about:
- Implicit transactions — Prisma wraps every migration file in
BEGIN/COMMITby default. Operations that cannot run inside a transaction (e.g.CREATE INDEX CONCURRENTLY) require a-- prisma-migrate-disable-next-transactionheader, and mixing multiple statements in such a file removes rollback protection. - Prisma-managed tables — Join tables like
_CategoryToPostare fully controlled by Prisma. Direct modifications break its relation management. - ENUM recreation — PostgreSQL has no
ALTER TYPE … DROP VALUE. Prisma works around this by recreating the type, which fails if existing rows still hold the removed value. @updatedAtmanagement — Adding a DB-levelDEFAULTor trigger on an@updatedAtcolumn conflicts with Prisma Client's own update logic.
These 13 Prisma-specific rules cover what generic tools leave undetected.
Checks
Potentially dangerous operations:
- prisma-strong-migrations
- Installation
- How It Works
- Usage
- Checks
- Removing a column
- Renaming a column
- Renaming a table
- Changing the type of a column
- Adding an index non-concurrently
- Removing an index non-concurrently
- Adding a foreign key
- Adding a check constraint
- Adding a unique constraint
- Adding an exclusion constraint
- Setting NOT NULL on an existing column
- Adding a json column
- Adding a column with a volatile default value
- Adding an auto-incrementing column
- Adding a stored generated column
- Renaming a schema
- Keeping non-unique indexes to three columns or less
- Skipping Checks
- Configuration
- Custom Rules
- CI Integration
- Development
- Documentation
- Credits
- License
Best practices:
Removing a column
Bad
Removing a column may cause application errors. If you deploy the migration before updating your application code, requests handled by old application instances will fail.
ALTER TABLE "users" DROP COLUMN "name";Good
- Remove all usages of the
namefield from your application code - Run
npx prisma generateto update Prisma Client - Deploy the code changes
- Then apply this migration with a disable comment:
-- prisma-strong-migrations-disable-next-line removeColumn
-- Reason: All references removed in PR #123
ALTER TABLE "users" DROP COLUMN "name";Renaming a column
Bad
Renaming a column that's in use will cause errors in your application.
ALTER TABLE "users" RENAME COLUMN "name" TO "full_name";Good
A safer approach is to:
- Create a new column
- Write to both columns in your application
- Backfill data from the old column to the new column
- Move reads from the old column to the new column
- Stop writing to the old column
- Drop the old column
Renaming a table
Bad
Renaming a table that's in use will cause errors in your application.
ALTER TABLE "users" RENAME TO "customers";Good
A safer approach is to:
- Create a new table
- Write to both tables in your application
- Backfill data from the old table to the new table
- Move reads from the old table to the new table
- Stop writing to the old table
- Drop the old table
Changing the type of a column
Bad
Changing the type of a column causes the entire table to be rewritten. During this time, reads and writes are blocked.
ALTER TABLE "users" ALTER COLUMN "age" TYPE bigint;Some changes don't require a table rewrite and are safe in Postgres:
| Type | Safe Changes |
| -------------- | ------------------------------------------------------- |
| varchar(n) | Increasing or removing limit, changing to text |
| text | Changing to varchar with no limit |
| numeric(p,s) | Increasing precision at same scale |
| timestamp | Changing to timestamptz when session time zone is UTC |
Good
For other type changes, a safer approach is to:
- Create a new column
- Write to both columns in your application
- Backfill data from the old column to the new column
- Move reads from the old column to the new column
- Stop writing to the old column
- Drop the old column
Adding an index non-concurrently
Bad
Adding an index non-concurrently blocks writes.
CREATE INDEX "users_email_idx" ON "users"("email");Good
Add indexes concurrently. Because Prisma wraps migrations in transactions by default, you must disable the transaction for this migration file.
- Generate migration file only:
npx prisma migrate dev --create-only --name add_users_email_index - Edit the generated file — add
-- prisma-migrate-disable-next-transactionas the first line, then addCONCURRENTLY:-- prisma-migrate-disable-next-transaction CREATE INDEX CONCURRENTLY "users_email_idx" ON "users"("email"); - Apply the migration:
npx prisma migrate dev
Note:
-- prisma-migrate-disable-next-transactiondisables transaction protection for the entire file. Keep this migration file minimal — ideally one statement only.
Removing an index non-concurrently
Bad
Removing an index non-concurrently blocks writes.
DROP INDEX "users_email_idx";Good
Remove indexes concurrently.
DROP INDEX CONCURRENTLY "users_email_idx";Adding a foreign key
Bad
Adding a foreign key blocks writes on both tables.
ALTER TABLE "posts"
ADD CONSTRAINT "posts_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id");Good
Add the foreign key without validating existing rows, then validate in a separate migration.
Migration 1:
ALTER TABLE "posts"
ADD CONSTRAINT "posts_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
NOT VALID;Migration 2:
ALTER TABLE "posts"
VALIDATE CONSTRAINT "posts_user_id_fkey";Adding a check constraint
Bad
Adding a check constraint blocks reads and writes while every row is checked.
ALTER TABLE "products"
ADD CONSTRAINT "products_price_check"
CHECK (price > 0);Good
Add the check constraint without validating existing rows, then validate in a separate migration.
Migration 1:
ALTER TABLE "products"
ADD CONSTRAINT "products_price_check"
CHECK (price > 0)
NOT VALID;Migration 2:
ALTER TABLE "products"
VALIDATE CONSTRAINT "products_price_check";Adding a unique constraint
Bad
Adding a unique constraint creates a unique index, which blocks reads and writes.
ALTER TABLE "users"
ADD CONSTRAINT "users_email_unique"
UNIQUE ("email");Good
Create a unique index concurrently, then use it for the constraint.
Migration 1:
CREATE UNIQUE INDEX CONCURRENTLY "users_email_idx" ON "users"("email");Migration 2:
ALTER TABLE "users"
ADD CONSTRAINT "users_email_unique"
UNIQUE USING INDEX "users_email_idx";Adding an exclusion constraint
Bad
Adding an exclusion constraint blocks reads and writes while every row is checked.
ALTER TABLE "reservations"
ADD CONSTRAINT "reservations_no_overlap"
EXCLUDE USING gist (room_id WITH =, tsrange(start_time, end_time) WITH &&);Good
There's no safe way to add an exclusion constraint (they cannot be marked NOT VALID). Consider:
- Running during a maintenance window
- Using application-level validation instead
Setting NOT NULL on an existing column
Bad
Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL;Good
Add a check constraint first, then set NOT NULL.
Migration 1:
ALTER TABLE "users"
ADD CONSTRAINT "users_email_not_null"
CHECK ("email" IS NOT NULL)
NOT VALID;Migration 2:
ALTER TABLE "users" VALIDATE CONSTRAINT "users_email_not_null";
ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL;
ALTER TABLE "users" DROP CONSTRAINT "users_email_not_null";Adding a json column
Bad
In Postgres, there's no equality operator for the json column type, which can cause errors for SELECT DISTINCT queries.
ALTER TABLE "users" ADD COLUMN "metadata" json;Good
Use jsonb instead.
ALTER TABLE "users" ADD COLUMN "metadata" jsonb;In Prisma schema:
model User {
metadata Json @db.JsonB
}Adding a column with a volatile default value
Bad
Adding a column with a volatile default value (like gen_random_uuid() or now()) causes the entire table to be rewritten.
ALTER TABLE "users" ADD COLUMN "uuid" uuid DEFAULT gen_random_uuid();Good
Add the column without a default value, then change the default.
Migration 1:
ALTER TABLE "users" ADD COLUMN "uuid" uuid;
ALTER TABLE "users" ALTER COLUMN "uuid" SET DEFAULT gen_random_uuid();Then backfill existing rows in batches (outside a transaction):
UPDATE "users" SET "uuid" = gen_random_uuid() WHERE "uuid" IS NULL;Adding an auto-incrementing column
Bad
Adding an auto-incrementing column (SERIAL or BIGSERIAL) causes the entire table to be rewritten.
ALTER TABLE "users" ADD COLUMN "id" SERIAL;Good
Create a new table and migrate the data with the same steps as renaming a table.
Adding a stored generated column
Bad
Adding a stored generated column causes the entire table to be rewritten.
ALTER TABLE "users"
ADD COLUMN "full_name" text
GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;Good
Add a non-generated column and use triggers or application logic instead.
Renaming a schema
Bad
Renaming a schema that's in use will cause errors in your application.
ALTER SCHEMA "old_schema" RENAME TO "new_schema";Good
A safer approach is to:
- Create a new schema
- Write to both schemas in your application
- Backfill data from the old schema to the new schema
- Move reads from the old schema to the new schema
- Stop writing to the old schema
- Drop the old schema
Dropping a table
Bad
Dropping a table that's still referenced by application code will cause errors. All data is permanently lost.
DROP TABLE "users";Good
- Remove all references to the model from application code
- Run
npx prisma generateto update Prisma Client - Deploy the application code changes
- Then apply this migration with a disable comment:
-- prisma-strong-migrations-disable-next-line dropTable
-- Reason: Model removed in PR #456, all references cleaned up
DROP TABLE "users";Disabling transaction protection
Bad
Using -- prisma-migrate-disable-next-transaction disables rollback for the entire file. Mixing other DDL statements risks a partial state on failure.
-- prisma-migrate-disable-next-transaction
CREATE INDEX CONCURRENTLY "users_email_idx" ON "users"("email");
ALTER TABLE "users" ADD COLUMN "bio" text;Good
Keep the file to one statement only when disabling transactions.
-- prisma-migrate-disable-next-transaction
CREATE INDEX CONCURRENTLY "users_email_idx" ON "users"("email");Adding a NOT NULL column without a default value
Bad
Adding a NOT NULL column without a default value fails if the table has existing rows.
ALTER TABLE "users" ADD COLUMN "status" text NOT NULL;Good
Add the column with a temporary default value, then remove it if needed.
Migration 1:
ALTER TABLE "users" ADD COLUMN "status" text NOT NULL DEFAULT 'active';Migration 2 (optional — remove the default after backfilling):
ALTER TABLE "users" ALTER COLUMN "status" DROP DEFAULT;Truncating a table
Bad
TRUNCATE acquires an AccessExclusiveLock and deletes all rows. Locks propagate to foreign-key-referencing tables, and accidental execution in production is catastrophic.
TRUNCATE TABLE "users";Good
Delete rows in application code where scope is controlled:
await prisma.users.deleteMany({});Disabling triggers
Bad
DISABLE TRIGGER turns off foreign key and other constraint triggers, which can silently corrupt data integrity. If the migration fails after disabling, triggers remain off.
ALTER TABLE "users" DISABLE TRIGGER ALL;Good
Do not disable triggers in migrations. If unavoidable, always re-enable before the migration ends.
Running VACUUM inside a migration
Bad
VACUUM cannot execute inside a transaction block. Prisma wraps migrations in BEGIN/COMMIT, so this always fails and marks the migration as broken.
VACUUM ANALYZE "users";Good
Run VACUUM as a separate maintenance task outside migrations:
psql -c "VACUUM ANALYZE \"users\";"Moving a table to another tablespace
Bad
SET TABLESPACE physically relocates the table, holding an AccessExclusiveLock for the entire duration. On large tables this can block production for minutes.
ALTER TABLE "users" SET TABLESPACE pg_default;Good
Run this operation in a scheduled maintenance window, not in a regular migration.
Clustering a table
Bad
CLUSTER physically rewrites the table in index order, holding an AccessExclusiveLock throughout. On large tables this blocks all reads and writes for a long time.
CLUSTER "users" USING "users_pkey";Good
Use pg_repack to reorder rows without an exclusive lock, or run CLUSTER during a low-traffic maintenance window.
Creating a table from a SELECT query
Bad
CREATE TABLE AS SELECT copies all rows inside the migration. On large tables this takes a long time and can cause timeouts.
CREATE TABLE "users_backup" AS SELECT * FROM "users";Good
Run data copies outside migrations using pg_dump or a separate backfill job.
Keeping non-unique indexes to three columns or less
Bad
Adding a non-unique index with more than three columns rarely improves performance.
CREATE INDEX "users_multi_idx" ON "users"("a", "b", "c", "d");Good
Start an index with columns that narrow down the results the most.
CREATE INDEX CONCURRENTLY "users_idx" ON "users"("d", "b");Using CONCURRENTLY without disabling the transaction
Bad
Prisma wraps every migration in a transaction. PostgreSQL does not allow CONCURRENTLY operations inside a transaction block, so the migration will fail at runtime.
CREATE INDEX CONCURRENTLY "idx_users_email" ON "users"("email");Good
Add -- prisma-migrate-disable-next-transaction as the first line of the file and keep the file to one statement:
-- prisma-migrate-disable-next-transaction
CREATE INDEX CONCURRENTLY "idx_users_email" ON "users"("email");Adding NOT VALID and VALIDATE CONSTRAINT in the same file
Bad
NOT VALID is meant to defer the expensive table scan to a later VALIDATE CONSTRAINT step. Putting both in the same file negates the optimization — the full table scan still occurs.
ALTER TABLE "orders" ADD CONSTRAINT "orders_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") NOT VALID;
ALTER TABLE "orders" VALIDATE CONSTRAINT "orders_user_id_fkey";Good
Split into two separate migration files:
-- migration_1.sql
ALTER TABLE "orders" ADD CONSTRAINT "orders_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") NOT VALID;
-- migration_2.sql (deploy after migration_1)
ALTER TABLE "orders" VALIDATE CONSTRAINT "orders_user_id_fkey";Multiple statements with disabled transaction
Bad
When -- prisma-migrate-disable-next-transaction is present, the whole file runs without a transaction. If any statement fails, earlier statements cannot be rolled back.
-- prisma-migrate-disable-next-transaction
CREATE INDEX CONCURRENTLY "idx_a" ON "users"("email");
ALTER TABLE "users" ADD COLUMN "name" text;Good
Keep files with a disabled transaction to one SQL statement only:
-- prisma-migrate-disable-next-transaction
CREATE INDEX CONCURRENTLY "idx_a" ON "users"("email");UPDATE without WHERE clause
Bad
An UPDATE without a WHERE clause updates every row in the table, which can lock the table for a long time on large datasets.
UPDATE "users" SET "status" = 'active';Good
Add a WHERE clause to limit the affected rows:
UPDATE "users" SET "status" = 'active' WHERE "status" IS NULL;DELETE FROM without WHERE clause
Bad
A DELETE FROM without a WHERE clause deletes every row in the table.
DELETE FROM "sessions";Good
Add a WHERE clause to limit the deleted rows:
DELETE FROM "sessions" WHERE "expires_at" < NOW();Mixing schema changes and data backfill
Bad
Combining ALTER TABLE schema changes with UPDATE backfill in one migration can cause long-running locks on large tables.
ALTER TABLE "users" ADD COLUMN "full_name" text;
UPDATE "users" SET "full_name" = first_name || ' ' || last_name;Good
Split into two separate migration files:
-- migration_1.sql: schema change only
ALTER TABLE "users" ADD COLUMN "full_name" text;
-- migration_2.sql: backfill only
UPDATE "users" SET "full_name" = first_name || ' ' || last_name;Removing an ENUM value
Prisma recreates the ENUM type when removing a value. If existing data contains the removed value, the migration will fail.
-- ❌ Bad: will fail if existing rows have the removed value
ALTER TYPE "Role" RENAME TO "Role_old";
CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER');
ALTER TABLE "User" ALTER COLUMN "role" TYPE "Role" USING "role"::text::"Role";
DROP TYPE "Role_old";-- ✅ Good: backfill data before removing the enum value
UPDATE "User" SET "role" = 'ADMIN' WHERE "role" = 'MEMBER';
-- then apply the migration after deploying code that no longer references MEMBERCreating an implicit M2M join table
Prisma generates _XToY tables for implicit M2M relations. These tables are limited to columns A and B, cannot hold extra fields, and use opaque naming. Explicit M2M models are clearer and more flexible.
Bad
-- Prisma generates this for implicit M2M — columns A and B only
CREATE TABLE "_CategoryToPost" (
"A" integer NOT NULL,
"B" integer NOT NULL
);Good
-- ✅ Convert to explicit M2M in schema.prisma
model CategoryOnPost {
post Post @relation(fields: [postId], references: [id])
postId Int
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
assignedAt DateTime @default(now())
@@id([postId, categoryId])
}Directly modifying an implicit M2M table
Prisma auto-manages join tables named _AToB. Direct modifications break Prisma's relation management.
-- ❌ Bad: bypasses Prisma's M2M management
ALTER TABLE "_CategoryToPost" ADD COLUMN "extra" TEXT;-- ✅ Good: convert to explicit M2M in schema.prisma
model CategoriesOnPosts {
postId Int
categoryId Int
post Post @relation(fields: [postId], references: [id])
category Category @relation(fields: [categoryId], references: [id])
@@id([postId, categoryId])
}Using SERIAL (32-bit) for a primary key
SERIAL has a maximum of ~2.1 billion rows. Migrating to BigInt later is nearly impossible in production.
-- ❌ Bad: 32-bit integer, max ~2.1 billion rows
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
...
);-- ✅ Good: use BigInt or UUID v7 in schema.prisma
model User {
id BigInt @id @default(autoincrement())
-- or
id String @id @default(uuid(7))
}Dropping the default from an id column
Dropping a database-level default from the id column can break ID generation for inserts that bypass Prisma Client.
-- ❌ Bad: breaks ID generation if the column relies on a DB default
ALTER TABLE "User" ALTER COLUMN "id" DROP DEFAULT;-- ✅ Good: change ID strategy in schema.prisma and let Prisma regenerate
model User {
id String @id @default(uuid(7))
}Setting a DB-level default or trigger on @updatedAt
Prisma's @updatedAt manages the column automatically. Adding a DB-level default or trigger conflicts with Prisma's updates.
-- ❌ Bad: conflicts with Prisma's @updatedAt management
ALTER TABLE "User" ALTER COLUMN "updatedAt" SET DEFAULT NOW();
CREATE TRIGGER set_updated_at BEFORE UPDATE ON "User"
FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at();-- ✅ Good: let Prisma manage it exclusively via @updatedAt
model User {
updatedAt DateTime @updatedAt
}Skipping Checks
If you've reviewed the warning and want to proceed anyway, add a disable comment:
-- prisma-strong-migrations-disable-next-line removeColumn
-- Reason: Column deprecated, no references found
ALTER TABLE "users" DROP COLUMN "name";Skip multiple rules
-- prisma-strong-migrations-disable-next-line removeColumn renameColumn
ALTER TABLE "users" DROP COLUMN "name";Skip all rules for a statement
-- prisma-strong-migrations-disable-next-line
ALTER TABLE "users" DROP COLUMN "name";Configuration
Create prisma-strong-migrations.config.js in your project root:
module.exports = {
// Disable specific rules globally
disabledRules: ["indexColumnsCount"],
// Skip specific migrations (matched by substring)
ignoreMigrations: ["20240101_initial"],
// Custom rules directory
customRulesDir: "./prisma-strong-migrations-rules",
// Directory to scan for migration files
migrationsDir: "./prisma/migrations",
// Treat warnings as errors
warningsAsErrors: false,
// Exit non-zero when warnings are found
failOnWarning: false,
// Exit non-zero when errors are found
failOnError: true,
};Configuration options
| Option | Type | Default | Description |
| ------------------ | ---------- | ------------------------------------ | ----------------------------------------- |
| disabledRules | string[] | [] | Rule names to disable globally |
| ignoreMigrations | string[] | [] | Migration names to skip (substring match) |
| customRulesDir | string | "./prisma-strong-migrations-rules" | Directory containing custom rule files |
| migrationsDir | string | "./prisma/migrations" | Directory to scan for migration files |
| warningsAsErrors | boolean | false | Treat warnings as errors (exit non-zero) |
| failOnWarning | boolean | false | Exit non-zero when warnings are found |
| failOnError | boolean | true | Exit non-zero when errors are found |
Custom Rules
Create custom rules for your project-specific needs. See docs/RULES.md for details.
CI Integration
Add a check step to your pull request workflow so unsafe migrations are caught before they reach production.
GitHub Actions
name: Migration Check
on:
pull_request:
paths:
- "prisma/schema.prisma"
- "prisma/migrations/**"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm ci
- run: npx psm checkUse --format json to parse results in custom scripts or post findings as pull request comments. See docs/WORKFLOW.md for more CI/CD integration examples.
Development
Using devcontainer (Recommended)
This project uses Docker + devcontainer for development.
# Open in VSCode
# 1. Open the project in VSCode
# 2. Command Palette (Cmd+Shift+P) → "Dev Containers: Reopen in Container"Available Commands
vp install # Install dependencies
vp test # Run tests
vp check # Run lint, format, and type checks
vp pack # Build library for publishingDocumentation
- Design Document - Architecture and implementation details
- Rules Reference - Detailed explanation of each rule
- Testing Strategy - Unit and integration testing
- Workflow Guide - Development workflow and CI/CD
- Agent Guidelines - Guidelines for AI agents
Credits
Inspired by strong_migrations by Andrew Kane.
License
MIT
