Implement local DB migrations and archived configuration lifecycle
This commit is contained in:
140
internal/localdb/migrations.go
Normal file
140
internal/localdb/migrations.go
Normal file
@@ -0,0 +1,140 @@
|
||||
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,
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user