Repository: ariga/atlas
Pull request #1346 (2022-12-12 10:13:10)

sql: support multi-statement reverse commands


Patch:
diff --git a/sql/internal/sqlx/plan.go b/sql/internal/sqlx/plan.go
index 5373643d..14983cba 100644
--- a/sql/internal/sqlx/plan.go
+++ b/sql/internal/sqlx/plan.go
@@ -56,6 +56,23 @@ func ApplyChanges(ctx context.Context, changes []schema.Change, p execPlanner, o
 	return nil
 }
 
+// SetReversible sets the Reversible field to
+// true if all planned changes are reversible.
+func SetReversible(p *migrate.Plan) error {
+	reversible := true
+	for _, c := range p.Changes {
+		stmts, err := c.ReverseStmts()
+		if err != nil {
+			return err
+		}
+		if len(stmts) == 0 {
+			reversible = false
+		}
+	}
+	p.Reversible = reversible
+	return nil
+}
+
 // 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
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 3d3dd601..8f4d2b10 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -46,15 +46,29 @@ type (
 		// A Comment describes the change.
 		Comment string
 
-		// Reverse contains the "reversed statement" if
-		// command is reversible.
-		Reverse string
+		// Reverse contains the "reversed" statement(s) if
+		// the command is reversible.
+		Reverse any // string | []string
 
 		// The Source that caused this change, or nil.
 		Source schema.Change
 	}
 )
 
+// ReverseStmts returns the reverse statements of a Change, if any.
+func (c *Change) ReverseStmts() (cmd []string, err error) {
+	switch r := c.Reverse.(type) {
+	case nil:
+	case string:
+		cmd = []string{r}
+	case []string:
+		cmd = r
+	default:
+		err = fmt.Errorf("sql/migrate: unexpected type %T for reverse commands", r)
+	}
+	return
+}
+
 type (
 	// The Driver interface must be implemented by the different dialects to support database
 	// migration authoring/planning and applying. ExecQuerier, Inspector and Differ, provide
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 1cf0e28a..5c63f9e4 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -25,9 +25,6 @@ func (p *planApply) PlanChanges(_ context.Context, name string, changes []schema
 		conn: p.conn,
 		Plan: migrate.Plan{
 			Name: name,
-			// A plan is reversible, if all
-			// its changes are reversible.
-			Reversible: true,
 			// All statements generated by state will cause implicit commit.
 			// https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html
 			Transactional: false,
@@ -39,11 +36,8 @@ func (p *planApply) PlanChanges(_ context.Context, name string, changes []schema
 	if err := s.plan(changes); err != nil {
 		return nil, err
 	}
-	for _, c := range s.Changes {
-		if c.Reverse == "" {
-			s.Reversible = false
-			break
-		}
+	if err := sqlx.SetReversible(&s.Plan); err != nil {
+		return nil, err
 	}
 	return &s.Plan, nil
 }
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index 93171c01..264a45ef 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -25,7 +25,6 @@ func (p *planApply) PlanChanges(ctx context.Context, name string, changes []sche
 		conn: p.conn,
 		Plan: migrate.Plan{
 			Name:          name,
-			Reversible:    true,
 			Transactional: true,
 		},
 		created: make(map[string]*schema.EnumType),
@@ -38,10 +37,8 @@ func (p *planApply) PlanChanges(ctx context.Context, name string, changes []sche
 	if err := s.plan(ctx, changes); err != nil {
 		return nil, err
 	}
-	for _, c := range s.Changes {
-		if c.Reverse == "" {
-			s.Reversible = false
-		}
+	if err := sqlx.SetReversible(&s.Plan); err != nil {
+		return nil, err
 	}
 	return &s.Plan, nil
 }
@@ -83,7 +80,7 @@ func (s *state) plan(ctx context.Context, changes []schema.Change) error {
 		case *schema.AddTable:
 			err = s.addTable(ctx, c)
 		case *schema.DropTable:
-			s.dropTable(c)
+			err = s.dropTable(ctx, c)
 		case *schema.ModifyTable:
 			err = s.modifyTable(ctx, c)
 		case *schema.RenameTable:
@@ -194,7 +191,11 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 }
 
 // dropTable builds and executes the query for dropping a table from a schema.
-func (s *state) dropTable(drop *schema.DropTable) {
+func (s *state) dropTable(ctx context.Context, drop *schema.DropTable) error {
+	rs := &state{conn: s.conn, PlanOptions: s.PlanOptions}
+	if err := rs.addTable(ctx, &schema.AddTable{T: drop.T}); err != nil {
+		return fmt.Errorf("calculate reverse for drop table %q: %w", drop.T.Name, err)
+	}
 	b := s.Build("DROP TABLE")
 	if sqlx.Has(drop.Extra, &schema.IfExists{}) {
 		b.P("IF EXISTS")
@@ -204,7 +205,20 @@ func (s *state) dropTable(drop *schema.DropTable) {
 		Cmd:     b.String(),
 		Source:  drop,
 		Comment: fmt.Sprintf("drop %q table", drop.T.Name),
+		// The reverse of 'DROP TABLE' might be a multi
+		// statement operation. e.g., table with indexes.
+		Reverse: func() any {
+			cmd := make([]string, len(rs.Changes))
+			for i, c := range rs.Changes {
+				cmd[i] = c.Cmd
+			}
+			if len(cmd) == 1 {
+				return cmd[0]
+			}
+			return cmd
+		}(),
 	})
+	return nil
 }
 
 // modifyTable builds the statements that bring the table into its modified state.
@@ -324,13 +338,17 @@ func (s *state) modifyTable(ctx context.Context, modify *schema.ModifyTable) err
 			alter = append(alter, change)
 		}
 	}
-	s.dropIndexes(modify.T, dropI...)
+	if err := s.dropIndexes(modify.T, dropI...); err != nil {
+		return err
+	}
 	if len(alter) > 0 {
 		if err := s.alterTable(modify.T, alter); err != nil {
 			return err
 		}
 	}
-	s.addIndexes(modify.T, addI...)
+	if err := s.addIndexes(modify.T, addI...); err != nil {
+		return err
+	}
 	s.append(changes...)
 	return nil
 }
@@ -667,16 +685,19 @@ func (s *state) indexComment(t *schema.Table, idx *schema.Index, to, from string
 	}
 }
 
-func (s *state) dropIndexes(t *schema.Table, indexes ...*schema.Index) {
-	rs := &state{conn: s.conn}
-	rs.addIndexes(t, indexes...)
+func (s *state) dropIndexes(t *schema.Table, indexes ...*schema.Index) error {
+	rs := &state{conn: s.conn, PlanOptions: s.PlanOptions}
+	if err := rs.addIndexes(t, indexes...); err != nil {
+		return err
+	}
 	for i, idx := range indexes {
 		s.append(&migrate.Change{
-			Cmd:     rs.Changes[i].Reverse,
+			Cmd:     rs.Changes[i].Reverse.(string),
 			Comment: fmt.Sprintf("drop index %q from table: %q", idx.Name, t.Name),
 			Reverse: rs.Changes[i].Cmd,
 		})
 	}
+	return nil
 }
 
 func (s *state) mayAddEnums(ctx context.Context, t *schema.Table, columns ...*schema.Column) error {
diff --git a/sql/sqlite/migrate.go b/sql/sqlite/migrate.go
index 0b2dca48..e8d06de6 100644
--- a/sql/sqlite/migrate.go
+++ b/sql/sqlite/migrate.go
@@ -23,7 +23,6 @@ func (p *planApply) PlanChanges(ctx context.Context, name string, changes []sche
 		conn: p.conn,
 		Plan: migrate.Plan{
 			Name:          name,
-			Reversible:    true,
 			Transactional: true,
 		},
 		PlanOptions: migrate.PlanOptions{
@@ -38,10 +37,16 @@ func (p *planApply) PlanChanges(ctx context.Context, name string, changes []sche
 	if err := s.plan(ctx, changes); err != nil {
 		return nil, err
 	}
-	for _, c := range s.Changes {
-		if c.Reverse == "" {
-			s.Reversible = false
-		}
+	if err := sqlx.SetReversible(&s.Plan); err != nil {
+		return nil, err
+	}
+	// Disable foreign-keys enforcement if it is required
+	// by one of the changes in the plan.
+	if s.skipFKs {
+		// Callers should note that these 2 pragmas are no-op in transactions,
+		// See: https://sqlite.org/pragma.html#pragma_foreign_keys.
+		s.Changes = append([]*migrate.Change{{Cmd: "PRAGMA foreign_keys = off", Comment: "disable the enforcement of foreign-keys constraints"}}, s.Changes...)
+		s.append(&migrate.Change{Cmd: "PRAGMA foreign_keys = on", Comment: "enable back the enforcement of foreign-keys constraints"})
 	}
 	return &s.Plan, nil
 }
@@ -71,7 +76,7 @@ func (s *state) plan(ctx context.Context, changes []schema.Change) (err error) {
 		case *schema.AddTable:
 			err = s.addTable(ctx, c)
 		case *schema.DropTable:
-			err = s.dropTable(c)
+			err = s.dropTable(ctx, c)
 		case *schema.ModifyTable:
 			err = s.modifyTable(ctx, c)
 		case *schema.RenameTable:
@@ -83,14 +88,6 @@ func (s *state) plan(ctx context.Context, changes []schema.Change) (err error) {
 			return err
 		}
 	}
-	// Disable foreign-keys enforcement if it is required
-	// by one of the changes in the plan.
-	if s.skipFKs {
-		// Callers should note that these 2 pragmas are no-op in transactions,
-		// See: https://sqlite.org/pragma.html#pragma_foreign_keys.
-		s.Changes = append([]*migrate.Change{{Cmd: "PRAGMA foreign_keys = off", Comment: "disable the enforcement of foreign-keys constraints"}}, s.Changes...)
-		s.append(&migrate.Change{Cmd: "PRAGMA foreign_keys = on", Comment: "enable back the enforcement of foreign-keys constraints"})
-	}
 	return nil
 }
 
@@ -144,7 +141,11 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 }
 
 // dropTable builds and executes the query for dropping a table from a schema.
-func (s *state) dropTable(drop *schema.DropTable) error {
+func (s *state) dropTable(ctx context.Context, drop *schema.DropTable) error {
+	rs := &state{conn: s.conn, PlanOptions: s.PlanOptions}
+	if err := rs.addTable(ctx, &schema.AddTable{T: drop.T}); err != nil {
+		return fmt.Errorf("calculate reverse for drop table %q: %w", drop.T.Name, err)
+	}
 	s.skipFKs = true
 	b := s.Build("DROP TABLE").Ident(drop.T.Name)
 	if sqlx.Has(drop.Extra, &schema.IfExists{}) {
@@ -154,6 +155,18 @@ func (s *state) dropTable(drop *schema.DropTable) error {
 		Cmd:     b.String(),
 		Source:  drop,
 		Comment: fmt.Sprintf("drop %q table", drop.T.Name),
+		// The reverse of 'DROP TABLE' might be a multi
+		// statement operation. e.g., table with indexes.
+		Reverse: func() any {
+			cmd := make([]string, len(rs.Changes))
+			for i, c := range rs.Changes {
+				cmd[i] = c.Cmd
+			}
+			if len(cmd) == 1 {
+				return cmd[0]
+			}
+			return cmd
+		}(),
 	})
 	return nil
 }
@@ -244,7 +257,7 @@ func (s *state) dropIndexes(t *schema.Table, indexes ...*schema.Index) error {
 	}
 	for i := range rs.Changes {
 		s.append(&migrate.Change{
-			Cmd:     rs.Changes[i].Reverse,
+			Cmd:     rs.Changes[i].Reverse.(string),
 			Reverse: rs.Changes[i].Cmd,
 			Comment: fmt.Sprintf("drop index %q from table: %q", indexes[i].Name, t.Name),
 		})
diff --git a/sql/sqltool/tool.go b/sql/sqltool/tool.go
index d6a5b321..a0768751 100644
--- a/sql/sqltool/tool.go
+++ b/sql/sqltool/tool.go
@@ -26,7 +26,7 @@ var (
 		"{{ now }}{{ with .Name }}_{{ . }}{{ end }}.up.sql",
 		`{{ range .Changes }}{{ with .Comment }}-- {{ println . }}{{ end }}{{ printf "%s;\n" .Cmd }}{{ end }}`,
 		"{{ now }}{{ with .Name }}_{{ . }}{{ end }}.down.sql",
-		`{{ range rev .Changes }}{{ if .Reverse }}{{ with .Comment }}-- reverse: {{ println . }}{{ end }}{{ printf "%s;\n" .Reverse }}{{ end }}{{ end }}`,
+		`{{ range $c := rev .Changes }}{{ with $stmts := .ReverseStmts }}{{ with $c.Comment }}-- reverse: {{ println . }}{{ end }}{{ range $stmts }}{{ printf "%s;\n" . }}{{ end }}{{ end }}{{ end }}`,
 	)
 	// GooseFormatter returns migrate.Formatter compatible with pressly/goose.
 	GooseFormatter = templateFormatter(
@@ -34,14 +34,14 @@ var (
 		`-- +goose Up
 {{ range .Changes }}{{ with .Comment }}-- {{ println . }}{{ end }}{{ printf "%s;\n" .Cmd }}{{ end }}
 -- +goose Down
-{{ range rev .Changes }}{{ if .Reverse }}{{ with .Comment }}-- reverse: {{ println . }}{{ end }}{{ printf "%s;\n" .Reverse }}{{ end }}{{ end }}`,
+{{ range $c := rev .Changes }}{{ with $stmts := .ReverseStmts }}{{ with $c.Comment }}-- reverse: {{ println . }}{{ end }}{{ range $stmts }}{{ printf "%s;\n" . }}{{ end }}{{ end }}{{ end }}`,
 	)
 	// FlywayFormatter returns migrate.Formatter compatible with Flyway.
 	FlywayFormatter = templateFormatter(
 		"V{{ now }}{{ with .Name }}__{{ . }}{{ end }}.sql",
 		`{{ range .Changes }}{{ with .Comment }}-- {{ println . }}{{ end }}{{ printf "%s;\n" .Cmd }}{{ end }}`,
 		"U{{ now }}{{ with .Name }}__{{ . }}{{ end }}.sql",
-		`{{ range rev .Changes }}{{ if .Reverse }}{{ with .Comment }}-- reverse: {{ println . }}{{ end }}{{ printf "%s;\n" .Reverse }}{{ end }}{{ end }}`,
+		`{{ range $c := rev .Changes }}{{ with $stmts := .ReverseStmts }}{{ with $c.Comment }}-- reverse: {{ println . }}{{ end }}{{ range $stmts }}{{ printf "%s;\n" . }}{{ end }}{{ end }}{{ end }}`,
 	)
 	// LiquibaseFormatter returns migrate.Formatter compatible with Liquibase.
 	LiquibaseFormatter = templateFormatter(
@@ -53,8 +53,8 @@ var (
 --changeset atlas:{{ $now }}-{{ inc $index }}
 {{ with $change.Comment }}--comment: {{ . }}{{ end }}
 {{ $change.Cmd }};
-{{ with $change.Reverse }}--rollback: {{ . }};{{ end }}
-{{ end }}`,
+{{ with $stmts := .ReverseStmts }}{{ range $stmts }}{{ printf "--rollback: %s;\n" . }}{{ end }}{{ end }}
+{{- end }}`,
 	)
 	// DBMateFormatter returns migrate.Formatter compatible with amacneil/dbmate.
 	DBMateFormatter = templateFormatter(
@@ -62,7 +62,7 @@ var (
 		`-- migrate:up
 {{ range .Changes }}{{ with .Comment }}-- {{ println . }}{{ end }}{{ printf "%s;\n" .Cmd }}{{ end }}
 -- migrate:down
-{{ range rev .Changes }}{{ if .Reverse }}{{ with .Comment }}-- reverse: {{ println . }}{{ end }}{{ printf "%s;\n" .Reverse }}{{ end }}{{ end }}`,
+{{ range $c := rev .Changes }}{{ with $stmts := .ReverseStmts }}{{ with $c.Comment }}-- reverse: {{ println . }}{{ end }}{{ range $stmts }}{{ printf "%s;\n" . }}{{ end }}{{ end }}{{ end }}`,
 	)
 	// DbmateFormatter is the same as DBMateFormatter.
 	// Deprecated: Use DBMateFormatter instead.


Test patch:
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index a6cfaac4..1947d60b 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -322,13 +322,46 @@ func TestPlanChanges(t *testing.T) {
 		},
 		{
 			changes: []schema.Change{
-				&schema.DropTable{T: &schema.Table{Name: "posts"}},
+				&schema.DropTable{T: schema.NewTable("posts").AddColumns(schema.NewIntColumn("id", "int"))},
 			},
 			wantPlan: &migrate.Plan{
-				Reversible:    false,
+				Reversible:    true,
+				Transactional: true,
+				Changes: []*migrate.Change{
+					{
+						Cmd:     `DROP TABLE "posts"`,
+						Reverse: `CREATE TABLE "posts" ("id" integer NOT NULL)`,
+					},
+				},
+			},
+		},
+		{
+			changes: []schema.Change{
+				&schema.DropTable{
+					T: func() *schema.Table {
+						id := schema.NewIntColumn("id", "int")
+						return schema.NewTable("posts").
+							SetComment("a8m's posts").
+							AddColumns(id).
+							AddIndexes(
+								schema.NewIndex("idx").AddColumns(id).SetComment("a8m's index"),
+							)
+					}(),
+				},
+			},
+			wantPlan: &migrate.Plan{
+				Reversible:    true,
 				Transactional: true,
 				Changes: []*migrate.Change{
-					{Cmd: `DROP TABLE "posts"`},
+					{
+						Cmd: `DROP TABLE "posts"`,
+						Reverse: []string{
+							`CREATE TABLE "posts" ("id" integer NOT NULL)`,
+							`CREATE INDEX "idx" ON "posts" ("id")`,
+							`COMMENT ON TABLE "posts" IS 'a8m''s posts'`,
+							`COMMENT ON INDEX "idx" IS 'a8m''s index'`,
+						},
+					},
 				},
 			},
 		},
diff --git a/sql/sqlite/migrate_test.go b/sql/sqlite/migrate_test.go
index d98df146..1bab0d8b 100644
--- a/sql/sqlite/migrate_test.go
+++ b/sql/sqlite/migrate_test.go
@@ -79,13 +79,47 @@ func TestPlanChanges(t *testing.T) {
 		},
 		{
 			changes: []schema.Change{
-				&schema.DropTable{T: &schema.Table{Name: "posts"}},
+				&schema.DropTable{T: schema.NewTable("posts").AddColumns(schema.NewIntColumn("id", "integer"))},
 			},
 			plan: &migrate.Plan{
+				Reversible:    true,
 				Transactional: true,
 				Changes: []*migrate.Change{
 					{Cmd: "PRAGMA foreign_keys = off"},
-					{Cmd: "DROP TABLE `posts`"},
+					{
+						Cmd:     "DROP TABLE `posts`",
+						Reverse: "CREATE TABLE `posts` (`id` integer NOT NULL)",
+					},
+					{Cmd: "PRAGMA foreign_keys = on"},
+				},
+			},
+		},
+		{
+			changes: []schema.Change{
+				&schema.DropTable{
+					T: func() *schema.Table {
+						id := schema.NewIntColumn("id", "int")
+						return schema.NewTable("posts").
+							SetComment("a8m's posts").
+							AddColumns(id).
+							AddIndexes(
+								schema.NewIndex("idx").AddColumns(id).SetComment("a8m's index"),
+							)
+					}(),
+				},
+			},
+			plan: &migrate.Plan{
+				Reversible:    true,
+				Transactional: true,
+				Changes: []*migrate.Change{
+					{Cmd: "PRAGMA foreign_keys = off"},
+					{
+						Cmd: "DROP TABLE `posts`",
+						Reverse: []string{
+							"CREATE TABLE `posts` (`id` int NOT NULL)",
+							"CREATE INDEX `idx` ON `posts` (`id`)",
+						},
+					},
 					{Cmd: "PRAGMA foreign_keys = on"},
 				},
 			},
diff --git a/sql/sqltool/tool_test.go b/sql/sqltool/tool_test.go
index 9ba415a2..83cea39b 100644
--- a/sql/sqltool/tool_test.go
+++ b/sql/sqltool/tool_test.go
@@ -21,6 +21,7 @@ var plan = &migrate.Plan{
 	Changes: []*migrate.Change{
 		{Cmd: "CREATE TABLE t1(c int)", Reverse: "DROP TABLE t1 IF EXISTS", Comment: "create table t1"},
 		{Cmd: "CREATE TABLE t2(c int)", Reverse: "DROP TABLE t2", Comment: "create table t2"},
+		{Cmd: "DROP TABLE t3", Reverse: []string{"CREATE TABLE t1(id int)", "CREATE INDEX idx ON t1(id)"}, Comment: "drop table t3"},
 	},
 }
 
@@ -39,8 +40,13 @@ func TestFormatters(t *testing.T) {
 CREATE TABLE t1(c int);
 -- create table t2
 CREATE TABLE t2(c int);
+-- drop table t3
+DROP TABLE t3;
 `,
-				v + "_tooling-plan.down.sql": `-- reverse: create table t2
+				v + "_tooling-plan.down.sql": `-- reverse: drop table t3
+CREATE TABLE t1(id int);
+CREATE INDEX idx ON t1(id);
+-- reverse: create table t2
 DROP TABLE t2;
 -- reverse: create table t1
 DROP TABLE t1 IF EXISTS;
@@ -56,8 +62,13 @@ DROP TABLE t1 IF EXISTS;
 CREATE TABLE t1(c int);
 -- create table t2
 CREATE TABLE t2(c int);
+-- drop table t3
+DROP TABLE t3;
 
 -- +goose Down
+-- reverse: drop table t3
+CREATE TABLE t1(id int);
+CREATE INDEX idx ON t1(id);
 -- reverse: create table t2
 DROP TABLE t2;
 -- reverse: create table t1
@@ -73,8 +84,13 @@ DROP TABLE t1 IF EXISTS;
 CREATE TABLE t1(c int);
 -- create table t2
 CREATE TABLE t2(c int);
+-- drop table t3
+DROP TABLE t3;
 `,
-				"U" + v + "__tooling-plan.sql": `-- reverse: create table t2
+				"U" + v + "__tooling-plan.sql": `-- reverse: drop table t3
+CREATE TABLE t1(id int);
+CREATE INDEX idx ON t1(id);
+-- reverse: create table t2
 DROP TABLE t2;
 -- reverse: create table t1
 DROP TABLE t1 IF EXISTS;
@@ -86,16 +102,22 @@ DROP TABLE t1 IF EXISTS;
 			sqltool.LiquibaseFormatter,
 			map[string]string{
 				v + "_tooling-plan.sql": fmt.Sprintf(`--liquibase formatted sql
---changeset atlas:%s-1
+--changeset atlas:%[1]s-1
 --comment: create table t1
 CREATE TABLE t1(c int);
 --rollback: DROP TABLE t1 IF EXISTS;
 
---changeset atlas:%s-2
+--changeset atlas:%[1]s-2
 --comment: create table t2
 CREATE TABLE t2(c int);
 --rollback: DROP TABLE t2;
-`, v, v),
+
+--changeset atlas:%[1]s-3
+--comment: drop table t3
+DROP TABLE t3;
+--rollback: CREATE TABLE t1(id int);
+--rollback: CREATE INDEX idx ON t1(id);
+`, v),
 			},
 		},
 		{
@@ -107,8 +129,13 @@ CREATE TABLE t2(c int);
 CREATE TABLE t1(c int);
 -- create table t2
 CREATE TABLE t2(c int);
+-- drop table t3
+DROP TABLE t3;
 
 -- migrate:down
+-- reverse: drop table t3
+CREATE TABLE t1(id int);
+CREATE INDEX idx ON t1(id);
 -- reverse: create table t2
 DROP TABLE t2;
 -- reverse: create table t1