Migrations were run by hand and nothing recorded which had run, so re-running the setup script against an existing database failed on the first CREATE TABLE, and shipping a new migration gave an operator no way to know whether an estate had it. A missed migration is not a startup error - it is a query referencing a column that is not there, surfacing later on whichever endpoint touches it first. server/internal/migrate applies pending migrations at boot and refuses to start against a schema it does not match. One transaction per file holding both the DDL and the row that records it; an advisory lock so two servers starting at once cannot both apply 008; checksums so an edited migration is refused by name rather than silently skipped; numeric ordering so 010 does not run before 009. `migrate -baseline N` adopts a database built before any of this existed, because "the clients table exists" does not say whether 007's index does. Verified on the live database: adopted 001-007, applied 008. 008 adds two indexes on `purchases`, found by asking the database which foreign keys had nothing behind them and then checking what queries the table. The conversion report filters client_id + occurred_at, which is exactly the estate-wide case with no site to narrow it. run-local.sh had two bugs, both found by running it rather than reading it: it reused a broker container whose bind mount pointed at a directory that no longer existed, and it discarded stderr on the mosquitto_passwd call, so under `set -e` it exited at step 5 with no output at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
74 lines
2.1 KiB
Go
74 lines
2.1 KiB
Go
package migrate
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func TestMigrationsRunInNumericOrderNotAlphabetical(t *testing.T) {
|
|
// The bug this prevents: at ten migrations, "10_x.sql" sorts before
|
|
// "9_x.sql" as text, so the tenth would run before the ninth and the
|
|
// failure would be a column that does not exist yet - on the day the
|
|
// project happens to reach double figures.
|
|
src := fstest.MapFS{
|
|
"002_b.sql": {Data: []byte("select 2")},
|
|
"010_j.sql": {Data: []byte("select 10")},
|
|
"009_i.sql": {Data: []byte("select 9")},
|
|
"001_a.sql": {Data: []byte("select 1")},
|
|
}
|
|
files, err := Load(src)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
got := make([]int, len(files))
|
|
for i, f := range files {
|
|
got[i] = f.Version
|
|
}
|
|
want := []int{1, 2, 9, 10}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("order was %v, want %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTwoFilesCannotShareAVersion(t *testing.T) {
|
|
// Two people adding "008" on separate branches is the ordinary way this
|
|
// happens. Whichever runs first is then decided by the filesystem, which is
|
|
// not an order, and one of them silently never runs at all.
|
|
_, err := Load(fstest.MapFS{
|
|
"008_one.sql": {Data: []byte("select 1")},
|
|
"008_two.sql": {Data: []byte("select 2")},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("duplicate versions were accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "share version 8") {
|
|
t.Fatalf("error does not name the collision: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAFileThatIsNotNumberedIsRejected(t *testing.T) {
|
|
for _, name := range []string{"schema.sql", "abc_x.sql", "0_zero.sql"} {
|
|
if _, err := Load(fstest.MapFS{name: {Data: []byte("select 1")}}); err == nil {
|
|
t.Fatalf("%s was accepted as a migration", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheChecksumChangesWithTheContent(t *testing.T) {
|
|
// What makes an edited-after-applying migration detectable at all.
|
|
a, err := Load(fstest.MapFS{"001_a.sql": {Data: []byte("select 1")}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := Load(fstest.MapFS{"001_a.sql": {Data: []byte("select 2")}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a[0].Checksum == b[0].Checksum {
|
|
t.Fatal("two different migrations hashed the same")
|
|
}
|
|
}
|