├── Makefile ├── README.md ├── UNLICENSE ├── doc.go ├── migration.go └── session.vim /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build 3 | 4 | push: 5 | git push origin master 6 | git push github master 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Package migration for Golang automatically handles versioning of a database 2 | schema by applying a series of migrations supplied by the client. It uses 3 | features only from the database/sql package, so it tries to be driver 4 | independent. However, to track the version of the database, it is necessary to 5 | execute some SQL. I've made an effort to keep those queries simple, but if they 6 | don't work with your database, you may override them. 7 | 8 | This package works by applying a series of migrations to a database. Once a 9 | migration is created, it should never be changed. Every time a database is 10 | opened with this package, all necessary migrations are executed in a single 11 | transaction. If any part of the process fails, an error is returned and the 12 | transaction is rolled back so that the database is left untouched. (Note that 13 | for this to be useful, you'll need to use a database that supports rolling back 14 | changes to your schema. Notably, MySQL does not support this, although SQLite 15 | and PostgreSQL do.) 16 | 17 | The version of a database is defined as the number of migrations applied to it. 18 | 19 | 20 | ### Installation 21 | 22 | If you have Go installed and 23 | [your GOPATH is setup](http://golang.org/doc/code.html#GOPATH), then 24 | `migration` can be installed with `go get`: 25 | 26 | go get github.com/BurntSushi/migration 27 | 28 | 29 | ### Documentation 30 | 31 | Documentation is available at 32 | [godoc.org/github.com/BurntSushi/migration](http://godoc.org/github.com/BurntSushi/migration). 33 | 34 | 35 | ### Unstable 36 | 37 | At the moment, I'm still experimenting with the public API, so I may still 38 | introduce breaking changes. In general though, I am happy with the overall 39 | architecture. 40 | 41 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package migration automatically handles versioning of a database 3 | schema by applying a series of migrations supplied by the client. It uses 4 | features only from the database/sql package, so it tries to be driver 5 | independent. However, to track the version of the database, it is necessary to 6 | execute some SQL. I've made an effort to keep those queries simple, but if they 7 | don't work with your database, you may override them. 8 | 9 | This package works by applying a series of migrations to a database. Once a 10 | migration is created, it should never be changed. Every time a database is 11 | opened with this package, all necessary migrations are executed in a single 12 | transaction. If any part of the process fails, an error is returned and the 13 | transaction is rolled back so that the database is left untouched. (Note that 14 | for this to be useful, you'll need to use a database that supports rolling back 15 | changes to your schema. Notably, MySQL does not support this, although SQLite 16 | and PostgreSQL do.) 17 | 18 | The version of a database is defined as the number of migrations applied to it. 19 | */ 20 | package migration 21 | -------------------------------------------------------------------------------- /migration.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | var ef = fmt.Errorf 9 | 10 | // LimitedTx specifies the behavior of a transaction *without* commit and 11 | // rollback functions. Values with this type are given to client functions. 12 | // In particular, the migration routines in this package 13 | // handle transaction commits and rollbacks. Therefore the functions provided 14 | // by the client should not use them. 15 | type LimitedTx interface { 16 | Exec(query string, args ...interface{}) (sql.Result, error) 17 | Prepare(query string) (*sql.Stmt, error) 18 | Query(query string, args ...interface{}) (*sql.Rows, error) 19 | QueryRow(query string, args ...interface{}) *sql.Row 20 | Stmt(stmt *sql.Stmt) *sql.Stmt 21 | } 22 | 23 | // GetVersion is any function that can retrieve the migration version of a 24 | // particular database. It is exposed in case a client wants to override the 25 | // default behavior of this package. (For example, by using the `user_version` 26 | // PRAGMA in SQLite.) 27 | // 28 | // The DefaultGetVersion function provided with this package creates its own 29 | // table with a single column and a single row. 30 | // 31 | // The version returned should be equivalent to the number of migrations 32 | // applied to this database. It should be 0 if no migrations have been applied 33 | // yet. 34 | // 35 | // If an error is returned, the migration automatically fails. 36 | // 37 | // Note that a LimitedTx is used to emphasize that functions with this type 38 | // MUST NOT call Commit or Rollback. The migration routine in this pacakge will 39 | // do it for you. 40 | type GetVersion func(LimitedTx) (int, error) 41 | 42 | // The default way to get the version from a database. If the database has 43 | // had no migrations performed, then it creates a table with a single row and 44 | // a single column storing the version as 0. It then returns 0. 45 | // 46 | // If the table exists, then the version stored in the table is returned. 47 | var DefaultGetVersion GetVersion = defaultGetVersion 48 | 49 | // SetVersion is the dual of GetVersion. It allows the client to define a 50 | // different mechanism for setting the database version than the one used by 51 | // DefaultSetVersion in this package. 52 | // 53 | // If an error is returned, the migration that tried to set the version 54 | // automatically fails. 55 | // 56 | // Note that a LimitedTx is used to emphasize that functions with this type 57 | // MUST NOT call Commit or Rollback. The migration routine in this pacakge will 58 | // do it for you. 59 | type SetVersion func(LimitedTx, int) error 60 | 61 | // The default way to set the version of the database. If the database has had 62 | // no migrations performed, then it creates a table with a single row and a 63 | // single column and storing the version given there. 64 | // 65 | // If the table exists, then the existing version is overwritten. 66 | var DefaultSetVersion SetVersion = defaultSetVersion 67 | 68 | // Migrator corresponds to a function that updates the database by one version. 69 | // Note that a migration should NOT call Rollback or Commit. Instead, this 70 | // package will call Rollback for you if your migration returns an error. If 71 | // no error is returned, then the next migration is applied. When all 72 | // migrations have been applied, the version is updated and the changes are 73 | // committed to the database. 74 | type Migrator func(LimitedTx) error 75 | 76 | // Open wraps the Open function from the database/sql package, but performs 77 | // a series of migrations on a database if they haven't been performed already. 78 | // 79 | // Migrations are tracked by a simple versioning scheme. The version of the 80 | // database is the number of migrations that have been performed on it. 81 | // Similarly, the version of your library is the number of migrations that are 82 | // given to this function. 83 | // 84 | // If Open returns successfully, then the database and your library will have 85 | // the same versions. If there was a problem migrating---or if the database 86 | // version is greater than your library version---then an error is returned. 87 | // Since all migrations are performed in a single transaction, if an error 88 | // occurs, no changes are made to the database. (Assuming you're using a 89 | // relational database that allows modifications to a schema to be rolled back.) 90 | // 91 | // Note that this versioning scheme includes no semantic analysis. It is up to 92 | // client to ensure that once a migration is defined, it never changes. 93 | // 94 | // The details of how the version is stored are opaque to the client, but in 95 | // general, it will add a table to your database called "migration_version" 96 | // with a single column containing a single row. 97 | func Open(driver, dsn string, migrations []Migrator) (*sql.DB, error) { 98 | return OpenWith(driver, dsn, migrations, nil, nil) 99 | } 100 | 101 | // OpenWith is exactly like Open, except it allows the client to specify their 102 | // own versioning scheme. Note that vget and vset must BOTH be 103 | // nil or BOTH be non-nil. Otherwise, this function panics. This is because the 104 | // implementation of one generally relies on the implementation of the other. 105 | // 106 | // If vget and vset are both set to nil, then the behavior of this 107 | // function is identical to the behavior of Open. 108 | func OpenWith( 109 | driver, dsn string, 110 | migrations []Migrator, 111 | vget GetVersion, vset SetVersion, 112 | ) (*sql.DB, error) { 113 | if (vget == nil && vset != nil) || (vget != nil && vset == nil) { 114 | panic("vget/vset must both be nil or both be non-nil") 115 | } 116 | if vget == nil { 117 | vget = DefaultGetVersion 118 | } 119 | if vset == nil { 120 | vset = DefaultSetVersion 121 | } 122 | 123 | db, err := sql.Open(driver, dsn) 124 | if err != nil { 125 | return nil, err 126 | } 127 | if err := (migration{db, migrations, vget, vset}).migrate(); err != nil { 128 | return nil, err 129 | } 130 | return db, nil 131 | } 132 | 133 | type migration struct { 134 | *sql.DB 135 | migrations []Migrator 136 | getVersion GetVersion 137 | setVersion SetVersion 138 | } 139 | 140 | // Stmt satisfies the LimitedTx interface. 141 | func (m migration) Stmt(stmt *sql.Stmt) *sql.Stmt { 142 | return stmt 143 | } 144 | 145 | func (m migration) migrate() error { 146 | libVersion := len(m.migrations) 147 | dbVersion, err := m.getVersion(m) 148 | if err != nil { 149 | return ef("Could not get DB version: %s", err) 150 | } 151 | if dbVersion > libVersion { 152 | return ef("Database version (%d) is greater than library version (%d).", 153 | dbVersion, libVersion) 154 | } 155 | if dbVersion == libVersion { 156 | return nil 157 | } 158 | 159 | tx, err := m.Begin() 160 | if err != nil { 161 | return ef("Could not start transaction: %s", err) 162 | } 163 | for i := dbVersion; i < libVersion; i++ { 164 | if err := m.migrations[i](tx); err != nil { 165 | if err2 := tx.Rollback(); err2 != nil { 166 | return ef( 167 | "When migrating from %d to %d, got error '%s' and "+ 168 | "got error '%s' after trying to rollback.", 169 | i, i+1, err, err2) 170 | } 171 | return ef( 172 | "When migrating from %d to %d, got error '%s' and "+ 173 | "successfully rolled back.", i, i+1, err) 174 | } 175 | } 176 | if err := m.setVersion(tx, libVersion); err != nil { 177 | if err2 := tx.Rollback(); err2 != nil { 178 | return ef( 179 | "When trying to set version to %d (from %d), got error '%s' "+ 180 | "and got error '%s' after trying to rollback.", 181 | libVersion, dbVersion, err, err2) 182 | } 183 | return ef( 184 | "When trying to set version to %d (from %d), got error '%s' "+ 185 | "and successfully rolled back.", 186 | libVersion, dbVersion, err) 187 | } 188 | if err := tx.Commit(); err != nil { 189 | return ef("Error committing migration from %d to %d: %s", 190 | dbVersion, libVersion, err) 191 | } 192 | return nil 193 | } 194 | 195 | func defaultGetVersion(tx LimitedTx) (int, error) { 196 | v, err := getVersion(tx) 197 | if err != nil { 198 | if err := createVersionTable(tx); err != nil { 199 | return 0, err 200 | } 201 | return getVersion(tx) 202 | } 203 | return v, nil 204 | } 205 | 206 | func defaultSetVersion(tx LimitedTx, version int) error { 207 | if err := setVersion(tx, version); err != nil { 208 | if err := createVersionTable(tx); err != nil { 209 | return err 210 | } 211 | return setVersion(tx, version) 212 | } 213 | return nil 214 | } 215 | 216 | func getVersion(tx LimitedTx) (int, error) { 217 | var version int 218 | r := tx.QueryRow("SELECT version FROM migration_version") 219 | if err := r.Scan(&version); err != nil { 220 | return 0, err 221 | } 222 | return version, nil 223 | } 224 | 225 | func setVersion(tx LimitedTx, version int) error { 226 | _, err := tx.Exec("UPDATE migration_version SET version = $1", version) 227 | return err 228 | } 229 | 230 | func createVersionTable(tx LimitedTx) error { 231 | _, err := tx.Exec(` 232 | CREATE TABLE migration_version ( 233 | version INTEGER 234 | ); 235 | INSERT INTO migration_version (version) VALUES (0)`) 236 | return err 237 | } 238 | -------------------------------------------------------------------------------- /session.vim: -------------------------------------------------------------------------------- 1 | au BufWritePost *.go silent!make tags > /dev/null 2>&1 2 | --------------------------------------------------------------------------------