Zero-dependency PostgreSQL DDL migration supervisor & linter for Node.js / TypeScript.
Eliminates lock queues, connection pool exhaustion, and deploy deadlocks across Prisma, Drizzle, and raw SQL migrations.
Drop ddlforge wrap directly in front of your ORM migration commands:
# Supervise Prisma deployments
npx ddlforge wrap -- npx prisma migrate deploy
# Supervise Drizzle migrations
npx ddlforge wrap -- npx drizzle-kit migrate- Auto-detects migration folders: Discovers prisma/migrationsordrizzledirectory layouts (or uses--diroverride).
- Filters pending migrations: Compares against database schema history tables (_prisma_migrationsor__drizzle_migrationswhen--db/DATABASE_URLis accessible), or verifies all candidate files if--dbis omitted.
- Pre-flight safety interception: Evaluates all pending migrations with zero-downtime rules. If any BLOCKERhazard is detected, it outputs rich diagnostics with remediation recipes and aborts deployment before running the child process (exit code1).
- Execution delegation: When pre-flight safety checks pass, it spawns your migration command with inherited stdio (stdio: 'inherit') and cleanly propagates the child process's exit code.
In PostgreSQL, DDL commands take table-level locks. Even a sub-millisecond migration can cause a catastrophic production outage:
┌───────────────────────────── The Migration Lock Trap ──────────────────────────────┐
│ │
│ 1. Long SELECT holds ACCESS SHARE lock on "users" │
│ 2. ALTER TABLE requests ACCESS EXCLUSIVE lock │
│ └─ BLOCKED: Waits behind the SELECT │
│ 3. Subsequent web queries (SELECT, INSERT, UPDATE) arrive │
│ └─ BLOCKED: Queue behind the waiting ALTER TABLE! │
│ 4. Connection pool saturates within 5–15 seconds │
│ └─ Outage: All application endpoints return 504 Gateway Timeout │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘
ddlforge eliminates this risk at every phase:
- In CI: ddlforge checkfails pull requests before dangerous DDL reachesmain.
- In ORM deployments: ddlforge wrapintercepts pending migrations and aborts before running unvalidated changes.
- In direct execution: ddlforge applyinjects statement lock-timeouts, retries with full jitter, and runs a background avalanche-breaker.
Supervises external migration commands by running pre-flight safety checks against pending migration files before delegating execution.
ddlforge wrap [options] -- <command...>
ARGUMENTS:
<command...> Migration command to execute if safety checks pass
FLAGS:
--dir <path> Migration directory (auto-detects prisma/migrations, drizzle, or ./migrations)
--db <url> Database URL for pending migration comparison (falls back to DATABASE_URL)
--allow-blockers Warn on blockers instead of aborting the command
--help, -h Print wrap help and exit
# Supervise standard Prisma deployment
npx ddlforge wrap -- npx prisma migrate deploy
# Supervise Drizzle deployment with explicit directory
npx ddlforge wrap --dir=./drizzle -- npx drizzle-kit migrate
# Allow blockers in emergency deploys (warns without aborting)
npx ddlforge wrap --allow-blockers -- npx prisma migrate deployStatically parses SQL files with a zero-dependency, single-pass lexer and AST analyzer. Emits human-readable terminal output, JSON, GitHub Markdown, or SARIF 2.1.0.
ddlforge check [paths...] [flags]
ARGUMENTS:
paths Migration files or directories (e.g. ./prisma/migrations, ./drizzle)
FLAGS:
--format <type> Output format: pretty | terminal | json | markdown | sarif (default: pretty)
--output <path> Write report directly to a file (ideal for SARIF or Markdown step summaries)
--pg <version> Target PostgreSQL version (default: 16)
--quiet, -q Show blockers only; suppress warnings and advisories
--changed-only Use git diff to lint only files modified in this branch / PR
--version, -v Print ddlforge version and exit
--help, -h Print check help and exit
# Scan Prisma migrations directory
npx ddlforge check ./prisma/migrations
# Lint only changed files in current PR
npx ddlforge check --changed-only
# Generate SARIF 2.1.0 report for GitHub Code Scanning
npx ddlforge check "prisma/migrations/**/*.sql" --format=sarif --output=ddlforge.sarif
# Export JSON summary for custom CI tooling
npx ddlforge check ./drizzle --format=json --output=report.jsonExecutes raw SQL migration files against live databases with automated safeguards:
- Per-statement timeout injection: Automatically injects session lock_timeoutandstatement_timeout.
- Non-transactional CONCURRENTLYsplitting: Automatically detects statements forbidden inside transactions (CREATE/DROP INDEX CONCURRENTLY,REINDEX CONCURRENTLY,VACUUM) and runs them outsideBEGIN...COMMITwith session-levelSET/RESETtimeouts.
- Exponential backoff with full jitter: Randomizes retry intervals on lock errors (55P03/57014) to prevent retry stampedes.
- Background lock-queue monitor: Continuously polls pg_lockson an isolated connection and executespg_cancel_backendif queue depth reaches threshold.
ddlforge apply <file.sql> --db <DATABASE_URL> [flags]
ARGUMENTS:
<file.sql> SQL migration file to execute
FLAGS:
--db <url> PostgreSQL connection URL (required, or set DATABASE_URL)
--lock-timeout <ms> Per-statement lock_timeout in ms (default: 3000ms)
--statement-timeout <ms> Per-statement statement_timeout in ms (default: 30000ms)
--max-retries <n> Max retry attempts on lock timeout (default: 5)
--dry-run Parse and display statements without executing
--lock-queue-threshold <n> Blocked queries behind migration before aborting (default: 1)
--monitor-poll-ms <ms> Lock-monitor polling interval in ms (default: 500)
--help, -h Print apply help and exit
npx ddlforge apply ./migrations/001_add_index.sql \
--db "$DATABASE_URL" \
--lock-timeout 2500 \
--statement-timeout 60000 \
--max-retries 5 \
--lock-queue-threshold 1-- ❌ Dangerous: Table scan under ACCESS EXCLUSIVE blocks all queries
ALTER TABLE orders ADD CONSTRAINT check_amt_positive CHECK (amount > 0);
-- ✅ Safe: Two-phase non-blocking validation
ALTER TABLE orders ADD CONSTRAINT check_amt_positive CHECK (amount > 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT check_amt_positive;-- ❌ Dangerous: SHARE lock blocks write traffic during index creation
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE (email);
-- ✅ Safe: Build index concurrently first, then attach instantly
CREATE UNIQUE INDEX CONCURRENTLY uq_users_email_idx ON users (email);
ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE USING INDEX uq_users_email_idx;-- ❌ Dangerous: Session locks leak across PgBouncer connection pools
SELECT pg_advisory_lock(98765);
-- ✅ Safe: Transaction-scoped lock automatically releases on COMMIT or ROLLBACK
SELECT pg_advisory_xact_lock(98765);-- ❌ Dangerous: Prisma creates DROP + ADD on field renames, wiping data
ALTER TABLE "users" DROP COLUMN "full_name";
ALTER TABLE "users" ADD COLUMN "name" TEXT NOT NULL;
-- ✅ Safe: Rename in place without data loss
ALTER TABLE "users" RENAME COLUMN "full_name" TO "name";Fails the PR if any lock blockers are introduced, and posts the report to the GitHub Step Summary.
name: Migration Safety Gate
on:
pull_request:
paths:
- '**/migrations/**'
- '**/*.sql'
jobs:
ddlforge-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Lint changed migrations
run: npx ddlforge check --changed-only --format=terminal
- name: Post Markdown summary to PR
if: always()
run: npx ddlforge check --changed-only --format=markdown >> $GITHUB_STEP_SUMMARYGenerates SARIF 2.1.0 results and uploads them to GitHub Code Scanning for inline pull request annotations.
name: Migration Code Scanning
on:
pull_request:
paths:
- '**/migrations/**'
- '**/*.sql'
push:
branches: [main]
jobs:
ddlforge-sarif:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Run ddlforge SARIF inspection
run: npx ddlforge check "prisma/migrations/**/*.sql" --format=sarif --output=ddlforge.sarif
continue-on-error: true # Upload SARIF annotations even when blockers are present
- name: Upload SARIF report to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ddlforge.sarif
category: ddlforge-migrationsSupervises production migration execution with automatic pre-flight verification:
name: Production Deployment
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Supervised Prisma Deployment
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npx ddlforge wrap -- npx prisma migrate deploySuppress specific rules on individual statements when intentional:
-- ddlforge-ignore require-concurrent-index
CREATE INDEX idx_scratch ON scratch_buffer (temp_id);
-- ddlforge-ignore unbatched-dml
UPDATE app_config SET maintenance_mode = true;Disable all checks for a specific statement:
-- ddlforge-ignore
ALTER TABLE legacy_import ADD COLUMN meta_data JSONB;Disable Prisma implicit transaction wrapper (file-level):
-- prisma:no-transaction
CREATE INDEX CONCURRENTLY "users_email_idx" ON "users"("email");import { analyzeSql, formatTerminal, formatSarif } from 'ddlforge';
const result = analyzeSql(`
ALTER TABLE users ADD CONSTRAINT uq_email UNIQUE (email);
`, {
filePath: 'migrations/001.sql',
pgVersion: 16,
});
if (result.hasBlockers) {
console.log(formatTerminal([result]));
process.exit(1);
}import { orchestrateWrap } from 'ddlforge';
const result = await orchestrateWrap({
dir: './prisma/migrations',
databaseUrl: process.env.DATABASE_URL,
allowBlockers: false,
command: ['npx', 'prisma', 'migrate', 'deploy'],
});
process.exit(result.exitCode);ddlforge/
├── bin/
│ └── ddlforge.ts # Global executable CLI entrypoint
├── src/
│ ├── lexer/
│ │ ├── sqlTokenizer.ts # Zero-dependency single-pass SQL lexer
│ │ └── tokens.ts # Token & Statement definitions
│ ├── engine/
│ │ ├── analyzer.ts # Static rule pipeline orchestrator
│ │ └── locks.ts # Postgres lock levels & conflict taxonomy
│ ├── rules/ # 10 zero-downtime safety rules
│ │ ├── indexConcurrently.ts
│ │ ├── transactionTrap.ts
│ │ ├── addColumnNotNull.ts
│ │ ├── foreignKeyNotValid.ts
│ │ ├── checkConstraintNotValid.ts
│ │ ├── uniqueConstraintUsingIndex.ts
│ │ ├── prismaRenameDropAdd.ts
│ │ ├── setNotNullFullScan.ts
│ │ ├── unbatchedBackfill.ts
│ │ └── sessionAdvisoryLock.ts
│ ├── wrapper/ # wrap — ORM supervisor orchestrator
│ │ └── orchestrator.ts # Pre-flight interceptor & child delegation
│ ├── runner/ # apply — runtime execution supervisor
│ │ ├── backoff.ts # Exponential backoff with full jitter
│ │ ├── locksMonitor.ts # pg_locks background queue-avalanche breaker
│ │ └── executor.ts # Non-transactional branching & timeouts
│ ├── reporters/
│ │ ├── terminal.ts # Colorized human-readable report
│ │ ├── json.ts # Machine-readable summary & findings
│ │ ├── markdown.ts # GitHub Step Summary & PR comments
│ │ └── sarif.ts # SARIF 2.1.0 for GitHub Code Scanning
│ └── cli.ts # CLI argument parser, check/apply/wrap dispatch
└── test/
├── analyzer.test.ts # Lexer, core rules & CLI tests
├── linter.test.ts # v0.3.0 rules & fixture verification tests
├── reporter.test.ts # SARIF 2.1.0 schema & reporter tests
├── runner.test.ts # Backoff, lock monitor, executor tests
└── wrapper.test.ts # ORM layout detection, pre-flight abort tests
npm test✔ 132 tests passed across 31 suites (0 failures)
✔ analyzes 100 migrations in under 15 milliseconds
MIT © x7sss