Repository: ariga/atlas
Pull request #936 (2022-07-12 07:38:00)

sql/migrate: introduce Snapshoter interface
This is a most basic implementation, that will only accept taking a snapshot, if a database is clean.

The drivers handle this differently:
- MySQL will consider a clean state if in case of a schema bound connection there are not tables, and in realm-bound connection there are not schemas.

- Postgres will consider a connection schema-bound, if it is connected to any other schema than `public`, or if the connection string explicitly requested `public` to be the connected schema (by using `search_path`). In the former, the database is clean of there are not tables in public and no other schemas. In the latter only the tables in public are taken into account.

- SQLite is not allowed to have any tables.


Patch:
diff --git a/cmd/atlas/internal/ci/ci.go b/cmd/atlas/internal/ci/ci.go
index 29440595..3399a102 100644
--- a/cmd/atlas/internal/ci/ci.go
+++ b/cmd/atlas/internal/ci/ci.go
@@ -181,13 +181,17 @@ func (d *DevLoader) LoadChanges(ctx context.Context, base, files []migrate.File)
 		return nil, fmt.Errorf("acquiring database lock: %w", err)
 	}
 	defer unlock()
-	// We need an empty database state to reliably replay the migration directory.
-	if err := migrate.IsClean(ctx, d.Dev.Driver); err != nil {
-		return nil, err
-	}
 	// Clean up after ourselves.
+	snap, ok := d.Dev.Driver.(migrate.Snapshoter)
+	if !ok {
+		return nil, errors.New("driver does not implement migrate.Snapshoter")
+	}
+	restore, err := snap.Snapshot(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("taking database snapshot: %w", err)
+	}
 	defer func() {
-		if err2 := migrate.Clean(ctx, d.Dev.Driver); err2 != nil {
+		if err2 := restore(ctx); err2 != nil {
 			if err != nil {
 				err = fmt.Errorf("%w: %v", err, err2)
 				return
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 01b2c508..766e58d3 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -937,6 +937,12 @@ func (dryRunDriver) Lock(context.Context, string, time.Duration) (schema.UnlockF
 	return func() error { return nil }, nil
 }
 
+// Snapshot implements the migrate.Snapshoter interface.
+func (dryRunDriver) Snapshot(context.Context) (migrate.RestoreFunc, error) {
+	// We dry-run, we don't execute anything. Snapshotting not required.
+	return func(context.Context) error { return nil }, nil
+}
+
 // WriteRevision overrides the wrapped migrate.RevisionReadWriter to not saved any changes to revisions.
 func (dryRunRevisions) WriteRevision(context.Context, *migrate.Revision) error {
 	return nil
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 216bbb3c..7c50a90f 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -332,6 +332,8 @@ var (
 	ErrNoPendingFiles = errors.New("sql/migrate: execute: nothing to do")
 	// ErrLockUnsupported is returned when the given driver does not implement the schema.Locker interface.
 	ErrLockUnsupported = errors.New("sql/migrate: driver does not support locking")
+	// ErrSnapshotUnsupported is returned when the given driver does not implement the Snapshoter interface.
+	ErrSnapshotUnsupported = errors.New("sql/migrate: driver does not support taking a database snapshot")
 )
 
 // NewExecutor creates a new Executor with default values. // TODO(masseelch): Operator Version and other Meta
@@ -349,6 +351,10 @@ func NewExecutor(drv Driver, dir Dir, rrw RevisionReadWriter, opts ...ExecutorOp
 	if _, ok := drv.(schema.Locker); !ok {
 		return nil, ErrLockUnsupported
 	}
+	// Check if the driver implements Snapshoter interface.
+	if _, ok := drv.(Snapshoter); !ok {
+		return nil, ErrSnapshotUnsupported
+	}
 	// Check if the Dir implements Scanner interface.
 	if _, ok := dir.(Scanner); !ok {
 		return nil, errors.New("sql/migrate: execute: no scanner available")
@@ -552,16 +558,6 @@ func (e *Executor) ExecuteN(ctx context.Context, n int) error {
 	return err
 }
 
-// NotCleanError is returned when the connected dev-db is not in a clean state (aka it has schemas and tables).
-// This check is done to ensure no data is lost by overriding it when working on the dev-dn.
-type NotCleanError struct {
-	Reason string // reason why the database is considered not clean
-}
-
-func (e NotCleanError) Error() string {
-	return "sql/migrate: connected database is not clean: " + e.Reason
-}
-
 // ReadState implements the StateReader interface.
 //
 // It does so by calling Execute and inspecting the database thereafter.
@@ -571,13 +567,13 @@ func (e *Executor) ReadState(ctx context.Context) (realm *schema.Realm, err erro
 		return nil, fmt.Errorf("sql/migrate: acquiring database lock: %w", err)
 	}
 	defer unlock()
-	// We need an empty database state to reliably replay the migration directory.
-	if err := IsClean(ctx, e.drv); err != nil {
-		return nil, fmt.Errorf("sql/migrate: checking database state: %w", err)
-	}
 	// Clean up after ourselves.
+	restore, err := e.drv.(Snapshoter).Snapshot(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("sql/migrate: taking database snapshot: %w", err)
+	}
 	defer func() {
-		if err2 := Clean(ctx, e.drv); err2 != nil {
+		if err2 := restore(ctx); err2 != nil {
 			err = wrap(err2, err)
 		}
 	}()
@@ -590,44 +586,27 @@ func (e *Executor) ReadState(ctx context.Context) (realm *schema.Realm, err erro
 	return
 }
 
-// IsClean checks if the given driver operates on a clean database.
-func IsClean(ctx context.Context, drv Driver) error {
-	if c, ok := drv.(interface {
-		// The IsClean method can be added to a Driver to override how to
-		// determine if a connected database is in a clean state.
-		// This interface exists solely to support SQLite flavors.
-		IsClean(context.Context) error
-	}); ok {
-		return c.IsClean(ctx)
-	}
-	realm, err := drv.InspectRealm(ctx, nil)
-	if err != nil {
-		return err
-	}
-	if len(realm.Schemas) > 0 {
-		return NotCleanError{fmt.Sprintf("found schema %q", realm.Schemas[0].Name)}
+type (
+	// Snapshoter wraps the Snapshot method.
+	Snapshoter interface {
+		// Snapshot takes a snapshot of the current database state and returns a function that can be called to restore
+		// that state. Snapshot should return an error, if the current state can not be restored completely, e.g. if
+		// there is a table already containing some rows.
+		Snapshot(context.Context) (RestoreFunc, error)
 	}
-	return nil
-}
 
-// Clean cleans the database the given driver is connected to.
-func Clean(ctx context.Context, drv Driver) error {
-	if e, ok := drv.(interface {
-		// The Clean method can be added to a Driver to change how to clean a database.
-		// This interface exists solely to support SQLite flavors.
-		Clean(context.Context) error
-	}); ok {
-		return e.Clean(ctx)
-	}
-	realm, err := drv.InspectRealm(ctx, nil)
-	if err != nil {
-		return err
-	}
-	del := make([]schema.Change, len(realm.Schemas))
-	for i, s := range realm.Schemas {
-		del[i] = &schema.DropSchema{S: s, Extra: []schema.Clause{&schema.IfExists{}}}
+	// RestoreFunc is returned by the Snapshoter to explicitly restore the database state.
+	RestoreFunc func(context.Context) error
+
+	// NotCleanError is returned when the connected dev-db is not in a clean state (aka it has schemas and tables).
+	// This check is done to ensure no data is lost by overriding it when working on the dev-db.
+	NotCleanError struct {
+		Reason string // reason why the database is considered not clean
 	}
-	return drv.ApplyChanges(ctx, del)
+)
+
+func (e NotCleanError) Error() string {
+	return "sql/migrate: connected database is not clean: " + e.Reason
 }
 
 // NopRevisionReadWriter is a RevisionsReadWriter that does nothing.
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 99bf0764..762caa9b 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -117,6 +117,51 @@ func (d *Driver) Lock(ctx context.Context, name string, timeout time.Duration) (
 	}, nil
 }
 
+// Snapshot implements migrate.Snapshoter.
+func (d *Driver) Snapshot(ctx context.Context) (migrate.RestoreFunc, error) {
+	// If the connection is bound to a schema, we can restore the state if the schema has no tables.
+	s, err := d.InspectSchema(ctx, "", nil)
+	if err != nil && !schema.IsNotExistError(err) {
+		return nil, err
+	}
+	// If a schema was found, it has to have no tables attached to be considered clean.
+	if s != nil {
+		if len(s.Tables) > 0 {
+			return nil, migrate.NotCleanError{Reason: fmt.Sprintf("found table %q in schema %q", s.Tables[0].Name, s.Name)}
+		}
+		return func(ctx context.Context) error {
+			current, err := d.InspectSchema(ctx, s.Name, nil)
+			if err != nil {
+				return err
+			}
+			changes, err := d.SchemaDiff(current, s)
+			if err != nil {
+				return err
+			}
+			return d.ApplyChanges(ctx, changes)
+		}, nil
+	}
+	// Otherwise, the database can not have any schema.
+	realm, err := d.InspectRealm(ctx, nil)
+	if err != nil {
+		return nil, err
+	}
+	if len(realm.Schemas) > 0 {
+		return nil, migrate.NotCleanError{Reason: fmt.Sprintf("found schema %q", realm.Schemas[0].Name)}
+	}
+	return func(ctx context.Context) error {
+		current, err := d.InspectRealm(ctx, nil)
+		if err != nil {
+			return err
+		}
+		changes, err := d.RealmDiff(current, realm)
+		if err != nil {
+			return err
+		}
+		return d.ApplyChanges(ctx, changes)
+	}, nil
+}
+
 func acquire(ctx context.Context, conn schema.ExecQuerier, name string, timeout time.Duration) error {
 	rows, err := conn.QueryContext(ctx, "SELECT GET_LOCK(?, ?)", name, int(timeout.Seconds()))
 	if err != nil {
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index f8323037..ef567323 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -6,6 +6,7 @@ package postgres
 
 import (
 	"context"
+	"database/sql"
 	"errors"
 	"fmt"
 	"hash/fnv"
@@ -27,6 +28,7 @@ type (
 		schema.Differ
 		schema.Inspector
 		migrate.PlanApplier
+		schema string // the schema given in the `search_path` parameter (if given)
 	}
 
 	// database connection and its information.
@@ -46,12 +48,35 @@ const DriverName = "postgres"
 func init() {
 	sqlclient.Register(
 		DriverName,
-		sqlclient.DriverOpener(Open),
+		sqlclient.OpenerFunc(opener),
+		sqlclient.RegisterDriverOpener(Open),
 		sqlclient.RegisterCodec(MarshalHCL, EvalHCL),
 		sqlclient.RegisterURLParser(parser{}),
 	)
 }
 
+func opener(_ context.Context, u *url.URL) (*sqlclient.Client, error) {
+	ur := parser{}.ParseURL(u)
+	db, err := sql.Open(DriverName, ur.DSN)
+	if err != nil {
+		return nil, err
+	}
+	drv, err := Open(db)
+	if err != nil {
+		if cerr := db.Close(); cerr != nil {
+			err = fmt.Errorf("%w: %v", err, cerr)
+		}
+		return nil, err
+	}
+	drv.(*Driver).schema = ur.Schema
+	return &sqlclient.Client{
+		Name:   DriverName,
+		DB:     db,
+		URL:    ur,
+		Driver: drv,
+	}, nil
+}
+
 // Open opens a new PostgreSQL driver.
 func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 	c := conn{ExecQuerier: db}
@@ -134,6 +159,59 @@ func (d *Driver) Lock(ctx context.Context, name string, timeout time.Duration) (
 	}, nil
 }
 
+// Snapshot implements migrate.Snapshoter.
+func (d *Driver) Snapshot(ctx context.Context) (migrate.RestoreFunc, error) {
+	// Postgres will only then be considered bound to a schema if the `search_path` was given.
+	// In all other cases, the connection is considered bound to the realm.
+	if d.schema != "" {
+		s, err := d.InspectSchema(ctx, d.schema, nil)
+		if err != nil {
+			return nil, err
+		}
+		if len(s.Tables) > 0 {
+			return nil, migrate.NotCleanError{Reason: fmt.Sprintf("found table %q in connected schema", s.Tables[0].Name)}
+		}
+		return func(ctx context.Context) error {
+			current, err := d.InspectSchema(ctx, s.Name, nil)
+			if err != nil {
+				return err
+			}
+			changes, err := d.SchemaDiff(current, s)
+			if err != nil {
+				return err
+			}
+			return d.ApplyChanges(ctx, changes)
+		}, nil
+	}
+	// Not bound to a schema.
+	realm, err := d.InspectRealm(ctx, nil)
+	if err != nil {
+		return nil, err
+	}
+	restore := func(ctx context.Context) error {
+		current, err := d.InspectRealm(ctx, nil)
+		if err != nil {
+			return err
+		}
+		changes, err := d.RealmDiff(current, realm)
+		if err != nil {
+			return err
+		}
+		return d.ApplyChanges(ctx, changes)
+	}
+	// Postgres is considered clean, if there are no schemas or the public schema has no tables.
+	if len(realm.Schemas) == 0 {
+		return restore, nil
+	}
+	if s, ok := realm.Schema("public"); len(realm.Schemas) == 1 && ok {
+		if len(s.Tables) > 0 {
+			return nil, migrate.NotCleanError{Reason: fmt.Sprintf("found table %q in schema %q", s.Tables[0].Name, s.Name)}
+		}
+		return restore, nil
+	}
+	return nil, migrate.NotCleanError{Reason: fmt.Sprintf("found schema %q", realm.Schemas[0].Name)}
+}
+
 func acquire(ctx context.Context, conn schema.ExecQuerier, id uint32, timeout time.Duration) error {
 	switch {
 	// With timeout (context-based).
diff --git a/sql/sqlclient/client.go b/sql/sqlclient/client.go
index ac922908..9d587321 100644
--- a/sql/sqlclient/client.go
+++ b/sql/sqlclient/client.go
@@ -273,7 +273,7 @@ func RegisterDriverOpener(open func(schema.ExecQuerier) (migrate.Driver, error))
 
 // DriverOpener is a helper Opener creator for sharing between all drivers.
 func DriverOpener(open func(schema.ExecQuerier) (migrate.Driver, error)) Opener {
-	return OpenerFunc(func(ctx context.Context, u *url.URL) (*Client, error) {
+	return OpenerFunc(func(_ context.Context, u *url.URL) (*Client, error) {
 		v, ok := drivers.Load(u.Scheme)
 		if !ok {
 			return nil, fmt.Errorf("sql/sqlclient: unexpected missing opener %q", u.Scheme)
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index 5c09b281..292663fd 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -89,31 +89,28 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 	}, nil
 }
 
-// IsClean implements the inlined IsClean interface to override what to consider a clean database.
-func (d *Driver) IsClean(ctx context.Context) error {
+// Snapshot implements migrate.Snapshoter.
+func (d *Driver) Snapshot(ctx context.Context) (migrate.RestoreFunc, error) {
 	r, err := d.InspectRealm(ctx, nil)
 	if err != nil {
-		return err
+		return nil, err
 	}
 	if !(r == nil || (len(r.Schemas) == 1 && r.Schemas[0].Name == mainFile && len(r.Schemas[0].Tables) == 0)) {
-		return migrate.NotCleanError{Reason: fmt.Sprintf("found table %q", r.Schemas[0].Tables[0].Name)}
+		return nil, migrate.NotCleanError{Reason: fmt.Sprintf("found table %q", r.Schemas[0].Tables[0].Name)}
 	}
-	return nil
-}
-
-// Clean implements the inlined Clean interface to override the "emptying" behavior.
-func (d *Driver) Clean(ctx context.Context) error {
-	for _, stmt := range []string{
-		"PRAGMA writable_schema = 1;",
-		"DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');",
-		"PRAGMA writable_schema = 0;",
-		"VACUUM;",
-	} {
-		if _, err := d.ExecContext(ctx, stmt); err != nil {
-			return err
+	return func(ctx context.Context) error {
+		for _, stmt := range []string{
+			"PRAGMA writable_schema = 1;",
+			"DELETE FROM sqlite_master WHERE type IN ('table', 'index', 'trigger');",
+			"PRAGMA writable_schema = 0;",
+			"VACUUM;",
+		} {
+			if _, err := d.ExecContext(ctx, stmt); err != nil {
+				return err
+			}
 		}
-	}
-	return nil
+		return nil
+	}, nil
 }
 
 // Lock implements the schema.Locker interface.


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate_test.go b/cmd/atlas/internal/cmdapi/migrate_test.go
index 2e59e84b..14c8ce5b 100644
--- a/cmd/atlas/internal/cmdapi/migrate_test.go
+++ b/cmd/atlas/internal/cmdapi/migrate_test.go
@@ -472,6 +472,10 @@ func (d *sqliteLockerDriver) Lock(context.Context, string, time.Duration) (schem
 	return func() error { return nil }, errLock
 }
 
+func (d *sqliteLockerDriver) Snapshot(ctx context.Context) (migrate.RestoreFunc, error) {
+	return d.Driver.(migrate.Snapshoter).Snapshot(ctx)
+}
+
 func countFiles(t *testing.T, p string) int {
 	files, err := os.ReadDir(p)
 	require.NoError(t, err)
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index f577175b..ab76225d 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -562,6 +562,39 @@ schema "test" {
 	})
 }
 
+func TestMySQL_Snapshot(t *testing.T) {
+	myRun(t, func(t *myTest) {
+		db, err := sql.Open("mysql", fmt.Sprintf("root:pass@tcp(localhost:%d)/", t.port))
+		require.NoError(t, err)
+		t.Cleanup(func() {
+			require.NoError(t, db.Close())
+		})
+		drv, err := mysql.Open(db)
+		require.NoError(t, err)
+
+		_, err = drv.(migrate.Snapshoter).Snapshot(context.Background())
+		require.ErrorAs(t, err, &migrate.NotCleanError{})
+
+		r, err := t.driver().InspectRealm(context.Background(), nil)
+		require.NoError(t, err)
+		restore, err := t.driver().(migrate.Snapshoter).Snapshot(context.Background())
+		require.NoError(t, err) // connected to test schema
+		t.migrate(&schema.AddTable{T: schema.NewTable("my_table").AddColumns(
+			schema.NewIntColumn("col_1", "integer").SetNull(true),
+			schema.NewIntColumn("col_2", "bigint"),
+		)})
+		t.Cleanup(func() {
+			t.dropTables("my_table")
+		})
+		require.NoError(t, restore(context.Background()))
+		r1, err := t.driver().InspectRealm(context.Background(), nil)
+		require.NoError(t, err)
+		diff, err := t.driver().RealmDiff(r1, r)
+		require.NoError(t, err)
+		require.Zero(t, diff)
+	})
+}
+
 func TestMySQL_CLI(t *testing.T) {
 	h := `
 			schema "test" {
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 0e082f30..85d8ceae 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -16,6 +16,7 @@ import (
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlclient"
 
 	"entgo.io/ent/dialect"
 	_ "github.com/lib/pq"
@@ -558,6 +559,46 @@ func (t *pgTest) applyRealmHcl(spec string) {
 	require.NoError(t, err)
 }
 
+func TestPostgres_Snapshot(t *testing.T) {
+	pgRun(t, func(t *pgTest) {
+		client, err := sqlclient.Open(context.Background(), fmt.Sprintf("postgres://postgres:pass@localhost:%d/test?sslmode=disable&search_path=another", t.port))
+		require.NoError(t, err)
+
+		_, err = client.ExecContext(context.Background(), "CREATE SCHEMA another")
+		require.NoError(t, err)
+		t.Cleanup(func() {
+			_, err = client.ExecContext(context.Background(), "DROP SCHEMA IF EXISTS another")
+			require.NoError(t, client.Close())
+		})
+		drv := client.Driver
+
+		_, err = t.driver().(migrate.Snapshoter).Snapshot(context.Background())
+		require.ErrorAs(t, err, &migrate.NotCleanError{})
+
+		r, err := drv.InspectRealm(context.Background(), nil)
+		require.NoError(t, err)
+		restore, err := drv.(migrate.Snapshoter).Snapshot(context.Background())
+		require.NoError(t, err) // connected to test schema
+		require.NoError(t, drv.ApplyChanges(context.Background(), []schema.Change{
+			&schema.AddTable{T: schema.NewTable("my_table").
+				AddColumns(
+					schema.NewIntColumn("col_1", "integer").SetNull(true),
+					schema.NewIntColumn("col_2", "bigint"),
+				),
+			},
+		}))
+		t.Cleanup(func() {
+			t.dropTables("my_table")
+		})
+		require.NoError(t, restore(context.Background()))
+		r1, err := drv.InspectRealm(context.Background(), nil)
+		require.NoError(t, err)
+		diff, err := drv.RealmDiff(r1, r)
+		require.NoError(t, err)
+		require.Zero(t, diff)
+	})
+}
+
 func TestPostgres_CLI(t *testing.T) {
 	h := `
 			schema "public" {
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index e53f3c1e..431b2b07 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -204,7 +204,6 @@ func TestHash_UnmarshalText(t *testing.T) {
 	require.Equal(t, h, ac)
 }
 
-// Deprecated: GlobStateReader will be removed once the Executor is functional.
 func TestExecutor_ReadState(t *testing.T) {
 	ctx := context.Background()
 	d, err := migrate.NewLocalDir("testdata/migrate")
@@ -241,22 +240,6 @@ func TestExecutor_ReadState(t *testing.T) {
 	require.Equal(t, 3, drv.lockCounter)
 	require.Equal(t, 3, drv.unlockCounter)
 	require.True(t, drv.released())
-
-	// Works.
-	edrv := &emptyMockDriver{drv}
-	ex, err = migrate.NewExecutor(edrv, d, migrate.NopRevisionReadWriter{})
-	require.NoError(t, err)
-	_, err = ex.ReadState(ctx)
-	require.NoError(t, err)
-	require.Equal(t, []schema.Change{
-		&schema.DropSchema{
-			S:     &schema.Schema{Name: "schema"},
-			Extra: []schema.Clause{&schema.IfExists{}},
-		},
-	}, edrv.applied)
-	require.Equal(t, 5, drv.lockCounter)
-	require.Equal(t, 5, drv.unlockCounter)
-	require.True(t, drv.released())
 }
 
 func TestLocalDir(t *testing.T) {
@@ -503,6 +486,16 @@ func (m *mockDriver) ApplyChanges(_ context.Context, changes []schema.Change) er
 	m.applied = changes
 	return nil
 }
+func (m *mockDriver) Snapshot(context.Context) (migrate.RestoreFunc, error) {
+	if len(m.realm.Schemas) > 0 {
+		return nil, migrate.NotCleanError{}
+	}
+	realm := m.realm
+	return func(context.Context) error {
+		m.realm = realm
+		return nil
+	}, nil
+}
 func (m *lockMockDriver) Lock(_ context.Context, name string, _ time.Duration) (schema.UnlockFunc, error) {
 	if _, ok := m.locks[name]; ok {
 		return nil, errors.New("lockErr")
@@ -521,9 +514,6 @@ func (m *lockMockDriver) Lock(_ context.Context, name string, _ time.Duration) (
 func (m *lockMockDriver) released() bool {
 	return len(m.locks) == 0
 }
-func (m *emptyMockDriver) IsClean(context.Context) error {
-	return nil
-}
 
 type mockRevisionReadWriter migrate.Revisions