-- Migration 039: wnba_player_game — the WNBA possession / usage / minutes feed. -- -- The chain's basketball chainFn is `usage x possessions x efficiency`. None of -- those three existed for WNBA anywhere in this codebase; this is the feed that -- makes the slot fillable. It is scoped to exactly those inputs plus the -- game-state the `redistribute` hook reads — NOT a general WNBA stats dump. -- (closing_captures grew to 4.2M rows of something nothing read; the lesson is -- to ingest for a named consumer or not at all.) -- -- ── POINT-IN-TIME IS STRUCTURAL, NOT A LATER RETROFIT ────────────────────── -- `statcast_aggregates` stores a SEASON AGGREGATE upserted in place, so every -- prior version is destroyed and a point-in-time question is unanswerable from -- it — which is why `statcast_history` had to be built afterwards, and why the -- skill backtest was honest only by accident. -- -- This stores PER GAME rows. A completed box score never changes, so an as-of -- profile is `WHERE game_date < asOf` — a filter over immutable facts, not a -- dated snapshot of a mutable aggregate. There is nothing to overwrite, so there -- is nothing to retain a history OF. -- -- STRICTLY `<`, never `<=`: a game played ON the as-of date may have tipped -- after grade time, and counting it would leak the evening being predicted into -- the prediction. Same rule as isPreGame in snapshotSettlementService. -- -- ── THE KEY ──────────────────────────────────────────────────────────────── -- (game_id, source_id) is unique: one row per player per game. `game_date` and -- `sport` lead the index because every read is date-bounded and single-sport, -- and safePaginate orders on the full tuple. Registered in src/utils/tableKeys.js -- — a stale entry there surfaces as a runtime duplicate-tuple throw. -- -- ── DERIVED vs SERVED ────────────────────────────────────────────────────── -- ESPN returns the COMPONENTS (minutes, FGA, FTA, TOV, OREB and team totals), -- not the rates. `usage_rate`, `team_possessions`, `team_pace`, `ts_pct` and -- `efg_pct` are computed here from the standard box-score identities and stored -- so a chainFn read is one query. Every component is stored ALONGSIDE its -- derived value, so each rate is re-derivable and checkable rather than an -- unfalsifiable number (the factorFreeze rule: store inputs, not just outputs). CREATE TABLE IF NOT EXISTS wnba_player_game ( sport text NOT NULL DEFAULT 'wnba', season integer, game_id text NOT NULL, game_date date NOT NULL, source_id text NOT NULL, player_key text NOT NULL, player_name text, team text, opponent text, home_away text, starter boolean, -- raw box components (the usage/possession inputs) minutes numeric, points integer, fgm integer, fga integer, fg3m integer, fg3a integer, ftm integer, fta integer, reb integer, oreb integer, dreb integer, ast integer, tov integer, stl integer, blk integer, pf integer, plus_minus integer, -- derived, per the identities in espnWnbaAdapter usage_rate numeric, ts_pct numeric, efg_pct numeric, -- team possession context, denormalised so a chainFn read is ONE query team_minutes numeric, team_fga integer, team_fta integer, team_tov integer, team_oreb integer, team_possessions numeric, team_pace numeric, -- game state — what the archetype `redistribute` hook reads (live in -- basketball: a blowout fades the star and feeds the bench) team_score integer, opp_score integer, final_margin integer, ingested_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (game_id, source_id) ); -- Every read is "this player, before this date" or "this slate's date range". CREATE INDEX IF NOT EXISTS wnba_player_game_asof_idx ON wnba_player_game (sport, player_key, game_date DESC); CREATE INDEX IF NOT EXISTS wnba_player_game_date_idx ON wnba_player_game (sport, game_date); COMMENT ON TABLE wnba_player_game IS 'WNBA per-player-per-game possession/usage/minutes feed for the chain basketball chainFn. PER-GAME grain makes point-in-time native: an as-of profile is WHERE game_date < asOf (strictly), over immutable completed box scores. Scoped to chainFn inputs + redistribute game-state.'; COMMENT ON COLUMN wnba_player_game.usage_rate IS 'DERIVED (not served by ESPN): 100*((FGA+0.44*FTA+TOV)*(TmMIN/5))/(MIN*(TmFGA+0.44*TmFTA+TmTOV)). Components stored alongside so it is re-derivable.'; COMMENT ON COLUMN wnba_player_game.team_possessions IS 'DERIVED: FGA - OREB + TOV + 0.44*FTA. The 0.44 free-throw-trip coefficient is the only estimated term.';