diff --git a/README.md b/README.md index e140701..3424d75 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/example/migrations/00003_no_transaction.sql b/example/migrations/00003_no_transaction.sql new file mode 100644 index 0000000..cde06b4 --- /dev/null +++ b/example/migrations/00003_no_transaction.sql @@ -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; diff --git a/migration.go b/migration.go index e37313c..fd266d5 100644 --- a/migration.go +++ b/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 diff --git a/migration_sql.go b/migration_sql.go index e31826f..18be3b9 100644 --- a/migration_sql.go +++ b/migration_sql.go @@ -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 diff --git a/migration_sql_test.go b/migration_sql_test.go index 5852960..bb899fd 100644 --- a/migration_sql_test.go +++ b/migration_sql_test.go @@ -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,