package store import ( "context" "database/sql" "fmt" "github.com/cosmic-clash/cosmic-clash/server/domain" ) // TierBandSelectSQL returns bands in evaluation order. domain.NewTierPolicy // requires strictly ascending thresholds, so ordering here is part of the // contract rather than a convenience. const TierBandSelectSQL = `SELECT tier, min_rating FROM tier_bands ORDER BY min_rating` // validTierBandTiers is the closed set a durable band may name. PROVISIONAL is // deliberately absent: it is derived from a player's ranked game count, not // from their rating, so a band claiming it would be unreachable at best and // would mask a real tier at worst. var validTierBandTiers = map[domain.RankTier]struct{}{ domain.RankTierBronze: {}, domain.RankTierSilver: {}, domain.RankTierGold: {}, domain.RankTierPlatinum: {}, domain.RankTierDiamond: {}, } // LoadTierPolicy reads the durable tier bands, falling back to the compiled // launch policy when none are configured. // // Tier thresholds used to be compiled into every API binary, so retuning a // band meant building and rolling a new image -- least attractive exactly when // it is most needed, as the rating distribution settles after launch. The // fallback means an empty table is a supported state: an operator can truncate // it to return to known-good defaults, and a fresh database works before the // seed migration has been reviewed. // // Bands are read once at startup, matching how every other operational input // to this binary is supplied. Changing them takes a rolling restart, not a // rebuild, which is the actual gain here. func LoadTierPolicy(ctx context.Context, db *sql.DB) (domain.TierPolicy, error) { if db == nil { return domain.TierPolicy{}, fmt.Errorf("invalid tier policy database") } rows, err := db.QueryContext(ctx, TierBandSelectSQL) if err != nil { return domain.TierPolicy{}, err } defer rows.Close() var bands []domain.TierBand for rows.Next() { var tier string var minRating float64 if err := rows.Scan(&tier, &minRating); err != nil { return domain.TierPolicy{}, err } if _, known := validTierBandTiers[domain.RankTier(tier)]; !known { return domain.TierPolicy{}, fmt.Errorf("tier_bands contains unknown tier %q", tier) } bands = append(bands, domain.TierBand{Tier: domain.RankTier(tier), MinRating: minRating}) } if err := rows.Err(); err != nil { return domain.TierPolicy{}, err } if len(bands) == 0 { return domain.DefaultTierPolicy(), nil } // Validated rather than trusted: a malformed durable policy must fail // loudly at startup, not silently mis-tier every player. NewTierPolicy // enforces a band at or below zero and strictly ascending finite // thresholds. policy, err := domain.NewTierPolicy(bands) if err != nil { return domain.TierPolicy{}, fmt.Errorf("durable tier policy is invalid: %w", err) } return policy, nil }