test(migrate): verify canonical SQL-only version ordering

This commit is contained in:
end committed 2026-10-27 12:05:21 -07:00
1 parent badeefb9a9
commit d912b475b5
5 files changed
+77 -9

No files matched your search

+13 -5
View File
@@ -40,11 +40,19 @@ not defer it when sharing the pool with the application. Use `"."` for SQL files
at the embedded root. Files in the selected directory are discovered directly, at the embedded root. Files in the selected directory are discovered directly,
without recursively searching subdirectories. without recursively searching subdirectories.
Use unique positive versions in filenames such as Use unique positive versions and Goose's migration creator naming convention:
`20261027114500_create_users.sql`; sequential versions such as `00007_users.sql` zero-padded sequential names such as `00007_users.sql` and `00010_events.sql`, or
also work. Never derive a version from a file count or add one to the current fixed-width timestamps such as `20261027114500_create_users.sql`. Filenames must
version. Keep applied migrations immutable and ship the complete history. sort lexically in numeric version order, including when mixing naming schemes;
Goose rejects newly introduced migrations below the current version by default. choose padding wide enough for the complete history. Goose v3.29.0 preserves
lexical source order for SQL-only providers, so arbitrary unpadded names such as
`7_users.sql` and `10_events.sql` are unsupported by this house convention: they
can produce incorrect latest versions and source/status ordering. This package
does not rename or normalize migration files.
Never derive a version from a file count or add one to the current version.
Keep applied migrations immutable and ship the complete history. Goose rejects
newly introduced migrations below the current version by default.
```sql ```sql
-- +goose Up -- +goose Up
+3 -2
View File
@@ -23,8 +23,9 @@ import (
// affinity (transaction-pooling proxies cannot safely hold session locks). // affinity (transaction-pooling proxies cannot safely hold session locks).
// //
// Use the returned Provider's Up, UpTo, DownTo, Status, GetDBVersion, GetVersions, // Use the returned Provider's Up, UpTo, DownTo, Status, GetDBVersion, GetVersions,
// and HasPending methods directly. Versions may be sequential or timestamps; // and HasPending methods directly. Filenames must sort lexically in numeric
// Goose orders and tracks them. Global Go migration registration is ignored. // version order: use zero-padded sequential or fixed-width timestamp prefixes,
// as produced by Goose's migration creator. Global Go registration is ignored.
func New(db *sql.DB, dialect goose.Dialect, files embed.FS, dir string, logger *slog.Logger, migrations ...*goose.Migration) (*goose.Provider, error) { func New(db *sql.DB, dialect goose.Dialect, files embed.FS, dir string, logger *slog.Logger, migrations ...*goose.Migration) (*goose.Provider, error) {
for i, migration := range migrations { for i, migration := range migrations {
if migration == nil { if migration == nil {
+50 -2
View File
@@ -34,7 +34,7 @@ func TestSQLite(t *testing.T) {
name string name string
run func(*testing.T, *sql.DB, goose.Dialect) run func(*testing.T, *sql.DB, goose.Dialect)
}{ }{
{"flow", testFlow}, {"failure", testFailure}, {"flow", testFlow}, {"sql-only", testSQLOnly}, {"failure", testFailure},
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "migrate.db")) db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "migrate.db"))
@@ -56,7 +56,7 @@ func TestPostgres(t *testing.T) {
name string name string
run func(*testing.T, *sql.DB, goose.Dialect) run func(*testing.T, *sql.DB, goose.Dialect)
}{ }{
{"flow", testFlow}, {"failure", testFailure}, {"flow", testFlow}, {"sql-only", testSQLOnly}, {"failure", testFailure},
} { } {
t.Run(test.name, func(t *testing.T) { test.run(t, postgresDatabase(t)(), goose.DialectPostgres) }) t.Run(test.name, func(t *testing.T) { test.run(t, postgresDatabase(t)(), goose.DialectPostgres) })
} }
@@ -133,6 +133,47 @@ func testFlow(t *testing.T, db *sql.DB, dialect goose.Dialect) {
} }
} }
func testSQLOnly(t *testing.T, db *sql.DB, dialect goose.Dialect) {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
for _, test := range []struct {
dir string
versions []int64
}{
{"testdata/numeric", []int64{7, 10}},
{"testdata/migrations", []int64{7, 20261026093000, 20261027114500}},
} {
p := newProvider(t, db, dialect, test.dir)
assertState(t, ctx, p, 0, test.versions, 0)
results, err := p.UpTo(ctx, test.versions[1]-1)
if err != nil {
t.Fatal(err)
}
assertResults(t, results, test.versions[:1])
assertState(t, ctx, p, test.versions[0], test.versions, 1)
results, err = p.Up(ctx)
if err != nil {
t.Fatal(err)
}
assertResults(t, results, test.versions[1:])
assertState(t, ctx, p, test.versions[len(test.versions)-1], test.versions, len(test.versions))
assertEvents(t, ctx, db, test.versions)
results, err = p.DownTo(ctx, test.versions[1]-1)
if err != nil {
t.Fatal(err)
}
wantDown := slices.Clone(test.versions[1:])
slices.Reverse(wantDown)
assertResults(t, results, wantDown)
assertState(t, ctx, p, test.versions[0], test.versions, 1)
if _, err := p.DownTo(ctx, 0); err != nil {
t.Fatal(err)
}
assertState(t, ctx, p, 0, test.versions, 0)
}
}
func testFailure(t *testing.T, db *sql.DB, dialect goose.Dialect) { func testFailure(t *testing.T, db *sql.DB, dialect goose.Dialect) {
t.Helper() t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), time.Minute) ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
@@ -165,6 +206,13 @@ func newProvider(t *testing.T, db *sql.DB, dialect goose.Dialect, dir string, mi
func assertState(t *testing.T, ctx context.Context, p *goose.Provider, current int64, versions []int64, applied int) { func assertState(t *testing.T, ctx context.Context, p *goose.Provider, current int64, versions []int64, applied int) {
t.Helper() t.Helper()
var sources []int64
for _, source := range p.ListSources() {
sources = append(sources, source.Version)
}
if !slices.Equal(sources, versions) {
t.Fatalf("ListSources = %v; want %v", sources, versions)
}
got, err := p.GetDBVersion(ctx) got, err := p.GetDBVersion(ctx)
if err != nil || got != current { if err != nil || got != current {
t.Fatalf("GetDBVersion = %d, %v; want %d", got, err, current) t.Fatalf("GetDBVersion = %d, %v; want %d", got, err, current)
+6
View File
@@ -0,0 +1,6 @@
-- +goose Up
CREATE TABLE migration_events (version BIGINT PRIMARY KEY);
INSERT INTO migration_events (version) VALUES (7);
-- +goose Down
DROP TABLE migration_events;
+5
View File
@@ -0,0 +1,5 @@
-- +goose Up
INSERT INTO migration_events (version) VALUES (10);
-- +goose Down
DELETE FROM migration_events WHERE version = 10;