Repository: ariga/atlas
Pull request #234 (2021-11-17 09:08:18)

sql/internal/sqlx: detach cycles in drop tables


Patch:
diff --git a/sql/internal/sqlx/plan.go b/sql/internal/sqlx/plan.go
index 450ab78a..46c384b3 100644
--- a/sql/internal/sqlx/plan.go
+++ b/sql/internal/sqlx/plan.go
@@ -10,9 +10,10 @@ import (
 	"ariga.io/atlas/sql/schema"
 )
 
-// DetachCycles takes a list of schema changes, and postpone
-// the foreign keys creation if there's at least one circular
-// reference in the changeset.
+// DetachCycles takes a list of schema changes, and detaches
+// references between changes if there is at least one circular
+// reference in the changeset. More explicitly, it postpones fks
+// creation, or deletes fks before deletes their tables.
 func DetachCycles(changes []schema.Change) []schema.Change {
 	sorted, hasCycle := sortMap(changes)
 	if hasCycle {
@@ -45,6 +46,21 @@ func detachReferences(changes []schema.Change) []schema.Change {
 				change = &schema.AddTable{T: &t, Extra: change.Extra}
 			}
 			planned = append(planned, change)
+		case *schema.DropTable:
+			var fks []schema.Change
+			for _, fk := range change.T.ForeignKeys {
+				if fk.RefTable != change.T {
+					fks = append(fks, &schema.DropForeignKey{F: fk})
+				}
+			}
+			if len(fks) > 0 {
+				planned = append(planned, &schema.ModifyTable{T: change.T, Changes: fks})
+				t := *change.T
+				t.ForeignKeys = nil
+				deferred = append(deferred, &schema.DropTable{T: &t, Extra: change.Extra})
+			} else {
+				planned = append(planned, change)
+			}
 		case *schema.ModifyTable:
 			var fks, rest []schema.Change
 			for _, c := range change.Changes {
@@ -71,31 +87,31 @@ func detachReferences(changes []schema.Change) []schema.Change {
 // sortMap returns an index-map indicates the position of table in a topological
 // sort in reversed order based on its references, and a boolean indicate if there
 // is a non-self loop.
-func sortMap(changes []schema.Change) (map[*schema.Table]int, bool) {
+func sortMap(changes []schema.Change) (map[string]int, bool) {
 	var (
-		visit      func(*schema.Table) bool
-		references = references(changes)
-		sorted     = make(map[*schema.Table]int)
-		progress   = make(map[*schema.Table]bool)
+		visit    func(string) bool
+		deps     = dependencies(changes)
+		sorted   = make(map[string]int)
+		progress = make(map[string]bool)
 	)
-	visit = func(node *schema.Table) bool {
-		if _, done := sorted[node]; done {
+	visit = func(name string) bool {
+		if _, done := sorted[name]; done {
 			return false
 		}
-		if progress[node] {
+		if progress[name] {
 			return true
 		}
-		progress[node] = true
-		for _, ref := range references[node] {
-			if visit(ref) {
+		progress[name] = true
+		for _, ref := range deps[name] {
+			if visit(ref.Name) {
 				return true
 			}
 		}
-		delete(progress, node)
-		sorted[node] = len(sorted)
+		delete(progress, name)
+		sorted[name] = len(sorted)
 		return false
 	}
-	for node := range references {
+	for node := range deps {
 		if visit(node) {
 			return nil, true
 		}
@@ -103,15 +119,21 @@ func sortMap(changes []schema.Change) (map[*schema.Table]int, bool) {
 	return sorted, false
 }
 
-// references returns an adjacency list of all child tables and their references in parent tables.
-func references(changes []schema.Change) map[*schema.Table][]*schema.Table {
-	refs := make(map[*schema.Table][]*schema.Table)
+// dependencies returned an adjacency list of all tables and the table they depend on
+func dependencies(changes []schema.Change) map[string][]*schema.Table {
+	deps := make(map[string][]*schema.Table)
 	for _, change := range changes {
 		switch change := change.(type) {
 		case *schema.AddTable:
 			for _, fk := range change.T.ForeignKeys {
 				if fk.RefTable != change.T {
-					refs[change.T] = append(refs[change.T], fk.RefTable)
+					deps[change.T.Name] = append(deps[change.T.Name], fk.RefTable)
+				}
+			}
+		case *schema.DropTable:
+			for _, fk := range change.T.ForeignKeys {
+				if isDropped(changes, fk.RefTable) {
+					deps[fk.RefTable.Name] = append(deps[fk.RefTable.Name], fk.Table)
 				}
 			}
 		case *schema.ModifyTable:
@@ -119,26 +141,38 @@ func references(changes []schema.Change) map[*schema.Table][]*schema.Table {
 				switch c := c.(type) {
 				case *schema.AddForeignKey:
 					if c.F.RefTable != change.T {
-						refs[change.T] = append(refs[change.T], c.F.RefTable)
+						deps[change.T.Name] = append(deps[change.T.Name], c.F.RefTable)
 					}
 				case *schema.ModifyForeignKey:
 					if c.To.RefTable != change.T {
-						refs[change.T] = append(refs[change.T], c.To.RefTable)
+						deps[change.T.Name] = append(deps[change.T.Name], c.To.RefTable)
 					}
 				}
 			}
 		}
 	}
-	return refs
+	return deps
 }
 
 // table extracts a table from the given change.
-func table(change schema.Change) (t *schema.Table) {
+func table(change schema.Change) (t string) {
 	switch change := change.(type) {
 	case *schema.AddTable:
-		t = change.T
+		t = change.T.Name
+	case *schema.DropTable:
+		t = change.T.Name
 	case *schema.ModifyTable:
-		t = change.T
+		t = change.T.Name
 	}
 	return
 }
+
+// isDropped checks if the given table is marked as a deleted in the changeset.
+func isDropped(changes []schema.Change, t *schema.Table) bool {
+	for _, c := range changes {
+		if c, ok := c.(*schema.DropTable); ok && c.T.Name == t.Name {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
new file mode 100644
index 00000000..be254fdc
--- /dev/null
+++ b/internal/integration/integration_test.go
@@ -0,0 +1,66 @@
+package integration
+
+import (
+	"testing"
+
+	"ariga.io/atlas/sql/schema"
+	"github.com/stretchr/testify/require"
+)
+
+// T holds the elements common between dialect tests.
+type T interface {
+	testing.TB
+	realm() *schema.Realm
+	loadRealm() *schema.Realm
+	users() *schema.Table
+	loadUsers() *schema.Table
+	posts() *schema.Table
+	loadPosts() *schema.Table
+	dropTables(...string)
+	migrate(...schema.Change)
+	diff(*schema.Table, *schema.Table) []schema.Change
+}
+
+func testAddDrop(t T) {
+	usersT := t.users()
+	postsT := t.posts()
+	petsT := &schema.Table{
+		Name:   "pets",
+		Schema: usersT.Schema,
+		Columns: []*schema.Column{
+			{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+			{Name: "owner_id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}, Null: true}},
+		},
+	}
+	petsT.PrimaryKey = &schema.Index{Parts: []*schema.IndexPart{{C: postsT.Columns[0]}}}
+	petsT.ForeignKeys = []*schema.ForeignKey{
+		{Symbol: "owner_id", Table: petsT, Columns: petsT.Columns[1:], RefTable: usersT, RefColumns: usersT.Columns[:1]},
+	}
+	t.dropTables(postsT.Name, usersT.Name, petsT.Name)
+	t.migrate(&schema.AddTable{T: petsT}, &schema.AddTable{T: usersT}, &schema.AddTable{T: postsT})
+	ensureNoChange(t, usersT, petsT, postsT)
+	t.migrate(&schema.DropTable{T: usersT}, &schema.DropTable{T: postsT}, &schema.DropTable{T: petsT})
+	// Ensure the realm is empty.
+	require.EqualValues(t, t.realm(), t.loadRealm())
+}
+
+func testRelation(t T) {
+	usersT, postsT := t.users(), t.posts()
+	t.dropTables(postsT.Name, usersT.Name)
+	t.migrate(
+		&schema.AddTable{T: usersT},
+		&schema.AddTable{T: postsT},
+	)
+	ensureNoChange(t, postsT, usersT)
+}
+
+func ensureNoChange(t T, tables ...*schema.Table) {
+	realm := t.loadRealm()
+	require.Equal(t, len(realm.Schemas[0].Tables), len(tables))
+	for i := range tables {
+		tt, ok := realm.Schemas[0].Table(tables[i].Name)
+		require.True(t, ok)
+		changes := t.diff(tt, tables[i])
+		require.Empty(t, changes)
+	}
+}
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index a37b70ee..c0b5ae1d 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -53,49 +53,23 @@ func myRun(t *testing.T, fn func(*myTest)) {
 
 func TestMySQL_AddDropTable(t *testing.T) {
 	myRun(t, func(t *myTest) {
-		ctx := context.Background()
-		usersT := t.users()
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.AddTable{T: usersT},
-		})
-		require.NoError(t, err)
-		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
-		require.Empty(t, changes)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.DropTable{T: usersT},
-		})
-		require.NoError(t, err)
-		// Ensure the realm is empty.
-		require.EqualValues(t, t.realm(), t.loadRealm())
+		testAddDrop(t)
 	})
 }
 
 func TestMySQL_Relation(t *testing.T) {
 	myRun(t, func(t *myTest) {
-		ctx := context.Background()
-		usersT, postsT := t.users(), t.posts()
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.AddTable{T: usersT},
-			&schema.AddTable{T: postsT},
-		})
-		require.NoError(t, err)
-		t.dropTables("posts", "users")
-		t.ensureNoChange(postsT, usersT)
+		testRelation(t)
 	})
 }
 
 func TestMySQL_AddIndexedColumns(t *testing.T) {
 	myRun(t, func(t *myTest) {
-		ctx := context.Background()
 		usersT := &schema.Table{
 			Name:    "users",
 			Columns: []*schema.Column{{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}}},
 		}
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.AddTable{T: usersT},
-		})
-		require.NoError(t, err)
+		t.migrate(&schema.AddTable{T: usersT})
 		t.dropTables(usersT.Name)
 		usersT.Columns = append(usersT.Columns, &schema.Column{
 			Name:    "a",
@@ -116,21 +90,17 @@ func TestMySQL_AddIndexedColumns(t *testing.T) {
 			Name:   "a_b_c_unique",
 			Parts:  []*schema.IndexPart{{C: parts[0]}, {C: parts[1]}, {C: parts[2]}},
 		})
-		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes := t.diff(t.loadUsers(), usersT)
 		require.NotEmpty(t, changes, "usersT contains 2 new columns and 1 new index")
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
 
 		// Dropping a column should remove
 		// it from the key.
 		usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
-		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(t.loadUsers())
+		changes = t.diff(t.loadUsers(), usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, t.loadUsers())
 
 		// Dropping a column from both table and index.
 		usersT = t.loadUsers()
@@ -139,12 +109,10 @@ func TestMySQL_AddIndexedColumns(t *testing.T) {
 		require.Len(t, idx.Parts, 2)
 		usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
 		idx.Parts = idx.Parts[:len(idx.Parts)-1]
-		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes = t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 2)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(t.loadUsers())
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, t.loadUsers())
 
 		// Dropping a column should remove
 		// single-column keys as well.
@@ -153,12 +121,10 @@ func TestMySQL_AddIndexedColumns(t *testing.T) {
 		require.True(t, ok)
 		require.Len(t, idx.Parts, 1)
 		usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
-		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes = t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(t.loadUsers())
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, t.loadUsers())
 		idx, ok = t.loadUsers().Index("a_b_c_unique")
 		require.False(t, ok)
 	})
@@ -166,10 +132,8 @@ func TestMySQL_AddIndexedColumns(t *testing.T) {
 
 func TestMySQL_AddColumns(t *testing.T) {
 	myRun(t, func(t *myTest) {
-		ctx := context.Background()
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: t.users()}})
-		require.NoError(t, err)
 		usersT := t.users()
+		t.migrate(&schema.AddTable{T: usersT})
 		t.dropTables(usersT.Name)
 		usersT.Columns = append(
 			usersT.Columns,
@@ -199,34 +163,28 @@ func TestMySQL_AddColumns(t *testing.T) {
 			&schema.Column{Name: "y", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "point"}}},
 			&schema.Column{Name: "z", Type: &schema.ColumnType{Type: &schema.TimeType{T: "timestamp"}}, Default: &schema.RawExpr{X: "CURRENT_TIMESTAMP"}},
 		)
-		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 24)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
 	})
 }
 
 func TestMySQL_ColumnInt(t *testing.T) {
-	ctx := context.Background()
 	t.Run("ChangeType", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			usersT := &schema.Table{
 				Name:    "users",
 				Columns: []*schema.Column{{Name: "a", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}}},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
 			for _, typ := range []string{"tinyint", "smallint", "mediumint", "bigint"} {
 				usersT.Columns[0].Type.Type = &schema.IntegerType{T: typ}
-				changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-				require.NoError(t, err)
+				changes := t.diff(t.loadUsers(), usersT)
 				require.Len(t, changes, 1)
-				err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-				require.NoError(t, err)
-				t.ensureNoChange(usersT)
+				t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+				ensureNoChange(t, usersT)
 			}
 		})
 	})
@@ -237,41 +195,35 @@ func TestMySQL_ColumnInt(t *testing.T) {
 				Name:    "users",
 				Columns: []*schema.Column{{Name: "a", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}, Default: &schema.RawExpr{X: "1"}}},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
-			t.ensureNoChange(usersT)
+			ensureNoChange(t, usersT)
 			for _, x := range []string{"2", "'3'", "10.1"} {
 				usersT.Columns[0].Default.(*schema.RawExpr).X = x
-				changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-				require.NoError(t, err)
+				changes := t.diff(t.loadUsers(), usersT)
 				require.Len(t, changes, 1)
-				err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-				t.ensureNoChange(usersT)
+				t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+				ensureNoChange(t, usersT)
 			}
 		})
 	})
 }
 
 func TestMySQL_ColumnString(t *testing.T) {
-	ctx := context.Background()
 	t.Run("ChangeType", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			usersT := &schema.Table{
 				Name:    "users",
 				Columns: []*schema.Column{{Name: "a", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(20)"}}}},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
 			for _, typ := range []string{"varchar(255)", "char(120)", "tinytext", "mediumtext", "longtext"} {
 				usersT.Columns[0].Type.Type = &schema.StringType{T: typ}
-				changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-				require.NoError(t, err)
+				changes := t.diff(t.loadUsers(), usersT)
 				require.Len(t, changes, 1)
-				err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-				require.NoError(t, err)
-				t.ensureNoChange(usersT)
+				t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+				ensureNoChange(t, usersT)
 			}
 		})
 	})
@@ -285,10 +237,9 @@ func TestMySQL_ColumnString(t *testing.T) {
 					{Name: "b", Type: &schema.ColumnType{Type: &schema.StringType{T: "char(255)"}}, Default: &schema.RawExpr{X: "'world'"}},
 				},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
-			t.ensureNoChange(usersT)
+			ensureNoChange(t, usersT)
 		})
 	})
 
@@ -298,24 +249,21 @@ func TestMySQL_ColumnString(t *testing.T) {
 				Name:    "users",
 				Columns: []*schema.Column{{Name: "a", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}, Default: &schema.RawExpr{X: "hello"}}},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
-			t.ensureNoChange(usersT)
+			ensureNoChange(t, usersT)
 			for _, x := range []string{"2", "'3'", "'world'"} {
 				usersT.Columns[0].Default.(*schema.RawExpr).X = x
-				changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-				require.NoError(t, err)
+				changes := t.diff(t.loadUsers(), usersT)
 				require.Len(t, changes, 1)
-				err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-				t.ensureNoChange(usersT)
+				t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+				ensureNoChange(t, usersT)
 			}
 		})
 	})
 }
 
 func TestMySQL_ColumnBool(t *testing.T) {
-	ctx := context.Background()
 	t.Run("Add", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			usersT := &schema.Table{
@@ -327,10 +275,9 @@ func TestMySQL_ColumnBool(t *testing.T) {
 					{Name: "d", Type: &schema.ColumnType{Type: &schema.BoolType{T: "tinyint(1)"}}},
 				},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
-			t.ensureNoChange(usersT)
+			ensureNoChange(t, usersT)
 		})
 	})
 
@@ -349,10 +296,9 @@ func TestMySQL_ColumnBool(t *testing.T) {
 					{Name: "h", Type: &schema.ColumnType{Type: &schema.BoolType{T: "bool"}}, Default: &schema.RawExpr{X: "FALSE"}},
 				},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
-			t.ensureNoChange(usersT)
+			ensureNoChange(t, usersT)
 		})
 	})
 
@@ -364,18 +310,16 @@ func TestMySQL_ColumnBool(t *testing.T) {
 					{Name: "a", Type: &schema.ColumnType{Type: &schema.BoolType{T: "bool"}}, Default: &schema.RawExpr{X: "1"}},
 				},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
-			t.ensureNoChange(usersT)
+			ensureNoChange(t, usersT)
 			// Change default from "true" to "false" to "true".
 			for _, x := range []string{"false", "true"} {
 				usersT.Columns[0].Default.(*schema.RawExpr).X = x
-				changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-				require.NoError(t, err)
+				changes := t.diff(t.loadUsers(), usersT)
 				require.Len(t, changes, 1)
-				err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-				t.ensureNoChange(usersT)
+				t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+				ensureNoChange(t, usersT)
 			}
 		})
 	})
@@ -388,48 +332,39 @@ func TestMySQL_ColumnBool(t *testing.T) {
 					{Name: "a", Type: &schema.ColumnType{Type: &schema.BoolType{T: "bool"}, Null: true}},
 				},
 			}
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-			require.NoError(t, err)
+			t.migrate(&schema.AddTable{T: usersT})
 			t.dropTables(usersT.Name)
-			t.ensureNoChange(usersT)
+			ensureNoChange(t, usersT)
 			usersT.Columns[0].Type.Null = false
-			changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadUsers(), usersT)
 			require.Len(t, changes, 1)
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+			ensureNoChange(t, usersT)
 		})
 	})
 }
 
 func TestMySQL_ForeignKey(t *testing.T) {
-	ctx := context.Background()
 	t.Run("ChangeAction", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			usersT, postsT := t.users(), t.posts()
 			t.dropTables(postsT.Name, usersT.Name)
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{
-				&schema.AddTable{T: usersT},
-				&schema.AddTable{T: postsT},
-			})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.AddTable{T: usersT}, &schema.AddTable{T: postsT})
+			ensureNoChange(t, postsT, usersT)
 
 			postsT = t.loadPosts()
 			fk, ok := postsT.ForeignKey("author_id")
 			require.True(t, ok)
 			fk.OnUpdate = schema.SetNull
 			fk.OnDelete = schema.Cascade
-			changes, err := t.drv.Diff().TableDiff(t.loadPosts(), postsT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadPosts(), postsT)
 			require.Len(t, changes, 1)
 			modifyF, ok := changes[0].(*schema.ModifyForeignKey)
 			require.True(t, ok)
 			require.True(t, modifyF.Change == schema.ChangeUpdateAction|schema.ChangeDeleteAction)
 
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: postsT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.ModifyTable{T: postsT, Changes: changes})
+			ensureNoChange(t, postsT, usersT)
 		})
 	})
 
@@ -441,12 +376,8 @@ func TestMySQL_ForeignKey(t *testing.T) {
 			require.True(t, ok)
 			fk.OnDelete = schema.SetNull
 			fk.OnUpdate = schema.SetNull
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{
-				&schema.AddTable{T: usersT},
-				&schema.AddTable{T: postsT},
-			})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.AddTable{T: usersT}, &schema.AddTable{T: postsT})
+			ensureNoChange(t, postsT, usersT)
 
 			postsT = t.loadPosts()
 			c, ok := postsT.Column("author_id")
@@ -456,8 +387,7 @@ func TestMySQL_ForeignKey(t *testing.T) {
 			require.True(t, ok)
 			fk.OnUpdate = schema.NoAction
 			fk.OnDelete = schema.NoAction
-			changes, err := t.drv.Diff().TableDiff(t.loadPosts(), postsT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadPosts(), postsT)
 			require.Len(t, changes, 2)
 			modifyC, ok := changes[0].(*schema.ModifyColumn)
 			require.True(t, ok)
@@ -466,9 +396,8 @@ func TestMySQL_ForeignKey(t *testing.T) {
 			require.True(t, ok)
 			require.True(t, modifyF.Change == schema.ChangeUpdateAction|schema.ChangeDeleteAction)
 
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: postsT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.ModifyTable{T: postsT, Changes: changes})
+			ensureNoChange(t, postsT, usersT)
 		})
 	})
 
@@ -476,11 +405,8 @@ func TestMySQL_ForeignKey(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			usersT := t.users()
 			t.dropTables(usersT.Name)
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{
-				&schema.AddTable{T: usersT},
-			})
-			require.NoError(t, err)
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.AddTable{T: usersT})
+			ensureNoChange(t, usersT)
 
 			// Add foreign key.
 			usersT.Columns = append(usersT.Columns, &schema.Column{
@@ -496,8 +422,7 @@ func TestMySQL_ForeignKey(t *testing.T) {
 				OnDelete:   schema.NoAction,
 			})
 
-			changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadUsers(), usersT)
 			require.Len(t, changes, 2)
 			addC, ok := changes[0].(*schema.AddColumn)
 			require.True(t, ok)
@@ -505,19 +430,16 @@ func TestMySQL_ForeignKey(t *testing.T) {
 			addF, ok := changes[1].(*schema.AddForeignKey)
 			require.True(t, ok)
 			require.Equal(t, "spouse_id", addF.F.Symbol)
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+			ensureNoChange(t, usersT)
 
 			// Drop foreign keys.
 			usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
 			usersT.ForeignKeys = usersT.ForeignKeys[:len(usersT.ForeignKeys)-1]
-			changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-			require.NoError(t, err)
+			changes = t.diff(t.loadUsers(), usersT)
 			require.Len(t, changes, 2)
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+			ensureNoChange(t, usersT)
 		})
 	})
 }
@@ -561,14 +483,15 @@ func (t *myTest) applyHcl(spec string) {
 	require.NoError(t, err)
 }
 
-func (t *myTest) ensureNoChange(tables ...*schema.Table) {
-	realm := t.loadRealm()
-	require.Equal(t, len(realm.Schemas[0].Tables), len(tables))
-	for i := range tables {
-		changes, err := t.drv.Diff().TableDiff(realm.Schemas[0].Tables[i], tables[i])
-		require.NoError(t, err)
-		require.Empty(t, changes)
-	}
+func (t *myTest) diff(t1, t2 *schema.Table) []schema.Change {
+	changes, err := t.drv.Diff().TableDiff(t1, t2)
+	require.NoError(t, err)
+	return changes
+}
+
+func (t *myTest) migrate(changes ...schema.Change) {
+	err := t.drv.Migrate().Exec(context.Background(), changes)
+	require.NoError(t, err)
 }
 
 func (t *myTest) dropTables(names ...string) {
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index d035f49c..99b4e257 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -52,50 +52,23 @@ func pgRun(t *testing.T, fn func(*pgTest)) {
 
 func TestPostgres_AddDropTable(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
-		ctx := context.Background()
-		usersT := t.users()
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.AddTable{T: usersT},
-		})
-		require.NoError(t, err)
-		realm := t.loadRealm()
-		changes, err := t.drv.Diff().TableDiff(realm.Schemas[0].Tables[0], usersT)
-		require.NoError(t, err)
-		require.Empty(t, changes)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.DropTable{T: usersT},
-		})
-		require.NoError(t, err)
-		// Ensure the realm is empty.
-		require.EqualValues(t, t.realm(), t.loadRealm())
+		testAddDrop(t)
 	})
 }
 
 func TestPostgres_Relation(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
-		ctx := context.Background()
-		usersT, postsT := t.users(), t.posts()
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.AddTable{T: usersT},
-			&schema.AddTable{T: postsT},
-		})
-		require.NoError(t, err)
-		t.dropTables(postsT.Name, usersT.Name)
-		t.ensureNoChange(postsT, usersT)
+		testRelation(t)
 	})
 }
 
 func TestPostgres_AddIndexedColumns(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
-		ctx := context.Background()
 		usersT := &schema.Table{
 			Name:    "users",
 			Columns: []*schema.Column{{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}}},
 		}
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{
-			&schema.AddTable{T: usersT},
-		})
-		require.NoError(t, err)
+		t.migrate(&schema.AddTable{T: usersT})
 		t.dropTables(usersT.Name)
 		usersT.Columns = append(usersT.Columns, &schema.Column{
 			Name:    "a",
@@ -116,21 +89,17 @@ func TestPostgres_AddIndexedColumns(t *testing.T) {
 			Name:   "a_b_c_unique",
 			Parts:  []*schema.IndexPart{{C: parts[0]}, {C: parts[1]}, {C: parts[2]}},
 		})
-		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes := t.diff(t.loadUsers(), usersT)
 		require.NotEmpty(t, changes, "usersT contains 3 new columns and 1 new index")
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
 
 		// Dropping a column involves in a multi-column
 		// index causes the index to be dropped as well.
 		usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
-		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(t.loadUsers())
+		changes = t.diff(t.loadUsers(), usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, t.loadUsers())
 		usersT = t.loadUsers()
 		_, ok := usersT.Index("a_b_c_unique")
 		require.False(t, ok)
@@ -139,11 +108,9 @@ func TestPostgres_AddIndexedColumns(t *testing.T) {
 
 func TestPostgres_AddColumns(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
-		ctx := context.Background()
 		usersT := t.users()
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-		require.NoError(t, err)
 		t.dropTables(usersT.Name)
+		t.migrate(&schema.AddTable{T: usersT})
 		usersT.Columns = append(
 			usersT.Columns,
 			&schema.Column{Name: "a", Type: &schema.ColumnType{Type: &schema.BinaryType{T: "bytea"}}},
@@ -162,12 +129,10 @@ func TestPostgres_AddColumns(t *testing.T) {
 			&schema.Column{Name: "n", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "point"}, Null: true}, Default: &schema.RawExpr{X: "'(1,2)'"}},
 			&schema.Column{Name: "o", Type: &schema.ColumnType{Type: &schema.SpatialType{T: "line"}, Null: true}, Default: &schema.RawExpr{X: "'{1,2,3}'"}},
 		)
-		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 15)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
 	})
 }
 
@@ -183,12 +148,10 @@ func TestPostgres_ColumnInt(t *testing.T) {
 			require.NoError(t, err)
 			t.dropTables(usersT.Name)
 			change(usersT.Columns[0])
-			changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadUsers(), usersT)
 			require.Len(t, changes, 1)
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+			ensureNoChange(t, usersT)
 		})
 	}
 
@@ -213,36 +176,30 @@ func TestPostgres_ColumnInt(t *testing.T) {
 
 func TestPostgres_ColumnArray(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
-		ctx := context.Background()
 		usersT := t.users()
-		err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
-		require.NoError(t, err)
 		t.dropTables(usersT.Name)
+		t.migrate(&schema.AddTable{T: usersT})
 
 		// Add column.
 		usersT.Columns = append(
 			usersT.Columns,
 			&schema.Column{Name: "a", Type: &schema.ColumnType{Raw: "int[]", Type: &postgres.ArrayType{T: "int[]"}}, Default: &schema.RawExpr{X: "'{1}'"}},
 		)
-		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
 
 		// Check default.
 		usersT.Columns[2].Default = &schema.RawExpr{X: "ARRAY[1]"}
-		t.ensureNoChange(usersT)
+		ensureNoChange(t, usersT)
 
 		// Change default.
 		usersT.Columns[2].Default = &schema.RawExpr{X: "ARRAY[1,2]"}
-		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes = t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
-		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err)
-		t.ensureNoChange(usersT)
+		t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+		ensureNoChange(t, usersT)
 	})
 }
 
@@ -265,70 +222,60 @@ func TestPostgres_Enums(t *testing.T) {
 		err := t.drv.Migrate().Exec(ctx, []schema.Change{&schema.AddTable{T: usersT}})
 		require.NoError(t, err, "create a new table with an enum column")
 		t.dropTables(usersT.Name)
-		t.ensureNoChange(usersT)
+		ensureNoChange(t, usersT)
 
 		// Add another enum column.
 		usersT.Columns = append(
 			usersT.Columns,
 			&schema.Column{Name: "day", Type: &schema.ColumnType{Type: &schema.EnumType{T: "day", Values: []string{"sunday", "monday"}}}},
 		)
-		changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
 		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
 		require.NoError(t, err, "add a new enum column to existing table")
-		t.ensureNoChange(usersT)
+		ensureNoChange(t, usersT)
 
 		// Add a new value to an existing enum.
 		e := usersT.Columns[1].Type.Type.(*schema.EnumType)
 		e.Values = append(e.Values, "tuesday")
-		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes = t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
 		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
 		require.NoError(t, err, "append a value to existing enum")
-		t.ensureNoChange(usersT)
+		ensureNoChange(t, usersT)
 
 		// Add multiple new values to an existing enum.
 		e = usersT.Columns[1].Type.Type.(*schema.EnumType)
 		e.Values = append(e.Values, "wednesday", "thursday", "friday", "saturday")
-		changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-		require.NoError(t, err)
+		changes = t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
 		err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
 		require.NoError(t, err, "append multiple values to existing enum")
-		t.ensureNoChange(usersT)
+		ensureNoChange(t, usersT)
 	})
 }
 
 func TestPostgres_ForeignKey(t *testing.T) {
-	ctx := context.Background()
 	t.Run("ChangeAction", func(t *testing.T) {
 		pgRun(t, func(t *pgTest) {
 			usersT, postsT := t.users(), t.posts()
 			t.dropTables(postsT.Name, usersT.Name)
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{
-				&schema.AddTable{T: usersT},
-				&schema.AddTable{T: postsT},
-			})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.AddTable{T: usersT}, &schema.AddTable{T: postsT})
+			ensureNoChange(t, postsT, usersT)
 
 			postsT = t.loadPosts()
 			fk, ok := postsT.ForeignKey("author_id")
 			require.True(t, ok)
 			fk.OnUpdate = schema.SetNull
 			fk.OnDelete = schema.Cascade
-			changes, err := t.drv.Diff().TableDiff(t.loadPosts(), postsT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadPosts(), postsT)
 			require.Len(t, changes, 1)
 			modifyF, ok := changes[0].(*schema.ModifyForeignKey)
 			require.True(t, ok)
 			require.True(t, modifyF.Change == schema.ChangeUpdateAction|schema.ChangeDeleteAction)
 
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: postsT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.ModifyTable{T: postsT, Changes: changes})
+			ensureNoChange(t, postsT, usersT)
 		})
 	})
 
@@ -340,12 +287,8 @@ func TestPostgres_ForeignKey(t *testing.T) {
 			require.True(t, ok)
 			fk.OnDelete = schema.SetNull
 			fk.OnUpdate = schema.SetNull
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{
-				&schema.AddTable{T: usersT},
-				&schema.AddTable{T: postsT},
-			})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.AddTable{T: usersT}, &schema.AddTable{T: postsT})
+			ensureNoChange(t, postsT, usersT)
 
 			postsT = t.loadPosts()
 			c, ok := postsT.Column("author_id")
@@ -355,8 +298,7 @@ func TestPostgres_ForeignKey(t *testing.T) {
 			require.True(t, ok)
 			fk.OnUpdate = schema.NoAction
 			fk.OnDelete = schema.NoAction
-			changes, err := t.drv.Diff().TableDiff(t.loadPosts(), postsT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadPosts(), postsT)
 			require.Len(t, changes, 2)
 			modifyC, ok := changes[0].(*schema.ModifyColumn)
 			require.True(t, ok)
@@ -365,9 +307,8 @@ func TestPostgres_ForeignKey(t *testing.T) {
 			require.True(t, ok)
 			require.True(t, modifyF.Change == schema.ChangeUpdateAction|schema.ChangeDeleteAction)
 
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: postsT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(postsT, usersT)
+			t.migrate(&schema.ModifyTable{T: postsT, Changes: changes})
+			ensureNoChange(t, postsT, usersT)
 		})
 	})
 
@@ -375,11 +316,8 @@ func TestPostgres_ForeignKey(t *testing.T) {
 		pgRun(t, func(t *pgTest) {
 			usersT := t.users()
 			t.dropTables(usersT.Name)
-			err := t.drv.Migrate().Exec(ctx, []schema.Change{
-				&schema.AddTable{T: usersT},
-			})
-			require.NoError(t, err)
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.AddTable{T: usersT})
+			ensureNoChange(t, usersT)
 
 			// Add foreign key.
 			usersT.Columns = append(usersT.Columns, &schema.Column{
@@ -395,8 +333,7 @@ func TestPostgres_ForeignKey(t *testing.T) {
 				OnDelete:   schema.NoAction,
 			})
 
-			changes, err := t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-			require.NoError(t, err)
+			changes := t.diff(t.loadUsers(), usersT)
 			require.Len(t, changes, 2)
 			addC, ok := changes[0].(*schema.AddColumn)
 			require.True(t, ok)
@@ -404,19 +341,16 @@ func TestPostgres_ForeignKey(t *testing.T) {
 			addF, ok := changes[1].(*schema.AddForeignKey)
 			require.True(t, ok)
 			require.Equal(t, "spouse_id", addF.F.Symbol)
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+			ensureNoChange(t, usersT)
 
 			// Drop foreign keys.
 			usersT.Columns = usersT.Columns[:len(usersT.Columns)-1]
 			usersT.ForeignKeys = usersT.ForeignKeys[:len(usersT.ForeignKeys)-1]
-			changes, err = t.drv.Diff().TableDiff(t.loadUsers(), usersT)
-			require.NoError(t, err)
+			changes = t.diff(t.loadUsers(), usersT)
 			require.Len(t, changes, 2)
-			err = t.drv.Migrate().Exec(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-			require.NoError(t, err)
-			t.ensureNoChange(usersT)
+			t.migrate(&schema.ModifyTable{T: usersT, Changes: changes})
+			ensureNoChange(t, usersT)
 		})
 	})
 }
@@ -520,14 +454,15 @@ func (t *pgTest) realm() *schema.Realm {
 	return r
 }
 
-func (t *pgTest) ensureNoChange(tables ...*schema.Table) {
-	realm := t.loadRealm()
-	require.Equal(t, len(realm.Schemas[0].Tables), len(tables))
-	for i := range tables {
-		changes, err := t.drv.Diff().TableDiff(realm.Schemas[0].Tables[i], tables[i])
-		require.NoError(t, err)
-		require.Empty(t, changes)
-	}
+func (t *pgTest) diff(t1, t2 *schema.Table) []schema.Change {
+	changes, err := t.drv.Diff().TableDiff(t1, t2)
+	require.NoError(t, err)
+	return changes
+}
+
+func (t *pgTest) migrate(changes ...schema.Change) {
+	err := t.drv.Migrate().Exec(context.Background(), changes)
+	require.NoError(t, err)
 }
 
 func (t *pgTest) dropTables(names ...string) {
diff --git a/sql/internal/sqlx/plan_test.go b/sql/internal/sqlx/plan_test.go
index 8869f0fb..e76b8858 100644
--- a/sql/internal/sqlx/plan_test.go
+++ b/sql/internal/sqlx/plan_test.go
@@ -34,6 +34,10 @@ func TestDetachCycles(t *testing.T) {
 	planned := DetachCycles(changes)
 	require.Equal(t, changes, planned)
 
+	deletion := []schema.Change{&schema.DropTable{T: users}, &schema.DropTable{T: workplaces}}
+	planned = DetachCycles(deletion)
+	require.Equal(t, deletion, planned)
+
 	// Create a circular reference.
 	workplaces.ForeignKeys = []*schema.ForeignKey{
 		{Symbol: "owner", Table: workplaces, Columns: workplaces.Columns[1:], RefTable: users, RefColumns: users.Columns[:1]},
@@ -58,4 +62,25 @@ func TestDetachCycles(t *testing.T) {
 			},
 		},
 	}, planned[3])
+
+	planned = DetachCycles(deletion)
+	require.Equal(t, &schema.ModifyTable{
+		T: users,
+		Changes: []schema.Change{
+			&schema.DropForeignKey{
+				F: &schema.ForeignKey{Symbol: "workplace", Table: users, Columns: users.Columns[1:], RefTable: workplaces, RefColumns: workplaces.Columns[:1]},
+			},
+		},
+	}, planned[0])
+	require.Equal(t, &schema.ModifyTable{
+		T: workplaces,
+		Changes: []schema.Change{
+			&schema.DropForeignKey{
+				F: &schema.ForeignKey{Symbol: "owner", Table: workplaces, Columns: workplaces.Columns[1:], RefTable: users, RefColumns: users.Columns[:1]},
+			},
+		},
+	}, planned[1])
+	users.ForeignKeys = nil
+	workplaces.ForeignKeys = nil
+	require.Equal(t, deletion, planned[2:])
 }