Repository: amacneil/dbmate (go)

Issue:
Invalid Sqlite schema.sql when INTEGER PRIMARY KEY AUTOINCREMENT is used
## TL;DR 
 
Dbmate always outputs the full schema, including `sqlite_sequence` which is auto-generated when autoincrement appears in at least one table definition. This table is reserved for internal use in sqlite and cannot be created by the user (see https://www.sqlite.org/autoinc.html section 3).  

This schema.sql file fails to load on an empty database. 

## Setup: 
dbmate 1.15.0, sqlite 3.39 

## To reproduce:

Run `dbmate new test`, let's call it `db/migrations/20220818223405_test.sql`.

The file should contain:

```
-- migrate:up

create table t (
  id INTEGER PRIMARY KEY AUTOINCREMENT
);

-- migrate:down

drop table t
```

Run `dbmate -u sqlite3:test.db up`

The file `db/schema.sql` should contain: 

```
CREATE TABLE IF NOT EXISTS "schema_migrations" (version varchar(255) primary key);
CREATE TABLE t (
  id INTEGER PRIMARY KEY AUTOINCREMENT
);
CREATE TABLE sqlite_sequence(name,seq);
-- Dbmate schema migrations
INSERT INTO "schema_migrations" (version) VALUES
  ('20220818223405');
```

Now test the schema file, it should be possible to use on an empty db to initialise it.

Run `sqlite3 test2.db`

```
SQLite version 3.39.....
Enter ".help" for usage hints.
sqlite> .read db/schema.sql
Parse error near line 5: object name reserved for internal use: sqlite_sequence
```

## Solution 

The table `sqlite_sequence` should not be extracted in the schema, sqlite will generate it automatically. 

Pull request:
SQLite: Do not dump system tables
Pass the `--nosys` flag through to the SQLite `.schema` command. This will stop the `sqlite` command from writing out system tables `sqlite_*`.

This flag is documented:

```
sqlite> .help .schema
.schema ?PATTERN?        Show the CREATE statements matching PATTERN
   Options:
      --indent             Try to pretty-print the schema
      --nosys              Omit objects whose names start with "sqlite_"
```

I wasn't sure if this would constitute a breaking change so I didn't hide it behind a flag as the issue was marked as `Bug`, but I'm happy to add this if it is correct to.

This was also quite hard to test as the test suite passed with or without the `--nosys` flag. I'm presuming this is a change in SQLite but I couldn't find a reference to it. I added a test any way just to make sure it doesn't creap back in.

I also confirmed this behaviour on the command line.

Fixes #315

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fe51262..4d831fa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -154,7 +154,7 @@ jobs:
             type=semver,pattern={{major}}
 
       - name: Publish Docker image
-        uses: docker/build-push-action@v3
+        uses: docker/build-push-action@v4
         if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
         with:
           context: .
diff --git a/main.go b/main.go
index 04a777f..b32c594 100644
--- a/main.go
+++ b/main.go
@@ -33,6 +33,7 @@ func NewApp() *cli.App {
 	app.Usage = "A lightweight, framework-independent database migration tool."
 	app.Version = dbmate.Version
 
+	defaultDB := dbmate.New(nil)
 	app.Flags = []cli.Flag{
 		&cli.StringFlag{
 			Name:    "url",
@@ -49,20 +50,20 @@ func NewApp() *cli.App {
 			Name:    "migrations-dir",
 			Aliases: []string{"d"},
 			EnvVars: []string{"DBMATE_MIGRATIONS_DIR"},
-			Value:   dbmate.DefaultMigrationsDir,
+			Value:   defaultDB.MigrationsDir,
 			Usage:   "specify the directory containing migration files",
 		},
 		&cli.StringFlag{
 			Name:    "migrations-table",
 			EnvVars: []string{"DBMATE_MIGRATIONS_TABLE"},
-			Value:   dbmate.DefaultMigrationsTableName,
+			Value:   defaultDB.MigrationsTableName,
 			Usage:   "specify the database table to record migrations in",
 		},
 		&cli.StringFlag{
 			Name:    "schema-file",
 			Aliases: []string{"s"},
 			EnvVars: []string{"DBMATE_SCHEMA_FILE"},
-			Value:   dbmate.DefaultSchemaFile,
+			Value:   defaultDB.SchemaFile,
 			Usage:   "specify the schema file location",
 		},
 		&cli.BoolFlag{
@@ -79,7 +80,7 @@ func NewApp() *cli.App {
 			Name:    "wait-timeout",
 			EnvVars: []string{"DBMATE_WAIT_TIMEOUT"},
 			Usage:   "timeout for --wait flag",
-			Value:   dbmate.DefaultWaitTimeout,
+			Value:   defaultDB.WaitTimeout,
 		},
 	}
 
@@ -231,9 +232,9 @@ func action(f func(*dbmate.DB, *cli.Context) error) cli.ActionFunc {
 		db.MigrationsTableName = c.String("migrations-table")
 		db.SchemaFile = c.String("schema-file")
 		db.WaitBefore = c.Bool("wait")
-		overrideTimeout := c.Duration("wait-timeout")
-		if overrideTimeout != 0 {
-			db.WaitTimeout = overrideTimeout
+		waitTimeout := c.Duration("wait-timeout")
+		if waitTimeout != 0 {
+			db.WaitTimeout = waitTimeout
 		}
 
 		return f(db, c)
diff --git a/pkg/dbmate/db.go b/pkg/dbmate/db.go
index 5c1c3a2..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"
@@ -15,21 +16,6 @@ import (
 	"github.com/amacneil/dbmate/pkg/dbutil"
 )
 
-// DefaultMigrationsDir specifies default directory to find migration files
-const DefaultMigrationsDir = "./db/migrations"
-
-// DefaultMigrationsTableName specifies default database tables to record migraitons in
-const DefaultMigrationsTableName = "schema_migrations"
-
-// DefaultSchemaFile specifies default location for schema.sql
-const DefaultSchemaFile = "./db/schema.sql"
-
-// DefaultWaitInterval specifies length of time between connection attempts
-const DefaultWaitInterval = time.Second
-
-// DefaultWaitTimeout specifies maximum time for connection attempts
-const DefaultWaitTimeout = 60 * time.Second
-
 // Error codes
 var (
 	ErrNoMigrationFiles      = errors.New("no migration files found")
@@ -44,23 +30,35 @@ var (
 	ErrCreateDirectory       = errors.New("unable to create directory")
 )
 
+// migrationFileRegexp pattern for valid migration files
+var migrationFileRegexp = regexp.MustCompile(`^(\d+).*\.sql$`)
+
 // DB allows dbmate actions to be performed on a specified database
 type DB struct {
-	AutoDumpSchema      bool
-	DatabaseURL         *url.URL
-	MigrationsDir       string
+	// AutoDumpSchema generates schema.sql after each action
+	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
+	MigrationsDir string
+	// MigrationsTableName specifies the database table to record migrations in
 	MigrationsTableName string
-	SchemaFile          string
-	Verbose             bool
-	WaitBefore          bool
-	WaitInterval        time.Duration
-	WaitTimeout         time.Duration
-	Log                 io.Writer
+	// SchemaFile specifies the location for schema.sql file
+	SchemaFile string
+	// Verbose prints the result of each statement execution
+	Verbose bool
+	// WaitBefore will wait for database to become available before running any actions
+	WaitBefore bool
+	// WaitInterval specifies length of time between connection attempts
+	WaitInterval time.Duration
+	// WaitTimeout specifies maximum time for connection attempts
+	WaitTimeout time.Duration
 }
 
-// migrationFileRegexp pattern for valid migration files
-var migrationFileRegexp = regexp.MustCompile(`^\d.*\.sql$`)
-
 // StatusResult represents an available migration status
 type StatusResult struct {
 	Filename string
@@ -72,18 +70,20 @@ func New(databaseURL *url.URL) *DB {
 	return &DB{
 		AutoDumpSchema:      true,
 		DatabaseURL:         databaseURL,
-		MigrationsDir:       DefaultMigrationsDir,
-		MigrationsTableName: DefaultMigrationsTableName,
-		SchemaFile:          DefaultSchemaFile,
-		WaitBefore:          false,
-		WaitInterval:        DefaultWaitInterval,
-		WaitTimeout:         DefaultWaitTimeout,
+		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,
 	}
 }
 
-// GetDriver initializes the appropriate database driver
-func (db *DB) GetDriver() (Driver, error) {
+// Driver initializes the appropriate database driver
+func (db *DB) Driver() (Driver, error) {
 	if db.DatabaseURL == nil || db.DatabaseURL.Scheme == "" {
 		return nil, ErrInvalidURL
 	}
@@ -95,22 +95,18 @@ func (db *DB) GetDriver() (Driver, error) {
 
 	config := DriverConfig{
 		DatabaseURL:         db.DatabaseURL,
-		MigrationsTableName: db.MigrationsTableName,
 		Log:                 db.Log,
+		MigrationsTableName: db.MigrationsTableName,
 	}
+	drv := driverFunc(config)
 
-	return driverFunc(config), nil
-}
-
-// Wait blocks until the database server is available. It does not verify that
-// the specified database exists, only that the host is ready to accept connections.
-func (db *DB) Wait() error {
-	drv, err := db.GetDriver()
-	if err != nil {
-		return err
+	if db.WaitBefore {
+		if err := db.wait(drv); err != nil {
+			return nil, err
+		}
 	}
 
-	return db.wait(drv)
+	return drv, nil
 }
 
 func (db *DB) wait(drv Driver) error {
@@ -140,18 +136,23 @@ func (db *DB) wait(drv Driver) error {
 	return fmt.Errorf("%w: %s", ErrCantConnect, err)
 }
 
-// CreateAndMigrate creates the database (if necessary) and runs migrations
-func (db *DB) CreateAndMigrate() error {
-	drv, err := db.GetDriver()
+// Wait blocks until the database server is available. It does not verify that
+// the specified database exists, only that the host is ready to accept connections.
+func (db *DB) Wait() error {
+	drv, err := db.Driver()
 	if err != nil {
 		return err
 	}
 
-	if db.WaitBefore {
-		err := db.wait(drv)
-		if err != nil {
-			return err
-		}
+	// if db.WaitBefore is true, wait() will get called twice, no harm
+	return db.wait(drv)
+}
+
+// CreateAndMigrate creates the database (if necessary) and runs migrations
+func (db *DB) CreateAndMigrate() error {
+	drv, err := db.Driver()
+	if err != nil {
+		return err
 	}
 
 	// create database if it does not already exist
@@ -165,61 +166,36 @@ func (db *DB) CreateAndMigrate() error {
 	}
 
 	// migrate
-	return db.migrate(drv)
+	return db.Migrate()
 }
 
 // Create creates the current database
 func (db *DB) Create() error {
-	drv, err := db.GetDriver()
+	drv, err := db.Driver()
 	if err != nil {
 		return err
 	}
 
-	if db.WaitBefore {
-		err := db.wait(drv)
-		if err != nil {
-			return err
-		}
-	}
-
 	return drv.CreateDatabase()
 }
 
 // Drop drops the current database (if it exists)
 func (db *DB) Drop() error {
-	drv, err := db.GetDriver()
+	drv, err := db.Driver()
 	if err != nil {
 		return err
 	}
 
-	if db.WaitBefore {
-		err := db.wait(drv)
-		if err != nil {
-			return err
-		}
-	}
-
 	return drv.DropDatabase()
 }
 
 // DumpSchema writes the current database schema to a file
 func (db *DB) DumpSchema() error {
-	drv, err := db.GetDriver()
+	drv, err := db.Driver()
 	if err != nil {
 		return err
 	}
 
-	return db.dumpSchema(drv)
-}
-
-func (db *DB) dumpSchema(drv Driver) error {
-	if db.WaitBefore {
-		err := db.wait(drv)
-		if err != nil {
-			return err
-		}
-	}
-
 	sqlDB, err := db.openDatabaseForMigration(drv)
 	if err != nil {
 		return err
@@ -319,59 +295,41 @@ func (db *DB) openDatabaseForMigration(drv Driver) (*sql.DB, error) {
 
 // Migrate migrates database to the latest version
 func (db *DB) Migrate() error {
-	drv, err := db.GetDriver()
+	drv, err := db.Driver()
 	if err != nil {
 		return err
 	}
 
-	return db.migrate(drv)
-}
-
-func (db *DB) migrate(drv Driver) error {
-	files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp)
+	migrations, err := db.FindMigrations()
 	if err != nil {
 		return err
 	}
 
-	if len(files) == 0 {
+	if len(migrations) == 0 {
 		return ErrNoMigrationFiles
 	}
 
-	if db.WaitBefore {
-		err := db.wait(drv)
-		if err != nil {
-			return err
-		}
-	}
-
 	sqlDB, err := db.openDatabaseForMigration(drv)
 	if err != nil {
 		return err
 	}
 	defer dbutil.MustClose(sqlDB)
 
-	applied, err := drv.SelectMigrations(sqlDB, -1)
-	if err != nil {
-		return err
-	}
-
-	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 {
@@ -379,10 +337,10 @@ func (db *DB) migrate(drv Driver) 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 {
@@ -397,7 +355,7 @@ func (db *DB) migrate(drv Driver) error {
 
 	// automatically update schema file, silence errors
 	if db.AutoDumpSchema {
-		_ = db.dumpSchema(drv)
+		_ = db.DumpSchema()
 	}
 
 	return nil
@@ -414,104 +372,111 @@ 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, err
+	}
+
+	if migrationsTableExists {
+		appliedMigrations, err = drv.SelectMigrations(sqlDB, -1)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// find filesystem migrations
+	files, err := fs.ReadDir(db.FS, filepath.Clean(db.MigrationsDir))
 	if err != nil {
-		return nil, fmt.Errorf("%w `%s`", ErrMigrationDirNotFound, dir)
+		return nil, fmt.Errorf("%w `%s`", ErrMigrationDirNotFound, db.MigrationsDir)
 	}
 
-	matches := []string{}
+	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
 func (db *DB) Rollback() error {
-	drv, err := db.GetDriver()
+	drv, err := db.Driver()
 	if err != nil {
 		return err
 	}
 
-	if db.WaitBefore {
-		err := db.wait(drv)
-		if err != nil {
-			return err
-		}
-	}
-
 	sqlDB, err := db.openDatabaseForMigration(drv)
 	if err != nil {
 		return err
 	}
 	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 {
@@ -519,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 {
@@ -536,7 +501,7 @@ func (db *DB) Rollback() error {
 
 	// automatically update schema file, silence errors
 	if db.AutoDumpSchema {
-		_ = db.dumpSchema(drv)
+		_ = db.DumpSchema()
 	}
 
 	return nil
@@ -544,12 +509,7 @@ func (db *DB) Rollback() error {
 
 // Status shows the status of all migrations
 func (db *DB) Status(quiet bool) (int, error) {
-	drv, err := db.GetDriver()
-	if err != nil {
-		return -1, err
-	}
-
-	results, err := db.CheckMigrationsStatus(drv)
+	results, err := db.FindMigrations()
 	if err != nil {
 		return -1, err
 	}
@@ -559,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)
@@ -578,51 +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(drv Driver) ([]StatusResult, error) {
-	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/driver.go b/pkg/dbmate/driver.go
index 730a29b..4ad2b74 100644
--- a/pkg/dbmate/driver.go
+++ b/pkg/dbmate/driver.go
@@ -26,8 +26,8 @@ type Driver interface {
 // DriverConfig holds configuration passed to driver constructors
 type DriverConfig struct {
 	DatabaseURL         *url.URL
-	MigrationsTableName string
 	Log                 io.Writer
+	MigrationsTableName string
 }
 
 // DriverFunc represents a driver constructor
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
diff --git a/pkg/driver/clickhouse/clickhouse.go b/pkg/driver/clickhouse/clickhouse.go
index 2059cbe..c785002 100644
--- a/pkg/driver/clickhouse/clickhouse.go
+++ b/pkg/driver/clickhouse/clickhouse.go
@@ -233,7 +233,7 @@ func (drv *Driver) DatabaseExists() (bool, error) {
 // MigrationsTableExists checks if the schema_migrations table exists
 func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) {
 	exists := false
-	err := db.QueryRow("EXISTS TABLE $1", drv.quotedMigrationsTableName()).
+	err := db.QueryRow(fmt.Sprintf("EXISTS TABLE %s", drv.quotedMigrationsTableName())).
 		Scan(&exists)
 	if err == sql.ErrNoRows {
 		return false, nil
diff --git a/pkg/driver/sqlite/sqlite.go b/pkg/driver/sqlite/sqlite.go
index 0009710..d4150d3 100644
--- a/pkg/driver/sqlite/sqlite.go
+++ b/pkg/driver/sqlite/sqlite.go
@@ -131,7 +131,7 @@ func (drv *Driver) schemaMigrationsDump(db *sql.DB) ([]byte, error) {
 // DumpSchema returns the current database schema
 func (drv *Driver) DumpSchema(db *sql.DB) ([]byte, error) {
 	path := ConnectionString(drv.databaseURL)
-	schema, err := dbutil.RunCommand("sqlite3", path, ".schema")
+	schema, err := dbutil.RunCommand("sqlite3", path, ".schema --nosys")
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/pkg/dbmate/db_test.go b/pkg/dbmate/db_test.go
index b3a0a79..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"
@@ -52,21 +53,21 @@ func TestNew(t *testing.T) {
 func TestGetDriver(t *testing.T) {
 	t.Run("missing URL", func(t *testing.T) {
 		db := dbmate.New(nil)
-		drv, err := db.GetDriver()
+		drv, err := db.Driver()
 		require.Nil(t, drv)
 		require.EqualError(t, err, "invalid url, have you set your --url flag or DATABASE_URL environment variable?")
 	})
 
 	t.Run("missing schema", func(t *testing.T) {
 		db := dbmate.New(dbutil.MustParseURL("//hi"))
-		drv, err := db.GetDriver()
+		drv, err := db.Driver()
 		require.Nil(t, drv)
 		require.EqualError(t, err, "invalid url, have you set your --url flag or DATABASE_URL environment variable?")
 	})
 
 	t.Run("invalid driver", func(t *testing.T) {
 		db := dbmate.New(dbutil.MustParseURL("foo://bar"))
-		drv, err := db.GetDriver()
+		drv, err := db.Driver()
 		require.EqualError(t, err, "unsupported driver: foo")
 		require.Nil(t, drv)
 	})
@@ -260,7 +261,7 @@ func TestMigrate(t *testing.T) {
 	for _, u := range testURLs() {
 		t.Run(u.Scheme, func(t *testing.T) {
 			db := newTestDB(t, u)
-			drv, err := db.GetDriver()
+			drv, err := db.Driver()
 			require.NoError(t, err)
 
 			// drop and recreate database
@@ -295,7 +296,7 @@ func TestUp(t *testing.T) {
 	for _, u := range testURLs() {
 		t.Run(u.Scheme, func(t *testing.T) {
 			db := newTestDB(t, u)
-			drv, err := db.GetDriver()
+			drv, err := db.Driver()
 			require.NoError(t, err)
 
 			// drop database
@@ -328,14 +329,21 @@ func TestRollback(t *testing.T) {
 	for _, u := range testURLs() {
 		t.Run(u.Scheme, func(t *testing.T) {
 			db := newTestDB(t, u)
-			drv, err := db.GetDriver()
+			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,11 +377,11 @@ 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)
-			drv, err := db.GetDriver()
+			drv, err := db.Driver()
 			require.NoError(t, err)
 
 			// drop, recreate, and migrate database
@@ -388,7 +396,7 @@ func TestStatus(t *testing.T) {
 			defer dbutil.MustClose(sqlDB)
 
 			// two pending
-			results, err := db.CheckMigrationsStatus(drv)
+			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(drv)
+			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(drv)
+			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())
 }
diff --git a/pkg/driver/clickhouse/clickhouse_test.go b/pkg/driver/clickhouse/clickhouse_test.go
index 8f1e53e..28698d1 100644
--- a/pkg/driver/clickhouse/clickhouse_test.go
+++ b/pkg/driver/clickhouse/clickhouse_test.go
@@ -14,7 +14,7 @@ import (
 
 func testClickHouseDriver(t *testing.T) *Driver {
 	u := dbutil.MustParseURL(os.Getenv("CLICKHOUSE_TEST_URL"))
-	drv, err := dbmate.New(u).GetDriver()
+	drv, err := dbmate.New(u).Driver()
 	require.NoError(t, err)
 
 	return drv.(*Driver)
@@ -40,7 +40,7 @@ func prepTestClickHouseDB(t *testing.T) *sql.DB {
 
 func TestGetDriver(t *testing.T) {
 	db := dbmate.New(dbutil.MustParseURL("clickhouse://"))
-	drvInterface, err := db.GetDriver()
+	drvInterface, err := db.Driver()
 	require.NoError(t, err)
 
 	// driver should have URL and default migrations table set
@@ -203,6 +203,11 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) {
 			"code: 60, message: Table dbmate_test.schema_migrations doesn't exist",
 		)
 
+		// use driver function to check the same as above
+		exists, err := drv.MigrationsTableExists(db)
+		require.NoError(t, err)
+		require.Equal(t, false, exists)
+
 		// create table
 		err = drv.CreateMigrationsTable(db)
 		require.NoError(t, err)
@@ -211,6 +216,11 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) {
 		err = db.QueryRow("select count(*) from schema_migrations").Scan(&count)
 		require.NoError(t, err)
 
+		// use driver function to check the same as above
+		exists, err = drv.MigrationsTableExists(db)
+		require.NoError(t, err)
+		require.Equal(t, true, exists)
+
 		// create table should be idempotent
 		err = drv.CreateMigrationsTable(db)
 		require.NoError(t, err)
@@ -232,6 +242,11 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) {
 			"code: 60, message: Table dbmate_test.testMigrations doesn't exist",
 		)
 
+		// use driver function to check the same as above
+		exists, err := drv.MigrationsTableExists(db)
+		require.NoError(t, err)
+		require.Equal(t, false, exists)
+
 		// create table
 		err = drv.CreateMigrationsTable(db)
 		require.NoError(t, err)
@@ -240,6 +255,11 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) {
 		err = db.QueryRow("select count(*) from \"testMigrations\"").Scan(&count)
 		require.NoError(t, err)
 
+		// use driver function to check the same as above
+		exists, err = drv.MigrationsTableExists(db)
+		require.NoError(t, err)
+		require.Equal(t, true, exists)
+
 		// create table should be idempotent
 		err = drv.CreateMigrationsTable(db)
 		require.NoError(t, err)
diff --git a/pkg/driver/mysql/mysql_test.go b/pkg/driver/mysql/mysql_test.go
index 46d3f51..70a3113 100644
--- a/pkg/driver/mysql/mysql_test.go
+++ b/pkg/driver/mysql/mysql_test.go
@@ -14,7 +14,7 @@ import (
 
 func testMySQLDriver(t *testing.T) *Driver {
 	u := dbutil.MustParseURL(os.Getenv("MYSQL_TEST_URL"))
-	drv, err := dbmate.New(u).GetDriver()
+	drv, err := dbmate.New(u).Driver()
 	require.NoError(t, err)
 
 	return drv.(*Driver)
@@ -40,7 +40,7 @@ func prepTestMySQLDB(t *testing.T) *sql.DB {
 
 func TestGetDriver(t *testing.T) {
 	db := dbmate.New(dbutil.MustParseURL("mysql://"))
-	drvInterface, err := db.GetDriver()
+	drvInterface, err := db.Driver()
 	require.NoError(t, err)
 
 	// driver should have URL and default migrations table set
diff --git a/pkg/driver/postgres/postgres_test.go b/pkg/driver/postgres/postgres_test.go
index 3b699e5..fd29f00 100644
--- a/pkg/driver/postgres/postgres_test.go
+++ b/pkg/driver/postgres/postgres_test.go
@@ -15,7 +15,7 @@ import (
 
 func testPostgresDriver(t *testing.T) *Driver {
 	u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL"))
-	drv, err := dbmate.New(u).GetDriver()
+	drv, err := dbmate.New(u).Driver()
 	require.NoError(t, err)
 
 	return drv.(*Driver)
@@ -41,7 +41,7 @@ func prepTestPostgresDB(t *testing.T) *sql.DB {
 
 func TestGetDriver(t *testing.T) {
 	db := dbmate.New(dbutil.MustParseURL("postgres://"))
-	drvInterface, err := db.GetDriver()
+	drvInterface, err := db.Driver()
 	require.NoError(t, err)
 
 	// driver should have URL and default migrations table set
diff --git a/pkg/driver/sqlite/sqlite_test.go b/pkg/driver/sqlite/sqlite_test.go
index 858957b..010373d 100644
--- a/pkg/driver/sqlite/sqlite_test.go
+++ b/pkg/driver/sqlite/sqlite_test.go
@@ -16,7 +16,7 @@ import (
 
 func testSQLiteDriver(t *testing.T) *Driver {
 	u := dbutil.MustParseURL(os.Getenv("SQLITE_TEST_URL"))
-	drv, err := dbmate.New(u).GetDriver()
+	drv, err := dbmate.New(u).Driver()
 	require.NoError(t, err)
 
 	return drv.(*Driver)
@@ -42,7 +42,7 @@ func prepTestSQLiteDB(t *testing.T) *sql.DB {
 
 func TestGetDriver(t *testing.T) {
 	db := dbmate.New(dbutil.MustParseURL("sqlite://"))
-	drvInterface, err := db.GetDriver()
+	drvInterface, err := db.Driver()
 	require.NoError(t, err)
 
 	// driver should have URL and default migrations table set
@@ -180,22 +180,29 @@ func TestSQLiteDumpSchema(t *testing.T) {
 	err = drv.InsertMigration(db, "abc2")
 	require.NoError(t, err)
 
+	// create a table that will trigger `sqlite_sequence` system table
+	_, err = db.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT)")
+	require.NoError(t, err)
+
 	// DumpSchema should return schema
 	schema, err := drv.DumpSchema(db)
 	require.NoError(t, err)
+	require.Contains(t, string(schema), "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT)")
 	require.Contains(t, string(schema), "CREATE TABLE IF NOT EXISTS \"test_migrations\"")
 	require.Contains(t, string(schema), ");\n-- Dbmate schema migrations\n"+
 		"INSERT INTO \"test_migrations\" (version) VALUES\n"+
 		"  ('abc1'),\n"+
 		"  ('abc2');\n")
 
+	// sqlite_* tables should not be present in the dump (.schema --nosys)
+	require.NotContains(t, string(schema), "sqlite_")
+
 	// DumpSchema should return error if command fails
 	drv.databaseURL = dbutil.MustParseURL(".")
 	schema, err = drv.DumpSchema(db)
 	require.Nil(t, schema)
 	require.Error(t, err)
-	require.EqualError(t, err, "Error: unable to open database \"/.\": "+
-		"unable to open database file")
+	require.EqualError(t, err, "Error: unable to open database \"/.\": unable to open database file")
 }
 
 func TestSQLiteDatabaseExists(t *testing.T) {