Repository: ariga/atlas
Pull request #924 (2022-07-08 17:35:33)

sql/sqltool: add basic support for golang directory


Patch:
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 1229c05a..8309ea11 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -748,7 +748,7 @@ func (d *LocalDir) Files() ([]File, error) {
 		if err != nil {
 			return nil, fmt.Errorf("sql/migrate: read file %q: %w", n, err)
 		}
-		ret[i] = &LocalFile{n: n, b: b}
+		ret[i] = NewLocalFile(n, b)
 	}
 	return ret, nil
 }
@@ -783,6 +783,11 @@ type LocalFile struct {
 	b []byte
 }
 
+// NewLocalFile returns a new local file.
+func NewLocalFile(path string, data []byte) *LocalFile {
+	return &LocalFile{path, data}
+}
+
 // Name implements File.Name.
 func (f LocalFile) Name() string {
 	return f.n
diff --git a/sql/sqltool/tool.go b/sql/sqltool/tool.go
index 5919ac12..5e080d81 100644
--- a/sql/sqltool/tool.go
+++ b/sql/sqltool/tool.go
@@ -5,6 +5,10 @@
 package sqltool
 
 import (
+	"fmt"
+	"io/fs"
+	"sort"
+	"strings"
 	"text/template"
 	"time"
 
@@ -47,15 +51,59 @@ var (
 {{ with $change.Reverse }}--rollback: {{ . }};{{ end }}
 {{ end }}`,
 	)
-	// funcs contains the template.FuncMap for the different formatters.
-	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().UTC().Format("20060102150405") },
-		"rev": reverse,
-	}
 )
 
+// GolangMigrateDir wraps a migrate.LocalDir and provides a migrate.Scanner
+// implementation compatible with golang-migrate/migrate.
+type GolangMigrateDir struct{ *migrate.LocalDir }
+
+// NewGolandMigrateDir returns a new GolangMigrateDir.
+func NewGolandMigrateDir(path string) (*GolangMigrateDir, error) {
+	dir, err := migrate.NewLocalDir(path)
+	if err != nil {
+		return nil, err
+	}
+	return &GolangMigrateDir{dir}, nil
+}
+
+// Files implements Scanner.Files. It looks for all files with up.sql suffix and orders them by filename-
+func (d *GolangMigrateDir) Files() ([]migrate.File, error) {
+	names, err := fs.Glob(d, "*.up.sql")
+	if err != nil {
+		return nil, err
+	}
+	// Sort files lexicographically.
+	sort.Slice(names, func(i, j int) bool {
+		return names[i] < names[j]
+	})
+	ret := make([]migrate.File, len(names))
+	for i, n := range names {
+		b, err := fs.ReadFile(d, n)
+		if err != nil {
+			return nil, fmt.Errorf("sql/migrate: read file %q: %w", n, err)
+		}
+		ret[i] = migrate.NewLocalFile(n, b)
+	}
+	return ret, nil
+}
+
+// Desc implements Scanner.Desc.
+func (d *GolangMigrateDir) Desc(f migrate.File) (string, error) {
+	desc, err := d.LocalDir.Desc(f)
+	if err != nil {
+		return "", err
+	}
+	return strings.TrimSuffix(desc, ".up"), nil
+}
+
+// funcs contains the template.FuncMap for the different formatters.
+var funcs = template.FuncMap{
+	"inc": func(x int) int { return x + 1 },
+	// now formats the current time in a lexicographically ascending order while maintaining human readability.
+	"now": func() string { return time.Now().UTC().Format("20060102150405") },
+	"rev": reverse,
+}
+
 // templateFormatter parses the given templates and passes them on to the migrate.NewTemplateFormatter.
 func templateFormatter(templates ...string) migrate.Formatter {
 	tpls := make([]*template.Template, len(templates))


Test patch:
diff --git a/sql/sqltool/testdata/golang-migrate/1_initial.down.sql b/sql/sqltool/testdata/golang-migrate/1_initial.down.sql
new file mode 100644
index 00000000..1b52286d
--- /dev/null
+++ b/sql/sqltool/testdata/golang-migrate/1_initial.down.sql
@@ -0,0 +1,1 @@
+DROP TABLE tbl;
\ No newline at end of file
diff --git a/sql/sqltool/testdata/golang-migrate/1_initial.up.sql b/sql/sqltool/testdata/golang-migrate/1_initial.up.sql
new file mode 100644
index 00000000..08d0187d
--- /dev/null
+++ b/sql/sqltool/testdata/golang-migrate/1_initial.up.sql
@@ -0,0 +1,4 @@
+CREATE TABLE tbl
+(
+    col INT
+);
\ No newline at end of file
diff --git a/sql/sqltool/testdata/golang-migrate/2_second_migration.down.sql b/sql/sqltool/testdata/golang-migrate/2_second_migration.down.sql
new file mode 100644
index 00000000..670ad7e8
--- /dev/null
+++ b/sql/sqltool/testdata/golang-migrate/2_second_migration.down.sql
@@ -0,0 +1,1 @@
+DROP TABLE tbl_2;
\ No newline at end of file
diff --git a/sql/sqltool/testdata/golang-migrate/2_second_migration.up.sql b/sql/sqltool/testdata/golang-migrate/2_second_migration.up.sql
new file mode 100644
index 00000000..b398c9c9
--- /dev/null
+++ b/sql/sqltool/testdata/golang-migrate/2_second_migration.up.sql
@@ -0,0 +1,1 @@
+CREATE TABLE tbl_2 (col INT);
\ No newline at end of file
diff --git a/sql/sqltool/tool_test.go b/sql/sqltool/tool_test.go
index 4f5bfedb..add53bb6 100644
--- a/sql/sqltool/tool_test.go
+++ b/sql/sqltool/tool_test.go
@@ -112,6 +112,37 @@ CREATE TABLE t2(c int);
 	}
 }
 
+func TestScanners(t *testing.T) {
+	d, err := sqltool.NewGolandMigrateDir("testdata/golang-migrate")
+	require.NoError(t, err)
+
+	files, err := d.Files()
+	require.NoError(t, err)
+	require.Len(t, files, 2)
+	require.Equal(t, "1_initial.up.sql", files[0].Name())
+	require.Equal(t, "2_second_migration.up.sql", files[1].Name())
+
+	first, second := files[0], files[1]
+
+	stmts, err := d.Stmts(first)
+	require.NoError(t, err)
+	require.Equal(t, []string{"CREATE TABLE tbl\n(\n    col INT\n);"}, stmts)
+	stmts, err = d.Stmts(second)
+	require.NoError(t, err)
+	require.Equal(t, []string{"CREATE TABLE tbl_2 (col INT);"}, stmts)
+
+	v, err := d.Version(first)
+	require.NoError(t, err)
+	require.Equal(t, "1", v)
+
+	desc, err := d.Desc(first)
+	require.NoError(t, err)
+	require.Equal(t, "initial", desc)
+	desc, err = d.Desc(second)
+	require.NoError(t, err)
+	require.Equal(t, "second_migration", desc)
+}
+
 func dir(t *testing.T) migrate.Dir {
 	p := t.TempDir()
 	d, err := migrate.NewLocalDir(p)