Initial commit with the lib version of my personal migration tool
This commit is contained in:
commit
f5da41703d
6 changed files with 385 additions and 0 deletions
94
.gitignore
vendored
Normal file
94
.gitignore
vendored
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
# Created by https://www.gitignore.io/api/go,linux,macos,windows
|
||||||
|
# Edit at https://www.gitignore.io/?templates=go,linux,macos,windows
|
||||||
|
|
||||||
|
### Go ###
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
### Go Patch ###
|
||||||
|
/vendor/
|
||||||
|
/Godeps/
|
||||||
|
|
||||||
|
### Linux ###
|
||||||
|
*~
|
||||||
|
|
||||||
|
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||||
|
.fuse_hidden*
|
||||||
|
|
||||||
|
# KDE directory preferences
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# Linux trash folder which might appear on any partition or disk
|
||||||
|
.Trash-*
|
||||||
|
|
||||||
|
# .nfs files are created when an open file is removed but is still being accessed
|
||||||
|
.nfs*
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# End of https://www.gitignore.io/api/go,linux,macos,windows
|
9
go.mod
Normal file
9
go.mod
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module github.com/joncalhoun/migrate
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jmoiron/sqlx v1.2.0
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||||
|
google.golang.org/appengine v1.6.6 // indirect
|
||||||
|
)
|
18
go.sum
Normal file
18
go.sum
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
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/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
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=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
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/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.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
136
sqlx.go
Normal file
136
sqlx.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package migrate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sqlx is a migrator that uses github.com/jmoiron/sqlx
|
||||||
|
type Sqlx struct {
|
||||||
|
Migrations []SqlxMigration
|
||||||
|
// Printf is used to print out additional information during a migration, such
|
||||||
|
// as which step the migration is currently on. It can be replaced with any
|
||||||
|
// custom printf function, including one that just ignores inputs. If nil it
|
||||||
|
// will default to fmt.Printf.
|
||||||
|
Printf func(format string, a ...interface{}) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate will run the migrations using the provided db connection.
|
||||||
|
func (s *Sqlx) Migrate(sqlDB *sql.DB, dialect string) error {
|
||||||
|
db := sqlx.NewDb(sqlDB, dialect)
|
||||||
|
|
||||||
|
s.printf("Creating/checking migrations table...\n")
|
||||||
|
err := s.createMigrationTable(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, m := range s.Migrations {
|
||||||
|
var found string
|
||||||
|
err := db.Get(&found, "SELECT id FROM migrations WHERE id=$1", m.ID)
|
||||||
|
switch err {
|
||||||
|
case sql.ErrNoRows:
|
||||||
|
s.printf("Running migration: %v\n", m.ID)
|
||||||
|
// we need to run the migration so we continue to code below
|
||||||
|
case nil:
|
||||||
|
s.printf("Skipping migration: %v\n", m.ID)
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("looking up migration by id: %w", err)
|
||||||
|
}
|
||||||
|
err = s.runMigration(db, m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sqlx) printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
printf := s.Printf
|
||||||
|
if printf == nil {
|
||||||
|
printf = fmt.Printf
|
||||||
|
}
|
||||||
|
return printf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sqlx) createMigrationTable(db *sqlx.DB) error {
|
||||||
|
_, err := db.Exec("CREATE TABLE IF NOT EXISTS migrations (id TEXT PRIMARY KEY )")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating migrations table: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sqlx) runMigration(db *sqlx.DB, m SqlxMigration) error {
|
||||||
|
errorf := func(err error) error { return fmt.Errorf("running migration: %w", err) }
|
||||||
|
|
||||||
|
tx, err := db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
return errorf(err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec("INSERT INTO migrations (id) VALUES ($1)", m.ID)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return errorf(err)
|
||||||
|
}
|
||||||
|
err = m.Migrate(tx)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return errorf(err)
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return errorf(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqlxMigration is a unique ID plus a function that uses a sqlx transaction
|
||||||
|
// to perform a database migration step.
|
||||||
|
//
|
||||||
|
// Note: Long term this could have a Rollback field if we wanted to support
|
||||||
|
// that.
|
||||||
|
type SqlxMigration struct {
|
||||||
|
ID string
|
||||||
|
Migrate func(tx *sqlx.Tx) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqlxQueryMigration will create a SqlxMigration using the provided id and
|
||||||
|
// query string. It is a helper function designed to simplify the process of
|
||||||
|
// creating migrations that only depending on a SQL query string.
|
||||||
|
func SqlxQueryMigration(id, query string) SqlxMigration {
|
||||||
|
m := SqlxMigration{
|
||||||
|
ID: id,
|
||||||
|
Migrate: func(tx *sqlx.Tx) error {
|
||||||
|
_, err := tx.Exec(query)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqlxFileMigration will create a SqlxMigration using the provided file.
|
||||||
|
func SqlxFileMigration(id, filename string) SqlxMigration {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
// We could return a migration that errors when the migration is run, but I
|
||||||
|
// think it makes more sense to panic here.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fileBytes, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
m := SqlxMigration{
|
||||||
|
ID: id,
|
||||||
|
Migrate: func(tx *sqlx.Tx) error {
|
||||||
|
_, err := tx.Exec(string(fileBytes))
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
122
sqlx_test.go
Normal file
122
sqlx_test.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package migrate_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/joncalhoun/migrate"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sqliteInMem(t *testing.T) *sql.DB {
|
||||||
|
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Open() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Close() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add more exhaustive testing. Perhaps try different dialects? Good enough for now tho.
|
||||||
|
func TestSqlx(t *testing.T) {
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
db := sqliteInMem(t)
|
||||||
|
migrator := migrate.Sqlx{
|
||||||
|
Printf: func(format string, args ...interface{}) (int, error) {
|
||||||
|
t.Logf(format, args...)
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
Migrations: []migrate.SqlxMigration{
|
||||||
|
migrate.SqlxQueryMigration("001_create_courses", createCoursesSql),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := migrator.Migrate(db, "sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Migrate() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec("INSERT INTO courses (name) VALUES ($1) ", "cor_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("db.Exec() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("existing migrations", func(t *testing.T) {
|
||||||
|
db := sqliteInMem(t)
|
||||||
|
migrator := migrate.Sqlx{
|
||||||
|
Printf: func(format string, args ...interface{}) (int, error) {
|
||||||
|
t.Logf(format, args...)
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
Migrations: []migrate.SqlxMigration{
|
||||||
|
migrate.SqlxQueryMigration("001_create_courses", createCoursesSql),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := migrator.Migrate(db, "sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Migrate() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec("INSERT INTO courses (name) VALUES ($1) ", "cor_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("db.Exec() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the real test
|
||||||
|
migrator = migrate.Sqlx{
|
||||||
|
Printf: func(format string, args ...interface{}) (int, error) {
|
||||||
|
t.Logf(format, args...)
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
Migrations: []migrate.SqlxMigration{
|
||||||
|
migrate.SqlxQueryMigration("001_create_courses", createCoursesSql),
|
||||||
|
migrate.SqlxQueryMigration("002_create_users", createUsersSql),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = migrator.Migrate(db, "sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Migrate() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec("INSERT INTO users (email) VALUES ($1) ", "abc@test.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("db.Exec() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("file", func(t *testing.T) {
|
||||||
|
db := sqliteInMem(t)
|
||||||
|
migrator := migrate.Sqlx{
|
||||||
|
Printf: func(format string, args ...interface{}) (int, error) {
|
||||||
|
t.Logf(format, args...)
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
Migrations: []migrate.SqlxMigration{
|
||||||
|
migrate.SqlxFileMigration("001_create_widgets", "testdata/widgets.sql"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := migrator.Migrate(db, "sqlite3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Migrate() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec("INSERT INTO widgets (color, price) VALUES ($1, $2)", "red", 1200)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("db.Exec() err = %v; want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var createCoursesSql = `
|
||||||
|
CREATE TABLE courses (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
name text
|
||||||
|
);`
|
||||||
|
|
||||||
|
var createUsersSql = `
|
||||||
|
CREATE TABLE users (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
email text UNIQUE NOT NULL
|
||||||
|
);`
|
6
testdata/widgets.sql
vendored
Normal file
6
testdata/widgets.sql
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE widgets (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
color text NOT NULL,
|
||||||
|
price integer
|
||||||
|
);
|
||||||
|
|
Loading…
Reference in a new issue