Repository: ariga/atlas
Pull request #300 (2021-12-11 21:51:02)

sq/internal/sqlx: add support for realm-level diffing
Some parts I can export while I still work on migration. 


Patch:
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index f0d3e4f4..84b439b3 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -60,12 +60,46 @@ type (
 	}
 )
 
+// RealmDiff implements the schema.Differ for Realm objects and returns a list of changes
+// that need to be applied in order to move a database from the current state to the desired.
+func (d *Diff) RealmDiff(from, to *schema.Realm) ([]schema.Change, error) {
+	var changes []schema.Change
+	// Drop or modify schema.
+	for _, s1 := range from.Schemas {
+		s2, ok := to.Schema(s1.Name)
+		if !ok {
+			changes = append(changes, &schema.DropSchema{S: s1})
+			continue
+		}
+		change, err := d.SchemaDiff(s1, s2)
+		if err != nil {
+			return nil, err
+		}
+		changes = append(changes, change...)
+	}
+	// Add schemas.
+	for _, s1 := range to.Schemas {
+		if _, ok := from.Schema(s1.Name); ok {
+			continue
+		}
+		changes = append(changes, &schema.AddSchema{S: s1})
+		for _, t := range s1.Tables {
+			changes = append(changes, &schema.AddTable{T: t})
+		}
+	}
+	return changes, nil
+}
+
 // SchemaDiff implements the schema.Differ interface and returns a list of
 // changes that need to be applied in order to move from one state to the other.
 func (d *Diff) SchemaDiff(from, to *schema.Schema) ([]schema.Change, error) {
 	var changes []schema.Change
 	// Drop or modify attributes (collations, checks, etc).
-	changes = append(changes, d.SchemaAttrDiff(from, to)...)
+	if change := d.SchemaAttrDiff(from, to); len(change) > 0 {
+		changes = append(changes, &schema.ModifySchema{
+			Changes: change,
+		})
+	}
 
 	// Drop or modify tables.
 	for _, t1 := range from.Tables {
diff --git a/sql/schema/migrate.go b/sql/schema/migrate.go
index fb13c8a2..24a23f31 100644
--- a/sql/schema/migrate.go
+++ b/sql/schema/migrate.go
@@ -24,6 +24,26 @@ type (
 		change()
 	}
 
+	// AddSchema describes a schema (named database) creation change.
+	// Unlike table creation, schemas and their elements are described
+	// with separate changes. For example, "AddSchema" and "AddTable"
+	AddSchema struct {
+		S     *Schema
+		Extra []Expr // Extra clauses and options.
+	}
+
+	// DropSchema describes a schema (named database) removal change.
+	DropSchema struct {
+		S     *Schema
+		Extra []Expr // Extra clauses and options.
+	}
+
+	// ModifySchema describes a modification change for schema attributes.
+	ModifySchema struct {
+		S       *Schema
+		Changes []Change
+	}
+
 	// AddTable describes a table creation change.
 	AddTable struct {
 		T     *Table
@@ -168,6 +188,11 @@ type (
 	// Differ is the interface implemented by the different
 	// drivers for comparing and diffing schema top elements.
 	Differ interface {
+		// RealmDiff returns a diff report for migrating a realm
+		// (or a database) from state "from" to state "to". An error
+		// is returned if such step is not possible.
+		RealmDiff(from, to *Realm) ([]Change, error)
+
 		// SchemaDiff returns a diff report for migrating a schema
 		// from state "from" to state "to". An error is returned
 		// if such step is not possible.
@@ -193,6 +218,9 @@ type (
 func (*AddAttr) change()          {}
 func (*DropAttr) change()         {}
 func (*ModifyAttr) change()       {}
+func (*AddSchema) change()        {}
+func (*DropSchema) change()       {}
+func (*ModifySchema) change()     {}
 func (*AddTable) change()         {}
 func (*DropTable) change()        {}
 func (*ModifyTable) change()      {}
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index 2cee9289..414181f2 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -78,6 +78,16 @@ type (
 	}
 )
 
+// Schema returns the first schema that matched the given name.
+func (r *Realm) Schema(name string) (*Schema, bool) {
+	for _, t := range r.Schemas {
+		if t.Name == name {
+			return t, true
+		}
+	}
+	return nil, false
+}
+
 // Table returns the first table that matched the given name.
 func (s *Schema) Table(name string) (*Table, bool) {
 	for _, t := range s.Tables {


Test patch:
diff --git a/sql/mysql/diff_test.go b/sql/mysql/diff_test.go
index fb63d06b..47d3fef6 100644
--- a/sql/mysql/diff_test.go
+++ b/sql/mysql/diff_test.go
@@ -312,9 +312,74 @@ func TestDiff_SchemaDiff(t *testing.T) {
 	changes, err := d.SchemaDiff(from, to)
 	require.NoError(t, err)
 	require.EqualValues(t, []schema.Change{
-		&schema.ModifyAttr{From: from.Attrs[0], To: to.Attrs[0]},
+		&schema.ModifySchema{Changes: []schema.Change{&schema.ModifyAttr{From: from.Attrs[0], To: to.Attrs[0]}}},
 		&schema.ModifyTable{T: from.Tables[0], Changes: []schema.Change{&schema.AddColumn{C: to.Tables[0].Columns[0]}}},
 		&schema.DropTable{T: from.Tables[1]},
 		&schema.AddTable{T: to.Tables[1]},
 	}, changes)
 }
+
+func TestDiff_RealmDiff(t *testing.T) {
+	var (
+		d    = (&Driver{}).Diff()
+		from = &schema.Realm{
+			Schemas: []*schema.Schema{
+				{
+					Name: "public",
+					Tables: []*schema.Table{
+						{Name: "users"},
+						{Name: "pets"},
+					},
+					Attrs: []schema.Attr{
+						&schema.Collation{V: "latin1"},
+					},
+				},
+				{
+					Name: "internal",
+					Tables: []*schema.Table{
+						{Name: "pets"},
+					},
+				},
+			},
+		}
+		to = &schema.Realm{
+			Schemas: []*schema.Schema{
+				{
+					Name: "public",
+					Tables: []*schema.Table{
+						{
+							Name: "users",
+							Columns: []*schema.Column{
+								{Name: "t2_id", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}},
+							},
+						},
+						{Name: "pets"},
+					},
+					Attrs: []schema.Attr{
+						&schema.Collation{V: "utf8"},
+					},
+				},
+				{
+					Name: "test",
+					Tables: []*schema.Table{
+						{Name: "pets"},
+					},
+				},
+			},
+		}
+	)
+	from.Schemas[0].Realm = from
+	from.Schemas[0].Tables[0].Schema = from.Schemas[0]
+	from.Schemas[0].Tables[1].Schema = from.Schemas[0]
+	to.Schemas[0].Realm = to
+	to.Schemas[0].Tables[0].Schema = to.Schemas[0]
+	changes, err := d.RealmDiff(from, to)
+	require.NoError(t, err)
+	require.EqualValues(t, []schema.Change{
+		&schema.ModifySchema{Changes: []schema.Change{&schema.ModifyAttr{From: from.Schemas[0].Attrs[0], To: to.Schemas[0].Attrs[0]}}},
+		&schema.ModifyTable{T: from.Schemas[0].Tables[0], Changes: []schema.Change{&schema.AddColumn{C: to.Schemas[0].Tables[0].Columns[0]}}},
+		&schema.DropSchema{S: from.Schemas[1]},
+		&schema.AddSchema{S: to.Schemas[1]},
+		&schema.AddTable{T: to.Schemas[1].Tables[0]},
+	}, changes)
+}