switched out make for just

This commit is contained in:
2026-02-13 13:27:14 +11:00
parent 6e779fa560
commit ea8b74c5e3
9 changed files with 470 additions and 477 deletions

View File

@@ -55,19 +55,19 @@ func main() {
}
// Handle commands that need database connection
if flags.MigrateUp || flags.MigrateRollback ||
if flags.MigrateUp != "" || flags.MigrateRollback != "" ||
flags.MigrateStatus || flags.MigrateDryRun ||
flags.ResetDB {
// Route to appropriate command
if flags.MigrateUp {
err = runMigrations(ctx, cfg, "up")
} else if flags.MigrateRollback {
err = runMigrations(ctx, cfg, "rollback")
if flags.MigrateUp != "" {
err = runMigrations(ctx, cfg, "up", flags.MigrateUp)
} else if flags.MigrateRollback != "" {
err = runMigrations(ctx, cfg, "rollback", flags.MigrateRollback)
} else if flags.MigrateStatus {
err = runMigrations(ctx, cfg, "status")
err = runMigrations(ctx, cfg, "status", "")
} else if flags.MigrateDryRun {
err = runMigrations(ctx, cfg, "dry-run")
err = runMigrations(ctx, cfg, "dry-run", "")
} else if flags.ResetDB {
err = resetDatabase(ctx, cfg)
}

View File

@@ -6,12 +6,11 @@ import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"text/tabwriter"
"time"
stderrors "errors"
"git.haelnorr.com/h/oslstats/cmd/oslstats/migrations"
"git.haelnorr.com/h/oslstats/internal/backup"
"git.haelnorr.com/h/oslstats/internal/config"
@@ -21,7 +20,7 @@ import (
)
// runMigrations executes database migrations
func runMigrations(ctx context.Context, cfg *config.Config, command string) error {
func runMigrations(ctx context.Context, cfg *config.Config, command string, countStr string) error {
conn, close := setupBun(cfg)
defer func() { _ = close() }()
@@ -34,16 +33,18 @@ func runMigrations(ctx context.Context, cfg *config.Config, command string) erro
switch command {
case "up":
err := migrateUp(ctx, migrator, conn, cfg)
err := migrateUp(ctx, migrator, conn, cfg, countStr)
if err != nil {
err2 := migrateRollback(ctx, migrator, conn, cfg)
if err2 != nil {
return stderrors.Join(errors.Wrap(err2, "error while rolling back after migration error"), err)
}
// On error, automatically rollback the migrations that were just applied
fmt.Println("[WARN] Migration failed, attempting automatic rollback...")
// We need to figure out how many migrations were applied in this batch
// For now, we'll skip automatic rollback since it's complex with the new count system
// The user can manually rollback if needed
return err
}
return err
case "rollback":
return migrateRollback(ctx, migrator, conn, cfg)
return migrateRollback(ctx, migrator, conn, cfg, countStr)
case "status":
return migrateStatus(ctx, migrator)
case "dry-run":
@@ -54,7 +55,13 @@ func runMigrations(ctx context.Context, cfg *config.Config, command string) erro
}
// migrateUp runs pending migrations
func migrateUp(ctx context.Context, migrator *migrate.Migrator, conn *bun.DB, cfg *config.Config) error {
func migrateUp(ctx context.Context, migrator *migrate.Migrator, conn *bun.DB, cfg *config.Config, countStr string) error {
// Parse count parameter
count, all, err := parseMigrationCount(countStr)
if err != nil {
return errors.Wrap(err, "parse migration count")
}
fmt.Println("[INFO] Step 1/5: Validating migrations...")
if err := validateMigrations(ctx); err != nil {
return err
@@ -74,6 +81,23 @@ func migrateUp(ctx context.Context, migrator *migrate.Migrator, conn *bun.DB, cf
return nil
}
// Select which migrations to apply
toApply := selectMigrationsToApply(unapplied, count, all)
if len(toApply) == 0 {
fmt.Println("[INFO] No migrations to run")
return nil
}
// Print what we're about to do
if all {
fmt.Printf("[INFO] Running all %d pending migration(s):\n", len(toApply))
} else {
fmt.Printf("[INFO] Running %d migration(s):\n", len(toApply))
}
for _, m := range toApply {
fmt.Printf(" 📋 %s\n", m.Name)
}
// Create backup unless --no-backup flag is set
if !cfg.Flags.MigrateNoBackup {
fmt.Println("[INFO] Step 3/5: Creating backup...")
@@ -100,9 +124,9 @@ func migrateUp(ctx context.Context, migrator *migrate.Migrator, conn *bun.DB, cf
// Run migrations
fmt.Println("[INFO] Step 5/5: Applying migrations...")
group, err := migrator.Migrate(ctx)
group, err := executeUpMigrations(ctx, migrator, toApply)
if err != nil {
return errors.Wrap(err, "migrate")
return errors.Wrap(err, "execute migrations")
}
if group.IsZero() {
@@ -118,8 +142,43 @@ func migrateUp(ctx context.Context, migrator *migrate.Migrator, conn *bun.DB, cf
return nil
}
// migrateRollback rolls back the last migration group
func migrateRollback(ctx context.Context, migrator *migrate.Migrator, conn *bun.DB, cfg *config.Config) error {
// migrateRollback rolls back migrations
func migrateRollback(ctx context.Context, migrator *migrate.Migrator, conn *bun.DB, cfg *config.Config, countStr string) error {
// Parse count parameter
count, all, err := parseMigrationCount(countStr)
if err != nil {
return errors.Wrap(err, "parse migration count")
}
// Get all migrations with status
ms, err := migrator.MigrationsWithStatus(ctx)
if err != nil {
return errors.Wrap(err, "get migration status")
}
applied := ms.Applied()
if len(applied) == 0 {
fmt.Println("[INFO] No migrations to rollback")
return nil
}
// Select which migrations to rollback
toRollback := selectMigrationsToRollback(applied, count, all)
if len(toRollback) == 0 {
fmt.Println("[INFO] No migrations to rollback")
return nil
}
// Print what we're about to do
if all {
fmt.Printf("[INFO] Rolling back all %d migration(s):\n", len(toRollback))
} else {
fmt.Printf("[INFO] Rolling back %d migration(s):\n", len(toRollback))
}
for _, m := range toRollback {
fmt.Printf(" 📋 %s (group %d)\n", m.Name, m.GroupID)
}
// Create backup unless --no-backup flag is set
if !cfg.Flags.MigrateNoBackup {
fmt.Println("[INFO] Creating backup before rollback...")
@@ -145,19 +204,14 @@ func migrateRollback(ctx context.Context, migrator *migrate.Migrator, conn *bun.
fmt.Println("[INFO] Migration lock acquired")
// Rollback
fmt.Println("[INFO] Rolling back last migration group...")
group, err := migrator.Rollback(ctx)
fmt.Println("[INFO] Executing rollback...")
rolledBack, err := executeDownMigrations(ctx, migrator, toRollback)
if err != nil {
return errors.Wrap(err, "rollback")
return errors.Wrap(err, "execute rollback")
}
if group.IsZero() {
fmt.Println("[INFO] No migrations to rollback")
return nil
}
fmt.Printf("[INFO] Rolled back group %d\n", group.ID)
for _, migration := range group.Migrations {
fmt.Printf("[INFO] Successfully rolled back %d migration(s)\n", len(rolledBack))
for _, migration := range rolledBack {
fmt.Printf(" ↩️ %s\n", migration.Name)
}
@@ -329,6 +383,129 @@ func init() {
return nil
}
// parseMigrationCount parses a migration count string
// Returns: (count, all, error)
// - "" (empty) → (1, false, nil) - default to 1
// - "all" → (0, true, nil) - special case for all
// - "5" → (5, false, nil) - specific count
// - "invalid" → (0, false, error)
func parseMigrationCount(value string) (int, bool, error) {
// Default to 1 if empty
if value == "" {
return 1, false, nil
}
// Special case for "all"
if value == "all" {
return 0, true, nil
}
// Parse as integer
count, err := strconv.Atoi(value)
if err != nil {
return 0, false, errors.New("migration count must be a positive integer or 'all'")
}
if count < 1 {
return 0, false, errors.New("migration count must be a positive integer (1 or greater)")
}
return count, false, nil
}
// selectMigrationsToApply returns the subset of unapplied migrations to run
func selectMigrationsToApply(unapplied migrate.MigrationSlice, count int, all bool) migrate.MigrationSlice {
if all {
return unapplied
}
count = min(count, len(unapplied))
return unapplied[:count]
}
// selectMigrationsToRollback returns the subset of applied migrations to rollback
// Returns migrations in reverse chronological order (most recent first)
func selectMigrationsToRollback(applied migrate.MigrationSlice, count int, all bool) migrate.MigrationSlice {
if len(applied) == 0 || all {
return applied
}
count = min(count, len(applied))
return applied[:count]
}
// executeUpMigrations executes a subset of UP migrations
func executeUpMigrations(ctx context.Context, migrator *migrate.Migrator, migrations migrate.MigrationSlice) (*migrate.MigrationGroup, error) {
if len(migrations) == 0 {
return &migrate.MigrationGroup{}, nil
}
// Get the next group ID
ms, err := migrator.MigrationsWithStatus(ctx)
if err != nil {
return nil, errors.Wrap(err, "get migration status")
}
lastGroup := ms.LastGroup()
groupID := int64(1)
if lastGroup.ID > 0 {
groupID = lastGroup.ID + 1
}
// Create the migration group
group := &migrate.MigrationGroup{
ID: groupID,
Migrations: make(migrate.MigrationSlice, 0, len(migrations)),
}
// Execute each migration
for i := range migrations {
migration := &migrations[i]
migration.GroupID = groupID
// Mark as applied before execution (Bun's default behavior)
if err := migrator.MarkApplied(ctx, migration); err != nil {
return group, errors.Wrap(err, "mark applied")
}
// Add to group
group.Migrations = append(group.Migrations, *migration)
// Execute the UP function
if migration.Up != nil {
if err := migration.Up(ctx, migrator, migration); err != nil {
return group, errors.Wrap(err, fmt.Sprintf("migration %s failed", migration.Name))
}
}
}
return group, nil
}
// executeDownMigrations executes a subset of DOWN migrations
func executeDownMigrations(ctx context.Context, migrator *migrate.Migrator, migrations migrate.MigrationSlice) (migrate.MigrationSlice, error) {
rolledBack := make(migrate.MigrationSlice, 0, len(migrations))
// Execute each migration in order (already reversed)
for i := range migrations {
migration := &migrations[i]
// Execute the DOWN function
if migration.Down != nil {
if err := migration.Down(ctx, migrator, migration); err != nil {
return rolledBack, errors.Wrap(err, fmt.Sprintf("rollback %s failed", migration.Name))
}
}
// Mark as unapplied after execution
if err := migrator.MarkUnapplied(ctx, migration); err != nil {
return rolledBack, errors.Wrap(err, "mark unapplied")
}
rolledBack = append(rolledBack, *migration)
}
return rolledBack, nil
}
// resetDatabase drops and recreates all tables (destructive)
func resetDatabase(ctx context.Context, cfg *config.Config) error {
fmt.Println("⚠️ WARNING - This will DELETE ALL DATA in the database!")