db: Create custom migrations support

This commit is contained in:
IRHM
2026-06-27 23:18:03 +01:00
committed by momi
parent f73105e9c7
commit e980a1f70e
4 changed files with 152 additions and 0 deletions
+20
View File
@@ -1,26 +1,37 @@
package database
import (
"log/slog"
"path"
"github.com/sbondCo/Watcharr/config"
"github.com/sbondCo/Watcharr/database/entity"
"github.com/sbondCo/Watcharr/database/migrate"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Create a new database connection.
// Also runs migrations, etc, before returning connection.
// Any error returned from this func should always make our app Exit (caller
// handled).
func New() (*gorm.DB, error) {
slog.Info("New: Opening new database connection")
// Open the database.
db, err := gorm.Open(
sqlite.Open(path.Join(config.DataPath, "watcharr.db")),
&gorm.Config{TranslateError: true},
)
if err != nil {
slog.Error("New: Opening database failed.")
return nil, err
}
return nil, err
}
// Perform auto migration.
slog.Info("New: AutoMigrating")
err = db.AutoMigrate(
&migrate.MigrationRecord{},
&entity.User{},
&entity.UserServices{},
&entity.Content{},
@@ -36,6 +47,15 @@ func New() (*gorm.DB, error) {
&entity.Tag{},
)
if err != nil {
slog.Error("New: Auto migration failed.")
return nil, err
}
slog.Info("New: AutoMigrated")
// Perform our manual migrations.
if err := migrate.Now(db); err != nil {
slog.Error("New: Manual migrations failed.", "error", err)
return nil, err
}
return nil, err
}
return db, nil
+11
View File
@@ -0,0 +1,11 @@
package migrate
import "time"
// Record of all our applied migrations.
type MigrationRecord struct {
// Migration ID
ID string `gorm:"primarykey"`
// When migration was applied on this db.
CreatedAt time.Time
}
+100
View File
@@ -0,0 +1,100 @@
package migrate
import (
"log/slog"
"time"
"gorm.io/gorm"
)
type Migration struct {
// ID of migration, stick to YYYYMMDDHHMM.
ID string
// Apply migration func.
Up func(tx *gorm.DB) error
// When `true`, the migration is not run inside of a transaction.
// You should only use this when is required by sqlite that the command
// we need to run for eg cannot be ran from within a transaction!
// YOU SHOULD ENSURE YOU ONLY RUN ONE COMMAND PER MIGRATION WHEN USING
// THIS WITH `TRUE` TO AVOID BEING LEFT IN A BAD OR INCOMPLETE STATE!!!
//
// ALSO: All statements used with this should take into account that since,
// it isn't inside of a transaction, it's possible the migration succeeds,
// but creating the record of it doesn't. If a user starts the server again
// after we error in this case, the migration will run again, so it must
// not break data integrity or make any assumptions of it being the first
// time running!
UNSAFE bool
}
// Start our migrations.
// NOTE: This is only to be ran after GORM's AutoMigrate.
func Now(db *gorm.DB) error {
slog.Info("Starting migrations.")
for _, mig := range migrations {
slog.Debug("Processing migration.", "id", mig.ID)
migRecord := MigrationRecord{ID: mig.ID}
// First ensure that the migration hasn't already been applied.
var alreadyApplied int64
res := db.
Model(&MigrationRecord{}).
Where(&migRecord).
Count(&alreadyApplied)
if res.Error != nil {
slog.Error("already applied check failed!")
return res.Error
}
if alreadyApplied > 0 {
// If record exists in our table, then migration was applied
// already, so skip processing it.
slog.Debug("Migration already applied.", "id", mig.ID)
continue
}
// Timing the migration.
timeBeforeMig := time.Now()
// Apply the migration.
if mig.UNSAFE {
// Unsafe migrations are not ran inside of a transaction
// and are only used when required by sqlite engine.
if err := mig.Up(db); err != nil {
slog.Error("Migration failed!", "id", mig.ID, "error", err)
return err
}
// Record the migration record.
if res := db.Create(&migRecord); res.Error != nil {
slog.Error("Unsafe migration succeeded, but we failed to create the record of it!",
"id", mig.ID, "error", res.Error)
return res.Error
}
} else {
// Migrations go through a transaction wrapper.
err := db.Transaction(func(tx *gorm.DB) error {
if err := mig.Up(tx); err != nil {
// Errored.. rollback any changes made.
return err
}
// Migration succeeded.. record it.
// If the Create succeeds, all will be committed.
return tx.Create(&migRecord).Error
})
if err != nil {
// If any migration fails, we return here.
slog.Error("Migration failed!", "id", mig.ID, "error", err)
return err
}
}
slog.Debug("Migration applied successfully.",
"id", mig.ID,
"duration", time.Since(timeBeforeMig))
}
slog.Info("Done processing all migrations.")
return nil
}
+21
View File
@@ -0,0 +1,21 @@
package migrate
import (
"errors"
"log/slog"
"strings"
"github.com/sbondCo/Watcharr/database/entity"
"gorm.io/gorm"
)
// NOTE: For obvious reasons, once a migration is created and in production,
// it is set in stone, so there should be almost no reason to change an existing
// migration, create a new one instead!
// If it's not obvious, changing an existing migration won't apply for people
// who already have applied it and only apply for people who haven't yet,
// so we are risking splitting the consistency of everyones databases as a
// whole. I can't forsee any circumstance that would require doing so..
var migrations = []Migration{
}