$ dbdiff diff current.sqlite desired.sqlite
ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
CREATE INDEX "users_email" ON "users" ("email");
DROP TABLE "audit";
CREATE VIEW active_users AS SELECT id, email FROM users;dbdiff supports SQLite, PostgreSQL, MySQL, and MariaDB. Each side is a database, a .sql file, or a directory of .sql files.
Download a binary from the releases page, or install with Go:
go install github.com/quantumsheep/dbdiff/cmd/dbdiff@latestdbdiff needs no C compiler. The SQLite driver supports the virtual table modules fts5, rtree, and geopoly. It does not support fts3 and fts4.
| Command | Result |
|---|---|
dbdiff diff <current> <desired> |
Print the statements that make the current schema equal to the desired schema |
dbdiff migrate generate <name> |
Write the next migration file |
dbdiff migrate status |
List each migration and its state |
dbdiff migrate preview |
Print the statements that up will run |
dbdiff migrate up |
Apply every pending migration |
dbdiff migrate step |
Apply every pending migration, and ask before each file |
dbdiff migrate verify |
Compare the database against the replay of the applied migrations |
dbdiff migrate repair |
Make the history table agree with the migration files |
dbdiff migrate baseline |
Record every migration file as applied, and run no file |
dbdiff diff changes no database. To apply its output, send it to the client of the engine:
dbdiff diff current.sqlite schema.sql | sqlite3 current.sqlite
dbdiff diff postgres://localhost:5432/current postgres://localhost:5432/desiredCaution
Read the output before you apply it. It can drop tables, columns, and rows.
dbdiff completion <shell> prints the completion script for bash, zsh, fish, or pwsh.
source <(dbdiff completion bash) # bash
source <(dbdiff completion zsh) # zsh
dbdiff completion fish > ~/.config/fish/completions/dbdiff.fish # fish| Flag | Value | Purpose |
|---|---|---|
--driver |
sqlite3, postgres, or mysql |
Select the engine. dbdiff detects it from the arguments by default. |
--schema |
A schema name | Select the PostgreSQL schema. The default is the search path. Only the postgres driver accepts this flag. |
--data |
none | Compare the rows too. |
--comments |
none | Print a comment before each change. |
--privileges |
none | Compare the owners and the privileges (PostgreSQL and MySQL). |
--exit-code |
none | If the schemas differ, exit with code 1. |
--config |
A file path | Read this configuration file. The default is dbdiff.yaml in the working directory. |
--ignore-table |
A table name | Ignore this table. Repeat the flag for more tables. |
--server-version |
A server version | Select the version of the temporary server. Only the postgres driver and the mysql driver accept this flag. |
--scratch-database |
A connection URL | Run the SQL files on this server, not on a temporary server. Only the postgres driver and the mysql driver accept this flag. |
| Flag | Value | Purpose |
|---|---|---|
--config |
A file path | Read this configuration file. The default is dbdiff.yaml in the working directory. |
--migrations |
A directory path | Read the migration files from this directory. |
--desired |
A .sql file, a directory, or a connection URL |
Compare the migration files with this schema. Only generate accepts this flag. |
--database |
A connection URL | Read or change this database. Each command except generate accepts this flag. |
--to |
A migration version | For up and baseline, stop after this version. The default is the last migration. |
--run |
none | For preview, apply the pending files in one transaction. Then roll back the transaction. |
--empty |
none | For generate, write a file with no statement. The command does not read the desired schema. |
--driver |
sqlite3, postgres, or mysql |
Select the engine. dbdiff detects it from the arguments by default. |
--schema |
A schema name | Select the PostgreSQL schema. The default is the search path. Only the postgres driver accepts this flag. |
--server-version |
A server version | Select the version of the temporary server. Only the postgres driver and the mysql driver accept this flag. |
--scratch-database |
A connection URL | Run the SQL files on this server, not on a temporary server. Only the postgres driver and the mysql driver accept this flag. |
--ignore-table |
A table name | Ignore this table. Repeat the flag for more tables. |
dbdiff runs a .sql file, or the .sql files of a directory in name order, in a temporary database. For PostgreSQL and MySQL, it downloads a temporary server on the first run, with the version of the other side. The --server-version flag sets another version.
Each file runs in one transaction. If a file holds a statement that cannot run in a transaction, for example CREATE INDEX CONCURRENTLY, add the line -- dbdiff:no-transaction to the file.
To use an existing server instead of a temporary server, give its connection URL with --scratch-database, or with the key scratch-database. dbdiff then downloads nothing and starts no server. For each SQL side, dbdiff creates a database with a name that starts with dbdiff_. At the end, dbdiff drops the database, also when the diff fails. The user of the URL must have the privilege to create and drop a database. If both sides are SQL files and --driver is not set, the scheme of the URL selects the driver. Do not use --scratch-database together with --server-version.
dbdiff diff --scratch-database postgres://user:password@localhost:5432/postgres current.sql desired.sqlA scratch database needs none of the server requirements below. It also works for MariaDB on Linux with arm64.
| System | Run before the first use |
|---|---|
| macOS, with MariaDB | brew install mariadb |
| Debian, Ubuntu before 24.04 | apt-get install libaio1 |
| Ubuntu 24.04 and later | apt-get install libaio1t64, then the ln -s command below |
ln -s /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/libaio.so.1t64 \
/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/libaio.so.1dbdiff does not support MariaDB on Linux with arm64.
--data compares the rows by primary key and prints INSERT, UPDATE, and DELETE statements. dbdiff skips a table with no primary key.
dbdiff migrate reads dbdiff.yaml from the working directory. Each key has the name of its long flag. A flag overrides its key. The keys are driver, migrations, desired, database, schema, server-version, scratch-database, and ignore-table. --config, --to, and --run have no key.
dbdiff diff also reads dbdiff.yaml. It uses only the keys driver, schema, server-version, scratch-database, and ignore-table, and it ignores the other keys. Give the two schemas of diff as arguments.
driver: postgres
migrations: ./migrations # migration files
desired: ./schema.sql # desired schema, for `migrate generate`
database: ${DATABASE_URL} # database, for the other commands
schema: app
server-version: 17.11.0 # temporary server version
ignore-table:
- exampledbdiff replaces each ${NAME} in a value of the file with the environment variable NAME. If the variable is not set, dbdiff stops with an error. dbdiff replaces only the form ${NAME}. It does not replace $NAME, and it does not replace a value of a flag. A value of the file cannot hold the literal text ${NAME}.
migrate generate writes the difference between the migration files and the desired schema as a new file:
$ dbdiff migrate generate add_created_at
migrations/20260822143000_add_created_at.sqlFor a data change, add the --empty flag. generate then writes a file with no statement, and it does not read the desired schema. Write the statements in the file.
$ dbdiff migrate generate --empty backfill_users
migrations/20260822143100_backfill_users.sqlThe other commands act on the database that --database or the database key names:
export DATABASE_URL=postgres://user:password@localhost:5432/production
dbdiff migrate upmigrate upapplies each file in its own transaction and records it indbdiff_migrations.--to <version>stops after that version.- On MySQL, and in a file with
-- dbdiff:no-transaction, a file that stops before its end can leave part of its changes. If this occurs, repair the database. Then runmigrate repair. migrate baselinerecords the files as applied, and runs no file. If the database already has the schema, use this command.
Run a diff:
import "github.com/quantumsheep/dbdiff/drivers"
driver, err := drivers.NewDriver(drivers.SQLiteDriverName)
statements, err := driver.Diff(ctx,
drivers.NewConnectionStringDataSource("app.sqlite"),
drivers.NewFileDataSource("schema.sql"),
)
fmt.Println(statements.String())Apply the migrations, for example at server start:
import (
"github.com/quantumsheep/dbdiff/drivers"
"github.com/quantumsheep/dbdiff/migrations"
)
driver, err := drivers.NewDriver(drivers.PostgresDriverName)
migrator := driver.Migrator(
migrations.WithDatabase(
drivers.NewConnectionStringDataSource(os.Getenv("DATABASE_URL")),
),
migrations.WithMigrations("migrations"),
)
err = migrator.Up(ctx)| Object | SQLite | PostgreSQL | MySQL / MariaDB |
|---|---|---|---|
| Tables | ✅ | ✅ | ✅ |
| Identity columns | ➖ | ✅ | ➖ |
| Table options | ✅ (WITHOUT ROWID, STRICT) | ✅ (storage parameters) | ✅ (ENGINE, collation) |
| Virtual tables | ✅ | ➖ | ➖ |
| Generated columns | ✅ | ✅ | ✅ |
| Column storage and statistics | ➖ | ✅ | ➖ |
| Indexes | ✅ | ✅ | ✅ |
| Constraints | ✅ (foreign keys, primary keys, unique, checks) | ✅ | ✅ (foreign keys, primary keys, unique, checks) |
| Triggers | ✅ (of a table and of a view) | ✅ (with the mode) | ✅ |
| Views | ✅ | ✅ | ✅ |
| Materialized views | ➖ | ✅ | ➖ |
| Rules | ➖ | ✅ | ➖ |
| Extended statistics | ➖ | ✅ | ➖ |
| Partitioned tables | ➖ | ✅ | ✅ |
| Replica identity | ➖ | ✅ | ➖ |
| Sequences | ➖ | ✅ | ✅ (MariaDB) |
| Enum types | ➖ | ✅ | ➖ (the column type covers it) |
| Domains | ➖ | ✅ | ➖ |
| Composite types | ➖ | ✅ | ➖ |
| Functions | ➖ | ✅ | ✅ |
| Procedures | ➖ | ✅ | ✅ |
| Events | ➖ | ➖ | ✅ |
| Aggregates | ➖ | ✅ | ➖ |
| Operators | ➖ | ✅ | ➖ |
| Casts | ➖ | ✅ | ➖ |
| Extensions | ➖ | ✅ | ➖ |
| Comments | ➖ | ✅ | ✅ (of a column) |
| Row level security | ➖ | ✅ | ➖ |
| Privileges | ➖ | ✅ (--privileges) |
✅ (--privileges) |
| Data | ✅ | ✅ | ✅ |
✅: dbdiff supports the object. ➖: the engine has no such object.
- PostgreSQL: dbdiff compares one schema per run. It does not print
CREATE SCHEMA. - MySQL: dbdiff compares one database per run.
--privilegescompares only the grants of the database and of the tables. - The
mysqlclient splits the bodies of triggers and routines at each semicolon. To apply such a diff, usedbdiff migrate up. - dbdiff has no down migrations. This is a design choice. A rollback is a new up migration that reverses the change. To roll back, change the desired schema back, run
dbdiff migrate generate, then rundbdiff migrate up.
docker compose up -d # Start PostgreSQL, MySQL, and MariaDB
go test ./...
# Without the containers
DBDIFF_TEST_SKIP_POSTGRES=1 DBDIFF_TEST_SKIP_MYSQL=1 go test -short ./...