diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5d0aaf2 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +start-postgres: + podman stop dbump-postgres + podman rm dbump-postgres + podman run --name dbump-postgres -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres -d postgres + +test-pgx: + cd dbump_pgx && go test -v -race -shuffle=on ./... diff --git a/dbump_pgx/go.mod b/dbump_pgx/go.mod index d5697a2..558ce07 100644 --- a/dbump_pgx/go.mod +++ b/dbump_pgx/go.mod @@ -3,6 +3,6 @@ module github.com/cristalhq/dbump/dbump_pgx go 1.16 require ( - github.com/cristalhq/dbump v0.2.0 + github.com/cristalhq/dbump v0.7.1 github.com/jackc/pgx/v4 v4.16.1 ) diff --git a/dbump_pgx/go.sum b/dbump_pgx/go.sum index 2e9aa60..d78c4de 100644 --- a/dbump_pgx/go.sum +++ b/dbump_pgx/go.sum @@ -5,8 +5,8 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/cristalhq/dbump v0.2.0 h1:zwk0d4UFBGtXswYh6lAZ5KDCEgnn9px9HMzLCUQSDso= -github.com/cristalhq/dbump v0.2.0/go.mod h1:rAjULuStbuNPCLrJT62Eu7Sp/2gVt/4URUvsnPK1yFA= +github.com/cristalhq/dbump v0.7.1 h1:zKXPyU1YR+2FFIs5EMo4pBWYVsI0GD+cV+JNJ6kyl14= +github.com/cristalhq/dbump v0.7.1/go.mod h1:rAjULuStbuNPCLrJT62Eu7Sp/2gVt/4URUvsnPK1yFA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -127,6 +127,8 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -135,6 +137,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -147,6 +150,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/dbump_pgx/pgx.go b/dbump_pgx/pgx.go index 8df482a..e7f7b59 100644 --- a/dbump_pgx/pgx.go +++ b/dbump_pgx/pgx.go @@ -2,6 +2,8 @@ package dbump_pgx import ( "context" + "errors" + "fmt" "github.com/cristalhq/dbump" "github.com/jackc/pgx/v4" @@ -14,58 +16,107 @@ var _ dbump.Migrator = &Migrator{} // Migrator to migrate Postgres. type Migrator struct { - conn *pgx.Conn + conn *pgx.Conn + tx pgx.Tx + tableName string +} + +// Config for the migrator. +type Config struct { + // Schema for the dbump version table. Default is empty which means "public" schema. + Schema string + // Table for the dbump version table. Default is empty which means "_dbump_log" table. + Table string } // NewMigrator instantiates new Migrator. -func NewMigrator(conn *pgx.Conn) *Migrator { +func NewMigrator(conn *pgx.Conn, cfg Config) *Migrator { + if cfg.Schema != "" { + cfg.Schema += "." + } + if cfg.Table == "" { + cfg.Table = "_dbump_log" + } return &Migrator{ - conn: conn, + conn: conn, + tableName: cfg.Schema + cfg.Table, } } -// Init migrator. +// Init is a method from Migrator interface. func (pg *Migrator) Init(ctx context.Context) error { - query := `CREATE TABLE IF NOT EXISTS _dbump_schema_version ( - version BIGINT NOT NULL PRIMARY KEY, + query := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s ( + version BIGINT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL -);` +);`, pg.tableName) _, err := pg.conn.Exec(ctx, query) return err } -// LockDB is a method for Migrator interface. +// LockDB is a method from Migrator interface. func (pg *Migrator) LockDB(ctx context.Context) error { _, err := pg.conn.Exec(ctx, "SELECT pg_advisory_lock($1);", lockNum) return err } -// UnlockDB is a method for Migrator interface. +// UnlockDB is a method from Migrator interface. func (pg *Migrator) UnlockDB(ctx context.Context) error { _, err := pg.conn.Exec(ctx, "SELECT pg_advisory_unlock($1);", lockNum) return err } -// Version is a method for Migrator interface. +// Version is a method from Migrator interface. func (pg *Migrator) Version(ctx context.Context) (version int, err error) { - query := "SELECT COUNT(*) FROM _dbump_schema_version;" - row := pg.conn.QueryRow(ctx, query) + query := fmt.Sprintf("SELECT version FROM %s ORDER BY created_at DESC LIMIT 1;", pg.tableName) + var row pgx.Row + if pg.tx != nil { + row = pg.tx.QueryRow(ctx, query) + } else { + row = pg.conn.QueryRow(ctx, query) + } err = row.Scan(&version) + if err != nil && errors.Is(err, pgx.ErrNoRows) { + return 0, nil + } return version, err } -// SetVersion is a method for Migrator interface. +// SetVersion is a method from Migrator interface. func (pg *Migrator) SetVersion(ctx context.Context, version int) error { - query := `INSERT INTO _dbump_schema_version (version, created_at) -VALUES ($1, NOW()) -ON CONFLICT (version) DO UPDATE -SET created_at = NOW();` - _, err := pg.conn.Exec(ctx, query, version) + query := fmt.Sprintf("INSERT INTO %s (version, created_at) VALUES ($1, NOW());", pg.tableName) + var err error + if pg.tx != nil { + _, err = pg.tx.Exec(ctx, query, version) + } else { + _, err = pg.conn.Exec(ctx, query, version) + } return err } -// Exec is a method for Migrator interface. +// Begin is a method from Migrator interface. +func (pg *Migrator) Begin(ctx context.Context) error { + var err error + pg.tx, err = pg.conn.Begin(ctx) + return err +} + +// Commit is a method from Migrator interface. +func (pg *Migrator) Commit(ctx context.Context) error { + return pg.tx.Commit(ctx) +} + +// Rollback is a method from Migrator interface. +func (pg *Migrator) Rollback(ctx context.Context) error { + return pg.tx.Rollback(ctx) +} + +// Exec is a method from Migrator interface. func (pg *Migrator) Exec(ctx context.Context, query string, args ...interface{}) error { - _, err := pg.conn.Exec(ctx, query, args...) + var err error + if pg.tx != nil { + _, err = pg.tx.Exec(ctx, query, args...) + } else { + _, err = pg.conn.Exec(ctx, query, args...) + } return err } diff --git a/dbump_pgx/pgx_test.go b/dbump_pgx/pgx_test.go index 4efba54..9eccf90 100644 --- a/dbump_pgx/pgx_test.go +++ b/dbump_pgx/pgx_test.go @@ -1,8 +1,9 @@ -package dbump +package dbump_pgx import ( "context" - "errors" + "fmt" + "os" "reflect" "testing" @@ -12,464 +13,524 @@ import ( var conn *pgx.Conn -func TestMigrateUp(t *testing.T) { - migrations := []*dbump.Migration{ - { - ID: 1, - Apply: "SELECT 1;", - Revert: "SELECT 10;", - }, - { - ID: 2, - Apply: "SELECT 2;", - Revert: "SELECT 20;", - }, - } +func init() { + host := envOrDef("DBUMP_PG_HOST", "localhost") + port := envOrDef("DBUMP_PG_PORT", "5432") + username := envOrDef("DBUMP_PG_USER", "postgres") + password := envOrDef("DBUMP_PG_PASS", "postgres") + db := envOrDef("DBUMP_PG_DB", "postgres") + sslmode := envOrDef("DBUMP_PG_SSL", "disable") - cfg := Config{ - Migrator: NewMigrator(conn, Config{Table: "TestMigrateUp"}), - Loader: NewSliceLoader(migrations), - Mode: ModeUp, - } - - failIfErr(t, Run(context.Background(), cfg)) -} - -func TestMigrateUpWhenFull(t *testing.T) { - wantLog := []string{ - "init", "lockdb", "getversion", "unlockdb", - } + dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + host, port, username, password, db, sslmode) - mm := &MockMigrator{ - VersionFn: func(ctx context.Context) (version int, err error) { - return 5, nil - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - } - - err := Run(context.Background(), cfg) - failIfErr(t, err) - mustEqual(t, mm.log, wantLog) -} - -func TestMigrateUpOne(t *testing.T) { - currVersion := 3 - wantLog := []string{ - "init", "lockdb", "getversion", - "exec", "SELECT 4;", "[]", "setversion", "4", - "unlockdb", - } - - mm := &MockMigrator{ - VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUpOne, - } - - err := Run(context.Background(), cfg) - failIfErr(t, err) - mustEqual(t, mm.log, wantLog) -} - -func TestMigrateDown(t *testing.T) { - wantLog := []string{ - "init", "lockdb", "getversion", - "exec", "SELECT 50;", "[]", "setversion", "4", - "exec", "SELECT 40;", "[]", "setversion", "3", - "exec", "SELECT 30;", "[]", "setversion", "2", - "exec", "SELECT 20;", "[]", "setversion", "1", - "exec", "SELECT 10;", "[]", "setversion", "0", - "unlockdb", - } - - mm := &MockMigrator{ - VersionFn: func(ctx context.Context) (version int, err error) { - return 5, nil - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeDown, - } - - err := Run(context.Background(), cfg) - failIfErr(t, err) - mustEqual(t, mm.log, wantLog) -} - -func TestMigrateDownWhenEmpty(t *testing.T) { - wantLog := []string{ - "init", "lockdb", "getversion", "unlockdb", - } - - mm := &MockMigrator{ - VersionFn: func(ctx context.Context) (version int, err error) { - return 0, nil - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeDown, - } - - err := Run(context.Background(), cfg) - failIfErr(t, err) - mustEqual(t, mm.log, wantLog) -} - -func TestMigrateDownOne(t *testing.T) { - currVersion := 3 - wantLog := []string{ - "init", "lockdb", "getversion", - "exec", "SELECT 30;", "[]", "setversion", "2", - "unlockdb", - } - - mm := &MockMigrator{ - VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeDownOne, - } - - err := Run(context.Background(), cfg) - failIfErr(t, err) - mustEqual(t, mm.log, wantLog) -} - -func TestUseForce(t *testing.T) { - currVersion := 3 - wantLog := []string{ - "init", "lockdb", "unlockdb", "lockdb", "getversion", - "exec", "SELECT 4;", "[]", "setversion", "4", - "exec", "SELECT 5;", "[]", "setversion", "5", - "unlockdb", - } - - isLocked := true - - mm := &MockMigrator{ - LockDBFn: func(ctx context.Context) error { - if isLocked { - return errors.New("cannot get lock") - } - return nil - }, - UnlockDBFn: func(ctx context.Context) error { - isLocked = false - return nil - }, - VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - UseForce: true, - } - - err := Run(context.Background(), cfg) - failIfErr(t, err) - mustEqual(t, mm.log, wantLog) -} - -func TestZigZag(t *testing.T) { - wantLog := []string{ - "init", "lockdb", "getversion", - "exec", "SELECT 1;", "[]", "setversion", "1", - "exec", "SELECT 10;", "[]", "setversion", "0", - "exec", "SELECT 1;", "[]", "setversion", "1", - - "exec", "SELECT 2;", "[]", "setversion", "2", - "exec", "SELECT 20;", "[]", "setversion", "1", - "exec", "SELECT 2;", "[]", "setversion", "2", - - "exec", "SELECT 3;", "[]", "setversion", "3", - "exec", "SELECT 30;", "[]", "setversion", "2", - "exec", "SELECT 3;", "[]", "setversion", "3", - - "exec", "SELECT 4;", "[]", "setversion", "4", - "exec", "SELECT 40;", "[]", "setversion", "3", - "exec", "SELECT 4;", "[]", "setversion", "4", - - "exec", "SELECT 5;", "[]", "setversion", "5", - "exec", "SELECT 50;", "[]", "setversion", "4", - "exec", "SELECT 5;", "[]", "setversion", "5", - "unlockdb", - } - - mm := &MockMigrator{} - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - ZigZag: true, - } - - err := Run(context.Background(), cfg) - failIfErr(t, err) - mustEqual(t, mm.log, wantLog) -} - -func TestFailOnInitError(t *testing.T) { - wantLog := []string{"init"} - mm := &MockMigrator{ - InitFn: func(ctx context.Context) error { - return errors.New("no access") - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - } - - err := Run(context.Background(), cfg) - if err == nil { - t.Fail() - } - mustEqual(t, mm.log, wantLog) -} - -func TestFailOnLockDB(t *testing.T) { - wantLog := []string{ - "init", "lockdb", - } - mm := &MockMigrator{ - LockDBFn: func(ctx context.Context) (err error) { - return errors.New("no access") - }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - } - - err := Run(context.Background(), cfg) - if err == nil { - t.Fail() + var err error + conn, err = pgx.Connect(context.Background(), dsn) + if err != nil { + panic(fmt.Sprintf("dbump_pgx: cannot connect to container: %s", err)) } - mustEqual(t, mm.log, wantLog) } -func TestFailOnUnlockDB(t *testing.T) { - currVersion := 4 - wantLog := []string{ - "init", "lockdb", "getversion", - "exec", "SELECT 5;", "[]", "setversion", "5", - "unlockdb", - } - mm := &MockMigrator{ - UnlockDBFn: func(ctx context.Context) (err error) { - return errors.New("no access") - }, - VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil +func TestNonDefaultSchemaTable(t *testing.T) { + testCases := []struct { + name string + schema string + table string + wantTableName string + }{ + { + name: "all empty", + schema: "", + table: "", + wantTableName: "_dbump_log", }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - } - - err := Run(context.Background(), cfg) - if err == nil { - t.Fail() - } - mustEqual(t, mm.log, wantLog) -} - -func TestFailOnGetVersionError(t *testing.T) { - wantLog := []string{ - "init", "lockdb", "getversion", "unlockdb", - } - mm := &MockMigrator{ - VersionFn: func(ctx context.Context) (version int, err error) { - return 0, errors.New("no access") + { + name: "schema set", + schema: "test_schema", + table: "", + wantTableName: "test_schema._dbump_log", }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - } - - err := Run(context.Background(), cfg) - if err == nil { - t.Fail() - } - mustEqual(t, mm.log, wantLog) -} - -func TestFailOnSetVersionError(t *testing.T) { - wantLog := []string{ - "init", "lockdb", "getversion", - "exec", "SELECT 1;", "[]", "setversion", "1", - "unlockdb", - } - mm := &MockMigrator{ - SetVersionFn: func(ctx context.Context, version int) error { - return errors.New("no access") + { + name: "table set", + schema: "", + table: "test_table", + wantTableName: "test_table", }, - } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - } - - err := Run(context.Background(), cfg) - if err == nil { - t.Fail() - } - mustEqual(t, mm.log, wantLog) -} - -func TestFailOnExec(t *testing.T) { - wantLog := []string{ - "init", "lockdb", "getversion", - "exec", "SELECT 1;", "[]", - "unlockdb", - } - mm := &MockMigrator{ - ExecFn: func(ctx context.Context, query string, args ...interface{}) error { - return errors.New("syntax error") + { + name: "schema and table set", + schema: "test_schema", + table: "test_table", + wantTableName: "test_schema.test_table", }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeUp, - } - - err := Run(context.Background(), cfg) - if err == nil { - t.Fail() - } - mustEqual(t, mm.log, wantLog) -} -func TestFailOnLoad(t *testing.T) { - cfg := Config{ - Migrator: &MockMigrator{}, - Loader: &MockLoader{ - LoaderFn: func() ([]*Migration, error) { - return nil, errors.New("forgot to commit") - }, - }, - Mode: ModeUp, - } - err := Run(context.Background(), cfg) - if err == nil { - t.Fail() + for _, tc := range testCases { + m := NewMigrator(conn, Config{ + Schema: tc.schema, + Table: tc.table, + }) + mustEqual(t, m.tableName, tc.wantTableName) } } -func Test_loadMigrations(t *testing.T) { - testCases := []struct { - testName string - migrations []*Migration - wantMigrations []*Migration - wantErr error - }{ - { - "ok (migrations are sorted)", - []*Migration{ - {ID: 2}, - {ID: 1}, - }, - []*Migration{ - {ID: 1}, - {ID: 2}, - }, - nil, - }, - +func TestMigrateUp(t *testing.T) { + migrations := []*dbump.Migration{ { - "fail (missing migration)", - []*Migration{ - {ID: 3}, - {ID: 1}, - }, - nil, - errors.New("missing migration number: 2 (have 3)"), + ID: 1, + Apply: "SELECT 1;", + Revert: "SELECT 10;", }, - { - "fail (duplicate id)", - []*Migration{ - {ID: 2, Name: "mig2"}, - {ID: 2, Name: "mig2fix"}, - {ID: 1}, - }, - nil, - errors.New("duplicate migration number: 2 (mig2)"), + ID: 2, + Apply: "SELECT 2;", + Revert: "SELECT 20;", }, } - for _, tc := range testCases { - m := mig{ - Loader: NewSliceLoader(tc.migrations), - } - - migs, err := m.load() - mustEqual(t, err != nil, tc.wantErr != nil) - mustEqual(t, migs, tc.wantMigrations) + cfg := dbump.Config{ + Migrator: NewMigrator(conn, Config{Table: "TestMigrateUp"}), + Loader: dbump.NewSliceLoader(migrations), + Mode: dbump.ModeUp, } -} -var testdataMigrations = []*Migration{ - { - ID: 1, - Name: `0001_init.sql`, - Apply: `SELECT 1;`, - Revert: `SELECT 10;`, - }, - { - ID: 2, - Name: `0002_another.sql`, - Apply: `SELECT 2;`, - Revert: `SELECT 20;`, - }, - { - ID: 3, - Name: `0003_even-better.sql`, - Apply: `SELECT 3;`, - Revert: `SELECT 30;`, - }, - { - ID: 4, - Name: `0004_but_fix.sql`, - Apply: `SELECT 4;`, - Revert: `SELECT 40;`, - }, - { - ID: 5, - Name: `0005_final.sql`, - Apply: `SELECT 5;`, - Revert: `SELECT 50;`, - }, + failIfErr(t, dbump.Run(context.Background(), cfg)) } +// func TestMigrateUpWhenFull(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", "getversion", "unlockdb", +// } + +// mm := &MockMigrator{ +// VersionFn: func(ctx context.Context) (version int, err error) { +// return 5, nil +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// } + +// err := Run(context.Background(), cfg) +// failIfErr(t, err) +// mustEqual(t, mm.log, wantLog) +// } + +// func TestMigrateUpOne(t *testing.T) { +// currVersion := 3 +// wantLog := []string{ +// "init", "lockdb", "getversion", +// "exec", "SELECT 4;", "[]", "setversion", "4", +// "unlockdb", +// } + +// mm := &MockMigrator{ +// VersionFn: func(ctx context.Context) (version int, err error) { +// return currVersion, nil +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUpOne, +// } + +// err := Run(context.Background(), cfg) +// failIfErr(t, err) +// mustEqual(t, mm.log, wantLog) +// } + +// func TestMigrateDown(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", "getversion", +// "exec", "SELECT 50;", "[]", "setversion", "4", +// "exec", "SELECT 40;", "[]", "setversion", "3", +// "exec", "SELECT 30;", "[]", "setversion", "2", +// "exec", "SELECT 20;", "[]", "setversion", "1", +// "exec", "SELECT 10;", "[]", "setversion", "0", +// "unlockdb", +// } + +// mm := &MockMigrator{ +// VersionFn: func(ctx context.Context) (version int, err error) { +// return 5, nil +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeDown, +// } + +// err := Run(context.Background(), cfg) +// failIfErr(t, err) +// mustEqual(t, mm.log, wantLog) +// } + +// func TestMigrateDownWhenEmpty(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", "getversion", "unlockdb", +// } + +// mm := &MockMigrator{ +// VersionFn: func(ctx context.Context) (version int, err error) { +// return 0, nil +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeDown, +// } + +// err := Run(context.Background(), cfg) +// failIfErr(t, err) +// mustEqual(t, mm.log, wantLog) +// } + +// func TestMigrateDownOne(t *testing.T) { +// currVersion := 3 +// wantLog := []string{ +// "init", "lockdb", "getversion", +// "exec", "SELECT 30;", "[]", "setversion", "2", +// "unlockdb", +// } + +// mm := &MockMigrator{ +// VersionFn: func(ctx context.Context) (version int, err error) { +// return currVersion, nil +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeDownOne, +// } + +// err := Run(context.Background(), cfg) +// failIfErr(t, err) +// mustEqual(t, mm.log, wantLog) +// } + +// func TestUseForce(t *testing.T) { +// currVersion := 3 +// wantLog := []string{ +// "init", "lockdb", "unlockdb", "lockdb", "getversion", +// "exec", "SELECT 4;", "[]", "setversion", "4", +// "exec", "SELECT 5;", "[]", "setversion", "5", +// "unlockdb", +// } + +// isLocked := true + +// mm := &MockMigrator{ +// LockDBFn: func(ctx context.Context) error { +// if isLocked { +// return errors.New("cannot get lock") +// } +// return nil +// }, +// UnlockDBFn: func(ctx context.Context) error { +// isLocked = false +// return nil +// }, +// VersionFn: func(ctx context.Context) (version int, err error) { +// return currVersion, nil +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// UseForce: true, +// } + +// err := Run(context.Background(), cfg) +// failIfErr(t, err) +// mustEqual(t, mm.log, wantLog) +// } + +// func TestZigZag(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", "getversion", +// "exec", "SELECT 1;", "[]", "setversion", "1", +// "exec", "SELECT 10;", "[]", "setversion", "0", +// "exec", "SELECT 1;", "[]", "setversion", "1", + +// "exec", "SELECT 2;", "[]", "setversion", "2", +// "exec", "SELECT 20;", "[]", "setversion", "1", +// "exec", "SELECT 2;", "[]", "setversion", "2", + +// "exec", "SELECT 3;", "[]", "setversion", "3", +// "exec", "SELECT 30;", "[]", "setversion", "2", +// "exec", "SELECT 3;", "[]", "setversion", "3", + +// "exec", "SELECT 4;", "[]", "setversion", "4", +// "exec", "SELECT 40;", "[]", "setversion", "3", +// "exec", "SELECT 4;", "[]", "setversion", "4", + +// "exec", "SELECT 5;", "[]", "setversion", "5", +// "exec", "SELECT 50;", "[]", "setversion", "4", +// "exec", "SELECT 5;", "[]", "setversion", "5", +// "unlockdb", +// } + +// mm := &MockMigrator{} +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// ZigZag: true, +// } + +// err := Run(context.Background(), cfg) +// failIfErr(t, err) +// mustEqual(t, mm.log, wantLog) +// } + +// func TestFailOnInitError(t *testing.T) { +// wantLog := []string{"init"} +// mm := &MockMigrator{ +// InitFn: func(ctx context.Context) error { +// return errors.New("no access") +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// } + +// err := Run(context.Background(), cfg) +// if err == nil { +// t.Fail() +// } +// mustEqual(t, mm.log, wantLog) +// } + +// func TestFailOnLockDB(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", +// } +// mm := &MockMigrator{ +// LockDBFn: func(ctx context.Context) (err error) { +// return errors.New("no access") +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// } + +// err := Run(context.Background(), cfg) +// if err == nil { +// t.Fail() +// } +// mustEqual(t, mm.log, wantLog) +// } + +// func TestFailOnUnlockDB(t *testing.T) { +// currVersion := 4 +// wantLog := []string{ +// "init", "lockdb", "getversion", +// "exec", "SELECT 5;", "[]", "setversion", "5", +// "unlockdb", +// } +// mm := &MockMigrator{ +// UnlockDBFn: func(ctx context.Context) (err error) { +// return errors.New("no access") +// }, +// VersionFn: func(ctx context.Context) (version int, err error) { +// return currVersion, nil +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// } + +// err := Run(context.Background(), cfg) +// if err == nil { +// t.Fail() +// } +// mustEqual(t, mm.log, wantLog) +// } + +// func TestFailOnGetVersionError(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", "getversion", "unlockdb", +// } +// mm := &MockMigrator{ +// VersionFn: func(ctx context.Context) (version int, err error) { +// return 0, errors.New("no access") +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// } + +// err := Run(context.Background(), cfg) +// if err == nil { +// t.Fail() +// } +// mustEqual(t, mm.log, wantLog) +// } + +// func TestFailOnSetVersionError(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", "getversion", +// "exec", "SELECT 1;", "[]", "setversion", "1", +// "unlockdb", +// } +// mm := &MockMigrator{ +// SetVersionFn: func(ctx context.Context, version int) error { +// return errors.New("no access") +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// } + +// err := Run(context.Background(), cfg) +// if err == nil { +// t.Fail() +// } +// mustEqual(t, mm.log, wantLog) +// } + +// func TestFailOnExec(t *testing.T) { +// wantLog := []string{ +// "init", "lockdb", "getversion", +// "exec", "SELECT 1;", "[]", +// "unlockdb", +// } +// mm := &MockMigrator{ +// ExecFn: func(ctx context.Context, query string, args ...interface{}) error { +// return errors.New("syntax error") +// }, +// } +// cfg := Config{ +// Migrator: mm, +// Loader: NewSliceLoader(testdataMigrations), +// Mode: ModeUp, +// } + +// err := Run(context.Background(), cfg) +// if err == nil { +// t.Fail() +// } +// mustEqual(t, mm.log, wantLog) +// } + +// func TestFailOnLoad(t *testing.T) { +// cfg := Config{ +// Migrator: &MockMigrator{}, +// Loader: &MockLoader{ +// LoaderFn: func() ([]*Migration, error) { +// return nil, errors.New("forgot to commit") +// }, +// }, +// Mode: ModeUp, +// } +// err := Run(context.Background(), cfg) +// if err == nil { +// t.Fail() +// } +// } + +// func Test_loadMigrations(t *testing.T) { +// testCases := []struct { +// testName string +// migrations []*Migration +// wantMigrations []*Migration +// wantErr error +// }{ +// { +// "ok (migrations are sorted)", +// []*Migration{ +// {ID: 2}, +// {ID: 1}, +// }, +// []*Migration{ +// {ID: 1}, +// {ID: 2}, +// }, +// nil, +// }, + +// { +// "fail (missing migration)", +// []*Migration{ +// {ID: 3}, +// {ID: 1}, +// }, +// nil, +// errors.New("missing migration number: 2 (have 3)"), +// }, + +// { +// "fail (duplicate id)", +// []*Migration{ +// {ID: 2, Name: "mig2"}, +// {ID: 2, Name: "mig2fix"}, +// {ID: 1}, +// }, +// nil, +// errors.New("duplicate migration number: 2 (mig2)"), +// }, +// } + +// for _, tc := range testCases { +// m := mig{ +// Loader: NewSliceLoader(tc.migrations), +// } + +// migs, err := m.load() +// mustEqual(t, err != nil, tc.wantErr != nil) +// mustEqual(t, migs, tc.wantMigrations) +// } +// } + +// var testdataMigrations = []*Migration{ +// { +// ID: 1, +// Name: `0001_init.sql`, +// Apply: `SELECT 1;`, +// Revert: `SELECT 10;`, +// }, +// { +// ID: 2, +// Name: `0002_another.sql`, +// Apply: `SELECT 2;`, +// Revert: `SELECT 20;`, +// }, +// { +// ID: 3, +// Name: `0003_even-better.sql`, +// Apply: `SELECT 3;`, +// Revert: `SELECT 30;`, +// }, +// { +// ID: 4, +// Name: `0004_but_fix.sql`, +// Apply: `SELECT 4;`, +// Revert: `SELECT 40;`, +// }, +// { +// ID: 5, +// Name: `0005_final.sql`, +// Apply: `SELECT 5;`, +// Revert: `SELECT 50;`, +// }, +// } + func failIfErr(t testing.TB, err error) { t.Helper() if err != nil { @@ -483,3 +544,10 @@ func mustEqual(t testing.TB, got, want interface{}) { t.Fatalf("\nhave %+v\nwant %+v", got, want) } } + +func envOrDef(env, def string) string { + if val := os.Getenv(env); val != "" { + return val + } + return def +}