瀏覽代碼

initial working version

Aneurin Barker Snook 11 月之前
當前提交
97bce22b38
共有 7 個文件被更改,包括 368 次插入0 次删除
  1. 1 0
      .gitignore
  2. 46 0
      error.go
  3. 35 0
      func_migration.go
  4. 5 0
      go.mod
  5. 12 0
      interfaces.go
  6. 105 0
      module.go
  7. 164 0
      module_test.go

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+go.sum

+ 46 - 0
error.go

@@ -0,0 +1,46 @@
+package migres
+
+import (
+	"fmt"
+
+	"github.com/annybs/go/version"
+)
+
+// Migration error.
+var (
+	ErrMigrationFailed = Error{Message: "migration failed at version %q: %s"}
+)
+
+// Error reflects an error that occurred during a migration.
+type Error struct {
+	Message       string           // Error message template.
+	PreviousError error            // Original error encountered during the migration.
+	Version       *version.Version // Version at which the migration error occured.
+	LastVersion   *version.Version // Version
+}
+
+// Error retrieves the message of a migration error.
+// This does not necessarily include all information about the error, such as the last
+func (e *Error) Error() string {
+	return fmt.Sprintf(e.Message, e.Version, e.PreviousError)
+}
+
+// Is determines whether the Error is an instance of the target.
+// https://pkg.go.dev/errors#Is
+//
+// This implementation does not compare versions.
+func (e *Error) Is(target error) bool {
+	if t, ok := target.(*Error); ok {
+		return t.Message == e.Message
+	}
+	return false
+}
+
+func failMigration(err error, v, last *version.Version) *Error {
+	return &Error{
+		Message:       ErrMigrationFailed.Message,
+		PreviousError: err,
+		Version:       v,
+		LastVersion:   last,
+	}
+}

+ 35 - 0
func_migration.go

@@ -0,0 +1,35 @@
+package migres
+
+// FuncMigration enables creating a functional migration using callback functions.
+//
+//	type MyModule struct{}
+//
+//	func Module() migres.Module {
+//	  return migres.Module{
+//	    "1.0.0": migres.Func(MyModule.upgradeV1, MyModule.downgradeV1),
+//	    "2.0.0": migres.Func(MyModule.upgradeV2, MyModule.downgradeV2),
+//	  }
+//	}
+//
+// In this example MyModule can be defined with multiple upgrade/downgrade functions.
+// This may be simpler than defining separate migration structs in many cases.
+type FuncMigration struct {
+	D func() error
+	U func() error
+}
+
+func (fm *FuncMigration) Downgrade() error {
+	return fm.D()
+}
+
+func (fm *FuncMigration) Upgrade() error {
+	return fm.U()
+}
+
+// Func creates a functional migration.
+func Func(up, down func() error) *FuncMigration {
+	return &FuncMigration{
+		D: down,
+		U: up,
+	}
+}

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module github.com/annybs/migres
+
+go 1.21.4
+
+require github.com/annybs/go/version v0.0.0-20240715075456-18d811b39774

+ 12 - 0
interfaces.go

@@ -0,0 +1,12 @@
+package migres
+
+// Migration is anything that can upgrade or downgrade external state - commonly, but not limited to, database schemas.
+//
+// Each migration SHOULD be able to upgrade or downgrade freely, allowing any changes to be reverted with ease.
+//
+// Of course, this is not always possible.
+// In the case of irreversible state change, the opposite function should return an error e.g. if an upgrade deletes something irrecoverably, have the corresponding downgrade function throw a descriptive error.
+type Migration interface {
+	Downgrade() error // Perform a downgrade.
+	Upgrade() error   // Perform an upgrade.
+}

+ 105 - 0
module.go

@@ -0,0 +1,105 @@
+package migres
+
+import (
+	"slices"
+	"sort"
+
+	"github.com/annybs/go/version"
+)
+
+// Module provides migrations keyed by version string.
+// This helps to organise migrations execute upgrades or downgrades in the correct version order.
+//
+// For example:
+//
+//	mod := Module{"1": migration1, "3": migration3, "2": migration2}
+//	mod.Upgrade("1", "3")
+//
+// Here, the module's three migrations will be sorted into the proper order of 1-2-3, then their upgrades will be performed in that same order.
+type Module map[string]Migration
+
+// Downgrade migrations at versions after from until (and including) to.
+func (mod Module) Downgrade(from, to string) error {
+	fromVersion, err := version.Parse(from)
+	if err != nil {
+		return err
+	}
+
+	toVersion, err := version.Parse(to)
+	if err != nil {
+		return err
+	}
+
+	versions, err := mod.Versions()
+	if err != nil {
+		return err
+	}
+
+	versions = versions.Match(&version.Constraint{
+		Lte: fromVersion,
+		Gt:  toVersion,
+	})
+	sort.Stable(versions)
+	slices.Reverse(versions)
+
+	var lastVersion *version.Version
+	for _, version := range versions {
+		m := mod[version.Text]
+		if err := m.Downgrade(); err != nil {
+			return failMigration(err, version, lastVersion)
+		}
+		lastVersion = version
+	}
+
+	return nil
+}
+
+// Upgrade migrations at versions after from until (and including) to.
+func (mod Module) Upgrade(from, to string) error {
+	fromVersion, err := version.Parse(from)
+	if err != nil {
+		return err
+	}
+
+	toVersion, err := version.Parse(to)
+	if err != nil {
+		return err
+	}
+
+	versions, err := mod.Versions()
+	if err != nil {
+		return err
+	}
+
+	versions = versions.Match(&version.Constraint{
+		Gt:  fromVersion,
+		Lte: toVersion,
+	})
+	sort.Stable(versions)
+
+	var lastVersion *version.Version
+	for _, version := range versions {
+		m := mod[version.Text]
+		if err := m.Upgrade(); err != nil {
+			return failMigration(err, version, lastVersion)
+		}
+		lastVersion = version
+	}
+
+	return nil
+}
+
+// Versions gets a list of all versions in the module.
+func (mod Module) Versions() (version.List, error) {
+	list := version.List{}
+
+	for str := range mod {
+		v, err := version.Parse(str)
+		if err != nil {
+			return nil, err
+		}
+		list = append(list, v)
+	}
+
+	return list, nil
+}

+ 164 - 0
module_test.go

@@ -0,0 +1,164 @@
+package migres
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/annybs/go/version"
+)
+
+func TestModule_Upgrade(t *testing.T) {
+	type TestCase struct {
+		Input Module
+		Up    bool
+		From  string
+		To    string
+
+		Expected []string
+		Err      error
+	}
+
+	testOutput := []string{}
+
+	testMigration := func(v string, ok bool) Migration {
+		f := func() error {
+			if ok {
+				testOutput = append(testOutput, v)
+				return nil
+			}
+			return errors.New("test")
+		}
+		return Func(f, f)
+	}
+
+	testCases := []TestCase{
+		{
+			Input: Module{
+				"1.0.0": testMigration("1.0.0", true),
+				"2.0.0": testMigration("2.0.0", true),
+				"3.0.0": testMigration("3.0.0", true),
+			},
+			Up:   true,
+			From: "0",
+			To:   "3",
+
+			Expected: []string{"1.0.0", "2.0.0", "3.0.0"},
+		},
+		{
+			Input: Module{
+				"1.0.0": testMigration("1.0.0", true),
+				"2.0.0": testMigration("2.0.0", true),
+				"3.0.0": testMigration("3.0.0", true),
+			},
+			Up:   true,
+			From: "1",
+			To:   "3",
+
+			Expected: []string{"2.0.0", "3.0.0"},
+		},
+		{
+			Input: Module{
+				"1.0.0": testMigration("1.0.0", true),
+				"2.0.0": testMigration("2.0.0", true),
+				"3.0.0": testMigration("3.0.0", true),
+			},
+			From: "3",
+			To:   "0",
+
+			Expected: []string{"3.0.0", "2.0.0", "1.0.0"},
+		},
+		{
+			Input: Module{
+				"1.0.0": testMigration("1.0.0", true),
+				"2.0.0": testMigration("2.0.0", true),
+				"3.0.0": testMigration("3.0.0", true),
+			},
+			From: "3",
+			To:   "2",
+
+			Expected: []string{"3.0.0"},
+		},
+		{
+			Input: Module{
+				"1.0.0": testMigration("1.0.0", true),
+				"2.0.0": testMigration("2.0.0", false),
+				"3.0.0": testMigration("3.0.0", true),
+			},
+			Up:   true,
+			From: "0",
+			To:   "3",
+
+			Err: failMigration(errors.New("test"), version.MustParse("2.0.0"), version.MustParse("1.0.0")),
+		},
+		{
+			Input: Module{
+				"1.0.0": testMigration("1.0.0", true),
+				"2.0.0": testMigration("2.0.0", false),
+				"3.0.0": testMigration("3.0.0", true),
+			},
+			From: "4",
+			To:   "1",
+
+			Err: failMigration(errors.New("test"), version.MustParse("2.0.0"), version.MustParse("3.0.0")),
+		},
+		{
+			Input: Module{
+				"1.0.0": testMigration("1.0.0", true),
+				"2.0.0": testMigration("2.0.0", true),
+				"3.0.0": testMigration("3.0.0", false),
+			},
+			From: "4",
+			To:   "1",
+
+			Err: failMigration(errors.New("test"), version.MustParse("3.0.0"), nil),
+		},
+	}
+
+	for i, testCase := range testCases {
+		testOutput = []string{}
+
+		var err error
+		if testCase.Up {
+			err = testCase.Input.Upgrade(testCase.From, testCase.To)
+		} else {
+			err = testCase.Input.Downgrade(testCase.From, testCase.To)
+		}
+
+		if err != nil {
+			if testCase.Err == nil {
+				t.Errorf("test %d failed (expected nil error, got error %v)", i, err)
+			} else if !errors.Is(err, testCase.Err) {
+				t.Errorf("test %d failed (expected error %v, got error %v)", i, testCase.Err, err)
+			} else {
+				a := err.(*Error)
+				e := testCase.Err.(*Error)
+				if !a.Version.Equal(e.Version) {
+					t.Errorf("test %d failed (expected error.Version %s, got error.Version %s)", i, e.Version, a.Version)
+				} else if !a.LastVersion.Equal(e.LastVersion) {
+					t.Errorf("test %d failed (expected error.LastVersion %s, got error.LastVersion %s)", i, e.LastVersion, a.LastVersion)
+				} else {
+					t.Logf("test %d passed (expected error %v, got error %v)", i, testCase.Err, err)
+				}
+			}
+			continue
+		} else if testCase.Err != nil {
+			t.Errorf("test %d failed (expected error %v, got nil error)", i, testCase.Err)
+			continue
+		}
+
+		if len(testOutput) != len(testCase.Expected) {
+			t.Errorf("test %d failed (expected %v, got %v)", i, testCase.Expected, testOutput)
+			continue
+		}
+		ok := true
+		for j, v := range testOutput {
+			if v != testCase.Expected[j] {
+				t.Errorf("test %d failed (expected version %q at index %d, got version %q)", i, testCase.Expected[j], j, v)
+				ok = false
+			}
+		}
+		if ok {
+			t.Logf("test %d passed (expected %v, got %v)", i, testCase.Expected, testOutput)
+		}
+	}
+}