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.
|
||||
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue