iTranslated by AI
I created a CLI to validate golang-migrate migration filenames
TL;DR
I created miglint, a CLI tool to check whether the migration filenames (in the format <VERSION>_<NAME>.(up|down).<ext>) created by the golang-migrate migrate create command are as expected.
With this tool, you can detect issues such as "broken naming conventions," "duplicated versions," or "missing up/down pairs."
Why I Made It
When using migration tools like golang-migrate in a team, keeping the migration versions in the expected order was a subtle but constant concern.
It is tedious to manually check the following points during reviews or before merging, and there's always a possibility of overlooking them:
- Do the
NAMEparts of the up and down files match? - (When using sequential numbers) Are there any missing numbers?
- Will versions clash when pulling the latest changes? (This often happens when a migration file with the same version is created in another branch and that change is merged first.)
- Do both the up and down files exist?
I thought it would be great to check these things mechanically, so I created a lightweight lint tool that focuses solely on filenames.
golang-migrate's Migration Naming Conventions
golang-migrate provides a migrate create command. When you use it to create migration files, they are generated with the following naming convention:
<VERSION>_<NAME>.up.<ext><VERSION>_<NAME>.down.<ext>
For example, running migrate create -ext sql -seq create_users_table results in the following:
path/to/000001_create_users_table.up.sql
path/to/000001_create_users_table.down.sql
(If -seq is not specified, the format will be something like 20060102150405_<NAME>....)
What is Checked?
miglint inspects the specified directory and checks for filename consistency based on the following criteria:
- Is it in the
VERSION_NAME.(up|down).EXTformat? - Are there multiple up/down files for the same version?
- Is a
downfile missing? (Optional) - Do the
NAMEand extension of the up/down files match? (Optional) - Do you want to fix the number of digits for the version (e.g.,
000001)? (Optional) - Are gaps in version numbers (e.g.,
000001, 000002, 000004 ...) allowed? (Optional)
Many of these are optional because the behavior of migrate create itself is flexible. Since operational rules may vary by project, you can enable only the specific checks you need.
Usage
Installation
go install github.com/tetzng/miglint/cmd/miglint@latest
Command Execution Example
miglint -path ./db/migrations
Similar to the migrate command, use the -path option to specify the directory where your migration files are located. This option is required.
Option Descriptions
-require-down
Detects files that have an up migration but are missing the corresponding down migration.
-strict-name-match
Checks whether the NAME and extension of the up/down pair match.
For example, it can detect discrepancies like this:
- 000001_create_users.up.sql
- 000001_create_user.down.sql ← "users" vs "user"
-digits
In golang-migrate, you can use sequential numbers without padding (like 1_, 2_...) or with padding (0001_, 000001_). Use this if you want to enforce a specific number of digits.
miglint -path ./db/migrations -digits 6
This would detect files like 001_create.up.sql that don't match the required 6 digits.
-no-gaps
Detects gaps in version numbers.
- 000001_create.up.sql
- 000003_add_column.up.sql ← 000002 is missing
This detects such cases. This option is primarily intended for use when operating with sequential (seq) versions.
-ext / -enforce-ext
Use this when you want to limit the extension to just .sql.
miglint -path ./db/migrations -ext sql
Furthermore, if you want to detect files that have a migration-like naming convention but a different extension, add -enforce-ext.
For example, it will detect files like 000001_a.down.txt.
It also supports composite extensions like sql.gz.
- With
-ext sql.gz, it matches thesql.gzpart of.up.sql.gz. - With
-ext gz, it matches the finalgzpart.
-strict-pattern
Detects cases where files that do not follow the migration file format are mixed into the migrations directory.
- 123notes.sql (Starts with a number but has a different format)
- 000001_add_user.up (Ends with .up but has no extension)
- 000001_a.up. (Ends with a trailing dot)
My Recommended Settings
Since I often operate with sequential numbers, I plan to use it with the following settings:
miglint -path ./db/migrations \
-ext sql -enforce-ext \
-require-down \
-strict-name-match \
-digits 6 \
-no-gaps \
-strict-pattern
Points of Caution
- It only checks files directly within the specified directory (it does not search subdirectories).
- Filenames that do not match the migration file format are ignored by default (can be turned into errors with
-strict-pattern). - Symbolic links and other similar entries are ignored.
Integrating into CI
Depending on your project's operational policy, incorporating this into your CI can significantly reduce human errors related to migrations.
Here is an example for GitHub Actions:
name: migration lint
on:
pull_request:
jobs:
miglint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Install miglint
run: go install github.com/tetzng/miglint/cmd/miglint@latest
- name: Lint migrations
run: |
miglint -path ./db/migrations \
-ext sql -enforce-ext \
-require-down \
-strict-name-match \
-digits 6 \
-no-gaps \
-strict-pattern
Conclusion
I created miglint for the project I am involved in, but I would be glad if it helps anyone experiencing similar issues.
If you have suggestions like "I want this kind of check" or "I want to allow this naming convention," please feel free to open an Issue or a PR.
Discussion