Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 140 additions & 19 deletions db/migrations.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
package db

import (
"fmt"
"time"

"gorm.io/gorm"
)

// Migration represents a single database migration
type Migration struct {
ID int
Name string
Up func(*gorm.DB) error
}

// allMigrations is the ordered list of all migrations
// Each migration has a unique ID and is applied in order
var allMigrations = []Migration{
{
ID: 1,
Name: "0001_rename_last_commit_to_local_commit",
Up: migration0001RenameLastCommitToLocalCommit,
},
{
ID: 2,
Name: "0002_rename_watcher_enabled_to_auto_deploy_enabled",
Up: migration0002RenameWatcherEnabledToAutoDeployEnabled,
},
{
ID: 3,
Name: "0003_cleanup_old_migration_records",
Up: migration0003CleanupOldMigrationRecords,
},
}

// AllModels returns all the models that need to be migrated
// This is the single source of truth for database migrations
func AllModels() []any {
Expand All @@ -18,16 +46,13 @@ func AllModels() []any {

// AutoMigrateAll runs auto-migration for all application models
func AutoMigrateAll(db *gorm.DB) error {
// Run manual migrations BEFORE AutoMigrate to avoid conflicts
// (e.g., AutoMigrate would create local_commit if we haven't renamed last_commit yet)

// First, ensure migrations table exists
if err := db.AutoMigrate(&MigrationModel{}); err != nil {
return err
}

// Run manual migrations
if err := migrateLastCommitToLocalCommit(db); err != nil {
// Run all manual migrations in order
if err := RunMigrations(db, len(allMigrations)); err != nil {
return err
}

Expand All @@ -39,6 +64,41 @@ func AutoMigrateAll(db *gorm.DB) error {
return nil
}

// RunMigrations runs all migrations up to and including the specified ID
// If targetID is 0 or negative, all migrations are run
func RunMigrations(db *gorm.DB, targetID int) error {
if targetID <= 0 {
targetID = len(allMigrations)
}

for _, migration := range allMigrations {
if migration.ID > targetID {
break
}

// Check if migration has already been applied
applied, err := migrationApplied(db, migration.Name)
if err != nil {
return fmt.Errorf("failed to check migration %s: %w", migration.Name, err)
}
if applied {
continue
}

// Run the migration
if err := migration.Up(db); err != nil {
return fmt.Errorf("failed to apply migration %s: %w", migration.Name, err)
}

// Record that migration was applied
if err := recordMigration(db, migration.Name); err != nil {
return fmt.Errorf("failed to record migration %s: %w", migration.Name, err)
}
}

return nil
}

// migrationApplied checks if a migration has already been applied
func migrationApplied(db *gorm.DB, name string) (bool, error) {
var count int64
Expand All @@ -58,30 +118,91 @@ func recordMigration(db *gorm.DB, name string) error {
return db.Create(&migration).Error
}

// migrateLastCommitToLocalCommit handles the rename from last_commit to local_commit
func migrateLastCommitToLocalCommit(db *gorm.DB) error {
migrationName := "rename_last_commit_to_local_commit"
// Schema snapshots represent the database state at each migration point
// These are used by tests to create databases at specific migration versions

// Check if migration has already been applied
applied, err := migrationApplied(db, migrationName)
if err != nil {
// CreateSchemaAtMigration creates the database schema as it existed at a specific migration version
// migrationID 0 = initial schema before any migrations
// migrationID N = schema after applying migrations 1 through N
func CreateSchemaAtMigration(db *gorm.DB, migrationID int) error {
// First ensure migrations table exists
if err := db.AutoMigrate(&MigrationModel{}); err != nil {
return err
}

// Create initial schema (before any migrations)
if err := createInitialSchema(db); err != nil {
return err
}
if applied {
return nil // Migration already done

// Apply migrations up to the target
if migrationID > 0 {
return RunMigrations(db, migrationID)
}

return nil
}

// createInitialSchema creates the schema as it existed before any migrations (migration 0)
func createInitialSchema(db *gorm.DB) error {
// Create projects table with original column names
return db.Exec(`
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
git_url TEXT NOT NULL,
git_branch TEXT NOT NULL,
git_auth_type TEXT,
git_auth_credentials TEXT,
working_dir TEXT NOT NULL,
compose_files TEXT NOT NULL,
compose_override TEXT,
variables TEXT NOT NULL,
status TEXT NOT NULL,
last_commit TEXT,
remote_commit TEXT,
watcher_enabled INTEGER NOT NULL,
created_at DATETIME,
updated_at DATETIME
)
`).Error
}

// migration0001RenameLastCommitToLocalCommit handles the rename from last_commit to local_commit
func migration0001RenameLastCommitToLocalCommit(db *gorm.DB) error {
// Check if old column exists - if not, this is a fresh database and nothing to migrate
if !db.Migrator().HasColumn(&ProjectModel{}, "last_commit") {
// Record migration as applied anyway to prevent future attempts
return recordMigration(db, migrationName)
return nil // Nothing to migrate
}

// Rename column directly (requires SQLite 3.25.0+)
if err := db.Exec("ALTER TABLE projects RENAME COLUMN last_commit TO local_commit").Error; err != nil {
return err
return db.Exec("ALTER TABLE projects RENAME COLUMN last_commit TO local_commit").Error
}

// migration0002RenameWatcherEnabledToAutoDeployEnabled handles the rename from watcher_enabled to auto_deploy_enabled
func migration0002RenameWatcherEnabledToAutoDeployEnabled(db *gorm.DB) error {
// Check if old column exists - if not, this is a fresh database and nothing to migrate
if !db.Migrator().HasColumn(&ProjectModel{}, "watcher_enabled") {
return nil // Nothing to migrate
}

// Record that migration was applied
return recordMigration(db, migrationName)
// Rename column directly (requires SQLite 3.25.0+)
return db.Exec("ALTER TABLE projects RENAME COLUMN watcher_enabled TO auto_deploy_enabled").Error
}

// migration0003CleanupOldMigrationRecords removes duplicate migration records from the old naming scheme
func migration0003CleanupOldMigrationRecords(db *gorm.DB) error {
// Delete old migration records that used the old naming convention (without number prefix)
oldMigrationNames := []string{
"rename_last_commit_to_local_commit",
"rename_watcher_enabled_to_auto_deploy_enabled",
}

for _, name := range oldMigrationNames {
if err := db.Where("name = ?", name).Delete(&MigrationModel{}).Error; err != nil {
return err
}
}

return nil
}
Loading