-- --------------------------------------------------------------- -- 014 — Data pipeline tables. -- odds_cache : last-seen odds per book/prop -- line_history : append-only line samples (drives steam detection + CLV) -- cascade_alerts : injury / lineup / ump-ref / weather deltas -- player_stats_cache : enricher payloads from nba_api, pybaseball, etc. -- grade_history : every grade issued + result (drives Ledger + CLV) -- prop_correlations : measured correlation between stat pairs -- player_id_map : cross-source player ID mapping -- -- RLS posture: -- Every table here is WRITTEN by the service role only (no policy = no -- row visible to anon/authenticated for INSERT/UPDATE/DELETE — service -- role bypasses RLS). Public-readable surfaces (grade_history) get a -- SELECT policy. Internal-only tables stay locked down. -- --------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.odds_cache ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), game_id TEXT NOT NULL, sport TEXT NOT NULL, player_id TEXT, player_name TEXT, stat_type TEXT, book TEXT NOT NULL, line NUMERIC, odds_over INTEGER, odds_under INTEGER, source TEXT NOT NULL, fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (game_id, COALESCE(player_id, ''), COALESCE(stat_type, ''), book) ); CREATE INDEX IF NOT EXISTS idx_odds_cache_game ON public.odds_cache (game_id, fetched_at DESC); CREATE INDEX IF NOT EXISTS idx_odds_cache_player ON public.odds_cache (player_id, stat_type); CREATE TABLE IF NOT EXISTS public.line_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), game_id TEXT NOT NULL, player_id TEXT, stat_type TEXT, book TEXT NOT NULL, line NUMERIC, odds INTEGER, recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_line_history_recent ON public.line_history (game_id, recorded_at DESC); CREATE INDEX IF NOT EXISTS idx_line_history_prop ON public.line_history (player_id, stat_type, recorded_at DESC); CREATE TABLE IF NOT EXISTS public.cascade_alerts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), trigger_type TEXT NOT NULL CHECK (trigger_type IN ( 'injury', 'lineup', 'weather', 'ref', 'umpire', 'manual' )), trigger_detail JSONB NOT NULL, affected_props JSONB NOT NULL DEFAULT '[]'::jsonb, affected_count INTEGER NOT NULL DEFAULT 0, resolved BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_cascade_recent ON public.cascade_alerts (created_at DESC) WHERE resolved = FALSE; CREATE TABLE IF NOT EXISTS public.player_stats_cache ( player_id TEXT NOT NULL, sport TEXT NOT NULL, bucket TEXT NOT NULL DEFAULT 'default', stats_data JSONB NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (player_id, sport, bucket) ); CREATE TABLE IF NOT EXISTS public.grade_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), player_id TEXT NOT NULL, player_name TEXT, sport TEXT NOT NULL, stat_type TEXT, line NUMERIC, direction TEXT CHECK (direction IN ('over', 'under')), grade TEXT, projection NUMERIC, modeled_prob NUMERIC, implied_prob NUMERIC, game_date DATE, result TEXT CHECK (result IN ('hit', 'miss', 'push', 'pending')), actual_value NUMERIC, graded_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_grade_history_player ON public.grade_history (player_id, game_date DESC); CREATE INDEX IF NOT EXISTS idx_grade_history_recent ON public.grade_history (graded_at DESC); CREATE TABLE IF NOT EXISTS public.prop_correlations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), player_id TEXT NOT NULL, stat_a TEXT NOT NULL, stat_b TEXT NOT NULL, correlation NUMERIC NOT NULL, sample_size INTEGER, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (player_id, stat_a, stat_b) ); CREATE TABLE IF NOT EXISTS public.player_id_map ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), display_name TEXT NOT NULL, sport TEXT NOT NULL, team TEXT, nba_id TEXT, espn_id TEXT, mlbam_id TEXT, nhl_id TEXT, nfl_gsis_id TEXT, ufc_id TEXT, headshot_url TEXT, active BOOLEAN NOT NULL DEFAULT TRUE, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (sport, display_name, team) ); CREATE INDEX IF NOT EXISTS idx_player_sport ON public.player_id_map (sport, active); CREATE INDEX IF NOT EXISTS idx_player_name ON public.player_id_map (display_name); -- ── RLS ──────────────────────────────────────────────────────────────────── ALTER TABLE public.odds_cache ENABLE ROW LEVEL SECURITY; ALTER TABLE public.line_history ENABLE ROW LEVEL SECURITY; ALTER TABLE public.cascade_alerts ENABLE ROW LEVEL SECURITY; ALTER TABLE public.player_stats_cache ENABLE ROW LEVEL SECURITY; ALTER TABLE public.grade_history ENABLE ROW LEVEL SECURITY; ALTER TABLE public.prop_correlations ENABLE ROW LEVEL SECURITY; ALTER TABLE public.player_id_map ENABLE ROW LEVEL SECURITY; -- Public-readable surfaces. The Ledger is published; correlation data and -- the player ID map are referenced by the UI for headshots + cross-source -- IDs. odds_cache / line_history / cascade_alerts / player_stats_cache stay -- locked to the service role. DROP POLICY IF EXISTS "Anyone can read grade history" ON public.grade_history; CREATE POLICY "Anyone can read grade history" ON public.grade_history FOR SELECT USING (TRUE); DROP POLICY IF EXISTS "Anyone can read prop correlations" ON public.prop_correlations; CREATE POLICY "Anyone can read prop correlations" ON public.prop_correlations FOR SELECT USING (TRUE); DROP POLICY IF EXISTS "Anyone can read player id map" ON public.player_id_map; CREATE POLICY "Anyone can read player id map" ON public.player_id_map FOR SELECT USING (TRUE); GRANT SELECT ON public.grade_history TO anon, authenticated; GRANT SELECT ON public.prop_correlations TO anon, authenticated; GRANT SELECT ON public.player_id_map TO anon, authenticated;