Repository: amacneil/dbmate
Pull request #388 (2023-02-20 23:12:02)

Support embedded filesystem migrations
Support passing `fs.FS` to support embedded filesystems. Defaults to the directory filesystem.

Also cleaned up `Migration` object and parsing, and simplified `FindMigrations()` to always return the application status of each migration.

Closes https://github.com/amacneil/dbmate/issues/193
Closes https://github.com/amacneil/dbmate/pull/346


Patch:
diff --git a/pkg/dbmate/db.go b/pkg/dbmate/db.go
index 5d47c10..92ee4a8 100644
--- a/pkg/dbmate/db.go
+++ b/pkg/dbmate/db.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"net/url"
 	"os"
 	"path/filepath"
@@ -30,7 +31,7 @@ var (
 )
 
 // migrationFileRegexp pattern for valid migration files
-var migrationFileRegexp = regexp.MustCompile(`^\d.*\.sql$`)
+var migrationFileRegexp = regexp.MustCompile(`^(\d+).*\.sql$`)
 
 // DB allows dbmate actions to be performed on a specified database
 type DB struct {
@@ -38,6 +39,8 @@ type DB struct {
 	AutoDumpSchema bool
 	// DatabaseURL is the database connection string
 	DatabaseURL *url.URL
+	// FS allows overriding the filesystem
+	FS fs.FS
 	// Log is the interface to write stdout
 	Log io.Writer
 	// MigrationsDir specifies the directory to find migration files
@@ -67,13 +70,15 @@ func New(databaseURL *url.URL) *DB {
 	return &DB{
 		AutoDumpSchema:      true,
 		DatabaseURL:         databaseURL,
+		FS:                  os.DirFS("."),
+		Log:                 os.Stdout,
 		MigrationsDir:       "./db/migrations",
 		MigrationsTableName: "schema_migrations",
 		SchemaFile:          "./db/schema.sql",
+		Verbose:             false,
 		WaitBefore:          false,
 		WaitInterval:        time.Second,
 		WaitTimeout:         60 * time.Second,
-		Log:                 os.Stdout,
 	}
 }
 
@@ -290,48 +295,41 @@ func (db *DB) openDatabaseForMigration(drv Driver) (*sql.DB, error) {
 
 // Migrate migrates database to the latest version
 func (db *DB) Migrate() error {
-	files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp)
+	drv, err := db.Driver()
 	if err != nil {
 		return err
 	}
 
-	if len(files) == 0 {
-		return ErrNoMigrationFiles
-	}
-
-	drv, err := db.Driver()
+	migrations, err := db.FindMigrations()
 	if err != nil {
 		return err
 	}
 
-	sqlDB, err := db.openDatabaseForMigration(drv)
-	if err != nil {
-		return err
+	if len(migrations) == 0 {
+		return ErrNoMigrationFiles
 	}
-	defer dbutil.MustClose(sqlDB)
 
-	applied, err := drv.SelectMigrations(sqlDB, -1)
+	sqlDB, err := db.openDatabaseForMigration(drv)
 	if err != nil {
 		return err
 	}
+	defer dbutil.MustClose(sqlDB)
 
-	for _, filename := range files {
-		ver := migrationVersion(filename)
-		if ok := applied[ver]; ok {
-			// migration already applied
+	for _, migration := range migrations {
+		if migration.Applied {
 			continue
 		}
 
-		fmt.Fprintf(db.Log, "Applying: %s\n", filename)
+		fmt.Fprintf(db.Log, "Applying: %s\n", migration.FileName)
 
-		up, _, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
+		parsed, err := migration.Parse()
 		if err != nil {
 			return err
 		}
 
 		execMigration := func(tx dbutil.Transaction) error {
 			// run actual migration
-			result, err := tx.Exec(up.Contents)
+			result, err := tx.Exec(parsed.Up)
 			if err != nil {
 				return err
 			} else if db.Verbose {
@@ -339,10 +337,10 @@ func (db *DB) Migrate() error {
 			}
 
 			// record migration
-			return drv.InsertMigration(tx, ver)
+			return drv.InsertMigration(tx, migration.Version)
 		}
 
-		if up.Options.Transaction() {
+		if parsed.UpOptions.Transaction() {
 			// begin transaction
 			err = doTransaction(sqlDB, execMigration)
 		} else {
@@ -374,53 +372,69 @@ func (db *DB) printVerbose(result sql.Result) {
 	}
 }
 
-func findMigrationFiles(dir string, re *regexp.Regexp) ([]string, error) {
-	files, err := os.ReadDir(dir)
+// FindMigrations lists all available migrations
+func (db *DB) FindMigrations() ([]Migration, error) {
+	drv, err := db.Driver()
+	if err != nil {
+		return nil, err
+	}
+
+	sqlDB, err := drv.Open()
+	if err != nil {
+		return nil, err
+	}
+	defer dbutil.MustClose(sqlDB)
+
+	// find applied migrations
+	appliedMigrations := map[string]bool{}
+	migrationsTableExists, err := drv.MigrationsTableExists(sqlDB)
 	if err != nil {
-		return nil, fmt.Errorf("%w `%s`", ErrMigrationDirNotFound, dir)
+		return nil, err
+	}
+
+	if migrationsTableExists {
+		appliedMigrations, err = drv.SelectMigrations(sqlDB, -1)
+		if err != nil {
+			return nil, err
+		}
 	}
 
-	matches := []string{}
+	// find filesystem migrations
+	files, err := fs.ReadDir(db.FS, filepath.Clean(db.MigrationsDir))
+	if err != nil {
+		return nil, fmt.Errorf("%w `%s`", ErrMigrationDirNotFound, db.MigrationsDir)
+	}
+
+	migrations := []Migration{}
 	for _, file := range files {
 		if file.IsDir() {
 			continue
 		}
 
-		name := file.Name()
-		if !re.MatchString(name) {
+		matches := migrationFileRegexp.FindStringSubmatch(file.Name())
+		if len(matches) < 2 {
 			continue
 		}
 
-		matches = append(matches, name)
-	}
-
-	sort.Strings(matches)
-
-	return matches, nil
-}
-
-func findMigrationFile(dir string, ver string) (string, error) {
-	if ver == "" {
-		panic("migration version is required")
-	}
-
-	ver = regexp.QuoteMeta(ver)
-	re := regexp.MustCompile(fmt.Sprintf(`^%s.*\.sql$`, ver))
-
-	files, err := findMigrationFiles(dir, re)
-	if err != nil {
-		return "", err
-	}
+		migration := Migration{
+			Applied:  false,
+			FileName: matches[0],
+			FilePath: filepath.Join(db.MigrationsDir, matches[0]),
+			FS:       db.FS,
+			Version:  matches[1],
+		}
+		if ok := appliedMigrations[migration.Version]; ok {
+			migration.Applied = true
+		}
 
-	if len(files) == 0 {
-		return "", fmt.Errorf("%w: %s*.sql", ErrMigrationNotFound, ver)
+		migrations = append(migrations, migration)
 	}
 
-	return files[0], nil
-}
+	sort.Slice(migrations, func(i, j int) bool {
+		return migrations[i].FileName < migrations[j].FileName
+	})
 
-func migrationVersion(filename string) string {
-	return regexp.MustCompile(`^\d+`).FindString(filename)
+	return migrations, nil
 }
 
 // Rollback rolls back the most recent migration
@@ -436,35 +450,33 @@ func (db *DB) Rollback() error {
 	}
 	defer dbutil.MustClose(sqlDB)
 
-	applied, err := drv.SelectMigrations(sqlDB, 1)
+	// find last applied migration
+	var latest *Migration
+	migrations, err := db.FindMigrations()
 	if err != nil {
 		return err
 	}
 
-	// grab most recent applied migration (applied has len=1)
-	latest := ""
-	for ver := range applied {
-		latest = ver
-	}
-	if latest == "" {
-		return ErrNoRollback
+	for _, migration := range migrations {
+		if migration.Applied {
+			latest = &migration
+		}
 	}
 
-	filename, err := findMigrationFile(db.MigrationsDir, latest)
-	if err != nil {
-		return err
+	if latest == nil {
+		return ErrNoRollback
 	}
 
-	fmt.Fprintf(db.Log, "Rolling back: %s\n", filename)
+	fmt.Fprintf(db.Log, "Rolling back: %s\n", latest.FileName)
 
-	_, down, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
+	parsed, err := latest.Parse()
 	if err != nil {
 		return err
 	}
 
 	execMigration := func(tx dbutil.Transaction) error {
 		// rollback migration
-		result, err := tx.Exec(down.Contents)
+		result, err := tx.Exec(parsed.Down)
 		if err != nil {
 			return err
 		} else if db.Verbose {
@@ -472,10 +484,10 @@ func (db *DB) Rollback() error {
 		}
 
 		// remove migration record
-		return drv.DeleteMigration(tx, latest)
+		return drv.DeleteMigration(tx, latest.Version)
 	}
 
-	if down.Options.Transaction() {
+	if parsed.DownOptions.Transaction() {
 		// begin transaction
 		err = doTransaction(sqlDB, execMigration)
 	} else {
@@ -497,7 +509,7 @@ func (db *DB) Rollback() error {
 
 // Status shows the status of all migrations
 func (db *DB) Status(quiet bool) (int, error) {
-	results, err := db.CheckMigrationsStatus()
+	results, err := db.FindMigrations()
 	if err != nil {
 		return -1, err
 	}
@@ -507,10 +519,10 @@ func (db *DB) Status(quiet bool) (int, error) {
 
 	for _, res := range results {
 		if res.Applied {
-			line = fmt.Sprintf("[X] %s", res.Filename)
+			line = fmt.Sprintf("[X] %s", res.FileName)
 			totalApplied++
 		} else {
-			line = fmt.Sprintf("[ ] %s", res.Filename)
+			line = fmt.Sprintf("[ ] %s", res.FileName)
 		}
 		if !quiet {
 			fmt.Fprintln(db.Log, line)
@@ -526,56 +538,3 @@ func (db *DB) Status(quiet bool) (int, error) {
 
 	return totalPending, nil
 }
-
-// CheckMigrationsStatus returns the status of all available mgirations
-func (db *DB) CheckMigrationsStatus() ([]StatusResult, error) {
-	drv, err := db.Driver()
-	if err != nil {
-		return nil, err
-	}
-
-	files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp)
-	if err != nil {
-		return nil, err
-	}
-
-	if len(files) == 0 {
-		return nil, ErrNoMigrationFiles
-	}
-
-	sqlDB, err := drv.Open()
-	if err != nil {
-		return nil, err
-	}
-	defer dbutil.MustClose(sqlDB)
-
-	applied := map[string]bool{}
-
-	migrationsTableExists, err := drv.MigrationsTableExists(sqlDB)
-	if err != nil {
-		return nil, err
-	}
-
-	if migrationsTableExists {
-		applied, err = drv.SelectMigrations(sqlDB, -1)
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	var results []StatusResult
-
-	for _, filename := range files {
-		ver := migrationVersion(filename)
-		res := StatusResult{Filename: filename}
-		if ok := applied[ver]; ok {
-			res.Applied = true
-		} else {
-			res.Applied = false
-		}
-
-		results = append(results, res)
-	}
-
-	return results, nil
-}
diff --git a/pkg/dbmate/migration.go b/pkg/dbmate/migration.go
index 4bb18ef..c2df7ea 100644
--- a/pkg/dbmate/migration.go
+++ b/pkg/dbmate/migration.go
@@ -2,43 +2,49 @@ package dbmate
 
 import (
 	"errors"
-	"os"
+	"io/fs"
 	"regexp"
 	"strings"
 )
 
-// MigrationOptions is an interface for accessing migration options
-type MigrationOptions interface {
-	Transaction() bool
+// Migration represents an available migration and status
+type Migration struct {
+	Applied  bool
+	FileName string
+	FilePath string
+	FS       fs.FS
+	Version  string
 }
 
-type migrationOptions map[string]string
+// Parse a migration
+func (m *Migration) Parse() (*ParsedMigration, error) {
+	bytes, err := fs.ReadFile(m.FS, m.FilePath)
+	if err != nil {
+		return nil, err
+	}
 
-// Transaction returns whether or not this migration should run in a transaction
-// Defaults to true.
-func (m migrationOptions) Transaction() bool {
-	return m["transaction"] != "false"
+	return parseMigrationContents(string(bytes))
 }
 
-// Migration contains the migration contents and options
-type Migration struct {
-	Contents string
-	Options  MigrationOptions
+// ParsedMigration contains the migration contents and options
+type ParsedMigration struct {
+	Up          string
+	UpOptions   ParsedMigrationOptions
+	Down        string
+	DownOptions ParsedMigrationOptions
 }
 
-// NewMigration constructs a Migration object
-func NewMigration() Migration {
-	return Migration{Contents: "", Options: make(migrationOptions)}
+// ParsedMigrationOptions is an interface for accessing migration options
+type ParsedMigrationOptions interface {
+	Transaction() bool
 }
 
-// parseMigration reads a migration file and returns (up Migration, down Migration, error)
-func parseMigration(path string) (Migration, Migration, error) {
-	data, err := os.ReadFile(path)
-	if err != nil {
-		return NewMigration(), NewMigration(), err
-	}
-	up, down, err := parseMigrationContents(string(data))
-	return up, down, err
+type migrationOptions map[string]string
+
+// Transaction returns whether or not this migration should run in a transaction
+// Defaults to true.
+func (m migrationOptions) Transaction() bool {
+	return m["transaction"] != "false"
 }
 
 var (
@@ -62,17 +68,14 @@ var (
 // block and the second representing the "down" block. This function
 // requires that at least an up block was defined and will otherwise
 // return an error.
-func parseMigrationContents(contents string) (Migration, Migration, error) {
-	up := NewMigration()
-	down := NewMigration()
-
+func parseMigrationContents(contents string) (*ParsedMigration, error) {
 	upDirectiveStart, upDirectiveEnd, hasDefinedUpBlock := getMatchPositions(contents, upRegExp)
 	downDirectiveStart, downDirectiveEnd, hasDefinedDownBlock := getMatchPositions(contents, downRegExp)
 
 	if !hasDefinedUpBlock {
-		return up, down, ErrParseMissingUp
+		return nil, ErrParseMissingUp
 	} else if statementsPrecedeMigrateBlocks(contents, upDirectiveStart, downDirectiveStart) {
-		return up, down, ErrParseUnexpectedStmt
+		return nil, ErrParseUnexpectedStmt
 	}
 
 	upEnd := len(contents)
@@ -89,13 +92,13 @@ func parseMigrationContents(contents string) (Migration, Migration, error) {
 	upDirective := substring(contents, upDirectiveStart, upDirectiveEnd)
 	downDirective := substring(contents, downDirectiveStart, downDirectiveEnd)
 
-	up.Options = parseMigrationOptions(upDirective)
-	up.Contents = substring(contents, upDirectiveStart, upEnd)
-
-	down.Options = parseMigrationOptions(downDirective)
-	down.Contents = substring(contents, downDirectiveStart, downEnd)
-
-	return up, down, nil
+	parsed := ParsedMigration{
+		Up:          substring(contents, upDirectiveStart, upEnd),
+		UpOptions:   parseMigrationOptions(upDirective),
+		Down:        substring(contents, downDirectiveStart, downEnd),
+		DownOptions: parseMigrationOptions(downDirective),
+	}
+	return &parsed, nil
 }
 
 // parseMigrationOptions parses the migration options out of a block
@@ -105,7 +108,7 @@ func parseMigrationContents(contents string) (Migration, Migration, error) {
 //
 //	fmt.Printf("%#v", parseMigrationOptions("-- migrate:up transaction:false"))
 //	// migrationOptions{"transaction": "false"}
-func parseMigrationOptions(contents string) MigrationOptions {
+func parseMigrationOptions(contents string) ParsedMigrationOptions {
 	options := make(migrationOptions)
 
 	// strip away the -- migrate:[up|down] part


Test patch:
diff --git a/pkg/dbmate/db_test.go b/pkg/dbmate/db_test.go
index cab7421..100558a 100644
--- a/pkg/dbmate/db_test.go
+++ b/pkg/dbmate/db_test.go
@@ -5,6 +5,7 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
+	"testing/fstest"
 	"time"
 
 	"github.com/amacneil/dbmate/pkg/dbmate"
@@ -331,11 +332,18 @@ func TestRollback(t *testing.T) {
 			drv, err := db.Driver()
 			require.NoError(t, err)
 
-			// drop, recreate, and migrate database
+			// drop and create database
 			err = db.Drop()
 			require.NoError(t, err)
 			err = db.Create()
 			require.NoError(t, err)
+
+			// rollback should return error
+			err = db.Rollback()
+			require.Error(t, err)
+			require.ErrorContains(t, err, "can't rollback: no migrations have been applied")
+
+			// migrate database
 			err = db.Migrate()
 			require.NoError(t, err)
 
@@ -369,7 +377,7 @@ func TestRollback(t *testing.T) {
 	}
 }
 
-func TestStatus(t *testing.T) {
+func TestFindMigrations(t *testing.T) {
 	for _, u := range testURLs() {
 		t.Run(u.Scheme, func(t *testing.T) {
 			db := newTestDB(t, u)
@@ -388,7 +396,7 @@ func TestStatus(t *testing.T) {
 			defer dbutil.MustClose(sqlDB)
 
 			// two pending
-			results, err := db.CheckMigrationsStatus()
+			results, err := db.FindMigrations()
 			require.NoError(t, err)
 			require.Len(t, results, 2)
 			require.False(t, results[0].Applied)
@@ -402,7 +410,7 @@ func TestStatus(t *testing.T) {
 			require.NoError(t, err)
 
 			// two applied
-			results, err = db.CheckMigrationsStatus()
+			results, err = db.FindMigrations()
 			require.NoError(t, err)
 			require.Len(t, results, 2)
 			require.True(t, results[0].Applied)
@@ -413,7 +421,7 @@ func TestStatus(t *testing.T) {
 			require.NoError(t, err)
 
 			// one applied, one pending
-			results, err = db.CheckMigrationsStatus()
+			results, err = db.FindMigrations()
 			require.NoError(t, err)
 			require.Len(t, results, 2)
 			require.True(t, results[0].Applied)
@@ -421,3 +429,57 @@ func TestStatus(t *testing.T) {
 		})
 	}
 }
+
+func TestFindMigrationsFS(t *testing.T) {
+	mapFS := fstest.MapFS{
+		"db/migrations/20151129054053_test_migration.sql": {},
+		"db/migrations/001_test_migration.sql": {
+			Data: []byte(`-- migrate:up
+create table users (id serial, name text);
+-- migrate:down
+drop table users;
+`),
+		},
+		"db/migrations/002_test_migration.sql":                {},
+		"db/migrations/003_not_sql.txt":                       {},
+		"db/migrations/missing_version.sql":                   {},
+		"db/not_migrations/20151129054053_test_migration.sql": {},
+	}
+
+	u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL"))
+	db := newTestDB(t, u)
+	db.FS = mapFS
+
+	// drop and recreate database
+	err := db.Drop()
+	require.NoError(t, err)
+	err = db.Create()
+	require.NoError(t, err)
+
+	actual, err := db.FindMigrations()
+	require.NoError(t, err)
+
+	// test migrations are correct and in order
+	require.Equal(t, "001_test_migration.sql", actual[0].FileName)
+	require.Equal(t, "db/migrations/001_test_migration.sql", actual[0].FilePath)
+	require.Equal(t, "001", actual[0].Version)
+	require.Equal(t, false, actual[0].Applied)
+
+	require.Equal(t, "002_test_migration.sql", actual[1].FileName)
+	require.Equal(t, "db/migrations/002_test_migration.sql", actual[1].FilePath)
+	require.Equal(t, "002", actual[1].Version)
+	require.Equal(t, false, actual[1].Applied)
+
+	require.Equal(t, "20151129054053_test_migration.sql", actual[2].FileName)
+	require.Equal(t, "db/migrations/20151129054053_test_migration.sql", actual[2].FilePath)
+	require.Equal(t, "20151129054053", actual[2].Version)
+	require.Equal(t, false, actual[2].Applied)
+
+	// test parsing first migration
+	parsed, err := actual[0].Parse()
+	require.Nil(t, err)
+	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", parsed.Up)
+	require.True(t, parsed.UpOptions.Transaction())
+	require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down)
+	require.True(t, parsed.DownOptions.Transaction())
+}
diff --git a/pkg/dbmate/migration_test.go b/pkg/dbmate/migration_test.go
index a2e4fb0..8e9d90d 100644
--- a/pkg/dbmate/migration_test.go
+++ b/pkg/dbmate/migration_test.go
@@ -2,10 +2,38 @@ package dbmate
 
 import (
 	"testing"
+	"testing/fstest"
 
 	"github.com/stretchr/testify/require"
 )
 
+func TestParse(t *testing.T) {
+	fs := fstest.MapFS{
+		"bar/123_foo.sql": {
+			Data: []byte(`-- migrate:up
+create table users (id serial, name text);
+-- migrate:down
+drop table users;
+`),
+		},
+	}
+
+	migration := &Migration{
+		Applied:  false,
+		FileName: "123_foo.sql",
+		FilePath: "bar/123_foo.sql",
+		FS:       fs,
+		Version:  "123",
+	}
+
+	parsed, err := migration.Parse()
+	require.Nil(t, err)
+	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", parsed.Up)
+	require.True(t, parsed.UpOptions.Transaction())
+	require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down)
+	require.True(t, parsed.DownOptions.Transaction())
+}
+
 func TestParseMigrationContents(t *testing.T) {
 	// It supports the typical use case.
 	migration := `-- migrate:up
@@ -13,14 +41,14 @@ create table users (id serial, name text);
 -- migrate:down
 drop table users;`
 
-	up, down, err := parseMigrationContents(migration)
+	parsed, err := parseMigrationContents(migration)
 	require.Nil(t, err)
 
-	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", up.Contents)
-	require.Equal(t, true, up.Options.Transaction())
+	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", parsed.Up)
+	require.Equal(t, true, parsed.UpOptions.Transaction())
 
-	require.Equal(t, "-- migrate:down\ndrop table users;", down.Contents)
-	require.Equal(t, true, down.Options.Transaction())
+	require.Equal(t, "-- migrate:down\ndrop table users;", parsed.Down)
+	require.Equal(t, true, parsed.DownOptions.Transaction())
 
 	// It does not require space between the '--' and 'migrate'
 	migration = `
@@ -31,14 +59,14 @@ create table users (id serial, name text);
 drop table users;
 `
 
-	up, down, err = parseMigrationContents(migration)
+	parsed, err = parseMigrationContents(migration)
 	require.Nil(t, err)
 
-	require.Equal(t, "--migrate:up\ncreate table users (id serial, name text);\n\n", up.Contents)
-	require.Equal(t, true, up.Options.Transaction())
+	require.Equal(t, "--migrate:up\ncreate table users (id serial, name text);\n\n", parsed.Up)
+	require.Equal(t, true, parsed.UpOptions.Transaction())
 
-	require.Equal(t, "--migrate:down\ndrop table users;\n", down.Contents)
-	require.Equal(t, true, down.Options.Transaction())
+	require.Equal(t, "--migrate:down\ndrop table users;\n", parsed.Down)
+	require.Equal(t, true, parsed.DownOptions.Transaction())
 
 	// It is acceptable for down to be defined before up
 	migration = `-- migrate:down
@@ -47,14 +75,14 @@ drop table users;
 create table users (id serial, name text);
 `
 
-	up, down, err = parseMigrationContents(migration)
+	parsed, err = parseMigrationContents(migration)
 	require.Nil(t, err)
 
-	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", up.Contents)
-	require.Equal(t, true, up.Options.Transaction())
+	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", parsed.Up)
+	require.Equal(t, true, parsed.UpOptions.Transaction())
 
-	require.Equal(t, "-- migrate:down\ndrop table users;\n", down.Contents)
-	require.Equal(t, true, down.Options.Transaction())
+	require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down)
+	require.Equal(t, true, parsed.DownOptions.Transaction())
 
 	// It supports turning transactions off for a given migration block,
 	// e.g., the below would not work in Postgres inside a transaction.
@@ -63,21 +91,21 @@ create table users (id serial, name text);
 ALTER TYPE colors ADD VALUE 'orange' AFTER 'red';
 `
 
-	up, down, err = parseMigrationContents(migration)
+	parsed, err = parseMigrationContents(migration)
 	require.Nil(t, err)
 
-	require.Equal(t, "-- migrate:up transaction:false\nALTER TYPE colors ADD VALUE 'orange' AFTER 'red';\n", up.Contents)
-	require.Equal(t, false, up.Options.Transaction())
+	require.Equal(t, "-- migrate:up transaction:false\nALTER TYPE colors ADD VALUE 'orange' AFTER 'red';\n", parsed.Up)
+	require.Equal(t, false, parsed.UpOptions.Transaction())
 
-	require.Equal(t, "", down.Contents)
-	require.Equal(t, true, down.Options.Transaction())
+	require.Equal(t, "", parsed.Down)
+	require.Equal(t, true, parsed.DownOptions.Transaction())
 
 	// It does *not* support omitting the up block.
 	migration = `-- migrate:down
 drop table users;
 `
 
-	_, _, err = parseMigrationContents(migration)
+	_, err = parseMigrationContents(migration)
 	require.NotNil(t, err)
 	require.Equal(t, "dbmate requires each migration to define an up block with '-- migrate:up'", err.Error())
 
@@ -93,14 +121,14 @@ create table users (id serial, name text);
 drop table users;
 `
 
-	up, down, err = parseMigrationContents(migration)
+	parsed, err = parseMigrationContents(migration)
 	require.Nil(t, err)
 
-	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n\n", up.Contents)
-	require.Equal(t, true, up.Options.Transaction())
+	require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n\n", parsed.Up)
+	require.Equal(t, true, parsed.UpOptions.Transaction())
 
-	require.Equal(t, "-- migrate:down\ndrop table users;\n", down.Contents)
-	require.Equal(t, true, down.Options.Transaction())
+	require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down)
+	require.Equal(t, true, parsed.DownOptions.Transaction())
 
 	// It does *not* allow arbitrary statements preceding the migrate blocks
 	migration = `
@@ -116,7 +144,7 @@ ALTER TABLE users
 DROP COLUMN status;
 `
 
-	_, _, err = parseMigrationContents(migration)
+	_, err = parseMigrationContents(migration)
 	require.NotNil(t, err)
 	require.Equal(t, "dbmate does not support statements defined outside of the '-- migrate:up' or '-- migrate:down' blocks", err.Error())
 
@@ -126,7 +154,7 @@ ALTER TABLE users
 ADD COLUMN status status_type DEFAULT 'active';
 `
 
-	_, _, err = parseMigrationContents(migration)
+	_, err = parseMigrationContents(migration)
 	require.NotNil(t, err)
 	require.Equal(t, "dbmate requires each migration to define an up block with '-- migrate:up'", err.Error())
 }