We have released ddl-mt, short for damn delightful migrations, under the MIT license. It is a standalone command-line tool that applies SQL migrations to PostgreSQL. This article explains the problem it solves for us, the choices we made, and why it is public.
The problem
As consultants we work in many repositories at the same time. Most of them have a PostgreSQL database, and most of those exist in more than one environment. Schema changes are a small part of each project, and they are also the part where a mistake is hardest to undo.
Migration tools usually arrive with an application framework or an ORM. That works well inside one project. Across many projects it means a different tool, a different history table and a different set of habits in every repository. We wanted the opposite: one tool that behaves the same everywhere, while each repository still owns its own SQL.
What ddl-mt does
Each repository keeps its migrations as numbered SQL files, together with a small ddl-mt.json project file. One installed binary manages all of them.
The project file defines named targets. A target connects a migration directory to the database it expects, a source of credentials and a history table:
{
"project": "shop",
"targets": {
"development": {
"database": "shop_dev",
"migrations_dir": "migrations/main",
"dsn_env": "SHOP_DEV_DSN"
},
"production": {
"database": "shop",
"migrations_dir": "migrations/main",
"dsn_env": "SHOP_PROD_DSN"
}
}
} Development and production share the same SQL even though the databases have different names. The daily workflow is a handful of commands:
ddl-mt --target development new create_widgets
ddl-mt --target development validate
ddl-mt --target development status
ddl-mt --target development apply check exits with an error when a database has pending, failed or changed migrations, which makes it suitable for a CI step. baseline records migrations that an existing database already contains, so a database with history can be adopted without running its SQL again.
The choices we made
You always name the target. There is no implicit default. After connecting, ddl-mt compares the actual database name with the one the target expects and stops if they differ. Running a production command against the wrong database should fail before any SQL runs.
Plain SQL, forward only. Migrations are SQL files that you can read and review. Every file is checksummed, so an applied migration that is later edited or renamed is detected. There are no down-migrations and no schema diffing. A deployed change is corrected with a new migration.
Each migration is atomic. A migration and its history record are committed in the same transaction. If the migration fails, it is rolled back and nothing is recorded.
Connection pooling is a first-class concern. By default ddl-mt takes a transaction-level advisory lock for each migration and rereads the history under that lock. This works on a direct PostgreSQL connection and behind PgBouncer in transaction or session pooling mode, and two runners that start at the same moment cannot apply the same file twice.
The exceptions are explicit. Statements such as CREATE INDEX CONCURRENTLY cannot run inside a transaction. ddl-mt only runs them when the target opts in and uses a session connection. It writes a durable marker before the SQL starts, and after a failure an operator has to decide whether to retry or mark the change as applied. The tool does not guess.
Credentials stay out of the command line. Connection strings come from an environment variable or a credentials file, never from an argument. Connection errors are reported as categories such as authentication, DNS or TLS failure, without printing the connection string or the raw server message.
What it is not
ddl-mt supports PostgreSQL only. It does not generate migrations, compare schemas or roll changes back, and it does not apply migrations to every target in one command. It is a small tool, and we intend to keep it small. It is also young: the first public version is 0.2.0, and the 0.x series may still introduce breaking changes.
Why open source
Our daily work depends on open source: PostgreSQL itself, Go, and the pgx driver that ddl-mt uses. Publishing a tool that others can use is a way to give something back.
There is also a practical reason. A tool that changes production databases should be easy to inspect. With the source, the tests and the changelog in public, our clients and anyone else who uses it can read exactly what it does before trusting it.
Try it
With Go 1.26 or newer:
go install github.com/qbite-tech/ddl-mt@latest
ddl-mt create-config The README covers configuration, PgBouncer and recovery in detail. Bug reports and contributions are welcome in the repository, and you can reach us at opensource@qbite.tech.