├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _examples └── main.go ├── darwin.go ├── darwin_test.go ├── dialect.go ├── driver.go ├── driver_test.go ├── mysql_dialect.go ├── postgres_dialect.go ├── ql_dialect.go ├── ql_dialect_test.go └── sqlite_dialect.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.sqlite3 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - 1.5 6 | - 1.6 7 | - 1.7 8 | - tip 9 | 10 | before_install: 11 | - go get -t -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Claudemiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/GuiaBolso/darwin.svg?branch=master)](https://travis-ci.org/GuiaBolso/darwin) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/GuiaBolso/darwin)](https://goreportcard.com/report/github.com/GuiaBolso/darwin) 3 | [![GoDoc](https://godoc.org/github.com/GuiaBolso/darwin?status.svg)](https://godoc.org/github.com/GuiaBolso/darwin) 4 | 5 | Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/GuiaBolso/darwin)! 6 | 7 | # Darwin 8 | 9 | Database schema evolution library for Go 10 | 11 | # Example 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "database/sql" 18 | "log" 19 | 20 | "github.com/GuiaBolso/darwin" 21 | _ "github.com/go-sql-driver/mysql" 22 | ) 23 | 24 | var ( 25 | migrations = []darwin.Migration{ 26 | { 27 | Version: 1, 28 | Description: "Creating table posts", 29 | Script: `CREATE TABLE posts ( 30 | id INT auto_increment, 31 | title VARCHAR(255), 32 | PRIMARY KEY (id) 33 | ) ENGINE=InnoDB CHARACTER SET=utf8;`, 34 | }, 35 | { 36 | Version: 2, 37 | Description: "Adding column body", 38 | Script: "ALTER TABLE posts ADD body TEXT AFTER title;", 39 | }, 40 | } 41 | ) 42 | 43 | func main() { 44 | database, err := sql.Open("mysql", "root:@/darwin") 45 | 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | driver := darwin.NewGenericDriver(database, darwin.MySQLDialect{}) 51 | 52 | d := darwin.New(driver, migrations, nil) 53 | err = d.Migrate() 54 | 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | } 59 | ``` 60 | 61 | # Questions 62 | 63 | Q. Why there is not a command line utility? 64 | 65 | A. The purpose of this library is just be a library. 66 | 67 | Q. How can I read migrations from file system? 68 | 69 | A. You can read with the standard library and build the migration list. 70 | 71 | Q. Can I put more than one statement in the same Script migration? 72 | 73 | A. I do not recommend. Put one database change per migration, if some migration fail, you exactly what statement caused the error. Also only postgres correctly handle rollback in DDL transactions. 74 | 75 | To be less annoying you can organize your migrations like? 1.0, 1.1, 1.2 and so on. 76 | 77 | Q. Why does not exists downgrade migrations? 78 | 79 | A. Please read https://flywaydb.org/documentation/faq#downgrade 80 | 81 | Q. Does Darwin perform a roll back if a migration fails? 82 | 83 | A. Please read https://flywaydb.org/documentation/faq#rollback 84 | 85 | Q. What is the best strategy for dealing with hot fixes? 86 | 87 | A. Plese read https://flywaydb.org/documentation/faq#hot-fixes 88 | 89 | 90 | # LICENSE 91 | 92 | The MIT License (MIT) 93 | 94 | Copyright (c) 2016 Claudemiro 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy 97 | of this software and associated documentation files (the "Software"), to deal 98 | in the Software without restriction, including without limitation the rights 99 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 100 | copies of the Software, and to permit persons to whom the Software is 101 | furnished to do so, subject to the following conditions: 102 | 103 | The above copyright notice and this permission notice shall be included in all 104 | copies or substantial portions of the Software. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 107 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 108 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 109 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 110 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 111 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 112 | SOFTWARE. 113 | -------------------------------------------------------------------------------- /_examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/GuiaBolso/darwin" 10 | _ "github.com/mattn/go-sqlite3" 11 | ) 12 | 13 | var ( 14 | migrations = []darwin.Migration{ 15 | { 16 | Version: 1, 17 | Description: "Creating table posts", 18 | Script: `CREATE TABLE posts ( 19 | id INTEGER PRIMARY KEY, 20 | title TEXT 21 | );;`, 22 | }, 23 | { 24 | Version: 2, 25 | Description: "Adding column body", 26 | Script: "ALTER TABLE posts ADD body TEXT AFTER title;", 27 | }, 28 | } 29 | ) 30 | 31 | func main() { 32 | var info bool 33 | 34 | flag.BoolVar(&info, "info", false, "If you want get info from database") 35 | flag.Parse() 36 | 37 | database, err := sql.Open("sqlite3", "database.sqlite3") 38 | 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | driver := darwin.NewGenericDriver(database, darwin.SqliteDialect{}) 44 | 45 | d := darwin.New(driver, migrations, nil) 46 | 47 | if info { 48 | infos, _ := d.Info() 49 | for _, info := range infos { 50 | fmt.Printf("%.1f %s %s\n", info.Migration.Version, info.Status, info.Migration.Description) 51 | } 52 | } else { 53 | err = d.Migrate() 54 | 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /darwin.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "sort" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // Status is a migration status value 12 | type Status int 13 | 14 | const ( 15 | // Ignored means that the migrations was not appied to the database 16 | Ignored Status = iota 17 | // Applied means that the migrations was successfully applied to the database 18 | Applied 19 | // Pending means that the migrations is a new migration and it is waiting to be applied to the database 20 | Pending 21 | // Error means that the migration could not be applied to the database 22 | Error 23 | ) 24 | 25 | func (s Status) String() string { 26 | switch s { 27 | case Ignored: 28 | return "IGNORED" 29 | case Applied: 30 | return "APPLIED" 31 | case Pending: 32 | return "PENDING" 33 | case Error: 34 | return "ERROR" 35 | default: 36 | return "INVALID" 37 | } 38 | } 39 | 40 | // A global mutex 41 | var mutex = &sync.Mutex{} 42 | 43 | // Migration represents a database migrations. 44 | type Migration struct { 45 | Version float64 46 | Description string 47 | Script string 48 | } 49 | 50 | // Checksum calculate the Script md5 51 | func (m Migration) Checksum() string { 52 | return fmt.Sprintf("%x", md5.Sum([]byte(m.Script))) 53 | } 54 | 55 | // MigrationInfo is a struct used in the infoChan to inform clients about 56 | // the migration being applied. 57 | type MigrationInfo struct { 58 | Status Status 59 | Error error 60 | Migration Migration 61 | } 62 | 63 | // Darwin is a helper struct to access the Validate and migration functions 64 | type Darwin struct { 65 | driver Driver 66 | migrations []Migration 67 | infoChan chan MigrationInfo 68 | } 69 | 70 | // Validate if the database migrations are applied and consistent 71 | func (d Darwin) Validate() error { 72 | return Validate(d.driver, d.migrations) 73 | } 74 | 75 | // Migrate executes the missing migrations in database 76 | func (d Darwin) Migrate() error { 77 | return Migrate(d.driver, d.migrations, d.infoChan) 78 | } 79 | 80 | // Info returns the status of all migrations 81 | func (d Darwin) Info() ([]MigrationInfo, error) { 82 | return Info(d.driver, d.migrations) 83 | } 84 | 85 | // New returns a new Darwin struct 86 | func New(driver Driver, migrations []Migration, infoChan chan MigrationInfo) Darwin { 87 | return Darwin{ 88 | driver: driver, 89 | migrations: migrations, 90 | infoChan: infoChan, 91 | } 92 | } 93 | 94 | // DuplicateMigrationVersionError is used to report when the migration list has duplicated entries 95 | type DuplicateMigrationVersionError struct { 96 | Version float64 97 | } 98 | 99 | func (d DuplicateMigrationVersionError) Error() string { 100 | return fmt.Sprintf("Multiple migrations have the version number %f.", d.Version) 101 | } 102 | 103 | // IllegalMigrationVersionError is used to report when the migration has an illegal Version number 104 | type IllegalMigrationVersionError struct { 105 | Version float64 106 | } 107 | 108 | func (i IllegalMigrationVersionError) Error() string { 109 | return fmt.Sprintf("Illegal migration version number %f.", i.Version) 110 | } 111 | 112 | // RemovedMigrationError is used to report when a migration is removed from the list 113 | type RemovedMigrationError struct { 114 | Version float64 115 | } 116 | 117 | func (r RemovedMigrationError) Error() string { 118 | return fmt.Sprintf("Migration %f was removed", r.Version) 119 | } 120 | 121 | // InvalidChecksumError is used to report when a migration was modified 122 | type InvalidChecksumError struct { 123 | Version float64 124 | } 125 | 126 | func (i InvalidChecksumError) Error() string { 127 | return fmt.Sprintf("Invalid cheksum for migration %f", i.Version) 128 | } 129 | 130 | // Validate if the database migrations are applied and consistent 131 | func Validate(d Driver, migrations []Migration) error { 132 | sort.Sort(byMigrationVersion(migrations)) 133 | 134 | if version, invalid := isInvalidVersion(migrations); invalid { 135 | return IllegalMigrationVersionError{Version: version} 136 | } 137 | 138 | if version, dup := isDuplicated(migrations); dup { 139 | return DuplicateMigrationVersionError{Version: version} 140 | } 141 | 142 | applied, err := d.All() 143 | 144 | if err != nil { 145 | return err 146 | } 147 | 148 | if version, removed := wasRemovedMigration(applied, migrations); removed { 149 | return RemovedMigrationError{Version: version} 150 | } 151 | 152 | if version, invalid := isInvalidChecksumMigration(applied, migrations); invalid { 153 | return InvalidChecksumError{Version: version} 154 | } 155 | 156 | return nil 157 | } 158 | 159 | // Info returns the status of all migrations 160 | func Info(d Driver, migrations []Migration) ([]MigrationInfo, error) { 161 | info := []MigrationInfo{} 162 | records, err := d.All() 163 | 164 | if err != nil { 165 | return info, err 166 | } 167 | 168 | sort.Sort(sort.Reverse(byMigrationRecordVersion(records))) 169 | 170 | for _, migration := range migrations { 171 | info = append(info, MigrationInfo{ 172 | Status: getStatus(records, migration), 173 | Error: nil, 174 | Migration: migration, 175 | }) 176 | } 177 | 178 | return info, nil 179 | } 180 | 181 | func getStatus(inDatabase []MigrationRecord, migration Migration) Status { 182 | last := inDatabase[0] 183 | 184 | // Check Pending 185 | if migration.Version > last.Version { 186 | return Pending 187 | } 188 | 189 | // Check Ignored 190 | found := false 191 | 192 | for _, record := range inDatabase { 193 | if record.Version == migration.Version { 194 | found = true 195 | } 196 | } 197 | 198 | if !found { 199 | return Ignored 200 | } 201 | 202 | return Applied 203 | } 204 | 205 | // Migrate executes the missing migrations in database. 206 | func Migrate(d Driver, migrations []Migration, infoChan chan MigrationInfo) error { 207 | mutex.Lock() 208 | defer mutex.Unlock() 209 | 210 | err := d.Create() 211 | 212 | if err != nil { 213 | return err 214 | } 215 | 216 | err = Validate(d, migrations) 217 | 218 | if err != nil { 219 | return err 220 | } 221 | 222 | planned, err := planMigration(d, migrations) 223 | 224 | if err != nil { 225 | return err 226 | } 227 | 228 | for _, migration := range planned { 229 | dur, err := d.Exec(migration.Script) 230 | 231 | if err != nil { 232 | notify(err, migration, infoChan) 233 | return err 234 | } 235 | 236 | err = d.Insert(MigrationRecord{ 237 | Version: migration.Version, 238 | Description: migration.Description, 239 | Checksum: migration.Checksum(), 240 | AppliedAt: time.Now(), 241 | ExecutionTime: dur, 242 | }) 243 | 244 | notify(err, migration, infoChan) 245 | 246 | if err != nil { 247 | return err 248 | } 249 | 250 | } 251 | 252 | return nil 253 | } 254 | 255 | func notify(err error, migration Migration, infoChan chan MigrationInfo) { 256 | status := Pending 257 | 258 | if err != nil { 259 | status = Error 260 | } else { 261 | status = Applied 262 | } 263 | 264 | // Send the migration over the infoChan 265 | // The listener could print in the Stdout a message about the applied migration 266 | if infoChan != nil { 267 | infoChan <- MigrationInfo{ 268 | Status: status, 269 | Error: err, 270 | Migration: migration, 271 | } 272 | } 273 | 274 | } 275 | 276 | func wasRemovedMigration(applied []MigrationRecord, migrations []Migration) (float64, bool) { 277 | versionMap := map[float64]Migration{} 278 | 279 | for _, migration := range migrations { 280 | versionMap[migration.Version] = migration 281 | } 282 | 283 | for _, migration := range applied { 284 | if _, ok := versionMap[migration.Version]; !ok { 285 | return migration.Version, true 286 | } 287 | } 288 | 289 | return 0, false 290 | } 291 | 292 | func isInvalidChecksumMigration(applied []MigrationRecord, migrations []Migration) (float64, bool) { 293 | versionMap := map[float64]MigrationRecord{} 294 | 295 | for _, migration := range applied { 296 | versionMap[migration.Version] = migration 297 | } 298 | 299 | for _, migration := range migrations { 300 | if m, ok := versionMap[migration.Version]; ok { 301 | if m.Checksum != migration.Checksum() { 302 | return migration.Version, true 303 | } 304 | } 305 | } 306 | 307 | return 0, false 308 | } 309 | 310 | func isInvalidVersion(migrations []Migration) (float64, bool) { 311 | for _, migration := range migrations { 312 | version := migration.Version 313 | 314 | if version < 0 { 315 | return version, true 316 | } 317 | } 318 | 319 | return 0, false 320 | } 321 | 322 | func isDuplicated(migrations []Migration) (float64, bool) { 323 | unique := map[float64]Migration{} 324 | 325 | for _, migration := range migrations { 326 | _, exists := unique[migration.Version] 327 | 328 | if exists { 329 | return migration.Version, true 330 | } 331 | 332 | unique[migration.Version] = migration 333 | } 334 | 335 | return 0, false 336 | } 337 | 338 | func planMigration(d Driver, migrations []Migration) ([]Migration, error) { 339 | records, err := d.All() 340 | 341 | if err != nil { 342 | return []Migration{}, err 343 | } 344 | 345 | // Apply all migrations 346 | if len(records) == 0 { 347 | return migrations, nil 348 | } 349 | 350 | // Which migrations needs to be applied 351 | planned := []Migration{} 352 | 353 | // Make sure the order is correct 354 | // Do not trust the driver. 355 | sort.Sort(sort.Reverse(byMigrationRecordVersion(records))) 356 | last := records[0] 357 | 358 | // Apply all migrations that are greater than the last migration 359 | for _, migration := range migrations { 360 | if migration.Version > last.Version { 361 | planned = append(planned, migration) 362 | } 363 | } 364 | 365 | // Make sure the order is correct 366 | sort.Sort(byMigrationVersion(planned)) 367 | 368 | return planned, nil 369 | } 370 | 371 | type byMigrationVersion []Migration 372 | 373 | func (b byMigrationVersion) Len() int { return len(b) } 374 | func (b byMigrationVersion) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 375 | func (b byMigrationVersion) Less(i, j int) bool { return b[i].Version < b[j].Version } 376 | -------------------------------------------------------------------------------- /darwin_test.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type dummyDriver struct { 12 | CreateError bool 13 | InsertError bool 14 | AllError bool 15 | ExecError bool 16 | records []MigrationRecord 17 | } 18 | 19 | func (d *dummyDriver) Create() error { 20 | if d.CreateError { 21 | return errors.New("Error") 22 | } 23 | return nil 24 | } 25 | 26 | func (d *dummyDriver) Insert(m MigrationRecord) error { 27 | if d.InsertError { 28 | return errors.New("Error") 29 | } 30 | 31 | d.records = append(d.records, m) 32 | return nil 33 | } 34 | 35 | func (d *dummyDriver) All() ([]MigrationRecord, error) { 36 | if d.AllError { 37 | return []MigrationRecord{}, errors.New("Error") 38 | } 39 | 40 | return d.records, nil 41 | } 42 | 43 | func (d *dummyDriver) Exec(string) (time.Duration, error) { 44 | if d.ExecError { 45 | return time.Millisecond * 1, errors.New("Error") 46 | } 47 | 48 | return time.Millisecond * 1, nil 49 | } 50 | 51 | func Test_Status_String(t *testing.T) { 52 | expectations := []struct { 53 | status Status 54 | expected string 55 | }{ 56 | { 57 | Ignored, "IGNORED", 58 | }, 59 | { 60 | Applied, "APPLIED", 61 | }, 62 | { 63 | Pending, "PENDING", 64 | }, 65 | { 66 | Error, "ERROR", 67 | }, 68 | { 69 | Status(-1), "INVALID", 70 | }, 71 | } 72 | 73 | for _, expectation := range expectations { 74 | if expectation.expected != expectation.status.String() { 75 | t.Errorf("Expected %s, got %s", expectation.expected, expectation.status.String()) 76 | t.FailNow() 77 | } 78 | } 79 | } 80 | 81 | func Test_Info(t *testing.T) { 82 | baseTime, _ := time.Parse(time.RFC3339, "2002-10-02T15:00:00Z") 83 | 84 | records := []MigrationRecord{ 85 | { 86 | Version: 1.0, 87 | Description: "1.0", 88 | AppliedAt: baseTime, 89 | }, 90 | { 91 | Version: 2.0, 92 | Description: "2.0", 93 | AppliedAt: baseTime.Add(2 * time.Second), 94 | }, 95 | } 96 | 97 | migrations := []Migration{ 98 | { 99 | Version: 1.0, 100 | Description: "Must Be APPLIED", 101 | Script: "does not matter!", 102 | }, 103 | { 104 | Version: 1.1, 105 | Description: "Must Be IGNORED", 106 | Script: "does not matter!", 107 | }, 108 | { 109 | Version: 2.0, 110 | Description: "Must Be APPLIED", 111 | Script: "does not matter!", 112 | }, 113 | { 114 | Version: 3.0, 115 | Description: "Must Be PENDING", 116 | Script: "does not matter!", 117 | }, 118 | } 119 | 120 | d := New(&dummyDriver{records: records}, migrations, nil) 121 | d.Migrate() 122 | infos, err := d.Info() 123 | 124 | if err != nil { 125 | t.Error("Must not return error") 126 | t.FailNow() 127 | } 128 | 129 | expectations := []Status{Applied, Ignored, Applied, Pending} 130 | 131 | for i, info := range infos { 132 | if expectations[i] != info.Status { 133 | t.Errorf("Expected %s, got %s", expectations[i], info.Status) 134 | t.FailNow() 135 | } 136 | } 137 | } 138 | 139 | func Test_Info_with_error(t *testing.T) { 140 | driver := &dummyDriver{AllError: true} 141 | migrations := []Migration{} 142 | 143 | _, err := Info(driver, migrations) 144 | 145 | if err == nil { 146 | t.Error("Must emit error") 147 | } 148 | } 149 | 150 | func Test_DuplicateMigrationVersionError_Error(t *testing.T) { 151 | err := DuplicateMigrationVersionError{Version: 1} 152 | 153 | if err.Error() != fmt.Sprintf("Multiple migrations have the version number %f.", 1.0) { 154 | t.Error("Must inform the version of the duplicated migration") 155 | } 156 | } 157 | 158 | func Test_IllegalMigrationVersionError_Error(t *testing.T) { 159 | err := IllegalMigrationVersionError{Version: 1} 160 | 161 | if err.Error() != fmt.Sprintf("Illegal migration version number %f.", 1.0) { 162 | t.Error("Must inform the version of the invalid migration") 163 | } 164 | } 165 | 166 | func Test_RemovedMigrationError_Error(t *testing.T) { 167 | err := RemovedMigrationError{Version: 1} 168 | 169 | if err.Error() != fmt.Sprintf("Migration %f was removed", 1.0) { 170 | t.Error("Must inform when a migration is removed from the list") 171 | } 172 | } 173 | 174 | func Test_InvalidChecksumError_Error(t *testing.T) { 175 | err := InvalidChecksumError{Version: 1} 176 | 177 | if err.Error() != fmt.Sprintf("Invalid cheksum for migration %f", 1.0) { 178 | t.Error("Must inform when a migration have an invalid checksum") 179 | } 180 | } 181 | 182 | func Test_Validate_invalid_version(t *testing.T) { 183 | migrations := []Migration{ 184 | { 185 | Version: -1, 186 | Description: "Hello World", 187 | Script: "does not matter!", 188 | }, 189 | } 190 | 191 | err := Validate(&dummyDriver{}, migrations) 192 | 193 | if err.(IllegalMigrationVersionError).Version != -1 { 194 | t.Errorf("Must not accept migrations with invalid version numbers") 195 | } 196 | } 197 | 198 | func Test_Validate_duplicated_version(t *testing.T) { 199 | migrations := []Migration{ 200 | { 201 | Version: 1, 202 | Description: "Hello World", 203 | Script: "does not matter!", 204 | }, 205 | { 206 | Version: 1, 207 | Description: "Hello World", 208 | Script: "does not matter!", 209 | }, 210 | } 211 | 212 | err := Validate(&dummyDriver{}, migrations) 213 | 214 | if err.(DuplicateMigrationVersionError).Version != 1 { 215 | t.Errorf("Must not accept migrations with duplicated version numbers") 216 | } 217 | } 218 | 219 | func Test_Validate_removed_migration(t *testing.T) { 220 | // Other fields are not necessary for testing... 221 | records := []MigrationRecord{ 222 | { 223 | Version: 1.0, 224 | }, 225 | { 226 | Version: 1.1, 227 | }, 228 | } 229 | 230 | migrations := []Migration{ 231 | { 232 | Version: 1.1, 233 | Description: "Hello World", 234 | Script: "does not matter!", 235 | }, 236 | } 237 | 238 | // Running with struct 239 | d := New(&dummyDriver{records: records}, migrations, nil) 240 | err := d.Validate() 241 | 242 | if err.(RemovedMigrationError).Version != 1 { 243 | t.Errorf("Must not validate when some migration was removed from the migration list") 244 | } 245 | } 246 | 247 | func Test_Validate_invalid_checksum(t *testing.T) { 248 | // Other fields are not necessary for testing... 249 | records := []MigrationRecord{ 250 | { 251 | Version: 1.0, 252 | Checksum: "3310d0ff858faac79e854454c9e403db", 253 | }, 254 | } 255 | 256 | migrations := []Migration{ 257 | { 258 | Version: 1.0, 259 | Description: "Hello World", 260 | Script: "does not matter!", 261 | }, 262 | } 263 | 264 | err := Validate(&dummyDriver{records: records}, migrations) 265 | 266 | if err.(InvalidChecksumError).Version != 1 { 267 | t.Errorf("Must not validate when some migration differ from the migration applied in the database") 268 | } 269 | } 270 | 271 | func Test_Migrate_migrate_all(t *testing.T) { 272 | migrations := []Migration{ 273 | { 274 | Version: 1, 275 | Description: "First Migration", 276 | Script: "does not matter!", 277 | }, 278 | { 279 | Version: 2, 280 | Description: "Second Migration", 281 | Script: "does not matter!", 282 | }, 283 | } 284 | 285 | driver := &dummyDriver{records: []MigrationRecord{}} 286 | 287 | infoChan := make(chan MigrationInfo, 2) 288 | 289 | Migrate(driver, migrations, infoChan) 290 | 291 | all, _ := driver.All() 292 | 293 | if len(all) != 2 { 294 | t.Errorf("Must not apply all migrations") 295 | } 296 | 297 | info := <-infoChan 298 | 299 | if info.Migration.Version != 1 { 300 | t.Errorf("Must send a message for each migration applied") 301 | } 302 | 303 | info = <-infoChan 304 | 305 | if info.Migration.Version != 2 { 306 | t.Errorf("Must send a message for each migration applied") 307 | } 308 | } 309 | 310 | func Test_Migrate_migrate_partial(t *testing.T) { 311 | applied := []MigrationRecord{ 312 | { 313 | Version: 1, 314 | Checksum: "3310d0ff858faac79e854454c9e403da", 315 | }, 316 | } 317 | 318 | migrations := []Migration{ 319 | { 320 | Version: 1, 321 | Description: "First Migration", 322 | Script: "does not matter!", 323 | }, 324 | { 325 | Version: 2, 326 | Description: "Second Migration", 327 | Script: "does not matter!", 328 | }, 329 | { 330 | Version: 3, 331 | Description: "Third Migration", 332 | Script: "does not matter!", 333 | }, 334 | } 335 | 336 | driver := &dummyDriver{records: applied} 337 | 338 | all, _ := driver.All() 339 | 340 | if len(all) != 1 { 341 | t.Errorf("Should have 1 migration already applied") 342 | } 343 | 344 | // Running with struct 345 | d := New(driver, migrations, nil) 346 | d.Migrate() 347 | 348 | all, _ = driver.All() 349 | 350 | if len(all) != 3 { 351 | t.Errorf("Must not apply all migrations") 352 | } 353 | } 354 | 355 | func Test_Migrate_migrate_error(t *testing.T) { 356 | driver := &dummyDriver{CreateError: true} 357 | migrations := []Migration{} 358 | 359 | err := Migrate(driver, migrations, nil) 360 | 361 | if err == nil { 362 | t.Error("Must emit error") 363 | } 364 | } 365 | 366 | func Test_Migrate_with_error_in_Validate(t *testing.T) { 367 | driver := &dummyDriver{AllError: true} 368 | migrations := []Migration{} 369 | 370 | err := Migrate(driver, migrations, nil) 371 | 372 | if err == nil { 373 | t.Error("Must emit error") 374 | } 375 | } 376 | 377 | func Test_Migrate_with_error_in_driver_insert(t *testing.T) { 378 | driver := &dummyDriver{InsertError: true} 379 | migrations := []Migration{ 380 | { 381 | Version: 1, 382 | Description: "First Migration", 383 | Script: "does not matter!", 384 | }, 385 | } 386 | 387 | err := Migrate(driver, migrations, nil) 388 | 389 | if err == nil { 390 | t.Error("Must emit error") 391 | } 392 | } 393 | 394 | func Test_Migrate_with_error_in_driver_exec(t *testing.T) { 395 | driver := &dummyDriver{ExecError: true} 396 | migrations := []Migration{ 397 | { 398 | Version: 1, 399 | Description: "First Migration", 400 | Script: "does not matter!", 401 | }, 402 | } 403 | 404 | Migrate(driver, migrations, nil) 405 | 406 | all, _ := driver.All() 407 | 408 | if len(all) != 0 { 409 | t.Errorf("Must not apply all migrations") 410 | } 411 | } 412 | 413 | func Test_planMigration_error_driver(t *testing.T) { 414 | driver := &dummyDriver{AllError: true} 415 | migrations := []Migration{} 416 | 417 | _, err := planMigration(driver, migrations) 418 | 419 | if err == nil { 420 | t.Error("Must emit error") 421 | } 422 | } 423 | 424 | func Test_byMigrationVersion(t *testing.T) { 425 | unordered := []Migration{ 426 | { 427 | Version: 3, 428 | Description: "Hello World", 429 | Script: "does not matter!", 430 | }, 431 | { 432 | Version: 1, 433 | Description: "Hello World", 434 | Script: "does not matter!", 435 | }, 436 | } 437 | 438 | sort.Sort(byMigrationVersion(unordered)) 439 | 440 | if unordered[0].Version != 1.0 { 441 | t.Errorf("Must order by version number") 442 | } 443 | } 444 | -------------------------------------------------------------------------------- /dialect.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | // Dialect is used to use multiple databases 4 | type Dialect interface { 5 | // CreateTableSQL returns the SQL to create the schema table 6 | CreateTableSQL() string 7 | 8 | // InsertSQL returns the SQL to insert a new migration in the schema table 9 | InsertSQL() string 10 | 11 | // AllSQL returns a SQL to get all entries in the table 12 | AllSQL() string 13 | } 14 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | // MigrationRecord is the entry in schema table 10 | type MigrationRecord struct { 11 | Version float64 12 | Description string 13 | Checksum string 14 | AppliedAt time.Time 15 | ExecutionTime time.Duration 16 | } 17 | 18 | // Driver a database driver abstraction 19 | type Driver interface { 20 | Create() error 21 | Insert(e MigrationRecord) error 22 | All() ([]MigrationRecord, error) 23 | Exec(string) (time.Duration, error) 24 | } 25 | 26 | // GenericDriver is the default Driver, it can be configured to any database. 27 | type GenericDriver struct { 28 | DB *sql.DB 29 | Dialect Dialect 30 | } 31 | 32 | // NewGenericDriver creates a new GenericDriver configured with db and dialect. 33 | // Panic if db or dialect is nil 34 | func NewGenericDriver(db *sql.DB, dialect Dialect) *GenericDriver { 35 | if db == nil { 36 | panic("darwin: sql.DB is nil") 37 | } 38 | 39 | if dialect == nil { 40 | panic("darwin: dialect is nil") 41 | } 42 | 43 | return &GenericDriver{DB: db, Dialect: dialect} 44 | } 45 | 46 | // Create create the table darwin_migrations if necessary 47 | func (m *GenericDriver) Create() error { 48 | err := transaction(m.DB, func(tx *sql.Tx) error { 49 | _, err := tx.Exec(m.Dialect.CreateTableSQL()) 50 | return err 51 | }) 52 | 53 | return err 54 | } 55 | 56 | // Insert insert a migration entry into database 57 | func (m *GenericDriver) Insert(e MigrationRecord) error { 58 | 59 | err := transaction(m.DB, func(tx *sql.Tx) error { 60 | _, err := tx.Exec(m.Dialect.InsertSQL(), 61 | e.Version, 62 | e.Description, 63 | e.Checksum, 64 | e.AppliedAt.Unix(), 65 | e.ExecutionTime, 66 | ) 67 | return err 68 | }) 69 | 70 | return err 71 | } 72 | 73 | // All returns all migrations applied 74 | func (m *GenericDriver) All() ([]MigrationRecord, error) { 75 | entries := []MigrationRecord{} 76 | 77 | rows, err := m.DB.Query(m.Dialect.AllSQL()) 78 | 79 | if err != nil { 80 | return []MigrationRecord{}, err 81 | } 82 | 83 | for rows.Next() { 84 | var ( 85 | version float64 86 | description string 87 | checksum string 88 | appliedAt int64 89 | executionTime float64 90 | ) 91 | 92 | rows.Scan( 93 | &version, 94 | &description, 95 | &checksum, 96 | &appliedAt, 97 | &executionTime, 98 | ) 99 | 100 | entry := MigrationRecord{ 101 | Version: version, 102 | Description: description, 103 | Checksum: checksum, 104 | AppliedAt: time.Unix(appliedAt, 0), 105 | ExecutionTime: time.Duration(executionTime), 106 | } 107 | 108 | entries = append(entries, entry) 109 | } 110 | 111 | rows.Close() 112 | 113 | return entries, nil 114 | } 115 | 116 | // Exec execute sql scripts into database 117 | func (m *GenericDriver) Exec(script string) (time.Duration, error) { 118 | start := time.Now() 119 | 120 | err := transaction(m.DB, func(tx *sql.Tx) error { 121 | _, err := tx.Exec(script) 122 | return err 123 | }) 124 | 125 | return time.Since(start), err 126 | } 127 | 128 | // transaction is a utility function to execute the SQL inside a transaction 129 | // Panic if db is nil 130 | // see: http://stackoverflow.com/a/23502629 131 | func transaction(db *sql.DB, f func(*sql.Tx) error) (err error) { 132 | if db == nil { 133 | panic("darwin: sql.DB is nil") 134 | } 135 | 136 | tx, err := db.Begin() 137 | 138 | if err != nil { 139 | return 140 | } 141 | 142 | defer func() { 143 | if p := recover(); p != nil { 144 | switch p := p.(type) { 145 | case error: 146 | err = p 147 | default: 148 | err = fmt.Errorf("%s", p) 149 | } 150 | } 151 | if err != nil { 152 | tx.Rollback() 153 | return 154 | } 155 | err = tx.Commit() 156 | }() 157 | 158 | return f(tx) 159 | } 160 | 161 | type byMigrationRecordVersion []MigrationRecord 162 | 163 | func (b byMigrationRecordVersion) Len() int { return len(b) } 164 | func (b byMigrationRecordVersion) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 165 | func (b byMigrationRecordVersion) Less(i, j int) bool { return b[i].Version < b[j].Version } 166 | -------------------------------------------------------------------------------- /driver_test.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "regexp" 7 | "sort" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/DATA-DOG/go-sqlmock" 13 | ) 14 | 15 | func assertPanic(t *testing.T, f func()) { 16 | defer func() { 17 | if r := recover(); r == nil { 18 | t.Errorf("The code did not panic") 19 | } 20 | }() 21 | f() 22 | } 23 | 24 | func Test_NewGenericDriver_sql_nil(t *testing.T) { 25 | assertPanic(t, func() { 26 | NewGenericDriver(nil, MySQLDialect{}) 27 | }) 28 | } 29 | 30 | func Test_NewGenericDriver_driver_nil(t *testing.T) { 31 | db, _, _ := sqlmock.New() 32 | 33 | defer db.Close() 34 | 35 | assertPanic(t, func() { 36 | NewGenericDriver(db, nil) 37 | }) 38 | } 39 | 40 | func Test_GenericDriver_Create(t *testing.T) { 41 | db, mock, err := sqlmock.New() 42 | 43 | if err != nil { 44 | t.Errorf("sqlmock.New().error != nil, wants nil") 45 | } 46 | 47 | defer db.Close() 48 | 49 | dialect := MySQLDialect{} 50 | 51 | mock.ExpectBegin() 52 | mock.ExpectExec(escapeQuery(dialect.CreateTableSQL())).WillReturnResult(sqlmock.NewResult(0, 0)) 53 | mock.ExpectCommit() 54 | 55 | d := NewGenericDriver(db, dialect) 56 | d.Create() 57 | 58 | if err := mock.ExpectationsWereMet(); err != nil { 59 | t.Errorf("there were unfulfilled expections: %s", err) 60 | } 61 | } 62 | 63 | func Test_GenericDriver_Insert(t *testing.T) { 64 | db, mock, err := sqlmock.New() 65 | 66 | if err != nil { 67 | t.Errorf("sqlmock.New().error != nil, wants nil") 68 | } 69 | 70 | defer db.Close() 71 | 72 | record := MigrationRecord{ 73 | Version: 1.0, 74 | Description: "Description", 75 | Checksum: "7ebca1c6f05333a728a8db4629e8d543", 76 | AppliedAt: time.Now(), 77 | ExecutionTime: time.Millisecond * 1, 78 | } 79 | 80 | dialect := MySQLDialect{} 81 | 82 | d := NewGenericDriver(db, dialect) 83 | 84 | mock.ExpectBegin() 85 | mock.ExpectExec(escapeQuery(dialect.InsertSQL())). 86 | WithArgs( 87 | record.Version, 88 | record.Description, 89 | record.Checksum, 90 | record.AppliedAt.Unix(), 91 | record.ExecutionTime, 92 | ). 93 | WillReturnResult(sqlmock.NewResult(1, 1)) 94 | 95 | mock.ExpectCommit() 96 | 97 | d.Insert(record) 98 | 99 | if err := mock.ExpectationsWereMet(); err != nil { 100 | t.Errorf("there were unfulfilled expections: %s", err) 101 | } 102 | } 103 | 104 | func Test_GenericDriver_All_success(t *testing.T) { 105 | db, mock, err := sqlmock.New() 106 | 107 | if err != nil { 108 | t.Errorf("sqlmock.New().error != nil, wants nil") 109 | } 110 | 111 | defer db.Close() 112 | 113 | dialect := MySQLDialect{} 114 | 115 | d := NewGenericDriver(db, dialect) 116 | 117 | rows := sqlmock.NewRows([]string{ 118 | "version", "description", "checksum", "applied_at", "execution_time", "success", 119 | }).AddRow( 120 | 1, "Description", "7ebca1c6f05333a728a8db4629e8d543", 121 | time.Now().Unix(), 122 | time.Millisecond*1, true, 123 | ) 124 | 125 | mock.ExpectQuery(escapeQuery(dialect.AllSQL())). 126 | WillReturnRows(rows) 127 | 128 | migrations, _ := d.All() 129 | 130 | if len(migrations) != 1 { 131 | t.Errorf("len(migrations) == %d, wants 1", len(migrations)) 132 | } 133 | 134 | if err := mock.ExpectationsWereMet(); err != nil { 135 | t.Errorf("there were unfulfilled expections: %s", err) 136 | } 137 | } 138 | 139 | func Test_GenericDriver_All_error(t *testing.T) { 140 | db, mock, err := sqlmock.New() 141 | 142 | if err != nil { 143 | t.Errorf("sqlmock.New().error != nil, wants nil") 144 | } 145 | 146 | defer db.Close() 147 | 148 | dialect := MySQLDialect{} 149 | 150 | d := NewGenericDriver(db, dialect) 151 | 152 | mock.ExpectQuery(escapeQuery(dialect.AllSQL())). 153 | WillReturnError(errors.New("Generic error")) 154 | 155 | migrations, _ := d.All() 156 | 157 | if len(migrations) != 0 { 158 | t.Errorf("len(migrations) == %d, wants 0", len(migrations)) 159 | } 160 | 161 | if err := mock.ExpectationsWereMet(); err != nil { 162 | t.Errorf("there were unfulfilled expections: %s", err) 163 | } 164 | } 165 | 166 | func Test_GenericDriver_Exec(t *testing.T) { 167 | db, mock, err := sqlmock.New() 168 | 169 | if err != nil { 170 | t.Errorf("sqlmock.New().error != nil, wants nil") 171 | } 172 | 173 | defer db.Close() 174 | 175 | stmt := "CREATE TABLE HELLO (id INT);" 176 | dialect := MySQLDialect{} 177 | 178 | d := NewGenericDriver(db, dialect) 179 | 180 | mock.ExpectBegin() 181 | mock.ExpectExec(escapeQuery(stmt)). 182 | WillReturnResult(sqlmock.NewResult(1, 1)) 183 | mock.ExpectCommit() 184 | 185 | d.Exec(stmt) 186 | 187 | if err := mock.ExpectationsWereMet(); err != nil { 188 | t.Errorf("there were unfulfilled expections: %s", err) 189 | } 190 | } 191 | 192 | func Test_GenericDriver_Exec_error(t *testing.T) { 193 | db, mock, err := sqlmock.New() 194 | 195 | if err != nil { 196 | t.Errorf("sqlmock.New().error != nil, wants nil") 197 | } 198 | 199 | defer db.Close() 200 | 201 | stmt := "CREATE TABLE HELLO (id INT);" 202 | dialect := MySQLDialect{} 203 | 204 | d := NewGenericDriver(db, dialect) 205 | 206 | mock.ExpectBegin() 207 | mock.ExpectExec(escapeQuery(stmt)). 208 | WillReturnError(errors.New("Generic Error")) 209 | mock.ExpectRollback() 210 | 211 | d.Exec(stmt) 212 | 213 | if err := mock.ExpectationsWereMet(); err != nil { 214 | t.Errorf("there were unfulfilled expections: %s", err) 215 | } 216 | } 217 | 218 | func Test_byMigrationRecordVersion(t *testing.T) { 219 | unordered := []MigrationRecord{ 220 | { 221 | Version: 1.1, 222 | Description: "Description", 223 | Checksum: "7ebca1c6f05333a728a8db4629e8d543", 224 | AppliedAt: time.Now(), 225 | ExecutionTime: time.Millisecond * 1, 226 | }, 227 | { 228 | Version: 1.0, 229 | Description: "Description", 230 | Checksum: "7ebca1c6f05333a728a8db4629e8d543", 231 | AppliedAt: time.Now(), 232 | ExecutionTime: time.Millisecond * 1, 233 | }, 234 | } 235 | 236 | sort.Sort(byMigrationRecordVersion(unordered)) 237 | 238 | if unordered[0].Version != 1.0 { 239 | t.Errorf("Must order by version number") 240 | } 241 | } 242 | 243 | func Test_transaction_panic_sql_nil(t *testing.T) { 244 | assertPanic(t, func() { 245 | transaction(nil, func(tx *sql.Tx) error { 246 | return nil 247 | }) 248 | }) 249 | } 250 | 251 | func Test_transaction_error_begin(t *testing.T) { 252 | db, mock, err := sqlmock.New() 253 | 254 | if err != nil { 255 | t.Errorf("sqlmock.New().error != nil, wants nil") 256 | } 257 | 258 | defer db.Close() 259 | 260 | mock.ExpectBegin().WillReturnError(errors.New("Generic Error")) 261 | 262 | transaction(db, func(tx *sql.Tx) error { 263 | return nil 264 | }) 265 | 266 | if err := mock.ExpectationsWereMet(); err != nil { 267 | t.Errorf("there were unfulfilled expections: %s", err) 268 | } 269 | } 270 | 271 | func Test_transaction_panic_with_error(t *testing.T) { 272 | db, mock, err := sqlmock.New() 273 | 274 | if err != nil { 275 | t.Errorf("sqlmock.New().error != nil, wants nil") 276 | } 277 | 278 | defer db.Close() 279 | 280 | mock.ExpectBegin() 281 | mock.ExpectRollback() 282 | 283 | err = transaction(db, func(tx *sql.Tx) error { 284 | panic(errors.New("Generic Error")) 285 | }) 286 | 287 | if err == nil { 288 | t.Errorf("Should handle the panic inside the transaction") 289 | } 290 | 291 | if err := mock.ExpectationsWereMet(); err != nil { 292 | t.Errorf("there were unfulfilled expections: %s", err) 293 | } 294 | } 295 | 296 | func Test_transaction_panic_with_message(t *testing.T) { 297 | db, mock, err := sqlmock.New() 298 | 299 | if err != nil { 300 | t.Errorf("sqlmock.New().error != nil, wants nil") 301 | } 302 | 303 | defer db.Close() 304 | 305 | mock.ExpectBegin() 306 | mock.ExpectRollback() 307 | 308 | err = transaction(db, func(tx *sql.Tx) error { 309 | panic("Generic Error") 310 | }) 311 | 312 | if err == nil { 313 | t.Errorf("Should handle the panic inside the transaction") 314 | } 315 | 316 | if err := mock.ExpectationsWereMet(); err != nil { 317 | t.Errorf("there were unfulfilled expections: %s", err) 318 | } 319 | } 320 | 321 | func escapeQuery(s string) string { 322 | re := regexp.MustCompile("\\s+") 323 | 324 | s1 := regexp.QuoteMeta(s) 325 | s1 = strings.TrimSpace(re.ReplaceAllString(s1, " ")) 326 | return s1 327 | } 328 | -------------------------------------------------------------------------------- /mysql_dialect.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | // MySQLDialect a Dialect configured for MySQL 4 | type MySQLDialect struct{} 5 | 6 | // CreateTableSQL returns the SQL to create the schema table 7 | func (m MySQLDialect) CreateTableSQL() string { 8 | return `CREATE TABLE IF NOT EXISTS darwin_migrations 9 | ( 10 | id INT auto_increment, 11 | version FLOAT NOT NULL, 12 | description VARCHAR(255) NOT NULL, 13 | checksum VARCHAR(32) NOT NULL, 14 | applied_at INT NOT NULL, 15 | execution_time FLOAT NOT NULL, 16 | UNIQUE (version), 17 | PRIMARY KEY (id) 18 | ) ENGINE=InnoDB CHARACTER SET=utf8;` 19 | } 20 | 21 | // InsertSQL returns the SQL to insert a new migration in the schema table 22 | func (m MySQLDialect) InsertSQL() string { 23 | return `INSERT INTO darwin_migrations 24 | ( 25 | version, 26 | description, 27 | checksum, 28 | applied_at, 29 | execution_time 30 | ) 31 | VALUES (?, ?, ?, ?, ?);` 32 | } 33 | 34 | // AllSQL returns a SQL to get all entries in the table 35 | func (m MySQLDialect) AllSQL() string { 36 | return `SELECT 37 | version, 38 | description, 39 | checksum, 40 | applied_at, 41 | execution_time 42 | FROM 43 | darwin_migrations 44 | ORDER BY version ASC;` 45 | } 46 | -------------------------------------------------------------------------------- /postgres_dialect.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | // PostgresDialect a Dialect configured for PostgreSQL 4 | type PostgresDialect struct{} 5 | 6 | // CreateTableSQL returns the SQL to create the schema table 7 | func (p PostgresDialect) CreateTableSQL() string { 8 | return `CREATE TABLE IF NOT EXISTS darwin_migrations 9 | ( 10 | id SERIAL NOT NULL, 11 | version REAL NOT NULL, 12 | description CHARACTER VARYING (255) NOT NULL, 13 | checksum CHARACTER VARYING (32) NOT NULL, 14 | applied_at INTEGER NOT NULL, 15 | execution_time REAL NOT NULL, 16 | UNIQUE (version), 17 | PRIMARY KEY (id) 18 | );` 19 | } 20 | 21 | // InsertSQL returns the SQL to insert a new migration in the schema table 22 | func (p PostgresDialect) InsertSQL() string { 23 | return `INSERT INTO darwin_migrations 24 | ( 25 | version, 26 | description, 27 | checksum, 28 | applied_at, 29 | execution_time 30 | ) 31 | VALUES ($1, $2, $3, $4, $5);` 32 | } 33 | 34 | // AllSQL returns a SQL to get all entries in the table 35 | func (p PostgresDialect) AllSQL() string { 36 | return `SELECT 37 | version, 38 | description, 39 | checksum, 40 | applied_at, 41 | execution_time 42 | FROM 43 | darwin_migrations 44 | ORDER BY version ASC;` 45 | } 46 | -------------------------------------------------------------------------------- /ql_dialect.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | //QLDialect implements Dialect interface for ql database 4 | type QLDialect struct { 5 | } 6 | 7 | // CreateTableSQL returns the SQL to create the schema table 8 | func (QLDialect) CreateTableSQL() string { 9 | return ` 10 | CREATE TABLE IF NOT EXISTS darwin_migrations( 11 | version float, 12 | description string, 13 | checksum string, 14 | applied_at int64, 15 | execution_time int64, 16 | ); 17 | CREATE UNIQUE INDEX IF NOT EXISTS idx_versions on darwin_migrations(version); 18 | ` 19 | } 20 | 21 | // InsertSQL returns the SQL to insert a new migration in the schema table 22 | func (QLDialect) InsertSQL() string { 23 | return `INSERT INTO darwin_migrations 24 | ( 25 | version, 26 | description, 27 | checksum, 28 | applied_at, 29 | execution_time 30 | ) 31 | VALUES ($1, $2, $3, $4, $5);` 32 | } 33 | 34 | // AllSQL returns a SQL to get all entries in the table 35 | func (QLDialect) AllSQL() string { 36 | return `SELECT 37 | version, 38 | description, 39 | checksum, 40 | applied_at, 41 | execution_time 42 | FROM 43 | darwin_migrations 44 | ORDER BY version ASC;` 45 | } 46 | -------------------------------------------------------------------------------- /ql_dialect_test.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "testing" 7 | 8 | _ "github.com/cznic/ql/driver" 9 | ) 10 | 11 | func TestQLDialect(t *testing.T) { 12 | migrations := []Migration{ 13 | { 14 | Version: 1, 15 | Description: "Creating table posts", 16 | Script: `CREATE TABLE posts ( 17 | id int, 18 | title string, 19 | );;`, 20 | }, 21 | { 22 | Version: 2, 23 | Description: "Adding column body", 24 | Script: "ALTER TABLE posts ADD body string;", 25 | }, 26 | } 27 | db, err := sql.Open("ql-mem", "test.db") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | dv := NewGenericDriver(db, QLDialect{}) 32 | 33 | d := New(dv, migrations, nil) 34 | err = d.Migrate() 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | if !hasTable(db, "posts", t) { 39 | t.Error("expected the tble posts to exist") 40 | } 41 | cols := getAllColumns(db, "posts", t) 42 | if len(cols) != 3 { 43 | t.Errorf("expected 3 columns got %d", len(cols)) 44 | } 45 | } 46 | 47 | func hasTable(db *sql.DB, tableName string, t *testing.T) bool { 48 | querry := "select count() from __Table where Name=$1" 49 | var count int 50 | err := db.QueryRow(querry, tableName).Scan(&count) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | return count > 0 55 | } 56 | 57 | func getAllColumns(db *sql.DB, tableName string, t *testing.T) []string { 58 | var result []string 59 | query := `select Name from __Column where TableName=$1` 60 | rows, err := db.Query(query, tableName) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | defer rows.Close() 65 | for rows.Next() { 66 | var name string 67 | if err := rows.Scan(&name); err != nil { 68 | log.Fatal(err) 69 | } 70 | result = append(result, name) 71 | } 72 | if err := rows.Err(); err != nil { 73 | t.Fatal(err) 74 | } 75 | return result 76 | } 77 | -------------------------------------------------------------------------------- /sqlite_dialect.go: -------------------------------------------------------------------------------- 1 | package darwin 2 | 3 | // SqliteDialect a Dialect configured for Sqlite3 4 | type SqliteDialect struct{} 5 | 6 | // CreateTableSQL returns the SQL to create the schema table 7 | func (s SqliteDialect) CreateTableSQL() string { 8 | return `CREATE TABLE IF NOT EXISTS darwin_migrations 9 | ( 10 | id INTEGER PRIMARY KEY, 11 | version FLOAT NOT NULL, 12 | description TEXT NOT NULL, 13 | checksum TEXT NOT NULL, 14 | applied_at DATETIME NOT NULL, 15 | execution_time FLOAT NOT NULL, 16 | UNIQUE (version) 17 | );` 18 | } 19 | 20 | // InsertSQL returns the SQL to insert a new migration in the schema table 21 | func (s SqliteDialect) InsertSQL() string { 22 | return `INSERT INTO darwin_migrations 23 | ( 24 | version, 25 | description, 26 | checksum, 27 | applied_at, 28 | execution_time 29 | ) 30 | VALUES (?, ?, ?, ?, ?);` 31 | } 32 | 33 | // AllSQL returns a SQL to get all entries in the table 34 | func (s SqliteDialect) AllSQL() string { 35 | return `SELECT 36 | version, 37 | description, 38 | checksum, 39 | applied_at, 40 | execution_time 41 | FROM 42 | darwin_migrations 43 | ORDER BY version ASC;` 44 | } 45 | --------------------------------------------------------------------------------