-- 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',

  primary key (sport, season, source_id)
);

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);
