github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/db/migration/migration_test.go (about) 1 package migration_test 2 3 import ( 4 "database/sql" 5 "io/ioutil" 6 "math/rand" 7 "strconv" 8 "strings" 9 "sync" 10 "time" 11 12 "code.cloudfoundry.org/lager" 13 14 "github.com/pf-qiu/concourse/v6/atc/db/lock" 15 "github.com/pf-qiu/concourse/v6/atc/db/migration" 16 "github.com/pf-qiu/concourse/v6/atc/db/migration/migrationfakes" 17 "github.com/lib/pq" 18 19 . "github.com/onsi/ginkgo" 20 . "github.com/onsi/gomega" 21 ) 22 23 const initialSchemaVersion = 1510262030 24 const upgradedSchemaVersion = 1510670987 25 26 var _ = Describe("Migration", func() { 27 var ( 28 err error 29 db *sql.DB 30 lockDB *sql.DB 31 lockFactory lock.LockFactory 32 bindata *migrationfakes.FakeBindata 33 fakeLogFunc = func(logger lager.Logger, id lock.LockID) {} 34 ) 35 36 BeforeEach(func() { 37 db, err = sql.Open("postgres", postgresRunner.DataSourceName()) 38 Expect(err).NotTo(HaveOccurred()) 39 40 lockDB, err = sql.Open("postgres", postgresRunner.DataSourceName()) 41 Expect(err).NotTo(HaveOccurred()) 42 43 lockFactory = lock.NewLockFactory(lockDB, fakeLogFunc, fakeLogFunc) 44 45 bindata = new(migrationfakes.FakeBindata) 46 bindata.AssetStub = asset 47 }) 48 49 AfterEach(func() { 50 _ = db.Close() 51 _ = lockDB.Close() 52 }) 53 54 Context("Migration test run", func() { 55 It("Runs all the migrations", func() { 56 migrator := migration.NewMigrator(db, lockFactory) 57 58 err := migrator.Up(nil, nil) 59 Expect(err).NotTo(HaveOccurred()) 60 }) 61 }) 62 63 Context("Version Check", func() { 64 It("CurrentVersion reports the current version stored in the database", func() { 65 bindata.AssetNamesReturns([]string{ 66 "1000_some_migration.up.sql", 67 "1510262030_initial_schema.up.sql", 68 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 69 "2000000000_latest_migration_does_not_matter.up.sql", 70 }) 71 bindata.AssetStub = func(name string) ([]byte, error) { 72 if name == "1000_some_migration.up.sql" { 73 return []byte{}, nil 74 } else if name == "2000000000_latest_migration_does_not_matter.up.sql" { 75 return []byte{}, nil 76 } 77 return asset(name) 78 } 79 80 myDatabaseVersion := 1234567890 81 82 SetupMigrationsHistoryTableToExistAtVersion(db, myDatabaseVersion) 83 84 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 85 86 version, err := migrator.CurrentVersion() 87 Expect(err).NotTo(HaveOccurred()) 88 Expect(version).To(Equal(myDatabaseVersion)) 89 }) 90 91 It("SupportedVersion reports the highest supported migration version", func() { 92 93 SetupMigrationsHistoryTableToExistAtVersion(db, initialSchemaVersion) 94 95 bindata.AssetNamesReturns([]string{ 96 "1000_some_migration.up.sql", 97 "1510262030_initial_schema.up.sql", 98 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 99 "300000_this_is_to_prove_we_dont_use_string_sort.up.sql", 100 "2000000000_latest_migration.up.sql", 101 }) 102 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 103 104 version, err := migrator.SupportedVersion() 105 Expect(err).NotTo(HaveOccurred()) 106 Expect(version).To(Equal(2000000000)) 107 }) 108 109 It("Ignores files it can't parse", func() { 110 111 SetupMigrationsHistoryTableToExistAtVersion(db, initialSchemaVersion) 112 113 bindata.AssetNamesReturns([]string{ 114 "1000_some_migration.up.sql", 115 "1510262030_initial_schema.up.sql", 116 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 117 "300000_this_is_to_prove_we_dont_use_string_sort.up.sql", 118 "2000000000_latest_migration.up.sql", 119 "migrations.go", 120 }) 121 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 122 123 version, err := migrator.SupportedVersion() 124 Expect(err).NotTo(HaveOccurred()) 125 Expect(version).To(Equal(2000000000)) 126 }) 127 }) 128 129 Context("Upgrade", func() { 130 Context("old schema_migrations table exist", func() { 131 var dirty bool 132 133 JustBeforeEach(func() { 134 SetupSchemaMigrationsTable(db, 8878, dirty) 135 }) 136 137 Context("dirty state is true", func() { 138 BeforeEach(func() { 139 dirty = true 140 }) 141 142 It("errors", func() { 143 144 Expect(err).NotTo(HaveOccurred()) 145 146 migrator := migration.NewMigrator(db, lockFactory) 147 148 err = migrator.Up(nil, nil) 149 Expect(err).To(HaveOccurred()) 150 Expect(err.Error()).To(ContainSubstring("database is in a dirty state")) 151 152 var newTableCreated bool 153 err = db.QueryRow("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name='migrations_history')").Scan(&newTableCreated) 154 Expect(newTableCreated).To(BeFalse()) 155 }) 156 }) 157 158 Context("dirty state is false", func() { 159 BeforeEach(func() { 160 dirty = false 161 }) 162 163 It("populate migrations_history table with starting version from schema_migrations table", func() { 164 startTime := time.Now() 165 migrator := migration.NewMigrator(db, lockFactory) 166 167 err = migrator.Up(nil, nil) 168 Expect(err).NotTo(HaveOccurred()) 169 170 var ( 171 version int 172 isDirty bool 173 timeStamp pq.NullTime 174 status string 175 direction string 176 ) 177 err = db.QueryRow("SELECT * from migrations_history ORDER BY tstamp ASC LIMIT 1").Scan(&version, &timeStamp, &direction, &status, &isDirty) 178 Expect(version).To(Equal(8878)) 179 Expect(isDirty).To(BeFalse()) 180 Expect(timeStamp.Time.After(startTime)).To(Equal(true)) 181 Expect(direction).To(Equal("up")) 182 Expect(status).To(Equal("passed")) 183 }) 184 185 Context("when the migrations_history table already exists", func() { 186 It("does not repopulate the migrations_history table", func() { 187 SetupMigrationsHistoryTableToExistAtVersion(db, 8878) 188 startTime := time.Now() 189 migrator := migration.NewMigrator(db, lockFactory) 190 191 err = migrator.Up(nil, nil) 192 Expect(err).NotTo(HaveOccurred()) 193 194 var timeStamp pq.NullTime 195 rows, err := db.Query("SELECT tstamp FROM migrations_history WHERE version=8878") 196 Expect(err).NotTo(HaveOccurred()) 197 var numRows = 0 198 for rows.Next() { 199 err = rows.Scan(&timeStamp) 200 numRows++ 201 } 202 Expect(numRows).To(Equal(1)) 203 Expect(timeStamp.Time.Before(startTime)).To(Equal(true)) 204 }) 205 }) 206 }) 207 }) 208 209 Context("sql migrations", func() { 210 It("runs a migration", func() { 211 simpleMigrationFilename := "1000_test_table_created.up.sql" 212 bindata.AssetReturns([]byte(` 213 BEGIN; 214 CREATE TABLE some_table (id integer); 215 COMMIT; 216 `), nil) 217 218 bindata.AssetNamesReturns([]string{ 219 simpleMigrationFilename, 220 }) 221 222 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 223 224 migrations, err := migrator.Migrations() 225 Expect(err).NotTo(HaveOccurred()) 226 Expect(len(migrations)).To(Equal(1)) 227 228 err = migrator.Up(nil, nil) 229 Expect(err).NotTo(HaveOccurred()) 230 231 By("Creating the table in the database") 232 var exists string 233 err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM information_schema.tables where table_name = 'some_table')").Scan(&exists) 234 Expect(err).NotTo(HaveOccurred()) 235 Expect(exists).To(Equal("true")) 236 237 By("Updating the migrations_history table") 238 ExpectDatabaseMigrationVersionToEqual(migrator, 1000) 239 }) 240 241 It("ignores migrations before the current version", func() { 242 SetupMigrationsHistoryTableToExistAtVersion(db, 1000) 243 244 simpleMigrationFilename := "1000_test_table_created.up.sql" 245 bindata.AssetStub = func(name string) ([]byte, error) { 246 if name == simpleMigrationFilename { 247 return []byte(` 248 BEGIN; 249 CREATE TABLE some_table (id integer); 250 COMMIT; 251 `), nil 252 } 253 return asset(name) 254 } 255 bindata.AssetNamesReturns([]string{ 256 simpleMigrationFilename, 257 }) 258 259 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 260 err := migrator.Up(nil, nil) 261 Expect(err).NotTo(HaveOccurred()) 262 263 By("Not creating the database referenced in the migration") 264 var exists string 265 err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM information_schema.tables where table_name = 'some_table')").Scan(&exists) 266 Expect(err).NotTo(HaveOccurred()) 267 Expect(exists).To(Equal("false")) 268 }) 269 270 It("runs the up migrations in ascending order", func() { 271 addTableMigrationFilename := "1000_test_table_created.up.sql" 272 removeTableMigrationFilename := "1001_test_table_created.up.sql" 273 274 bindata.AssetStub = func(name string) ([]byte, error) { 275 if name == addTableMigrationFilename { 276 return []byte(` 277 BEGIN; 278 CREATE TABLE some_table (id integer); 279 COMMIT; 280 `), nil 281 } else if name == removeTableMigrationFilename { 282 return []byte(` 283 BEGIN; 284 DROP TABLE some_table; 285 COMMIT; 286 `), nil 287 } 288 return asset(name) 289 } 290 291 bindata.AssetNamesReturns([]string{ 292 removeTableMigrationFilename, 293 addTableMigrationFilename, 294 }) 295 296 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 297 err := migrator.Up(nil, nil) 298 Expect(err).NotTo(HaveOccurred()) 299 300 }) 301 302 Context("when sql migrations fail", func() { 303 BeforeEach(func() { 304 bindata.AssetNamesReturns([]string{ 305 "1510262030_initial_schema.up.sql", 306 "1525724789_drop_reaper_addr_from_workers.up.sql", 307 }) 308 }) 309 310 It("rolls back and leaves the database clean", func() { 311 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 312 err := migrator.Up(nil, nil) 313 Expect(err).To(HaveOccurred()) 314 Expect(err.Error()).To(ContainSubstring("failed and was rolled back")) 315 ExpectDatabaseMigrationVersionToEqual(migrator, initialSchemaVersion) 316 ExpectMigrationToHaveFailed(db, 1525724789, false) 317 }) 318 }) 319 320 It("Doesn't fail if there are no migrations to run", func() { 321 bindata.AssetNamesReturns([]string{ 322 "1510262030_initial_schema.up.sql", 323 }) 324 325 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 326 err := migrator.Up(nil, nil) 327 Expect(err).NotTo(HaveOccurred()) 328 329 err = migrator.Up(nil, nil) 330 Expect(err).NotTo(HaveOccurred()) 331 332 ExpectDatabaseMigrationVersionToEqual(migrator, initialSchemaVersion) 333 334 ExpectMigrationVersionTableNotToExist(db) 335 336 ExpectToBeAbleToInsertData(db) 337 }) 338 339 It("Locks the database so multiple ATCs don't all run migrations at the same time", func() { 340 SetupMigrationsHistoryTableToExistAtVersion(db, 1510262030) 341 342 SetupSchemaFromFile(db, "migrations/1510262030_initial_schema.up.sql") 343 344 bindata.AssetNamesReturns([]string{ 345 "1510262030_initial_schema.up.sql", 346 }) 347 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 348 349 var wg sync.WaitGroup 350 wg.Add(3) 351 352 go TryRunUpAndVerifyResult(db, migrator, &wg) 353 go TryRunUpAndVerifyResult(db, migrator, &wg) 354 go TryRunUpAndVerifyResult(db, migrator, &wg) 355 356 wg.Wait() 357 }) 358 }) 359 360 Context("golang migrations", func() { 361 It("runs a migration with Migrate", func() { 362 363 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 364 bindata.AssetNamesReturns([]string{ 365 "1510262030_initial_schema.up.sql", 366 "1516643303_update_auth_providers.up.go", 367 }) 368 369 By("applying the initial migration") 370 err := migrator.Migrate(nil, nil, 1510262030) 371 var columnExists string 372 err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM information_schema.columns where table_name = 'teams' AND column_name='basic_auth')").Scan(&columnExists) 373 Expect(err).NotTo(HaveOccurred()) 374 Expect(columnExists).To(Equal("true")) 375 376 err = migrator.Migrate(nil, nil, 1516643303) 377 Expect(err).NotTo(HaveOccurred()) 378 379 By("applying the go migration") 380 err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM information_schema.columns where table_name = 'teams' AND column_name='basic_auth')").Scan(&columnExists) 381 Expect(err).NotTo(HaveOccurred()) 382 Expect(columnExists).To(Equal("false")) 383 384 By("updating the schema migrations table") 385 ExpectDatabaseMigrationVersionToEqual(migrator, 1516643303) 386 }) 387 388 It("runs a migration with Up", func() { 389 390 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 391 bindata.AssetNamesReturns([]string{ 392 "1510262030_initial_schema.up.sql", 393 "1516643303_update_auth_providers.up.go", 394 }) 395 396 err := migrator.Up(nil, nil) 397 Expect(err).NotTo(HaveOccurred()) 398 399 By("applying the migration") 400 var columnExists string 401 err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM information_schema.columns where table_name = 'teams' AND column_name='basic_auth')").Scan(&columnExists) 402 Expect(err).NotTo(HaveOccurred()) 403 Expect(columnExists).To(Equal("false")) 404 405 By("updating the schema migrations table") 406 ExpectDatabaseMigrationVersionToEqual(migrator, 1516643303) 407 }) 408 }) 409 }) 410 411 Context("Downgrade", func() { 412 Context("Downgrades to a version that uses the old mattes/migrate schema_migrations table", func() { 413 It("Downgrades to a given version and write it to a new created schema_migrations table", func() { 414 bindata.AssetNamesReturns([]string{ 415 "1510262030_initial_schema.up.sql", 416 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 417 "1510670987_update_unique_constraint_for_resource_caches.down.sql", 418 }) 419 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 420 421 err := migrator.Up(nil, nil) 422 Expect(err).NotTo(HaveOccurred()) 423 424 currentVersion, err := migrator.CurrentVersion() 425 Expect(err).NotTo(HaveOccurred()) 426 Expect(currentVersion).To(Equal(upgradedSchemaVersion)) 427 428 err = migrator.Migrate(nil, nil, initialSchemaVersion) 429 Expect(err).NotTo(HaveOccurred()) 430 431 currentVersion, err = migrator.CurrentVersion() 432 Expect(err).NotTo(HaveOccurred()) 433 Expect(currentVersion).To(Equal(initialSchemaVersion)) 434 435 ExpectDatabaseVersionToEqual(db, initialSchemaVersion, "schema_migrations") 436 437 ExpectToBeAbleToInsertData(db) 438 }) 439 440 It("Downgrades to a given version and write it to the existing schema_migrations table with dirty true", func() { 441 442 bindata.AssetNamesReturns([]string{ 443 "1510262030_initial_schema.up.sql", 444 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 445 "1510670987_update_unique_constraint_for_resource_caches.down.sql", 446 }) 447 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 448 449 err := migrator.Up(nil, nil) 450 Expect(err).NotTo(HaveOccurred()) 451 452 currentVersion, err := migrator.CurrentVersion() 453 Expect(err).NotTo(HaveOccurred()) 454 Expect(currentVersion).To(Equal(upgradedSchemaVersion)) 455 456 SetupSchemaMigrationsTable(db, 8878, true) 457 458 err = migrator.Migrate(nil, nil, initialSchemaVersion) 459 Expect(err).NotTo(HaveOccurred()) 460 461 currentVersion, err = migrator.CurrentVersion() 462 Expect(err).NotTo(HaveOccurred()) 463 Expect(currentVersion).To(Equal(initialSchemaVersion)) 464 465 ExpectDatabaseVersionToEqual(db, initialSchemaVersion, "schema_migrations") 466 467 ExpectToBeAbleToInsertData(db) 468 }) 469 }) 470 471 Context("Downgrades to a version with new migrations_history table", func() { 472 It("Downgrades to a given version", func() { 473 bindata.AssetNamesReturns([]string{ 474 "1510262030_initial_schema.up.sql", 475 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 476 "1510670987_update_unique_constraint_for_resource_caches.down.sql", 477 }) 478 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 479 480 err := migrator.Up(nil, nil) 481 Expect(err).NotTo(HaveOccurred()) 482 483 currentVersion, err := migrator.CurrentVersion() 484 Expect(err).NotTo(HaveOccurred()) 485 Expect(currentVersion).To(Equal(upgradedSchemaVersion)) 486 487 err = migrator.Migrate(nil, nil, initialSchemaVersion) 488 Expect(err).NotTo(HaveOccurred()) 489 490 currentVersion, err = migrator.CurrentVersion() 491 Expect(err).NotTo(HaveOccurred()) 492 Expect(currentVersion).To(Equal(initialSchemaVersion)) 493 494 ExpectToBeAbleToInsertData(db) 495 }) 496 497 It("Doesn't fail if already at the requested version", func() { 498 bindata.AssetNamesReturns([]string{ 499 "1510262030_initial_schema.up.sql", 500 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 501 }) 502 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 503 504 err := migrator.Migrate(nil, nil, upgradedSchemaVersion) 505 Expect(err).NotTo(HaveOccurred()) 506 507 currentVersion, err := migrator.CurrentVersion() 508 Expect(err).NotTo(HaveOccurred()) 509 Expect(currentVersion).To(Equal(upgradedSchemaVersion)) 510 511 err = migrator.Migrate(nil, nil, upgradedSchemaVersion) 512 Expect(err).NotTo(HaveOccurred()) 513 514 currentVersion, err = migrator.CurrentVersion() 515 Expect(err).NotTo(HaveOccurred()) 516 Expect(currentVersion).To(Equal(upgradedSchemaVersion)) 517 518 ExpectToBeAbleToInsertData(db) 519 }) 520 521 It("Locks the database so multiple consumers don't run downgrade at the same time", func() { 522 migrator := migration.NewMigratorForMigrations(db, lockFactory, bindata) 523 bindata.AssetNamesReturns([]string{ 524 "1510262030_initial_schema.up.sql", 525 "1510670987_update_unique_constraint_for_resource_caches.up.sql", 526 "1510670987_update_unique_constraint_for_resource_caches.down.sql", 527 }) 528 529 err := migrator.Up(nil, nil) 530 Expect(err).NotTo(HaveOccurred()) 531 532 var wg sync.WaitGroup 533 wg.Add(3) 534 535 go TryRunMigrateAndVerifyResult(db, migrator, initialSchemaVersion, &wg) 536 go TryRunMigrateAndVerifyResult(db, migrator, initialSchemaVersion, &wg) 537 go TryRunMigrateAndVerifyResult(db, migrator, initialSchemaVersion, &wg) 538 539 wg.Wait() 540 }) 541 }) 542 }) 543 544 }) 545 546 func TryRunUpAndVerifyResult(db *sql.DB, migrator migration.Migrator, wg *sync.WaitGroup) { 547 defer GinkgoRecover() 548 defer wg.Done() 549 550 err := migrator.Up(nil, nil) 551 Expect(err).NotTo(HaveOccurred()) 552 553 ExpectDatabaseMigrationVersionToEqual(migrator, initialSchemaVersion) 554 555 ExpectToBeAbleToInsertData(db) 556 } 557 558 func TryRunMigrateAndVerifyResult(db *sql.DB, migrator migration.Migrator, version int, wg *sync.WaitGroup) { 559 defer GinkgoRecover() 560 defer wg.Done() 561 562 err := migrator.Migrate(nil, nil, version) 563 Expect(err).NotTo(HaveOccurred()) 564 565 ExpectDatabaseMigrationVersionToEqual(migrator, version) 566 567 ExpectToBeAbleToInsertData(db) 568 } 569 570 func SetupMigrationsHistoryTableToExistAtVersion(db *sql.DB, version int) { 571 _, err := db.Exec(`CREATE TABLE migrations_history(version bigint, tstamp timestamp with time zone, direction varchar, status varchar, dirty boolean)`) 572 Expect(err).NotTo(HaveOccurred()) 573 574 _, err = db.Exec(`INSERT INTO migrations_history(version, tstamp, direction, status, dirty) VALUES($1, current_timestamp, 'up', 'passed', false)`, version) 575 Expect(err).NotTo(HaveOccurred()) 576 } 577 578 func SetupSchemaMigrationsTable(db *sql.DB, version int, dirty bool) { 579 _, err := db.Exec("CREATE TABLE IF NOT EXISTS schema_migrations (version bigint, dirty boolean)") 580 Expect(err).NotTo(HaveOccurred()) 581 _, err = db.Exec("INSERT INTO schema_migrations (version, dirty) VALUES ($1, $2)", version, dirty) 582 Expect(err).NotTo(HaveOccurred()) 583 } 584 585 func SetupSchemaFromFile(db *sql.DB, path string) { 586 migrations, err := ioutil.ReadFile(path) 587 Expect(err).NotTo(HaveOccurred()) 588 589 for _, migration := range strings.Split(string(migrations), ";") { 590 _, err = db.Exec(migration) 591 Expect(err).NotTo(HaveOccurred()) 592 } 593 } 594 595 func ExpectDatabaseMigrationVersionToEqual(migrator migration.Migrator, expectedVersion int) { 596 var dbVersion int 597 dbVersion, err := migrator.CurrentVersion() 598 Expect(err).NotTo(HaveOccurred()) 599 Expect(dbVersion).To(Equal(expectedVersion)) 600 } 601 602 func ExpectToBeAbleToInsertData(dbConn *sql.DB) { 603 rand.Seed(time.Now().UnixNano()) 604 605 teamID := rand.Intn(10000) 606 _, err := dbConn.Exec("INSERT INTO teams(id, name) VALUES ($1, $2)", teamID, strconv.Itoa(teamID)) 607 Expect(err).NotTo(HaveOccurred()) 608 609 pipelineID := rand.Intn(10000) 610 _, err = dbConn.Exec("INSERT INTO pipelines(id, team_id, name) VALUES ($1, $2, $3)", pipelineID, teamID, strconv.Itoa(pipelineID)) 611 Expect(err).NotTo(HaveOccurred()) 612 613 jobID := rand.Intn(10000) 614 _, err = dbConn.Exec("INSERT INTO jobs(id, pipeline_id, name, config) VALUES ($1, $2, $3, '{}')", jobID, pipelineID, strconv.Itoa(jobID)) 615 Expect(err).NotTo(HaveOccurred()) 616 } 617 618 func ExpectMigrationToHaveFailed(dbConn *sql.DB, failedVersion int, expectDirty bool) { 619 var status string 620 var dirty bool 621 err := dbConn.QueryRow("SELECT status, dirty FROM migrations_history WHERE version=$1 ORDER BY tstamp desc LIMIT 1", failedVersion).Scan(&status, &dirty) 622 Expect(err).NotTo(HaveOccurred()) 623 Expect(status).To(Equal("failed")) 624 Expect(dirty).To(Equal(expectDirty)) 625 }