refactor: split PlayerSeasonStats into BattingSeasonStats and PitchingSeasonStats
Some checks failed
Build Docker Image / build (push) Has been cancelled
Some checks failed
Build Docker Image / build (push) Has been cancelled
Separate batting and pitching into distinct tables with descriptive column names. Eliminates naming collisions (so/k ambiguity) and column mismatches between the ORM model and raw SQL. Each table now covers all aggregatable fields from its source (BattingStat/PitchingStat) including sac, ibb, gidp, earned_runs, runs_allowed, wild_pitches, balks, and games_started. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4ed62dea2c
commit
bd8e4578cc
114
app/db_engine.py
114
app/db_engine.py
@ -1050,73 +1050,113 @@ decision_index = ModelIndex(Decision, (Decision.game, Decision.pitcher), unique=
|
||||
Decision.add_index(decision_index)
|
||||
|
||||
|
||||
class PlayerSeasonStats(BaseModel):
|
||||
class BattingSeasonStats(BaseModel):
|
||||
player = ForeignKeyField(Player)
|
||||
team = ForeignKeyField(Team)
|
||||
season = IntegerField()
|
||||
|
||||
# Batting stats
|
||||
games_batting = IntegerField(default=0)
|
||||
games = IntegerField(default=0)
|
||||
pa = IntegerField(default=0)
|
||||
ab = IntegerField(default=0)
|
||||
hits = IntegerField(default=0)
|
||||
hr = IntegerField(default=0)
|
||||
doubles = IntegerField(default=0)
|
||||
triples = IntegerField(default=0)
|
||||
bb = IntegerField(default=0)
|
||||
hbp = IntegerField(default=0)
|
||||
so_batter = IntegerField(default=0)
|
||||
hr = IntegerField(default=0)
|
||||
rbi = IntegerField(default=0)
|
||||
runs = IntegerField(default=0)
|
||||
bb = IntegerField(default=0)
|
||||
strikeouts = IntegerField(default=0)
|
||||
hbp = IntegerField(default=0)
|
||||
sac = IntegerField(default=0)
|
||||
ibb = IntegerField(default=0)
|
||||
gidp = IntegerField(default=0)
|
||||
sb = IntegerField(default=0)
|
||||
cs = IntegerField(default=0)
|
||||
|
||||
# Pitching stats
|
||||
games_pitching = IntegerField(default=0)
|
||||
outs = IntegerField(default=0)
|
||||
so_pitcher = IntegerField(default=0)
|
||||
bb_allowed = IntegerField(default=0)
|
||||
hits_allowed = IntegerField(default=0)
|
||||
hr_allowed = IntegerField(default=0)
|
||||
wins = IntegerField(default=0)
|
||||
losses = IntegerField(default=0)
|
||||
saves = IntegerField(default=0)
|
||||
holds = IntegerField(default=0)
|
||||
blown_saves = IntegerField(default=0)
|
||||
|
||||
# Meta
|
||||
last_game = ForeignKeyField(StratGame, null=True)
|
||||
last_updated_at = DateTimeField(null=True)
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
table_name = "player_season_stats"
|
||||
table_name = "batting_season_stats"
|
||||
|
||||
|
||||
pss_unique_index = ModelIndex(
|
||||
PlayerSeasonStats,
|
||||
(PlayerSeasonStats.player, PlayerSeasonStats.team, PlayerSeasonStats.season),
|
||||
bss_unique_index = ModelIndex(
|
||||
BattingSeasonStats,
|
||||
(BattingSeasonStats.player, BattingSeasonStats.team, BattingSeasonStats.season),
|
||||
unique=True,
|
||||
)
|
||||
PlayerSeasonStats.add_index(pss_unique_index)
|
||||
BattingSeasonStats.add_index(bss_unique_index)
|
||||
|
||||
pss_team_season_index = ModelIndex(
|
||||
PlayerSeasonStats,
|
||||
(PlayerSeasonStats.team, PlayerSeasonStats.season),
|
||||
bss_team_season_index = ModelIndex(
|
||||
BattingSeasonStats,
|
||||
(BattingSeasonStats.team, BattingSeasonStats.season),
|
||||
unique=False,
|
||||
)
|
||||
PlayerSeasonStats.add_index(pss_team_season_index)
|
||||
BattingSeasonStats.add_index(bss_team_season_index)
|
||||
|
||||
pss_player_season_index = ModelIndex(
|
||||
PlayerSeasonStats,
|
||||
(PlayerSeasonStats.player, PlayerSeasonStats.season),
|
||||
bss_player_season_index = ModelIndex(
|
||||
BattingSeasonStats,
|
||||
(BattingSeasonStats.player, BattingSeasonStats.season),
|
||||
unique=False,
|
||||
)
|
||||
PlayerSeasonStats.add_index(pss_player_season_index)
|
||||
BattingSeasonStats.add_index(bss_player_season_index)
|
||||
|
||||
|
||||
class PitchingSeasonStats(BaseModel):
|
||||
player = ForeignKeyField(Player)
|
||||
team = ForeignKeyField(Team)
|
||||
season = IntegerField()
|
||||
games = IntegerField(default=0)
|
||||
games_started = IntegerField(default=0)
|
||||
outs = IntegerField(default=0)
|
||||
strikeouts = IntegerField(default=0)
|
||||
bb = IntegerField(default=0)
|
||||
hits_allowed = IntegerField(default=0)
|
||||
runs_allowed = IntegerField(default=0)
|
||||
earned_runs = IntegerField(default=0)
|
||||
hr_allowed = IntegerField(default=0)
|
||||
hbp = IntegerField(default=0)
|
||||
wild_pitches = IntegerField(default=0)
|
||||
balks = IntegerField(default=0)
|
||||
wins = IntegerField(default=0)
|
||||
losses = IntegerField(default=0)
|
||||
holds = IntegerField(default=0)
|
||||
saves = IntegerField(default=0)
|
||||
blown_saves = IntegerField(default=0)
|
||||
last_game = ForeignKeyField(StratGame, null=True)
|
||||
last_updated_at = DateTimeField(null=True)
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
table_name = "pitching_season_stats"
|
||||
|
||||
|
||||
pitss_unique_index = ModelIndex(
|
||||
PitchingSeasonStats,
|
||||
(PitchingSeasonStats.player, PitchingSeasonStats.team, PitchingSeasonStats.season),
|
||||
unique=True,
|
||||
)
|
||||
PitchingSeasonStats.add_index(pitss_unique_index)
|
||||
|
||||
pitss_team_season_index = ModelIndex(
|
||||
PitchingSeasonStats,
|
||||
(PitchingSeasonStats.team, PitchingSeasonStats.season),
|
||||
unique=False,
|
||||
)
|
||||
PitchingSeasonStats.add_index(pitss_team_season_index)
|
||||
|
||||
pitss_player_season_index = ModelIndex(
|
||||
PitchingSeasonStats,
|
||||
(PitchingSeasonStats.player, PitchingSeasonStats.season),
|
||||
unique=False,
|
||||
)
|
||||
PitchingSeasonStats.add_index(pitss_player_season_index)
|
||||
|
||||
|
||||
if not SKIP_TABLE_CREATION:
|
||||
db.create_tables([StratGame, StratPlay, Decision, PlayerSeasonStats], safe=True)
|
||||
db.create_tables(
|
||||
[StratGame, StratPlay, Decision, BattingSeasonStats, PitchingSeasonStats],
|
||||
safe=True,
|
||||
)
|
||||
|
||||
|
||||
class ScoutOpportunity(BaseModel):
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""PlayerSeasonStats ORM model.
|
||||
"""Season stats ORM models.
|
||||
|
||||
Model is defined in db_engine alongside all other Peewee models; this
|
||||
module re-exports it so callers can import from `app.models.season_stats`.
|
||||
Models are defined in db_engine alongside all other Peewee models; this
|
||||
module re-exports them so callers can import from `app.models.season_stats`.
|
||||
"""
|
||||
|
||||
from ..db_engine import PlayerSeasonStats # noqa: F401
|
||||
from ..db_engine import BattingSeasonStats, PitchingSeasonStats # noqa: F401
|
||||
|
||||
@ -4,9 +4,8 @@ Covers WP-13 (Post-Game Callback Integration):
|
||||
POST /api/v2/season-stats/update-game/{game_id}
|
||||
|
||||
Aggregates BattingStat and PitchingStat rows for a completed game and
|
||||
increments the corresponding player_season_stats rows via an additive upsert.
|
||||
|
||||
Lazy-imports PlayerSeasonStats so this module loads before WP-05 merges.
|
||||
increments the corresponding batting_season_stats / pitching_season_stats
|
||||
rows via an additive upsert.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@ -32,13 +31,14 @@ def _ip_to_outs(ip: float) -> int:
|
||||
|
||||
@router.post("/update-game/{game_id}")
|
||||
async def update_game_season_stats(game_id: int, token: str = Depends(oauth2_scheme)):
|
||||
"""Increment player_season_stats with batting and pitching deltas from a game.
|
||||
"""Increment season stats with batting and pitching deltas from a game.
|
||||
|
||||
Queries BattingStat and PitchingStat rows for game_id, aggregates by
|
||||
(player_id, team_id, season), then performs an additive ON CONFLICT upsert
|
||||
into player_season_stats. Idempotent: replaying the same game_id a second
|
||||
time will double-count stats, so callers must ensure this is only called once
|
||||
per game.
|
||||
into batting_season_stats and pitching_season_stats respectively.
|
||||
|
||||
Replaying the same game_id will double-count stats, so callers must ensure
|
||||
this is only called once per game.
|
||||
|
||||
Response: {"updated": N} where N is the number of player rows touched.
|
||||
"""
|
||||
@ -73,14 +73,14 @@ async def update_game_season_stats(game_id: int, token: str = Depends(oauth2_sch
|
||||
season,
|
||||
pa,
|
||||
ab,
|
||||
r,
|
||||
runs,
|
||||
hits,
|
||||
doubles,
|
||||
triples,
|
||||
hr,
|
||||
rbi,
|
||||
bb,
|
||||
so,
|
||||
strikeouts,
|
||||
hbp,
|
||||
sac,
|
||||
ibb,
|
||||
@ -90,28 +90,28 @@ async def update_game_season_stats(game_id: int, token: str = Depends(oauth2_sch
|
||||
) = row
|
||||
db.execute_sql(
|
||||
"""
|
||||
INSERT INTO player_season_stats
|
||||
INSERT INTO batting_season_stats
|
||||
(player_id, team_id, season,
|
||||
pa, ab, r, hits, doubles, triples, hr, rbi,
|
||||
bb, so_batter, hbp, sac, ibb, gidp, sb, cs)
|
||||
pa, ab, runs, hits, doubles, triples, hr, rbi,
|
||||
bb, strikeouts, hbp, sac, ibb, gidp, sb, cs)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (player_id, team_id, season) DO UPDATE SET
|
||||
pa = player_season_stats.pa + EXCLUDED.pa,
|
||||
ab = player_season_stats.ab + EXCLUDED.ab,
|
||||
r = player_season_stats.r + EXCLUDED.r,
|
||||
hits = player_season_stats.hits + EXCLUDED.hits,
|
||||
doubles= player_season_stats.doubles+ EXCLUDED.doubles,
|
||||
triples= player_season_stats.triples+ EXCLUDED.triples,
|
||||
hr = player_season_stats.hr + EXCLUDED.hr,
|
||||
rbi = player_season_stats.rbi + EXCLUDED.rbi,
|
||||
bb = player_season_stats.bb + EXCLUDED.bb,
|
||||
so_batter= player_season_stats.so_batter+ EXCLUDED.so_batter,
|
||||
hbp = player_season_stats.hbp + EXCLUDED.hbp,
|
||||
sac = player_season_stats.sac + EXCLUDED.sac,
|
||||
ibb = player_season_stats.ibb + EXCLUDED.ibb,
|
||||
gidp = player_season_stats.gidp + EXCLUDED.gidp,
|
||||
sb = player_season_stats.sb + EXCLUDED.sb,
|
||||
cs = player_season_stats.cs + EXCLUDED.cs
|
||||
pa = batting_season_stats.pa + EXCLUDED.pa,
|
||||
ab = batting_season_stats.ab + EXCLUDED.ab,
|
||||
runs = batting_season_stats.runs + EXCLUDED.runs,
|
||||
hits = batting_season_stats.hits + EXCLUDED.hits,
|
||||
doubles = batting_season_stats.doubles + EXCLUDED.doubles,
|
||||
triples = batting_season_stats.triples + EXCLUDED.triples,
|
||||
hr = batting_season_stats.hr + EXCLUDED.hr,
|
||||
rbi = batting_season_stats.rbi + EXCLUDED.rbi,
|
||||
bb = batting_season_stats.bb + EXCLUDED.bb,
|
||||
strikeouts= batting_season_stats.strikeouts+ EXCLUDED.strikeouts,
|
||||
hbp = batting_season_stats.hbp + EXCLUDED.hbp,
|
||||
sac = batting_season_stats.sac + EXCLUDED.sac,
|
||||
ibb = batting_season_stats.ibb + EXCLUDED.ibb,
|
||||
gidp = batting_season_stats.gidp + EXCLUDED.gidp,
|
||||
sb = batting_season_stats.sb + EXCLUDED.sb,
|
||||
cs = batting_season_stats.cs + EXCLUDED.cs
|
||||
""",
|
||||
(
|
||||
player_id,
|
||||
@ -119,14 +119,14 @@ async def update_game_season_stats(game_id: int, token: str = Depends(oauth2_sch
|
||||
season,
|
||||
pa,
|
||||
ab,
|
||||
r,
|
||||
runs,
|
||||
hits,
|
||||
doubles,
|
||||
triples,
|
||||
hr,
|
||||
rbi,
|
||||
bb,
|
||||
so,
|
||||
strikeouts,
|
||||
hbp,
|
||||
sac,
|
||||
ibb,
|
||||
@ -161,72 +161,72 @@ async def update_game_season_stats(game_id: int, token: str = Depends(oauth2_sch
|
||||
team_id,
|
||||
season,
|
||||
ip,
|
||||
so_pitcher,
|
||||
h_allowed,
|
||||
r_allowed,
|
||||
er,
|
||||
bb_p,
|
||||
hbp_p,
|
||||
wp,
|
||||
balk,
|
||||
hr_p,
|
||||
gs,
|
||||
w,
|
||||
strikeouts,
|
||||
hits_allowed,
|
||||
runs_allowed,
|
||||
earned_runs,
|
||||
bb,
|
||||
hbp,
|
||||
wild_pitches,
|
||||
balks,
|
||||
hr_allowed,
|
||||
games_started,
|
||||
wins,
|
||||
losses,
|
||||
hold,
|
||||
sv,
|
||||
bsv,
|
||||
holds,
|
||||
saves,
|
||||
blown_saves,
|
||||
) = row
|
||||
outs = _ip_to_outs(float(ip))
|
||||
db.execute_sql(
|
||||
"""
|
||||
INSERT INTO player_season_stats
|
||||
INSERT INTO pitching_season_stats
|
||||
(player_id, team_id, season,
|
||||
outs, so_pitcher, h_allowed, r_allowed, er,
|
||||
bb_p, hbp_p, wp, balk, hr_p,
|
||||
gs, w, l, hold, sv, bsv)
|
||||
outs, strikeouts, hits_allowed, runs_allowed, earned_runs,
|
||||
bb, hbp, wild_pitches, balks, hr_allowed,
|
||||
games_started, wins, losses, holds, saves, blown_saves)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (player_id, team_id, season) DO UPDATE SET
|
||||
outs = player_season_stats.outs + EXCLUDED.outs,
|
||||
so_pitcher= player_season_stats.so_pitcher+ EXCLUDED.so_pitcher,
|
||||
h_allowed= player_season_stats.h_allowed+ EXCLUDED.h_allowed,
|
||||
r_allowed= player_season_stats.r_allowed+ EXCLUDED.r_allowed,
|
||||
er = player_season_stats.er + EXCLUDED.er,
|
||||
bb_p = player_season_stats.bb_p + EXCLUDED.bb_p,
|
||||
hbp_p = player_season_stats.hbp_p + EXCLUDED.hbp_p,
|
||||
wp = player_season_stats.wp + EXCLUDED.wp,
|
||||
balk = player_season_stats.balk + EXCLUDED.balk,
|
||||
hr_p = player_season_stats.hr_p + EXCLUDED.hr_p,
|
||||
gs = player_season_stats.gs + EXCLUDED.gs,
|
||||
w = player_season_stats.w + EXCLUDED.w,
|
||||
l = player_season_stats.l + EXCLUDED.l,
|
||||
hold = player_season_stats.hold + EXCLUDED.hold,
|
||||
sv = player_season_stats.sv + EXCLUDED.sv,
|
||||
bsv = player_season_stats.bsv + EXCLUDED.bsv
|
||||
outs = pitching_season_stats.outs + EXCLUDED.outs,
|
||||
strikeouts = pitching_season_stats.strikeouts + EXCLUDED.strikeouts,
|
||||
hits_allowed= pitching_season_stats.hits_allowed+ EXCLUDED.hits_allowed,
|
||||
runs_allowed= pitching_season_stats.runs_allowed+ EXCLUDED.runs_allowed,
|
||||
earned_runs = pitching_season_stats.earned_runs + EXCLUDED.earned_runs,
|
||||
bb = pitching_season_stats.bb + EXCLUDED.bb,
|
||||
hbp = pitching_season_stats.hbp + EXCLUDED.hbp,
|
||||
wild_pitches= pitching_season_stats.wild_pitches+ EXCLUDED.wild_pitches,
|
||||
balks = pitching_season_stats.balks + EXCLUDED.balks,
|
||||
hr_allowed = pitching_season_stats.hr_allowed + EXCLUDED.hr_allowed,
|
||||
games_started= pitching_season_stats.games_started+ EXCLUDED.games_started,
|
||||
wins = pitching_season_stats.wins + EXCLUDED.wins,
|
||||
losses = pitching_season_stats.losses + EXCLUDED.losses,
|
||||
holds = pitching_season_stats.holds + EXCLUDED.holds,
|
||||
saves = pitching_season_stats.saves + EXCLUDED.saves,
|
||||
blown_saves = pitching_season_stats.blown_saves + EXCLUDED.blown_saves
|
||||
""",
|
||||
(
|
||||
player_id,
|
||||
team_id,
|
||||
season,
|
||||
outs,
|
||||
so_pitcher,
|
||||
h_allowed,
|
||||
r_allowed,
|
||||
er,
|
||||
bb_p,
|
||||
hbp_p,
|
||||
wp,
|
||||
balk,
|
||||
hr_p,
|
||||
gs,
|
||||
w,
|
||||
strikeouts,
|
||||
hits_allowed,
|
||||
runs_allowed,
|
||||
earned_runs,
|
||||
bb,
|
||||
hbp,
|
||||
wild_pitches,
|
||||
balks,
|
||||
hr_allowed,
|
||||
games_started,
|
||||
wins,
|
||||
losses,
|
||||
hold,
|
||||
sv,
|
||||
bsv,
|
||||
holds,
|
||||
saves,
|
||||
blown_saves,
|
||||
),
|
||||
)
|
||||
updated += 1
|
||||
|
||||
logging.info(f"update-game/{game_id}: updated {updated} player_season_stats rows")
|
||||
logging.info(f"update-game/{game_id}: updated {updated} season stats rows")
|
||||
return {"updated": updated}
|
||||
|
||||
@ -4,9 +4,9 @@ Three pure functions that compute a numeric evolution value from career stats,
|
||||
plus helpers for formula dispatch and tier classification.
|
||||
|
||||
Stats attributes expected by each formula:
|
||||
compute_batter_value: pa, hits, doubles, triples, hr
|
||||
compute_sp_value: outs, so_pitcher (pitcher strikeouts, from PlayerSeasonStats)
|
||||
compute_rp_value: outs, so_pitcher
|
||||
compute_batter_value: pa, hits, doubles, triples, hr (from BattingSeasonStats)
|
||||
compute_sp_value: outs, strikeouts (from PitchingSeasonStats)
|
||||
compute_rp_value: outs, strikeouts (from PitchingSeasonStats)
|
||||
"""
|
||||
|
||||
from typing import Protocol
|
||||
@ -22,7 +22,7 @@ class BatterStats(Protocol):
|
||||
|
||||
class PitcherStats(Protocol):
|
||||
outs: int
|
||||
so_pitcher: int
|
||||
strikeouts: int
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -31,20 +31,20 @@ class PitcherStats(Protocol):
|
||||
|
||||
|
||||
def compute_batter_value(stats) -> float:
|
||||
"""PA + (TB × 2) where TB = 1B + 2×2B + 3×3B + 4×HR."""
|
||||
"""PA + (TB x 2) where TB = 1B + 2x2B + 3x3B + 4xHR."""
|
||||
singles = stats.hits - stats.doubles - stats.triples - stats.hr
|
||||
tb = singles + 2 * stats.doubles + 3 * stats.triples + 4 * stats.hr
|
||||
return float(stats.pa + tb * 2)
|
||||
|
||||
|
||||
def compute_sp_value(stats) -> float:
|
||||
"""IP + K where IP = outs / 3. Uses stats.so_pitcher (pitcher strikeouts)."""
|
||||
return stats.outs / 3 + stats.so_pitcher
|
||||
"""IP + K where IP = outs / 3."""
|
||||
return stats.outs / 3 + stats.strikeouts
|
||||
|
||||
|
||||
def compute_rp_value(stats) -> float:
|
||||
"""IP + K (same formula as SP; thresholds differ). Uses stats.so_pitcher."""
|
||||
return stats.outs / 3 + stats.so_pitcher
|
||||
"""IP + K (same formula as SP; thresholds differ)."""
|
||||
return stats.outs / 3 + stats.strikeouts
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -75,7 +75,7 @@ def compute_value_for_track(card_type: str, stats) -> float:
|
||||
|
||||
|
||||
def tier_from_value(value: float, track) -> int:
|
||||
"""Return the evolution tier (0–4) for a computed value against a track.
|
||||
"""Return the evolution tier (0-4) for a computed value against a track.
|
||||
|
||||
Tier boundaries are inclusive on the lower end:
|
||||
T0: value < t1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""Tests for the formula engine (WP-09).
|
||||
|
||||
Unit tests only — no database required. Stats inputs are simple namespace
|
||||
objects whose attributes match what PlayerSeasonStats exposes.
|
||||
objects whose attributes match what BattingSeasonStats/PitchingSeasonStats expose.
|
||||
|
||||
Tier thresholds used (from evolution_tracks.json seed data):
|
||||
Batter: t1=37, t2=149, t3=448, t4=896
|
||||
@ -35,7 +35,7 @@ def batter_stats(**kwargs):
|
||||
|
||||
def pitcher_stats(**kwargs):
|
||||
"""Build a minimal pitcher stats object with all fields defaulting to 0."""
|
||||
defaults = {"outs": 0, "so_pitcher": 0}
|
||||
defaults = {"outs": 0, "strikeouts": 0}
|
||||
defaults.update(kwargs)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
@ -84,7 +84,7 @@ def test_batter_formula_hr_heavy():
|
||||
|
||||
def test_sp_formula_standard():
|
||||
"""18 outs + 5 K: IP = 18/3 = 6.0, value = 6.0 + 5 = 11.0."""
|
||||
stats = pitcher_stats(outs=18, so_pitcher=5)
|
||||
stats = pitcher_stats(outs=18, strikeouts=5)
|
||||
assert compute_sp_value(stats) == 11.0
|
||||
|
||||
|
||||
@ -95,7 +95,7 @@ def test_sp_formula_standard():
|
||||
|
||||
def test_rp_formula_standard():
|
||||
"""3 outs + 2 K: IP = 3/3 = 1.0, value = 1.0 + 2 = 3.0."""
|
||||
stats = pitcher_stats(outs=3, so_pitcher=2)
|
||||
stats = pitcher_stats(outs=3, strikeouts=2)
|
||||
assert compute_rp_value(stats) == 3.0
|
||||
|
||||
|
||||
@ -132,13 +132,13 @@ def test_dispatch_batter():
|
||||
|
||||
def test_dispatch_sp():
|
||||
"""compute_value_for_track('sp', ...) delegates to compute_sp_value."""
|
||||
stats = pitcher_stats(outs=18, so_pitcher=5)
|
||||
stats = pitcher_stats(outs=18, strikeouts=5)
|
||||
assert compute_value_for_track("sp", stats) == compute_sp_value(stats)
|
||||
|
||||
|
||||
def test_dispatch_rp():
|
||||
"""compute_value_for_track('rp', ...) delegates to compute_rp_value."""
|
||||
stats = pitcher_stats(outs=3, so_pitcher=2)
|
||||
stats = pitcher_stats(outs=3, strikeouts=2)
|
||||
assert compute_value_for_track("rp", stats) == compute_rp_value(stats)
|
||||
|
||||
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
"""Tests for PlayerSeasonStats Peewee model (WP-02).
|
||||
"""Tests for BattingSeasonStats and PitchingSeasonStats Peewee models.
|
||||
|
||||
Unit tests verify model structure and defaults on unsaved instances without
|
||||
touching a database. Integration tests use an in-memory SQLite database to
|
||||
verify table creation, unique constraints, indexes, and the delta-update
|
||||
(increment) pattern.
|
||||
|
||||
Note on column naming: the spec labels the pitching strikeout column as
|
||||
"so (K)". This model names it `k` to avoid collision with the batting
|
||||
strikeout column `so`.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from peewee import SqliteDatabase, IntegrityError
|
||||
|
||||
from app.models.season_stats import PlayerSeasonStats
|
||||
from app.models.season_stats import BattingSeasonStats, PitchingSeasonStats
|
||||
from app.db_engine import Rarity, Event, Cardset, MlbPlayer, Player, Team, StratGame
|
||||
|
||||
# Dependency order matters for FK resolution.
|
||||
@ -25,7 +21,8 @@ _TEST_MODELS = [
|
||||
Player,
|
||||
Team,
|
||||
StratGame,
|
||||
PlayerSeasonStats,
|
||||
BattingSeasonStats,
|
||||
PitchingSeasonStats,
|
||||
]
|
||||
|
||||
_test_db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1})
|
||||
@ -92,218 +89,278 @@ def make_game(home_team, away_team, season=10):
|
||||
)
|
||||
|
||||
|
||||
def make_stats(player, team, season=10, **kwargs):
|
||||
return PlayerSeasonStats.create(player=player, team=team, season=season, **kwargs)
|
||||
def make_batting_stats(player, team, season=10, **kwargs):
|
||||
return BattingSeasonStats.create(player=player, team=team, season=season, **kwargs)
|
||||
|
||||
|
||||
def make_pitching_stats(player, team, season=10, **kwargs):
|
||||
return PitchingSeasonStats.create(player=player, team=team, season=season, **kwargs)
|
||||
|
||||
|
||||
# ── Unit: column completeness ────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestColumnCompleteness:
|
||||
"""All required columns are present in the model's field definitions."""
|
||||
class TestBattingColumnCompleteness:
|
||||
"""All required columns are present in BattingSeasonStats."""
|
||||
|
||||
BATTING_COLS = [
|
||||
"games_batting",
|
||||
EXPECTED_COLS = [
|
||||
"games",
|
||||
"pa",
|
||||
"ab",
|
||||
"hits",
|
||||
"hr",
|
||||
"doubles",
|
||||
"triples",
|
||||
"bb",
|
||||
"hbp",
|
||||
"so_batter",
|
||||
"hr",
|
||||
"rbi",
|
||||
"runs",
|
||||
"bb",
|
||||
"strikeouts",
|
||||
"hbp",
|
||||
"sac",
|
||||
"ibb",
|
||||
"gidp",
|
||||
"sb",
|
||||
"cs",
|
||||
]
|
||||
PITCHING_COLS = [
|
||||
"games_pitching",
|
||||
"outs",
|
||||
"so_pitcher",
|
||||
"bb_allowed",
|
||||
"hits_allowed",
|
||||
"hr_allowed",
|
||||
"wins",
|
||||
"losses",
|
||||
"saves",
|
||||
"holds",
|
||||
"blown_saves",
|
||||
]
|
||||
META_COLS = ["last_game", "last_updated_at"]
|
||||
KEY_COLS = ["player", "team", "season"]
|
||||
META_COLS = ["last_game", "last_updated_at"]
|
||||
|
||||
def test_batting_columns_present(self):
|
||||
"""All batting aggregate columns defined in the spec are present."""
|
||||
fields = PlayerSeasonStats._meta.fields
|
||||
for col in self.BATTING_COLS:
|
||||
def test_stat_columns_present(self):
|
||||
"""All batting aggregate columns are present."""
|
||||
fields = BattingSeasonStats._meta.fields
|
||||
for col in self.EXPECTED_COLS:
|
||||
assert col in fields, f"Missing batting column: {col}"
|
||||
|
||||
def test_pitching_columns_present(self):
|
||||
"""All pitching aggregate columns defined in the spec are present."""
|
||||
fields = PlayerSeasonStats._meta.fields
|
||||
for col in self.PITCHING_COLS:
|
||||
assert col in fields, f"Missing pitching column: {col}"
|
||||
|
||||
def test_meta_columns_present(self):
|
||||
"""Meta columns last_game and last_updated_at are present."""
|
||||
fields = PlayerSeasonStats._meta.fields
|
||||
for col in self.META_COLS:
|
||||
assert col in fields, f"Missing meta column: {col}"
|
||||
|
||||
def test_key_columns_present(self):
|
||||
"""player, team, and season columns are present."""
|
||||
fields = PlayerSeasonStats._meta.fields
|
||||
fields = BattingSeasonStats._meta.fields
|
||||
for col in self.KEY_COLS:
|
||||
assert col in fields, f"Missing key column: {col}"
|
||||
|
||||
def test_excluded_columns_absent(self):
|
||||
"""team_wins and quality_starts are NOT in the model (removed from scope)."""
|
||||
fields = PlayerSeasonStats._meta.fields
|
||||
assert "team_wins" not in fields
|
||||
assert "quality_starts" not in fields
|
||||
def test_meta_columns_present(self):
|
||||
"""Meta columns last_game and last_updated_at are present."""
|
||||
fields = BattingSeasonStats._meta.fields
|
||||
for col in self.META_COLS:
|
||||
assert col in fields, f"Missing meta column: {col}"
|
||||
|
||||
|
||||
class TestPitchingColumnCompleteness:
|
||||
"""All required columns are present in PitchingSeasonStats."""
|
||||
|
||||
EXPECTED_COLS = [
|
||||
"games",
|
||||
"games_started",
|
||||
"outs",
|
||||
"strikeouts",
|
||||
"bb",
|
||||
"hits_allowed",
|
||||
"runs_allowed",
|
||||
"earned_runs",
|
||||
"hr_allowed",
|
||||
"hbp",
|
||||
"wild_pitches",
|
||||
"balks",
|
||||
"wins",
|
||||
"losses",
|
||||
"holds",
|
||||
"saves",
|
||||
"blown_saves",
|
||||
]
|
||||
KEY_COLS = ["player", "team", "season"]
|
||||
META_COLS = ["last_game", "last_updated_at"]
|
||||
|
||||
def test_stat_columns_present(self):
|
||||
"""All pitching aggregate columns are present."""
|
||||
fields = PitchingSeasonStats._meta.fields
|
||||
for col in self.EXPECTED_COLS:
|
||||
assert col in fields, f"Missing pitching column: {col}"
|
||||
|
||||
def test_key_columns_present(self):
|
||||
"""player, team, and season columns are present."""
|
||||
fields = PitchingSeasonStats._meta.fields
|
||||
for col in self.KEY_COLS:
|
||||
assert col in fields, f"Missing key column: {col}"
|
||||
|
||||
def test_meta_columns_present(self):
|
||||
"""Meta columns last_game and last_updated_at are present."""
|
||||
fields = PitchingSeasonStats._meta.fields
|
||||
for col in self.META_COLS:
|
||||
assert col in fields, f"Missing meta column: {col}"
|
||||
|
||||
|
||||
# ── Unit: default values ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDefaultValues:
|
||||
class TestBattingDefaultValues:
|
||||
"""All integer stat columns default to 0; nullable meta fields default to None."""
|
||||
|
||||
INT_STAT_COLS = [
|
||||
"games_batting",
|
||||
"games",
|
||||
"pa",
|
||||
"ab",
|
||||
"hits",
|
||||
"hr",
|
||||
"doubles",
|
||||
"triples",
|
||||
"bb",
|
||||
"hbp",
|
||||
"so_batter",
|
||||
"hr",
|
||||
"rbi",
|
||||
"runs",
|
||||
"bb",
|
||||
"strikeouts",
|
||||
"hbp",
|
||||
"sac",
|
||||
"ibb",
|
||||
"gidp",
|
||||
"sb",
|
||||
"cs",
|
||||
"games_pitching",
|
||||
"outs",
|
||||
"so_pitcher",
|
||||
"bb_allowed",
|
||||
"hits_allowed",
|
||||
"hr_allowed",
|
||||
"wins",
|
||||
"losses",
|
||||
"saves",
|
||||
"holds",
|
||||
"blown_saves",
|
||||
]
|
||||
|
||||
def test_all_int_columns_default_to_zero(self):
|
||||
"""Every integer stat column defaults to 0 on an unsaved instance."""
|
||||
row = PlayerSeasonStats()
|
||||
row = BattingSeasonStats()
|
||||
for col in self.INT_STAT_COLS:
|
||||
val = getattr(row, col)
|
||||
assert val == 0, f"Column {col!r} default is {val!r}, expected 0"
|
||||
|
||||
def test_last_game_defaults_to_none(self):
|
||||
"""last_game FK is nullable and defaults to None."""
|
||||
row = PlayerSeasonStats()
|
||||
row = BattingSeasonStats()
|
||||
assert row.last_game_id is None
|
||||
|
||||
def test_last_updated_at_defaults_to_none(self):
|
||||
"""last_updated_at defaults to None."""
|
||||
row = PlayerSeasonStats()
|
||||
row = BattingSeasonStats()
|
||||
assert row.last_updated_at is None
|
||||
|
||||
|
||||
class TestPitchingDefaultValues:
|
||||
"""All integer stat columns default to 0; nullable meta fields default to None."""
|
||||
|
||||
INT_STAT_COLS = [
|
||||
"games",
|
||||
"games_started",
|
||||
"outs",
|
||||
"strikeouts",
|
||||
"bb",
|
||||
"hits_allowed",
|
||||
"runs_allowed",
|
||||
"earned_runs",
|
||||
"hr_allowed",
|
||||
"hbp",
|
||||
"wild_pitches",
|
||||
"balks",
|
||||
"wins",
|
||||
"losses",
|
||||
"holds",
|
||||
"saves",
|
||||
"blown_saves",
|
||||
]
|
||||
|
||||
def test_all_int_columns_default_to_zero(self):
|
||||
"""Every integer stat column defaults to 0 on an unsaved instance."""
|
||||
row = PitchingSeasonStats()
|
||||
for col in self.INT_STAT_COLS:
|
||||
val = getattr(row, col)
|
||||
assert val == 0, f"Column {col!r} default is {val!r}, expected 0"
|
||||
|
||||
def test_last_game_defaults_to_none(self):
|
||||
"""last_game FK is nullable and defaults to None."""
|
||||
row = PitchingSeasonStats()
|
||||
assert row.last_game_id is None
|
||||
|
||||
def test_last_updated_at_defaults_to_none(self):
|
||||
"""last_updated_at defaults to None."""
|
||||
row = PitchingSeasonStats()
|
||||
assert row.last_updated_at is None
|
||||
|
||||
|
||||
# ── Integration: unique constraint ───────────────────────────────────────────
|
||||
|
||||
|
||||
class TestUniqueConstraint:
|
||||
class TestBattingUniqueConstraint:
|
||||
"""UNIQUE on (player_id, team_id, season) is enforced at the DB level."""
|
||||
|
||||
def test_duplicate_player_team_season_raises(self):
|
||||
def test_duplicate_raises(self):
|
||||
"""Inserting a second row for the same (player, team, season) raises IntegrityError."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
make_stats(player, team, season=10)
|
||||
make_batting_stats(player, team, season=10)
|
||||
with pytest.raises(IntegrityError):
|
||||
make_stats(player, team, season=10)
|
||||
make_batting_stats(player, team, season=10)
|
||||
|
||||
def test_same_player_different_season_allowed(self):
|
||||
def test_different_season_allowed(self):
|
||||
"""Same (player, team) in a different season creates a separate row."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
make_stats(player, team, season=10)
|
||||
row2 = make_stats(player, team, season=11)
|
||||
make_batting_stats(player, team, season=10)
|
||||
row2 = make_batting_stats(player, team, season=11)
|
||||
assert row2.id is not None
|
||||
|
||||
def test_same_player_different_team_allowed(self):
|
||||
def test_different_team_allowed(self):
|
||||
"""Same (player, season) on a different team creates a separate row."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team1 = make_team("TM1", gmid=111)
|
||||
team2 = make_team("TM2", gmid=222)
|
||||
make_stats(player, team1, season=10)
|
||||
row2 = make_stats(player, team2, season=10)
|
||||
make_batting_stats(player, team1, season=10)
|
||||
row2 = make_batting_stats(player, team2, season=10)
|
||||
assert row2.id is not None
|
||||
|
||||
|
||||
class TestPitchingUniqueConstraint:
|
||||
"""UNIQUE on (player_id, team_id, season) is enforced at the DB level."""
|
||||
|
||||
def test_duplicate_raises(self):
|
||||
"""Inserting a second row for the same (player, team, season) raises IntegrityError."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
make_pitching_stats(player, team, season=10)
|
||||
with pytest.raises(IntegrityError):
|
||||
make_pitching_stats(player, team, season=10)
|
||||
|
||||
def test_different_season_allowed(self):
|
||||
"""Same (player, team) in a different season creates a separate row."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
make_pitching_stats(player, team, season=10)
|
||||
row2 = make_pitching_stats(player, team, season=11)
|
||||
assert row2.id is not None
|
||||
|
||||
|
||||
# ── Integration: delta update pattern ───────────────────────────────────────
|
||||
|
||||
|
||||
class TestDeltaUpdatePattern:
|
||||
"""Stats can be incremented (delta update) without replacing existing values."""
|
||||
class TestBattingDeltaUpdate:
|
||||
"""Batting stats can be incremented (delta update) without replacing existing values."""
|
||||
|
||||
def test_increment_batting_stats(self):
|
||||
"""Updating pa and hits increments without touching pitching columns."""
|
||||
"""Updating pa and hits increments correctly."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
row = make_stats(player, team, season=10, pa=5, hits=2)
|
||||
row = make_batting_stats(player, team, season=10, pa=5, hits=2)
|
||||
|
||||
PlayerSeasonStats.update(
|
||||
pa=PlayerSeasonStats.pa + 3,
|
||||
hits=PlayerSeasonStats.hits + 1,
|
||||
BattingSeasonStats.update(
|
||||
pa=BattingSeasonStats.pa + 3,
|
||||
hits=BattingSeasonStats.hits + 1,
|
||||
).where(
|
||||
(PlayerSeasonStats.player == player)
|
||||
& (PlayerSeasonStats.team == team)
|
||||
& (PlayerSeasonStats.season == 10)
|
||||
(BattingSeasonStats.player == player)
|
||||
& (BattingSeasonStats.team == team)
|
||||
& (BattingSeasonStats.season == 10)
|
||||
).execute()
|
||||
|
||||
updated = PlayerSeasonStats.get_by_id(row.id)
|
||||
updated = BattingSeasonStats.get_by_id(row.id)
|
||||
assert updated.pa == 8
|
||||
assert updated.hits == 3
|
||||
assert updated.games_pitching == 0 # untouched
|
||||
|
||||
def test_increment_pitching_stats(self):
|
||||
"""Updating outs and so_pitcher increments without touching batting columns."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
row = make_stats(player, team, season=10, outs=9, so_pitcher=3)
|
||||
|
||||
PlayerSeasonStats.update(
|
||||
outs=PlayerSeasonStats.outs + 6,
|
||||
so_pitcher=PlayerSeasonStats.so_pitcher + 2,
|
||||
).where(
|
||||
(PlayerSeasonStats.player == player)
|
||||
& (PlayerSeasonStats.team == team)
|
||||
& (PlayerSeasonStats.season == 10)
|
||||
).execute()
|
||||
|
||||
updated = PlayerSeasonStats.get_by_id(row.id)
|
||||
assert updated.outs == 15
|
||||
assert updated.so_pitcher == 5
|
||||
assert updated.pa == 0 # untouched
|
||||
|
||||
def test_last_game_fk_is_nullable(self):
|
||||
"""last_game FK can be set to a StratGame instance or left NULL."""
|
||||
@ -311,45 +368,116 @@ class TestDeltaUpdatePattern:
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
row = make_stats(player, team, season=10)
|
||||
row = make_batting_stats(player, team, season=10)
|
||||
assert row.last_game_id is None
|
||||
|
||||
game = make_game(home_team=team, away_team=team)
|
||||
PlayerSeasonStats.update(last_game=game).where(
|
||||
PlayerSeasonStats.id == row.id
|
||||
BattingSeasonStats.update(last_game=game).where(
|
||||
BattingSeasonStats.id == row.id
|
||||
).execute()
|
||||
|
||||
updated = PlayerSeasonStats.get_by_id(row.id)
|
||||
updated = BattingSeasonStats.get_by_id(row.id)
|
||||
assert updated.last_game_id == game.id
|
||||
|
||||
|
||||
class TestPitchingDeltaUpdate:
|
||||
"""Pitching stats can be incremented (delta update) without replacing existing values."""
|
||||
|
||||
def test_increment_pitching_stats(self):
|
||||
"""Updating outs and strikeouts increments correctly."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
row = make_pitching_stats(player, team, season=10, outs=9, strikeouts=3)
|
||||
|
||||
PitchingSeasonStats.update(
|
||||
outs=PitchingSeasonStats.outs + 6,
|
||||
strikeouts=PitchingSeasonStats.strikeouts + 2,
|
||||
).where(
|
||||
(PitchingSeasonStats.player == player)
|
||||
& (PitchingSeasonStats.team == team)
|
||||
& (PitchingSeasonStats.season == 10)
|
||||
).execute()
|
||||
|
||||
updated = PitchingSeasonStats.get_by_id(row.id)
|
||||
assert updated.outs == 15
|
||||
assert updated.strikeouts == 5
|
||||
|
||||
def test_last_game_fk_is_nullable(self):
|
||||
"""last_game FK can be set to a StratGame instance or left NULL."""
|
||||
rarity = make_rarity()
|
||||
cardset = make_cardset()
|
||||
player = make_player(cardset, rarity)
|
||||
team = make_team()
|
||||
row = make_pitching_stats(player, team, season=10)
|
||||
assert row.last_game_id is None
|
||||
|
||||
game = make_game(home_team=team, away_team=team)
|
||||
PitchingSeasonStats.update(last_game=game).where(
|
||||
PitchingSeasonStats.id == row.id
|
||||
).execute()
|
||||
|
||||
updated = PitchingSeasonStats.get_by_id(row.id)
|
||||
assert updated.last_game_id == game.id
|
||||
|
||||
|
||||
# ── Integration: index existence ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestIndexExistence:
|
||||
"""Required indexes on (team_id, season) and (player_id, season) exist in SQLite."""
|
||||
class TestBattingIndexExistence:
|
||||
"""Required indexes exist on batting_season_stats."""
|
||||
|
||||
def _get_index_columns(self, db, table):
|
||||
def _get_index_columns(self, db_conn, table):
|
||||
"""Return a set of frozensets, each being the column set of one index."""
|
||||
indexes = db.execute_sql(f"PRAGMA index_list({table})").fetchall()
|
||||
indexes = db_conn.execute_sql(f"PRAGMA index_list({table})").fetchall()
|
||||
result = set()
|
||||
for idx in indexes:
|
||||
idx_name = idx[1]
|
||||
cols = db.execute_sql(f"PRAGMA index_info({idx_name})").fetchall()
|
||||
cols = db_conn.execute_sql(f"PRAGMA index_info({idx_name})").fetchall()
|
||||
result.add(frozenset(col[2] for col in cols))
|
||||
return result
|
||||
|
||||
def test_unique_index_on_player_team_season(self, setup_test_db):
|
||||
"""A unique index covering (player_id, team_id, season) exists."""
|
||||
index_sets = self._get_index_columns(setup_test_db, "player_season_stats")
|
||||
index_sets = self._get_index_columns(setup_test_db, "batting_season_stats")
|
||||
assert frozenset({"player_id", "team_id", "season"}) in index_sets
|
||||
|
||||
def test_index_on_team_season(self, setup_test_db):
|
||||
"""An index covering (team_id, season) exists."""
|
||||
index_sets = self._get_index_columns(setup_test_db, "player_season_stats")
|
||||
index_sets = self._get_index_columns(setup_test_db, "batting_season_stats")
|
||||
assert frozenset({"team_id", "season"}) in index_sets
|
||||
|
||||
def test_index_on_player_season(self, setup_test_db):
|
||||
"""An index covering (player_id, season) exists."""
|
||||
index_sets = self._get_index_columns(setup_test_db, "player_season_stats")
|
||||
index_sets = self._get_index_columns(setup_test_db, "batting_season_stats")
|
||||
assert frozenset({"player_id", "season"}) in index_sets
|
||||
|
||||
|
||||
class TestPitchingIndexExistence:
|
||||
"""Required indexes exist on pitching_season_stats."""
|
||||
|
||||
def _get_index_columns(self, db_conn, table):
|
||||
"""Return a set of frozensets, each being the column set of one index."""
|
||||
indexes = db_conn.execute_sql(f"PRAGMA index_list({table})").fetchall()
|
||||
result = set()
|
||||
for idx in indexes:
|
||||
idx_name = idx[1]
|
||||
cols = db_conn.execute_sql(f"PRAGMA index_info({idx_name})").fetchall()
|
||||
result.add(frozenset(col[2] for col in cols))
|
||||
return result
|
||||
|
||||
def test_unique_index_on_player_team_season(self, setup_test_db):
|
||||
"""A unique index covering (player_id, team_id, season) exists."""
|
||||
index_sets = self._get_index_columns(setup_test_db, "pitching_season_stats")
|
||||
assert frozenset({"player_id", "team_id", "season"}) in index_sets
|
||||
|
||||
def test_index_on_team_season(self, setup_test_db):
|
||||
"""An index covering (team_id, season) exists."""
|
||||
index_sets = self._get_index_columns(setup_test_db, "pitching_season_stats")
|
||||
assert frozenset({"team_id", "season"}) in index_sets
|
||||
|
||||
def test_index_on_player_season(self, setup_test_db):
|
||||
"""An index covering (player_id, season) exists."""
|
||||
index_sets = self._get_index_columns(setup_test_db, "pitching_season_stats")
|
||||
assert frozenset({"player_id", "season"}) in index_sets
|
||||
|
||||
Loading…
Reference in New Issue
Block a user