package migration import ( "strings" "testing" "github.com/glebarez/sqlite" "gorm.io/gorm" ) type checkedSchema struct { ID uint `gorm:"primaryKey"` Owner uint `gorm:"index:idx_checked_owner_state,priority:1"` State int `gorm:"index:idx_checked_owner_state,priority:2"` Code string `gorm:"uniqueIndex"` } func TestSchemaCheckRejectsMissingColumnsAndIncorrectIndexes(t *testing.T) { db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := CheckModels(db, &checkedSchema{}); err == nil { t.Fatal("missing table passed") } if err := db.Migrator().CreateTable(&checkedSchema{}); err != nil { t.Fatal(err) } if err := CheckModels(db, &checkedSchema{}); err != nil { t.Fatal(err) } for _, statement := range []string{ "DROP INDEX idx_checked_owner_state", "CREATE INDEX idx_checked_owner_state ON checked_schemas(state,owner)", } { if err := db.Exec(statement).Error; err != nil { t.Fatal(err) } } if err := CheckModels(db, &checkedSchema{}); err == nil || !strings.Contains(err.Error(), "idx_checked_owner_state") { t.Fatalf("reversed index passed: %v", err) } if err := db.Exec("CREATE INDEX legacy_owner_state ON checked_schemas(owner,state,id)").Error; err != nil { t.Fatal(err) } if err := CheckModels(db, &checkedSchema{}); err != nil { t.Fatalf("equivalent covering index rejected: %v", err) } if err := db.Exec("DROP INDEX idx_checked_schemas_code").Error; err != nil { t.Fatal(err) } if err := db.Exec("CREATE INDEX idx_checked_schemas_code ON checked_schemas(code)").Error; err != nil { t.Fatal(err) } if err := CheckModels(db, &checkedSchema{}); err == nil { t.Fatal("non-unique replacement passed") } if err := db.Migrator().DropColumn(&checkedSchema{}, "Owner"); err != nil { t.Fatal(err) } if err := CheckModels(db, &checkedSchema{}); err == nil || !strings.Contains(err.Error(), "missing column") { t.Fatalf("missing column passed: %v", err) } } func TestCheckAppliedDoesNotCreateOrApplyMigrations(t *testing.T) { db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatal(err) } called := false steps := []Step{{ID: "required", Migrate: func(*gorm.DB) error { called = true; return nil }}} if err := CheckApplied(db, steps); err == nil { t.Fatal("missing ledger passed") } if db.Migrator().HasTable(TableName) || called { t.Fatal("read-only check mutated the database") } if err := db.Migrator().CreateTable(&record{}); err != nil { t.Fatal(err) } if err := CheckApplied(db, steps); err == nil || !strings.Contains(err.Error(), "required") { t.Fatalf("pending migration passed: %v", err) } if err := db.Create(&record{ID: "required"}).Error; err != nil { t.Fatal(err) } // openWithDriverConfig returns a settings-bearing handle for MySQL's // table options. Read-only checks must not retain their Model on it. configured := db.Set("gorm:table_options", "ENGINE=InnoDB") if err := CheckApplied(configured, steps); err != nil { t.Fatal(err) } if configured.Statement.Model != nil { t.Fatal("schema check leaked the migration model into the shared handle") } if err := CheckApplied(db, steps); err != nil { t.Fatal(err) } if err := db.Create(&record{ID: "unknown"}).Error; err != nil { t.Fatal(err) } if err := CheckApplied(db, steps); err == nil { t.Fatal("unknown version passed") } if called { t.Fatal("read-only check ran a migration") } }