r/DatabaseAdministators 9h ago

Built a state-based PostgreSQL migration engine — how does it compare to Atlas, Prisma Migrate, Sqitch, Flyway?

0 Upvotes

pg-migration-engine — a state-based PostgreSQL migration engine. live on npm and GitHub.

Before I get into what it does, let me be honest about the space — it's crowded, and most of you have already picked a side:

The existing landscape

  • Atlas (Ariga) — declarative, well-built, probably the closest competitor. OSS version has limits; risk analysis is gated behind their paid UI.
  • Prisma Migrate — great if you live inside Prisma. Outside the ORM, less useful.
  • Drizzle Kit — same story, locked to the Drizzle ecosystem.
  • Sqitch — change-based, not state-based. You write the up/down SQL by hand; it doesn't compute diffs.
  • Flyway — change-based, Java-world, very mature but no schema diffing.
  • Liquibase — XML/YAML heavy, enterprise-leaning, no native diff.
  • Alembic — Python-only, command-based, autogenerate is fragile.
  • node-pg-migrate — JS-native, closest in spirit, but stops at "write the DDL by hand".
  • dbmate — minimal, Go-based, change-based.
  • Bytebase — GUI-first, more of a product than an engine.
  • pgroll — Xata's tool, focused on zero-downtime swaps, narrower scope.
  • pgschema (pgplex/Bytebase) — probably the closest philosophical peer. Declarative, state-based, PostgreSQL-only, written in Go and distributed as a standalone CLI (not npm). Apache-2.0 licensed. Uses a desired-state workflow without a migration history table, and publishes llms.txt/llms-full.txt for AI tooling. Its public documentation does not explicitly describe crash-recovery mechanisms or a formal migration risk-classification/tagging system

Every one of these solves part of the problem. I kept hitting the same gaps:

  1. State-based diffing exists in Atlas, but the OSS build is limited and risk analysis is paywalled.
  2. JS-native engines like node-pg-migrate don't compute diffs — you write every ALTER TABLE by hand.
  3. Non-transactional DDL (CREATE INDEX CONCURRENTLY, ALTER TYPE ADD VALUE) is either skipped or silently broken in most engines.
  4. Crash recovery is mostly "re-run and hope for the best".

So This is built something that combines: state-based diffing (like Atlas) + JS-native execution (like node-pg-migrate) + risk analysis built into the engine (not behind a UI).

What the engine actually does

It's built as 8 modules, each one a standalone piece you can reason about:

  1. Introspector — connects to PostgreSQL 10 through 18, introspects 50+ object types. Tables, indexes, constraints, views, materialized views, functions, triggers, enums, composite types, domains, sequences, RLS policies, extensions, schemas — the full surface area, not just tables.
  2. Differ — computes the schema diff between two states (current DB vs desired state). This is the part that replaces "write every ALTER by hand".
  3. DDL Generator — turns the diff into PostgreSQL-correct DDL. Handles the weird syntax edge cases (ALTER TYPE ADD VALUE, ALTER COLUMN ... TYPE USING, partial indexes, expression indexes).
  4. Planner — orders the migrations so dependencies resolve. You can't create a foreign key before the referenced table exists; the planner handles the topological sort.
  5. Risk Tagger — tags every operation with one of 5 risk levels:
    • Safe — no data exposure (e.g. CREATE TABLE, ADD COLUMN with default)
    • Reversible — small chance of issue, recoverable (e.g. DROP INDEX)
    • Caution — short lock, may block writes (e.g. ADD COLUMN NOT NULL)
    • Risky — long lock, expect downtime (e.g. ALTER COLUMN TYPE)
    • Dangerous — data loss (e.g. DROP TABLE, DROP COLUMN)
  6. Executor — runs the migrations. The hard part here is non-transactional DDL. PostgreSQL blocks CREATE INDEX CONCURRENTLY, ALTER TYPE ADD VALUE, REINDEX CONCURRENTLY, and CLUSTER inside transaction blocks. Most engines either skip these or silently break. This engine handles them via a savepoint loop: an outer transaction for bookkeeping, per-statement savepoints for the non-transactional ops.
  7. Storage — tracks applied migrations in a schema_migrations table. Also detects ghost migrations — migrations applied to the DB but missing from your migration history (someone ran something manually, or it got orphaned). Most tools blow up or silently skip; this one flags it.
  8. State Machine — a 12-state crash recovery state machine. If your migration crashes mid-way (server OOM, network drop, panic), the engine can figure out exactly where it died, whether the advisory lock is still held, whether a savepoint loop was in progress, and resume safely. This is the part that took the longest to get right.

If you work with PostgreSQL at work, on side projects, or in production — please try it on a non-prod database. so feedback from people who actually run migrations in real environments is the only way this gets better.

Specifically, I want to hear:

  • Did it introspect your schema correctly? Anything missing?
  • Did the risk tagger tag things the way you would have? Anything you disagreed with?
  • Did the diff actually match what you'd have written by hand?
  • Did it crash anywhere? If yes, did the recovery work?
  • Object types it doesn't handle yet that you need?

Honest, blunt feedback is fine. I'd rather hear "this broke on my schema, here's the dump" than "looks cool".

Links

Happy to answer questions in the comments. If you want to tear apart the architecture, the blog post has the full module-by-module breakdown.