Files
oslstats/internal/db/player.go

351 lines
11 KiB
Go

package db
import (
"context"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
type Player struct {
bun.BaseModel `bun:"table:players,alias:p"`
ID int `bun:"id,pk,autoincrement" json:"id"`
SlapID *uint32 `bun:"slap_id,unique" json:"slap_id"`
DiscordID string `bun:"discord_id,unique,notnull" json:"discord_id"`
UserID *int `bun:"user_id,unique" json:"user_id"`
Name string `bun:"name,notnull" json:"name"`
User *User `bun:"rel:belongs-to,join:user_id=id" json:"-"`
}
func (p *Player) DisplayName() string {
if p.User != nil {
return p.User.Username
}
return p.Name
}
// NewPlayer creates a new player in the database. If there is an existing user with the same
// discordID, it will automatically link that user to the player
func NewPlayer(ctx context.Context, tx bun.Tx, name, discordID string, audit *AuditMeta) (*Player, error) {
player := &Player{DiscordID: discordID, Name: name}
user, err := GetUserByDiscordID(ctx, tx, discordID)
if err != nil && !IsBadRequest(err) {
return nil, errors.Wrap(err, "GetUserByDiscordID")
}
if user != nil {
player.UserID = &user.ID
player.Name = user.Username
}
err = Insert(tx, player).
WithAudit(audit, nil).Exec(ctx)
if err != nil {
return nil, errors.Wrap(err, "Insert")
}
return player, nil
}
// ConnectPlayer links the user to an existing player, or creates a new player to link if not found
// Populates User.Player on success
func (u *User) ConnectPlayer(ctx context.Context, tx bun.Tx, audit *AuditMeta) error {
player, err := GetByField[Player](tx, "p.discord_id", u.DiscordID).
Relation("User").Get(ctx)
if err != nil {
if !IsBadRequest(err) {
// Unexpected error occured
return errors.Wrap(err, "GetByField")
}
// Player doesn't exist, create a new one
player, err = NewPlayer(ctx, tx, u.Username, u.DiscordID, audit)
if err != nil {
return errors.Wrap(err, "NewPlayer")
}
// New player should automatically get linked to the user
u.Player = player
return nil
}
// Player was found
if player.UserID != nil {
if player.UserID == &u.ID {
return nil
}
return errors.New("player with that discord_id already linked to a user")
}
player.UserID = &u.ID
err = UpdateByID(tx, player.ID, player).Column("user_id").Exec(ctx)
if err != nil {
return errors.Wrap(err, "UpdateByID")
}
u.Player = player
return nil
}
func GetPlayer(ctx context.Context, tx bun.Tx, playerID int) (*Player, error) {
return GetByID[Player](tx, playerID).Relation("User").Get(ctx)
}
func UpdatePlayerSlapID(ctx context.Context, tx bun.Tx, playerID int, slapID uint32, audit *AuditMeta) error {
player, err := GetPlayer(ctx, tx, playerID)
if err != nil {
return errors.Wrap(err, "GetPlayer")
}
player.SlapID = &slapID
err = UpdateByID(tx, player.ID, player).Column("slap_id").
WithAudit(audit, nil).Exec(ctx)
if err != nil {
return errors.Wrap(err, "UpdateByID")
}
return nil
}
// PlayerAllTimeStats holds aggregated all-time stats for a single player
type PlayerAllTimeStats struct {
GamesPlayed int `bun:"games_played"`
PeriodsPlayed int `bun:"total_periods_played"`
Goals int `bun:"total_goals"`
Assists int `bun:"total_assists"`
Saves int `bun:"total_saves"`
Shots int `bun:"total_shots"`
Blocks int `bun:"total_blocks"`
Passes int `bun:"total_passes"`
}
// GetPlayerAllTimeStats returns aggregated all-time stats for a player
// across all finalized fixture results (period 3 totals).
func GetPlayerAllTimeStats(ctx context.Context, tx bun.Tx, playerID int) (*PlayerAllTimeStats, error) {
if playerID == 0 {
return nil, errors.New("playerID not provided")
}
stats := new(PlayerAllTimeStats)
err := tx.NewRaw(`
SELECT
COUNT(DISTINCT frps.fixture_result_id) AS games_played,
COALESCE(SUM(frps.periods_played), 0) AS total_periods_played,
COALESCE(SUM(frps.goals), 0) AS total_goals,
COALESCE(SUM(frps.assists), 0) AS total_assists,
COALESCE(SUM(frps.saves), 0) AS total_saves,
COALESCE(SUM(frps.shots), 0) AS total_shots,
COALESCE(SUM(frps.blocks), 0) AS total_blocks,
COALESCE(SUM(frps.passes), 0) AS total_passes
FROM fixture_result_player_stats frps
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
WHERE fr.finalized = true
AND frps.player_id = ?
AND frps.period_num = 3
`, playerID).Scan(ctx, stats)
if err != nil {
return nil, errors.Wrap(err, "tx.NewRaw")
}
return stats, nil
}
// GetPlayerStatsBySeason returns aggregated stats for a player filtered by season.
func GetPlayerStatsBySeason(ctx context.Context, tx bun.Tx, playerID, seasonID int) (*PlayerAllTimeStats, error) {
if playerID == 0 {
return nil, errors.New("playerID not provided")
}
if seasonID == 0 {
return nil, errors.New("seasonID not provided")
}
stats := new(PlayerAllTimeStats)
err := tx.NewRaw(`
SELECT
COUNT(DISTINCT frps.fixture_result_id) AS games_played,
COALESCE(SUM(frps.periods_played), 0) AS total_periods_played,
COALESCE(SUM(frps.goals), 0) AS total_goals,
COALESCE(SUM(frps.assists), 0) AS total_assists,
COALESCE(SUM(frps.saves), 0) AS total_saves,
COALESCE(SUM(frps.shots), 0) AS total_shots,
COALESCE(SUM(frps.blocks), 0) AS total_blocks,
COALESCE(SUM(frps.passes), 0) AS total_passes
FROM fixture_result_player_stats frps
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
JOIN fixtures f ON f.id = fr.fixture_id
WHERE fr.finalized = true
AND frps.player_id = ?
AND frps.period_num = 3
AND f.season_id = ?
`, playerID, seasonID).Scan(ctx, stats)
if err != nil {
return nil, errors.Wrap(err, "tx.NewRaw")
}
return stats, nil
}
// GetPlayerStatsByTeam returns aggregated stats for a player filtered by team.
func GetPlayerStatsByTeam(ctx context.Context, tx bun.Tx, playerID, teamID int) (*PlayerAllTimeStats, error) {
if playerID == 0 {
return nil, errors.New("playerID not provided")
}
if teamID == 0 {
return nil, errors.New("teamID not provided")
}
stats := new(PlayerAllTimeStats)
err := tx.NewRaw(`
SELECT
COUNT(DISTINCT frps.fixture_result_id) AS games_played,
COALESCE(SUM(frps.periods_played), 0) AS total_periods_played,
COALESCE(SUM(frps.goals), 0) AS total_goals,
COALESCE(SUM(frps.assists), 0) AS total_assists,
COALESCE(SUM(frps.saves), 0) AS total_saves,
COALESCE(SUM(frps.shots), 0) AS total_shots,
COALESCE(SUM(frps.blocks), 0) AS total_blocks,
COALESCE(SUM(frps.passes), 0) AS total_passes
FROM fixture_result_player_stats frps
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
WHERE fr.finalized = true
AND frps.player_id = ?
AND frps.period_num = 3
AND frps.team_id = ?
`, playerID, teamID).Scan(ctx, stats)
if err != nil {
return nil, errors.Wrap(err, "tx.NewRaw")
}
return stats, nil
}
// PlayerTeamInfo holds a team the player has played on and how many seasons
type PlayerTeamInfo struct {
Team *Team
SeasonsCount int
}
// GetPlayerTeams returns all teams the player has been rostered on,
// with a count of distinct seasons per team.
func GetPlayerTeams(ctx context.Context, tx bun.Tx, playerID int) ([]*PlayerTeamInfo, error) {
if playerID == 0 {
return nil, errors.New("playerID not provided")
}
type teamRow struct {
TeamID int `bun:"team_id"`
SeasonsCount int `bun:"seasons_count"`
Name string `bun:"name"`
ShortName string `bun:"short_name"`
AltShortName string `bun:"alt_short_name"`
Color string `bun:"color"`
}
var rows []teamRow
err := tx.NewRaw(`
SELECT
t.id AS team_id,
t.name AS name,
t.short_name AS short_name,
t.alt_short_name AS alt_short_name,
t.color AS color,
COUNT(DISTINCT tr.season_id) AS seasons_count
FROM team_rosters tr
JOIN teams t ON t.id = tr.team_id
WHERE tr.player_id = ?
GROUP BY t.id, t.name, t.short_name, t.alt_short_name, t.color
ORDER BY seasons_count DESC
`, playerID).Scan(ctx, &rows)
if err != nil {
return nil, errors.Wrap(err, "tx.NewRaw")
}
var results []*PlayerTeamInfo
for _, row := range rows {
results = append(results, &PlayerTeamInfo{
Team: &Team{
ID: row.TeamID,
Name: row.Name,
ShortName: row.ShortName,
AltShortName: row.AltShortName,
Color: row.Color,
},
SeasonsCount: row.SeasonsCount,
})
}
return results, nil
}
// PlayerSeasonInfo holds info about a player's participation in a specific season
type PlayerSeasonInfo struct {
Season *Season
League *League
Team *Team
IsManager bool
}
// GetPlayerSeasons returns all season/league/team combos the player has been rostered in.
func GetPlayerSeasons(ctx context.Context, tx bun.Tx, playerID int) ([]*PlayerSeasonInfo, error) {
if playerID == 0 {
return nil, errors.New("playerID not provided")
}
var rosters []*TeamRoster
err := tx.NewSelect().
Model(&rosters).
Where("tr.player_id = ?", playerID).
Relation("Season").
Relation("League").
Relation("Team").
Order("season.start_date DESC").
Scan(ctx)
if err != nil {
return nil, errors.Wrap(err, "tx.NewSelect")
}
var results []*PlayerSeasonInfo
for _, r := range rosters {
results = append(results, &PlayerSeasonInfo{
Season: r.Season,
League: r.League,
Team: r.Team,
IsManager: r.IsManager,
})
}
return results, nil
}
// GetPlayerSeasonsList returns distinct seasons the player has participated in (for filter dropdowns).
func GetPlayerSeasonsList(ctx context.Context, tx bun.Tx, playerID int) ([]*Season, error) {
if playerID == 0 {
return nil, errors.New("playerID not provided")
}
var seasons []*Season
err := tx.NewSelect().
Model(&seasons).
Join("JOIN team_rosters tr ON tr.season_id = s.id").
Where("tr.player_id = ?", playerID).
GroupExpr("s.id").
Order("s.start_date DESC").
Scan(ctx)
if err != nil {
return nil, errors.Wrap(err, "tx.NewSelect")
}
return seasons, nil
}
// GetPlayerTeamsList returns distinct teams the player has played on (for filter dropdowns).
func GetPlayerTeamsList(ctx context.Context, tx bun.Tx, playerID int) ([]*Team, error) {
if playerID == 0 {
return nil, errors.New("playerID not provided")
}
var teams []*Team
err := tx.NewSelect().
Model(&teams).
Join("JOIN team_rosters tr ON tr.team_id = t.id").
Where("tr.player_id = ?", playerID).
GroupExpr("t.id").
Order("t.name ASC").
Scan(ctx)
if err != nil {
return nil, errors.Wrap(err, "tx.NewSelect")
}
return teams, nil
}
func GetPlayersNotOnTeam(ctx context.Context, tx bun.Tx, seasonID, leagueID int) ([]*Player, error) {
players, err := GetList[Player](tx).Relation("User").
Join("LEFT JOIN team_rosters tr ON tr.player_id = p.id").
Where("NOT (tr.season_id = ? and tr.league_id = ?) OR (tr.season_id IS NULL and tr.league_id IS NULL)",
seasonID, leagueID).
Order("p.name ASC").
GetAll(ctx)
if err != nil {
return nil, errors.Wrap(err, "GetList")
}
return players, nil
}