Repository: ariga/atlas
Pull request #773 (2022-05-04 14:47:53)

cmd/atlascmd: migrate commands support format flag to spedify which t…
…ooling to use (golang-migrate/goose/flyway/liquibase)


Patch:
diff --git a/cmd/atlascmd/migrate.go b/cmd/atlascmd/migrate.go
index 8080d62f..56b1cbca 100644
--- a/cmd/atlascmd/migrate.go
+++ b/cmd/atlascmd/migrate.go
@@ -15,15 +15,17 @@ import (
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
+	"ariga.io/atlas/sql/sqltool"
 
 	"github.com/spf13/cobra"
 )
 
 const (
+	migrateDiffFlagDevURL  = "dev-url"
 	migrateFlagDir         = "dir"
-	migrateFlagSchema      = "schema"
 	migrateFlagForce       = "force"
-	migrateDiffFlagDevURL  = "dev-url"
+	migrateFlagFormat      = "format"
+	migrateFlagSchema      = "schema"
 	migrateDiffFlagTo      = "to"
 	migrateDiffFlagVerbose = "verbose"
 )
@@ -35,6 +37,7 @@ var (
 		DevURL  string
 		ToURL   string
 		Schemas []string
+		Format  string
 		Force   bool
 		Verbose bool
 	}
@@ -117,6 +120,7 @@ func init() {
 	// Global flags.
 	MigrateCmd.PersistentFlags().StringVarP(&MigrateFlags.DirURL, migrateFlagDir, "", "file://migrations", "select migration directory using DSN format")
 	MigrateCmd.PersistentFlags().StringSliceVarP(&MigrateFlags.Schemas, migrateFlagSchema, "", nil, "set schema names")
+	MigrateCmd.PersistentFlags().StringVarP(&MigrateFlags.Format, migrateFlagFormat, "", formatAtlas, "set migration file format")
 	MigrateCmd.PersistentFlags().BoolVarP(&MigrateFlags.Force, migrateFlagForce, "", false, "force a command to run on a broken migration directory state")
 	MigrateCmd.PersistentFlags().SortFlags = false
 	// Diff flags.
@@ -158,8 +162,12 @@ func CmdMigrateDiffRun(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
+	f, err := formatter()
+	if err != nil {
+		return err
+	}
 	// Plan the changes and create a new migration file.
-	pl := migrate.NewPlanner(dev.Driver, dir)
+	pl := migrate.NewPlanner(dev.Driver, dir, migrate.WithFormatter(f))
 	var name string
 	if len(args) > 0 {
 		name = args[0]
@@ -178,11 +186,15 @@ func CmdMigrateNewRun(_ *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
+	f, err := formatter()
+	if err != nil {
+		return err
+	}
 	var name string
 	if len(args) > 0 {
 		name = args[0]
 	}
-	return migrate.NewPlanner(nil, dir).WritePlan(&migrate.Plan{Name: name})
+	return migrate.NewPlanner(nil, dir, migrate.WithFormatter(f)).WritePlan(&migrate.Plan{Name: name})
 }
 
 // CmdMigrateHashRun is the command executed when running the CLI with 'migrate hash' args.
@@ -265,3 +277,28 @@ func to(ctx context.Context, client *sqlclient.Client) (migrate.StateReader, err
 		}, nil
 	}
 }
+
+const (
+	formatAtlas         = "atlas"
+	formatGolangMigrate = "golang-migrate"
+	formatGoose         = "goose"
+	formatFlyway        = "flyway"
+	formatLiquibase     = "liquibase"
+)
+
+func formatter() (migrate.Formatter, error) {
+	switch MigrateFlags.Format {
+	case formatAtlas:
+		return migrate.DefaultFormatter, nil
+	case formatGolangMigrate:
+		return sqltool.GolangMigrateFormatter, nil
+	case formatGoose:
+		return sqltool.GooseFormatter, nil
+	case formatFlyway:
+		return sqltool.FlywayFormatter, nil
+	case formatLiquibase:
+		return sqltool.LiquibaseFormatter, nil
+	default:
+		return nil, fmt.Errorf("unknown format %q", MigrateFlags.Format)
+	}
+}
diff --git a/doc/md/cli/reference.md b/doc/md/cli/reference.md
index be4ab93b..e0640c88 100644
--- a/doc/md/cli/reference.md
+++ b/doc/md/cli/reference.md
@@ -62,6 +62,7 @@ atlas migrate
 ```
       --dir string       select migration directory using DSN format (default "file://migrations")
       --force            force a command to run on a broken migration directory state
+      --format string    set migration file format (default "atlas")
       --schema strings   set schema names
 
 ```
diff --git a/sql/sqltool/tool.go b/sql/sqltool/tool.go
index 91806d43..5919ac12 100644
--- a/sql/sqltool/tool.go
+++ b/sql/sqltool/tool.go
@@ -51,7 +51,7 @@ var (
 	funcs = template.FuncMap{
 		"inc": func(x int) int { return x + 1 },
 		// now format the current time in a lexicographically ascending order while maintaining human readability.
-		"now": func() string { return time.Now().Format("20060102150405") },
+		"now": func() string { return time.Now().UTC().Format("20060102150405") },
 		"rev": reverse,
 	}
 )


Test patch:
diff --git a/cmd/atlascmd/migrate_test.go b/cmd/atlascmd/migrate_test.go
index ae4fb638..7dba8efe 100644
--- a/cmd/atlascmd/migrate_test.go
+++ b/cmd/atlascmd/migrate_test.go
@@ -83,19 +83,50 @@ func TestMigrate_Diff(t *testing.T) {
 }
 
 func TestMigrate_New(t *testing.T) {
-	p := t.TempDir()
+	var (
+		p = t.TempDir()
+		v = time.Now().UTC().Format("20060102150405")
+	)
 
 	s, err := runCmd(Root, "migrate", "new", "--dir", "file://"+p)
 	require.Zero(t, s)
 	require.NoError(t, err)
-	require.FileExists(t, filepath.Join(p, fmt.Sprintf("%s.sql", time.Now().UTC().Format("20060102150405"))))
+	require.FileExists(t, filepath.Join(p, v+".sql"))
 	require.FileExists(t, filepath.Join(p, "atlas.sum"))
+	require.Equal(t, 2, countFiles(t, p))
 
 	s, err = runCmd(Root, "migrate", "new", "my-migration-file", "--dir", "file://"+p)
 	require.Zero(t, s)
 	require.NoError(t, err)
-	require.FileExists(t, filepath.Join(p, fmt.Sprintf("%s_my-migration-file.sql", time.Now().UTC().Format("20060102150405"))))
+	require.FileExists(t, filepath.Join(p, v+"_my-migration-file.sql"))
 	require.FileExists(t, filepath.Join(p, "atlas.sum"))
+	require.Equal(t, 3, countFiles(t, p))
+
+	s, err = runCmd(Root, "migrate", "new", "golang-migrate", "--dir", "file://"+p, "--format", formatGolangMigrate)
+	require.Zero(t, s)
+	require.NoError(t, err)
+	require.FileExists(t, filepath.Join(p, v+"_golang-migrate.up.sql"))
+	require.FileExists(t, filepath.Join(p, v+"_golang-migrate.down.sql"))
+	require.Equal(t, 5, countFiles(t, p))
+
+	s, err = runCmd(Root, "migrate", "new", "goose", "--dir", "file://"+p, "--format", formatGoose)
+	require.Zero(t, s)
+	require.NoError(t, err)
+	require.FileExists(t, filepath.Join(p, v+"_goose.sql"))
+	require.Equal(t, 6, countFiles(t, p))
+
+	s, err = runCmd(Root, "migrate", "new", "flyway", "--dir", "file://"+p, "--format", formatFlyway)
+	require.Zero(t, s)
+	require.NoError(t, err)
+	require.FileExists(t, filepath.Join(p, fmt.Sprintf("V%s__%s.sql", v, formatFlyway)))
+	require.FileExists(t, filepath.Join(p, fmt.Sprintf("U%s__%s.sql", v, formatFlyway)))
+	require.Equal(t, 8, countFiles(t, p))
+
+	s, err = runCmd(Root, "migrate", "new", "liquibase", "--dir", "file://"+p, "--format", formatLiquibase)
+	require.Zero(t, s)
+	require.NoError(t, err)
+	require.FileExists(t, filepath.Join(p, v+"_liquibase.sql"))
+	require.Equal(t, 9, countFiles(t, p))
 }
 
 func TestMigrate_NewError(t *testing.T) {
@@ -112,7 +143,7 @@ func TestMigrate_NewError(t *testing.T) {
 	if err, ok := err.(*exec.ExitError); ok && !err.Success() {
 		return
 	}
-	t.Fatalf("process ran with err %v, want exist status 1", err)
+	t.Fatalf("process ran with err %v, want exit status 1", err)
 }
 
 func TestMigrate_Validate(t *testing.T) {
@@ -135,7 +166,7 @@ func TestMigrate_ValidateError(t *testing.T) {
 	if err, ok := err.(*exec.ExitError); ok && !err.Success() {
 		return
 	}
-	t.Fatalf("process ran with err %v, want exist status 1", err)
+	t.Fatalf("process ran with err %v, want exit status 1", err)
 }
 
 func TestMigrate_Hash(t *testing.T) {
@@ -176,7 +207,7 @@ func TestMigrate_HashError(t *testing.T) {
 	if err, ok := err.(*exec.ExitError); ok && !err.Success() {
 		return
 	}
-	t.Fatalf("process ran with err %v, want exist status 1", err)
+	t.Fatalf("process ran with err %v, want exit status 1", err)
 }
 
 const hcl = `
@@ -287,3 +318,9 @@ const lockErr = "lockErr"
 func (d *sqliteLockerDriver) Lock(context.Context, string, time.Duration) (schema.UnlockFunc, error) {
 	return func() error { return nil }, errors.New(lockErr)
 }
+
+func countFiles(t *testing.T, p string) int {
+	files, err := os.ReadDir(p)
+	require.NoError(t, err)
+	return len(files)
+}