diff --git a/go.sum b/go.sum index deb3eb8..c6f38eb 100644 --- a/go.sum +++ b/go.sum @@ -273,7 +273,6 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= github.com/mitchellh/hashstructure/v2 v2.0.1 h1:L60q1+q7cXE4JeEJJKMnh2brFIe3rZxCihYAB61ypAY= github.com/mitchellh/hashstructure/v2 v2.0.1/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= diff --git a/main.go b/main.go index 8226767..c0c1e2d 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "github.com/gorilla/handlers" "github.com/markbates/pkger" conf "github.com/muety/wakapi/config" - "github.com/muety/wakapi/migrations/common" + "github.com/muety/wakapi/migrations" "github.com/muety/wakapi/repositories" "gorm.io/gorm/logger" "log" @@ -96,9 +96,9 @@ func main() { defer sqlDb.Close() // Migrate database schema - common.RunCustomPreMigrations(db, config) + migrations.RunPreMigrations(db, config) runDatabaseMigrations() - common.RunCustomPostMigrations(db, config) + migrations.RunCustomPostMigrations(db, config) // Repositories aliasRepository = repositories.NewAliasRepository(db) diff --git a/migrations/00000000_apply_fixtures.go b/migrations/00000000_apply_fixtures.go new file mode 100644 index 0000000..4adc5bc --- /dev/null +++ b/migrations/00000000_apply_fixtures.go @@ -0,0 +1,17 @@ +package migrations + +import ( + "github.com/muety/wakapi/config" + "gorm.io/gorm" +) + +func init() { + f := migrationFunc{ + name: "000-apply_fixtures", + f: func(db *gorm.DB, cfg *config.Config) error { + return cfg.GetFixturesFunc(cfg.Db.Dialect)(db) + }, + } + + registerPostMigration(f) +} diff --git a/migrations/20201103_rename_language_mappings_table.go b/migrations/20201103_rename_language_mappings_table.go new file mode 100644 index 0000000..3473cfd --- /dev/null +++ b/migrations/20201103_rename_language_mappings_table.go @@ -0,0 +1,32 @@ +package migrations + +import ( + "github.com/emvi/logbuch" + "github.com/muety/wakapi/config" + "github.com/muety/wakapi/models" + "gorm.io/gorm" +) + +func init() { + f := migrationFunc{ + name: "20201103-rename_language_mappings_table", + f: func(db *gorm.DB, cfg *config.Config) error { + migrator := db.Migrator() + oldTableName, newTableName := "custom_rules", "language_mappings" + oldIndexName, newIndexName := "idx_customrule_user", "idx_language_mapping_user" + + if migrator.HasTable(oldTableName) { + logbuch.Info("renaming '%s' table to '%s'", oldTableName, newTableName) + if err := migrator.RenameTable(oldTableName, &models.LanguageMapping{}); err != nil { + return err + } + + logbuch.Info("renaming '%s' index to '%s'", oldIndexName, newIndexName) + return migrator.RenameIndex(&models.LanguageMapping{}, oldIndexName, newIndexName) + } + return nil + }, + } + + registerPreMigration(f) +} diff --git a/migrations/20201106_migration_cascade_constraints.go b/migrations/20201106_migration_cascade_constraints.go new file mode 100644 index 0000000..cf6f773 --- /dev/null +++ b/migrations/20201106_migration_cascade_constraints.go @@ -0,0 +1,79 @@ +package migrations + +import ( + "github.com/emvi/logbuch" + "github.com/muety/wakapi/config" + "github.com/muety/wakapi/models" + "gorm.io/gorm" +) + +func init() { + const name = "20201106-migration_cascade_constraints" + + f := migrationFunc{ + name: name, + f: func(db *gorm.DB, cfg *config.Config) error { + // drop all already existing foreign key constraints + // afterwards let them be re-created by auto migrate with the newly introduced cascade settings, + + migrator := db.Migrator() + + if cfg.Db.Dialect == config.SQLDialectSqlite { + // https://stackoverflow.com/a/1884893/3112139 + // unfortunately, we can't migrate existing sqlite databases to the newly introduced cascade settings + // things like deleting all summaries won't work in those cases unless an entirely new db is created + logbuch.Info("not attempting to drop and regenerate constraints on sqlite") + return nil + } + + if !migrator.HasTable(&models.KeyStringValue{}) { + logbuch.Info("key-value table not yet existing") + return nil + } + + condition := "key = ?" + if cfg.Db.Dialect == config.SQLDialectMysql { + condition = "`key` = ?" + } + lookupResult := db.Where(condition, name).First(&models.KeyStringValue{}) + if lookupResult.Error == nil && lookupResult.RowsAffected > 0 { + logbuch.Info("no need to migrate '%s'", name) + return nil + } + + // SELECT * FROM INFORMATION_SCHEMA.table_constraints; + constraints := map[string]interface{}{ + "fk_summaries_editors": &models.SummaryItem{}, + "fk_summaries_languages": &models.SummaryItem{}, + "fk_summaries_machines": &models.SummaryItem{}, + "fk_summaries_operating_systems": &models.SummaryItem{}, + "fk_summaries_projects": &models.SummaryItem{}, + "fk_summary_items_summary": &models.SummaryItem{}, + "fk_summaries_user": &models.Summary{}, + "fk_language_mappings_user": &models.LanguageMapping{}, + "fk_heartbeats_user": &models.Heartbeat{}, + "fk_aliases_user": &models.Alias{}, + } + + for name, table := range constraints { + if migrator.HasConstraint(table, name) { + logbuch.Info("dropping constraint '%s'", name) + if err := migrator.DropConstraint(table, name); err != nil { + return err + } + } + } + + if err := db.Create(&models.KeyStringValue{ + Key: name, + Value: "done", + }).Error; err != nil { + return err + } + + return nil + }, + } + + registerPreMigration(f) +} diff --git a/migrations/20210202_fix_cascade_for_alias_user_constraint.go b/migrations/20210202_fix_cascade_for_alias_user_constraint.go new file mode 100644 index 0000000..eab15a8 --- /dev/null +++ b/migrations/20210202_fix_cascade_for_alias_user_constraint.go @@ -0,0 +1,58 @@ +package migrations + +import ( + "github.com/emvi/logbuch" + "github.com/muety/wakapi/config" + "github.com/muety/wakapi/models" + "gorm.io/gorm" +) + +func init() { + const name = "20210202-fix_cascade_for_alias_user_constraint" + + f := migrationFunc{ + name: name, + f: func(db *gorm.DB, cfg *config.Config) error { + migrator := db.Migrator() + + if cfg.Db.Dialect == config.SQLDialectSqlite { + // see 20201106_migration_cascade_constraints + logbuch.Info("not attempting to drop and regenerate constraints on sqlite") + return nil + } + + if !migrator.HasTable(&models.KeyStringValue{}) { + logbuch.Info("key-value table not yet existing") + return nil + } + + condition := "key = ?" + if cfg.Db.Dialect == config.SQLDialectMysql { + condition = "`key` = ?" + } + lookupResult := db.Where(condition, name).First(&models.KeyStringValue{}) + if lookupResult.Error == nil && lookupResult.RowsAffected > 0 { + logbuch.Info("no need to migrate '%s'", name) + return nil + } + + if migrator.HasConstraint(&models.Alias{}, "fk_aliases_user") { + logbuch.Info("dropping constraint 'fk_aliases_user'") + if err := migrator.DropConstraint(&models.Alias{}, "fk_aliases_user"); err != nil { + return err + } + } + + if err := db.Create(&models.KeyStringValue{ + Key: name, + Value: "done", + }).Error; err != nil { + return err + } + + return nil + }, + } + + registerPreMigration(f) +} diff --git a/migrations/common/common.go b/migrations/common/common.go deleted file mode 100644 index 3cc0c99..0000000 --- a/migrations/common/common.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -import ( - "github.com/muety/wakapi/config" - "gorm.io/gorm" -) - -type migrationFunc struct { - f func(db *gorm.DB, cfg *config.Config) error - name string -} diff --git a/migrations/common/custom_post.go b/migrations/common/custom_post.go deleted file mode 100644 index 9d19e48..0000000 --- a/migrations/common/custom_post.go +++ /dev/null @@ -1,30 +0,0 @@ -package common - -import ( - "github.com/emvi/logbuch" - "github.com/muety/wakapi/config" - "gorm.io/gorm" -) - -var customPostMigrations []migrationFunc - -func init() { - customPostMigrations = []migrationFunc{ - { - f: func(db *gorm.DB, cfg *config.Config) error { - return cfg.GetFixturesFunc(cfg.Db.Dialect)(db) - }, - name: "apply fixtures", - }, - // TODO: add function to modify aggregated summaries according to configured custom language mappings - } -} - -func RunCustomPostMigrations(db *gorm.DB, cfg *config.Config) { - for _, m := range customPostMigrations { - logbuch.Info("potentially running migration '%s'", m.name) - if err := m.f(db, cfg); err != nil { - logbuch.Fatal("migration '%s' failed – %v", m.name, err) - } - } -} diff --git a/migrations/common/custom_pre.go b/migrations/common/custom_pre.go deleted file mode 100644 index 90a29e5..0000000 --- a/migrations/common/custom_pre.go +++ /dev/null @@ -1,108 +0,0 @@ -package common - -import ( - "github.com/emvi/logbuch" - "github.com/muety/wakapi/config" - "github.com/muety/wakapi/models" - "gorm.io/gorm" -) - -var customPreMigrations []migrationFunc - -func init() { - customPreMigrations = []migrationFunc{ - { - f: func(db *gorm.DB, cfg *config.Config) error { - migrator := db.Migrator() - oldTableName, newTableName := "custom_rules", "language_mappings" - oldIndexName, newIndexName := "idx_customrule_user", "idx_language_mapping_user" - - if migrator.HasTable(oldTableName) { - logbuch.Info("renaming '%s' table to '%s'", oldTableName, newTableName) - if err := migrator.RenameTable(oldTableName, &models.LanguageMapping{}); err != nil { - return err - } - - logbuch.Info("renaming '%s' index to '%s'", oldIndexName, newIndexName) - return migrator.RenameIndex(&models.LanguageMapping{}, oldIndexName, newIndexName) - } - return nil - }, - name: "rename language mappings table", - }, - { - f: func(db *gorm.DB, cfg *config.Config) error { - // drop all already existing foreign key constraints - // afterwards let them be re-created by auto migrate with the newly introduced cascade settings, - - migrator := db.Migrator() - const lookupKey = "20201106-migration_cascade_constraints" - - if cfg.Db.Dialect == config.SQLDialectSqlite { - // https://stackoverflow.com/a/1884893/3112139 - // unfortunately, we can't migrate existing sqlite databases to the newly introduced cascade settings - // things like deleting all summaries won't work in those cases unless an entirely new db is created - logbuch.Info("not attempting to drop and regenerate constraints on sqlite") - return nil - } - - if !migrator.HasTable(&models.KeyStringValue{}) { - logbuch.Info("key-value table not yet existing") - return nil - } - - condition := "key = ?" - if cfg.Db.Dialect == config.SQLDialectMysql { - condition = "`key` = ?" - } - lookupResult := db.Where(condition, lookupKey).First(&models.KeyStringValue{}) - if lookupResult.Error == nil && lookupResult.RowsAffected > 0 { - logbuch.Info("no need to migrate cascade constraints") - return nil - } - - // SELECT * FROM INFORMATION_SCHEMA.table_constraints; - constraints := map[string]interface{}{ - "fk_summaries_editors": &models.SummaryItem{}, - "fk_summaries_languages": &models.SummaryItem{}, - "fk_summaries_machines": &models.SummaryItem{}, - "fk_summaries_operating_systems": &models.SummaryItem{}, - "fk_summaries_projects": &models.SummaryItem{}, - "fk_summary_items_summary": &models.SummaryItem{}, - "fk_summaries_user": &models.Summary{}, - "fk_language_mappings_user": &models.LanguageMapping{}, - "fk_heartbeats_user": &models.Heartbeat{}, - "fk_aliases_user": &models.Alias{}, - } - - for name, table := range constraints { - if migrator.HasConstraint(table, name) { - logbuch.Info("dropping constraint '%s'", name) - if err := migrator.DropConstraint(table, name); err != nil { - return err - } - } - } - - if err := db.Create(&models.KeyStringValue{ - Key: lookupKey, - Value: "done", - }).Error; err != nil { - return err - } - - return nil - }, - name: "add cascade constraints", - }, - } -} - -func RunCustomPreMigrations(db *gorm.DB, cfg *config.Config) { - for _, m := range customPreMigrations { - logbuch.Info("potentially running migration '%s'", m.name) - if err := m.f(db, cfg); err != nil { - logbuch.Fatal("migration '%s' failed – %v", m.name, err) - } - } -} diff --git a/migrations/common/languages.go b/migrations/common/languages.go deleted file mode 100644 index e7922d0..0000000 --- a/migrations/common/languages.go +++ /dev/null @@ -1,25 +0,0 @@ -package common - -import ( - "github.com/emvi/logbuch" - "github.com/muety/wakapi/config" - "github.com/muety/wakapi/models" - "gorm.io/gorm" -) - -func MigrateLanguages(db *gorm.DB) { - cfg := config.Get() - - for k, v := range cfg.App.CustomLanguages { - result := db.Model(models.Heartbeat{}). - Where("language = ?", ""). - Where("entity LIKE ?", "%."+k). - Updates(models.Heartbeat{Language: v}) - if result.Error != nil { - logbuch.Fatal(result.Error.Error()) - } - if result.RowsAffected > 0 { - logbuch.Info("migrated %+v rows for custom language %+s", result.RowsAffected, k) - } - } -} diff --git a/migrations/common/fixtures/1_imprint_content.sql b/migrations/fixtures/1_imprint_content.sql similarity index 100% rename from migrations/common/fixtures/1_imprint_content.sql rename to migrations/fixtures/1_imprint_content.sql diff --git a/migrations/migrations.go b/migrations/migrations.go new file mode 100644 index 0000000..1507b44 --- /dev/null +++ b/migrations/migrations.go @@ -0,0 +1,67 @@ +package migrations + +import ( + "github.com/emvi/logbuch" + "github.com/muety/wakapi/config" + "gorm.io/gorm" + "sort" + "strings" +) + +type migrationFunc struct { + f func(db *gorm.DB, cfg *config.Config) error + name string +} + +type migrationFuncs []migrationFunc + +var ( + preMigrations migrationFuncs + postMigrations migrationFuncs +) + +func registerPreMigration(f migrationFunc) { + preMigrations = append(preMigrations, f) +} + +func registerPostMigration(f migrationFunc) { + postMigrations = append(postMigrations, f) +} + +// NOTE: Currently, migrations themselves keep track +// of whether they have run, yet or not, because some +// simply run on every start. + +func RunPreMigrations(db *gorm.DB, cfg *config.Config) { + sort.Sort(preMigrations) + + for _, m := range preMigrations { + logbuch.Info("potentially running migration '%s'", m.name) + if err := m.f(db, cfg); err != nil { + logbuch.Fatal("migration '%s' failed – %v", m.name, err) + } + } +} + +func RunCustomPostMigrations(db *gorm.DB, cfg *config.Config) { + sort.Sort(postMigrations) + + for _, m := range postMigrations { + logbuch.Info("potentially running migration '%s'", m.name) + if err := m.f(db, cfg); err != nil { + logbuch.Fatal("migration '%s' failed – %v", m.name, err) + } + } +} + +func (m migrationFuncs) Len() int { + return len(m) +} + +func (m migrationFuncs) Less(i, j int) bool { + return strings.Compare(m[i].name, m[j].name) < 0 +} + +func (m migrationFuncs) Swap(i, j int) { + m[i], m[j] = m[j], m[i] +} diff --git a/models/alias.go b/models/alias.go index 316745b..8649e98 100644 --- a/models/alias.go +++ b/models/alias.go @@ -2,8 +2,8 @@ package models type Alias struct { ID uint `gorm:"primary_key"` - Type uint8 `gorm:"not null; index:idx_alias_type_key; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` - User *User `json:"-" gorm:"not null"` + Type uint8 `gorm:"not null; index:idx_alias_type_key"` + User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` UserID string `gorm:"not null; index:idx_alias_user"` Key string `gorm:"not null; index:idx_alias_type_key"` Value string `gorm:"not null"`