If you work with PostgreSQL long enough, you start noticing a pattern: migration tools often become more complicated than the schema changes they are supposed to manage.
Some tools invent their own DSL.
Some hide behavior in config files.
Some couple migrations to an ORM.
Some force a directory layout that looks neat in a demo but awkward in a real project.
And then there is the simpler question:
Why canβt PostgreSQL migrations just stay plain SQL?
That is the idea behind gopgmigrate.
It is a SQL-first migration tool for PostgreSQL that keeps the core workflow boring in the best possible way:
- write normal
.sqlfiles - organize them however you want
- run them in order
- track what was applied
- support rollbacks
- support repeatable migrations
- make non-transactional migrations explicit
No YAML. No hidden DSL. No ORM lock-in. No magic comments.
Just SQL files and a clear naming convention.
Why this approach matters
A migration file should be easy to:
- read in a code review
- open in your editor
- run directly with
psql - troubleshoot at 2 AM
- keep using even if you stop using the tool
That last point matters more than many teams realize.
A good migration format should outlive the tool that executes it. Your schema history is long-term infrastructure. It should not depend on a framework-specific abstraction that becomes painful to migrate away from later.
With gopgmigrate, the migration files remain usable as ordinary SQL. The tool adds safety and structure on top, but it does not take ownership of your database change process.
What gopgmigrate does
At a high level, the workflow is simple:
- Scan a directory tree recursively for SQL migration files
- Sort them globally by revision
- Compare them with the migration history stored in PostgreSQL
- Apply only what is pending
- Record hashes and metadata for auditability
- Support rolling back the last applied migrations
- Re-run repeatable scripts only when their content changes
That gives you a clean PostgreSQL migration workflow with a small mental model.
The naming convention is the API
One of the nicest design choices in gopgmigrate is that the file name itself declares the migration behavior.
Example:
0000001-create-users-table.up.sql
0000001-create-users-table.down.sql
0000003-fn-get-users.r.up.sql
0000004-vacuum-users.notx.up.sql
0000005-refresh-stats.rnotx.up.sql
This is refreshingly explicit.
Versioned migrations
These run once in order:
0000002-add-roles-table.up.sql
Rollbacks
Rollback files are separate and predictable:
0000002-add-roles-table.down.sql
Repeatable migrations
Useful for functions, views, triggers, or other SQL objects you may want to refresh when the file changes:
0000003-fn-get-users.r.up.sql
Non-transactional migrations
Some PostgreSQL operations cannot run inside a transaction, for example:
VACUUMCREATE INDEX CONCURRENTLYDROP INDEX CONCURRENTLY- some forms of
REINDEX ALTER SYSTEM
Those are made explicit in the file name:
0000004-vacuum-users.notx.up.sql
And if a migration is both repeatable and non-transactional:
0000005-refresh-stats.rnotx.up.sql
This is a small detail, but it solves a real operational problem: the migration behavior is visible before you open the file.
Real projects are not flat folders
A lot of migration tools quietly assume every team wants the same directory structure.
Reality is messier.
Some teams want to split:
- schema
- data
- functions
- maintenance
- environment-specific files
- release-based groups
That is why I like that gopgmigrate does not force a rigid directory layout.
You can organize migrations by concern:
migrations/
schema/
data/
functions/
no-transaction/
down/
Or by release:
migrations/
v1.0.0/
v1.1.0/
down/
Or however your team naturally thinks about database changes.
The only rule is that version ordering remains global.
That is a practical compromise: freedom in layout, predictability in execution.
Why SQL-first migrations are still the best default
There is a reason SQL-first tools keep appealing to engineers who work close to PostgreSQL.
PostgreSQL already has a powerful language for schema and data changes. It is called SQL.
When a tool stays out of the way, you get a few concrete advantages:
Better reviewability
A migration diff is just SQL. Reviewers do not have to mentally decode a framework abstraction.
Better portability
You can run the file with psql, a database IDE, automation scripts, or CI jobs.
Better debugging
When something fails, you are looking at the actual statement PostgreSQL rejected.
Better longevity
Your migration history remains useful years later, even if your application stack changes.
That makes SQL-first migration tooling especially attractive for:
- platform teams
- backend teams with multiple services
- teams that avoid ORM-heavy workflows
- projects with long-lived PostgreSQL databases
- teams that want plain operational ownership
Safety features that matter in practice
Simple does not mean naive.
For a migration tool to be usable in production, it needs a few guardrails. gopgmigrate includes some of the right ones:
Advisory locking
This helps prevent concurrent migration runs from stepping on each other.
Transactional safety by default
Most PostgreSQL DDL can run inside a transaction, and that is the safe default.
Explicit non-transactional mode
Instead of hiding exceptions, the tool makes them obvious in the filename.
Hash-based change detection
This is particularly useful for repeatable migrations. If the content changes, the tool knows it should re-apply the script.
History tracking
Applied migrations are recorded in a history table, along with metadata such as hash and timing-related details.
That is the kind of boring reliability you want from migration tooling.
Example CLI workflow
The CLI is intentionally straightforward.
Apply pending migrations:
gopgmigrate migrate \
--dirname ./migrations \
--connstr postgres://user:pass@localhost:5432/mydb
Preview without applying:
gopgmigrate migrate \
--dirname ./migrations \
--connstr postgres://user:pass@localhost:5432/mydb \
--dry-run
Rollback the last migration count:
gopgmigrate rollback-count 2 \
--dirname ./migrations \
--connstr postgres://user:pass@localhost:5432/mydb
Use environment variables in CI:
export PGMIGRATE_DIRNAME=./migrations
export PGMIGRATE_CONNSTR=postgres://user:pass@localhost:5432/mydb
gopgmigrate migrate
That is the kind of interface that works well in local development, CI pipelines, containerized jobs, and release automation.
Where this fits especially well
I think gopgmigrate is especially appealing in a few scenarios.
1. PostgreSQL-first teams
If your team understands PostgreSQL and prefers direct SQL over framework migration layers, this fits naturally.
2. Teams with mixed migration types
Schema changes, data fixes, repeatable view/function refreshes, and non-transactional maintenance are all first-class cases here.
3. Repos with real structure
If your migration directory stopped being a cute flat demo folder a long time ago, recursive scanning and flexible layouts are genuinely useful.
4. CI/CD and automation
The CLI is simple enough to drop into pipelines without teaching your delivery system a new configuration language.
5. Engineers who dislike lock-in
Your migration files stay plain SQL. That is a strong long-term property.
What I like most about this design
The best tools often win not because they do more, but because they make fewer damaging decisions for you.
gopgmigrate seems built around a healthy principle:
the tool should manage execution, not redefine how SQL migrations ought to exist.
That means:
- your files remain readable
- your shell workflows still work
- your database knowledge stays relevant
- your migration history does not become framework glue
In database tooling, that is a strong design choice.
Final thoughts
There are plenty of PostgreSQL migration tools out there. Many are good. But a lot of them drift toward abstraction for its own sake.
If what you want is:
- PostgreSQL migrations
- plain SQL files
- explicit rollbacks
- repeatable migrations
- non-transaction support
- advisory locking
- transactional safety
- hash-based change detection
- flexible directory layouts
- clean CLI usage
- minimal ceremony
then gopgmigrate is worth a look.
It takes a very practical path: keep migrations human-readable, keep behavior explicit, and keep the tool small enough that you can trust what it is doing.
That is a solid direction for database change management.
If you find gopgmigrate useful, consider giving the repo a star on GitHub. It helps more people discover the project.
Repository: https://github.com/hashmap-kz/gopgmigrate

Top comments (0)