temporary holding ground for migrate
This commit is contained in:
commit
f7c677a3f5
6
doc.go
Normal file
6
doc.go
Normal file
@ -0,0 +1,6 @@
|
||||
package migrate
|
||||
|
||||
// migrate is a package for SQL datbase migrations in the spirit of dbstore(rsc.io/dbstore)
|
||||
// it is intended to keep its footprint small, requiring only an addiutional table in the database
|
||||
// there is no rollback support as you should only ever roll forward.
|
||||
// uses SQL99 compatible SQL only.
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/jchenry/tmp/migrate
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.7
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
100
migrate.go
Normal file
100
migrate.go
Normal file
@ -0,0 +1,100 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const table = "dbversion"
|
||||
|
||||
var tableCreateSql = "CREATE TABLE " + table + ` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
description VARCHAR,
|
||||
applied TIMESTAMP
|
||||
);`
|
||||
var tableCheckSql = "SELECT * FROM " + table + ";"
|
||||
var versionCheckSql = "SELECT id FROM " + table + " ORDER BY id DESC LIMIT 0, 1;"
|
||||
var versionInsertSql = "INSERT INTO " + table + "(description, applied) VALUES (?,?);"
|
||||
|
||||
type Error struct {
|
||||
description string
|
||||
wrapped error
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("%s: %v", e.description, e.wrapped)
|
||||
}
|
||||
|
||||
func (e Error) Unwrap() error {
|
||||
return e.wrapped
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Description string
|
||||
F func(ctx Context) error
|
||||
}
|
||||
|
||||
type Context interface {
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
func Apply(ctx Context, migrations []Record) (err error) {
|
||||
if err = initialize(ctx); err == nil {
|
||||
var currentVersion int64
|
||||
if currentVersion, err = dbVersion(ctx); err == nil {
|
||||
migrations = migrations[currentVersion:] // only apply what hasnt been been applied already
|
||||
for i, m := range migrations {
|
||||
if err = apply(ctx, m); err != nil {
|
||||
err = Error{
|
||||
description: fmt.Sprintf("error performing migration \"%s\"", migrations[i].Description),
|
||||
wrapped: err,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func initialize(ctx Context) (err error) {
|
||||
if noVersionTable(ctx) {
|
||||
return createVersionTable(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func noVersionTable(ctx Context) bool {
|
||||
rows, table_check := ctx.Query(tableCheckSql)
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
return table_check != nil
|
||||
}
|
||||
|
||||
func apply(ctx Context, r Record) (err error) {
|
||||
if err = r.F(ctx); err == nil {
|
||||
err = incrementVersion(ctx, r.Description)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func createVersionTable(ctx Context) (err error) {
|
||||
_, err = ctx.Exec(tableCreateSql)
|
||||
return
|
||||
}
|
||||
|
||||
func incrementVersion(ctx Context, description string) (err error) {
|
||||
_, err = ctx.Exec(versionInsertSql, description, time.Now())
|
||||
return
|
||||
}
|
||||
|
||||
func dbVersion(ctx Context) (id int64, err error) {
|
||||
row, err := ctx.Query(versionCheckSql)
|
||||
if row.Next() {
|
||||
err = row.Scan(&id)
|
||||
}
|
||||
return
|
||||
}
|
216
migrate_test.go
Normal file
216
migrate_test.go
Normal file
@ -0,0 +1,216 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func TestHelperFuncs(t *testing.T) {
|
||||
path, db, err := createTestDB()
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
if err = teardownTestDB(path, db); err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateVersionTable(t *testing.T) {
|
||||
path, db, err := createTestDB()
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
err = createVersionTable(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = teardownTestDB(path, db); err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementVersion(t *testing.T) {
|
||||
path, db, err := createTestDB()
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
err = createVersionTable(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
descriptions := []string{
|
||||
"this is a test",
|
||||
"this is another test",
|
||||
}
|
||||
|
||||
for _, d := range descriptions {
|
||||
err = incrementVersion(db, d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := db.Query("SELECT id, description from dbversion")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var id int
|
||||
var description string
|
||||
for r := 1; rows.Next(); r++ {
|
||||
err = rows.Scan(&id, &description)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != r || !strings.EqualFold(description, descriptions[r-1]) {
|
||||
t.Fatalf("first row does not match %d %s: %d %s", id, descriptions[r-1], r, description)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err = teardownTestDB(path, db); err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestDbVersion(t *testing.T) {
|
||||
path, db, err := createTestDB()
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
err = createVersionTable(db)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ver, err := dbVersion(db)
|
||||
if ver != 0 || err != nil {
|
||||
t.Fatalf("version not 0 as expected (actual %d) or err: %#v", ver, err)
|
||||
}
|
||||
|
||||
err = incrementVersion(db, "Test 1")
|
||||
ver, err = dbVersion(db)
|
||||
if ver != 1 {
|
||||
t.Fatalf("version not 1 as expected (actual %d)", ver)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("err on dbversion of first increment: %#v", err)
|
||||
}
|
||||
|
||||
// err = incrementVersion(db, d)
|
||||
|
||||
if err = teardownTestDB(path, db); err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
path, db, err := createTestDB()
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
records :=
|
||||
[]Record{
|
||||
{
|
||||
Description: "create people table",
|
||||
F: func(ctx Context) (err error) {
|
||||
_, err = ctx.Exec(`
|
||||
CREATE TABLE people (
|
||||
given_name VARCHAR(20),
|
||||
surname VARCHAR(30),
|
||||
sex CHAR(1),
|
||||
age SMALLINT);
|
||||
`)
|
||||
return
|
||||
},
|
||||
},
|
||||
{
|
||||
Description: "Insert a person into people",
|
||||
F: func(ctx Context) (err error) {
|
||||
_, err = ctx.Exec(`INSERT INTO people VALUES('Henry','Colin','M', 42)`)
|
||||
return
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = Apply(db, records)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := db.QueryRow("SELECT given_name FROM people")
|
||||
|
||||
var given_name string
|
||||
r.Scan(&given_name)
|
||||
|
||||
if given_name != "Henry" {
|
||||
t.Fatalf("second migration did not complete: %s != %s", given_name, "Henry")
|
||||
}
|
||||
|
||||
// reapply and make sure we dont re-run anything
|
||||
err = Apply(db, records)
|
||||
ver, err := dbVersion(db)
|
||||
if ver != 2 {
|
||||
t.Fatalf("version not 2 as expected (actual %d)", ver)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("err on dbversion of re-apply: %#v", err)
|
||||
}
|
||||
|
||||
// add bad (causes migrate.Error) case here.
|
||||
|
||||
ishouldntHideUserErrors := errors.New("I should fail")
|
||||
|
||||
records = append(records, Record{
|
||||
Description: "Insert a person into people",
|
||||
F: func(ctx Context) (err error) {
|
||||
return ishouldntHideUserErrors
|
||||
},
|
||||
})
|
||||
|
||||
err = Apply(db, records)
|
||||
|
||||
if errors.Unwrap(err) != ishouldntHideUserErrors {
|
||||
t.Fatalf("unexpected error returned that should have been record function error: %#v", err)
|
||||
}
|
||||
ver, err = dbVersion(db)
|
||||
if ver != 2 {
|
||||
t.Fatalf("version not 2 as expected (actual %d) after bad record apply", ver)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("err on dbversion of re-apply with bad record: %#v", err)
|
||||
}
|
||||
|
||||
if err = teardownTestDB(path, db); err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createTestDB() (path string, db *sql.DB, err error) {
|
||||
if f, err := ioutil.TempFile(os.TempDir(), "migrate-test-db"); err == nil {
|
||||
f.Close()
|
||||
if db, err := sql.Open("sqlite3", f.Name()); err == nil {
|
||||
return f.Name(), db, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
func teardownTestDB(path string, db *sql.DB) (err error) {
|
||||
if err = db.Close(); err == nil {
|
||||
err = os.Remove(path)
|
||||
}
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue
Block a user