github.com/CloudCom/goose@v0.0.0-20151110184009-e03c3249c21b/lib/goose/migration_sql.go (about)

     1  package goose
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"database/sql"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  )
    13  
    14  const sqlCmdPrefix = "-- +goose "
    15  
    16  // Checks the line to see if the line has a statement-ending semicolon
    17  // or if the line contains a double-dash comment.
    18  func endsWithSemicolon(line string) bool {
    19  
    20  	prev := ""
    21  	scanner := bufio.NewScanner(strings.NewReader(line))
    22  	scanner.Split(bufio.ScanWords)
    23  
    24  	for scanner.Scan() {
    25  		word := scanner.Text()
    26  		if strings.HasPrefix(word, "--") {
    27  			break
    28  		}
    29  		prev = word
    30  	}
    31  
    32  	return strings.HasSuffix(prev, ";")
    33  }
    34  
    35  // Split the given sql script into individual statements.
    36  //
    37  // The base case is to simply split on semicolons, as these
    38  // naturally terminate a statement.
    39  //
    40  // However, more complex cases like pl/pgsql can have semicolons
    41  // within a statement. For these cases, we provide the explicit annotations
    42  // 'StatementBegin' and 'StatementEnd' to allow the script to
    43  // tell us to ignore semicolons.
    44  func splitSQLStatements(r io.Reader, direction Direction) (stmts []string) {
    45  	var buf bytes.Buffer
    46  	scanner := bufio.NewScanner(r)
    47  
    48  	// track the count of each section
    49  	// so we can diagnose scripts with no annotations
    50  	upSections := 0
    51  	downSections := 0
    52  
    53  	statementEnded := false
    54  	ignoreSemicolons := false
    55  	directionIsActive := false
    56  
    57  	for scanner.Scan() {
    58  
    59  		line := scanner.Text()
    60  
    61  		// handle any goose-specific commands
    62  		if strings.HasPrefix(line, sqlCmdPrefix) {
    63  			cmd := strings.TrimSpace(line[len(sqlCmdPrefix):])
    64  			switch cmd {
    65  			case "Up":
    66  				directionIsActive = (direction == DirectionUp)
    67  				upSections++
    68  				break
    69  
    70  			case "Down":
    71  				directionIsActive = (direction == DirectionDown)
    72  				downSections++
    73  				break
    74  
    75  			case "StatementBegin":
    76  				if directionIsActive {
    77  					ignoreSemicolons = true
    78  				}
    79  				break
    80  
    81  			case "StatementEnd":
    82  				if directionIsActive {
    83  					statementEnded = (ignoreSemicolons == true)
    84  					ignoreSemicolons = false
    85  				}
    86  				break
    87  			}
    88  		}
    89  
    90  		if !directionIsActive {
    91  			continue
    92  		}
    93  
    94  		if _, err := buf.WriteString(line + "\n"); err != nil {
    95  			log.Fatalf("io err: %v", err)
    96  		}
    97  
    98  		// Wrap up the two supported cases: 1) basic with semicolon; 2) psql statement
    99  		// Lines that end with semicolon that are in a statement block
   100  		// do not conclude statement.
   101  		if (!ignoreSemicolons && endsWithSemicolon(line)) || statementEnded {
   102  			statementEnded = false
   103  			stmts = append(stmts, buf.String())
   104  			buf.Reset()
   105  		}
   106  	}
   107  
   108  	if err := scanner.Err(); err != nil {
   109  		log.Fatalf("scanning migration: %v", err)
   110  	}
   111  
   112  	// diagnose likely migration script errors
   113  	if ignoreSemicolons {
   114  		log.Println("WARNING: saw '-- +goose StatementBegin' with no matching '-- +goose StatementEnd'")
   115  	}
   116  
   117  	if bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {
   118  		log.Printf("WARNING: Unexpected unfinished SQL query: %s. Missing a semicolon?\n", bufferRemaining)
   119  	}
   120  
   121  	if upSections == 0 && downSections == 0 {
   122  		log.Fatalf(`ERROR: no Up/Down annotations found, so no statements were executed.
   123  			See https://github.com/cloudcom/goose for details.`)
   124  	}
   125  
   126  	return
   127  }
   128  
   129  // Run a migration specified in raw SQL.
   130  //
   131  // Sections of the script can be annotated with a special comment,
   132  // starting with "-- +goose" to specify whether the section should
   133  // be applied during an Up or Down migration
   134  //
   135  // All statements following an Up or Down directive are grouped together
   136  // until another direction directive is found.
   137  func runSQLMigration(conf *DBConf, db *sql.DB, scriptFile string, v int64, direction Direction) error {
   138  
   139  	txn, err := db.Begin()
   140  	if err != nil {
   141  		log.Fatal("db.Begin:", err)
   142  	}
   143  
   144  	f, err := os.Open(scriptFile)
   145  	if err != nil {
   146  		log.Fatal(err)
   147  	}
   148  
   149  	// find each statement, checking annotations for up/down direction
   150  	// and execute each of them in the current transaction.
   151  	// Commits the transaction if successfully applied each statement and
   152  	// records the version into the version table or returns an error and
   153  	// rolls back the transaction.
   154  	for _, query := range splitSQLStatements(f, direction) {
   155  		if _, err = txn.Exec(query); err != nil {
   156  			txn.Rollback()
   157  			log.Fatalf("FAIL %s (%v), quitting migration.", filepath.Base(scriptFile), err)
   158  			return err
   159  		}
   160  	}
   161  
   162  	if err = FinalizeMigration(conf, txn, direction, v); err != nil {
   163  		log.Fatalf("error finalizing migration %s, quitting. (%v)", filepath.Base(scriptFile), err)
   164  	}
   165  
   166  	return nil
   167  }