-- Layer 1 — MECHANISM DATA (Session 68). -- Per-player-per-season Statcast aggregates. The HOT tier of the two-tier -- mechanism pattern: small, indexed, joined at classification time. Raw -- per-pitch stays out of Postgres on purpose (one season ~0.85 GB vs this -- table's ~2,300 rows) — see docs/MECHANISM-DATA.md. -- -- The table is SPORT-SCOPED from day one: NBA tracking and NFL Next Gen land -- in the same shape with a different `sport` and their own source ids. create table if not exists statcast_aggregates ( sport text not null default 'mlb', season int not null, -- Source-native player id (MLB: MLBAM). The join key — an integer both -- systems use natively, which is why the measured match rate is 100%. source_id bigint not null, -- Our canonical key. Nullable ON PURPOSE: a player Statcast has but we have -- never graded still gets stored, and joins later. player_key text, player_name text, role text not null check (role in ('batter','pitcher')), -- Handedness: pitchers from the movement feed, batters from the roster join. -- NULL = honest-absent, never guessed. throws text, bats text, -- Sample size — the gate for every downstream claim. sample_pa numeric, sample_ip numeric, sample_bip numeric, -- Plate discipline / contact quality (shared vocabulary, both roles). k_pct numeric, bb_pct numeric, whiff_pct numeric, swing_pct numeric, chase_pct numeric, barrel_pct numeric, hard_hit_pct numeric, -- Batter mechanism. avg_exit_velo numeric, max_exit_velo numeric, avg_launch_angle numeric, sweet_spot_pct numeric, ev95_pct numeric, -- Pitcher mechanism. arm_angle numeric, gb_pct numeric, fb_pct numeric, ld_pct numeric, pitch_mix jsonb, -- Everything else the feeds carry, verbatim. Layer 2/3 can reach a field we -- did not promote to a column WITHOUT a re-ingest. metrics jsonb not null default '{}'::jsonb, -- Freshness is a TRUTH property: the staleness alarm reads this, and no -- surface may present a stale aggregate as current. updated_at timestamptz not null default now(), source text not null default 'baseball_savant', -- ROLE is in the key: two-way players have a real batter profile AND a -- real pitcher profile (see migration 031). primary key (sport, season, source_id, role) ); create index if not exists statcast_agg_player_key_idx on statcast_aggregates (sport, season, player_key); create index if not exists statcast_agg_role_idx on statcast_aggregates (sport, season, role); create index if not exists statcast_agg_updated_idx on statcast_aggregates (updated_at desc); alter table statcast_aggregates enable row level security; -- Commodity public data: readable by anyone, written ONLY by the service role -- (the pipeline). Same posture as the public ledger rows. drop policy if exists statcast_agg_read on statcast_aggregates; create policy statcast_agg_read on statcast_aggregates for select using (true);