Tier: Platform · Status: Full · Java original: Flyway · .NET project: EF Core migrations / DbUp
migrations is the framework's versioned-SQL migration runner.
Migration files are named V{version}__{description}.sql (e.g.
V001__init.sql); each file runs once, in version order, inside a
transaction. The applied versions are recorded in a
firefly_migrations table for idempotency.
The runner works against any database/sql.DB driver — the SQL
is parameter-free and ANSI-compatible. Tested against PostgreSQL and
SQLite.
Flyway and DbUp are mature in the JVM and .NET worlds. Go has many
migration libraries, each with a slightly different file-name
convention, locking strategy, and recovery story. migrations
provides one convention so every Firefly Go service handles
schema evolution the same way.
db/
├── V001__init.sql
├── V002__add_orders_index.sql
└── V003__seed_reference_data.sql
The runner only matches files with the V{version}__{description}.sql
pattern; everything else (READMEs, .gitkeep) is ignored.
type Migration struct {
Version int
Description string
Filename string
SQL string
Checksum string // SHA-256 of the SQL bytes
}
type Source interface { List() ([]Migration, error) }
func NewFSSource(fsys fs.FS, dir string) *FSSource // perfect for embed.FS
type SliceSource struct { Items []Migration } // hand-built (tests)
func Run(ctx, *sql.DB, Source) error
func Inspect(ctx, *sql.DB, Source) (Status, error)
type Status struct{ Applied, Pending []Migration }
var ErrChecksumMismatch = errors.New("…checksum mismatch")CREATE TABLE IF NOT EXISTS firefly_migrations (
version INTEGER PRIMARY KEY,
description TEXT NOT NULL,
filename TEXT NOT NULL,
checksum TEXT NOT NULL,
applied_at TIMESTAMP NOT NULL
);When a migration is applied, its SHA-256 checksum is stored. If the
file is later edited (something you should never do —
migrations are append-only history), a subsequent Run returns
ErrChecksumMismatch rather than silently skipping.
Embed your migrations:
//go:embed db/*.sql
var migrationFS embed.FS
src := migrations.NewFSSource(migrationFS, "db")
if err := migrations.Run(ctx, db, src); err != nil { log.Fatal(err) }Inspect status:
st, _ := migrations.Inspect(ctx, db, src)
fmt.Printf("applied=%d pending=%d\n", len(st.Applied), len(st.Pending))Hand-built (tests):
src := &migrations.SliceSource{Items: []migrations.Migration{
{Version: 1, Filename: "V001__init.sql", SQL: `CREATE TABLE t (id INTEGER)`},
}}
_ = migrations.Run(ctx, db, src)cd migrations
go test ./...Covers fresh apply, idempotent re-run, checksum-mismatch rejection, and the embed.FS variant.