package postgres import ( "context" "embed" "fmt" "path/filepath" "sort" "strconv" "strings" "github.com/jackc/pgx/v5" ) //go:embed migrations/*.sql var migrationsFS embed.FS // RunMigrations runs all embedded migrations in order. func RunMigrations(ctx context.Context, conn *pgx.Conn) error { // Create schema_migrations table if not exists _, err := conn.Exec(ctx, ` CREATE TABLE IF NOT EXISTS schema_migrations ( version BIGINT PRIMARY KEY, applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); `) if err != nil { return fmt.Errorf("failed to create schema_migrations table: %w", err) } entries, err := migrationsFS.ReadDir("migrations") if err != nil { return fmt.Errorf("failed to read migrations directory: %w", err) } var migrationFiles []string for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".up.sql") { migrationFiles = append(migrationFiles, entry.Name()) } } sort.Strings(migrationFiles) if len(migrationFiles) == 0 { return fmt.Errorf("no up migration files found") } for _, filename := range migrationFiles { parts := strings.Split(filename, "_") if len(parts) == 0 { return fmt.Errorf("invalid migration filename: %s", filename) } version, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { return fmt.Errorf("failed to parse migration version from filename %s: %w", filename, err) } // Check if version is already applied var exists bool err = conn.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version = $1)", version).Scan(&exists) if err != nil { return fmt.Errorf("failed to check migration status for version %d: %w", version, err) } if exists { continue } // Read migration content content, err := migrationsFS.ReadFile(filepath.Join("migrations", filename)) if err != nil { return fmt.Errorf("failed to read migration file %s: %w", filename, err) } // Run migration within transaction tx, err := conn.Begin(ctx) if err != nil { return fmt.Errorf("failed to begin transaction for migration %s: %w", filename, err) } defer tx.Rollback(ctx) _, err = tx.Exec(ctx, string(content)) if err != nil { return fmt.Errorf("failed to execute migration %s: %w", filename, err) } _, err = tx.Exec(ctx, "INSERT INTO schema_migrations (version) VALUES ($1)", version) if err != nil { return fmt.Errorf("failed to record migration version %d: %w", version, err) } err = tx.Commit(ctx) if err != nil { return fmt.Errorf("failed to commit transaction for migration %s: %w", filename, err) } } return nil }