Repository: ariga/atlas
Pull request #732 (2022-04-24 13:17:43)

sql/schema: add helper methods for working with changesets
I'm not super enthusiastic about this API, but we should make users' life easier when working with changes.  


Patch:
diff --git a/sql/schema/migrate.go b/sql/schema/migrate.go
index 6cf7cf7a..b2dccc85 100644
--- a/sql/schema/migrate.go
+++ b/sql/schema/migrate.go
@@ -283,6 +283,88 @@ type (
 	}
 )
 
+// Changes is a list of changes allow for searching and mutating changes.
+type Changes []Change
+
+// IndexAddTable returns the index of the first AddTable in the changes with
+// the given name, or -1 if there is no such change in the Changes.
+func (c Changes) IndexAddTable(name string) int {
+	return c.search(func(c Change) bool {
+		a, ok := c.(*AddTable)
+		return ok && a.T.Name == name
+	})
+}
+
+// IndexDropTable returns the index of the first DropTable in the changes with
+// the given name, or -1 if there is no such change in the Changes.
+func (c Changes) IndexDropTable(name string) int {
+	return c.search(func(c Change) bool {
+		a, ok := c.(*DropTable)
+		return ok && a.T.Name == name
+	})
+}
+
+// IndexAddColumn returns the index of the first AddColumn in the changes with
+// the given name, or -1 if there is no such change in the Changes.
+func (c Changes) IndexAddColumn(name string) int {
+	return c.search(func(c Change) bool {
+		a, ok := c.(*AddColumn)
+		return ok && a.C.Name == name
+	})
+}
+
+// IndexDropColumn returns the index of the first DropColumn in the changes with
+// the given name, or -1 if there is no such change in the Changes.
+func (c Changes) IndexDropColumn(name string) int {
+	return c.search(func(c Change) bool {
+		d, ok := c.(*DropColumn)
+		return ok && d.C.Name == name
+	})
+}
+
+// IndexAddIndex returns the index of the first AddIndex in the changes with
+// the given name, or -1 if there is no such change in the Changes.
+func (c Changes) IndexAddIndex(name string) int {
+	return c.search(func(c Change) bool {
+		a, ok := c.(*AddIndex)
+		return ok && a.I.Name == name
+	})
+}
+
+// IndexDropIndex returns the index of the first DropIndex in the changes with
+// the given name, or -1 if there is no such change in the Changes.
+func (c Changes) IndexDropIndex(name string) int {
+	return c.search(func(c Change) bool {
+		a, ok := c.(*DropIndex)
+		return ok && a.I.Name == name
+	})
+}
+
+// RemoveIndex removes elements in the given indexes from the Changes.
+func (c *Changes) RemoveIndex(indexes ...int) {
+	changes := make([]Change, 0, len(*c)-len(indexes))
+Loop:
+	for i := range *c {
+		for _, idx := range indexes {
+			if i == idx {
+				continue Loop
+			}
+		}
+		changes = append(changes, (*c)[i])
+	}
+	*c = changes
+}
+
+// search returns the index of the first call to f that returns true, or -1.
+func (c Changes) search(f func(Change) bool) int {
+	for i := range c {
+		if f(c[i]) {
+			return i
+		}
+	}
+	return -1
+}
+
 // changes.
 func (*AddAttr) change()          {}
 func (*DropAttr) change()         {}


Test patch:
diff --git a/sql/schema/migrate_test.go b/sql/schema/migrate_test.go
new file mode 100644
index 00000000..3f12dfae
--- /dev/null
+++ b/sql/schema/migrate_test.go
@@ -0,0 +1,126 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package schema_test
+
+import (
+	"fmt"
+	"log"
+	"strconv"
+	"testing"
+
+	"ariga.io/atlas/sql/schema"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestChanges_IndexAddTable(t *testing.T) {
+	changes := schema.Changes{
+		&schema.AddTable{T: schema.NewTable("users")},
+		&schema.DropTable{T: schema.NewTable("posts")},
+		&schema.AddTable{T: schema.NewTable("posts")},
+		&schema.AddTable{T: schema.NewTable("posts")},
+	}
+	require.Equal(t, 2, changes.IndexAddTable("posts"))
+	require.Equal(t, -1, changes.IndexAddTable("post_tags"))
+}
+
+func TestChanges_IndexDropTable(t *testing.T) {
+	changes := schema.Changes{
+		&schema.DropTable{T: schema.NewTable("users")},
+		&schema.AddTable{T: schema.NewTable("posts")},
+		&schema.DropTable{T: schema.NewTable("posts")},
+	}
+	require.Equal(t, 2, changes.IndexDropTable("posts"))
+	require.Equal(t, -1, changes.IndexDropTable("post_tags"))
+}
+
+func TestChanges_IndexAddColumn(t *testing.T) {
+	changes := schema.Changes{
+		&schema.AddColumn{C: schema.NewColumn("name")},
+		&schema.DropColumn{C: schema.NewColumn("name")},
+		&schema.AddColumn{C: schema.NewColumn("name")},
+	}
+	require.Equal(t, 0, changes.IndexAddColumn("name"))
+	require.Equal(t, -1, changes.IndexAddColumn("created_at"))
+}
+
+func TestChanges_IndexDropColumn(t *testing.T) {
+	changes := schema.Changes{
+		&schema.AddColumn{C: schema.NewColumn("name")},
+		&schema.DropColumn{C: schema.NewColumn("name")},
+		&schema.AddColumn{C: schema.NewColumn("name")},
+	}
+	require.Equal(t, 1, changes.IndexDropColumn("name"))
+	require.Equal(t, -1, changes.IndexDropColumn("created_at"))
+}
+
+func TestChanges_IndexAddIndex(t *testing.T) {
+	changes := schema.Changes{
+		&schema.DropIndex{I: schema.NewIndex("name")},
+		&schema.AddIndex{I: schema.NewIndex("created_at")},
+		&schema.AddIndex{I: schema.NewIndex("name")},
+	}
+	require.Equal(t, 2, changes.IndexAddIndex("name"))
+	require.Equal(t, -1, changes.IndexAddIndex("age"))
+}
+
+func TestChanges_IndexDropIndex(t *testing.T) {
+	changes := schema.Changes{
+		&schema.AddIndex{I: schema.NewIndex("name")},
+		&schema.DropIndex{I: schema.NewIndex("created_at")},
+		&schema.DropIndex{I: schema.NewIndex("name")},
+	}
+	require.Equal(t, 2, changes.IndexDropIndex("name"))
+	require.Equal(t, -1, changes.IndexDropIndex("age"))
+}
+
+func TestChanges_RemoveIndex(t *testing.T) {
+	changes := make(schema.Changes, 0, 5)
+	for i := 0; i < 5; i++ {
+		changes = append(changes, &schema.AddColumn{C: schema.NewColumn(strconv.Itoa(i))})
+	}
+	changes.RemoveIndex(0)
+	require.Equal(t, 4, len(changes))
+	for i := 0; i < 4; i++ {
+		require.Equal(t, strconv.Itoa(i+1), changes[i].(*schema.AddColumn).C.Name)
+	}
+	changes.RemoveIndex(0, 3, 2)
+	require.Equal(t, 1, len(changes))
+	require.Equal(t, "2", changes[0].(*schema.AddColumn).C.Name)
+}
+
+func ExampleChanges_Replace() {
+	changes := schema.Changes{
+		&schema.AddIndex{I: schema.NewIndex("id")},
+		&schema.AddColumn{C: schema.NewColumn("new_name")},
+		&schema.AddColumn{C: schema.NewColumn("id")},
+		&schema.AddColumn{C: schema.NewColumn("created_at")},
+		&schema.DropColumn{C: schema.NewColumn("old_name")},
+	}
+	i, j := changes.IndexAddColumn("new_name"), changes.IndexDropColumn("old_name")
+	if i == -1 || j == -1 {
+		log.Fatalln("Unexpected change positions")
+	}
+	// Replace "add" and "drop" with "rename".
+	changes = append(changes, &schema.RenameColumn{From: changes[j].(*schema.DropColumn).C, To: changes[i].(*schema.AddColumn).C})
+	changes.RemoveIndex(i, j)
+	for _, c := range changes {
+		switch c := c.(type) {
+		case *schema.AddColumn:
+			fmt.Printf("%T(%s)\n", c, c.C.Name)
+		case *schema.DropColumn:
+			fmt.Printf("%T(%s)\n", c, c.C.Name)
+		case *schema.RenameColumn:
+			fmt.Printf("%T(%s -> %s)\n", c, c.From.Name, c.To.Name)
+		case *schema.AddIndex:
+			fmt.Printf("%T(%s)\n", c, c.I.Name)
+		}
+	}
+	// Output:
+	// *schema.AddIndex(id)
+	// *schema.AddColumn(id)
+	// *schema.AddColumn(created_at)
+	// *schema.RenameColumn(old_name -> new_name)
+}