[#5618] added support to conditionally reapply migrations
This commit is contained in:
@@ -175,8 +175,13 @@ type App interface {
|
||||
AuxNonconcurrentDB() dbx.Builder
|
||||
|
||||
// HasTable checks if a table (or view) with the provided name exists (case insensitive).
|
||||
// in the current app.DB() instance.
|
||||
HasTable(tableName string) bool
|
||||
|
||||
// AuxHasTable checks if a table (or view) with the provided name exists (case insensitive)
|
||||
// in the current app.AuxDB() instance.
|
||||
AuxHasTable(tableName string) bool
|
||||
|
||||
// TableColumns returns all column names of a single table by its name.
|
||||
TableColumns(tableName string) ([]string, error)
|
||||
|
||||
|
||||
+25
-14
@@ -7,20 +7,6 @@ import (
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
// HasTable checks if a table (or view) with the provided name exists (case insensitive).
|
||||
func (app *BaseApp) HasTable(tableName string) bool {
|
||||
var exists bool
|
||||
|
||||
err := app.DB().Select("(1)").
|
||||
From("sqlite_schema").
|
||||
AndWhere(dbx.HashExp{"type": []any{"table", "view"}}).
|
||||
AndWhere(dbx.NewExp("LOWER([[name]])=LOWER({:tableName})", dbx.Params{"tableName": tableName})).
|
||||
Limit(1).
|
||||
Row(&exists)
|
||||
|
||||
return err == nil && exists
|
||||
}
|
||||
|
||||
// TableColumns returns all column names of a single table by its name.
|
||||
func (app *BaseApp) TableColumns(tableName string) ([]string, error) {
|
||||
columns := []string{}
|
||||
@@ -109,6 +95,31 @@ func (app *BaseApp) DeleteTable(tableName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// HasTable checks if a table (or view) with the provided name exists (case insensitive).
|
||||
// in the current app.DB() instance.
|
||||
func (app *BaseApp) HasTable(tableName string) bool {
|
||||
return app.hasTable(app.DB(), tableName)
|
||||
}
|
||||
|
||||
// AuxHasTable checks if a table (or view) with the provided name exists (case insensitive)
|
||||
// in the current app.AuxDB() instance.
|
||||
func (app *BaseApp) AuxHasTable(tableName string) bool {
|
||||
return app.hasTable(app.AuxDB(), tableName)
|
||||
}
|
||||
|
||||
func (app *BaseApp) hasTable(db dbx.Builder, tableName string) bool {
|
||||
var exists bool
|
||||
|
||||
err := db.Select("(1)").
|
||||
From("sqlite_schema").
|
||||
AndWhere(dbx.HashExp{"type": []any{"table", "view"}}).
|
||||
AndWhere(dbx.NewExp("LOWER([[name]])=LOWER({:tableName})", dbx.Params{"tableName": tableName})).
|
||||
Limit(1).
|
||||
Row(&exists)
|
||||
|
||||
return err == nil && exists
|
||||
}
|
||||
|
||||
// Vacuum executes VACUUM on the current app.DB() instance
|
||||
// in order to reclaim unused data db disk space.
|
||||
func (app *BaseApp) Vacuum() error {
|
||||
|
||||
@@ -42,6 +42,31 @@ func TestHasTable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuxHasTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
tableName string
|
||||
expected bool
|
||||
}{
|
||||
{"", false},
|
||||
{"test", false},
|
||||
{"_lOGS", true}, // table names are case insensitives by default
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.tableName, func(t *testing.T) {
|
||||
result := app.AuxHasTable(s.tableName)
|
||||
if result != s.expected {
|
||||
t.Fatalf("Expected %v, got %v", s.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableColumns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+23
-4
@@ -7,9 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type Migration struct {
|
||||
Up func(txApp App) error
|
||||
Down func(txApp App) error
|
||||
File string
|
||||
Up func(txApp App) error
|
||||
Down func(txApp App) error
|
||||
File string
|
||||
ReapplyCondition func(txApp App, runner *MigrationsRunner, fileName string) (bool, error)
|
||||
}
|
||||
|
||||
// MigrationsList defines a list with migration definitions
|
||||
@@ -34,9 +35,27 @@ func (l *MigrationsList) Copy(list MigrationsList) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds adds an existing migration definition to the list.
|
||||
//
|
||||
// If m.File is not provided, it will try to get the name from its .go file.
|
||||
//
|
||||
// The list will be sorted automatically based on the migrations file name.
|
||||
func (l *MigrationsList) Add(m *Migration) {
|
||||
if m.File == "" {
|
||||
_, path, _, _ := runtime.Caller(1)
|
||||
m.File = filepath.Base(path)
|
||||
}
|
||||
|
||||
l.list = append(l.list, m)
|
||||
|
||||
sort.SliceStable(l.list, func(i int, j int) bool {
|
||||
return l.list[i].File < l.list[j].File
|
||||
})
|
||||
}
|
||||
|
||||
// Register adds new migration definition to the list.
|
||||
//
|
||||
// If `optFilename` is not provided, it will try to get the name from its .go file.
|
||||
// If optFilename is not provided, it will try to get the name from its .go file.
|
||||
//
|
||||
// The list will be sorted automatically based on the migrations file name.
|
||||
func (l *MigrationsList) Register(
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
|
||||
func TestMigrationsList(t *testing.T) {
|
||||
l1 := core.MigrationsList{}
|
||||
l1.Add(&core.Migration{File: "5_test.go"})
|
||||
l1.Add(&core.Migration{ /* auto detect file name */ })
|
||||
l1.Register(nil, nil, "3_test.go")
|
||||
l1.Register(nil, nil, "1_test.go")
|
||||
l1.Register(nil, nil, "2_test.go")
|
||||
@@ -22,12 +24,19 @@ func TestMigrationsList(t *testing.T) {
|
||||
"2_test.go",
|
||||
"3_test.go",
|
||||
"4_test.go",
|
||||
"5_test.go",
|
||||
// twice because there 2 test migrations with auto filename
|
||||
"migrations_list_test.go",
|
||||
"migrations_list_test.go",
|
||||
}
|
||||
|
||||
items := l2.Items()
|
||||
if len(items) != len(expected) {
|
||||
t.Fatalf("Expected %d items, got %d: \n%#v", len(expected), len(items), items)
|
||||
names := make([]string, len(items))
|
||||
for i, item := range items {
|
||||
names[i] = item.File
|
||||
}
|
||||
t.Fatalf("Expected %d items, got %d:\n%v", len(expected), len(names), names)
|
||||
}
|
||||
|
||||
for i, name := range expected {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
)
|
||||
|
||||
var AppMigrations MigrationsList
|
||||
|
||||
var SystemMigrations MigrationsList
|
||||
|
||||
const DefaultMigrationsTable = "_migrations"
|
||||
@@ -134,9 +133,23 @@ func (r *MigrationsRunner) Up() ([]string, error) {
|
||||
err := r.app.AuxRunInTransaction(func(txApp App) error {
|
||||
return txApp.RunInTransaction(func(txApp App) error {
|
||||
for _, m := range r.migrationsList.Items() {
|
||||
// skip applied
|
||||
// applied migrations check
|
||||
if r.isMigrationApplied(txApp, m.File) {
|
||||
continue
|
||||
if m.ReapplyCondition == nil {
|
||||
continue // no need to reapply
|
||||
}
|
||||
|
||||
shouldReapply, err := m.ReapplyCondition(txApp, r, m.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !shouldReapply {
|
||||
continue
|
||||
}
|
||||
|
||||
// clear previous history stored entry
|
||||
// (it will be recreated after successful execution)
|
||||
r.saveRevertedMigration(txApp, m.File)
|
||||
}
|
||||
|
||||
// ignore empty Up action
|
||||
|
||||
@@ -41,11 +41,51 @@ func TestMigrationsRunnerUpAndDown(t *testing.T) {
|
||||
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 run migration
|
||||
// ---------------------------------------------------------------
|
||||
// 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()
|
||||
@@ -61,7 +101,7 @@ func TestMigrationsRunnerUpAndDown(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedUpCallsOrder := `["up1","up3"]` // skip up2 since it was applied previously
|
||||
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 {
|
||||
@@ -79,9 +119,9 @@ func TestMigrationsRunnerUpAndDown(t *testing.T) {
|
||||
|
||||
// simulate unrun migration
|
||||
l.Register(nil, func(app core.App) error {
|
||||
callsOrder = append(callsOrder, "down4")
|
||||
callsOrder = append(callsOrder, "down6")
|
||||
return nil
|
||||
}, "4_test")
|
||||
}, "6_test")
|
||||
|
||||
// simulate applied migrations from different migrations list
|
||||
_, err = app.DB().Insert(core.DefaultMigrationsTable, dbx.Params{
|
||||
@@ -102,7 +142,7 @@ func TestMigrationsRunnerUpAndDown(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedDownCallsOrder := `["down3","down1"]` // revert in the applied order
|
||||
expectedDownCallsOrder := `["down5","down3"]` // revert in the applied order
|
||||
|
||||
downCallsOrder, err := json.Marshal(callsOrder)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user