Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Atomic migrations #7909

Merged
merged 5 commits into from
Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion internal/db/dbutil/dbutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/url"
"os"
"strconv"
"strings"
"time"

// Register driver
Expand Down Expand Up @@ -110,8 +111,28 @@ func NewDB(dsn, app string) (*sql.DB, error) {
return db, nil
}

// injectVersionUpdate fixes the dirty state (set by golang-migrate) after a
// successful migration. If the frontend starts a migration that will turn out
// to be successful but does not stay alive for the duration of the query due to
// a startup timeout, there will be no chance to set the new version or unset
// the dirty flag. This function ensures that each successful migration sets the
// version and dirty flag itself, without requiring the frontend to be alive
// once the migration is committed.
//
// See https://github.com/golang-migrate/migrate/issues/325.
func injectVersionUpdate(f bindata.AssetFunc) bindata.AssetFunc {
return func(name string) ([]byte, error) {
oldContents, err := f(name)
if err != nil {
return nil, err
}
newContents := strings.Replace(string(oldContents), "COMMIT;", fmt.Sprintf("UPDATE schema_migrations SET dirty=false;\nCOMMIT;"), 1)
return []byte(newContents), nil
}
}

func NewMigrationSourceLoader(dataSource string) *bindata.AssetSource {
return bindata.Resource(migrations.AssetNames(), migrations.Asset)
return bindata.Resource(migrations.AssetNames(), injectVersionUpdate(migrations.Asset))
}

func NewMigrate(db *sql.DB, dataSource string) (*migrate.Migrate, error) {
Expand Down
12 changes: 12 additions & 0 deletions internal/db/dbutil/dbutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,15 @@ func TestPostgresDSN(t *testing.T) {
})
}
}

func TestInjectVersionUpdate(t *testing.T) {
gotContents, err := injectVersionUpdate(func(name string) ([]byte, error) { return []byte("BEGIN;\n-- some statements...\nCOMMIT;"), nil })("migrations/100_dummy.up.sql")
if err != nil {
t.Fatal(err)
}
got := string(gotContents)
want := "BEGIN;\n-- some statements...\nUPDATE schema_migrations SET dirty=false;\nCOMMIT;"
if got != want {
t.Errorf("incorrect contents: got != want\ngot: %v\nwant: %v", got, want)
}
}
21 changes: 21 additions & 0 deletions migrations/migrations_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package migrations_test

import (
"io/ioutil"
"path/filepath"
"reflect"
"sort"
Expand Down Expand Up @@ -39,6 +40,26 @@ func TestIDConstraints(t *testing.T) {
}
}

// Makes sure that every migration contains exactly one `COMMIT;` so that
// `InjectVersionUpdate` in internal/db/dbutil/dbutil.go is guaranteed to succeed.
func TestTransactions(t *testing.T) {
ups, err := filepath.Glob("*.up.sql")
if err != nil {
t.Fatal(err)
}

for _, name := range ups {
contents, err := ioutil.ReadFile(name)
if err != nil {
t.Fatalf("failed to read migration file %q: %v", name, err)
}
commitCount := strings.Count(string(contents), "COMMIT;")
if commitCount != 1 {
t.Fatalf("expected migration %q to contain exactly one COMMIT; but it contains %d", name, commitCount)
}
}
}

func TestNeedsGenerate(t *testing.T) {
want, err := filepath.Glob("*.sql")
if err != nil {
Expand Down