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 }