Repository: ariga/atlas
Pull request #736 (2022-04-26 10:10:31)

sql/migrate: default Scanner implementation

a8m: Where's the default implementation?
masseelch: > Where's the default implementation?

It somehow ended up in the PR before. I am sorry :-(

Patch:
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index c0c62bfb..2bcc8d6e 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -14,7 +14,6 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"sort"
@@ -312,6 +311,9 @@ func (p *Planner) WritePlan(plan *Plan) error {
 	return nil
 }
 
+// ErrNoPendingFiles is returned when there are no pending migration files to execute on the managed database.
+var ErrNoPendingFiles = errors.New("sql/migrate: execute: nothing to do")
+
 // NewExecutor creates a new Executor with default values. // TODO(masseelch): Operator Version and other Meta
 func NewExecutor(drv Driver, dir Dir, opts ...ExecutorOption) (*Executor, error) {
 	p := &Executor{drv: drv, dir: dir}
@@ -375,7 +377,7 @@ func (e *Executor) Execute(ctx context.Context, n int) error {
 		}
 	}
 	if len(migrations) == len(revisions) {
-		return fmt.Errorf("sql/migrate: execute: nothing to do")
+		return ErrNoPendingFiles
 	}
 	defer rrw.WriteRevisions(ctx, revisions) // TODO:(masseelch): handle error
 	// TODO(masseelch): run in a transaction
@@ -552,7 +554,7 @@ func (d *LocalDir) WriteFile(name string, b []byte) error {
 	return os.WriteFile(filepath.Join(d.dir, name), b, 0644)
 }
 
-// Files implements Scanner.Files.
+// Files implements Scanner.Files. It looks for all files with .sql suffix and orders them by filename-
 func (d *LocalDir) Files() ([]File, error) {
 	names, err := fs.Glob(d, "*.sql")
 	if err != nil {
@@ -563,12 +565,12 @@ func (d *LocalDir) Files() ([]File, error) {
 		return names[i] < names[j]
 	})
 	ret := make([]File, len(names))
-	for _, n := range names {
-		b, err := ioutil.ReadFile(n)
+	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 = append(ret, &LocalFile{bytes.NewBuffer(b), n})
+		ret[i] = &LocalFile{bytes.NewBuffer(b), n}
 	}
 	return ret, nil
 }
@@ -602,7 +604,7 @@ func (d *LocalDir) Desc(f File) (string, error) {
 	if len(split) == 1 {
 		return "", nil
 	}
-	return split[1], nil
+	return strings.TrimSuffix(split[1], ".sql"), nil
 }
 
 var (


Test patch:
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index 68c8c53b..f0da7862 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -188,6 +188,7 @@ func TestHash_UnmarshalText(t *testing.T) {
 	require.Equal(t, h, ac)
 }
 
+// Deprecated: GlobStateReader will be removed once the Executor is functional.
 func TestGlobStateReader(t *testing.T) {
 	var (
 		drv = &mockDriver{}
@@ -235,14 +236,17 @@ func TestGlobStateReader(t *testing.T) {
 }
 
 func TestLocalDir(t *testing.T) {
+	// Files don't work.
 	d, err := migrate.NewLocalDir("migrate.go")
 	require.ErrorContains(t, err, "sql/migrate: \"migrate.go\" is not a dir")
 	require.Nil(t, d)
 
+	// Does not create a dir for you.
 	d, err = migrate.NewLocalDir("foo/bar")
 	require.EqualError(t, err, "sql/migrate: stat foo/bar: no such file or directory")
 	require.Nil(t, d)
 
+	// Open and WriteFile work.
 	d, err = migrate.NewLocalDir(t.TempDir())
 	require.NoError(t, err)
 	require.NotNil(t, d)
@@ -255,6 +259,37 @@ func TestLocalDir(t *testing.T) {
 	c, err := io.ReadAll(f)
 	require.NoError(t, err)
 	require.Equal(t, "content", string(c))
+
+	// Default Scanner implementation.
+	d, err = migrate.NewLocalDir("testdata/sub")
+	require.NoError(t, err)
+	require.NotNil(t, d)
+
+	files, err := d.Files()
+	require.NoError(t, err)
+	require.Len(t, files, 2)
+	require.Equal(t, "1.a_sub.up.sql", files[0].Name())
+	require.Equal(t, "2.10.x-20_description.sql", files[1].Name())
+
+	stmts, err := d.Stmts(files[0])
+	require.NoError(t, err)
+	require.Equal(t, []string{"CREATE TABLE t_sub(c int);", "ALTER TABLE t_sub ADD c1 int;"}, stmts)
+	v, err := d.Version(files[0])
+	require.NoError(t, err)
+	require.Equal(t, "1.a", v)
+	desc, err := d.Desc(files[0])
+	require.NoError(t, err)
+	require.Equal(t, "sub.up", desc)
+
+	stmts, err = d.Stmts(files[1])
+	require.NoError(t, err)
+	require.Equal(t, []string{"ALTER TABLE t_sub ADD c2 int;"}, stmts)
+	v, err = d.Version(files[1])
+	require.NoError(t, err)
+	require.Equal(t, "2.10.x-20", v)
+	desc, err = d.Desc(files[1])
+	require.NoError(t, err)
+	require.Equal(t, "description", desc)
 }
 
 type (
diff --git a/sql/migrate/testdata/atlas.sum b/sql/migrate/testdata/atlas.sum
index 680b7129..58bb0b1b 100644
--- a/sql/migrate/testdata/atlas.sum
+++ b/sql/migrate/testdata/atlas.sum
@@ -1,4 +1,5 @@
-h1:DCXCNGIZS22tT+MwN4xlx80g3Pz2ePVWIu5YqR/Qiuc=
+h1:0cmvdmEYFYPQVtN6KDezcRIIMLwpSz11Lm7FeHv1xGg=
 1_initial.down.sql h1:0zypK43rgPbgvVUgVJABGN25VgM1QSeU+LJDBb8cEQI=
 1_initial.up.sql h1:hFhs5XhRml4KTWGF5td6h1s7xNqAFnaEBbC5Y/NF7i4=
-sub/1.a_sub.up.sql h1:37EywUcuf8yZJsQiQFn6I1NXRaqtSnCTB4svZAg0OQ8=
+sub/1.a_sub.up.sql h1:GomhYquMSavc1O/WeukxL02orVIHO2EUK6pWsedwELc=
+sub/2.10.x-20_description.sql h1:9HvZYZZMi9S7eLQvNAR9W9BxaMRvFz0A9TUmpdEHt4o=
diff --git a/sql/migrate/testdata/sub/1.a_sub.up.sql b/sql/migrate/testdata/sub/1.a_sub.up.sql
index 07c223a8..a8884fc4 100644
--- a/sql/migrate/testdata/sub/1.a_sub.up.sql
+++ b/sql/migrate/testdata/sub/1.a_sub.up.sql
@@ -1,1 +1,4 @@
-CREATE TABLE t_sub(c int);
\ No newline at end of file
+-- create table "t_sub"
+CREATE TABLE t_sub(c int);
+-- add c1 column
+ALTER TABLE t_sub ADD c1 int;
\ No newline at end of file
diff --git a/sql/migrate/testdata/sub/2.10.x-20_description.sql b/sql/migrate/testdata/sub/2.10.x-20_description.sql
new file mode 100644
index 00000000..d440646d
--- /dev/null
+++ b/sql/migrate/testdata/sub/2.10.x-20_description.sql
@@ -0,0 +1,2 @@
+-- add c2 column
+ALTER TABLE t_sub ADD c2 int;
\ No newline at end of file