package localdb import ( "fmt" "log/slog" "time" "github.com/google/uuid" "gorm.io/gorm" ) type LocalSchemaMigration struct { ID string `gorm:"primaryKey;size:128"` Name string `gorm:"not null;size:255"` AppliedAt time.Time `gorm:"not null"` } func (LocalSchemaMigration) TableName() string { return "local_schema_migrations" } type localMigration struct { id string name string run func(tx *gorm.DB) error } var localMigrations = []localMigration{ { id: "2026_02_04_versioning_backfill", name: "Ensure configuration versioning data and current pointers", run: backfillConfigurationVersions, }, { id: "2026_02_04_is_active_backfill", name: "Ensure is_active defaults to true for existing configurations", run: backfillConfigurationIsActive, }, } func runLocalMigrations(db *gorm.DB) error { if err := db.AutoMigrate(&LocalSchemaMigration{}); err != nil { return fmt.Errorf("migrate local schema migrations table: %w", err) } for _, migration := range localMigrations { var count int64 if err := db.Model(&LocalSchemaMigration{}).Where("id = ?", migration.id).Count(&count).Error; err != nil { return fmt.Errorf("check local migration %s: %w", migration.id, err) } if count > 0 { continue } if err := db.Transaction(func(tx *gorm.DB) error { if err := migration.run(tx); err != nil { return fmt.Errorf("run migration %s: %w", migration.id, err) } record := &LocalSchemaMigration{ ID: migration.id, Name: migration.name, AppliedAt: time.Now(), } if err := tx.Create(record).Error; err != nil { return fmt.Errorf("insert migration %s record: %w", migration.id, err) } return nil }); err != nil { return err } slog.Info("local migration applied", "id", migration.id, "name", migration.name) } return nil } func backfillConfigurationVersions(tx *gorm.DB) error { var configs []LocalConfiguration if err := tx.Find(&configs).Error; err != nil { return fmt.Errorf("load local configurations for backfill: %w", err) } for i := range configs { cfg := configs[i] var versionCount int64 if err := tx.Model(&LocalConfigurationVersion{}). Where("configuration_uuid = ?", cfg.UUID). Count(&versionCount).Error; err != nil { return fmt.Errorf("count versions for %s: %w", cfg.UUID, err) } if versionCount == 0 { snapshot, err := BuildConfigurationSnapshot(&cfg) if err != nil { return fmt.Errorf("build initial snapshot for %s: %w", cfg.UUID, err) } note := "Initial snapshot backfill (v1)" version := LocalConfigurationVersion{ ID: uuid.NewString(), ConfigurationUUID: cfg.UUID, VersionNo: 1, Data: snapshot, ChangeNote: ¬e, AppVersion: "backfill", CreatedAt: chooseNonZeroTime(cfg.CreatedAt, time.Now()), } if err := tx.Create(&version).Error; err != nil { return fmt.Errorf("create v1 backfill for %s: %w", cfg.UUID, err) } } if cfg.CurrentVersionID == nil || *cfg.CurrentVersionID == "" { var latest LocalConfigurationVersion if err := tx.Where("configuration_uuid = ?", cfg.UUID). Order("version_no DESC"). First(&latest).Error; err != nil { return fmt.Errorf("load latest version for %s: %w", cfg.UUID, err) } if err := tx.Model(&LocalConfiguration{}). Where("uuid = ?", cfg.UUID). Update("current_version_id", latest.ID).Error; err != nil { return fmt.Errorf("set current version for %s: %w", cfg.UUID, err) } } } return nil } func backfillConfigurationIsActive(tx *gorm.DB) error { return tx.Exec("UPDATE local_configurations SET is_active = 1 WHERE is_active IS NULL").Error } func chooseNonZeroTime(candidate time.Time, fallback time.Time) time.Time { if candidate.IsZero() { return fallback } return candidate }