diff --git a/.travis.yml b/.travis.yml index 373042e..2450d99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,13 @@ env: - MYSQL_HOST=127.0.0.1 - MYSQL_PORT=3306 - MYSQL_DATABASE=kamimai +install: + - go get -d github.com/BurntSushi/toml + - go get -d github.com/go-sql-driver/mysql + - go get -d github.com/lib/pq + - go get -d github.com/stretchr/testify/assert + - go get -d gopkg.in/yaml.v2 + - go get -u github.com/golang/lint/golint before_install: - mysql -e "CREATE DATABASE IF NOT EXISTS kamimai;" -uroot - make init diff --git a/driver/init.go b/driver/init.go index 99a1cea..662ff9d 100644 --- a/driver/init.go +++ b/driver/init.go @@ -8,4 +8,5 @@ const versionTableName = "schema_version" func init() { core.RegisterDriver("mysql", &MySQL{}) + core.RegisterDriver("postgres", &Postgres{}) } diff --git a/driver/postgres.go b/driver/postgres.go new file mode 100644 index 0000000..d41f2bb --- /dev/null +++ b/driver/postgres.go @@ -0,0 +1,217 @@ +package driver + +import ( + "bytes" + "database/sql" + "errors" + "fmt" + "strings" + "sync" + + "github.com/eure/kamimai/core" + "github.com/lib/pq" +) + +type ( + // Postgres driver object. + Postgres struct { + db *sql.DB + tx *sql.Tx + mu sync.Mutex + } +) + +// Open is the first function to be called. +// Check the dsn string and open and verify any connection +// that has to be made. +func (d *Postgres) Open(dsn string) error { + z := strings.SplitN(dsn, "postgres:", 2) + if len(z) != 2 { + return errors.New("invalid data source name of postgres") + } + + db, err := sql.Open("postgres", z[1]) + if err != nil { + return err + } + if err := db.Ping(); err != nil { + return err + } + d.db = db + + return d.Version().Create() +} + +// Close is the last function to be called. +// Close any open connection here. +func (d *Postgres) Close() error { + return d.db.Close() +} + +// Ext returns the sql file extension used by path. The extension is the +// suffix beginning at the final dot in the final element of path; it is +// empty if there is no dot. +func (d *Postgres) Ext() string { + return ".sql" +} + +// Transaction starts a db transaction. The isolation level is dependent on the +// driver. +func (d *Postgres) Transaction(fn func(*sql.Tx) error) error { + d.mu.Lock() + defer func() { + d.tx = nil + d.mu.Unlock() + }() + + tx, err := d.db.Begin() + if err != nil { + return err + } + d.tx = tx + + // Procedure + if err := fn(d.tx); err != nil { + if rberr := d.tx.Rollback(); rberr != nil { + return rberr + } + return err + } + + // Commit + if err := d.tx.Commit(); err != nil { + if rberr := d.tx.Rollback(); rberr != nil { + return rberr + } + return err + } + + return nil +} + +// Exec executes a query without returning any rows. The args are for any +// placeholder parameters in the query. +func (d *Postgres) Exec(query string, args ...interface{}) (sql.Result, error) { + if d.tx != nil { + return d.tx.Exec(query, args...) + } + return d.db.Exec(query, args...) +} + +// Version returns a version interface. +func (d *Postgres) Version() core.Version { + return d +} + +// Migrate applies migration file. +func (d *Postgres) Migrate(m *core.Migration) error { + b, err := m.Read() + if err != nil { + return err + } + + stmts := bytes.Split(b, []byte(";")) + for _, stmt := range stmts { + query := strings.TrimSpace(string(stmt)) + if len(query) == 0 { + continue + } + _, err = d.Exec(query) + if err != nil { + isWarn := strings.Contains(err.Error(), pq.Ewarning) + if !isWarn { + return err + } + } + } + + return nil +} + +// Insert inserts the given migration version. +func (d *Postgres) Insert(val uint64) error { + query := fmt.Sprintf(`INSERT INTO %s (version) VALUES (%d)`, + versionTableName, val) + + _, err := d.Exec(query) + if err != nil { + isWarn := strings.Contains(err.Error(), pq.Ewarning) + if !isWarn { + return err + } + } + return nil +} + +// Delete deletes the given migration version. +func (d *Postgres) Delete(val uint64) error { + query := fmt.Sprintf(`DELETE FROM %s WHERE version = %d`, + versionTableName, val) + + _, err := d.Exec(query) + if err != nil { + isWarn := strings.Contains(err.Error(), pq.Ewarning) + if !isWarn { + return err + } + } + return nil +} + +// Count counts number of row the given migration version. +func (d *Postgres) Count(val uint64) int { + query := fmt.Sprintf(`SELECT count(version) count FROM %s WHERE version = %d`, + versionTableName, val) + + var count int + if err := d.db.QueryRow(query).Scan(&count); err != nil { + return 0 + } + return count +} + +// Current returns the current migration version. +func (d *Postgres) Current() (uint64, error) { + const query = `SELECT version FROM ` + + versionTableName + ` ORDER BY version DESC LIMIT 1` + + var version uint64 + err := d.db.QueryRow(query).Scan(&version) + switch { + case err == sql.ErrNoRows: + return 0, nil + case err != nil: + return 0, err + } + return version, nil +} + +// Create creates +func (d *Postgres) Create() error { + const query = `CREATE TABLE IF NOT EXISTS ` + + versionTableName + ` (version BIGINT NOT NULL PRIMARY KEY);` + + _, err := d.Exec(query) + + if err != nil { + isWarn := strings.Contains(err.Error(), pq.Ewarning) + if !isWarn { + return err + } + } + return nil +} + +// Drop drops +func (d *Postgres) Drop() error { + const query = `DROP TABLE IF EXISTS ` + versionTableName + + _, err := d.Exec(query) + if err != nil { + isWarn := strings.Contains(err.Error(), pq.Ewarning) + if !isWarn { + return err + } + } + return nil +} diff --git a/examples/postgres/config.tml b/examples/postgres/config.tml new file mode 100644 index 0000000..5ea5f37 --- /dev/null +++ b/examples/postgres/config.tml @@ -0,0 +1,8 @@ +[develop] +driver = "postgres" +dsn = "postgres:user=$POSTGRES_USER dbname=hogedb password=$POSTGRES_PASSWORD sslmode=disable" + +[test1] +driver = "postgres" +dsn = "postgres:user=$POSTGRES_USER dbname=hogedb password=$POSTGRES_PASSWORD sslmode=disable" +directory = "test1" diff --git a/examples/postgres/migrations/001_create_product_down.sql b/examples/postgres/migrations/001_create_product_down.sql new file mode 100644 index 0000000..6bfed0e --- /dev/null +++ b/examples/postgres/migrations/001_create_product_down.sql @@ -0,0 +1 @@ +DROP TABLE product; \ No newline at end of file diff --git a/examples/postgres/migrations/001_create_product_up.sql b/examples/postgres/migrations/001_create_product_up.sql new file mode 100644 index 0000000..fe3e558 --- /dev/null +++ b/examples/postgres/migrations/001_create_product_up.sql @@ -0,0 +1,4 @@ +CREATE TABLE product ( + id int4 PRIMARY KEY, + name text +); diff --git a/examples/postgres/test1/001_create_product_down.sql b/examples/postgres/test1/001_create_product_down.sql new file mode 100644 index 0000000..6bfed0e --- /dev/null +++ b/examples/postgres/test1/001_create_product_down.sql @@ -0,0 +1 @@ +DROP TABLE product; \ No newline at end of file diff --git a/examples/postgres/test1/001_create_product_up.sql b/examples/postgres/test1/001_create_product_up.sql new file mode 100644 index 0000000..fe3e558 --- /dev/null +++ b/examples/postgres/test1/001_create_product_up.sql @@ -0,0 +1,4 @@ +CREATE TABLE product ( + id int4 PRIMARY KEY, + name text +); diff --git a/examples/postgres/test1/002_insert_product_down.sql b/examples/postgres/test1/002_insert_product_down.sql new file mode 100644 index 0000000..a12b6bb --- /dev/null +++ b/examples/postgres/test1/002_insert_product_down.sql @@ -0,0 +1 @@ +DELETE FROM product WHERE id in (1, 2); \ No newline at end of file diff --git a/examples/postgres/test1/002_insert_product_up.sql b/examples/postgres/test1/002_insert_product_up.sql new file mode 100644 index 0000000..a5ade20 --- /dev/null +++ b/examples/postgres/test1/002_insert_product_up.sql @@ -0,0 +1 @@ +INSERT INTO product (id, name) VALUES (1, 'prod1'), (2, 'prod2'); \ No newline at end of file diff --git a/examples/postgres/test1/003_insert_product_down.sql b/examples/postgres/test1/003_insert_product_down.sql new file mode 100644 index 0000000..7798e7a --- /dev/null +++ b/examples/postgres/test1/003_insert_product_down.sql @@ -0,0 +1 @@ +DELETE FROM product WHERE id in (3, 4); \ No newline at end of file diff --git a/examples/postgres/test1/003_insert_product_up.sql b/examples/postgres/test1/003_insert_product_up.sql new file mode 100644 index 0000000..723baef --- /dev/null +++ b/examples/postgres/test1/003_insert_product_up.sql @@ -0,0 +1 @@ +INSERT INTO product (id, name) VALUES (3, 'prod3'), (4, 'prod4'); \ No newline at end of file diff --git a/glide.lock b/glide.lock index 0120b5a..684e32c 100644 --- a/glide.lock +++ b/glide.lock @@ -1,22 +1,14 @@ -hash: 22c7ec6a03938054e5df5b0767fdba487906a4d10833d7b0035954a8cdee61a5 -updated: 2016-09-15T09:32:38.908313496Z +hash: c649c713d65d80137a00778f01182f016b2a3a6bb17e07fb860a778da9a093b6 +updated: 2016-09-15T19:37:19.494950142+09:00 imports: - name: github.com/BurntSushi/toml version: 99064174e013895bbd9b025c31100bd1d9b590ca - name: github.com/go-sql-driver/mysql version: 0b58b37b664c21f3010e836f1b931e1d0b0b0685 +- name: github.com/lib/pq + version: 50761b0867bd1d9d069276790bcd4a3bccf2324a + subpackages: + - oid - name: gopkg.in/yaml.v2 version: 31c299268d302dd0aa9a0dcf765a3d58971ac83f -testImports: -- name: github.com/davecgh/go-spew - version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d - subpackages: - - spew -- name: github.com/pmezard/go-difflib - version: d8ed2627bdf02c080bf22230dbb337003b7aba2d - subpackages: - - difflib -- name: github.com/stretchr/testify - version: d77da356e56a7428ad25149ca77381849a6a5232 - subpackages: - - assert +devImports: [] diff --git a/glide.yaml b/glide.yaml index 39a3619..5f17fac 100644 --- a/glide.yaml +++ b/glide.yaml @@ -3,6 +3,7 @@ import: - package: github.com/BurntSushi/toml - package: github.com/go-sql-driver/mysql - package: gopkg.in/yaml.v2 +- package: github.com/lib/pq testImport: - package: github.com/stretchr/testify subpackages: