# DBDiff: turns two live databases into a reversible migration file

> DBDiff is a PHP command line tool that compares a MySQL, PostgreSQL or SQLite database against another and writes the up and down SQL that turns one into the other. It is built for teams that want database schema under version control, and it ships with a runner, a YAML ignore list and support for Flyway, Liquibase and Laravel migrations.

**DBDiff/DBDiff** — Compare MySQL, Postgres or SQLite databases & automatically create schema & data change scripts/migrations automatically for database version control. Supports Flyway, Liquibase and has a built-in migration tool.

- Repository: https://github.com/DBDiff/DBDiff
- Website: https://dbdiff.dev
- Stars: 747 · Forks: 158
- Language: PHP
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/dbdiff-dbdiff

## Two databases in, one reversible SQL file out

DBDiff compares two MySQL, PostgreSQL or SQLite databases, local or remote, and writes the migration that turns one into the other. The output is schema and data changes, in both directions, in a single file that is ready to apply.

The tool is a PHP project distributed through Packagist as dbdiff/dbdiff, and it runs from the command line on Windows, Linux and macOS. Connection is by DSN URL, with the flags --server1-url, --server2-url and --db-url, so any connection string works. The database engine is selected with --driver, and Supabase users get a one-flag shorthand in --supabase.

The pitch is database version control for a team. The documentation is direct about the purpose: used alongside a compatible migration tool, it can help enable database version control within a team or enterprise. That framing matters, because DBDiff does not try to be the migration tool. It produces SQL; something else applies it.

The claim about the output is stronger than the usual generated-SQL promise. The stated goal is migrations that run, where each one applies as written and leaves the target identical to the source, and the DOWN reverts it, including changes that need several statements in the right order. The example given is a column type change under a view, or an enum losing a label.

## What actually gets diffed across the three engines

The list of compared objects is long and worth reading, because it shows how much of a PostgreSQL catalog the tool understands. It diffs tables, partitions, views, materialized views, triggers, stored procedures and functions, enum types, composite types, domains, sequences, collations, row level security policies, comments, extensions, and data. The documentation describes the output as deterministic and predictable.

That coverage is the practical reason to use a diffing tool rather than writing migrations by hand. A hand-written migration for a policy or a stored procedure is easy to get subtly wrong, and easy to forget to reverse.

Engine support is versioned. MySQL 8.0, the 8.4 LTS and current Innovation releases are supported. PostgreSQL 14 and later are supported, and CI tests every supported major version, with the test matrix in the repository listing PostgreSQL 14 through 18 and MySQL 8.0, 8.4, 9.3 and 9.6 across PHP 8.1 through 8.5. SQLite is supported through the same --driver switch. The documentation is honest that other versions may work but are not actively tested.

Two scoping switches matter for real databases. The --schemas and --ignore-schemas flags let you diff every schema of a PostgreSQL database or only the ones you choose, which is how you avoid diffing an extension schema you did not intend to touch.

## Two DDL renderers, and partitioned tables always take the built-in one

For PostgreSQL, DBDiff renders CREATE TABLE and everything hanging off it, including indexes, constraints, identity options, collations, storage and compression, in one of two ways.

The built-in renderer is always available and needs nothing installed. The pg_dump renderer is used when pg_dump and pg_restore are on PATH and at least as new as the server, and it produces PostgreSQL's own rendering of the DDL.

The distinction matters because the two produce different output for the same database, which is why the migration header records which renderer was used, in a line reading -- Renderer: pg_dump. If you want byte-identical output across machines, the documentation tells you to pin the built-in one by setting DBDIFF_PG_DUMP_RENDERER=off. The environment variables DBDIFF_PG_DUMP and DBDIFF_PG_RESTORE override the binary paths when the PostgreSQL tools live somewhere unusual.

Partitioned tables are the case where the choice is made for you, and the reason is a real bug rather than a preference. pg_dump adds a partitioned parent's primary key with ALTER TABLE ONLY, which reaches only the partitions that exist at that moment. The built-in renderer puts the key inside CREATE TABLE, where every partition inherits it whatever order the statements run in. In other words, a pg_dump-rendered migration applied to a partially populated partitioned table can leave the new partitions without the key.

The correctness claim behind all of this is tested rather than asserted. Every change to the PostgreSQL support is tested by building, diffing, applying and fingerprinting a corpus of real-world shapes on every supported version, using the @akal/pg-conformance corpus, which holds shapes drawn from PostgreSQL's own regression suite plus hand-written hard cases.

## The type change cases show where the care went

One section of the documentation concentrates on the cases that separate a tool that emits valid SQL from one that emits correct SQL, and it is the most useful part of the reference.

Changing a column's type is the hard one. PostgreSQL refuses ALTER COLUMN ... TYPE while a view, materialized view, policy, trigger condition or stored generated column reads the column, so those objects are dropped and put back around the change, including views in any schema and through any partition, with their options such as security_invoker, their grants, their comments, their INSTEAD OF triggers and their indexes. Grants come back exactly: where default privileges would widen a recreated view, they are revoked first. A generated column is recomputed from its expression so no data is lost, and a change that is not a type change, such as a default or nullability, leaves those objects alone.

String to non-string conversions get a USING clause, for example text to uuid or varchar to integer. The tool parses each value and fails loudly on one it cannot read, and it never converts towards a string type, where an explicit cast would truncate silently. That asymmetry is deliberate and is the kind of detail that separates a tool that respects your data from one that quietly loses it.

Inheritance and identity have their own rules. A column a table inherits takes its type from the parent, so the type changes once, on the parent. Identity columns are changed in place, with SET GENERATED and SET INCREMENT BY, so the sequence carries on where it was, and a column becoming an identity gets a sequence started past the values already in the column.

## A built-in runner, or hand the SQL to Flyway or Liquibase

DBDiff has a built-in migration runner with the commands migration:up, down, status, validate, repair and baseline. That covers the lifecycle most teams need without adopting anything else, and the status and validate commands answer the question that matters during an incident, which is what has been applied and whether the state is consistent.

The alternative is to let another tool own the migrations. The documentation lists compatibility with Flyway, Liquibase, Laravel Migrations and more, and the framing is that DBDiff generates the migration while the compatible tool runs it. For a team already on Flyway or Liquibase, that is the shape that fits: DBDiff becomes the generator in front of the existing pipeline rather than a replacement for it.

There is a third route that is worth knowing about if you are on Supabase. The documentation points to SupaForge, a separate project that runs DBDiff for the schema and covers the rest of a Supabase project, including row level security, storage, auth configuration, Edge Functions, cron and webhooks, with a preview before anything is applied.

Two smaller controls round out the workflow. A YAML config file lets you ignore specific tables or fields, which is the escape hatch for the tables you do not want under version control. The tool is also Unicode and UTF-8 aware, and the schema diff is described as using a constant number of round trips however many tables there are, which is the detail that makes it usable against large or remote databases.

## Release candidates, a Docker test matrix, and a custom licence file

The project is moving fast and the version numbers say so. The three most recent tags are v3.0.0-rc.29 from 2026-10-15, v3.0.0-rc.28 from 2026-10-14 and v3.0.0-rc.27 from 2026-10-12, three release candidates in four days. The repository is not archived, the default branch is master, and the last push was on 2026-10-18.

In other words, there is no stable 3.0.0 to pin yet. Anyone adopting this is adopting a release candidate, and should expect the command surface to move between versions.

The test setup is unusual for a project of this size and explains how the conformance claims are kept honest. The compose file defines a service per combination of PHP version and database version, each depending on a healthy database container and running the CLI against a mounted working copy, with the vendor directory kept out of the mount so dependencies are not shadowed by the host. The environment file shows the matrix: PHP 8.3, 8.4 and 8.5, MySQL 8.0, 8.4, 9.3 and 9.6, PostgreSQL 14 through 18, and Dolt at 2.2.3, pinned because version 2.3.0 aborts DDL at random with a context canceled error.

The version matrix is declared in the environment file, where each supported version gets its own variable and each service its own port:

```bash
MYSQL_VERSION_80=8.0
MYSQL_VERSION_84=8.4
MYSQL_VERSION_93=9.3
MYSQL_VERSION_96=9.6
POSTGRES_VERSION_14=14
POSTGRES_VERSION_15=15
POSTGRES_VERSION_16=16
POSTGRES_VERSION_17=17
POSTGRES_VERSION_18=18
```

A separate package.json exists in the repository for test tooling only, describing itself as test-only tooling that exists so the Postgres conformance harness can consume the shared schema fingerprint and corpus published to npm. DBDiff itself is a Composer project, and the top-level entry points are dbdiff and dbdiff.php, with docker-compose.yml, a compose file for the phar build, start.sh and stop.sh, and a DOCKER.md alongside.

## Conclusion

DBDiff suits a team that has outgrown hand-written migrations, works on MySQL 8, PostgreSQL 14 or newer or SQLite, and wants the diff to be generated from the databases themselves rather than from an ORM. It does not suit a project that needs a stable 3.x release today, because every recent tag is a v3.0.0 release candidate, and it does not replace a migration tool, since the generated SQL still has to be applied by Flyway, Liquibase, Laravel or the built-in runner. Before adopting it, run it once against a disposable copy of two databases, read the generated DOWN before trusting the UP, and pin the renderer with DBDIFF_PG_DUMP_RENDERER=off if you need identical output across machines.

## FAQ

### How do I install DBDiff?

DBDiff is a PHP project published on Packagist as dbdiff/dbdiff, and the repository also provides a Docker setup through docker-compose.yml, start.sh and stop.sh with instructions in DOCKER.md. The database engine is chosen at runtime with the --driver flag, and connections are made with DSN URLs through --server1-url, --server2-url or --db-url.

### Which database versions does DBDiff support?

MySQL 8.0, the 8.4 LTS and current Innovation releases, PostgreSQL 14 and later, and SQLite through the --driver switch. Other versions may work but are not actively tested, and the CI test matrix covers PHP 8.1 to 8.5 against MySQL 8.0, 8.4, 9.3 and 9.6 and PostgreSQL 14 to 18.

### Does DBDiff generate a down migration?

Yes, up and down SQL are generated in the same file. The stated goal is that each migration applies as written and leaves the target identical to the source, and that the DOWN reverts it, including changes that need several statements in the right order such as a column type change under a view.

### What is the difference between the built-in renderer and pg_dump in DBDiff?

The built-in renderer needs nothing installed, while pg_dump is used when pg_dump and pg_restore are on PATH and at least as new as the server. The migration header records which was used, and setting DBDIFF_PG_DUMP_RENDERER=off pins the built-in one for byte-identical output across machines. Partitioned tables always use the built-in renderer.

### Can DBDiff work with Flyway or Liquibase?

Yes. The documentation lists compatibility with Flyway, Liquibase, Laravel Migrations and more, with DBDiff generating the migration and the compatible tool applying it. There is also a built-in runner with the commands migration:up, down, status, validate, repair and baseline.

## Sources

- [DBDiff/DBDiff on GitHub](https://github.com/DBDiff/DBDiff)
- [License: MIT](https://github.com/DBDiff/DBDiff/blob/master/LICENSE)
- [Project website](https://dbdiff.dev)
- [README](https://github.com/DBDiff/DBDiff/blob/master/README.md)
- [Releases](https://github.com/DBDiff/DBDiff/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dbdiff-dbdiff
