A .gitignore => .gitignore +1 -0
M go.mod => go.mod +6 -7
@@ 1,14 1,13 @@
module github.com/egtann/migrate
require (
- github.com/go-sql-driver/mysql v1.4.1
+ github.com/go-sql-driver/mysql v1.5.0
github.com/jmoiron/sqlx v1.2.0
- github.com/lib/pq v1.2.0
- github.com/mattn/go-sqlite3 v1.11.0
- github.com/pkg/errors v0.8.1
- golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
- golang.org/x/sys v0.0.0-20191010194322-b09406accb47
- google.golang.org/appengine v1.6.5 // indirect
+ github.com/lib/pq v1.3.0
+ github.com/mattn/go-sqlite3 v2.0.3+incompatible
+ github.com/pkg/errors v0.9.1
+ golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340
+ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5
)
go 1.13
M go.sum => go.sum +12 -24
@@ 1,37 1,25 @@
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
-github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
-github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
+github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
-github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 h1:KOcEaR10tFr7gdJV2GCKw8Os5yED1u1aOqHjOAb6d2Y=
+golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
-golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
M migrate.go => migrate.go +61 -13
@@ 16,6 16,9 @@ import (
"github.com/pkg/errors"
)
+// version of the migrate tool's database schema.
+const version = 1
+
type Migrate struct {
Migrations []Migration
Files []os.FileInfo
@@ 29,11 32,16 @@ type Migrate struct {
type Migration struct {
Filename string
Checksum string
+ Content string
}
var regexNum = regexp.MustCompile(`^\d+`)
-func New(db Store, log Logger, dir, skip string) (*Migrate, error) {
+func New(
+ db Store,
+ log Logger,
+ dir, skip string,
+) (*Migrate, error) {
m := &Migrate{db: db, log: log, dir: dir}
// Get files in migration dir and sort them
@@ 54,6 62,25 @@ func New(db Store, log Logger, dir, skip string) (*Migrate, error) {
if err = db.CreateMetaCheckpointsIfNotExists(); err != nil {
return nil, errors.Wrap(err, "create meta checkpoints table")
}
+ curVersion, err := db.CreateMetaVersionIfNotExists()
+ if err != nil {
+ return nil, errors.Wrap(err, "create meta version table")
+ }
+
+ // Migrate the database schema to match the tool's expectations
+ // automatically
+ if curVersion > version {
+ return nil, errors.New("must upgrade migrate: go get -u github.com/egtann/migrate")
+ }
+ if curVersion < 1 {
+ tmpMigrations, err := migrationsFromFiles(m)
+ if err != nil {
+ return nil, errors.Wrap(err, "migrations from files")
+ }
+ if err = db.UpgradeToV1(tmpMigrations); err != nil {
+ return nil, errors.Wrap(err, "upgrade to v1")
+ }
+ }
// If skip, then we record the migrations but do not perform them. This
// enables you to start using this package on an existing database
@@ 70,6 97,7 @@ func New(db Store, log Logger, dir, skip string) (*Migrate, error) {
if err != nil {
return nil, errors.Wrap(err, "get migrations")
}
+
if err = m.validHistory(); err != nil {
return nil, err
}
@@ 118,7 146,7 @@ func (m *Migrate) checkHash(mg Migration) error {
return err
}
defer fi.Close()
- check, err := computeChecksum(fi)
+ _, check, err := computeChecksum(fi)
if err != nil {
return err
}
@@ 171,7 199,7 @@ func (m *Migrate) migrateFile(filename string) error {
// Confirm the file up to our checkpoint has not changed
if i < len(checkpoints) {
r := strings.NewReader(cmd)
- checksum, err := computeChecksum(r)
+ _, checksum, err := computeChecksum(r)
if err != nil {
return errors.Wrap(err, "compute checkpoint checksum")
}
@@ 191,11 219,11 @@ func (m *Migrate) migrateFile(filename string) error {
}
// Save a checkpoint
- checksum, err := computeChecksum(strings.NewReader(cmd))
+ _, checksum, err := computeChecksum(strings.NewReader(cmd))
if err != nil {
return errors.Wrap(err, "compute checksum")
}
- err = m.db.InsertMetaCheckpoint(filename, checksum, i)
+ err = m.db.InsertMetaCheckpoint(filename, cmd, checksum, i)
if err != nil {
return errors.Wrap(err, "insert checkpoint")
}
@@ 207,11 235,11 @@ func (m *Migrate) migrateFile(filename string) error {
return errors.Wrap(err, "delete checkpoints")
}
- checksum, err := computeChecksum(bytes.NewReader(byt))
+ _, checksum, err := computeChecksum(bytes.NewReader(byt))
if err != nil {
return errors.Wrap(err, "compute file checksum")
}
- if err = m.db.InsertMigration(filename, checksum); err != nil {
+ if err = m.db.InsertMigration(filename, string(byt), checksum); err != nil {
return errors.Wrap(err, "insert migration")
}
return nil
@@ 238,12 266,12 @@ func (m *Migrate) skip(toFile string) (int, error) {
if err != nil {
return -1, err
}
- checksum, err := computeChecksum(fi)
+ content, checksum, err := computeChecksum(fi)
if err != nil {
fi.Close()
return -1, err
}
- if err = m.db.UpsertMigration(name, checksum); err != nil {
+ if err = m.db.UpsertMigration(name, content, checksum); err != nil {
fi.Close()
return -1, err
}
@@ 254,12 282,16 @@ func (m *Migrate) skip(toFile string) (int, error) {
return index, nil
}
-func computeChecksum(r io.Reader) (string, error) {
+func computeChecksum(r io.Reader) (content string, checksum string, err error) {
h := md5.New()
- if _, err := io.Copy(h, r); err != nil {
- return "", err
+ byt, err := ioutil.ReadAll(r)
+ if err != nil {
+ return "", "", errors.Wrap(err, "read all")
+ }
+ if _, err := io.Copy(h, bytes.NewReader(byt)); err != nil {
+ return "", "", err
}
- return fmt.Sprintf("%x", h.Sum(nil)), nil
+ return string(byt), fmt.Sprintf("%x", h.Sum(nil)), nil
}
// readdir collects file infos from the migration directory.
@@ 314,3 346,19 @@ func sortfiles(files []os.FileInfo) error {
})
return nameErr
}
+
+func migrationsFromFiles(m *Migrate) ([]Migration, error) {
+ ms := make([]Migration, len(m.Files))
+ for i, fileInfo := range m.Files {
+ filename := filepath.Join(m.dir, fileInfo.Name())
+ byt, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, errors.Wrap(err, "read file")
+ }
+ ms[i] = Migration{
+ Filename: fileInfo.Name(),
+ Content: string(byt),
+ }
+ }
+ return ms, nil
+}
M mysql/mysql.go => mysql/mysql.go +113 -12
@@ 3,6 3,7 @@ package mysql
import (
"crypto/tls"
"crypto/x509"
+ "database/sql"
"fmt"
"io/ioutil"
@@ 40,10 41,31 @@ func New(
return db, nil
}
+func (db *DB) CreateMetaVersionIfNotExists() (int, error) {
+ q := `CREATE TABLE IF NOT EXISTS metaversion (
+ version INTEGER NOT NULL
+ )`
+ if _, err := db.Exec(q); err != nil {
+ return 0, errors.Wrap(err, "create metaversion table")
+ }
+
+ var version int
+ q = `SELECT version FROM metaversion`
+ err := db.Get(&version, q)
+ switch {
+ case err == sql.ErrNoRows:
+ return 0, nil
+ case err != nil:
+ return 0, errors.Wrap(err, "get version")
+ }
+ return version, nil
+}
+
func (db *DB) CreateMetaIfNotExists() error {
q := `CREATE TABLE IF NOT EXISTS meta (
filename VARCHAR(255) UNIQUE NOT NULL,
md5 VARCHAR(255) NOT NULL,
+ content TEXT NOT NULL,
createdat DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
)`
if _, err := db.Exec(q); err != nil {
@@ 57,6 79,7 @@ func (db *DB) CreateMetaCheckpointsIfNotExists() error {
filename VARCHAR(255) NOT NULL,
idx INTEGER NOT NULL,
md5 VARCHAR(255) NOT NULL,
+ content TEXT NOT NULL,
createdat DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (filename, idx)
)`
@@ 68,7 91,10 @@ func (db *DB) CreateMetaCheckpointsIfNotExists() error {
func (db *DB) GetMigrations() ([]migrate.Migration, error) {
migrations := []migrate.Migration{}
- q := `SELECT filename, md5 AS checksum FROM meta ORDER BY filename * 1`
+ q := `
+ SELECT filename, content, md5 AS checksum
+ FROM meta
+ ORDER BY filename * 1`
err := db.Select(&migrations, q)
return migrations, err
@@ 81,25 107,28 @@ func (db *DB) GetMetaCheckpoints(filename string) ([]string, error) {
return checkpoints, err
}
-func (db *DB) UpsertMigration(filename, checksum string) error {
+func (db *DB) UpsertMigration(filename, content, checksum string) error {
q := `
- INSERT INTO meta (filename, md5) VALUES (?, ?)
- ON DUPLICATE KEY UPDATE md5=?`
- _, err := db.Exec(q, filename, checksum, checksum)
+ INSERT INTO meta (filename, content, md5) VALUES (?, ?, ?)
+ ON DUPLICATE KEY UPDATE md5=?, content=?`
+ _, err := db.Exec(q, filename, content, checksum, checksum, content)
return err
}
-func (db *DB) InsertMetaCheckpoint(filename, checksum string, idx int) error {
+func (db *DB) InsertMetaCheckpoint(
+ filename, content, checksum string,
+ idx int,
+) error {
q := `
- INSERT INTO metacheckpoints (filename, idx, md5)
- VALUES (?, ?, ?)`
- _, err := db.Exec(q, filename, idx, checksum)
+ INSERT INTO metacheckpoints (filename, content, idx, md5)
+ VALUES (?, ?, ?, ?)`
+ _, err := db.Exec(q, filename, content, idx, checksum)
return err
}
-func (db *DB) InsertMigration(filename, checksum string) error {
- q := `INSERT INTO meta (filename, md5) VALUES (?, ?)`
- _, err := db.Exec(q, filename, checksum)
+func (db *DB) InsertMigration(filename, content, checksum string) error {
+ q := `INSERT INTO meta (filename, content, md5) VALUES (?, ?, ?)`
+ _, err := db.Exec(q, filename, content, checksum)
return err
}
@@ 109,6 138,78 @@ func (db *DB) DeleteMetaCheckpoints() error {
return err
}
+// UpgradeToV1 migrates existing meta tables to the v1 format. Complete any
+// migrations before running this function; this will not succeed if have any
+// existing metacheckpoints.
+func (db *DB) UpgradeToV1(migrations []migrate.Migration) (err error) {
+ // Begin Tx
+ tx, err := db.Beginx()
+ if err != nil {
+ return errors.Wrap(err, "begin tx")
+ }
+ defer func() {
+ if err != nil {
+ _ = tx.Rollback()
+ return
+ }
+ err = tx.Commit()
+ }()
+
+ // Remove the uniqueness constraint from md5
+ q := `ALTER TABLE meta DROP CONSTRAINT md5`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "remove md5 unique")
+ return
+ }
+
+ // Add a content column to record the exact migration that ran
+ // alongside the md5, insert the appropriate data, then set not null
+ q = `ALTER TABLE meta ADD COLUMN content TEXT`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "add content column")
+ return
+ }
+ for _, m := range migrations {
+ q = `UPDATE meta SET content=? WHERE filename=?`
+ if _, err = tx.Exec(q, m.Content, m.Filename); err != nil {
+ err = errors.Wrap(err, "update meta content")
+ return
+ }
+ }
+ q = `ALTER TABLE meta MODIFY COLUMN content TEXT NOT NULL`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "update meta content not null")
+ return
+ }
+
+ // Add the content column to metacheckpoints
+ q = `
+ ALTER TABLE metacheckpoints
+ ADD COLUMN IF NOT EXISTS content TEXT NOT NULL`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "add metacheckpoints content")
+ return
+ }
+
+ q = `
+ CREATE TABLE IF NOT EXISTS metaversion (version INTEGER NOT NULL)`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "create metaversion table")
+ return
+ }
+ q = `DELETE FROM metaversion`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "delete metaversion")
+ return
+ }
+ q = `INSERT INTO metaversion (version) VALUES (1)`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "insert metaversion")
+ return
+ }
+ return nil
+}
+
func (db *DB) Open() error {
if db.tlsConfig != nil {
err := mysql.RegisterTLSConfig(db.tlsConfig.ServerName,
A mysql/mysql_test.go => mysql/mysql_test.go +265 -0
@@ 0,0 1,265 @@
+package mysql
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/egtann/migrate"
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/jmoiron/sqlx"
+ "github.com/pkg/errors"
+)
+
+const checkpointFile = "2.sql"
+
+func TestMain(m *testing.M) {
+ path := filepath.Join("..", "test.env")
+ err := parseEnv(path)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to parse %s: %s\n", path, err)
+ os.Exit(1)
+ }
+ os.Exit(m.Run())
+}
+
+func TestCreateMetaIfNotExists(t *testing.T) {
+ db := newDB(t)
+ defer teardown(t, db)
+
+ err := db.CreateMetaIfNotExists()
+ check(t, err)
+
+ var tmp []int
+ err = db.DB.Select(&tmp, `SELECT 1 FROM meta`)
+ check(t, err)
+}
+
+func TestCreateMetaCheckpointsIfNotExists(t *testing.T) {
+ db := newDB(t)
+ defer teardown(t, db)
+
+ err := db.CreateMetaCheckpointsIfNotExists()
+ check(t, err)
+
+ var tmp []int
+ err = db.DB.Select(&tmp, `SELECT 1 FROM metacheckpoints`)
+ check(t, err)
+}
+
+func TestGetMigrations(t *testing.T) {
+ db := setupDBV1(t)
+ defer teardown(t, db)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 1 {
+ t.Fatal("expected 1 migration")
+ }
+}
+
+func TestGetMetaCheckpoints(t *testing.T) {
+ db := setupDBV1(t)
+ defer teardown(t, db)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 1 {
+ t.Fatal("expected 1 checkpoint")
+ }
+}
+
+func TestUpsertMigration(t *testing.T) {
+ db := setupDBV1(t)
+ defer teardown(t, db)
+
+ // Test update
+ err := db.UpsertMigration("1.sql", "SELECT 1;", "md5")
+ check(t, err)
+
+ // Test insert
+ err = db.UpsertMigration("3.sql", "SELECT 3;", "md5")
+ check(t, err)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 2 {
+ t.Fatalf("expected 2 migrations, got %d", len(ms))
+ }
+}
+
+func TestInsertMetaCheckpoint(t *testing.T) {
+ db := setupDBV1(t)
+ defer teardown(t, db)
+
+ err := db.InsertMetaCheckpoint(checkpointFile, "SELECT 3;", "md5", 1)
+ check(t, err)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 2 {
+ t.Fatal("expected 2 checkpoints")
+ }
+}
+
+func TestInsertMigration(t *testing.T) {
+ db := setupDBV1(t)
+ defer teardown(t, db)
+
+ err := db.InsertMigration("3.sql", "SELECT 3;", "md5")
+ check(t, err)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 2 {
+ t.Fatal("expected 2 migrations")
+ }
+}
+
+func TestDeleteMetaCheckpoints(t *testing.T) {
+ db := setupDBV1(t)
+ defer teardown(t, db)
+
+ err := db.DeleteMetaCheckpoints()
+ check(t, err)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 0 {
+ t.Fatal("expected 0 checkpoints")
+ }
+}
+
+func check(t *testing.T, err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func must(err error) {
+}
+
+func newDB(t *testing.T) *DB {
+ db := createDBAndOpen(t)
+ return &DB{DB: db}
+}
+
+func parseEnv(filename string) error {
+ fi, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer fi.Close()
+
+ scn := bufio.NewScanner(fi)
+ for i := 1; scn.Scan(); i++ {
+ line := scn.Text()
+ if line == "" {
+ continue
+ }
+ parts := strings.SplitN(line, "=", 2)
+ if len(parts) != 2 {
+ return fmt.Errorf("bad line %d: %s", i, line)
+ }
+ if err = os.Setenv(parts[0], parts[1]); err != nil {
+ return errors.Wrap(err, "set env")
+ }
+ }
+ if err = scn.Err(); err != nil {
+ return errors.Wrap(err, "scan")
+ }
+ return nil
+}
+
+func createDBAndOpen(t *testing.T) *sqlx.DB {
+ user := os.Getenv("MYSQL_USER")
+ if user == "" {
+ panic("missing MYSQL_USER")
+ }
+ pass := os.Getenv("MYSQL_PASSWORD")
+ if pass == "" {
+ panic("missing MYSQL_PASSWORD")
+ }
+ host := os.Getenv("MYSQL_HOST")
+ if host == "" {
+ panic("missing MYSQL_HOST")
+ }
+
+ dsn := fmt.Sprintf("%s:%s@tcp(%s)/?timeout=1s", user, pass, host)
+ db, err := sqlx.Open("mysql", dsn)
+ check(t, err)
+
+ q := `DROP DATABASE IF EXISTS migrate_test`
+ _, err = db.Exec(q)
+ check(t, err)
+
+ q = `CREATE DATABASE migrate_test`
+ _, err = db.Exec(q)
+ check(t, err)
+
+ err = db.Close()
+ check(t, err)
+
+ dsn = fmt.Sprintf("%s:%s@tcp(%s)/migrate_test?timeout=1s", user, pass,
+ host)
+ db, err = sqlx.Open("mysql", dsn)
+ check(t, err)
+
+ return db
+}
+
+func teardown(t *testing.T, db *DB) {
+ q := `DROP DATABASE migrate_test`
+ _, err := db.Exec(q)
+ check(t, err)
+}
+
+func setupDBV1(t *testing.T) *DB {
+ db := setupDBV0(t)
+ err := db.UpgradeToV1([]migrate.Migration{{
+ Filename: "1.sql",
+ Checksum: "md5",
+ Content: "SELECT 1;",
+ }})
+ check(t, err)
+
+ q := `
+ INSERT INTO metacheckpoints (idx, filename, content, md5)
+ VALUES (?, ?, ?, ?)`
+ _, err = db.DB.Exec(q, 0, checkpointFile, "SELECT 2;", "md5")
+ check(t, err)
+
+ return db
+}
+
+func setupDBV0(t *testing.T) *DB {
+ db := newDB(t)
+
+ q := `CREATE TABLE IF NOT EXISTS meta (
+ filename VARCHAR(255) UNIQUE NOT NULL,
+ md5 VARCHAR(255) UNIQUE NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ )`
+ _, err := db.DB.Exec(q)
+ check(t, err)
+
+ q = `CREATE TABLE IF NOT EXISTS metacheckpoints (
+ filename VARCHAR(255) NOT NULL,
+ idx INTEGER NOT NULL,
+ md5 VARCHAR(255) NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (filename, idx)
+ )`
+ _, err = db.DB.Exec(q)
+ check(t, err)
+
+ q = `INSERT INTO meta (filename, md5) VALUES (?, ?)`
+ _, err = db.DB.Exec(q, "1.sql", "md5")
+ check(t, err)
+
+ return db
+}
M postgres/postgres.go => postgres/postgres.go +112 -12
@@ 1,6 1,7 @@
package postgres
import (
+ "database/sql"
"fmt"
"github.com/egtann/migrate"
@@ 39,6 40,7 @@ func (db *DB) CreateMetaIfNotExists() error {
q := `CREATE TABLE IF NOT EXISTS meta (
filename TEXT UNIQUE NOT NULL,
md5 TEXT NOT NULL,
+ content TEXT NOT NULL,
createdat TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'utc')
)`
if _, err := db.Exec(q); err != nil {
@@ 63,7 65,10 @@ func (db *DB) CreateMetaCheckpointsIfNotExists() error {
func (db *DB) GetMigrations() ([]migrate.Migration, error) {
migrations := []migrate.Migration{}
- q := `SELECT filename, md5 AS checksum FROM meta`
+ q := `
+ SELECT filename, content, md5 AS checksum
+ FROM meta
+ ORDER BY substring(filename, '^\d+')::int`
err := db.Select(&migrations, q)
return migrations, err
@@ 76,25 81,28 @@ func (db *DB) GetMetaCheckpoints(filename string) ([]string, error) {
return checkpoints, err
}
-func (db *DB) UpsertMigration(filename, checksum string) error {
+func (db *DB) UpsertMigration(filename, content, checksum string) error {
q := `
- INSERT INTO meta (filename, md5) VALUES ($1, $2)
- ON CONFLICT UPDATE md5=$3`
- _, err := db.Exec(q, filename, checksum, checksum)
+ INSERT INTO meta (filename, content, md5) VALUES ($1, $2, $3)
+ ON CONFLICT (filename) DO UPDATE SET md5=$4, content=$5`
+ _, err := db.Exec(q, filename, content, checksum, checksum, content)
return err
}
-func (db *DB) InsertMetaCheckpoint(filename, checksum string, idx int) error {
+func (db *DB) InsertMetaCheckpoint(
+ filename, content, checksum string,
+ idx int,
+) error {
q := `
- INSERT INTO metacheckpoints (filename, idx, md5)
- VALUES ($1, $2, $3)`
- _, err := db.Exec(q, filename, idx, checksum)
+ INSERT INTO metacheckpoints (filename, content, idx, md5)
+ VALUES ($1, $2, $3, $4)`
+ _, err := db.Exec(q, filename, content, idx, checksum)
return err
}
-func (db *DB) InsertMigration(filename, checksum string) error {
- q := `INSERT INTO meta (filename, md5) VALUES ($1, $2)`
- _, err := db.Exec(q, filename, checksum)
+func (db *DB) InsertMigration(filename, content, checksum string) error {
+ q := `INSERT INTO meta (filename, content, md5) VALUES ($1, $2, $3)`
+ _, err := db.Exec(q, filename, content, checksum)
return err
}
@@ 104,6 112,26 @@ func (db *DB) DeleteMetaCheckpoints() error {
return err
}
+func (db *DB) CreateMetaVersionIfNotExists() (int, error) {
+ q := `CREATE TABLE IF NOT EXISTS metaversion (
+ version INTEGER NOT NULL
+ )`
+ if _, err := db.Exec(q); err != nil {
+ return 0, errors.Wrap(err, "create metaversion table")
+ }
+
+ var version int
+ q = `SELECT version FROM metaversion`
+ err := db.Get(&version, q)
+ switch {
+ case err == sql.ErrNoRows:
+ return 0, nil
+ case err != nil:
+ return 0, errors.Wrap(err, "get version")
+ }
+ return version, nil
+}
+
func (db *DB) Open() error {
var err error
db.DB, err = sqlx.Open("postgres", db.connURL)
@@ 112,3 140,75 @@ func (db *DB) Open() error {
}
return nil
}
+
+// UpgradeToV1 migrates existing meta tables to the v1 format. Complete any
+// migrations before running this function; this will not succeed if have any
+// existing metacheckpoints.
+func (db *DB) UpgradeToV1(migrations []migrate.Migration) (err error) {
+ // Begin Tx
+ tx, err := db.Beginx()
+ if err != nil {
+ return errors.Wrap(err, "begin tx")
+ }
+ defer func() {
+ if err != nil {
+ _ = tx.Rollback()
+ return
+ }
+ err = tx.Commit()
+ }()
+
+ // Remove the uniqueness constraint from md5
+ q := `ALTER TABLE meta DROP CONSTRAINT meta_md5_key`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "remove md5 unique")
+ return
+ }
+
+ // Add a content column to record the exact migration that ran
+ // alongside the md5, insert the appropriate data, then set not null
+ q = `ALTER TABLE meta ADD COLUMN content TEXT`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "add content column")
+ return
+ }
+ for _, m := range migrations {
+ q = `UPDATE meta SET content=$1 WHERE filename=$2`
+ if _, err = tx.Exec(q, m.Content, m.Filename); err != nil {
+ err = errors.Wrap(err, "update meta content")
+ return
+ }
+ }
+ q = `ALTER TABLE meta ALTER COLUMN content SET NOT NULL`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "update meta content not null")
+ return
+ }
+
+ // Add the content column to metacheckpoints
+ q = `
+ ALTER TABLE metacheckpoints
+ ADD COLUMN IF NOT EXISTS content TEXT NOT NULL`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "add metacheckpoints content")
+ return
+ }
+
+ q = `
+ CREATE TABLE IF NOT EXISTS metaversion (version INTEGER NOT NULL)`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "create metaversion table")
+ return
+ }
+ q = `DELETE FROM metaversion`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "delete metaversion")
+ return
+ }
+ q = `INSERT INTO metaversion (version) VALUES (1)`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "insert metaversion")
+ return
+ }
+ return nil
+}
A postgres/postgres_test.go => postgres/postgres_test.go +272 -0
@@ 0,0 1,272 @@
+package postgres
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/egtann/migrate"
+ "github.com/jmoiron/sqlx"
+ _ "github.com/lib/pq"
+ "github.com/pkg/errors"
+)
+
+const checkpointFile = "2.sql"
+
+func TestMain(m *testing.M) {
+ path := filepath.Join("..", "test.env")
+ err := parseEnv(path)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to parse %s: %s\n", path, err)
+ os.Exit(1)
+ }
+ os.Exit(m.Run())
+}
+
+func TestCreateMetaIfNotExists(t *testing.T) {
+ db := newDB(t)
+
+ err := db.CreateMetaIfNotExists()
+ check(t, err)
+
+ var tmp []int
+ err = db.DB.Select(&tmp, `SELECT 1 FROM meta`)
+ check(t, err)
+}
+
+func TestCreateMetaCheckpointsIfNotExists(t *testing.T) {
+ db := newDB(t)
+
+ err := db.CreateMetaCheckpointsIfNotExists()
+ check(t, err)
+
+ var tmp []int
+ err = db.DB.Select(&tmp, `SELECT 1 FROM metacheckpoints`)
+ check(t, err)
+}
+
+func TestGetMigrations(t *testing.T) {
+ db := setupDBV1(t)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 1 {
+ t.Fatalf("expected 1 migration, got %d", len(ms))
+ }
+}
+
+func TestGetMetaCheckpoints(t *testing.T) {
+ db := setupDBV1(t)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 1 {
+ t.Fatal("expected 1 checkpoint")
+ }
+}
+
+func TestUpsertMigration(t *testing.T) {
+ db := setupDBV1(t)
+
+ // Test update
+ err := db.UpsertMigration("1.sql", "SELECT 1;", "md5")
+ check(t, err)
+
+ // Test insert
+ err = db.UpsertMigration("3.sql", "SELECT 3;", "md5")
+ check(t, err)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 2 {
+ t.Fatalf("expected 2 migrations, got %d", len(ms))
+ }
+}
+
+func TestInsertMetaCheckpoint(t *testing.T) {
+ db := setupDBV1(t)
+
+ err := db.InsertMetaCheckpoint(checkpointFile, "SELECT 3;", "md5", 1)
+ check(t, err)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 2 {
+ t.Fatal("expected 2 checkpoints")
+ }
+}
+
+func TestInsertMigration(t *testing.T) {
+ db := setupDBV1(t)
+
+ err := db.InsertMigration("3.sql", "SELECT 3;", "md5")
+ check(t, err)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 2 {
+ t.Fatal("expected 2 migrations")
+ }
+}
+
+func TestDeleteMetaCheckpoints(t *testing.T) {
+ db := setupDBV1(t)
+
+ err := db.DeleteMetaCheckpoints()
+ check(t, err)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 0 {
+ t.Fatal("expected 0 checkpoints")
+ }
+}
+
+func check(t *testing.T, err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func newDB(t *testing.T) *DB {
+ db := createDBAndOpen(t)
+ return &DB{DB: db}
+}
+
+func parseEnv(filename string) error {
+ fi, err := os.Open(filename)
+ if err != nil {
+ return err
+ }
+ defer fi.Close()
+
+ scn := bufio.NewScanner(fi)
+ for i := 1; scn.Scan(); i++ {
+ line := scn.Text()
+ if line == "" {
+ continue
+ }
+ parts := strings.SplitN(line, "=", 2)
+ if len(parts) != 2 {
+ return fmt.Errorf("bad line %d: %s", i, line)
+ }
+ if err = os.Setenv(parts[0], parts[1]); err != nil {
+ return errors.Wrap(err, "set env")
+ }
+ }
+ if err = scn.Err(); err != nil {
+ return errors.Wrap(err, "scan")
+ }
+ return nil
+}
+
+func createDBAndOpen(t *testing.T) *sqlx.DB {
+ db, err := sqlx.Open("postgres", dsnForDB(""))
+ check(t, err)
+
+ q := `DROP DATABASE IF EXISTS migrate_test`
+ _, err = db.Exec(q)
+ check(t, err)
+
+ q = `CREATE DATABASE migrate_test`
+ _, err = db.Exec(q)
+ check(t, err)
+
+ err = db.Close()
+ check(t, err)
+
+ db, err = sqlx.Open("postgres", dsnForDB("migrate_test"))
+ check(t, err)
+
+ t.Cleanup(teardown(db))
+ return db
+}
+
+func dsnForDB(dbName string) string {
+ user := os.Getenv("POSTGRES_USER")
+ if user == "" {
+ panic("missing POSTGRES_USER")
+ }
+ pass := os.Getenv("POSTGRES_PASSWORD")
+ if pass == "" {
+ panic("missing POSTGRES_PASSWORD")
+ }
+ host := os.Getenv("POSTGRES_HOST")
+ if host == "" {
+ panic("missing POSTGRES_HOST")
+ }
+ params := "sslmode=disable&connect_timeout=1"
+ return fmt.Sprintf("postgres://%s:%s@%s/%s?%s", user, pass, host,
+ dbName, params)
+}
+
+func teardown(db *sqlx.DB) func() {
+ return func() {
+ if err := db.Close(); err != nil {
+ return
+ }
+
+ var err error
+ db, err = sqlx.Open("postgres", dsnForDB(""))
+ if err != nil {
+ return
+ }
+ defer db.Close()
+
+ q := `DROP DATABASE migrate_test`
+ _, err = db.Exec(q)
+ if err != nil {
+ return
+ }
+ }
+}
+
+func setupDBV1(t *testing.T) *DB {
+ db := setupDBV0(t)
+ err := db.UpgradeToV1([]migrate.Migration{{
+ Filename: "1.sql",
+ Checksum: "md5",
+ Content: "SELECT 1;",
+ }})
+ check(t, err)
+
+ q := `
+ INSERT INTO metacheckpoints (idx, filename, content, md5)
+ VALUES ($1, $2, $3, $4)`
+ _, err = db.DB.Exec(q, 0, checkpointFile, "SELECT 2;", "md5")
+ check(t, err)
+
+ return db
+}
+
+func setupDBV0(t *testing.T) *DB {
+ db := newDB(t)
+
+ q := `CREATE TABLE IF NOT EXISTS meta (
+ filename VARCHAR(255) UNIQUE NOT NULL,
+ md5 VARCHAR(255) UNIQUE NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ )`
+ _, err := db.DB.Exec(q)
+ check(t, err)
+
+ q = `CREATE TABLE IF NOT EXISTS metacheckpoints (
+ filename VARCHAR(255) NOT NULL,
+ idx INTEGER NOT NULL,
+ md5 VARCHAR(255) NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (filename, idx)
+ )`
+ _, err = db.DB.Exec(q)
+ check(t, err)
+
+ q = `INSERT INTO meta (filename, md5) VALUES ($1, $2)`
+ _, err = db.DB.Exec(q, "1.sql", "md5")
+ check(t, err)
+
+ return db
+}
M sqlite/sqlite.go => sqlite/sqlite.go +173 -12
@@ 1,6 1,8 @@
package sqlite
import (
+ "database/sql"
+
"github.com/egtann/migrate"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
@@ 23,6 25,7 @@ func (db *DB) CreateMetaIfNotExists() error {
q := `CREATE TABLE IF NOT EXISTS meta (
filename TEXT UNIQUE NOT NULL,
md5 TEXT NOT NULL,
+ content TEXT NOT NULL,
createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)`
if _, err := db.Exec(q); err != nil {
@@ 34,6 37,7 @@ func (db *DB) CreateMetaIfNotExists() error {
func (db *DB) CreateMetaCheckpointsIfNotExists() error {
q := `CREATE TABLE IF NOT EXISTS metacheckpoints (
filename TEXT NOT NULL,
+ content TEXT NOT NULL,
idx INTEGER NOT NULL,
md5 TEXT NOT NULL,
createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ 47,7 51,7 @@ func (db *DB) CreateMetaCheckpointsIfNotExists() error {
func (db *DB) GetMigrations() ([]migrate.Migration, error) {
migrations := []migrate.Migration{}
- q := `SELECT filename, md5 AS checksum FROM meta`
+ q := `SELECT filename, content, md5 AS checksum FROM meta`
err := db.Select(&migrations, q)
return migrations, err
@@ 60,25 64,28 @@ func (db *DB) GetMetaCheckpoints(filename string) ([]string, error) {
return checkpoints, err
}
-func (db *DB) UpsertMigration(filename, checksum string) error {
+func (db *DB) UpsertMigration(filename, content, checksum string) error {
q := `
- INSERT INTO meta (filename, md5) VALUES ($1, $2)
- ON CONFLICT UPDATE md5=$3`
- _, err := db.Exec(q, filename, checksum, checksum)
+ INSERT INTO meta (filename, content, md5) VALUES ($1, $2, $3)
+ ON CONFLICT(filename) DO UPDATE SET md5=$4, content=$5`
+ _, err := db.Exec(q, filename, content, checksum, checksum, content)
return err
}
-func (db *DB) InsertMetaCheckpoint(filename, checksum string, idx int) error {
+func (db *DB) InsertMetaCheckpoint(
+ filename, content, checksum string,
+ idx int,
+) error {
q := `
- INSERT INTO metacheckpoints (filename, idx, md5)
- VALUES ($1, $2, $3)`
- _, err := db.Exec(q, filename, idx, checksum)
+ INSERT INTO metacheckpoints (filename, content, idx, md5)
+ VALUES ($1, $2, $3, $4)`
+ _, err := db.Exec(q, filename, content, idx, checksum)
return err
}
-func (db *DB) InsertMigration(filename, checksum string) error {
- q := `INSERT INTO meta (filename, md5) VALUES ($1, $2)`
- _, err := db.Exec(q, filename, checksum)
+func (db *DB) InsertMigration(filename, content, checksum string) error {
+ q := `INSERT INTO meta (filename, content, md5) VALUES ($1, $2, $3)`
+ _, err := db.Exec(q, filename, content, checksum)
return err
}
@@ 88,6 95,26 @@ func (db *DB) DeleteMetaCheckpoints() error {
return err
}
+func (db *DB) CreateMetaVersionIfNotExists() (int, error) {
+ q := `CREATE TABLE IF NOT EXISTS metaversion (
+ version INTEGER NOT NULL
+ )`
+ if _, err := db.Exec(q); err != nil {
+ return 0, errors.Wrap(err, "create metaversion table")
+ }
+
+ var version int
+ q = `SELECT version FROM metaversion`
+ err := db.Get(&version, q)
+ switch {
+ case err == sql.ErrNoRows:
+ return 0, nil
+ case err != nil:
+ return 0, errors.Wrap(err, "get version")
+ }
+ return version, nil
+}
+
func (db *DB) Open() error {
var err error
db.DB, err = sqlx.Open("sqlite3", db.filepath)
@@ 96,3 123,137 @@ func (db *DB) Open() error {
}
return nil
}
+
+// UpgradeToV1 migrates existing meta tables to the v1 format. Complete any
+// migrations before running this function; this will not succeed if have any
+// existing metacheckpoints.
+func (db *DB) UpgradeToV1(migrations []migrate.Migration) (err error) {
+ // Begin Tx
+ tx, err := db.Beginx()
+ if err != nil {
+ return errors.Wrap(err, "begin tx")
+ }
+ defer func() {
+ if err != nil {
+ _ = tx.Rollback()
+ return
+ }
+ err = tx.Commit()
+ }()
+
+ // Remove the uniqueness constraint from md5. sqlite doesn't support
+ // MODIFY COLUMN so we recreate the table.
+ q := `CREATE TABLE metatmp (
+ filename TEXT UNIQUE NOT NULL,
+ md5 TEXT NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ )`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "create metatmp")
+ return
+ }
+ q = `INSERT INTO metatmp SELECT filename, md5, createdat FROM meta`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "insert metatmp")
+ }
+ q = `DROP TABLE meta`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "drop meta")
+ }
+ q = `ALTER TABLE metatmp RENAME TO meta`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "rename metatmp 1")
+ return
+ }
+
+ // Add a content column to record the exact migration that ran
+ // alongside the md5, insert the appropriate data, then set not null
+ q = `ALTER TABLE meta ADD COLUMN content TEXT`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "add content column")
+ return
+ }
+ for _, m := range migrations {
+ q = `UPDATE meta SET content=$1 WHERE filename=$2`
+ if _, err = tx.Exec(q, m.Content, m.Filename); err != nil {
+ err = errors.Wrap(err, "update meta content")
+ return
+ }
+ }
+
+ // Once again, sqlite3 doesn't support modify column, so we have to
+ // recreate our tables
+ q = `CREATE TABLE metatmp (
+ filename TEXT UNIQUE NOT NULL,
+ content TEXT NOT NULL,
+ md5 TEXT NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ )`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "create metatmp")
+ return
+ }
+ q = `
+ INSERT INTO metatmp
+ SELECT filename, content, md5, createdat FROM meta`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "")
+ }
+ q = `DROP TABLE meta`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "drop meta")
+ return
+ }
+ q = `ALTER TABLE metatmp RENAME TO meta`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "rename metatmp 2")
+ return
+ }
+
+ // Add the content column to metacheckpoints. Same song and dance as above
+ q = `CREATE TABLE metacheckpointstmp (
+ filename TEXT NOT NULL,
+ content TEXT NOT NULL,
+ idx INTEGER NOT NULL,
+ md5 TEXT NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (filename, idx)
+ )`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "create metacheckpointstmp")
+ return
+ }
+ q = `
+ INSERT INTO metacheckpointstmp
+ SELECT filename, md5, createdat FROM meta`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "insert metacheckpointstmp")
+ }
+ q = `DROP TABLE metacheckpoints`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "drop metacheckpoints")
+ return
+ }
+ q = `ALTER TABLE metacheckpointstmp RENAME TO metacheckpoints`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "rename metacheckpointstmp")
+ return
+ }
+
+ q = `CREATE TABLE IF NOT EXISTS metaversion (version INTEGER NOT NULL)`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "create metaversion table")
+ return
+ }
+ q = `DELETE FROM metaversion`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "delete metaversion")
+ return
+ }
+ q = `INSERT INTO metaversion (version) VALUES (1)`
+ if _, err = tx.Exec(q); err != nil {
+ err = errors.Wrap(err, "update metaversion")
+ return
+ }
+ return nil
+}
A sqlite/sqlite_test.go => sqlite/sqlite_test.go +178 -0
@@ 0,0 1,178 @@
+package sqlite
+
+import (
+ "testing"
+
+ "github.com/egtann/migrate"
+ "github.com/jmoiron/sqlx"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+const checkpointFile = "2.sql"
+
+func TestCreateMetaIfNotExists(t *testing.T) {
+ t.Parallel()
+ db := newDB()
+
+ err := db.CreateMetaIfNotExists()
+ check(t, err)
+
+ var tmp []int
+ err = db.DB.Select(&tmp, `SELECT 1 FROM meta`)
+ check(t, err)
+}
+
+func TestCreateMetaCheckpointsIfNotExists(t *testing.T) {
+ t.Parallel()
+ db := newDB()
+ err := db.CreateMetaCheckpointsIfNotExists()
+ check(t, err)
+
+ var tmp []int
+ err = db.DB.Select(&tmp, `SELECT 1 FROM metacheckpoints`)
+ check(t, err)
+}
+
+func TestGetMigrations(t *testing.T) {
+ t.Parallel()
+ db := setupDBV1(t)
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 1 {
+ t.Fatal("expected 1 migration")
+ }
+}
+
+func TestGetMetaCheckpoints(t *testing.T) {
+ t.Parallel()
+ db := setupDBV1(t)
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 1 {
+ t.Fatal("expected 1 checkpoint")
+ }
+}
+
+func TestUpsertMigration(t *testing.T) {
+ t.Parallel()
+ db := setupDBV1(t)
+
+ // Test update
+ err := db.UpsertMigration("1.sql", "SELECT 1;", "md5")
+ check(t, err)
+
+ // Test insert
+ err = db.UpsertMigration("3.sql", "SELECT 3;", "md5")
+ check(t, err)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 2 {
+ t.Fatal("expected 2 migrations")
+ }
+}
+
+func TestInsertMetaCheckpoint(t *testing.T) {
+ t.Parallel()
+ db := setupDBV1(t)
+
+ err := db.InsertMetaCheckpoint(checkpointFile, "SELECT 3;", "md5", 1)
+ check(t, err)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 2 {
+ t.Fatal("expected 2 checkpoints")
+ }
+}
+
+func TestInsertMigration(t *testing.T) {
+ t.Parallel()
+ db := setupDBV1(t)
+
+ err := db.InsertMigration("3.sql", "SELECT 3;", "md5")
+ check(t, err)
+
+ ms, err := db.GetMigrations()
+ check(t, err)
+ if len(ms) != 2 {
+ t.Fatal("expected 2 migrations")
+ }
+}
+
+func TestDeleteMetaCheckpoints(t *testing.T) {
+ t.Parallel()
+ db := setupDBV1(t)
+
+ err := db.DeleteMetaCheckpoints()
+ check(t, err)
+
+ mcs, err := db.GetMetaCheckpoints(checkpointFile)
+ check(t, err)
+ if len(mcs) != 0 {
+ t.Fatal("expected 0 checkpoints")
+ }
+}
+
+func check(t *testing.T, err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func newDB() *DB {
+ // Every database connection sees a different database, which is
+ // perfect, as that lets us run tests in parallel.
+ db, err := sqlx.Open("sqlite3", ":memory:")
+ if err != nil {
+ panic(err)
+ }
+ return &DB{DB: db}
+}
+
+func setupDBV1(t *testing.T) *DB {
+ db := setupDBV0(t)
+ err := db.UpgradeToV1([]migrate.Migration{{
+ Filename: "1.sql",
+ Checksum: "md5",
+ Content: "SELECT 1;",
+ }})
+ check(t, err)
+
+ q := `
+ INSERT INTO metacheckpoints (idx, filename, content, md5)
+ VALUES ($1, $2, $3, $4)`
+ _, err = db.DB.Exec(q, 0, checkpointFile, "SELECT 2;", "md5")
+ check(t, err)
+
+ return db
+}
+
+func setupDBV0(t *testing.T) *DB {
+ db := newDB()
+
+ q := `CREATE TABLE IF NOT EXISTS meta (
+ filename TEXT UNIQUE NOT NULL,
+ md5 TEXT UNIQUE NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+ )`
+ _, err := db.DB.Exec(q)
+ check(t, err)
+
+ q = `CREATE TABLE IF NOT EXISTS metacheckpoints (
+ filename TEXT NOT NULL,
+ idx INTEGER NOT NULL,
+ md5 TEXT NOT NULL,
+ createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (filename, idx)
+ )`
+ _, err = db.DB.Exec(q)
+ check(t, err)
+
+ q = `INSERT INTO meta (filename, md5) VALUES ($1, $2)`
+ _, err = db.DB.Exec(q, "1.sql", "md5")
+ check(t, err)
+
+ return db
+}
M store.go => store.go +7 -3
@@ 8,14 8,18 @@ type Store interface {
Open() error
Exec(string, ...interface{}) (sql.Result, error)
+ // CreateMetaversionIfNotExists and report the current version.
+ CreateMetaVersionIfNotExists() (int, error)
CreateMetaIfNotExists() error
CreateMetaCheckpointsIfNotExists() error
GetMigrations() ([]Migration, error)
- InsertMigration(filename, checksum string) error
- UpsertMigration(filename, checksum string) error
+ InsertMigration(filename, content, checksum string) error
+ UpsertMigration(filename, content, checksum string) error
GetMetaCheckpoints(string) ([]string, error)
- InsertMetaCheckpoint(filename, checksum string, idx int) error
+ InsertMetaCheckpoint(filename, content, checksum string, idx int) error
DeleteMetaCheckpoints() error
+
+ UpgradeToV1([]Migration) error
}
A test.env.example => test.env.example +7 -0
@@ 0,0 1,7 @@
+MYSQL_USER=root
+MYSQL_PASSWORD=password
+MYSQL_HOST=127.0.0.1:3306
+
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=password
+POSTGRES_HOST=127.0.0.1:5432