Files
honeyDueAPI/internal/database/data_migrations.go
Trey t 6803f6ec18 Add honeycomb completion heatmap and data migration framework
- Add completion_summary endpoint data to residence detail response
- Track completed_from_column on task completions (overdue/due_soon/upcoming)
- Add GetCompletionSummary repo method with monthly aggregation
- Add one-time data migration framework (data_migrations table + registry)
- Add backfill migration to classify historical completions
- Add standalone backfill script for manual/dry-run usage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:05:10 -05:00

95 lines
2.2 KiB
Go

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
}