2018-04-30 18:42:17 +05:00
package goose
import (
"bufio"
"bytes"
"database/sql"
2019-10-13 13:55:38 +05:00
"fmt"
2018-04-30 18:42:17 +05:00
"io"
"os"
2019-10-13 13:55:38 +05:00
"regexp"
2018-04-30 18:42:17 +05:00
"strings"
2019-10-13 13:55:38 +05:00
"sync"
"github.com/pkg/errors"
2018-04-30 18:42:17 +05:00
)
const sqlCmdPrefix = "-- +goose "
2019-10-13 13:55:38 +05:00
const scanBufSize = 4 * 1024 * 1024
var bufferPool = sync . Pool {
New : func ( ) interface { } {
return make ( [ ] byte , scanBufSize )
} ,
}
2018-04-30 18:42:17 +05:00
// Checks the line to see if the line has a statement-ending semicolon
// or if the line contains a double-dash comment.
func endsWithSemicolon ( line string ) bool {
2019-10-13 13:55:38 +05:00
scanBuf := bufferPool . Get ( ) . ( [ ] byte )
defer bufferPool . Put ( scanBuf )
2018-04-30 18:42:17 +05:00
prev := ""
scanner := bufio . NewScanner ( strings . NewReader ( line ) )
2019-10-13 13:55:38 +05:00
scanner . Buffer ( scanBuf , scanBufSize )
2018-04-30 18:42:17 +05:00
scanner . Split ( bufio . ScanWords )
for scanner . Scan ( ) {
word := scanner . Text ( )
if strings . HasPrefix ( word , "--" ) {
break
}
prev = word
}
return strings . HasSuffix ( prev , ";" )
}
// Split the given sql script into individual statements.
//
// The base case is to simply split on semicolons, as these
// naturally terminate a statement.
//
// However, more complex cases like pl/pgsql can have semicolons
// within a statement. For these cases, we provide the explicit annotations
// 'StatementBegin' and 'StatementEnd' to allow the script to
// tell us to ignore semicolons.
2019-10-13 13:55:38 +05:00
func getSQLStatements ( r io . Reader , direction bool ) ( [ ] string , bool , error ) {
2018-04-30 18:42:17 +05:00
var buf bytes . Buffer
2019-10-13 13:55:38 +05:00
scanBuf := bufferPool . Get ( ) . ( [ ] byte )
defer bufferPool . Put ( scanBuf )
2018-04-30 18:42:17 +05:00
scanner := bufio . NewScanner ( r )
2019-10-13 13:55:38 +05:00
scanner . Buffer ( scanBuf , scanBufSize )
2018-04-30 18:42:17 +05:00
// track the count of each section
// so we can diagnose scripts with no annotations
upSections := 0
downSections := 0
statementEnded := false
ignoreSemicolons := false
directionIsActive := false
2019-10-13 13:55:38 +05:00
tx := true
stmts := [ ] string { }
2018-04-30 18:42:17 +05:00
for scanner . Scan ( ) {
line := scanner . Text ( )
// handle any goose-specific commands
if strings . HasPrefix ( line , sqlCmdPrefix ) {
cmd := strings . TrimSpace ( line [ len ( sqlCmdPrefix ) : ] )
switch cmd {
case "Up" :
directionIsActive = ( direction == true )
upSections ++
break
case "Down" :
directionIsActive = ( direction == false )
downSections ++
break
case "StatementBegin" :
if directionIsActive {
ignoreSemicolons = true
}
break
case "StatementEnd" :
if directionIsActive {
statementEnded = ( ignoreSemicolons == true )
ignoreSemicolons = false
}
break
case "NO TRANSACTION" :
tx = false
break
}
}
if ! directionIsActive {
continue
}
if _ , err := buf . WriteString ( line + "\n" ) ; err != nil {
2019-10-13 13:55:38 +05:00
return nil , false , fmt . Errorf ( "io err: %v" , err )
2018-04-30 18:42:17 +05:00
}
// Wrap up the two supported cases: 1) basic with semicolon; 2) psql statement
// Lines that end with semicolon that are in a statement block
// do not conclude statement.
if ( ! ignoreSemicolons && endsWithSemicolon ( line ) ) || statementEnded {
statementEnded = false
stmts = append ( stmts , buf . String ( ) )
buf . Reset ( )
}
}
if err := scanner . Err ( ) ; err != nil {
2019-10-13 13:55:38 +05:00
return nil , false , fmt . Errorf ( "scanning migration: %v" , err )
2018-04-30 18:42:17 +05:00
}
// diagnose likely migration script errors
if ignoreSemicolons {
2019-10-13 13:55:38 +05:00
return nil , false , fmt . Errorf ( "parsing migration: saw '-- +goose StatementBegin' with no matching '-- +goose StatementEnd'" )
2018-04-30 18:42:17 +05:00
}
if bufferRemaining := strings . TrimSpace ( buf . String ( ) ) ; len ( bufferRemaining ) > 0 {
2019-10-13 13:55:38 +05:00
return nil , false , fmt . Errorf ( "parsing migration: unexpected unfinished SQL query: %s. potential missing semicolon" , bufferRemaining )
2018-04-30 18:42:17 +05:00
}
if upSections == 0 && downSections == 0 {
2019-10-13 13:55:38 +05:00
return nil , false , fmt . Errorf ( "parsing migration: no Up/Down annotations found, so no statements were executed. See https://bitbucket.org/liamstask/goose/overview for details" )
2018-04-30 18:42:17 +05:00
}
2019-10-13 13:55:38 +05:00
return stmts , tx , nil
2018-04-30 18:42:17 +05:00
}
// Run a migration specified in raw SQL.
//
// Sections of the script can be annotated with a special comment,
// starting with "-- +goose" to specify whether the section should
// be applied during an Up or Down migration
//
// All statements following an Up or Down directive are grouped together
// until another direction directive is found.
2019-10-13 13:55:38 +05:00
func runSQLMigration ( db * sql . DB , sqlFile string , v int64 , direction bool ) error {
f , err := os . Open ( sqlFile )
2018-04-30 18:42:17 +05:00
if err != nil {
2019-10-13 13:55:38 +05:00
return errors . Wrap ( err , "failed to open SQL migration file" )
2018-04-30 18:42:17 +05:00
}
defer f . Close ( )
2019-10-13 13:55:38 +05:00
statements , useTx , err := getSQLStatements ( f , direction )
if err != nil {
return err
}
2018-04-30 18:42:17 +05:00
if useTx {
// TRANSACTION.
2019-10-13 13:55:38 +05:00
printInfo ( "Begin transaction\n" )
2018-04-30 18:42:17 +05:00
tx , err := db . Begin ( )
if err != nil {
2019-10-13 13:55:38 +05:00
errors . Wrap ( err , "failed to begin transaction" )
2018-04-30 18:42:17 +05:00
}
for _ , query := range statements {
2019-10-13 13:55:38 +05:00
printInfo ( "Executing statement: %s\n" , clearStatement ( query ) )
2018-04-30 18:42:17 +05:00
if _ , err = tx . Exec ( query ) ; err != nil {
2019-10-13 13:55:38 +05:00
printInfo ( "Rollback transaction\n" )
2018-04-30 18:42:17 +05:00
tx . Rollback ( )
2019-10-13 13:55:38 +05:00
return errors . Wrapf ( err , "failed to execute SQL query %q" , clearStatement ( query ) )
2018-04-30 18:42:17 +05:00
}
}
2019-10-13 13:55:38 +05:00
if direction {
if _ , err := tx . Exec ( GetDialect ( ) . insertVersionSQL ( ) , v , direction ) ; err != nil {
printInfo ( "Rollback transaction\n" )
tx . Rollback ( )
return errors . Wrap ( err , "failed to insert new goose version" )
}
} else {
if _ , err := tx . Exec ( GetDialect ( ) . deleteVersionSQL ( ) , v ) ; err != nil {
printInfo ( "Rollback transaction\n" )
tx . Rollback ( )
return errors . Wrap ( err , "failed to delete goose version" )
}
}
printInfo ( "Commit transaction\n" )
if err := tx . Commit ( ) ; err != nil {
return errors . Wrap ( err , "failed to commit transaction" )
2018-04-30 18:42:17 +05:00
}
2019-10-13 13:55:38 +05:00
return nil
2018-04-30 18:42:17 +05:00
}
// NO TRANSACTION.
for _ , query := range statements {
2019-10-13 13:55:38 +05:00
printInfo ( "Executing statement: %s\n" , clearStatement ( query ) )
2018-04-30 18:42:17 +05:00
if _ , err := db . Exec ( query ) ; err != nil {
2019-10-13 13:55:38 +05:00
return errors . Wrapf ( err , "failed to execute SQL query %q" , clearStatement ( query ) )
2018-04-30 18:42:17 +05:00
}
}
if _ , err := db . Exec ( GetDialect ( ) . insertVersionSQL ( ) , v , direction ) ; err != nil {
2019-10-13 13:55:38 +05:00
return errors . Wrap ( err , "failed to insert new goose version" )
2018-04-30 18:42:17 +05:00
}
return nil
}
2019-10-13 13:55:38 +05:00
func printInfo ( s string , args ... interface { } ) {
if verbose {
log . Printf ( s , args ... )
}
}
var (
matchSQLComments = regexp . MustCompile ( ` (?m)^--.*$[\r\n]* ` )
matchEmptyLines = regexp . MustCompile ( ` (?m)^$[\r\n]* ` )
)
func clearStatement ( s string ) string {
s = matchSQLComments . ReplaceAllString ( s , ` ` )
return matchEmptyLines . ReplaceAllString ( s , ` ` )
}