Repository: ariga/atlas
Pull request #213 (2021-11-10 14:43:24)

cmd/atlas: diff printing
![image](https://user-images.githubusercontent.com/5472778/141137712-08bafa1a-ee87-4b90-8d17-10a88383f233.png)



Patch:
diff --git a/cmd/atlas/apply.go b/cmd/atlas/apply.go
index 56917ba9..2a9e4b11 100644
--- a/cmd/atlas/apply.go
+++ b/cmd/atlas/apply.go
@@ -58,6 +58,15 @@ func applyRun(d *Driver, u schemaUnmarshaler, dsn string, file string) {
 	cobra.CheckErr(err)
 	changes, err := d.SchemaDiff(s, &desired)
 	cobra.CheckErr(err)
+	schemaCmd.Println("-- Planned Changes:")
+	for _, ch := range changes {
+		desc, err := changeDescriptor(ctx, ch, d)
+		cobra.CheckErr(err)
+		schemaCmd.Println("--", desc.typ, ":", desc.subject)
+		for _, q := range desc.queries {
+			schemaCmd.Println(q, ";")
+		}
+	}
 	prompt := promptui.Select{
 		Label: "Are you sure?",
 		Items: []string{answerApply, answerAbort},
diff --git a/cmd/atlas/diff.go b/cmd/atlas/diff.go
new file mode 100644
index 00000000..0ebf0f87
--- /dev/null
+++ b/cmd/atlas/diff.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"ariga.io/atlas/sql/schema"
+)
+
+type changeDesc struct {
+	typ, subject string
+	queries      []string
+}
+
+func changeDescriptor(ctx context.Context, c schema.Change, d *Driver) (*changeDesc, error) {
+	desc := &changeDesc{}
+	switch c := c.(type) {
+	case *schema.AddTable:
+		desc.typ = "Add Table"
+		desc.subject = c.T.Name
+	case *schema.DropTable:
+		desc.typ = "Drop Table"
+		desc.subject = c.T.Name
+	case *schema.ModifyTable:
+		desc.typ = "Modify Table"
+		desc.subject = c.T.Name
+	case *schema.AddColumn:
+		desc.typ = "Add Column"
+		desc.subject = c.C.Name
+	case *schema.DropColumn:
+		desc.typ = "Drop Column"
+		desc.subject = c.C.Name
+	case *schema.ModifyColumn:
+		desc.typ = "Modify Column"
+		desc.subject = c.From.Name
+	case *schema.AddAttr:
+		desc.typ = "Add Attr"
+	case *schema.ModifyAttr:
+		desc.typ = "Modify Attr"
+	case *schema.DropAttr:
+		desc.typ = "Drop Attr"
+	case *schema.AddIndex:
+		desc.typ = "Add Index"
+		desc.subject = c.I.Name
+	case *schema.DropIndex:
+		desc.typ = "Drop Index"
+		desc.subject = c.I.Name
+	case *schema.ModifyIndex:
+		desc.typ = "Modify Index"
+		desc.subject = c.From.Name
+	case *schema.AddForeignKey:
+		desc.typ = "Add ForeignKey"
+		desc.subject = c.F.Symbol
+	case *schema.DropForeignKey:
+		desc.typ = "Drop ForeignKey"
+		desc.subject = c.F.Symbol
+	case *schema.ModifyForeignKey:
+		desc.typ = "Modify ForeignKey"
+		desc.subject = c.From.Symbol
+	}
+	d.interceptor.on()
+	defer d.interceptor.clear()
+	defer d.interceptor.off()
+	if err := d.Exec(ctx, []schema.Change{c}); err != nil {
+		return nil, fmt.Errorf("atlas: failed getting planned sql: %w", err)
+	}
+	desc.queries = d.interceptor.history
+	return desc, nil
+}
diff --git a/cmd/atlas/mux.go b/cmd/atlas/mux.go
index 9b23d6b0..604c92d4 100644
--- a/cmd/atlas/mux.go
+++ b/cmd/atlas/mux.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"context"
+	"database/sql"
 	"fmt"
 	"strings"
 
@@ -23,6 +25,7 @@ type (
 		schema.Inspector
 		MarshalSpec   func(v interface{}, marshaler schemaspec.Marshaler) ([]byte, error)
 		UnmarshalSpec func(data []byte, unmarshaler schemaspec.Unmarshaler, v interface{}) error
+		interceptor   *interceptor
 	}
 
 	schemaUnmarshal struct {
@@ -112,3 +115,32 @@ func (p *schemaMarshal) marshal(s *schema.Schema) ([]byte, error) {
 func (p *schemaUnmarshal) unmarshal(b []byte, v interface{}) error {
 	return p.unmarshalSpec(b, p.unmarshaler, v)
 }
+
+// interceptor is an ExecQuerier that when activated can intercept and store queries initiated by Exec
+// invocations. interceptor is used for capturing the planned SQL statement that a migration is going
+// to perform.
+type interceptor struct {
+	schema.ExecQuerier
+	intercept bool
+	history   []string
+}
+
+func (i *interceptor) on() {
+	i.intercept = true
+}
+
+func (i *interceptor) off() {
+	i.intercept = false
+}
+
+func (i *interceptor) clear() {
+	i.history = nil
+}
+
+func (i *interceptor) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
+	if i.intercept {
+		i.history = append(i.history, query)
+		return nil, nil
+	}
+	return i.ExecQuerier.ExecContext(ctx, query, args...)
+}
diff --git a/cmd/atlas/provider.go b/cmd/atlas/provider.go
index fe54399a..3f4b14fe 100644
--- a/cmd/atlas/provider.go
+++ b/cmd/atlas/provider.go
@@ -17,7 +17,8 @@ func mysqlProvider(dsn string) (*Driver, error) {
 	if err != nil {
 		return nil, err
 	}
-	drv, err := mysql.Open(db)
+	i := &interceptor{ExecQuerier: db}
+	drv, err := mysql.Open(i)
 	if err != nil {
 		return nil, err
 	}
@@ -25,6 +26,7 @@ func mysqlProvider(dsn string) (*Driver, error) {
 		Differ:        drv.Diff(),
 		Execer:        drv.Migrate(),
 		Inspector:     drv,
+		interceptor:   i,
 		MarshalSpec:   mysql.MarshalSpec,
 		UnmarshalSpec: mysql.UnmarshalSpec,
 	}, nil
@@ -34,7 +36,8 @@ func postgresProvider(dsn string) (*Driver, error) {
 	if err != nil {
 		return nil, err
 	}
-	drv, err := postgres.Open(db)
+	i := &interceptor{ExecQuerier: db}
+	drv, err := postgres.Open(i)
 	if err != nil {
 		return nil, err
 	}
@@ -42,6 +45,7 @@ func postgresProvider(dsn string) (*Driver, error) {
 		Differ:        drv.Diff(),
 		Execer:        drv.Migrate(),
 		Inspector:     drv,
+		interceptor:   i,
 		MarshalSpec:   postgres.MarshalSpec,
 		UnmarshalSpec: postgres.UnmarshalSpec,
 	}, nil


Test patch:
diff --git a/cmd/atlas/diff_test.go b/cmd/atlas/diff_test.go
new file mode 100644
index 00000000..0ad5533c
--- /dev/null
+++ b/cmd/atlas/diff_test.go
@@ -0,0 +1,92 @@
+package main
+
+import (
+	"context"
+	"testing"
+
+	"ariga.io/atlas/schema/schemaspec/schemahcl"
+	"ariga.io/atlas/sql/mysql"
+	"ariga.io/atlas/sql/schema"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDiffPrint(t *testing.T) {
+
+	beforeHcl := `
+schema "default" {
+}
+table "users" {
+	schema = "default"
+	column "id" {
+		type = "int"
+	}
+}
+`
+	afterHcl := `
+schema "default" {
+}
+table "users" {
+	schema = "default"
+	column "id" {
+		type = "string"
+	}
+	column "name" {
+		type = "string"
+	}
+}
+
+table "groups" {
+	schema = "default"
+	column "id" {
+		type = "int"
+	}
+}
+`
+	ctx := context.Background()
+	d := dummyDriver()
+	changes := diff(t, d, beforeHcl, afterHcl)
+	var changeDescs []*changeDesc
+	for _, ch := range changes {
+		descriptor, err := changeDescriptor(ctx, ch, d)
+		require.NoError(t, err)
+		changeDescs = append(changeDescs, descriptor)
+	}
+	require.EqualValues(t, []*changeDesc{
+		{
+			typ:     "ModifyTable",
+			subject: "users",
+			queries: []string{
+				"ALTER TABLE `default`.`users` MODIFY COLUMN `id` varchar(255) NOT NULL, ADD COLUMN `name` varchar(255) NOT NULL",
+			},
+		},
+		{
+			typ:     "AddTable",
+			subject: "groups",
+			queries: []string{
+				"CREATE TABLE `default`.`groups` (`id` int NOT NULL)",
+			},
+		},
+	}, changeDescs)
+}
+
+func diff(t *testing.T, d *Driver, beforeHcl, afterHcl string) []schema.Change {
+	before, after := schema.Schema{}, schema.Schema{}
+	err := mysql.UnmarshalSpec([]byte(beforeHcl), schemahcl.Unmarshal, &before)
+	require.NoError(t, err)
+	err = mysql.UnmarshalSpec([]byte(afterHcl), schemahcl.Unmarshal, &after)
+	require.NoError(t, err)
+	before.Realm = &schema.Realm{}
+	diff, err := d.SchemaDiff(&before, &after)
+	require.NoError(t, err)
+	return diff
+}
+
+func dummyDriver() *Driver {
+	i := &interceptor{}
+	drv := &mysql.Driver{ExecQuerier: i}
+	return &Driver{
+		Execer:      drv.Migrate(),
+		Differ:      drv.Diff(),
+		interceptor: i,
+	}
+}