Repository: ariga/atlas
Pull request #984 (2022-07-25 16:52:05)

sql/migrate: merge Dir and Scanner to one interface
The next steps are supporting `Baseline` file(s), `Tags`, and `flyway` directory. 


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1cab0300..24d0c47e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,8 +38,6 @@ jobs:
       - name: go generate cmd/atlas
         working-directory: cmd/atlas
         run: go generate ./...
-        env:
-          GOWORK: off
       - name: Verify generated files are checked in properly
         run: |
           status=$(git status --porcelain | grep -v "go.\(sum\|mod\)" | cat)
diff --git a/cmd/atlas/generate.go b/cmd/atlas/generate.go
index 92a6aacd..1bc8fe06 100644
--- a/cmd/atlas/generate.go
+++ b/cmd/atlas/generate.go
@@ -4,4 +4,4 @@
 
 package main
 
-//go:generate go run -mod=mod doc.go
+//go:generate go run doc.go
diff --git a/cmd/atlas/go.mod b/cmd/atlas/go.mod
index 51556823..673d0b01 100644
--- a/cmd/atlas/go.mod
+++ b/cmd/atlas/go.mod
@@ -34,7 +34,5 @@ require (
 	github.com/zclconf/go-cty v1.8.0 // indirect
 	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
 	golang.org/x/text v0.3.7 // indirect
-	golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 // indirect
-	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/cmd/atlas/go.sum b/cmd/atlas/go.sum
index b2bbc61b..92d75049 100644
--- a/cmd/atlas/go.sum
+++ b/cmd/atlas/go.sum
@@ -1,5 +1,3 @@
-ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7 h1:SSmcPvU2K/1U2D5S12xIbriQ5FUj88cuW80H4aGNuOQ=
-ariga.io/atlas v0.5.1-0.20220714090227-c1fa4872ecf7/go.mod h1:ofVetkJqlaWle3mvYmaS2uyFGFcc7dSq436tmxa/Mzk=
 ariga.io/atlas v0.5.1-0.20220714144633-63ec7fc8c49a h1:pi8bppDVEpHRRqUiRoBvbzFV0wirfNrSQxQp9Q6AHTU=
 ariga.io/atlas v0.5.1-0.20220714144633-63ec7fc8c49a/go.mod h1:ofVetkJqlaWle3mvYmaS2uyFGFcc7dSq436tmxa/Mzk=
 entgo.io/ent v0.11.1 h1:im67R+2W3Nee2bNS2YnoYz8oAF0Qz4AOlIvKRIAEISY=
@@ -95,11 +93,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7 h1:M1gcVrIb2lSn2FIL19DG0+/b8nNVKJ7W7b4WcAGZAYM=
-golang.org/x/tools v0.1.9-0.20211216111533-8d383106f7e7/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
diff --git a/cmd/atlas/internal/ci/ci.go b/cmd/atlas/internal/ci/ci.go
index 3399a102..bcb3480b 100644
--- a/cmd/atlas/internal/ci/ci.go
+++ b/cmd/atlas/internal/ci/ci.go
@@ -32,21 +32,15 @@ type (
 		// LoadChanges converts each of the given migration files into one Changes.
 		LoadChanges(context.Context, []migrate.File) ([]*sqlcheck.File, error)
 	}
-
-	// DirScanner stitches migrate.Dir and migrate.Scanner into one interface.
-	DirScanner interface {
-		migrate.Dir
-		migrate.Scanner
-	}
 )
 
 type (
 	// GitChangeDetector implements the ChangeDetector interface by utilizing a git repository.
 	GitChangeDetector struct {
-		work string     // path to the git working directory (i.e. -C)
-		base string     // name of the base branch (e.g. master)
-		path string     // path of the migration directory relative to the repository root (in slash notation)
-		dir  DirScanner // the migration directory to load migration files from
+		work string      // path to the git working directory (i.e. -C)
+		base string      // name of the base branch (e.g. master)
+		path string      // path of the migration directory relative to the repository root (in slash notation)
+		dir  migrate.Dir // the migration directory to load migration files from
 	}
 
 	// GitChangeDetectorOption allows configuring GitChangeDetector with functional arguments.
@@ -54,7 +48,7 @@ type (
 )
 
 // NewGitChangeDetector configures a new GitChangeDetector.
-func NewGitChangeDetector(dir DirScanner, opts ...GitChangeDetectorOption) (*GitChangeDetector, error) {
+func NewGitChangeDetector(dir migrate.Dir, opts ...GitChangeDetectorOption) (*GitChangeDetector, error) {
 	if dir == nil {
 		return nil, errors.New("internal/ci: dir cannot be nil")
 	}
@@ -136,14 +130,14 @@ var _ ChangeDetector = (*GitChangeDetector)(nil)
 
 // latestChange implements the ChangeDetector by selecting the latest N files.
 type latestChange struct {
-	n   int        // number of (latest) files considered new.
-	dir DirScanner // migration directory to load migration files from.
+	n   int         // number of (latest) files considered new.
+	dir migrate.Dir // migration directory to load migration files from.
 }
 
 // LatestChanges implements the ChangeDetector interface by selecting the latest N files as new.
 // It is useful for executing analysis on files in development before they are committed or on
 // all files in a directory.
-func LatestChanges(dir DirScanner, n int) ChangeDetector {
+func LatestChanges(dir migrate.Dir, n int) ChangeDetector {
 	return &latestChange{n: n, dir: dir}
 }
 
@@ -165,8 +159,6 @@ func (d *latestChange) DetectChanges(context.Context) ([]migrate.File, []migrate
 type DevLoader struct {
 	// Dev environment used as a sandbox instantiated to the starting point (e.g. base branch).
 	Dev *sqlclient.Client
-	// Scan is used for scanning the migration directory.
-	Scan migrate.Scanner
 }
 
 // LoadChanges implements the ChangesLoader interface.
@@ -201,7 +193,7 @@ func (d *DevLoader) LoadChanges(ctx context.Context, base, files []migrate.File)
 	}()
 	// Bring the dev environment to the base point.
 	for _, f := range base {
-		stmt, err := d.Scan.Stmts(f)
+		stmt, err := f.Stmts()
 		if err != nil {
 			return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
 		}
@@ -220,7 +212,7 @@ func (d *DevLoader) LoadChanges(ctx context.Context, base, files []migrate.File)
 		diff[i] = &sqlcheck.File{
 			File: f,
 		}
-		stmts, err := d.Scan.Stmts(f)
+		stmts, err := f.Stmts()
 		if err != nil {
 			return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
 		}
diff --git a/cmd/atlas/internal/ci/run.go b/cmd/atlas/internal/ci/run.go
index f3a41a73..aacfc712 100644
--- a/cmd/atlas/internal/ci/run.go
+++ b/cmd/atlas/internal/ci/run.go
@@ -28,7 +28,7 @@ type Runner struct {
 	ChangeDetector ChangeDetector
 
 	// Dir is used for scanning and validating the migration directory.
-	Dir DirScanner
+	Dir migrate.Dir
 
 	// Analyzer defines the analysis to be run in the CI job.
 	Analyzer sqlcheck.Analyzer
@@ -61,7 +61,7 @@ func (r *Runner) summary(ctx context.Context) (*SummaryReport, error) {
 	if err != nil {
 		return nil, err
 	}
-	l := &DevLoader{Dev: r.Dev, Scan: r.Dir}
+	l := &DevLoader{Dev: r.Dev}
 	files, err := l.LoadChanges(ctx, base, feat)
 	if err != nil {
 		return nil, err
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index a7b9ba0c..80eedb25 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -330,10 +330,7 @@ func CmdMigrateApplyRun(cmd *cobra.Command, args []string) error {
 	if err != nil {
 		return err
 	}
-	if err := migrate.LogIntro(struct {
-		migrate.Logger
-		migrate.Scanner
-	}{l, dir.(migrate.Scanner)}, revs, pending); err != nil {
+	if err := migrate.LogIntro(l, revs, pending); err != nil {
 		return err
 	}
 	var (
@@ -464,8 +461,7 @@ func CmdMigrateStatusRun(cmd *cobra.Command, _ []string) error {
 	if err != nil {
 		return err
 	}
-	sc := dir.(migrate.Scanner) // all supported migration directories implement the migrate.Scanner
-	avail, err := sc.Files()
+	avail, err := dir.Files()
 	if err != nil {
 		return err
 	}
@@ -479,7 +475,7 @@ func CmdMigrateStatusRun(cmd *cobra.Command, _ []string) error {
 		if err != nil {
 			return err
 		}
-		return statusPrint(cmd.OutOrStdout(), sc, avail, avail, nil)
+		return statusPrint(cmd.OutOrStdout(), avail, avail, nil)
 	}
 	// Currently, only in DB revisions are supported.
 	opts := []entmigrate.Option{entmigrate.WithSchema(MigrateFlags.RevisionSchema)}
@@ -503,10 +499,10 @@ func CmdMigrateStatusRun(cmd *cobra.Command, _ []string) error {
 	if err != nil {
 		return err
 	}
-	return statusPrint(cmd.OutOrStdout(), sc, avail, pending, revs)
+	return statusPrint(cmd.OutOrStdout(), avail, pending, revs)
 }
 
-func statusPrint(out io.Writer, sc migrate.Scanner, avail, pending []migrate.File, revs migrate.Revisions) (err error) {
+func statusPrint(out io.Writer, avail, pending []migrate.File, revs migrate.Revisions) (err error) {
 	var (
 		cur, next, state string
 		applied          = avail[: len(avail)-len(pending) : len(avail)-len(pending)]
@@ -516,17 +512,9 @@ func statusPrint(out io.Writer, sc migrate.Scanner, avail, pending []migrate.Fil
 	case len(avail):
 		cur = "No version applied yet"
 	case 0:
-		cur, err = sc.Version(avail[len(avail)-1])
-		if err != nil {
-			return err
-		}
-		cur = cyan(cur)
+		cur = cyan(avail[len(avail)-1].Version())
 	default:
-		cur, err = sc.Version(avail[len(avail)-len(pending)])
-		if err != nil {
-			return err
-		}
-		cur = cyan(cur)
+		cur = cyan(avail[len(avail)-len(pending)].Version())
 		// If the last pending version is partially applied, tell so.
 		if partial {
 			cur += fmt.Sprintf(" (%d statements applied)", revs[len(revs)-1].Applied)
@@ -537,11 +525,7 @@ func statusPrint(out io.Writer, sc migrate.Scanner, avail, pending []migrate.Fil
 		next = "Already at latest version"
 	} else {
 		state = yellow("PENDING")
-		next, err = sc.Version(pending[0])
-		if err != nil {
-			return err
-		}
-		next = cyan(next)
+		next = cyan(pending[0].Version())
 		if partial {
 			next += fmt.Sprintf(" (%d statements left)", revs[len(revs)-1].Total-revs[len(revs)-1].Applied)
 		}
diff --git a/doc/md/concepts/workflows.md b/doc/md/concepts/workflows.md
new file mode 100644
index 00000000..7899f86e
--- /dev/null
+++ b/doc/md/concepts/workflows.md
@@ -0,0 +1,146 @@
+---
+title: Declarative vs Versioned Workflows
+id: workflows
+slug: /concepts/declarative-vs-versioned
+---
+
+This section introduces two types of workflows that are supported by Atlas
+to manage database schemas: _declarative_ and _versioned_ migrations. 
+
+### Declarative Migrations
+
+The declarative approach has become increasingly popular with engineers nowadays because it embodies
+a convenient separation of concerns between application and infrastructure engineers.
+Application engineers describe _what_ (the desired state) they need to happen, and
+infrastructure engineers build tools that plan and execute ways to get to that state (_how_).
+This division of labor allows for great efficiencies as it abstracts away the complicated
+inner workings of infrastructure behind a simple, easy to understand API for the application
+developers and allows for specialization and development of expertise to pay off for the
+infra people.
+
+With declarative migrations, the desired state of the database schema is given 
+as input to the migration engine, which plans and executes a set of actions to
+change the database to its desired state.
+
+For example, suppose your application uses a small SQLite database to store its data.
+In this database, you have a `users` table with this structure:
+```hcl
+schema "main" {}
+
+table "users" {
+  schema = schema.main
+  column "id" {
+    type = int
+  }
+  column "greeting" {
+    type = text
+  }
+}
+```
+Now, suppose that you want to add a default value of `"shalom"` to the `greeting`
+column. Many developers are not aware that it isn't possible to modify a column's
+default value in an existing table in SQLite. Instead, the common practice is to
+create a new table, copy the existing rows into the new table and drop the old one
+after. Using the declarative approach, developers can change the default value for
+the `greeting` column:
+
+```hcl {10}
+schema "main" {}
+
+table "users" {
+  schema = schema.main
+  column "id" {
+    type = int
+  }
+  column "greeting" {
+    type = text
+    default = "shalom"
+  }
+}
+```
+And have Atlas's engine devise a plan similar to this:
+```sql
+-- Planned Changes:
+-- Create "new_users" table
+CREATE TABLE `new_users` (`id` int NOT NULL, `greeting` text NOT NULL DEFAULT 'shalom')
+-- Copy rows from old table "users" to new temporary table "new_users"
+INSERT INTO `new_users` (`id`, `greeting`) SELECT `id`, IFNULL(`greeting`, 'shalom') AS `greeting` FROM `users`
+-- Drop "users" table after copying rows
+DROP TABLE `users`
+-- Rename temporary table "new_users" to "users"
+ALTER TABLE `new_users` RENAME TO `users`
+```
+
+### Versioned Migrations
+
+As the database is one of the most critical components in any system, applying changes
+to its schema is rightfully considered a dangerous operation. For this reason, many teams
+prefer a more imperative approach where each change to the database schema is checked-in 
+to source control and reviewed during code-review.  Each such change
+is called a "migration", as it migrates the database schema from the previous version to 
+the next. To support this kind of requirement, many popular database schema management
+tools such as [Flyway](https://flywaydb.org/), [Liquibase](https://liquibase.org/) or 
+[golang-migrate](https://github.com/golang-migrate/migrate) support a workflow that
+is commonly called "versioned migrations".
+
+In addition to the higher level of control which is provided by versioned migrations,
+applications are often deployed to multiple remote environments at once. These environments,
+are not controlled (or even accessible) by the development team. In such cases, declarative migrations, 
+which rely on a network connection to the target database and on human
+approval of migrations plans in real-time, are not a feasible strategy.
+
+With versioned migrations (sometimes called "change-based migrations") instead of describing 
+the desired state ("what the database should look like"), developers describe the changes themselves 
+("how to reach the state"). Most of the time, this is done by creating a set of SQL files 
+containing the statements needed. Each of the files is assigned a unique version and a
+description of the changes. Tools like the ones mentioned earlier are then able to 
+interpret the migration files and to apply (some of) them in the correct order to 
+transition to the desired database structure.
+
+The benefit of the versioned migrations approach is that it is explicit: engineers
+know _exactly_ what queries are going to be run against the database when the time
+comes to execute them.  Because changes are planned ahead of time, migration authors
+can control precisely how to reach the desired schema.  If we consider a migration as 
+a plan to get from state A to state B, oftentimes multiple paths exist, each with a
+very different impact on the database. To demonstrate, consider an initial state which
+contains a table with two columns:
+```sql
+CREATE TABLE users (
+    id int,
+    name varchar(255)
+);
+```
+Suppose our desired state is:
+```sql
+CREATE TABLE users (
+    id int,
+    user_name varchar(255)
+);
+```
+There are at least two ways get from the initial to the desired state:
+* Drop the `name` column and create a new `user_name` column.
+* Alter the name of the `name` column to `user_name`.
+
+Depending on the context, either may be the desired outcome for the developer
+planning the change. With versioned migrations, engineers have the ultimate confidence
+of what change is going to happen which may not be known ahead of time in a _declarative_
+approach.
+
+### Migration Authoring
+
+The downside of the _versioned migration_ approach is, of course, that it puts the 
+burden of planning the migration on developers. This requires a certain level 
+of expertise that is not always available to every engineer, as we demonstrated
+in our example of setting a default value in a SQLite database above.
+
+As part of the Atlas project we advocate for a third combined approach that we call 
+"Versioned Migration Authoring". Versioned Migration Authoring is an attempt to combine
+the simplicity and expressiveness of the declarative approach with the control and 
+explicitness of versioned migrations. 
+
+With versioned migration authoring, users still declare their desired state and use
+the Atlas engine to plan a safe migration from the existing to the new state. 
+However, instead of coupling planning and execution, plans are instead written 
+into normal migration files which can be checked into source control, fine-tuned manually and 
+reviewed in regular code review processes.
+
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index aa6c7f0a..aaf911d7 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -37,6 +37,7 @@ module.exports = {
             label: 'Concepts',
             collapsed: false,
             items: [
+                {type: 'doc', id: 'concepts/workflows', label: 'Declarative vs Versioned'},
                 {type: 'doc', id: 'concepts/concepts-url', label: 'URLs'},
                 {type: 'doc', id: 'concepts/dev-database', label: 'Dev Database'},
                 {type: 'doc', id: 'concepts/ddl', label: 'Data Definition Language'},
diff --git a/go.sum b/go.sum
index 669e9ec5..e721ca2f 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,7 @@ github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
 github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
 github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
+github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
 github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
 github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
@@ -50,13 +51,16 @@ github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUA
 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -66,6 +70,9 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/ci/ci.tmpl b/internal/ci/ci.tmpl
index ed7d11d5..32bdb3b2 100644
--- a/internal/ci/ci.tmpl
+++ b/internal/ci/ci.tmpl
@@ -38,8 +38,6 @@ jobs:
       - name: go generate cmd/atlas
         working-directory: cmd/atlas
         run: go generate ./...
-        env:
-          GOWORK: off
       - name: Verify generated files are checked in properly
         run: |
           status=$(git status --porcelain | grep -v "go.\(sum\|mod\)" | cat)
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index bf7e7abd..8d28e98c 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -140,11 +140,14 @@ func Conn(drv Driver, opts *schema.InspectRealmOption) StateReader {
 }
 
 type (
-	// Dir describes the methods needed for a Planner to manage migration files.
+	// Dir wraps the functionality used to interact with a migration directory.
 	Dir interface {
 		fs.FS
 		// WriteFile writes the data to the named file.
 		WriteFile(string, []byte) error
+
+		// Files returns a set of files stored in this Dir to be executed on a database.
+		Files() ([]File, error)
 	}
 
 	// Formatter wraps the Format method.
@@ -157,20 +160,14 @@ type (
 	File interface {
 		// Name returns the name of the migration file.
 		Name() string
+		// Desc returns the description of the migration File.
+		Desc() string
+		// Version returns the version of the migration File.
+		Version() string
 		// Bytes returns the read content of the file.
 		Bytes() []byte
-	}
-
-	// Scanner wraps several methods to interpret a migration Dir.
-	Scanner interface {
-		// Files returns a set of files from the given Dir to be executed on a database.
-		Files() ([]File, error)
-		// Stmts returns a set of SQL statements from the given File to be executed on a database.
-		Stmts(File) ([]string, error)
-		// Version returns the version of the migration File.
-		Version(File) (string, error)
-		// Desc returns the description of the migration File.
-		Desc(File) (string, error)
+		// Stmts returns the set of SQL statements this file holds.
+		Stmts() ([]string, error)
 	}
 
 	// A RevisionReadWriter reads and writes information about a Revisions to a persistent storage.
@@ -193,7 +190,6 @@ type (
 		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
-		sc  Scanner   // how to interpret a migration dir
 		sum bool      // whether to create a sum file for the migration directory
 	}
 
@@ -335,8 +331,6 @@ var (
 	ErrSnapshotUnsupported = errors.New("sql/migrate: driver does not support taking a database snapshot")
 	// ErrNotExist is returned if the requested revision is not found in the storage.
 	ErrNotExist = errors.New("sql/migrate: revision not found")
-	// ErrScanUnsupported is returned if there is no Scanner given.
-	ErrScanUnsupported = errors.New("sql/migrate: revision not found")
 )
 
 // NewExecutor creates a new Executor with default values. // TODO(masseelch): Operator Version and other Meta
@@ -365,9 +359,6 @@ func NewExecutor(drv Driver, dir Dir, rrw RevisionReadWriter, opts ...ExecutorOp
 	if _, ok := drv.(Snapshoter); !ok {
 		return nil, ErrSnapshotUnsupported
 	}
-	if _, ok := dir.(Scanner); !ok {
-		return nil, ErrScanUnsupported
-	}
 	return ex, nil
 }
 
@@ -401,7 +392,7 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 		return nil, fmt.Errorf("sql/migrate: execute: read revisions: %w", err)
 	}
 	// Select the correct migration files.
-	migrations, err := e.dir.(Scanner).Files()
+	migrations, err := e.dir.Files()
 	if err != nil {
 		return nil, fmt.Errorf("sql/migrate: execute: select migration files: %w", err)
 	}
@@ -420,21 +411,13 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 	}
 	for i := range revs {
 		m := migrations[i]
-		v, err := e.dir.(Scanner).Version(m)
-		if err != nil {
-			return nil, fmt.Errorf("sql/migrate: execute: scan version from %q: %w", m.Name(), err)
-		}
-		d, err := e.dir.(Scanner).Desc(m)
-		if err != nil {
-			return nil, fmt.Errorf("sql/migrate: execute: scan description from %q: %w", m.Name(), err)
-		}
 		r := revs[i]
 		// All fully applied revisions must match both version and description.
 		// In case of a not fully applied migration, the hash might change since the file could have been edited to fix
 		// a previous failure. Thus, hashes don't have to match in that case.
 		hash := (i < len(revs)-1 && hf[i].H != r.Hash) || (i == len(revs)-1 && r.Complete() == nil && hf[i].H != r.Hash)
-		if v != r.Version || d != r.Description || hash {
-			return nil, fmt.Errorf("sql/migrate: execute: revisions and migrations mismatch: rev %q <> file %q", v, r.Version)
+		if m.Version() != r.Version || m.Desc() != r.Description || hash {
+			return nil, fmt.Errorf("sql/migrate: execute: revisions and migrations mismatch: rev %q <> file %q", r.Version, m.Version())
 		}
 	}
 	// If there are no revisions yet, return all migration files as pending.
@@ -459,19 +442,11 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: compute hash: %w", err)
 	}
-	version, err := e.dir.(Scanner).Version(m)
-	if err != nil {
-		return fmt.Errorf("sql/migrate: execute: scan version from %q: %w", m.Name(), err)
-	}
-	desc, err := e.dir.(Scanner).Desc(m)
-	if err != nil {
-		return fmt.Errorf("sql/migrate: execute: scan description from %q: %w", m.Name(), err)
-	}
 	hash, err := hf.sumByName(m.Name())
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: scanning checksum from %q: %w", m.Name(), err)
 	}
-	stmts, err := e.dir.(Scanner).Stmts(m)
+	stmts, err := m.Stmts()
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: scanning statements from %q: %w", m.Name(), err)
 	}
@@ -486,6 +461,7 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 		}
 		sums[i] = base64.StdEncoding.EncodeToString(h.Sum(nil))
 	}
+	version := m.Version()
 	// If there already is a revision with this version in the database,
 	// and it is partially applied, continue where the last attempt was left off.
 	r, err := e.rrw.ReadRevision(ctx, version)
@@ -496,7 +472,7 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 		// Haven't seen this file before, create a new revision.
 		r = &Revision{
 			Version:     version,
-			Description: desc,
+			Description: m.Desc(),
 			Total:       len(stmts),
 			ExecutedAt:  time.Now(),
 			Hash:        hash,
@@ -560,12 +536,16 @@ func (e HistoryChangedError) Error() string {
 
 // ExecuteN executes n pending migration files. If n<=0 all pending migration files are executed. It will not attempt
 // an execution if the database is not "clean" (has only successfully applied migrations).
-func (e *Executor) ExecuteN(ctx context.Context, n int) error {
+func (e *Executor) ExecuteN(ctx context.Context, n int) (err error) {
 	unlock, err := e.Lock(ctx)
 	if err != nil {
 		return err
 	}
-	defer unlock()
+	defer func() {
+		if err2 := unlock(); err2 != nil {
+			err = wrap(err2, err)
+		}
+	}()
 	pending, err := e.Pending(ctx)
 	if err != nil {
 		return err
@@ -580,10 +560,7 @@ func (e *Executor) ExecuteN(ctx context.Context, n int) error {
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: read revisions: %w", err)
 	}
-	if err := LogIntro(struct {
-		Logger
-		Scanner
-	}{e.log, e.dir.(Scanner)}, revs, pending); err != nil {
+	if err := LogIntro(e.log, revs, pending); err != nil {
 		return err
 	}
 	for _, m := range pending {
@@ -603,7 +580,11 @@ func (e *Executor) ReadState(ctx context.Context) (realm *schema.Realm, err erro
 	if err != nil {
 		return nil, fmt.Errorf("sql/migrate: acquiring database lock: %w", err)
 	}
-	defer unlock()
+	defer func() {
+		if err2 := unlock(); err2 != nil {
+			err = wrap(err2, err)
+		}
+	}()
 	// Clean up after ourselves.
 	restore, err := e.drv.(Snapshoter).Snapshot(ctx)
 	if err != nil {
@@ -751,25 +732,15 @@ func (LogError) logEntry()     {}
 // Log implements the Logger interface.
 func (NopLogger) Log(LogEntry) {}
 
-// LogScanner stitches Logger and Scanner into one interface.
-type LogScanner interface {
-	Logger
-	Scanner
-}
-
 // LogIntro gathers some meta information from the migration files and stored revisions to
 // log some general information prior to actual execution.
-func LogIntro(l LogScanner, revs Revisions, files []File) error {
+func LogIntro(l Logger, revs Revisions, files []File) error {
 	names := make([]string, len(files))
 	for i := range files {
 		names[i] = files[i].Name()
 	}
 	last := files[len(files)-1]
-	v, err := l.(Scanner).Version(last)
-	if err != nil {
-		return fmt.Errorf("sql/migrate: execute: scan version from %q: %w", last.Name(), err)
-	}
-	e := LogExecution{To: v, Files: names}
+	e := LogExecution{To: last.Version(), Files: names}
 	if len(revs) > 0 {
 		e.From = revs[len(revs)-1].Version
 	}
@@ -783,6 +754,8 @@ type LocalDir struct {
 	path string
 }
 
+var _ Dir = (*LocalDir)(nil)
+
 // 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)
@@ -826,44 +799,22 @@ func (d *LocalDir) Files() ([]File, error) {
 		if err != nil {
 			return nil, fmt.Errorf("sql/migrate: read file %q: %w", n, err)
 		}
-		ret[i] = NewLocalFile(n, b)
+		ret[i] = &LocalFile{n: n, b: b}
 	}
 	return ret, nil
 }
 
-// Stmts implements Scanner.Stmts. It reads migration file line-by-line and expects a statement to be one line only.
-func (d *LocalDir) Stmts(f File) ([]string, error) {
-	return stmts(string(f.Bytes()))
-}
-
-// Version implements Scanner.Version.
-func (d *LocalDir) Version(f File) (string, error) {
-	return strings.SplitN(f.Name(), "_", 2)[0], nil
-}
-
-// Desc implements Scanner.Desc.
-func (d *LocalDir) Desc(f File) (string, error) {
-	split := strings.SplitN(f.Name(), "_", 2)
-	if len(split) == 1 {
-		return "", nil
-	}
-	return strings.TrimSuffix(split[1], ".sql"), nil
-}
-
-var _ interface {
-	Dir
-	Scanner
-} = (*LocalDir)(nil)
-
 // LocalFile is used by LocalDir to implement the Scanner interface.
 type LocalFile struct {
 	n string
 	b []byte
 }
 
+var _ File = (*LocalFile)(nil)
+
 // NewLocalFile returns a new local file.
-func NewLocalFile(path string, data []byte) *LocalFile {
-	return &LocalFile{path, data}
+func NewLocalFile(name string, data []byte) *LocalFile {
+	return &LocalFile{n: name, b: data}
 }
 
 // Name implements File.Name.
@@ -871,13 +822,30 @@ func (f LocalFile) Name() string {
 	return f.n
 }
 
+// Desc implements File.Desc.
+func (f LocalFile) Desc() string {
+	parts := strings.SplitN(f.n, "_", 2)
+	if len(parts) == 1 {
+		return ""
+	}
+	return strings.TrimSuffix(parts[1], ".sql")
+}
+
+// Version implements File.Version.
+func (f LocalFile) Version() string {
+	return strings.SplitN(f.n, "_", 2)[0]
+}
+
+// Stmts implements Scanner.Stmts. It reads migration file line-by-line and expects a statement to be one line only.
+func (f LocalFile) Stmts() ([]string, error) {
+	return stmts(string(f.b))
+}
+
 // Bytes returns local file data.
 func (f LocalFile) Bytes() []byte {
 	return f.b
 }
 
-var _ File = (*LocalFile)(nil)
-
 var (
 	// templateFuncs contains the template.FuncMap for the DefaultFormatter.
 	templateFuncs = template.FuncMap{"now": func() string { return time.Now().UTC().Format("20060102150405") }}
@@ -923,29 +891,21 @@ func NewTemplateFormatter(templates ...*template.Template) (*TemplateFormatter,
 func (t *TemplateFormatter) Format(plan *Plan) ([]File, error) {
 	files := make([]File, 0, len(t.templates))
 	for _, tpl := range t.templates {
-		var n, c bytes.Buffer
+		var n, b bytes.Buffer
 		if err := tpl.N.Execute(&n, plan); err != nil {
 			return nil, err
 		}
-		if err := tpl.C.Execute(&c, plan); err != nil {
+		if err := tpl.C.Execute(&b, plan); err != nil {
 			return nil, err
 		}
-		files = append(files, &templateFile{
-			Buffer: &c,
-			n:      n.String(),
+		files = append(files, &LocalFile{
+			n: n.String(),
+			b: b.Bytes(),
 		})
 	}
 	return files, nil
 }
 
-type templateFile struct {
-	*bytes.Buffer
-	n string
-}
-
-// Name implements the File interface.
-func (f *templateFile) Name() string { return f.n }
-
 const (
 	// HashFileName of the migration directory integrity sum file.
 	HashFileName = "atlas.sum"
diff --git a/sql/sqltool/tool.go b/sql/sqltool/tool.go
index 5e6fa02c..95c06abe 100644
--- a/sql/sqltool/tool.go
+++ b/sql/sqltool/tool.go
@@ -61,7 +61,7 @@ var (
 	)
 )
 
-// GolangMigrateDir wraps a migrate.LocalDir and provides a migrate.Scanner
+// GolangMigrateDir wraps migrate.LocalDir and provides
 // implementation compatible with golang-migrate/migrate.
 type GolangMigrateDir struct{ *migrate.LocalDir }
 
@@ -90,18 +90,20 @@ func (d *GolangMigrateDir) Files() ([]migrate.File, error) {
 		if err != nil {
 			return nil, fmt.Errorf("sql/migrate: read file %q: %w", n, err)
 		}
-		ret[i] = migrate.NewLocalFile(n, b)
+		ret[i] = &GolangMigrateFile{LocalFile: 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
+// GolangMigrateFile wraps migrate.LocalFile
+// with custom description function.
+type GolangMigrateFile struct {
+	*migrate.LocalFile
+}
+
+// Desc implements File.Desc.
+func (f *GolangMigrateFile) Desc() string {
+	return strings.TrimSuffix(f.LocalFile.Desc(), ".up")
 }
 
 // funcs contains the template.FuncMap for the different formatters.
@@ -118,11 +120,11 @@ func templateFormatter(templates ...string) migrate.Formatter {
 	for i, t := range templates {
 		tpls[i] = template.Must(template.New("").Funcs(funcs).Parse(t))
 	}
-	fmt, err := migrate.NewTemplateFormatter(tpls...)
+	tf, err := migrate.NewTemplateFormatter(tpls...)
 	if err != nil {
 		panic(err)
 	}
-	return fmt
+	return tf
 }
 
 // reverse changes for the down migration.


Test patch:
diff --git a/cmd/atlas/internal/ci/ci_test.go b/cmd/atlas/internal/ci/ci_test.go
index 29070852..b219d307 100644
--- a/cmd/atlas/internal/ci/ci_test.go
+++ b/cmd/atlas/internal/ci/ci_test.go
@@ -120,7 +120,7 @@ func TestDevLoader_LoadChanges(t *testing.T) {
 	c, err := sqlclient.Open(ctx, "sqlite://ci?mode=memory&cache=shared&_fk=1")
 	require.NoError(t, err)
 	defer c.Close()
-	l := &ci.DevLoader{Dev: c, Scan: testDir{}}
+	l := &ci.DevLoader{Dev: c}
 	diff, err := l.LoadChanges(ctx, nil, nil)
 	require.NoError(t, err)
 	require.Empty(t, diff)
@@ -173,7 +173,7 @@ func TestDevLoader_LoadChanges(t *testing.T) {
 }
 
 type testDir struct {
-	ci.DirScanner
+	migrate.Dir
 	files []migrate.File
 }
 
@@ -185,12 +185,8 @@ func (t testDir) Files() ([]migrate.File, error) {
 	return t.files, nil
 }
 
-func (testDir) Stmts(f migrate.File) ([]string, error) {
-	return strings.Split(string(f.Bytes()), "\n"), nil
-}
-
 type testFile struct {
-	fs.File
+	migrate.File
 	name, content string
 }
 
@@ -201,3 +197,7 @@ func (f testFile) Name() string {
 func (f testFile) Bytes() []byte {
 	return []byte(f.content)
 }
+
+func (f testFile) Stmts() ([]string, error) {
+	return strings.Split(string(f.Bytes()), "\n"), nil
+}
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index ff1e2998..c539477a 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -14,6 +14,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"runtime"
 	"strings"
 	"testing"
 	"time"
@@ -26,6 +27,7 @@ import (
 	_ "ariga.io/atlas/sql/sqlite"
 	_ "ariga.io/atlas/sql/sqlite/sqlitecheck"
 
+	"github.com/fatih/color"
 	_ "github.com/mattn/go-sqlite3"
 	"github.com/stretchr/testify/require"
 )
@@ -37,6 +39,9 @@ func TestMigrate(t *testing.T) {
 
 func TestMigrate_Apply(t *testing.T) {
 	p := t.TempDir()
+	// Disable text coloring in testing
+	// to assert on string matching.
+	color.NoColor = true
 
 	// Fails on empty directory.
 	s, err := runCmd(
@@ -173,7 +178,7 @@ func TestMigrate_Apply(t *testing.T) {
 	t.Cleanup(func() {
 		require.NoError(t, os.RemoveAll("testdata/sqlite3"))
 	})
-	require.NoError(t, exec.Command("sed", "-i", "s/col_2/col_5/g", "testdata/sqlite3/20220318104615_second.sql").Run())
+	sed(t, "s/col_2/col_5/g", "testdata/sqlite3/20220318104615_second.sql")
 	_, err = runCmd(Root, "migrate", "hash", "--force", "--dir", "file://testdata/sqlite3")
 	require.NoError(t, err)
 	s, err = runCmd(
@@ -184,8 +189,8 @@ func TestMigrate_Apply(t *testing.T) {
 	require.ErrorAs(t, err, &migrate.HistoryChangedError{})
 
 	// Fixing the migration file will finish without errors.
-	require.NoError(t, exec.Command("sed", "-i", "s/col_5/col_2/g", "testdata/sqlite3/20220318104615_second.sql").Run())
-	require.NoError(t, exec.Command("sed", "-i", "s/asdasd //g", "testdata/sqlite3/20220318104615_second.sql").Run())
+	sed(t, "s/col_5/col_2/g", "testdata/sqlite3/20220318104615_second.sql")
+	sed(t, "s/asdasd //g", "testdata/sqlite3/20220318104615_second.sql")
 	_, err = runCmd(Root, "migrate", "hash", "--force", "--dir", "file://testdata/sqlite3")
 	require.NoError(t, err)
 	s, err = runCmd(
@@ -582,3 +587,12 @@ func countFiles(t *testing.T, p string) int {
 	require.NoError(t, err)
 	return len(files)
 }
+
+func sed(t *testing.T, r, p string) {
+	args := []string{"-i"}
+	if runtime.GOOS == "darwin" {
+		args = append(args, ".bk")
+	}
+	buf, err := exec.Command("sed", append(args, r, p)...).CombinedOutput()
+	require.NoError(t, err, string(buf))
+}
diff --git a/sql/migrate/lex_test.go b/sql/migrate/lex_test.go
index c5461c8e..2f290f07 100644
--- a/sql/migrate/lex_test.go
+++ b/sql/migrate/lex_test.go
@@ -20,7 +20,7 @@ func TestLocalFile_Stmts(t *testing.T) {
 	files, err := dir.Files()
 	require.NoError(t, err)
 	for _, f := range files {
-		stmts, err := dir.Stmts(f)
+		stmts, err := f.Stmts()
 		require.NoError(t, err)
 		buf, err := os.ReadFile(filepath.Join(path, f.Name()+".golden"))
 		require.NoError(t, err)
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index 2c48a7a7..3c8e6c78 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -278,25 +278,17 @@ func TestLocalDir(t *testing.T) {
 	require.Equal(t, "2.10.x-20_description.sql", files[1].Name())
 	require.Equal(t, "3_partly.sql", files[2].Name())
 
-	stmts, err := d.Stmts(files[0])
+	stmts, err := files[0].Stmts()
 	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)
+	require.Equal(t, "1.a", files[0].Version())
+	require.Equal(t, "sub.up", files[0].Desc())
 
-	stmts, err = d.Stmts(files[1])
+	stmts, err = files[1].Stmts()
 	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)
+	require.Equal(t, "2.10.x-20", files[1].Version())
+	require.Equal(t, "description", files[1].Desc())
 }
 
 func TestExecutor_Pending(t *testing.T) {
@@ -481,7 +473,7 @@ func TestExecutor(t *testing.T) {
 	*rrw = mockRevisionReadWriter(migrate.Revisions{&migrate.Revision{Version: "unknown"}})
 	*drv = lockMockDriver{&mockDriver{}}
 	require.EqualError(t, ex.ExecuteN(context.Background(), 0),
-		`sql/migrate: execute: revisions and migrations mismatch: rev "1.a" <> file "unknown"`,
+		`sql/migrate: execute: revisions and migrations mismatch: rev "unknown" <> file "1.a"`,
 	)
 
 	require.Equal(t, drv.lockCounter, 1)
diff --git a/sql/sqltool/tool_test.go b/sql/sqltool/tool_test.go
index abaa7f2a..1c6a9b57 100644
--- a/sql/sqltool/tool_test.go
+++ b/sql/sqltool/tool_test.go
@@ -142,23 +142,17 @@ func TestScanners(t *testing.T) {
 
 	first, second := files[0], files[1]
 
-	stmts, err := d.Stmts(first)
+	stmts, err := first.Stmts()
 	require.NoError(t, err)
 	require.Equal(t, []string{"CREATE TABLE tbl\n(\n    col INT\n);"}, stmts)
-	stmts, err = d.Stmts(second)
+	stmts, err = second.Stmts()
 	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)
+	require.Equal(t, "1", first.Version())
+	require.Equal(t, "initial", first.Desc())
+	require.Equal(t, "2", second.Version())
+	require.Equal(t, "second_migration", second.Desc())
 }
 
 func dir(t *testing.T) migrate.Dir {