Repository: ariga/atlas
Pull request #649 (2022-03-14 08:35:09)

cmd/action: migrate tests


Patch:
diff --git a/cmd/action/migrate.go b/cmd/action/migrate.go
index 9f7ddea5..9a5e3aa7 100644
--- a/cmd/action/migrate.go
+++ b/cmd/action/migrate.go
@@ -6,6 +6,7 @@ package action
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"strings"
@@ -40,13 +41,16 @@ var (
 		Use:   "migrate",
 		Short: "Manage versioned migration files",
 		Long:  "'atlas migrate' wraps several sub-commands for migration management.",
-		PersistentPreRun: func(*cobra.Command, []string) {
+		PersistentPreRunE: func(*cobra.Command, []string) error {
 			// Migrate commands will not run on a broken migration directory, unless the force flag is given.
 			if !MigrateFlags.Force {
 				dir, err := dir()
-				cobra.CheckErr(err)
-				cobra.CheckErr(migrate.Validate(dir)) // TODO(masseelch): tell the user what's wrong
+				if err != nil {
+					return err
+				}
+				return migrate.Validate(dir) // TODO(masseelch): tell the user what's wrong
 			}
+			return nil
 		},
 	}
 	// MigrateDiffCmd represents the migrate diff command.
@@ -59,7 +63,7 @@ the migration directory state to the desired schema. The desired state can be an
 		Example: `  atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to mysql://user:pass@localhost:3306/dbname
   atlas migrate diff --dev-url mysql://user:pass@localhost:3306/dev --to file://atlas.hcl`,
 		Args: cobra.MaximumNArgs(1),
-		Run:  CmdMigrateDiffRun,
+		RunE: CmdMigrateDiffRun,
 	}
 )
 
@@ -78,19 +82,30 @@ func init() {
 }
 
 // CmdMigrateDiffRun is the command executed when running the CLI with 'migrate diff' args.
-func CmdMigrateDiffRun(cmd *cobra.Command, args []string) { // TODO(masseelch): tests
+func CmdMigrateDiffRun(cmd *cobra.Command, args []string) error {
 	// Open a dev driver.
 	dev, err := DefaultMux.OpenAtlas(MigrateFlags.DevURL)
-	cobra.CheckErr(err)
+	if err != nil {
+		return err
+	}
+	if err := checkClean(cmd.Context(), dev); err != nil {
+		return err
+	}
 	// Open the migration directory. For now only local directories are supported.
 	dir, err := dir()
-	cobra.CheckErr(err)
+	if err != nil {
+		return err
+	}
 	// Get a state reader for the desired state.
 	desired, err := to(cmd.Context(), dev)
-	cobra.CheckErr(err)
+	if err != nil {
+		return err
+	}
 	// Only create one file per plan.
 	tf, err := migrate.NewTemplateFormatter(nameTmpl, contentTmpl)
-	cobra.CheckErr(err)
+	if err != nil {
+		return err
+	}
 	// Plan the changes and create a new migration file.
 	pl := migrate.NewPlanner(dev, dir, migrate.WithFormatter(tf))
 	var name string
@@ -98,9 +113,11 @@ func CmdMigrateDiffRun(cmd *cobra.Command, args []string) { // TODO(masseelch):
 		name = args[0]
 	}
 	plan, err := pl.Plan(cmd.Context(), name, desired)
-	cobra.CheckErr(err)
+	if err != nil {
+		return err
+	}
 	// Write the plan to a new file.
-	cobra.CheckErr(pl.WritePlan(plan))
+	return pl.WritePlan(plan)
 	// TODO(masseelch): clean up dev after reading the state from migration dir.
 }
 
@@ -126,7 +143,9 @@ func to(ctx context.Context, d *Driver) (migrate.StateReader, error) {
 	}
 	schemas := MigrateFlags.Schemas
 	if n, err := SchemaNameFromURL(ctx, parts[0]); n != "" {
-		cobra.CheckErr(err)
+		if err != nil {
+			return nil, err
+		}
 		schemas = append(schemas, n)
 	}
 	switch parts[0] {
@@ -167,6 +186,21 @@ func to(ctx context.Context, d *Driver) (migrate.StateReader, error) {
 	}
 }
 
+func checkClean(ctx context.Context, drv *Driver) error {
+	realm, err := drv.InspectRealm(ctx, nil)
+	if err != nil {
+		return err
+	}
+	if len(realm.Schemas) == 0 {
+		return nil
+	}
+	// If this is an SQLite database it is considered clean if the "main" schema has no tables.
+	if strings.HasPrefix(MigrateFlags.DevURL, "sqlite") && realm.Schemas[0].Name == "main" && len(realm.Schemas[0].Tables) == 0 {
+		return nil
+	}
+	return errors.New("dev database must be clean")
+}
+
 var (
 	funcMap = template.FuncMap{
 		"now": func() string { return time.Now().Format("20060102150405") },
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index d800d0c9..640030f5 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -183,7 +183,7 @@ func NewPlanner(drv Driver, dir Dir, opts ...PlannerOption) *Planner {
 		p.fmt = DefaultFormatter
 	}
 	if p.dsr == nil {
-		p.dsr = GlobStateReader(p.dir, p.drv, "**/*.sql")
+		p.dsr = GlobStateReader(p.dir, p.drv, "*.sql")
 	}
 	return p
 }


Test patch:
diff --git a/cmd/action/diff_test.go b/cmd/action/diff_test.go
index c8b59baf..41a27087 100644
--- a/cmd/action/diff_test.go
+++ b/cmd/action/diff_test.go
@@ -62,6 +62,7 @@ func openSQLite(t *testing.T, seed string) string {
 func runCmd(cmd *cobra.Command, args ...string) (string, error) {
 	var out bytes.Buffer
 	cmd.SetOut(&out)
+	cmd.SetErr(&out)
 	cmd.SetArgs(args)
 	err := cmd.Execute()
 	return out.String(), err
diff --git a/cmd/action/env_test.go b/cmd/action/env_test.go
index d7d03f87..c18527a4 100644
--- a/cmd/action/env_test.go
+++ b/cmd/action/env_test.go
@@ -1,3 +1,7 @@
+// 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 action
 
 import (
diff --git a/cmd/action/migrate_test.go b/cmd/action/migrate_test.go
new file mode 100644
index 00000000..ba50be43
--- /dev/null
+++ b/cmd/action/migrate_test.go
@@ -0,0 +1,132 @@
+// 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 action
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestMigrate_Diff(t *testing.T) {
+	_, err := runCmd(RootCmd, "migrate")
+	require.NoError(t, err)
+	p := t.TempDir()
+
+	// Expect no clean dev error.
+	s, err := runCmd(
+		RootCmd, "migrate", "diff",
+		"name",
+		"--dir", "file://"+p,
+		"--dev-url", openSQLite(t, "create table t (c int);"),
+		"--to", hclURL(t),
+	)
+	require.True(t, strings.HasPrefix(s, "Error: dev database must be clean"))
+	require.EqualError(t, err, "dev database must be clean")
+
+	// Works.
+	s, err = runCmd(
+		RootCmd, "migrate", "diff",
+		"name",
+		"--dir", "file://"+p,
+		"--dev-url", openSQLite(t, ""),
+		"--to", hclURL(t),
+	)
+	require.Zero(t, s)
+	require.NoError(t, err)
+	require.FileExists(t, filepath.Join(p, fmt.Sprintf("%s_name.sql", time.Now().Format("20060102150405"))))
+	require.FileExists(t, filepath.Join(p, "atlas.sum"))
+}
+
+const hcl = `
+schema "main" {
+}
+
+table "table" {
+  schema = schema.main
+  column "col" {
+    type    = int
+    comment = "column comment"
+  }
+  column "age" {
+    type = int
+  }
+  column "price1" {
+    type = int
+  }
+  column "price2" {
+    type           = int
+  }
+  column "account_name" {
+    type = varchar(32)
+    null = true
+  }
+  column "created_at" {
+    type    = datetime
+    default = sql("current_timestamp")
+  }
+  primary_key {
+    columns = [table.table.column.col]
+  }
+  index "index" {
+    unique  = true
+    columns = [
+      table.table.column.col,
+      table.table.column.age,
+    ]
+    comment = "index comment"
+  }
+  foreign_key "accounts" {
+    columns = [
+      table.table.column.account_name,
+    ]
+    ref_columns = [
+      table.accounts.column.name,
+    ]
+    on_delete = SET_NULL
+    on_update = "NO_ACTION"
+  }
+  check "positive price" {
+    expr = "price1 > 0"
+  }
+  check {
+    expr     = "price1 <> price2"
+    enforced = true
+  }
+  check {
+    expr     = "price2 <> price1"
+    enforced = false
+  }
+  comment        = "table comment"
+}
+
+table "accounts" {
+  schema = schema.main
+  column "name" {
+    type = varchar(32)
+  }
+  column "unsigned_float" {
+    type     = float(10)
+    unsigned = true
+  }
+  column "unsigned_decimal" {
+    type     = decimal(10, 2)
+    unsigned = true
+  }
+  primary_key {
+    columns = [table.accounts.column.name]
+  }
+}`
+
+func hclURL(t *testing.T) string {
+	p := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(p, "atlas.hcl"), []byte(hcl), 0600))
+	return "file://" + filepath.Join(p, "atlas.hcl")
+}