package db import ( "testing" ) // TestMigrationVersionsAreUnique guards the failure mode that shipped two // migrations numbered 0028. // // runMigrations keys applied migrations by the integer parsed from the // filename and skips any version already present in schema_migrations. Two // files sharing an ordinal therefore mean the first one applied records the // version and the second is skipped in silence — no error, no log. Fresh // installs hide it completely, because they take the applySchema path where // schema.sql already contains everything and the migrations are only recorded. // Only an upgrade of an existing database is affected, which is the one case // with no test coverage and the most to lose. func TestMigrationVersionsAreUnique(t *testing.T) { migrations, err := loadMigrations() if err != nil { t.Fatalf("loadMigrations: %v", err) } if len(migrations) == 0 { t.Fatal("loadMigrations returned no migrations") } seen := make(map[int]string, len(migrations)) for _, m := range migrations { if prev, dup := seen[m.Version]; dup { t.Errorf("duplicate migration version %04d: %q and %q — one of them will be silently skipped on an existing database; renumber the later one", m.Version, prev, m.Name) continue } seen[m.Version] = m.Name } } // TestMigrationsApplyToExistingDatabase exercises the upgrade path rather than // the fresh-install path: it forces every migration to be treated as pending // and asserts they all apply. A migration that is skipped because it collides // with another version fails here even though InitDB on a fresh database would // report success. func TestMigrationsApplyToExistingDatabase(t *testing.T) { db, err := InitDB(":memory:", LibsqlConfig{}) if err != nil { t.Fatalf("InitDB: %v", err) } defer db.Close() if _, err := db.Exec("DELETE FROM schema_migrations"); err != nil { t.Fatalf("clear schema_migrations: %v", err) } migrations, err := loadMigrations() if err != nil { t.Fatalf("loadMigrations: %v", err) } // freshDB=true records each pending migration without executing its DDL, // which is what we want here: this database already has schema.sql applied, // and re-running additive ALTERs would fail for reasons unrelated to the // collision. The duplicate-version skip happens before the freshDB branch, // so a collision still shows up as a missing row below. if err := runMigrations(db, true); err != nil { t.Fatalf("runMigrations: %v", err) } for _, m := range migrations { var count int if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = ?", m.Version).Scan(&count); err != nil { t.Fatalf("query schema_migrations for %d: %v", m.Version, err) } if count == 0 { t.Errorf("migration %04d (%s) was never recorded as applied", m.Version, m.Name) } } var recorded int if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&recorded); err != nil { t.Fatalf("count schema_migrations: %v", err) } if recorded != len(migrations) { t.Errorf("recorded %d migrations but have %d files; a shortfall means two files share a version", recorded, len(migrations)) } }