Files
CosmicClash/server/store/maintenance_sql.go
T
2026-09-01 09:46:57 +01:00

68 lines
2.1 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/cosmic-clash/cosmic-clash/server/domain"
)
const DueSeasonRolloversSQL = `SELECT s.season_id, r.player_id, r.rating, r.deviation,
r.volatility, r.ranked_games
FROM seasons s
CROSS JOIN ratings r
LEFT JOIN ranked_season_rollovers rr ON rr.season_id = s.season_id AND rr.player_id = r.player_id
WHERE s.playlist = 'ranked' AND s.ends_at <= $1 AND rr.player_id IS NULL
ORDER BY s.ends_at, s.season_id, r.player_id
LIMIT $2`
const MarkSeasonRolledOverSQL = `UPDATE seasons SET rolled_over_at = $2
WHERE season_id = $1 AND rolled_over_at IS NULL
AND NOT EXISTS (SELECT 1 FROM ratings r
LEFT JOIN ranked_season_rollovers rr ON rr.season_id = $1 AND rr.player_id = r.player_id
WHERE rr.player_id IS NULL)`
type dueSeasonRollover struct {
seasonID string
playerID string
profile domain.RankedProfile
}
// RolloverDueSeasons processes a bounded batch. Each player update is its own
// exactly-once SERIALIZABLE transaction, so a worker crash can safely resume.
func RolloverDueSeasons(ctx context.Context, db *sql.DB, now time.Time, limit int) (int, error) {
if db == nil || now.IsZero() || limit < 1 || limit > 1000 {
return 0, fmt.Errorf("invalid season maintenance arguments")
}
rows, err := db.QueryContext(ctx, DueSeasonRolloversSQL, now, limit)
if err != nil {
return 0, err
}
defer rows.Close()
var due []dueSeasonRollover
for rows.Next() {
var item dueSeasonRollover
if err := rows.Scan(&item.seasonID, &item.playerID, &item.profile.Value, &item.profile.RD, &item.profile.Volatility, &item.profile.RankedGames); err != nil {
return 0, err
}
due = append(due, item)
}
if err := rows.Err(); err != nil {
return 0, err
}
count := 0
for _, item := range due {
if _, applied, err := ApplyRankedSeasonRollover(ctx, db, item.playerID, item.seasonID, item.profile, now); err != nil {
return count, err
} else if applied {
count++
}
if _, err := db.ExecContext(ctx, MarkSeasonRolledOverSQL, item.seasonID, now); err != nil {
return count, err
}
}
return count, nil
}