Repository: ariga/atlas
Pull request #616 (2022-03-03 12:02:41)

sql/migrate: (BC) read from state from migration directory instead of an extra state reader
This is the first PR of several to add the `atlas migrate` commands.


Patch:
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 65e7d4bf..909b38ec 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -128,7 +128,7 @@ func Schema(s *schema.Schema) StateReader {
 }
 
 type (
-	// Dir describes the methods needed to work with the Planner.
+	// Dir describes the methods needed for a Planner to manage migration files.
 	Dir interface {
 		fs.FS
 
@@ -152,25 +152,49 @@ type (
 	// Planner can plan the steps to take to migrate from one state to another. It uses the enclosed FS to write
 	// those changes to versioned migration files.
 	Planner struct {
-		drv Driver
-		dir Dir
-		fmt Formatter
+		drv Driver      // driver to use
+		dir Dir         // where migration files are stored and read from
+		fmt Formatter   // how to format a plan to migration files
+		dsr StateReader // how to read a state from the migration directory
 	}
+
+	// PlannerOption allows managing a Planner using functional arguments.
+	PlannerOption func(*Planner)
 )
 
-// New creates a new Planner.
-func New(drv Driver, dir Dir, fmt Formatter) *Planner {
-	return &Planner{
-		drv: drv,
-		dir: dir,
-		fmt: fmt,
+// NewPlanner creates a new Planner.
+func NewPlanner(drv Driver, dir Dir, opts ...PlannerOption) *Planner {
+	p := &Planner{drv: drv, dir: dir}
+	for _, opt := range opts {
+		opt(p)
+	}
+	if p.fmt == nil {
+		p.fmt = DefaultFormatter
+	}
+	if p.dsr == nil {
+		p.dsr = GlobStateReader(p.dir, p.drv, "*.sql")
+	}
+	return p
+}
+
+// WithFormatter sets the Formatter of a Planner.
+func WithFormatter(fmt Formatter) PlannerOption {
+	return func(p *Planner) {
+		p.fmt = fmt
+	}
+}
+
+// WithStateReader sets the StateReader of a Planner.
+func WithStateReader(dsr StateReader) PlannerOption {
+	return func(p *Planner) {
+		p.dsr = dsr
 	}
 }
 
 // Plan calculates the migration Plan required for moving the current state (from) state to
 // the next state (to). A StateReader can be a directory, static schema elements or a Driver connection.
-func (p *Planner) Plan(ctx context.Context, name string, from StateReader, to StateReader) (*Plan, error) {
-	current, err := from.ReadState(ctx)
+func (p *Planner) Plan(ctx context.Context, name string, to StateReader) (*Plan, error) {
+	current, err := p.dsr.ReadState(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -214,7 +238,11 @@ type LocalDir struct {
 // NewLocalDir returns a new the Dir used by a Planner to work on the given local path.
 func NewLocalDir(path string) (*LocalDir, error) {
 	fi, err := os.Stat(path)
-	if err != nil {
+	if err == os.ErrNotExist {
+		if err := os.MkdirAll(path, 0755); err != nil {
+			return nil, err
+		}
+	} else if err != nil {
 		return nil, err
 	}
 	if !fi.IsDir() {
@@ -225,9 +253,9 @@ func NewLocalDir(path string) (*LocalDir, error) {
 
 // GlobStateReader creates a StateReader that loads all files from Dir matching
 // glob in lexicographic order and uses the Driver to create a migration state.
-func (d *LocalDir) GlobStateReader(drv Driver, glob string) StateReaderFunc {
+func GlobStateReader(dir Dir, drv Driver, glob string) StateReaderFunc {
 	return func(ctx context.Context) (*schema.Realm, error) {
-		names, err := fs.Glob(d, glob)
+		names, err := fs.Glob(dir, glob)
 		if err != nil {
 			return nil, err
 		}
@@ -236,7 +264,7 @@ func (d *LocalDir) GlobStateReader(drv Driver, glob string) StateReaderFunc {
 			return names[i] < names[j]
 		})
 		for _, n := range names {
-			f, err := d.Open(n)
+			f, err := dir.Open(n)
 			if err != nil {
 				return nil, err
 			}
@@ -263,6 +291,8 @@ func (d *LocalDir) WriteFile(name string, b []byte) error {
 	return os.WriteFile(filepath.Join(d.dir, name), b, 0600)
 }
 
+var _ Dir = (*LocalDir)(nil)
+
 var (
 	// templateFuncs contains the template.FuncMap for the DefaultFormatter.
 	templateFuncs = template.FuncMap{


Test patch:
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index b0004251..a15087bf 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -33,7 +33,7 @@ func TestPlanner_WritePlan(t *testing.T) {
 	}
 
 	// DefaultFormatter
-	pl := migrate.New(nil, mfs, migrate.DefaultFormatter)
+	pl := migrate.NewPlanner(nil, mfs)
 	require.NotNil(t, pl)
 	require.NoError(t, pl.WritePlan(plan))
 	v := strconv.FormatInt(time.Now().Unix(), 10)
@@ -53,7 +53,7 @@ func TestPlanner_WritePlan(t *testing.T) {
 		template.Must(template.New("").Parse("{{range .Changes}}{{println .Cmd}}{{end}}")),
 	)
 	require.NoError(t, err)
-	pl = migrate.New(nil, mfs, fmt)
+	pl = migrate.NewPlanner(nil, mfs, migrate.WithFormatter(fmt))
 	require.NotNil(t, pl)
 	require.NoError(t, pl.WritePlan(plan))
 	require.Len(t, mfs.files, 3)
@@ -71,8 +71,8 @@ func TestPlanner_Plan(t *testing.T) {
 	)
 
 	// nothing to do
-	pl := migrate.New(drv, mfs, migrate.DefaultFormatter)
-	plan, err := pl.Plan(ctx, "empty", migrate.Realm(nil), migrate.Realm(nil))
+	pl := migrate.NewPlanner(drv, mfs)
+	plan, err := pl.Plan(ctx, "empty", migrate.Realm(nil))
 	require.ErrorIs(t, err, migrate.ErrNoPlan)
 	require.Nil(t, plan)
 
@@ -87,7 +87,7 @@ func TestPlanner_Plan(t *testing.T) {
 			{Cmd: "CREATE TABLE t2(c int);"},
 		},
 	}
-	plan, err = pl.Plan(ctx, "", migrate.Realm(nil), migrate.Realm(nil))
+	plan, err = pl.Plan(ctx, "", migrate.Realm(nil))
 	require.NoError(t, err)
 	require.Equal(t, drv.plan, plan)
 }
@@ -100,11 +100,11 @@ func TestGlobStateReader(t *testing.T) {
 	localFS, err := migrate.NewLocalDir("testdata")
 	require.NoError(t, err)
 
-	_, err = localFS.GlobStateReader(drv, "*.up.sql").ReadState(ctx)
+	_, err = migrate.GlobStateReader(localFS, drv, "*.up.sql").ReadState(ctx)
 	require.NoError(t, err)
 	require.Equal(t, drv.executed, []string{"CREATE TABLE t(c int);"})
 
-	_, err = localFS.GlobStateReader(drv, "*.down.sql").ReadState(ctx)
+	_, err = migrate.GlobStateReader(localFS, drv, "*.down.sql").ReadState(ctx)
 	require.NoError(t, err)
 	require.Equal(t, drv.executed, []string{"CREATE TABLE t(c int);", "DROP TABLE IF EXISTS t;"})
 }