mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-12 16:53:44 +00:00
fix(multiplayer): wire a durable RankedProfileProvider
Same discovery pattern as ResultSubmitter/SessionIssuer, one level deeper: api.Service.rankedProfile and .profile both only ever read from an in-memory RankedProfiles map with no durable-store equivalent at all -- not "adapter exists but unwired" this time, there was no adapter. Every real request to GET /v1/profile/ranked or /api/v1/profile always 404'd regardless of a player's actual rating. Add RankedProfileProvider (an interface, not a struct-literal adapter this time) and store.PostgresRankedProfiles reading the ratings table; Service.rankedProfileFor prefers it when set and falls back to the map otherwise, so every existing test/direct Service literal keeps compiling and passing unchanged. A missing ratings row maps to the exact same (zero value, false, nil) the map lookup already produced, preserving existing not-found semantics rather than reinterpreting them. LastSeasonID/SeasonHistory are deliberately left unset -- the ratings table has no season pointer, and reconstructing history needs its own query and display semantics, not bundled in here speculatively. Wired into both cmd/control-plane and cmd/testkit-api. Verified against real PostgreSQL via curl: a fresh identity's ranked profile correctly 404s through the real adapter (same behavior as before, now for a real reason instead of an empty map).
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
||||
)
|
||||
|
||||
const RankedProfileSelectSQL = `SELECT rating, deviation, volatility, ranked_games, updated_at
|
||||
FROM ratings
|
||||
WHERE player_id = $1`
|
||||
|
||||
// PostgresRankedProfiles reads the durable rating row api.Service's
|
||||
// RankedProfileProvider needs. A missing row means "this player has no
|
||||
// ranked profile yet" (never queued ranked, or their identity predates any
|
||||
// result) -- that's a real, expected state, not an error, and is reported
|
||||
// the same way the in-memory RankedProfiles map api.Service still falls
|
||||
// back to already did: (zero value, false, nil).
|
||||
//
|
||||
// LastSeasonID and SeasonHistory are deliberately left at their zero values.
|
||||
// The ratings table has no "current season" column, and reconstructing
|
||||
// season history means a second query against ranked_season_rollovers with
|
||||
// its own display semantics to settle -- a real, separate piece of work,
|
||||
// not bundled into this read path speculatively.
|
||||
type PostgresRankedProfiles struct{ DB *sql.DB }
|
||||
|
||||
func (p PostgresRankedProfiles) Get(ctx context.Context, playerID string) (domain.RankedProfile, bool, error) {
|
||||
if p.DB == nil || playerID == "" {
|
||||
return domain.RankedProfile{}, false, fmt.Errorf("invalid ranked profile lookup")
|
||||
}
|
||||
var profile domain.RankedProfile
|
||||
err := p.DB.QueryRowContext(ctx, RankedProfileSelectSQL, playerID).
|
||||
Scan(&profile.Value, &profile.RD, &profile.Volatility, &profile.RankedGames, &profile.LastRatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return domain.RankedProfile{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.RankedProfile{}, false, err
|
||||
}
|
||||
return profile, true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user