package database import ( "sort" "time" "github.com/rs/zerolog/log" "gorm.io/gorm" ) // DataMigration tracks one-time data migrations that have been applied. type DataMigration struct { ID uint `gorm:"primaryKey"` Name string `gorm:"column:name;uniqueIndex;size:255;not null"` AppliedAt time.Time `gorm:"column:applied_at;not null"` } func (DataMigration) TableName() string { return "data_migrations" } // dataMigrationEntry pairs a name with its run function. type dataMigrationEntry struct { Name string Fn func(tx *gorm.DB) error } // registry holds all registered one-time data migrations, in order. var registry []dataMigrationEntry // RegisterDataMigration adds a one-time migration. Call from init() functions. func RegisterDataMigration(name string, fn func(tx *gorm.DB) error) { registry = append(registry, dataMigrationEntry{Name: name, Fn: fn}) } // RunDataMigrations creates the tracking table and executes any migrations // that haven't been applied yet. Called once during server startup. func RunDataMigrations() error { if db == nil { return nil } // Ensure tracking table exists if err := db.AutoMigrate(&DataMigration{}); err != nil { return err } if len(registry) == 0 { return nil } // Sort by name for deterministic order sort.Slice(registry, func(i, j int) bool { return registry[i].Name < registry[j].Name }) // Load already-applied migrations var applied []DataMigration if err := db.Find(&applied).Error; err != nil { return err } appliedSet := make(map[string]bool, len(applied)) for _, m := range applied { appliedSet[m.Name] = true } // Run pending migrations for _, entry := range registry { if appliedSet[entry.Name] { continue } log.Info().Str("migration", entry.Name).Msg("Running data migration") err := db.Transaction(func(tx *gorm.DB) error { if err := entry.Fn(tx); err != nil { return err } // Record that this migration has been applied return tx.Create(&DataMigration{ Name: entry.Name, AppliedAt: time.Now().UTC(), }).Error }) if err != nil { log.Error().Err(err).Str("migration", entry.Name).Msg("Data migration failed") return err } log.Info().Str("migration", entry.Name).Msg("Data migration completed") } return nil }