package core_test import ( "encoding/json" "fmt" "testing" "time" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tests" ) func TestMigrationsRunnerUpAndDown(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() callsOrder := []string{} l := core.MigrationsList{} l.Register(func(app core.App) error { callsOrder = append(callsOrder, "up2") return nil }, func(app core.App) error { callsOrder = append(callsOrder, "down2") return nil }, "2_test") l.Register(func(app core.App) error { callsOrder = append(callsOrder, "up3") return nil }, func(app core.App) error { callsOrder = append(callsOrder, "down3") return nil }, "3_test") l.Register(func(app core.App) error { callsOrder = append(callsOrder, "up1") return nil }, func(app core.App) error { callsOrder = append(callsOrder, "down1") return nil }, "1_test") l.Register(func(app core.App) error { callsOrder = append(callsOrder, "up4") return nil }, func(app core.App) error { callsOrder = append(callsOrder, "down4") return nil }, "4_test") l.Add(&core.Migration{ Up: func(app core.App) error { callsOrder = append(callsOrder, "up5") return nil }, Down: func(app core.App) error { callsOrder = append(callsOrder, "down5") return nil }, File: "5_test", ReapplyCondition: func(txApp core.App, runner *core.MigrationsRunner, fileName string) (bool, error) { return true, nil }, }) runner := core.NewMigrationsRunner(app, l) // --------------------------------------------------------------- // simulate partially out-of-order applied migration // --------------------------------------------------------------- _, err := app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ "file": "4_test", "applied": time.Now().UnixMicro() - 2, }).Execute() if err != nil { t.Fatalf("Failed to insert 5_test migration: %v", err) } _, err = app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ "file": "5_test", "applied": time.Now().UnixMicro() - 1, }).Execute() if err != nil { t.Fatalf("Failed to insert 5_test migration: %v", err) } _, err = app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ "file": "2_test", "applied": time.Now().UnixMicro(), }).Execute() if err != nil { t.Fatalf("Failed to insert 2_test migration: %v", err) } // --------------------------------------------------------------- // Up() // --------------------------------------------------------------- if _, err := runner.Up(); err != nil { t.Fatal(err) } expectedUpCallsOrder := `["up1","up3","up5"]` // skip up2 and up4 since they were applied already (up5 has extra reapply condition) upCallsOrder, err := json.Marshal(callsOrder) if err != nil { t.Fatal(err) } if v := string(upCallsOrder); v != expectedUpCallsOrder { t.Fatalf("Expected Up() calls order %s, got %s", expectedUpCallsOrder, upCallsOrder) } // --------------------------------------------------------------- // reset callsOrder callsOrder = []string{} // simulate unrun migration l.Register(nil, func(app core.App) error { callsOrder = append(callsOrder, "down6") return nil }, "6_test") // simulate applied migrations from different migrations list _, err = app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ "file": "from_different_list", "applied": time.Now().UnixMicro(), }).Execute() if err != nil { t.Fatalf("Failed to insert from_different_list migration: %v", err) } // --------------------------------------------------------------- // --------------------------------------------------------------- // Down() // --------------------------------------------------------------- if _, err := runner.Down(2); err != nil { t.Fatal(err) } expectedDownCallsOrder := `["down5","down3"]` // revert in the applied order downCallsOrder, err := json.Marshal(callsOrder) if err != nil { t.Fatal(err) } if v := string(downCallsOrder); v != expectedDownCallsOrder { t.Fatalf("Expected Down() calls order %s, got %s", expectedDownCallsOrder, downCallsOrder) } } func TestMigrationsRunnerRemoveMissingAppliedMigrations(t *testing.T) { t.Parallel() app, _ := tests.NewTestApp() defer app.Cleanup() // mock migrations history for i := 1; i <= 3; i++ { _, err := app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{ "file": fmt.Sprintf("%d_test", i), "applied": time.Now().UnixMicro(), }).Execute() if err != nil { t.Fatal(err) } } if !isMigrationApplied(app, "2_test") { t.Fatalf("Expected 2_test migration to be applied") } // create a runner without 2_test to mock deleted migration l := core.MigrationsList{} l.Register(func(app core.App) error { return nil }, func(app core.App) error { return nil }, "1_test") l.Register(func(app core.App) error { return nil }, func(app core.App) error { return nil }, "3_test") r := core.NewMigrationsRunner(app, l) if err := r.RemoveMissingAppliedMigrations(); err != nil { t.Fatalf("Failed to remove missing applied migrations: %v", err) } if isMigrationApplied(app, "2_test") { t.Fatalf("Expected 2_test migration to NOT be applied") } } func isMigrationApplied(app core.App, file string) bool { var exists bool err := app.DB().Select("count(*)"). From(core.DefaultMigrationsTable). Where(dbx.HashExp{"file": file}). Limit(1). Row(&exists) return err == nil && exists } // // ------------------------------------------------------------------- // type testDB struct { // *dbx.DB // CalledQueries []string // } // // NB! Don't forget to call `db.Close()` at the end of the test. // func createTestDB() (*testDB, error) { // sqlDB, err := sql.Open("sqlite", ":memory:") // if err != nil { // return nil, err // } // db := testDB{DB: dbx.NewFromDB(sqlDB, "sqlite")} // db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { // db.CalledQueries = append(db.CalledQueries, sql) // } // db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { // db.CalledQueries = append(db.CalledQueries, sql) // } // return &db, nil // }