Ordered Migrations
Every exercise so far has started from a schema handed to you fully formed. A
real project never gets that luxury: the schema you have today is the result
of every change anyone has ever made to it, applied one at a time, in the
order they were written. That sequence — not the final CREATE TABLE — is
what a migration system actually manages.
migrations := []string{
`CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT NOT NULL)`,
`ALTER TABLE tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 1`,
}
Run these in either order and you learn something. Run them forwards and you
get a tasks table with a priority column. Swap them and the ALTER TABLE
fails — there's no tasks table yet to alter. The statements aren't
independent facts about the schema; they're a history, and history only
replays one way.
Fail loud, and say which one
for i, stmt := range migrations {
if _, err := db.Exec(stmt); err != nil {
return fmt.Errorf("migration %d: %w", i+1, err)
}
}
The error doesn't just say "a migration failed" — it says which one, by position. That index is the difference between a five-second fix and an afternoon spent re-reading a growing slice of SQL strings to find the one that no longer applies cleanly. As the list grows past ten or twenty entries — which happens fast on a real project — this is the only thing standing between you and guessing.
Stop, don't skip
The loop returns on the first error instead of trying the rest. That's not caution for its own sake: migration 2 might create the table that migration 3 alters, so once one fails, every migration after it is now running against a schema state that was never supposed to exist. Skipping ahead doesn't recover from a broken migration — it compounds it. The correct response to migration 2 failing is to stop, fix it, and re-run from the top, which is exactly what the next lesson makes possible without redoing work that already succeeded.
Try it
Implement ApplyMigrations. It runs each statement in the order given and,
on failure, reports which migration failed without touching anything after
it.
Practice
1. Apply Migrations In Order
IntermediateImplement
ApplyMigrations(db *sql.DB, migrations []string) error.- Run each statement in
migrations, in order, viadb.Exec. - On the first failure, stop — do not run any statement after it — and
return an error that names the 1-based index of the migration that failed
(e.g.
migration 2: ...), wrapping the underlying error with%w. - If every statement succeeds, return
nil.
package main import ( "database/sql" "fmt" _ "modernc.org/sqlite" ) // ApplyMigrations runs each statement in order, stopping on the first error. func ApplyMigrations(db *sql.DB, migrations []string) error { // TODO: implement return fmt.Errorf("not implemented") } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- Run each statement in