aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvan Leis <evan.explodes@gmail.com>2017-06-06 21:37:40 -0600
committerEvan Leis <evan.explodes@gmail.com>2017-06-06 21:43:12 -0600
commitc3bdd8c76cbc94b24e213835e9b13c228b3c5ef2 (patch)
tree24df4845069cab852266a74514e4015d9c5ca90e
downloadmigrations-go-c3bdd8c76cbc94b24e213835e9b13c228b3c5ef2.zip
migrations-go-c3bdd8c76cbc94b24e213835e9b13c228b3c5ef2.tar.gz
[initial] written, tested (pg, sqlite), and samples
-rw-r--r--README.md11
-rw-r--r--migration.go60
-rw-r--r--migration_test.go68
-rw-r--r--migrator.go130
-rw-r--r--sample/sample.go72
5 files changed, 341 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7115719
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Migrations
+
+Database migrations made easy!
+
+Tested on [postgres](https://github.com/lib/pq) and [sqlite3](https://github.com/mattn/go-sqlite3) drivers.
+
+## Usage
+
+For an example use case, explore `github.com/explodes/migrations/sample`
+
+
diff --git a/migration.go b/migration.go
new file mode 100644
index 0000000..9b72f4c
--- /dev/null
+++ b/migration.go
@@ -0,0 +1,60 @@
+package migrations
+
+import (
+ "database/sql"
+ "errors"
+)
+
+var (
+ ErrDowngradeNotSupported = errors.New("downgrade not supported")
+)
+
+// Migration is an individual migration
+type Migration interface {
+ // Name returns the name of the migration
+ Name() string
+
+ // Upgrade runs the upwards migration
+ Upgrade(db *sql.DB) error
+
+ // Downgrade performs the undoing of this migration.
+ // If downgrading is not supported, it should return
+ // ErrDowngradeNotSupported
+ Downgrade(db *sql.DB) error
+}
+
+type simpleSqlMigration struct {
+ name string
+ upgrade string
+ downgrade string
+}
+
+// NewSimpleMigration creates a migration that executes
+// the given sql statements on upgrade and downgrade
+//
+// If downgrade is the empty string, downgrade is not
+// supported for this migration
+func NewSimpleMigration(name, upgrade, downgrade string) Migration {
+ return simpleSqlMigration{
+ name: name,
+ upgrade: upgrade,
+ downgrade: downgrade,
+ }
+}
+
+func (m simpleSqlMigration) Name() string {
+ return m.name
+}
+
+func (m simpleSqlMigration) Upgrade(db *sql.DB) error {
+ _, err := db.Exec(m.upgrade)
+ return err
+}
+
+func (m simpleSqlMigration) Downgrade(db *sql.DB) error {
+ if m.downgrade == "" {
+ return ErrDowngradeNotSupported
+ }
+ _, err := db.Exec(m.downgrade)
+ return err
+}
diff --git a/migration_test.go b/migration_test.go
new file mode 100644
index 0000000..507ae32
--- /dev/null
+++ b/migration_test.go
@@ -0,0 +1,68 @@
+package migrations
+
+import (
+ "database/sql"
+ "testing"
+
+ _ "github.com/lib/pq"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+type testMigrationGetter struct{}
+
+func (testMigrationGetter) GetMigration(version int) Migration { return NewSimpleMigration("test", "select 1", "select 1") }
+
+func TestMigrator_Postgres(t *testing.T) {
+ db, err := sql.Open("postgres", "postgresql://test_mig:test_mig@localhost/test_mig?sslmode=disable")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ defer db.Close()
+ if _, err := db.Exec(`DELETE FROM migrations`); err != nil {
+ t.Error(err)
+ }
+ }()
+
+ test_migrator_on_connection(t, db)
+}
+
+func TestMigrator_Sqlite(t *testing.T) {
+ db, err := sql.Open("sqlite3", ":memory:")
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer db.Close()
+
+ test_migrator_on_connection(t, db)
+}
+
+func test_migrator_on_connection(t *testing.T, db *sql.DB) {
+ migrator := NewMigrator(db, testMigrationGetter{})
+
+ if err := migrator.MigrateToVersion(5); err != nil {
+ t.Error(err)
+ } else if version, err := migrator.GetCurrentVersion(); version != 5 || err != nil {
+ t.Errorf("unexpected state: version=%d err=%v", version, err)
+ }
+ if err := migrator.MigrateToVersion(10); err != nil {
+ t.Error(err)
+ } else if version, err := migrator.GetCurrentVersion(); version != 10 || err != nil {
+ t.Errorf("unexpected state: version=%d err=%v", version, err)
+ }
+
+ if err := migrator.MigrateToVersion(5); err != nil {
+ t.Error(err)
+ } else if version, err := migrator.GetCurrentVersion(); version != 5 || err != nil {
+ t.Errorf("unexpected state: version=%d err=%v", version, err)
+ }
+
+ if err := migrator.MigrateToVersion(0); err != nil {
+ t.Error(err)
+ } else if version, err := migrator.GetCurrentVersion(); version != 0 || err != nil {
+ t.Errorf("unexpected state: version=%d err=%v", version, err)
+ }
+
+}
diff --git a/migrator.go b/migrator.go
new file mode 100644
index 0000000..a20cae3
--- /dev/null
+++ b/migrator.go
@@ -0,0 +1,130 @@
+package migrations
+
+import (
+ "database/sql"
+ "fmt"
+ "time"
+)
+
+// MigrationGetter gets migrations by version number
+type MigrationGetter interface {
+ // GetMigrations gets a migration by version number
+ GetMigration(version int) Migration
+}
+
+// Migrator is used to upgrade and downgrade database versions.
+//
+// Version 0 is considered the "clean" slate version, and version 1 is the initial version.
+type Migrator struct {
+ db *sql.DB
+ getter MigrationGetter
+}
+
+// NewMigrator builds a new migrator to be used to run database upgrades
+// and downgrades using the given database connection using the given dialect
+func NewMigrator(db *sql.DB, getter MigrationGetter) *Migrator {
+ return &Migrator{
+ db: db,
+ getter: getter,
+ }
+}
+
+// MigrateToVersion will migrate the database up or down to get the specified version
+func (m *Migrator) MigrateToVersion(version int) error {
+ current, err := m.GetCurrentVersion()
+ if err != nil {
+ return err
+ }
+
+ // nothing to migrate!
+ if current == version {
+ return nil
+ }
+
+ // upgrade or downgrade
+ if version < current {
+ return m.DowngradeDatabase(current, version)
+ } else {
+ return m.UpgradeDatabase(current, version)
+ }
+}
+
+func (m *Migrator) GetCurrentVersion() (version int, err error) {
+ version = 0
+
+ // start our transaction
+ tx, err := m.db.Begin()
+ if err != nil {
+ return
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback()
+ return
+ }
+ err = tx.Commit()
+ }()
+
+ // create our table if it doesn't exist already
+ if _, err = tx.Exec(`CREATE TABLE IF NOT EXISTS migrations (version INTEGER NOT NULL UNIQUE, name TEXT NOT NULL, date_ran TIMESTAMP WITH TIME ZONE NOT NULL)`); err != nil {
+ return
+ }
+
+ var max sql.NullInt64
+ if err = tx.QueryRow(`SELECT MAX(version) FROM migrations`).Scan(&max); err != nil {
+ // if some driver considers MAX(foo) of an empty table as no row,
+ // then we should account for that
+ if err == sql.ErrNoRows {
+ version = 0
+ err = nil
+ return
+ }
+ return
+ }
+ if max.Valid {
+ version = int(max.Int64)
+ }
+ return
+}
+
+func (m *Migrator) recordUpgrade(version int, migration Migration) error {
+ _, err := m.db.Exec(`INSERT INTO migrations (version, name, date_ran) VALUES ($1,$2,$3)`, version+1, migration.Name(), time.Now())
+ return err
+}
+
+func (m *Migrator) UpgradeDatabase(from, to int) error {
+ for version := from; version < to; version ++ {
+ migration := m.getter.GetMigration(version + 1)
+ if migration == nil {
+ return fmt.Errorf("unexpected nil upgrade migration for version %d", version)
+ }
+ if err := migration.Upgrade(m.db); err != nil {
+ return err
+ }
+ if err := m.recordUpgrade(version, migration); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (m *Migrator) recordDowngrade(version int) error {
+ _, err := m.db.Exec(`DELETE FROM migrations WHERE version = $1`, version+1)
+ return err
+}
+
+func (m *Migrator) DowngradeDatabase(from, to int) error {
+ for version := from - 1; version >= to; version -- {
+ migration := m.getter.GetMigration(version + 1)
+ if migration == nil {
+ return fmt.Errorf("unexpected nil downgrade migration for version %d", version)
+ }
+ if err := migration.Downgrade(m.db); err != nil {
+ return err
+ }
+ if err := m.recordDowngrade(version); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/sample/sample.go b/sample/sample.go
new file mode 100644
index 0000000..8e23371
--- /dev/null
+++ b/sample/sample.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "database/sql"
+ "log"
+
+ "github.com/explodes/migrations-go"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+func main() {
+ db, err := sql.Open("sqlite3", ":memory:")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer db.Close()
+
+ migrator := migrations.NewMigrator(db, appMigrations{})
+ if err := migrator.MigrateToVersion(VersionLatest); err != nil {
+ log.Fatal(err)
+ }
+
+ // use db ....
+
+ var count int
+ if err := db.QueryRow(`SELECT COUNT(*) FROM images`).Scan(&count); err != nil {
+ log.Fatal(err)
+ }
+ log.Printf("There are %d images in the database", count)
+}
+
+const (
+ VersionInitial = 1
+ VersionImages = 2
+ VersionLatest = VersionImages
+ DowngradeNotSupported = ``
+ UpgradeInitial = `
+CREATE TABLE users (
+ id BIGSERIAL PRIMARY KEY,
+ username VARCHAR(64),
+ email VARCHAR(128)
+);
+
+CREATE TABLE post (
+ id BIGSERIAL PRIMARY KEY,
+ user_id INT NOT NULL,
+ content TEXT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id)
+);
+ `
+ UpgradeImages = `
+CREATE TABLE images (
+ id BIGSERIAL PRIMARY KEY,
+ url TEXT
+)
+`
+ DowgradeImages = `DROP TABLE images`
+)
+
+type appMigrations struct{}
+
+func (appMigrations) GetMigration(version int) migrations.Migration {
+ switch version {
+ case VersionInitial:
+ // empty string, downgrade not supported
+ return migrations.NewSimpleMigration("initial", UpgradeInitial, DowngradeNotSupported)
+ case VersionImages:
+ return migrations.NewSimpleMigration("create_images_table", UpgradeImages, DowgradeImages)
+ }
+
+ return nil
+}