github.com/safing/portbase@v0.19.5/database/migration/migration.go (about) 1 package migration 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sort" 8 "sync" 9 "time" 10 11 "github.com/hashicorp/go-version" 12 13 "github.com/safing/portbase/database" 14 "github.com/safing/portbase/database/record" 15 "github.com/safing/portbase/formats/dsd" 16 "github.com/safing/portbase/log" 17 ) 18 19 // MigrateFunc is called when a migration should be applied to the 20 // database. It receives the current version (from) and the target 21 // version (to) of the database and a dedicated interface for 22 // interacting with data stored in the DB. 23 // A dedicated log.ContextTracer is added to ctx for each migration 24 // run. 25 type MigrateFunc func(ctx context.Context, from, to *version.Version, dbInterface *database.Interface) error 26 27 // Migration represents a registered data-migration that should be applied to 28 // some database. Migrations are stacked on top and executed in order of increasing 29 // version number (see Version field). 30 type Migration struct { 31 // Description provides a short human-readable description of the 32 // migration. 33 Description string 34 // Version should hold the version of the database/subsystem after 35 // the migration has been applied. 36 Version string 37 // MigrateFuc is executed when the migration should be performed. 38 MigrateFunc MigrateFunc 39 } 40 41 // Registry holds a migration stack. 42 type Registry struct { 43 key string 44 45 lock sync.Mutex 46 migrations []Migration 47 } 48 49 // New creates a new migration registry. 50 // The key should be the name of the database key that is used to store 51 // the version of the last successfully applied migration. 52 func New(key string) *Registry { 53 return &Registry{ 54 key: key, 55 } 56 } 57 58 // Add adds one or more migrations to reg. 59 func (reg *Registry) Add(migrations ...Migration) error { 60 reg.lock.Lock() 61 defer reg.lock.Unlock() 62 for _, m := range migrations { 63 if _, err := version.NewSemver(m.Version); err != nil { 64 return fmt.Errorf("migration %q: invalid version %s: %w", m.Description, m.Version, err) 65 } 66 reg.migrations = append(reg.migrations, m) 67 } 68 return nil 69 } 70 71 // Migrate migrates the database by executing all registered 72 // migration in order of increasing version numbers. The error 73 // returned, if not nil, is always of type *Diagnostics. 74 func (reg *Registry) Migrate(ctx context.Context) (err error) { 75 reg.lock.Lock() 76 defer reg.lock.Unlock() 77 78 start := time.Now() 79 log.Infof("migration: migration of %s started", reg.key) 80 defer func() { 81 if err != nil { 82 log.Errorf("migration: migration of %s failed after %s: %s", reg.key, time.Since(start), err) 83 } else { 84 log.Infof("migration: migration of %s finished after %s", reg.key, time.Since(start)) 85 } 86 }() 87 88 db := database.NewInterface(&database.Options{ 89 Local: true, 90 Internal: true, 91 }) 92 93 startOfMigration, err := reg.getLatestSuccessfulMigration(db) 94 if err != nil { 95 return err 96 } 97 98 execPlan, diag, err := reg.getExecutionPlan(startOfMigration) 99 if err != nil { 100 return err 101 } 102 if len(execPlan) == 0 { 103 return nil 104 } 105 diag.TargetVersion = execPlan[len(execPlan)-1].Version 106 107 // finally, apply our migrations 108 lastAppliedMigration := startOfMigration 109 for _, m := range execPlan { 110 target, _ := version.NewSemver(m.Version) // we can safely ignore the error here 111 112 migrationCtx, tracer := log.AddTracer(ctx) 113 114 if err := m.MigrateFunc(migrationCtx, lastAppliedMigration, target, db); err != nil { 115 diag.Wrapped = err 116 diag.FailedMigration = m.Description 117 tracer.Errorf("migration: migration for %s failed: %s - %s", reg.key, target.String(), m.Description) 118 tracer.Submit() 119 return diag 120 } 121 122 lastAppliedMigration = target 123 diag.LastSuccessfulMigration = lastAppliedMigration.String() 124 125 if err := reg.saveLastSuccessfulMigration(db, target); err != nil { 126 diag.Message = "failed to persist migration status" 127 diag.Wrapped = err 128 diag.FailedMigration = m.Description 129 } 130 tracer.Infof("migration: applied migration for %s: %s - %s", reg.key, target.String(), m.Description) 131 tracer.Submit() 132 } 133 134 // all migrations have been applied successfully, we're done here 135 return nil 136 } 137 138 func (reg *Registry) getLatestSuccessfulMigration(db *database.Interface) (*version.Version, error) { 139 // find the latest version stored in the database 140 rec, err := db.Get(reg.key) 141 if errors.Is(err, database.ErrNotFound) { 142 return nil, nil 143 } 144 if err != nil { 145 return nil, &Diagnostics{ 146 Message: "failed to query database for migration status", 147 Wrapped: err, 148 } 149 } 150 151 // Unwrap the record to get the actual database 152 r, ok := rec.(*record.Wrapper) 153 if !ok { 154 return nil, &Diagnostics{ 155 Wrapped: errors.New("expected wrapped database record"), 156 } 157 } 158 159 sv, err := version.NewSemver(string(r.Data)) 160 if err != nil { 161 return nil, &Diagnostics{ 162 Message: "failed to parse version stored in migration status record", 163 Wrapped: err, 164 } 165 } 166 return sv, nil 167 } 168 169 func (reg *Registry) saveLastSuccessfulMigration(db *database.Interface, ver *version.Version) error { 170 r := &record.Wrapper{ 171 Data: []byte(ver.String()), 172 Format: dsd.RAW, 173 } 174 r.SetKey(reg.key) 175 176 return db.Put(r) 177 } 178 179 func (reg *Registry) getExecutionPlan(startOfMigration *version.Version) ([]Migration, *Diagnostics, error) { 180 // create a look-up map for migrations indexed by their semver created a 181 // list of version (sorted by increasing number) that we use as our execution 182 // plan. 183 lm := make(map[string]Migration) 184 versions := make(version.Collection, 0, len(reg.migrations)) 185 for _, m := range reg.migrations { 186 ver, err := version.NewSemver(m.Version) 187 if err != nil { 188 return nil, nil, &Diagnostics{ 189 Message: "failed to parse version of migration", 190 Wrapped: err, 191 FailedMigration: m.Description, 192 } 193 } 194 lm[ver.String()] = m // use .String() for a normalized string representation 195 versions = append(versions, ver) 196 } 197 sort.Sort(versions) 198 199 diag := new(Diagnostics) 200 if startOfMigration != nil { 201 diag.StartOfMigration = startOfMigration.String() 202 } 203 204 // prepare our diagnostics and the execution plan 205 execPlan := make([]Migration, 0, len(versions)) 206 for _, ver := range versions { 207 // skip an migration that has already been applied. 208 if startOfMigration != nil && startOfMigration.GreaterThanOrEqual(ver) { 209 continue 210 } 211 m := lm[ver.String()] 212 diag.ExecutionPlan = append(diag.ExecutionPlan, DiagnosticStep{ 213 Description: m.Description, 214 Version: ver.String(), 215 }) 216 execPlan = append(execPlan, m) 217 } 218 219 return execPlan, diag, nil 220 }