diff --git a/cmd/user/main.go b/cmd/user/main.go index 427ec37..306b738 100644 --- a/cmd/user/main.go +++ b/cmd/user/main.go @@ -14,7 +14,6 @@ import ( "golang.org/x/crypto/bcrypt" "mal/internal" "mal/internal/config" - "mal/internal/database" "mal/internal/db" "mal/internal/observability" errlog "mal/pkg" @@ -229,7 +228,7 @@ func updateAvatars(ctx context.Context, dbConn *sql.DB) { } func runFixes(ctx context.Context, dbConn *sql.DB) { - if err := database.RunMigrationsAndFixes(dbConn); err != nil { + if err := internal.RunMigrationsAndFixes(dbConn); err != nil { observability.Error("cli_run_migrations_and_fixes_failed", "cmd/user", "", nil, err) os.Exit(1) } diff --git a/internal/avatar.go b/internal/avatar.go index 205c5e6..13be768 100644 --- a/internal/avatar.go +++ b/internal/avatar.go @@ -1,8 +1,12 @@ package internal import ( + "database/sql" "net/url" "strings" + + "mal/internal/database" + dbfixes "mal/internal/database/fixes" ) func DefaultAvatarURL(username string) string { @@ -10,3 +14,9 @@ func DefaultAvatarURL(username string) string { params.Set("seed", strings.TrimSpace(username)) return "https://api.dicebear.com/9.x/dylan/svg?" + params.Encode() } + +func RunMigrationsAndFixes(sqlDB *sql.DB) error { + return database.RunMigrationsAndFixes(sqlDB, dbfixes.Dependencies{ + DefaultAvatarURL: DefaultAvatarURL, + }) +} diff --git a/internal/database/database.go b/internal/database/database.go index 4b95a05..347a947 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -6,6 +6,7 @@ import ( "embed" "fmt" "mal/internal/config" + dbfixes "mal/internal/database/fixes" "mal/internal/db" "mal/internal/observability" @@ -21,7 +22,6 @@ var Module = fx.Options( ProvideSQLDB, ProvideQueries, ), - fx.Invoke(RunMigrationsAndFixes), ) func ProvideSQLDB(cfg config.Config) (*sql.DB, error) { @@ -58,11 +58,11 @@ func RunMigrations(sqlDB *sql.DB) error { return nil } -func RunMigrationsAndFixes(sqlDB *sql.DB) error { +func RunMigrationsAndFixes(sqlDB *sql.DB, deps dbfixes.Dependencies) error { if err := RunMigrations(sqlDB); err != nil { return fmt.Errorf("run migrations: %w", err) } - if err := RunDataFixes(sqlDB); err != nil { + if err := RunDataFixes(sqlDB, deps); err != nil { return fmt.Errorf("run data fixes: %w", err) } return nil diff --git a/internal/database/fixes.go b/internal/database/fixes.go index f261b2e..13e0ab0 100644 --- a/internal/database/fixes.go +++ b/internal/database/fixes.go @@ -11,7 +11,7 @@ import ( errlog "mal/pkg" ) -func RunDataFixes(sqlDB *sql.DB) error { +func RunDataFixes(sqlDB *sql.DB, deps dbfixes.Dependencies) error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() @@ -43,7 +43,7 @@ func RunDataFixes(sqlDB *sql.DB) error { "id": fix.ID, }, ) - if err := fix.Apply(ctx, sqlDB); err != nil { + if err := fix.Apply(ctx, sqlDB, deps); err != nil { return fmt.Errorf("data fix %s failed: %w", fix.ID, err) } if err := markFixApplied(ctx, sqlDB, fix.ID); err != nil { diff --git a/internal/database/fixes/20260526_episode_availability_backfill_next_refresh_at.go b/internal/database/fixes/20260526_episode_availability_backfill_next_refresh_at.go index 79aca34..8a39d77 100644 --- a/internal/database/fixes/20260526_episode_availability_backfill_next_refresh_at.go +++ b/internal/database/fixes/20260526_episode_availability_backfill_next_refresh_at.go @@ -9,7 +9,7 @@ import ( func init() { Register(Fix{ ID: "20260526_episode_availability_backfill_next_refresh_at", - Apply: func(ctx context.Context, sqlDB *sql.DB) error { + Apply: func(ctx context.Context, sqlDB *sql.DB, _ Dependencies) error { // Old caches could have next_refresh_at NULL (especially for airing shows with missing broadcast metadata), // which can result in "never refresh again" behavior on the server. _, err := sqlDB.ExecContext(ctx, ` diff --git a/internal/database/fixes/20260528_backfill_avatar_url.go b/internal/database/fixes/20260528_backfill_avatar_url.go index 260bc83..4759e7f 100644 --- a/internal/database/fixes/20260528_backfill_avatar_url.go +++ b/internal/database/fixes/20260528_backfill_avatar_url.go @@ -4,14 +4,17 @@ import ( "context" "database/sql" "fmt" - "mal/internal" errlog "mal/pkg" ) func init() { Register(Fix{ ID: "20260528_backfill_avatar_url", - Apply: func(ctx context.Context, sqlDB *sql.DB) error { + Apply: func(ctx context.Context, sqlDB *sql.DB, deps Dependencies) error { + if deps.DefaultAvatarURL == nil { + return fmt.Errorf("default avatar URL dependency is required") + } + rows, err := sqlDB.QueryContext(ctx, `SELECT id, username FROM user WHERE avatar_url = ''`) if err != nil { return fmt.Errorf("query users missing avatar_url: %w", err) @@ -35,7 +38,7 @@ func init() { } for _, u := range toUpdate { - avatarURL := internal.DefaultAvatarURL(u.username) + avatarURL := deps.DefaultAvatarURL(u.username) if _, err := sqlDB.ExecContext(ctx, `UPDATE user SET avatar_url = ? WHERE id = ?`, avatarURL, u.id); err != nil { return fmt.Errorf("update avatar_url for user %s: %w", u.id, err) } diff --git a/internal/database/fixes/20260608_backfill_anime_duration_seconds.go b/internal/database/fixes/20260608_backfill_anime_duration_seconds.go index c3dd285..88ab1ee 100644 --- a/internal/database/fixes/20260608_backfill_anime_duration_seconds.go +++ b/internal/database/fixes/20260608_backfill_anime_duration_seconds.go @@ -18,8 +18,10 @@ type animeDurationRow struct { func init() { Register(Fix{ - ID: "20260608_backfill_anime_duration_seconds", - Apply: applyAnimeDurationSecondsBackfill, + ID: "20260608_backfill_anime_duration_seconds", + Apply: func(ctx context.Context, sqlDB *sql.DB, _ Dependencies) error { + return applyAnimeDurationSecondsBackfill(ctx, sqlDB) + }, }) } diff --git a/internal/database/fixes/registry.go b/internal/database/fixes/registry.go index ceeca05..b0f0d38 100644 --- a/internal/database/fixes/registry.go +++ b/internal/database/fixes/registry.go @@ -9,7 +9,11 @@ import ( type Fix struct { ID string - Apply func(ctx context.Context, sqlDB *sql.DB) error + Apply func(ctx context.Context, sqlDB *sql.DB, deps Dependencies) error +} + +type Dependencies struct { + DefaultAvatarURL func(username string) string } var registered []Fix