Files
vyndr/supabase/migrations/019_ledger_entries.sql
builtbykev 2c79373a3b Session 58: Phase 1 spec + ledger_entries migration (pre-apply commit)
Migration 019: the truth-infrastructure table. Committed BEFORE it runs,
per the Phase 1 GO instructions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:49:28 -04:00

79 lines
3.5 KiB
SQL

-- ---------------------------------------------------------------
-- 019 — ledger_entries (Session 58, work-order Phase 1 + CLV amendments).
--
-- The truth infrastructure: every grade — user scan AND pipeline pre-grade —
-- persists here, settles against the real result, and carries closing-line
-- value. Rows with user_id NULL are the PUBLIC model record.
--
-- DATA SEMANTICS: `line`, `locked_odds`, `book`, `closing_line`,
-- `closing_odds` are REAL book values captured at a timestamp — never
-- generated. `model_value` / grade / edge / confidence are VYNDR model
-- output. A row must never exist with a fabricated market value.
--
-- All writes go through the service role (pipeline + API). Clients only
-- read: their own rows, plus the public (user_id IS NULL) record.
-- ---------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.ledger_entries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NULL REFERENCES auth.users (id) ON DELETE CASCADE,
player_key text NOT NULL,
player_name text NOT NULL,
sport text NOT NULL,
stat text NOT NULL,
line numeric NOT NULL,
side text NOT NULL CHECK (side IN ('over', 'under')),
locked_odds text NULL,
book text NULL,
grade text NOT NULL,
edge numeric NULL,
confidence numeric NULL,
model_value numeric NULL,
graded_at timestamptz NOT NULL DEFAULT now(),
game_id text NOT NULL,
game_date date NOT NULL,
closing_line numeric NULL,
closing_odds text NULL,
clv numeric NULL,
clv_result text NULL CHECK (clv_result IN ('beat', 'faded', 'flat')),
outcome text NULL CHECK (outcome IN ('hit', 'miss', 'push')),
actual_value numeric NULL,
settled_at timestamptz NULL,
revised_from_grade text NULL
);
-- Idempotency: pipeline re-runs and double-taps upsert into this. PG15+
-- NULLS NOT DISTINCT makes the null-user (public record) rows dedupe too,
-- and — unlike a coalesce-expression index — PostgREST `on_conflict` can
-- target a real constraint's column list.
ALTER TABLE public.ledger_entries
ADD CONSTRAINT ledger_entries_dedupe
UNIQUE NULLS NOT DISTINCT (user_id, player_key, stat, line, side, game_id);
CREATE INDEX IF NOT EXISTS idx_ledger_user_graded
ON public.ledger_entries (user_id, graded_at DESC);
CREATE INDEX IF NOT EXISTS idx_ledger_gamedate_outcome
ON public.ledger_entries (game_date, outcome);
CREATE INDEX IF NOT EXISTS idx_ledger_player_stat
ON public.ledger_entries (player_key, stat);
CREATE INDEX IF NOT EXISTS idx_ledger_public_clv
ON public.ledger_entries (clv_result) WHERE user_id IS NULL;
-- The settlement worker's scan: unsettled rows by date.
CREATE INDEX IF NOT EXISTS idx_ledger_unsettled
ON public.ledger_entries (game_date) WHERE outcome IS NULL;
ALTER TABLE public.ledger_entries ENABLE ROW LEVEL SECURITY;
-- Users read their own rows.
CREATE POLICY ledger_select_own ON public.ledger_entries
FOR SELECT TO authenticated
USING (user_id = auth.uid());
-- Everyone (anon + authenticated) reads the public model record.
CREATE POLICY ledger_select_public ON public.ledger_entries
FOR SELECT TO anon, authenticated
USING (user_id IS NULL);
-- No INSERT/UPDATE/DELETE policies: client-side writes are impossible.
-- The pipeline + API write via the service role, which bypasses RLS.