DBDiff
DBDiff is an open-source PHP project with 739 GitHub stars. It is associated with migration, migrations, mysql, mysql-database. The repository has seen commits within the last year.
MITPermissive — free to use in commercial and proprietary software, with attribution.View license →
Production readiness
3/5- Actively maintainedCommits in the last 6 months
- No known vulnerabilitiesNot yet scanned
- Clear, usable licenseMIT (permissive)
- Proven adoptionSome adoption
- Has documentationDocumentation indexed
composer require dbdiffFeatures
Compares two databases (local or remote) and generates SQL migrations automatically
Supports MySQL, PostgreSQL, and SQLite via
--driverConnect via DSN URLs (
--server1-url,--server2-url,--db-url) — works with any connection stringSupabase-ready via
--supabaseone-flag shorthand (not required when using DSN URLs)Diffs tables, views, triggers, stored procedures/functions, enum types, and data — with deterministic, predictable output
Up and down SQL generated in the same file
Built-in migration runner:
migration:up,down,status,validate,repair,baselineIgnore specific tables or fields via a YAML config file
Unicode / UTF-8 aware
Fast — tested on databases with millions of rows
Runs on Windows, Linux and macOS (command-line / Terminal)
Supported Databases
Other versions may work but are not actively tested. PRs to add official support are welcome.
MySQL
VersionStatusMySQL 8.0.x✅ SupportedMySQL 8.4.x (LTS)✅ SupportedMySQL 9.3.x (Innovation)✅ SupportedMySQL 9.6.x (Innovation)✅ Supported
PostgreSQL
Use --driver=pgsql (or driver: pgsql in your .dbdiff config).
VersionStatusPostgreSQL 14.x✅ SupportedPostgreSQL 15.x✅ SupportedPostgreSQL 16.x (LTS)✅ SupportedPostgreSQL 17.x✅ SupportedPostgreSQL 18.x✅ Supported
SQLite
Use --driver=sqlite. The file path is passed as the database name:
./dbdiff --driver=sqlite server1./path/to/source.db:server1./path/to/target.db
SQLite 3.x is supported (any version supported by the installed pdo_sqlite PHP extension).
Supabase
--supabase sets driver=pgsql and enables SSL automatically:
./dbdiff --supabase --server1=user:pass@db.xxx.supabase.co:5432 server1.mydb:server1.mydb
Compatible Database Variants
The databases below work with DBDiff's existing drivers with no code changes. Unless otherwise noted, these have not been tested by the core team. PRs to add official support are welcome.
MySQL-compatible — --driver=mysql (default)
DatabaseNotesMariaDB 10.x / 11.xMySQL wire protocol; minor DDL dialect differencesAWS Aurora MySQLStandard MySQL protocolPlanetScaleMySQL-compatible SaaSVitess / VTGateMySQL wire protocol via VTGatePercona XtraDB ClusterMySQL-compatible; Galera replication metadata ignoredTiDBMySQL-compatible; default port 4000DoltMySQL-compatible, version-controlled; CI-tested
PostgreSQL-compatible — --driver=pgsql
DatabaseNotesAWS Aurora PostgreSQLStandard pgsql connectionAWS RDS PostgreSQLStandard pgsql connectionNeonStandard pgsql; supports branch diffing (see below)AlloyDB (Google Cloud)Google's Postgres-compatible offeringCockroachDBPostgres wire protocol; some DDL differencesYugabyteDBPostgres-compatible YSQL layerMultigresTransparent Postgres proxy; no changes neededTimescaleDBPostgres extension; hypertable DDL diffs nativelypgvectorvector(N) columns and HNSW/IVFFlat indexes diff natively
Neon Branching
Neon's copy-on-write branching lets you diff any two branches directly:
./dbdiff \
--server1-url postgres://user:pass@main-branch.hostname.neon.tech/mydb \
--server2-url postgres://user:pass@feature-branch.hostname.neon.tech/mydb \
--format=flyway --description=my_feature
Dolt (Git for Databases)
Dolt is a MySQL-compatible database with Git-style branching. Each branch is exposed as a separate database:
./dbdiff server1.main:server1.feature_add_users
Installation
The quickest way to get started is to download a pre-built release directly from GitHub Releases — no PHP, Node, or Composer required:
MethodAvailable on Releases?Best forPre-built binary✅ YesQuickest start — zero dependenciesPHAR✅ YesSingle portable file; requires PHP ≥ 8.1npm✅ Yes (via registry)Node.js projects or CI pipelinesDocker / Podman—Isolated environments, CI, or no local PHPComposer (source)—Contributing to DBDiff or PHP integration
PHP requirement: Pre-built binaries, npm packages, and Docker/Podman images bundle PHP 8.3 — no system PHP needed. The PHAR and Composer installs require PHP ≥ 8.1 on your system.
Pre-built Binaries
Download from GitHub Releases. No PHP, Node, or Composer required.
PlatformAssetLinux x64 (glibc)dbdiff-linux-x64Linux x64 (Alpine / musl)dbdiff-linux-x64-muslLinux arm64 (glibc)dbdiff-linux-arm64Linux arm64 (Alpine / musl)dbdiff-linux-arm64-muslmacOS Apple Silicondbdiff-darwin-arm64macOS Inteldbdiff-darwin-x64Windows x64dbdiff-win32-x64.exeWindows arm64dbdiff-win32-arm64.exe
After downloading, make it executable (Linux/macOS) and optionally move it to your PATH:
chmod +x dbdiff-linux-x64
sudo mv dbdiff-linux-x64 /usr/local/bin/dbdiff
dbdiff --version
npm
npm install -g @dbdiff/cli
dbdiff --version
The correct platform binary is selected automatically at install time. Supported: Linux x64/arm64 (glibc + musl), macOS x64/arm64, Windows x64/arm64.
Packages are also published to GitHub Packages as a mirror. If npmjs.org is unavailable:
npm install -g @dbdiff/cli --registry=https://npm.pkg.github.com
PHAR
Download dbdiff.phar from GitHub Releases. Requires PHP ≥ 8.1.
chmod +x dbdiff.phar
sudo mv dbdiff.phar /usr/local/bin/dbdiff
dbdiff --version
To build a PHAR locally from source, see Building a PHAR.
Docker / Podman
Pre-built multi-arch images (linux/amd64 + linux/arm64) are published to GHCR on every release. Both Docker and Podman are fully supported — use whichever is available on your system. No local PHP installation is required.
Pull and run (no build required)
Docker:
docker pull ghcr.io/dbdiff/dbdiff
docker run --rm ghcr.io/dbdiff/dbdiff --version
docker run --rm ghcr.io/dbdiff/dbdiff --driver=mysql \
--server1=user:pass@host:3306 server1.mydb:server1.mydb
Podman (drop-in replacement — commands are identical):
podman pull ghcr.io/dbdiff/dbdiff
podman run --rm ghcr.io/dbdiff/dbdiff --version
podman run --rm ghcr.io/dbdiff/dbdiff --driver=mysql \
--server1=user:pass@host:3306 server1.mydb:server1.mydb
Podman on Linux runs rootless by default — no daemon required. Install via your package manager:
sudo apt install podman(Debian/Ubuntu) orbrew install podman(macOS).
Image variants
Tag patternRegistryDescriptionlatest, {version}, slim-{version}GHCRSlim — PHAR + PHP Alpine (~120 MB). For production use / CI.full, full-{version}GHCRFull — Composer source install (~600 MB). For development and cross-version testing.
Build locally
# Slim image (requires dist/dbdiff.phar — run `vendor/bin/box compile` first)
# Replace 'docker' with 'podman' if using Podman
docker build -f docker/Dockerfile.slim -t dbdiff:slim .
docker run --rm dbdiff:slim --version
# Full image (Composer install from source — no PHAR needed)
docker build -f docker/Dockerfile -t dbdiff:full .
See DOCKER.md for cross-version testing, Podman setup, and start.sh flags.
Composer Source Install
git clone https://github.com/DBDiff/DBDiff.git
cd DBDiff
composer install --optimize-autoloader
Or as a project dependency:
composer require "dbdiff/dbdiff:@dev"
Or globally:
composer global require "dbdiff/dbdiff:@dev"
After installing from source, continue with Setup.
Setup
For source installs (git clone / Composer) only. Binaries, PHAR, npm, and Docker do not require these steps.
Create a
.dbdiffconfig file — see File ExamplesRun:
./dbdiff server1.db1:server1.db2
Expected output:
ℹ Now calculating schema diff for table `foo`
ℹ Now generating UP migration
ℹ Writing migration file to /path/to/dbdiff/migration.sql
✔ Completed
Command-Line API
diff (default command)
Flags always override settings in .dbdiff.
FlagDescription--server1=user:pass@host:portSource connection. Omit if using only one server.--server2=user:pass@host:portTarget connection (if different from server1).--server1-url=<dsn>Full DSN URL for source (e.g. postgres://user:pass@host:5432/db).--server2-url=<dsn>Full DSN URL for target. Supported schemes: mysql://, pgsql://, postgres://, postgresql://, sqlite://.--driver=mysql|pgsql|sqliteDatabase driver. Defaults to mysql.--supabaseShorthand for --driver=pgsql + SSL.--format=native|flyway|liquibase-xml|liquibase-yaml|laravelOutput format. Defaults to native.--description=<slug>Slug used in generated filenames.--template=<path>Custom output template.--type=schema|data|allWhat to diff. Defaults to schema.--include=up|down|bothDirections to include. Defaults to up. (all is accepted as an alias for both.)--nocommentsStrip comment headers from output.--config=<file>Config file path. Defaults to .dbdiff.--output=<path>Output file path. Defaults to migration.sql.--memory-limit=<value>PHP memory limit for this run (e.g. 512M, 1G, 2G, -1 for unlimited). Overrides the 1G default and any memory_limit setting in your config file.--tables=<list>Comma-separated table include list (supports globs: *, ?). Only these tables are diffed. Example: --tables=users,orders,wp_*--ignore-tables=<list>Comma-separated table exclude list (supports globs: *, ?). Example: --ignore-tables=cache_*,temp_*--debugEnable verbose error output.server1.db1:server2.db2Databases to compare. Or a single table: server1.db1.table1:server2.db2.table1.
UP only by default: The generated file includes only the UP (forward) migration by default. To also generate the DOWN (rollback) section, pass
--include=all. Example:dbdiff diff --include=all ...
DSN URLs vs
--serverflags: Use--server1-url/--server2-urlwhen you have a connection string (common with Supabase, Neon, Railway, etc.). Use--server1/--server2when specifying credentials separately.
Passwords with special characters: Embed the password percent-encoded in the URL. Use
dbdiff url:encodeto safely encode any password (seeurl:encodebelow). If dbdiff is not yet installed,scripts/encode-password.shworks without any dependencies.
Memory: The CLI sets a default PHP memory limit of 1G. Diffing very large databases may need more — pass
--memory-limit=2Gon the command line or addmemory_limit: 2Gto your.dbdiff/dbdiff.ymlconfig. The CLI flag always wins over the config file.
url:encode — Password encoder
Percent-encodes a raw password for safe embedding in any --server-url connection string.
dbdiff url:encode '<raw password>'
Capture the result directly into a connection flag:
PASS=$(dbdiff url:encode 'my$ecret#pass@word%123')
dbdiff diff \
--server1-url="postgres://user:${PASS}@db.xxxx.supabase.co:5432/postgres" \
--server2-url="postgres://user:pass@db.yyyy.supabase.co:5432/postgres"
Accepts stdin too, for use in pipelines:
echo 'my$ecret#pass' | dbdiff url:encode
All characters except RFC 3986 unreserved characters (A–Z a–z 0–9 - _ . ~) are encoded. This is the safe, zero-guesswork approach for any password — including ones containing @, #, ?, /, +, and literal %.
If dbdiff is not yet installed (e.g. you are setting up CI), use the included bash script instead — no Python, Node, or PHP required:
PASS=$(scripts/encode-password.sh 'my$ecret#pass@word%123')
Migration Runner
DBDiff includes a built-in migration runner. All migration:* commands accept:
FlagDescription--db-url=<dsn>Full DSN URL for the target database.--migrations-dir=<path>Override the migrations directory.--config=<file>Path to dbdiff.yml.
CommandDescriptionExtra flagsmigration:new <name>Scaffold a new migration file.--format=supabase — plain .sql (no DOWN); auto-set inside Supabase projectsmigration:upApply all pending migrations.--target=<version> — stop after this versionmigration:downRoll back applied migration(s).--last=<n> (default 1), --target=<version>migration:statusShow applied vs pending migrations. Adds a Supa? column inside Supabase projects.—migration:validateVerify on-disk checksums match the history table.—migration:repairRemove failed entries so they can be retried.--force — skip confirmationmigration:baselineMark current DB state as the migration baseline.--baseline-version=<YYYYMMDDHHmmss>, --description=<text>, --force
Migration file formats
DBDiff supports two on-disk formats in the same directory:
FormatFile patternBest forNative (default){version}_{name}.up.sql + optional .down.sqlNew projects, rollback supportSupabase{version}_{name}.sql (UP only)Existing supabase/migrations/ directories
If both formats exist for the same version timestamp, the native .up.sql file takes precedence.
Supabase project auto-detection
When DBDiff is run inside (or below) a directory that contains supabase/config.toml, it automatically:
Sets the migrations directory to
supabase/migrations/(no--migrations-dirneeded)Defaults
migration:newto Supabase format — creating a plain.sqlfileShows a Supa? column in
migration:status, indicating which migrations Supabase's ownschema_migrationstable considers applied
Pass --format=native to migration:new to override the auto-detected format.
Usage Examples
MySQL (default)
./dbdiff server1.db1:server2.db2
MySQL — data diff only
./dbdiff server1.dev.table1:server2.prod.table1 --nocomments --type=data
MySQL — Flyway format with output path
./dbdiff --format=flyway --description=add_users --include=all \
server1.db1:server2.db2 --output=./sql/
PostgreSQL
./dbdiff --driver=pgsql --server1=user:pass@localhost:5432 server1.staging:server1.production
Supabase
./dbdiff --supabase --server1=postgres:pass@db.xxxx.supabase.co:5432 \
server1.staging:server1.production
SQLite
./dbdiff --driver=sqlite server1./var/db/v1.db:server1./var/db/v2.db
DSN URLs
./dbdiff diff \
--server1-url='postgres://user:pass@db.xxxx.supabase.co:5432/postgres' \
--server2-url='postgres://user:pass@db.yyyy.supabase.co:5432/postgres'
If your password contains special characters, use dbdiff url:encode (see url:encode in the Command-Line API section):
PASS=$(dbdiff url:encode 'my$ecret#pass@word%123')
./dbdiff diff \
--server1-url="postgres://user:${PASS}@db.xxxx.supabase.co:5432/postgres" \
--server2-url='postgres://user:pass@db.yyyy.supabase.co:5432/postgres'
Migration runner
# Scaffold a new migration (DBDiff native format)
./dbdiff migration:new create_users_table
# Scaffold a Supabase-format migration (plain .sql, no DOWN file)
./dbdiff migration:new create_users_table --format=supabase
# Inside a Supabase project, format and directory are auto-detected:
cd my-supabase-project # contains supabase/config.toml
./dbdiff migration:new create_users_table # → supabase/migrations/{ts}_create_users_table.sql
# Apply all pending migrations
./dbdiff migration:up --db-url='postgres://user:pass@localhost:5432/mydb'
# Check status (adds a Supa? column inside Supabase projects)
./dbdiff migration:status --db-url='postgres://user:pass@localhost:5432/mydb'
# Roll back the last migration
./dbdiff migration:down --db-url='postgres://user:pass@localhost:5432/mydb'
# Validate checksums
./dbdiff migration:validate --db-url='postgres://user:pass@localhost:5432/mydb'
Supabase — diff with local stack auto-fill
When inside a Supabase project (supabase/config.toml present) and the local stack is
running (supabase start), --server1-url is resolved automatically from
supabase status:
# Only supply the remote (production) URL — local stack fills in automatically
./dbdiff diff --server2-url='postgres://user:pass@db.yyyy.supabase.co:5432/postgres'
File Examples
A single dbdiff.yml file in your project root configures both the diff command and the migration runner. Copy dbdiff.yml.example to get started.
Auto-detected filenames, in priority order:
FilenameNotes.dbdiffLegacy — still supported for backwards compatibilitydbdiff.ymlRecommended — YAML syntax highlighting, single file for everything.dbdiff.ymlHidden-file variantdbdiff.yaml.yaml extension variant
You can also pass any filename explicitly: ./dbdiff --config=myconfig.yml server1.db:server2.db
dbdiff.yml
# ── Diff command (./dbdiff server1.db:server2.db) ─────────────────────────
server1:
user: user
password: password
port: 3306 # MySQL: 3306 | PostgreSQL: 5432
host: localhost
server2:
user: user
password: password
port: 3306
host: host2
driver: mysql # mysql | pgsql | sqlite
type: all
include: all
nocomments: true
# ── Filtering ─────────────────────────────────────────────────────────────
# Include list — only these tables are diffed (supports globs: *, ?).
# When set, only matching tables are included. Omit to diff all tables.
# tables:
# - users
# - orders
# - wp_*
# Exclude list — skip these tables entirely (supports globs).
tablesToIgnore:
- table1
- table2
# Exclude from data diff only — schema is still diffed (supports globs).
# tablesDataToIgnore:
# - audit_log
# - event_stream
# Per-table column exclusion (keys support globs: *, ?).
fieldsToIgnore:
table1:
- field1
- field2
# Per-table row filtering — skip rows matching a column-value regex.
# rowsToIgnore:
# wp_options:
# - { column: option_name, pattern: "_transient_.*" }
# - { column: option_name, pattern: "_site_transient_.*" }
# sessions:
# - { column: status, pattern: "expired|archived" }
# Per-table scope override: schema, data, or all (default).
# tableScope:
# audit_log: schema
# config: data
# ── Migration runner (dbdiff migration:up) ────────────────────────────────
database:
driver: mysql
host: localhost
port: 3306
name: mydb
user: root
password: secret
migrations:
dir: ./migrations
history_table: _dbdiff_migrations
Filtering
DBDiff offers fine-grained control over what enters the diff. All list values support glob patterns (* matches any characters, ? matches a single character).
DimensionConfig keyCLI flagScopeTable include listtables--tablesschema + dataTable exclude listtablesToIgnore--ignore-tablesschema + dataData-only excludetablesDataToIgnore—data onlyColumn exclusionfieldsToIgnore—schema + dataRow filteringrowsToIgnore—data onlyPer-table scopetableScope—override
Priority: include list is applied first (only matching tables pass), then the exclude list narrows further. tablesDataToIgnore removes tables from data comparison only. tableScope can override a table to schema, data, or all.
Glob examples: wp_* matches all WordPress tables, *_backup matches any table ending in _backup, log_? matches log_a through log_z.
# Only diff tables matching these patterns
tables:
- users
- orders
- wp_*
# Skip these tables entirely
tablesToIgnore:
- cache_*
- _dbdiff_migrations
# Skip data diff for these (schema is still compared)
tablesDataToIgnore:
- audit_log
# Exclude columns per table (keys support globs)
fieldsToIgnore:
users:
- updated_at
- last_login
wp_*:
- ID
# Skip rows matching column-value regex
rowsToIgnore:
wp_options:
- { column: option_name, pattern: "_transient_.*" }
sessions:
- { column: status, pattern: "expired|archived" }
# Override scope per table: schema | data | all
tableScope:
audit_log: schema
config: data
How Does the Diff Work?
Comparisons run in this order:
Overall
Checks both databases exist and are accessible
Compares database collation between source and target
Schema
Detects differences in column count, name, type, collation or attributes
New columns in the source are added to the target
Views
Detects created, dropped, and altered views across source and target
ALTER = DROP IF EXISTS + CREATE with the new definition
Triggers
Detects created, dropped, and altered triggers
PostgreSQL DROP TRIGGER includes the required ON table clause
Stored Procedures / Functions
Detects created, dropped, and altered routines (MySQL and PostgreSQL)
SQLite has no stored procedures — routines are skipped automatically
MySQL definitions are normalized (DEFINER, ALGORITHM, SQL SECURITY stripped)
Enum Types (PostgreSQL)
Detects created, dropped, and altered
CREATE TYPE ... AS ENUMdefinitionsALTER = DROP TYPE IF EXISTS + CREATE TYPE with the new labels
Enum diffs are ordered before table diffs (tables may reference enum types)
MySQL and SQLite do not have standalone enum types — skipped automatically
Data
Compares table storage engine, collation, and row count
Records changed rows and missing rows per table
Compatible Migration Tools
DBDiff supports multiple output formats via --format. Use --description=<slug> to customise generated filenames.
--formatToolLanguageOutputNotesnative (default)Plain SQLAnymigration.sqlUp, down, or bothflywayFlywayJavaV{ts}__{desc}.sqlDown adds U{ts}__{desc}.sql (Flyway Teams)liquibase-xmlLiquibaseJavachangelog.xmlBoth directions in one fileliquibase-yamlLiquibaseJavachangelog.yamlBoth directions in one filelaravelLaravel MigrationsPHPYYYY_MM_DD_HHMMSS_{desc}.phpup()/down() methods(template)Simple DB MigratePythoncustomUse --template=templates/simple-db-migrate.tmpl
Let us know if you're using DBDiff with other tools so we can add them here.
Building a PHAR
PHARs are built automatically and attached to every GitHub Release. To build locally from source:
composer install
vendor/bin/box compile
Output: dist/dbdiff.phar — rename and move to /usr/local/bin/dbdiff if desired.
box.jsonis pre-configured with GZ compression andcheck-requirements: falseso the PHAR works correctly when stitched with the static micro SAPI runtime used in the pre-built binaries.
Releasing 🚀
Automated (recommended)
Go to GitHub Actions → Release DBDiff → Run workflow
Enter the version number (e.g.
2.1.0— novprefix)The workflow will:
Build the PHAR with Box
Build self-contained binaries for all 8 platforms via static-php-cli
Publish all
@dbdiff/cli-*packages to npm (skips any already published)Create or update the GitHub Release with all assets
Create the git tag (skipped if it already exists)
Manual / local
# Build PHAR + tag
scripts/release.sh v2.1.0
git push origin v2.1.0
# Build Linux binaries locally (requires Podman or Docker)
SKIP_PHAR=1 scripts/release-binaries.sh 2.1.0
# Upload assets to an existing GitHub Release
gh release upload v2.1.0 --clobber \
dist/dbdiff.phar \
packages/@dbdiff/cli-linux-x64/dbdiff \
packages/@dbdiff/cli-linux-x64-musl/dbdiff \
packages/@dbdiff/cli-linux-arm64/dbdiff \
packages/@dbdiff/cli-linux-arm64-musl/dbdiff
# Update the Homebrew tap formula
scripts/update-homebrew-formula.sh 2.1.0 ../homebrew-dbdiff
Cross-Version Testing
Test DBDiff locally against any combination of PHP and MySQL:
# Single combination
./start.sh 8.3 8.0
# All 16 combinations in parallel
./start.sh all all --parallel
The CI matrix: 5 PHP × 4 MySQL = 20 jobs, plus dedicated jobs for SQLite, PostgreSQL, DSN URLs, and Supabase.
See DOCKER.md for flags covering fast restarts, recording fixtures, and CI usage.
Questions & Support 💡
Open a new issue or check existing ones
For commercial support enquiries, get in touch
Contributions 💖
Please read the Contributing Guide before submitting a PR.
Feedback 💬
Could you spare 2 minutes to share your feedback?
https://forms.gle/gjdJxZxdVsz7BRxg7
License
On this page
- Features
- Supported Databases
- MySQL
- PostgreSQL
- SQLite
- Supabase
- Compatible Database Variants
- MySQL-compatible — --driver=mysql (default)
- PostgreSQL-compatible — --driver=pgsql
- Neon Branching
- Dolt (Git for Databases)
- Installation
- Pre-built Binaries
- npm
- PHAR
- Docker / Podman
- Pull and run (no build required)
- Image variants
- Build locally
- Composer Source Install
- Setup
- Command-Line API
- diff (default command)
- url:encode — Password encoder
- Migration Runner
- Migration file formats
- Supabase project auto-detection
- Usage Examples
- MySQL (default)
- MySQL — data diff only
- MySQL — Flyway format with output path
- PostgreSQL
- Supabase
- SQLite
- DSN URLs
- Migration runner
- Supabase — diff with local stack auto-fill
- File Examples
- dbdiff.yml
- Filtering
- How Does the Diff Work?
- Overall
- Schema
- Views
- Triggers
- Stored Procedures / Functions
- Enum Types (PostgreSQL)
- Data
- Compatible Migration Tools
- Building a PHAR
- Releasing 🚀
- Automated (recommended)
- Manual / local
- Cross-Version Testing
- Questions & Support 💡
- Contributions 💖
- Feedback 💬
- License