feat: PlayerSeasonStats Peewee model (#67) #82
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#82
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "ai/paper-dynasty-database#67"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #67
Summary
Implements WP-02:
PlayerSeasonStatsPeewee model — the materialized aggregate table updated incrementally after each game.Changes
app/db_engine.py: AddedPlayerSeasonStatsmodel afterDecision(since it holds a nullable FK toStratGame). Three indexes registered viaModelIndex:(player_id, team_id, season)— enforces one row per player-team-season(team_id, season)— team roster queries(player_id, season)— player career queriesapp/models/season_stats.py: Re-export module following the same pattern asapp/models/evolution.pytests/test_season_stats_model.py: 17 tests covering all acceptance criteria — all passColumn naming note
The spec lists
so (K)for pitching strikeouts. Sincesois already used for batting strikeouts in the same table, the pitching strikeout column is namedkto avoid a collision. WP-09 (formula engine) will need to accessstats.kfor pitcher strikeout counts rather thanstats.so.Test coverage
Other observations
_meta,id, and FK_idattributes throughoutdb_engine.pyand tests — same pattern as existing code, not introduced by this PR.AI Code Review
Files Reviewed
app/db_engine.py(modified — PlayerSeasonStats + Evolution models added)app/models/__init__.py(added — empty package init)app/models/season_stats.py(added — re-export module)app/models/evolution.py(added — re-export module)tests/__init__.py(added — empty package init)tests/conftest.py(added — shared test environment setup)tests/test_season_stats_model.py(added — 355 lines, 17 tests)tests/test_evolution_models.py(added — 338 lines, undisclosed scope)Findings
Correctness
PlayerSeasonStats — implementation is correct:
kcolumn for pitcher strikeouts is a reasonable workaround for thesocollision; the inline comment and PR body both document it clearly.ModelIndexregistrations are correct:UNIQUE(player, team, season), non-unique(team, season), non-unique(player, season). The registration pattern matches existing code (e.g.,decision_index).last_game = ForeignKeyField(StratGame, null=True)is correctly placed afterDecision(which holds the FK toStratGame).db.create_tablescall is correctly updated to includePlayerSeasonStats.Evolution models — appear structurally correct, but this PR provides no spec context, so correctness cannot be fully verified (see scope issue below).
Security
No issues. No user input handling, no credential exposure, no injection risk.
conftest.pyusesos.environ.setdefaultso it won't override a realPOSTGRES_PASSWORD— correct.Style & Conventions
conftest.pyapproach (settingDATABASE_TYPE=postgresqlbefore any app module is imported) is clean and prevents production SQLite mutation during test collection. Matches the project'sSKIP_TABLE_CREATIONgating pattern._test_db = SqliteDatabase(":memory:", ...)at module level withautouse=Trueper-test bind/drop is a standard Peewee testing pattern — acceptable for sequential pytest runs._TEST_MODELSis correct for both test modules.make_player/make_team/make_gamefixture helpers are well-structured and parameterised enough for reuse across test classes.Undisclosed Scope — blocking
The diff includes substantial work not mentioned in the PR title, body, or linked issue (#67):
EvolutionTrack,EvolutionCardState,EvolutionTierBoost,EvolutionCosmeticare added toapp/db_engine.pyafter theScoutClaimblock.app/models/evolution.pyre-export module is added.tests/test_evolution_models.py(338 lines, 20+ tests) is added.This appears to be WP-01 work mixed into a WP-02 PR. Without a spec reference or issue link it's impossible to verify:
EvolutionCardStateunique constraint on(player, team)— with noseasoncomponent — is intentional per the spec (compare:PlayerSeasonStatsunique on(player, team, season)).EvolutionTierBoost,EvolutionCosmetic) with only a single FK column each are complete or prematurely committed.EvolutionTrackmatch whatever spec WP-01 defines.Required action: Either (a) split the evolution models into their own PR with a proper description and issue reference, or (b) update this PR's title, body, and linked issue to explicitly cover WP-01 and provide the spec context needed to review the evolution schema.
Suggestions
test_last_game_fk_is_nullableinTestDeltaUpdatePatterntests both initial-null state and successful FK assignment. Consider splitting into two tests or renaming totest_last_game_fk_can_be_set.make_game(home_team=team, away_team=team)uses the same team for both sides — harmless for the test purpose but worth noting.Verdict: REQUEST_CHANGES
PlayerSeasonStats(WP-02) is correctly implemented with solid test coverage and goodconftest.pyinfrastructure. The blocking issue is the undisclosed addition of Evolution models (WP-01) — four new Peewee models, a re-export module, and 338 lines of tests — with no spec reference or issue link. The evolution schema cannot be properly reviewed without that context.Automated review by Claude PR Reviewer (posted as comment — Gitea self-review restriction applies)
Review Feedback Addressed
Removed all Evolution model (WP-01) code that was undisclosed in this PR:
app/db_engine.py: RemovedEvolutionTrack,EvolutionCardState,EvolutionTierBoost,EvolutionCosmeticclasses, their indexes, andcreate_tablescall.app/models/evolution.py: Deleted re-export module.tests/test_evolution_models.py: Deleted 338-line test file.This PR now contains only
PlayerSeasonStats(WP-02) as described in the title, body, and linked issue #67.The Evolution models should be submitted in a separate PR with a proper issue reference and spec context (particularly clarifying whether
EvolutionCardState's unique constraint on(player, team)— with no season component — is intentional).Commit:
8dfc5efAI Code Review
Files Reviewed
app/db_engine.py(modified —PlayerSeasonStatsmodel added)app/models/__init__.py(added — empty package init)app/models/season_stats.py(added — re-export module)tests/__init__.py(added — empty package init)tests/conftest.py(added — test DB configuration)tests/test_season_stats_model.py(added — 355 lines, 17 tests)Findings
Correctness
player,team) are correctly non-nullable;last_gameandlast_updated_atare correctly nullable.(player_id, team_id, season), non-unique on(team_id, season), non-unique on(player_id, season)— all match the PR description and follow the existingdecision_indexpattern.db.create_tables()updated correctly.PlayerSeasonStatsappended to the existing[StratGame, StratPlay, Decision]call — correct since it holds a nullable FK toStratGame._TEST_MODELSFK dependency order is correct.[Rarity, Event, Cardset, MlbPlayer, Player, Team, StratGame, PlayerSeasonStats]satisfies all FK constraints whenforeign_keys=1is enabled.conftest.pycorrectly prevents SQLite mutation.DATABASE_TYPE=postgresql→SKIP_TABLE_CREATION=Truebefore any app import; dummyPOSTGRES_PASSWORDpreventsPooledPostgresqlDatabaseinstantiation failure at collection time.so→kto avoid collision with battingsois well-documented in both the model and the PR body.Security
[REDACTED]regression, no injection surface.Style & Conventions
k = IntegerField(default=0)(closing paren on its own line) is slightly non-standard but harmless.BaseModel,table_name,safe=True).Suggestions
season_stats.pyfollows the pattern ofapp/models/evolution.py, but that file doesn't exist innext-release— it was part of an earlier version of this PR that was removed to address scope creep. The re-export pattern inseason_stats.pyis self-evident, so this is just a documentation nit; no code change needed.PRAGMA index_list/index_infoonly works in SQLite. The indexes will work correctly in production PostgreSQL (they're defined viaModelIndex), but the existence tests won't be portable if integration tests ever move to a real Postgres instance. Low priority.Verdict: APPROVED
Clean, focused diff — exactly the
PlayerSeasonStatsmodel described in issue #67/WP-02 and nothing more. Previous scope creep (evolution models) has been removed. Implementation is correct, tests are thorough, no security regressions.Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
app/db_engine.py(modified — model added betweenDecisionandScoutOpportunity)app/models/__init__.py(added — empty package init)app/models/season_stats.py(added — re-export module)tests/__init__.py(added — empty)tests/conftest.py(added — new test configuration)tests/test_season_stats_model.py(added — 17 tests)Findings
Correctness
Missing SQL migration (blocking). The production database runs PostgreSQL with
SKIP_TABLE_CREATION=True, which meansdb.create_tables()is never called in production. Theplayer_season_statstable will not exist in the production or dev PostgreSQL databases without a migration file. Every other table addition in this repo has a corresponding migration inmigrations/(see2026-03-04_add_scout_opportunities_claims.sqlfor the pattern). This PR needs a migration file, e.g.migrations/2026-03-13_add_player_season_stats.sql, before it can be safely merged and deployed.create_tablesgrouping is semantically broken. Indb_engine.py, the existing call at line 1053 onnext-releaseis:The PR modifies this to include
PlayerSeasonStatsin the same list. However,PlayerSeasonStatshas no FK relationship toStratGame,StratPlay, orDecision(only toPlayer,Team, andStratGamevialast_game). More importantly, it breaks the pattern where each logical group gets its owncreate_tablescall. The scout models get their own block;PlayerSeasonStatsshould too. SinceSKIP_TABLE_CREATIONisTruein production, this is cosmetic in prod but matters for SQLite dev environment clarity.Security
No issues found. No user input reaches the model definition layer. FK constraints are properly declared.
Style & Conventions
Multi-line
kfield declaration is inconsistent. All otherIntegerField(default=0)declarations in this model are single-line. Thekfield is formatted as:This formatting appears to have been done solely to attach a comment, but the result is inconsistent with the surrounding 24 other single-line integer field declarations. The comment belongs on the same line:
Test misclassification.
test_last_game_fk_is_nullable(line 299 intests/test_season_stats_model.py) is inTestDeltaUpdatePatternbut does not test any delta update operation — it only assertsrow.last_game_id is Noneon a freshly created row. This belongs inTestDefaultValues.Suggestions
app/models/__init__.pyis empty. Consider addingfrom .season_stats import PlayerSeasonStatsso the package has a public surface (from app.models import PlayerSeasonStats). This matches how other model packages tend to work and avoids consumers needing to know the submodule name.kcolumn naming decision is well-documented in the PR body and inline. Consider also adding a note inCLAUDE.mdunder the database section so future WP-09 implementers don't have to hunt through git history to learn that pitcher Ks arestats.k, notstats.so. The PR body already calls this out for WP-09 — a CLAUDE.md note ensures it survives context windows.Verdict: REQUEST_CHANGES
The model definition, indexes, defaults, test structure, and
season_stats.pyre-export are all solid. The 17 tests are thorough and theso/kcollision decision is well-reasoned. However, the missing SQL migration is a hard blocker: without it, theplayer_season_statstable will not exist on any PostgreSQL environment (dev or prod) and WP-09 and any downstream work will fail at the database layer on first use. Thekfield formatting inconsistency is a minor change-request that can be fixed in the same commit as the migration.Automated review by Claude PR Reviewer
@ -1053,0 +1073,4 @@# Pitching statsgames_pitching = IntegerField(default=0)outs = IntegerField(default=0)The
kfield multi-line format is inconsistent with every otherIntegerField(default=0)in this model. The comment can fit on one line:@ -1053,1 +1117,4 @@PlayerSeasonStats.add_index(pss_player_season_index)if not SKIP_TABLE_CREATION:PlayerSeasonStatsis appended to the existing[StratGame, StratPlay, Decision]create_tablesgroup. Per the project pattern (scout models get their owncreate_tablesblock), this should be a separate call:But more importantly: this line is moot in production since
SKIP_TABLE_CREATION=True. A SQL migration file is required.@ -0,0 +296,4 @@k=PlayerSeasonStats.k + 2,).where((PlayerSeasonStats.player == player)& (PlayerSeasonStats.team == team)This test only checks that
last_game_id is Noneon a fresh row — it does not test a delta update. It belongs inTestDefaultValuesalongsidetest_last_game_defaults_to_noneandtest_last_updated_at_defaults_to_none.