[#5618] added support to conditionally reapply migrations

This commit is contained in:
Gani Georgiev
2024-10-08 16:23:58 +03:00
parent ed1dc54f27
commit 646331bfa2
13 changed files with 320 additions and 207 deletions
+5
View File
@@ -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
View File
@@ -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 {
+25
View File
@@ -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
View File
@@ -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(
+10 -1
View File
@@ -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 {
+16 -3
View File
@@ -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
+45 -5
View File
@@ -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 {