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
Nicholas Duffy 2017-05-06 09:41:44 -06:00
parent cdb30a7da1
commit fa8806ecfd
5 changed files with 131 additions and 17 deletions

View File

@ -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.
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.
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:

View File

@ -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;

View File

@ -63,10 +63,6 @@ func (m *Migration) run(db *sql.DB, direction bool) error {
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))
@ -121,7 +117,7 @@ func CreateMigration(name, migrationType, dir string, t time.Time) (path string,
// Update the version table for the given migration,
// 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?
stmt := GetDialect().insertVersionSql()
@ -133,6 +129,18 @@ func FinalizeMigration(tx *sql.Tx, direction bool, v int64) error {
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(`
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied

View File

@ -8,6 +8,7 @@ import (
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
@ -127,6 +128,29 @@ func splitSQLStatements(r io.Reader, direction bool) (stmts []string) {
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.
//
// 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
// until another direction directive is found.
func runSQLMigration(db *sql.DB, scriptFile string, v int64, direction bool) error {
tx, err := db.Begin()
if err != nil {
log.Fatal("db.Begin:", err)
}
filePath := filepath.Base(scriptFile)
useTx := useTransactions(scriptFile)
f, err := os.Open(scriptFile)
if err != nil {
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
// and execute each of them in the current transaction.
// Commits the transaction if successfully applied each statement and
// records the version into the version table or returns an error and
// rolls back the transaction.
for _, query := range splitSQLStatements(f, direction) {
if _, err = tx.Exec(query); err != nil {
tx.Rollback()
log.Fatalf("FAIL %s (%v), quitting migration.", filepath.Base(scriptFile), err)
for _, query := range splitSQLStatements(r, direction) {
if _, err = txn.Exec(query); err != nil {
txn.Rollback()
return err
}
}
if err = FinalizeMigration(tx, direction, v); err != nil {
log.Fatalf("error finalizing migration %s, quitting. (%v)", filepath.Base(scriptFile), err)
if err = FinalizeMigrationTx(txn, direction, v); err != nil {
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

View File

@ -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
CREATE TABLE IF NOT EXISTS histories (
id BIGSERIAL PRIMARY KEY,