Files
CosmicClash/server/store/season_sql.go
T
2026-08-31 21:43:07 +01:00

65 lines
2.1 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/cosmic-clash/cosmic-clash/server/domain"
)
const SeasonRatingLockSQL = `SELECT player_id, rating, deviation, volatility, ranked_games, revision
FROM ratings
WHERE player_id = $1
FOR UPDATE`
const SeasonRolloverInsertSQL = `INSERT INTO ranked_season_rollovers
(player_id, season_id, rating, deviation, volatility, ranked_games, rolled_over_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (player_id, season_id) DO NOTHING`
const SeasonRatingUpdateSQL = `UPDATE ratings
SET rating = $2, deviation = $3, volatility = $4, revision = revision + 1, updated_at = $5
WHERE player_id = $1`
// ApplyRankedSeasonRollover persists the domain rollover exactly once. The
// marker insert and rating update share one SERIALIZABLE transaction, so a
// retry after a worker failure cannot apply compression twice or leave a
// marker without its corresponding rating snapshot.
func ApplyRankedSeasonRollover(ctx context.Context, db *sql.DB, playerID, seasonID string, profile domain.RankedProfile, now time.Time) (domain.RankedProfile, bool, error) {
if playerID == "" {
return domain.RankedProfile{}, false, fmt.Errorf("player ID is required")
}
updated, _, err := domain.ApplySeasonRollover(profile, seasonID)
if err != nil {
return domain.RankedProfile{}, false, err
}
applied := false
err = RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
if _, err := tx.ExecContext(ctx, SeasonRatingLockSQL, playerID); err != nil {
return err
}
result, err := tx.ExecContext(ctx, SeasonRolloverInsertSQL, playerID, seasonID, updated.Value, updated.RD, updated.Volatility, updated.RankedGames, now)
if err != nil {
return err
}
changed, err := result.RowsAffected()
if err != nil {
return err
}
if changed == 0 {
return nil
}
if _, err := tx.ExecContext(ctx, SeasonRatingUpdateSQL, playerID, updated.Value, updated.RD, updated.Volatility, now); err != nil {
return err
}
applied = true
return nil
})
if err != nil {
return domain.RankedProfile{}, false, err
}
return updated, applied, nil
}