package database import ( "fmt" "io/fs" "slices" "sort" "strconv" "strings" "gorm.io/gorm" ) // Atomically run migrations found from the given filesystem (with the format `*.up.sql`). // Skips already applied migrations, but fails on malformed or iisconfigured entries. Notably atomic // execution must be utilized to automatically roll back any partially applied migrations. func RunMigrations(db *gorm.DB, migrationsFS fs.FS) error { return db.Transaction(func(tx *gorm.DB) error { if err := tx.Exec(` CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY ) `).Error; err != nil { return err } // Query already applied migrations to prevent duplicate execution var applied []int if err := tx.Table("schema_migrations").Pluck("version", &applied).Error; err != nil { return err } migrationFiles, err := fs.Glob(migrationsFS, "*.up.sql") if err != nil { return err } for _, f := range sortMigrations(migrationFiles) { version, err := strconv.Atoi(strings.Split(f, "_")[0]) if err != nil { return fmt.Errorf("invalid migration filename: %s", f) } if slices.Contains(applied, version) { continue } sql, err := fs.ReadFile(migrationsFS, f) if err != nil { return err } if err := tx.Exec(string(sql)).Error; err != nil { return err } if err := tx.Exec(` INSERT INTO schema_migrations (version) VALUES (?) `, version).Error; err != nil { return err } } return nil }) } // Sort the given migration files to ascending order based on the filename integer prefix to then // be executed sequentially. func sortMigrations(files []string) []string { sort.Slice(files, func(i, j int) bool { v1, _ := strconv.Atoi(strings.Split(files[i], "_")[0]) v2, _ := strconv.Atoi(strings.Split(files[j], "_")[0]) return v1 < v2 }) return files }