feat: add PlayerSeasonStats Peewee model (#67)

Closes #67

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-03-12 16:35:02 -05:00
parent a66ef9bd7c
commit 4bfd878486
8 changed files with 857 additions and 1 deletions

View File

@ -1050,8 +1050,75 @@ decision_index = ModelIndex(Decision, (Decision.game, Decision.pitcher), unique=
Decision.add_index(decision_index)
class PlayerSeasonStats(BaseModel):
player = ForeignKeyField(Player)
team = ForeignKeyField(Team)
season = IntegerField()
# Batting stats
games_batting = 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 = IntegerField(default=0)
rbi = IntegerField(default=0)
runs = IntegerField(default=0)
sb = IntegerField(default=0)
cs = IntegerField(default=0)
# Pitching stats
games_pitching = IntegerField(default=0)
outs = IntegerField(default=0)
k = IntegerField(
default=0
) # pitcher Ks; spec names this "so (K)" but renamed to avoid collision with batting so
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"
pss_unique_index = ModelIndex(
PlayerSeasonStats,
(PlayerSeasonStats.player, PlayerSeasonStats.team, PlayerSeasonStats.season),
unique=True,
)
PlayerSeasonStats.add_index(pss_unique_index)
pss_team_season_index = ModelIndex(
PlayerSeasonStats,
(PlayerSeasonStats.team, PlayerSeasonStats.season),
unique=False,
)
PlayerSeasonStats.add_index(pss_team_season_index)
pss_player_season_index = ModelIndex(
PlayerSeasonStats,
(PlayerSeasonStats.player, PlayerSeasonStats.season),
unique=False,
)
PlayerSeasonStats.add_index(pss_player_season_index)
if not SKIP_TABLE_CREATION:
db.create_tables([StratGame, StratPlay, Decision], safe=True)
db.create_tables([StratGame, StratPlay, Decision, PlayerSeasonStats], safe=True)
class ScoutOpportunity(BaseModel):
@ -1089,6 +1156,69 @@ if not SKIP_TABLE_CREATION:
db.create_tables([ScoutOpportunity, ScoutClaim], safe=True)
class EvolutionTrack(BaseModel):
name = CharField()
card_type = CharField() # batter / sp / rp
formula = CharField()
t1_threshold = IntegerField()
t2_threshold = IntegerField()
t3_threshold = IntegerField()
t4_threshold = IntegerField()
class Meta:
database = db
table_name = "evolution_track"
class EvolutionCardState(BaseModel):
player = ForeignKeyField(Player)
team = ForeignKeyField(Team)
track = ForeignKeyField(EvolutionTrack)
current_tier = IntegerField(default=0) # valid range: 04
current_value = FloatField(default=0.0)
fully_evolved = BooleanField(default=False)
last_evaluated_at = DateTimeField(null=True)
class Meta:
database = db
table_name = "evolution_card_state"
ecs_index = ModelIndex(
EvolutionCardState,
(EvolutionCardState.player, EvolutionCardState.team),
unique=True,
)
EvolutionCardState.add_index(ecs_index)
class EvolutionTierBoost(BaseModel):
"""Phase 2 stub — minimal model, schema to be defined in phase 2."""
card_state = ForeignKeyField(EvolutionCardState)
class Meta:
database = db
table_name = "evolution_tier_boost"
class EvolutionCosmetic(BaseModel):
"""Phase 2 stub — minimal model, schema to be defined in phase 2."""
card_state = ForeignKeyField(EvolutionCardState)
class Meta:
database = db
table_name = "evolution_cosmetic"
if not SKIP_TABLE_CREATION:
db.create_tables(
[EvolutionTrack, EvolutionCardState, EvolutionTierBoost, EvolutionCosmetic],
safe=True,
)
db.close()
# scout_db = SqliteDatabase(

0
app/models/__init__.py Normal file
View File

12
app/models/evolution.py Normal file
View File

@ -0,0 +1,12 @@
"""Evolution ORM models.
Models are defined in db_engine alongside all other Peewee models; this
module re-exports them so callers can import from `app.models.evolution`.
"""
from ..db_engine import ( # noqa: F401
EvolutionTrack,
EvolutionCardState,
EvolutionTierBoost,
EvolutionCosmetic,
)

View File

@ -0,0 +1,7 @@
"""PlayerSeasonStats ORM model.
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`.
"""
from ..db_engine import PlayerSeasonStats # noqa: F401

0
tests/__init__.py Normal file
View File

14
tests/conftest.py Normal file
View File

@ -0,0 +1,14 @@
"""Pytest configuration for the paper-dynasty-database test suite.
Sets DATABASE_TYPE=postgresql before any app module is imported so that
db_engine.py sets SKIP_TABLE_CREATION=True and does not try to mutate the
production SQLite file during test collection. Each test module is
responsible for binding models to its own in-memory database.
"""
import os
os.environ["DATABASE_TYPE"] = "postgresql"
# Provide dummy credentials so PooledPostgresqlDatabase can be instantiated
# without raising a configuration error (it will not actually be used).
os.environ.setdefault("POSTGRES_PASSWORD", "test-dummy")

View File

@ -0,0 +1,338 @@
"""Tests for evolution Peewee models (WP-01).
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, FK relationships, and unique constraints.
"""
import pytest
from peewee import SqliteDatabase, IntegrityError
from playhouse.shortcuts import model_to_dict
from app.models.evolution import (
EvolutionTrack,
EvolutionCardState,
EvolutionTierBoost,
EvolutionCosmetic,
)
from app.db_engine import Rarity, Event, Cardset, MlbPlayer, Player, Team
# All models that must exist in the test database (dependency order).
_TEST_MODELS = [
Rarity,
Event,
Cardset,
MlbPlayer,
Player,
Team,
EvolutionTrack,
EvolutionCardState,
EvolutionTierBoost,
EvolutionCosmetic,
]
_test_db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1})
@pytest.fixture(autouse=True)
def setup_test_db():
"""Bind all models to an in-memory SQLite database, create tables, and
tear them down after each test so each test starts from a clean state."""
_test_db.bind(_TEST_MODELS)
_test_db.create_tables(_TEST_MODELS)
yield _test_db
_test_db.drop_tables(list(reversed(_TEST_MODELS)), safe=True)
# ── Fixture helpers ────────────────────────────────────────────────────────
def make_rarity():
return Rarity.create(value=1, name="Common", color="#ffffff")
def make_cardset():
return Cardset.create(name="2025", description="2025 Season", total_cards=100)
def make_player(cardset, rarity):
return Player.create(
player_id=1,
p_name="Test Player",
cost=100,
image="test.png",
mlbclub="BOS",
franchise="Boston",
cardset=cardset,
set_num=1,
rarity=rarity,
pos_1="OF",
description="Test",
)
def make_team():
return Team.create(
abbrev="TEST",
sname="Test",
lname="Test Team",
gmid=123456789,
gmname="testuser",
gsheet="https://example.com",
wallet=1000,
team_value=1000,
collection_value=1000,
season=1,
)
def make_track(card_type="batter"):
return EvolutionTrack.create(
name="Batter",
card_type=card_type,
formula="pa+tb*2",
t1_threshold=37,
t2_threshold=149,
t3_threshold=448,
t4_threshold=896,
)
# ── Unit: model field validation ───────────────────────────────────────────
class TestEvolutionTrackFields:
"""model_to_dict works on unsaved EvolutionTrack instances and all fields
are accessible with the correct values."""
def test_model_to_dict_unsaved(self):
"""All EvolutionTrack fields appear in model_to_dict on an unsaved instance."""
track = EvolutionTrack(
name="Batter",
card_type="batter",
formula="pa+tb*2",
t1_threshold=37,
t2_threshold=149,
t3_threshold=448,
t4_threshold=896,
)
data = model_to_dict(track, recurse=False)
assert data["name"] == "Batter"
assert data["card_type"] == "batter"
assert data["formula"] == "pa+tb*2"
assert data["t1_threshold"] == 37
assert data["t2_threshold"] == 149
assert data["t3_threshold"] == 448
assert data["t4_threshold"] == 896
def test_all_threshold_fields_present(self):
"""EvolutionTrack exposes all four tier threshold columns."""
fields = EvolutionTrack._meta.fields
for col in ("t1_threshold", "t2_threshold", "t3_threshold", "t4_threshold"):
assert col in fields, f"Missing column: {col}"
class TestEvolutionCardStateFields:
"""model_to_dict works on unsaved EvolutionCardState instances and
default values match the spec."""
def test_model_to_dict_defaults(self):
"""Defaults: current_tier=0, current_value=0.0, fully_evolved=False,
last_evaluated_at=None."""
state = EvolutionCardState()
data = model_to_dict(state, recurse=False)
assert data["current_tier"] == 0
assert data["current_value"] == 0.0
assert data["fully_evolved"] is False
assert data["last_evaluated_at"] is None
def test_no_progress_since_field(self):
"""EvolutionCardState must not have a progress_since field (removed from spec)."""
assert "progress_since" not in EvolutionCardState._meta.fields
class TestEvolutionStubFields:
"""Phase 2 stub models are importable and respond to model_to_dict."""
def test_tier_boost_importable(self):
assert EvolutionTierBoost is not None
def test_cosmetic_importable(self):
assert EvolutionCosmetic is not None
def test_tier_boost_model_to_dict_unsaved(self):
"""model_to_dict on an unsaved EvolutionTierBoost returns a dict."""
data = model_to_dict(EvolutionTierBoost(), recurse=False)
assert isinstance(data, dict)
def test_cosmetic_model_to_dict_unsaved(self):
"""model_to_dict on an unsaved EvolutionCosmetic returns a dict."""
data = model_to_dict(EvolutionCosmetic(), recurse=False)
assert isinstance(data, dict)
# ── Unit: constraint definitions ──────────────────────────────────────────
class TestTierConstraints:
"""current_tier defaults to 0 and valid tier values (0-4) can be saved."""
def test_tier_zero_is_default(self):
"""EvolutionCardState.current_tier defaults to 0 on create."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
track = make_track()
state = EvolutionCardState.create(player=player, team=team, track=track)
assert state.current_tier == 0
def test_tier_four_is_valid(self):
"""Tier 4 (fully evolved cap) can be persisted without error."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
track = make_track()
state = EvolutionCardState.create(
player=player, team=team, track=track, current_tier=4
)
assert state.current_tier == 4
class TestUniqueConstraint:
"""Unique index on (player_id, team_id) is enforced at the DB level."""
def test_duplicate_player_team_raises(self):
"""A second EvolutionCardState for the same (player, team) raises IntegrityError,
even when a different track is used."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
track1 = make_track("batter")
track2 = EvolutionTrack.create(
name="SP",
card_type="sp",
formula="ip+k",
t1_threshold=10,
t2_threshold=40,
t3_threshold=120,
t4_threshold=240,
)
EvolutionCardState.create(player=player, team=team, track=track1)
with pytest.raises(IntegrityError):
EvolutionCardState.create(player=player, team=team, track=track2)
def test_same_player_different_teams_allowed(self):
"""One EvolutionCardState per team is allowed for the same player."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team1 = make_team()
team2 = Team.create(
abbrev="TM2",
sname="T2",
lname="Team Two",
gmid=987654321,
gmname="user2",
gsheet="https://example.com",
wallet=1000,
team_value=1000,
collection_value=1000,
season=1,
)
track = make_track()
EvolutionCardState.create(player=player, team=team1, track=track)
state2 = EvolutionCardState.create(player=player, team=team2, track=track)
assert state2.id is not None
# ── Integration: table creation ────────────────────────────────────────────
class TestTableCreation:
"""All four evolution tables are created in the test DB and are queryable."""
def test_evolution_track_table_exists(self):
assert EvolutionTrack.select().count() == 0
def test_evolution_card_state_table_exists(self):
assert EvolutionCardState.select().count() == 0
def test_evolution_tier_boost_table_exists(self):
assert EvolutionTierBoost.select().count() == 0
def test_evolution_cosmetic_table_exists(self):
assert EvolutionCosmetic.select().count() == 0
# ── Integration: FK enforcement ────────────────────────────────────────────
class TestFKEnforcement:
"""FK columns resolve to the correct related instances."""
def test_card_state_player_fk_resolves(self):
"""EvolutionCardState.player_id matches the Player we inserted."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
track = make_track()
state = EvolutionCardState.create(player=player, team=team, track=track)
fetched = EvolutionCardState.get_by_id(state.id)
assert fetched.player_id == player.player_id
def test_card_state_team_fk_resolves(self):
"""EvolutionCardState.team_id matches the Team we inserted."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
track = make_track()
state = EvolutionCardState.create(player=player, team=team, track=track)
fetched = EvolutionCardState.get_by_id(state.id)
assert fetched.team_id == team.id
def test_card_state_track_fk_resolves(self):
"""EvolutionCardState.track_id matches the EvolutionTrack we inserted."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
track = make_track()
state = EvolutionCardState.create(player=player, team=team, track=track)
fetched = EvolutionCardState.get_by_id(state.id)
assert fetched.track_id == track.id
# ── Integration: model_to_dict on saved instances ──────────────────────────
class TestModelToDictOnSaved:
"""model_to_dict() works correctly on saved instances of all four models."""
def test_evolution_track_saved(self):
"""Saved EvolutionTrack round-trips through model_to_dict correctly."""
track = make_track()
data = model_to_dict(track, recurse=False)
assert data["name"] == "Batter"
assert data["card_type"] == "batter"
assert data["formula"] == "pa+tb*2"
assert data["t1_threshold"] == 37
def test_evolution_card_state_saved(self):
"""Saved EvolutionCardState round-trips through model_to_dict correctly."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
track = make_track()
state = EvolutionCardState.create(
player=player, team=team, track=track, current_value=42.5, current_tier=2
)
data = model_to_dict(state, recurse=False)
assert data["current_value"] == 42.5
assert data["current_tier"] == 2
assert data["fully_evolved"] is False

View File

@ -0,0 +1,355 @@
"""Tests for PlayerSeasonStats Peewee model (WP-02).
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.db_engine import Rarity, Event, Cardset, MlbPlayer, Player, Team, StratGame
# Dependency order matters for FK resolution.
_TEST_MODELS = [
Rarity,
Event,
Cardset,
MlbPlayer,
Player,
Team,
StratGame,
PlayerSeasonStats,
]
_test_db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1})
@pytest.fixture(autouse=True)
def setup_test_db():
"""Bind all models to an in-memory SQLite database, create tables, and
tear them down after each test so each test starts from a clean state."""
_test_db.bind(_TEST_MODELS)
_test_db.create_tables(_TEST_MODELS)
yield _test_db
_test_db.drop_tables(list(reversed(_TEST_MODELS)), safe=True)
# ── Fixture helpers ─────────────────────────────────────────────────────────
def make_rarity():
return Rarity.create(value=1, name="Common", color="#ffffff")
def make_cardset():
return Cardset.create(name="2025", description="2025 Season", total_cards=100)
def make_player(cardset, rarity, player_id=1):
return Player.create(
player_id=player_id,
p_name="Test Player",
cost=100,
image="test.png",
mlbclub="BOS",
franchise="Boston",
cardset=cardset,
set_num=1,
rarity=rarity,
pos_1="OF",
description="Test",
)
def make_team(abbrev="TEST", gmid=123456789):
return Team.create(
abbrev=abbrev,
sname=abbrev,
lname=f"Team {abbrev}",
gmid=gmid,
gmname="testuser",
gsheet="https://example.com",
wallet=1000,
team_value=1000,
collection_value=1000,
season=1,
)
def make_game(home_team, away_team, season=10):
return StratGame.create(
season=season,
game_type="ranked",
away_team=away_team,
home_team=home_team,
)
def make_stats(player, team, season=10, **kwargs):
return PlayerSeasonStats.create(player=player, team=team, season=season, **kwargs)
# ── Unit: column completeness ────────────────────────────────────────────────
class TestColumnCompleteness:
"""All required columns are present in the model's field definitions."""
BATTING_COLS = [
"games_batting",
"pa",
"ab",
"hits",
"hr",
"doubles",
"triples",
"bb",
"hbp",
"so",
"rbi",
"runs",
"sb",
"cs",
]
PITCHING_COLS = [
"games_pitching",
"outs",
"k",
"bb_allowed",
"hits_allowed",
"hr_allowed",
"wins",
"losses",
"saves",
"holds",
"blown_saves",
]
META_COLS = ["last_game", "last_updated_at"]
KEY_COLS = ["player", "team", "season"]
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:
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
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
# ── Unit: default values ─────────────────────────────────────────────────────
class TestDefaultValues:
"""All integer stat columns default to 0; nullable meta fields default to None."""
INT_STAT_COLS = [
"games_batting",
"pa",
"ab",
"hits",
"hr",
"doubles",
"triples",
"bb",
"hbp",
"so",
"rbi",
"runs",
"sb",
"cs",
"games_pitching",
"outs",
"k",
"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()
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()
assert row.last_game_id is None
def test_last_updated_at_defaults_to_none(self):
"""last_updated_at defaults to None."""
row = PlayerSeasonStats()
assert row.last_updated_at is None
# ── Integration: unique constraint ───────────────────────────────────────────
class TestUniqueConstraint:
"""UNIQUE on (player_id, team_id, season) is enforced at the DB level."""
def test_duplicate_player_team_season_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)
with pytest.raises(IntegrityError):
make_stats(player, team, season=10)
def test_same_player_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)
assert row2.id is not None
def test_same_player_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)
assert row2.id is not None
# ── Integration: delta update pattern ───────────────────────────────────────
class TestDeltaUpdatePattern:
"""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."""
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)
PlayerSeasonStats.update(
pa=PlayerSeasonStats.pa + 3,
hits=PlayerSeasonStats.hits + 1,
).where(
(PlayerSeasonStats.player == player)
& (PlayerSeasonStats.team == team)
& (PlayerSeasonStats.season == 10)
).execute()
updated = PlayerSeasonStats.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 k 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, k=3)
PlayerSeasonStats.update(
outs=PlayerSeasonStats.outs + 6,
k=PlayerSeasonStats.k + 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.k == 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."""
rarity = make_rarity()
cardset = make_cardset()
player = make_player(cardset, rarity)
team = make_team()
row = make_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
).execute()
updated = PlayerSeasonStats.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."""
def _get_index_columns(self, db, table):
"""Return a set of frozensets, each being the column set of one index."""
indexes = db.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()
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")
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")
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")
assert frozenset({"player_id", "season"}) in index_sets