- 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>
95 lines
2.2 KiB
Go
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
|
|
}
|