mirror of https://github.com/pressly/goose.git
Add support for optional migrations.
See the discussion in https://github.com/pressly/goose/issues/7. Credit goes to @dkotson for his gist here: https://github.com/pressly/goose/issues/7#issuecomment-241613482 I've just added that patch to this repository and change the transaction comment to `-- +goose NO TRANSACTIONS`. - Checks the migration file for `-- +goose NO TRANSACTIONS` - Based upon that line, either runs the up/down SQL with or without transactions - Add test to check for transactions - Update README Closes #7.pull/41/head
parent
cdb30a7da1
commit
fa8806ecfd
|
@ -134,6 +134,9 @@ DROP TABLE post;
|
||||||
|
|
||||||
Notice the annotations in the comments. Any statements following `-- +goose Up` will be executed as part of a forward migration, and any statements following `-- +goose Down` will be executed as part of a rollback.
|
Notice the annotations in the comments. Any statements following `-- +goose Up` will be executed as part of a forward migration, and any statements following `-- +goose Down` will be executed as part of a rollback.
|
||||||
|
|
||||||
|
By default, all migrations are run within a transaction. Some statements like `CREATE DATABASE`, however, cannot be run within a transaction. You may optionally add `-- +goose NO TRANSACTIONS` to the top of your migration
|
||||||
|
file in order to skip transactions within that specific migration file. Both Up and Down migrations within this file will be run without transactions.
|
||||||
|
|
||||||
By default, SQL statements are delimited by semicolons - in fact, query statements must end with a semicolon to be properly recognized by goose.
|
By default, SQL statements are delimited by semicolons - in fact, query statements must end with a semicolon to be properly recognized by goose.
|
||||||
|
|
||||||
More complex statements (PL/pgSQL) that have semicolons within them must be annotated with `-- +goose StatementBegin` and `-- +goose StatementEnd` to be properly recognized. For example:
|
More complex statements (PL/pgSQL) that have semicolons within them must be annotated with `-- +goose StatementBegin` and `-- +goose StatementEnd` to be properly recognized. For example:
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- +goose NO TRANSACTIONS --
|
||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE post (
|
||||||
|
id int NOT NULL,
|
||||||
|
title text,
|
||||||
|
body text,
|
||||||
|
PRIMARY KEY(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE post;
|
18
migration.go
18
migration.go
|
@ -63,10 +63,6 @@ func (m *Migration) run(db *sql.DB, direction bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = FinalizeMigration(tx, direction, m.Version); err != nil {
|
|
||||||
log.Fatalf("error finalizing migration %s, quitting. (%v)", filepath.Base(m.Source), err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("OK ", filepath.Base(m.Source))
|
fmt.Println("OK ", filepath.Base(m.Source))
|
||||||
|
@ -121,7 +117,7 @@ func CreateMigration(name, migrationType, dir string, t time.Time) (path string,
|
||||||
|
|
||||||
// Update the version table for the given migration,
|
// Update the version table for the given migration,
|
||||||
// and finalize the transaction.
|
// and finalize the transaction.
|
||||||
func FinalizeMigration(tx *sql.Tx, direction bool, v int64) error {
|
func FinalizeMigrationTx(tx *sql.Tx, direction bool, v int64) error {
|
||||||
|
|
||||||
// XXX: drop goose_db_version table on some minimum version number?
|
// XXX: drop goose_db_version table on some minimum version number?
|
||||||
stmt := GetDialect().insertVersionSql()
|
stmt := GetDialect().insertVersionSql()
|
||||||
|
@ -133,6 +129,18 @@ func FinalizeMigration(tx *sql.Tx, direction bool, v int64) error {
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the version table for the given migration without a transaction.
|
||||||
|
func FinalizeMigration(db *sql.DB, direction bool, v int64) error {
|
||||||
|
|
||||||
|
// XXX: drop goose_db_version table on some minimum version number?
|
||||||
|
stmt := GetDialect().insertVersionSql()
|
||||||
|
if _, err := db.Exec(stmt, v, direction); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`
|
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`
|
||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- SQL in section 'Up' is executed when this migration is applied
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -127,6 +128,29 @@ func splitSQLStatements(r io.Reader, direction bool) (stmts []string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func useTransactions(scriptFile string) bool {
|
||||||
|
f, err := os.Open(scriptFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
noTransactionsRegex, _ := regexp.Compile("--\\s\\+goose\\sNO\\sTRANSACTIONS")
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
if noTransactionsRegex.MatchString(line) {
|
||||||
|
f.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Run a migration specified in raw SQL.
|
// Run a migration specified in raw SQL.
|
||||||
//
|
//
|
||||||
// Sections of the script can be annotated with a special comment,
|
// Sections of the script can be annotated with a special comment,
|
||||||
|
@ -136,32 +160,71 @@ func splitSQLStatements(r io.Reader, direction bool) (stmts []string) {
|
||||||
// All statements following an Up or Down directive are grouped together
|
// All statements following an Up or Down directive are grouped together
|
||||||
// until another direction directive is found.
|
// until another direction directive is found.
|
||||||
func runSQLMigration(db *sql.DB, scriptFile string, v int64, direction bool) error {
|
func runSQLMigration(db *sql.DB, scriptFile string, v int64, direction bool) error {
|
||||||
|
filePath := filepath.Base(scriptFile)
|
||||||
tx, err := db.Begin()
|
useTx := useTransactions(scriptFile)
|
||||||
if err != nil {
|
|
||||||
log.Fatal("db.Begin:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Open(scriptFile)
|
f, err := os.Open(scriptFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if useTx {
|
||||||
|
err := runMigrationInTransaction(db, f, v, direction, filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("FAIL (tx) %s (%v), quitting migration.", filePath, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = runMigrationWithoutTransaction(db, f, v, direction, filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("FAIL (no tx) %s (%v), quitting migration.", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the migration within a transaction (recommended)
|
||||||
|
func runMigrationInTransaction(db *sql.DB, r io.Reader, v int64, direction bool, filePath string) error {
|
||||||
|
txn, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// find each statement, checking annotations for up/down direction
|
// find each statement, checking annotations for up/down direction
|
||||||
// and execute each of them in the current transaction.
|
|
||||||
// Commits the transaction if successfully applied each statement and
|
// Commits the transaction if successfully applied each statement and
|
||||||
// records the version into the version table or returns an error and
|
// records the version into the version table or returns an error and
|
||||||
// rolls back the transaction.
|
// rolls back the transaction.
|
||||||
for _, query := range splitSQLStatements(f, direction) {
|
for _, query := range splitSQLStatements(r, direction) {
|
||||||
if _, err = tx.Exec(query); err != nil {
|
if _, err = txn.Exec(query); err != nil {
|
||||||
tx.Rollback()
|
txn.Rollback()
|
||||||
log.Fatalf("FAIL %s (%v), quitting migration.", filepath.Base(scriptFile), err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = FinalizeMigration(tx, direction, v); err != nil {
|
if err = FinalizeMigrationTx(txn, direction, v); err != nil {
|
||||||
log.Fatalf("error finalizing migration %s, quitting. (%v)", filepath.Base(scriptFile), err)
|
log.Fatalf("error finalizing migration %s, quitting. (%v)", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrationWithoutTransaction(db *sql.DB, r io.Reader, v int64, direction bool, filePath string) error {
|
||||||
|
// find each statement, checking annotations for up/down direction
|
||||||
|
// Tecords the version into the version table or returns an error
|
||||||
|
for _, query := range splitSQLStatements(r, direction) {
|
||||||
|
if _, err := db.Exec(query); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := FinalizeMigration(db, direction, v); err != nil {
|
||||||
|
log.Fatalf("error finalizing migration %s, quitting. (%v)", filePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -86,6 +86,35 @@ func TestSplitStatements(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUseTransactions(t *testing.T) {
|
||||||
|
type testData struct {
|
||||||
|
fileName string
|
||||||
|
useTransactions bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testData{
|
||||||
|
{
|
||||||
|
fileName: "./example/migrations/00001_create_users_table.sql",
|
||||||
|
useTransactions: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: "./example/migrations/00002_rename_root.sql",
|
||||||
|
useTransactions: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: "./example/migrations/00003_no_transaction.sql",
|
||||||
|
useTransactions: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
result := useTransactions(test.fileName)
|
||||||
|
if result != test.useTransactions {
|
||||||
|
t.Errorf("Failed transaction check. got %v, want %v", result, test.useTransactions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var functxt = `-- +goose Up
|
var functxt = `-- +goose Up
|
||||||
CREATE TABLE IF NOT EXISTS histories (
|
CREATE TABLE IF NOT EXISTS histories (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
Loading…
Reference in New Issue