Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
-- BetonBLK Initial Schema
|
||||
-- VYNDR Initial Schema
|
||||
-- Feature 1.4 — All tables, indexes, RLS policies, triggers
|
||||
-- All timestamps use TIMESTAMPTZ (UTC)
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
-- Migration: 003_phase1_additions.sql
|
||||
-- Phase 1 additions: Role profiles, activations, predictions, joint outcomes, discrepancy scores
|
||||
-- Created: 2026-03-28
|
||||
|
||||
-- ============================================================
|
||||
-- Table 1: player_role_profiles
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS player_role_profiles (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
player_id TEXT NOT NULL,
|
||||
player_name TEXT NOT NULL,
|
||||
sport TEXT NOT NULL DEFAULT 'NBA',
|
||||
team TEXT,
|
||||
role_profile JSONB NOT NULL DEFAULT '{}',
|
||||
conditional_roles JSONB DEFAULT '{}',
|
||||
dominant_role TEXT,
|
||||
role_variance_score NUMERIC(4,3) DEFAULT 0,
|
||||
effective_date DATE DEFAULT CURRENT_DATE,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 2: lineup_role_profiles
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS lineup_role_profiles (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
lineup_id TEXT NOT NULL,
|
||||
player_id TEXT NOT NULL,
|
||||
sport TEXT NOT NULL DEFAULT 'NBA',
|
||||
team TEXT,
|
||||
role_in_this_lineup TEXT,
|
||||
historical_production JSONB DEFAULT '{}',
|
||||
instances_tracked INTEGER DEFAULT 0,
|
||||
last_game_date DATE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 3: player_role_activations
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS player_role_activations (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
game_id TEXT,
|
||||
player_id TEXT NOT NULL,
|
||||
activated_role TEXT NOT NULL,
|
||||
activation_trigger TEXT,
|
||||
activation_timestamp TIMESTAMPTZ DEFAULT NOW(),
|
||||
pre_activation_line NUMERIC(6,2),
|
||||
post_activation_line NUMERIC(6,2),
|
||||
line_movement_after NUMERIC(6,2),
|
||||
actual_result NUMERIC(6,2),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 4: model_predictions_extended
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS model_predictions_extended (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
prediction_id UUID REFERENCES picks(id),
|
||||
role_stability_score NUMERIC(4,3),
|
||||
role_variance_score NUMERIC(4,3),
|
||||
active_role_tonight TEXT,
|
||||
elevation_detected BOOLEAN DEFAULT FALSE,
|
||||
evolution_detected BOOLEAN DEFAULT FALSE,
|
||||
evolution_confidence NUMERIC(4,3),
|
||||
similarity_instances_found INTEGER DEFAULT 0,
|
||||
model_probability NUMERIC(5,4),
|
||||
confidence_interval_low NUMERIC(6,2),
|
||||
confidence_interval_high NUMERIC(6,2),
|
||||
confidence_interval_asymmetric BOOLEAN DEFAULT FALSE,
|
||||
optimal_alt_line NUMERIC(6,2),
|
||||
optimal_alt_line_edge NUMERIC(5,4),
|
||||
discrepancy_window_flag BOOLEAN DEFAULT FALSE,
|
||||
clv_at_prediction NUMERIC(6,2),
|
||||
clv_at_24hr NUMERIC(6,2),
|
||||
clv_at_tip NUMERIC(6,2),
|
||||
model_version TEXT DEFAULT '1.0',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 5: prediction_registry
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS prediction_registry (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
prediction_type TEXT NOT NULL,
|
||||
player_id TEXT NOT NULL,
|
||||
sport TEXT NOT NULL,
|
||||
stat_type TEXT NOT NULL,
|
||||
prediction_detail JSONB NOT NULL,
|
||||
registered_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
outcome TEXT DEFAULT 'pending' CHECK (outcome IN ('pending', 'verified', 'false')),
|
||||
verified_at TIMESTAMPTZ,
|
||||
public_url TEXT
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 6: joint_outcomes
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS joint_outcomes (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
game_id TEXT,
|
||||
player_a_id TEXT NOT NULL,
|
||||
player_a_stat TEXT NOT NULL,
|
||||
player_a_result NUMERIC(6,2),
|
||||
player_b_id TEXT NOT NULL,
|
||||
player_b_stat TEXT NOT NULL,
|
||||
player_b_result NUMERIC(6,2),
|
||||
phi_coefficient NUMERIC(5,4),
|
||||
sample_size INTEGER DEFAULT 0,
|
||||
game_state_at_outcome TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 7: discrepancy_reliability_scores
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS discrepancy_reliability_scores (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
prop_type TEXT NOT NULL,
|
||||
sport TEXT NOT NULL,
|
||||
reliability_score NUMERIC(4,3),
|
||||
avg_resolution_minutes NUMERIC(6,1),
|
||||
sample_size INTEGER DEFAULT 0,
|
||||
last_updated TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Indexes
|
||||
-- ============================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_role_profiles_player ON player_role_profiles(player_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_profiles_sport_team ON player_role_profiles(sport, team);
|
||||
CREATE INDEX IF NOT EXISTS idx_lineup_roles_player ON lineup_role_profiles(player_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lineup_roles_lineup ON lineup_role_profiles(lineup_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_activations_player ON player_role_activations(player_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_activations_game ON player_role_activations(game_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_predictions_ext_prediction ON model_predictions_extended(prediction_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_prediction_registry_player ON prediction_registry(player_id, sport);
|
||||
CREATE INDEX IF NOT EXISTS idx_prediction_registry_outcome ON prediction_registry(outcome);
|
||||
CREATE INDEX IF NOT EXISTS idx_joint_outcomes_players ON joint_outcomes(player_a_id, player_b_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_joint_outcomes_game ON joint_outcomes(game_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_discrepancy_scores_prop ON discrepancy_reliability_scores(prop_type, sport);
|
||||
|
||||
-- ============================================================
|
||||
-- Row Level Security
|
||||
-- ============================================================
|
||||
ALTER TABLE player_role_profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE lineup_role_profiles ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE player_role_activations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE model_predictions_extended ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE prediction_registry ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE joint_outcomes ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE discrepancy_reliability_scores ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Public read on prediction_registry
|
||||
CREATE POLICY "prediction_registry_public_read" ON prediction_registry
|
||||
FOR SELECT USING (true);
|
||||
|
||||
-- Service role write on prediction_registry
|
||||
CREATE POLICY "prediction_registry_service_write" ON prediction_registry
|
||||
FOR INSERT WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- Service role full access on all other tables
|
||||
CREATE POLICY "role_profiles_service_access" ON player_role_profiles
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY "lineup_roles_service_access" ON lineup_role_profiles
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY "role_activations_service_access" ON player_role_activations
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY "predictions_ext_service_access" ON model_predictions_extended
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY "joint_outcomes_service_access" ON joint_outcomes
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY "discrepancy_scores_service_access" ON discrepancy_reliability_scores
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,112 @@
|
||||
-- Migration: 004_affiliate_tables.sql
|
||||
-- Affiliate system: referral codes, conversions, payouts, wallets
|
||||
-- Created: 2026-04-12
|
||||
|
||||
-- ============================================================
|
||||
-- Table 1: referral_codes
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS referral_codes (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
owner_user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
affiliate_tier TEXT NOT NULL DEFAULT 'standard' CHECK (affiliate_tier IN ('standard', 'premium', 'founder')),
|
||||
commission_pct NUMERIC(5,2) NOT NULL DEFAULT 15.00,
|
||||
max_uses INTEGER,
|
||||
current_uses INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
expires_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 2: wallet_addresses (before payouts — FK dependency)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS wallet_addresses (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
chain TEXT NOT NULL CHECK (chain IN ('ethereum', 'polygon', 'solana', 'bitcoin', 'base', 'arbitrum')),
|
||||
address TEXT NOT NULL,
|
||||
label TEXT,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 3: referral_conversions
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS referral_conversions (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
referral_code_id UUID NOT NULL REFERENCES referral_codes(id),
|
||||
referred_user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
referrer_user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
conversion_type TEXT NOT NULL CHECK (conversion_type IN ('signup', 'upgrade', 'renewal')),
|
||||
subscription_tier TEXT NOT NULL CHECK (subscription_tier IN ('analyst', 'desk')),
|
||||
revenue_amount NUMERIC(10,2) NOT NULL DEFAULT 0,
|
||||
commission_amount NUMERIC(10,2) NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'paid', 'disputed', 'refunded')),
|
||||
converted_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
confirmed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Table 4: affiliate_payouts
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS affiliate_payouts (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
affiliate_user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
payout_amount NUMERIC(10,2) NOT NULL,
|
||||
payout_method TEXT NOT NULL CHECK (payout_method IN ('crypto', 'paypal', 'bank_transfer', 'stripe')),
|
||||
wallet_address_id UUID REFERENCES wallet_addresses(id),
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
||||
transaction_hash TEXT,
|
||||
period_start DATE NOT NULL,
|
||||
period_end DATE NOT NULL,
|
||||
conversions_included INTEGER NOT NULL DEFAULT 0,
|
||||
requested_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
processed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Indexes
|
||||
-- ============================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_codes_owner ON referral_codes(owner_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_codes_code ON referral_codes(code);
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_conversions_code ON referral_conversions(referral_code_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_conversions_referrer ON referral_conversions(referrer_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_conversions_referred ON referral_conversions(referred_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_referral_conversions_status ON referral_conversions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_affiliate_payouts_user ON affiliate_payouts(affiliate_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_affiliate_payouts_status ON affiliate_payouts(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_wallet_addresses_user ON wallet_addresses(user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Row Level Security
|
||||
-- ============================================================
|
||||
ALTER TABLE referral_codes ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE referral_conversions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE affiliate_payouts ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE wallet_addresses ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- referral_codes: public read, service role full access
|
||||
CREATE POLICY "referral_codes_public_read" ON referral_codes
|
||||
FOR SELECT USING (true);
|
||||
|
||||
CREATE POLICY "referral_codes_service_write" ON referral_codes
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
|
||||
-- referral_conversions: service role full access only
|
||||
CREATE POLICY "referral_conversions_service_access" ON referral_conversions
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
|
||||
-- affiliate_payouts: service role full access only
|
||||
CREATE POLICY "affiliate_payouts_service_access" ON affiliate_payouts
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
|
||||
-- wallet_addresses: service role full access only
|
||||
CREATE POLICY "wallet_addresses_service_access" ON wallet_addresses
|
||||
FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Migration: 005_lineup_scheme_data.sql
|
||||
-- Lineup scheme data for nightly play-by-play collection.
|
||||
-- Created: 2026-04-13
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lineup_scheme_data (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
game_id TEXT NOT NULL,
|
||||
game_date DATE NOT NULL,
|
||||
team_id TEXT NOT NULL,
|
||||
lineup_hash TEXT NOT NULL,
|
||||
player_ids TEXT[] NOT NULL,
|
||||
play_type TEXT NOT NULL,
|
||||
possessions INTEGER DEFAULT 0,
|
||||
points INTEGER DEFAULT 0,
|
||||
fg_made INTEGER DEFAULT 0,
|
||||
fg_attempted INTEGER DEFAULT 0,
|
||||
turnovers INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ls_team ON lineup_scheme_data(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ls_hash ON lineup_scheme_data(lineup_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_ls_date ON lineup_scheme_data(game_date);
|
||||
|
||||
ALTER TABLE lineup_scheme_data ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY ls_svc ON lineup_scheme_data FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,129 @@
|
||||
-- Migration: 006_data_warehouse_calibration.sql
|
||||
-- Data cache tables, grade_outcomes (all ship-version columns),
|
||||
-- player_calibrated_weights.
|
||||
-- Created: 2026-04-13
|
||||
|
||||
-- ============================================================
|
||||
-- NBA Data Cache
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS nba_data_cache (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
cache_key TEXT NOT NULL UNIQUE,
|
||||
data JSONB NOT NULL,
|
||||
fetched_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
ttl_hours INTEGER DEFAULT 6
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_nba_ck ON nba_data_cache(cache_key);
|
||||
|
||||
-- ============================================================
|
||||
-- MLB Data Cache
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS mlb_data_cache (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
cache_key TEXT NOT NULL UNIQUE,
|
||||
data JSONB NOT NULL,
|
||||
fetched_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
ttl_hours INTEGER DEFAULT 24
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_mlb_ck ON mlb_data_cache(cache_key);
|
||||
|
||||
-- ============================================================
|
||||
-- Grade Outcomes — ALL ship-version columns
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS grade_outcomes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport TEXT NOT NULL,
|
||||
player_id TEXT NOT NULL,
|
||||
player_name TEXT,
|
||||
stat_type TEXT NOT NULL,
|
||||
prop_line DECIMAL NOT NULL,
|
||||
over_under TEXT NOT NULL,
|
||||
grade TEXT,
|
||||
abstained BOOLEAN DEFAULT FALSE,
|
||||
abstention_reason TEXT,
|
||||
confidence DECIMAL,
|
||||
global_offset_applied DECIMAL DEFAULT 0.0,
|
||||
projected_value DECIMAL,
|
||||
projected_std DECIMAL,
|
||||
actual_value DECIMAL,
|
||||
hit BOOLEAN,
|
||||
real_edge DECIMAL,
|
||||
implied_probability DECIMAL,
|
||||
ev_per_dollar DECIMAL,
|
||||
kelly_recommended_pct DECIMAL,
|
||||
sub_scores JSONB NOT NULL,
|
||||
player_weights JSONB NOT NULL,
|
||||
bayesian_weights_used JSONB,
|
||||
similar_game_count INTEGER,
|
||||
similar_game_modifier DECIMAL,
|
||||
archetype_scores JSONB,
|
||||
player_profile_snapshot JSONB,
|
||||
pitcher_profile_snapshot JSONB,
|
||||
matchup_context JSONB,
|
||||
game_context JSONB,
|
||||
weather_context JSONB,
|
||||
teammate_context JSONB,
|
||||
bullpen_context JSONB,
|
||||
lineup_protection_context JSONB,
|
||||
home_road_context JSONB,
|
||||
day_night_context JSONB,
|
||||
opponent_quality_context JSONB,
|
||||
travel_fatigue_context JSONB,
|
||||
regime_check JSONB,
|
||||
lineup_status TEXT,
|
||||
lineup_source TEXT,
|
||||
line_source TEXT,
|
||||
american_odds INTEGER,
|
||||
bookmaker TEXT,
|
||||
clv_opening_line DECIMAL,
|
||||
clv_closing_line DECIMAL,
|
||||
clv_movement DECIMAL,
|
||||
clv_win BOOLEAN,
|
||||
market_alignment TEXT,
|
||||
discipline_score DECIMAL,
|
||||
umpire_id TEXT,
|
||||
umpire_adjustment DECIMAL,
|
||||
referee_crew_id TEXT,
|
||||
referee_adjustment DECIMAL,
|
||||
pick_number INTEGER,
|
||||
capper_post_text TEXT,
|
||||
game_id TEXT,
|
||||
game_date DATE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
resolved_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_go_player ON grade_outcomes(player_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_go_sport ON grade_outcomes(sport);
|
||||
CREATE INDEX IF NOT EXISTS idx_go_date ON grade_outcomes(game_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_go_resolved ON grade_outcomes(resolved_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_go_grade ON grade_outcomes(grade);
|
||||
CREATE INDEX IF NOT EXISTS idx_go_abstained ON grade_outcomes(abstained);
|
||||
|
||||
-- ============================================================
|
||||
-- Player Calibrated Weights
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS player_calibrated_weights (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
player_id TEXT NOT NULL,
|
||||
sport TEXT NOT NULL,
|
||||
stat_type TEXT NOT NULL,
|
||||
weights JSONB NOT NULL,
|
||||
sample_size INTEGER NOT NULL,
|
||||
calibrated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(player_id, sport, stat_type)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pcw_player ON player_calibrated_weights(player_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Row Level Security
|
||||
-- ============================================================
|
||||
ALTER TABLE nba_data_cache ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE mlb_data_cache ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE grade_outcomes ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE player_calibrated_weights ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY nba_c_svc ON nba_data_cache FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY mlb_c_svc ON mlb_data_cache FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY go_svc ON grade_outcomes FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY pcw_svc ON player_calibrated_weights FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,172 @@
|
||||
-- Migration: 007_lineup_odds_trust_health.sql
|
||||
-- Lineup updates, reporter trust, odds warehouse, line movements,
|
||||
-- reporter-line correlation, API health log, global calibration, joint outcomes.
|
||||
-- Created: 2026-04-13
|
||||
|
||||
-- ============================================================
|
||||
-- Lineup Updates
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS lineup_updates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport TEXT NOT NULL,
|
||||
team_id TEXT NOT NULL,
|
||||
player_name TEXT NOT NULL,
|
||||
player_id TEXT,
|
||||
status TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
trust_level TEXT NOT NULL,
|
||||
reporter_handle TEXT,
|
||||
confidence DECIMAL,
|
||||
raw_data JSONB,
|
||||
game_date DATE NOT NULL,
|
||||
detected_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_lu_team ON lineup_updates(team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lu_date ON lineup_updates(game_date);
|
||||
|
||||
-- ============================================================
|
||||
-- Reporter Trust
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS reporter_trust (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
handle TEXT NOT NULL UNIQUE,
|
||||
sport TEXT NOT NULL,
|
||||
team_id TEXT NOT NULL,
|
||||
outlet TEXT,
|
||||
source_type TEXT NOT NULL DEFAULT 'beat_writer',
|
||||
trust_level TEXT DEFAULT 'unverified',
|
||||
starting_trust TEXT,
|
||||
total_tracked INTEGER DEFAULT 0,
|
||||
correct_count INTEGER DEFAULT 0,
|
||||
accuracy DECIMAL DEFAULT 0.0,
|
||||
last_tracked_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_rt_handle ON reporter_trust(handle);
|
||||
CREATE INDEX IF NOT EXISTS idx_rt_trust ON reporter_trust(trust_level);
|
||||
|
||||
-- ============================================================
|
||||
-- Odds Warehouse
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS odds_warehouse (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport TEXT NOT NULL,
|
||||
game_id TEXT NOT NULL,
|
||||
game_date DATE NOT NULL,
|
||||
player_name TEXT NOT NULL,
|
||||
player_id TEXT,
|
||||
market TEXT NOT NULL,
|
||||
bookmaker TEXT NOT NULL,
|
||||
line DECIMAL,
|
||||
price INTEGER,
|
||||
over_under TEXT,
|
||||
scan_type TEXT NOT NULL,
|
||||
fetched_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ow_sport_date ON odds_warehouse(sport, game_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_ow_player ON odds_warehouse(player_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_ow_scan ON odds_warehouse(scan_type);
|
||||
|
||||
-- ============================================================
|
||||
-- Line Movements (ship version — new table, not conflicting with existing)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS ship_line_movements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport TEXT NOT NULL,
|
||||
player_name TEXT NOT NULL,
|
||||
market TEXT NOT NULL,
|
||||
bookmaker TEXT NOT NULL,
|
||||
opening_line DECIMAL,
|
||||
current_line DECIMAL,
|
||||
movement DECIMAL,
|
||||
direction TEXT,
|
||||
game_date DATE NOT NULL,
|
||||
detected_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_slm_sport_date ON ship_line_movements(sport, game_date);
|
||||
|
||||
-- ============================================================
|
||||
-- Reporter-Line Correlation
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS reporter_line_correlation (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reporter_handle TEXT NOT NULL,
|
||||
sport TEXT NOT NULL,
|
||||
player_name TEXT NOT NULL,
|
||||
tweet_content TEXT,
|
||||
tweet_time TIMESTAMPTZ NOT NULL,
|
||||
line_movement_time TIMESTAMPTZ,
|
||||
gap_minutes DECIMAL,
|
||||
prop_affected TEXT,
|
||||
line_before DECIMAL,
|
||||
line_after DECIMAL,
|
||||
bookmaker TEXT,
|
||||
game_date DATE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_rlc_reporter ON reporter_line_correlation(reporter_handle);
|
||||
CREATE INDEX IF NOT EXISTS idx_rlc_date ON reporter_line_correlation(game_date);
|
||||
|
||||
-- ============================================================
|
||||
-- API Health Log
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS api_health_log (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
api_name TEXT NOT NULL,
|
||||
error_message TEXT,
|
||||
games_tonight INTEGER DEFAULT 0,
|
||||
failed_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ahl_api ON api_health_log(api_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_ahl_time ON api_health_log(failed_at);
|
||||
|
||||
-- ============================================================
|
||||
-- Global Calibration
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS global_calibration (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport TEXT NOT NULL UNIQUE,
|
||||
offset_value DECIMAL DEFAULT 0.0,
|
||||
brier_score DECIMAL,
|
||||
sample_size INTEGER DEFAULT 0,
|
||||
blind_spots JSONB,
|
||||
calculated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Joint Outcomes (ship version)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS ship_joint_outcomes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
player_a_id TEXT NOT NULL,
|
||||
player_b_id TEXT NOT NULL,
|
||||
stat_a TEXT NOT NULL,
|
||||
stat_b TEXT NOT NULL,
|
||||
hit_a BOOLEAN NOT NULL,
|
||||
hit_b BOOLEAN NOT NULL,
|
||||
game_date DATE NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sjo_players ON ship_joint_outcomes(player_a_id, player_b_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sjo_date ON ship_joint_outcomes(game_date);
|
||||
|
||||
-- ============================================================
|
||||
-- Row Level Security
|
||||
-- ============================================================
|
||||
ALTER TABLE lineup_updates ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE reporter_trust ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE odds_warehouse ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE ship_line_movements ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE reporter_line_correlation ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE api_health_log ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE global_calibration ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE ship_joint_outcomes ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY lu_svc ON lineup_updates FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY rt_svc ON reporter_trust FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY ow_svc ON odds_warehouse FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY slm_svc ON ship_line_movements FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY rlc_svc ON reporter_line_correlation FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY ahl_svc ON api_health_log FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY gc_svc ON global_calibration FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY sjo_svc ON ship_joint_outcomes FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,82 @@
|
||||
-- Migration: 008_supplement_tables.sql
|
||||
-- Supplement intelligence systems: coaching tendencies, player-out history,
|
||||
-- evolution detections, unconventional validations.
|
||||
-- Created: 2026-04-13
|
||||
|
||||
-- ============================================================
|
||||
-- Coaching Tendencies
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS coaching_tendencies (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
coach_id TEXT NOT NULL,
|
||||
team_id TEXT NOT NULL,
|
||||
sport TEXT NOT NULL,
|
||||
season TEXT NOT NULL,
|
||||
tendencies JSONB NOT NULL,
|
||||
games_sampled INTEGER DEFAULT 0,
|
||||
last_updated DATE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(coach_id, team_id, sport, season)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ct_coach ON coaching_tendencies(coach_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ct_team ON coaching_tendencies(team_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Player-Out History (redistribution training data)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS player_out_history (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
player_out_id TEXT NOT NULL,
|
||||
team_id TEXT NOT NULL,
|
||||
game_id TEXT NOT NULL,
|
||||
game_date DATE NOT NULL,
|
||||
beneficiary_stats JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_poh_player ON player_out_history(player_out_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_poh_team ON player_out_history(team_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Evolution Detections (timestamped accuracy ledger)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS evolution_detections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
player_id TEXT NOT NULL,
|
||||
player_name TEXT,
|
||||
sport TEXT NOT NULL,
|
||||
detection_date DATE NOT NULL,
|
||||
metrics JSONB NOT NULL,
|
||||
market_adjusted_at DATE,
|
||||
confirmed BOOLEAN,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ed_player ON evolution_detections(player_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ed_date ON evolution_detections(detection_date);
|
||||
|
||||
-- ============================================================
|
||||
-- Unconventional Factor Validation Log
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS unconventional_validations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
factor_name TEXT NOT NULL,
|
||||
validated BOOLEAN NOT NULL,
|
||||
pearson_r DECIMAL,
|
||||
p_value DECIMAL,
|
||||
corrected_alpha DECIMAL,
|
||||
sample_size INTEGER,
|
||||
validated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_uv_factor ON unconventional_validations(factor_name);
|
||||
|
||||
-- ============================================================
|
||||
-- Row Level Security
|
||||
-- ============================================================
|
||||
ALTER TABLE coaching_tendencies ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE player_out_history ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE evolution_detections ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE unconventional_validations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY ct_svc ON coaching_tendencies FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY poh_svc ON player_out_history FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY ed_svc ON evolution_detections FOR ALL USING (auth.role() = 'service_role');
|
||||
CREATE POLICY uv_svc ON unconventional_validations FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Migration: 009_patch_supplement.sql
|
||||
-- Supplement context columns on grade_outcomes + unconventional_factor_data table.
|
||||
-- Created: 2026-04-13
|
||||
|
||||
-- ============================================================
|
||||
-- Add supplement columns to grade_outcomes
|
||||
-- ============================================================
|
||||
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS coaching_context JSONB;
|
||||
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS redistribution_context JSONB;
|
||||
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS evolution_flag BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS alt_line_opportunity JSONB;
|
||||
ALTER TABLE grade_outcomes ADD COLUMN IF NOT EXISTS unconventional_factors JSONB;
|
||||
|
||||
-- ============================================================
|
||||
-- Unconventional Factor Data (daily collection for validation)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS unconventional_factor_data (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
factor_name TEXT NOT NULL,
|
||||
game_id TEXT NOT NULL,
|
||||
game_date DATE NOT NULL,
|
||||
factor_value JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ufd_factor ON unconventional_factor_data(factor_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_ufd_date ON unconventional_factor_data(game_date);
|
||||
ALTER TABLE unconventional_factor_data ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY ufd_svc ON unconventional_factor_data FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Migration: 010_security_events.sql
|
||||
-- Security events table for audit logging.
|
||||
-- Created: 2026-04-13
|
||||
|
||||
CREATE TABLE IF NOT EXISTS security_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_type TEXT NOT NULL,
|
||||
ip_address TEXT,
|
||||
path TEXT,
|
||||
detail TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_se_type ON security_events(event_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_se_time ON security_events(created_at);
|
||||
ALTER TABLE security_events ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY se_svc ON security_events FOR ALL USING (auth.role() = 'service_role');
|
||||
@@ -0,0 +1,164 @@
|
||||
-- VYNDR Web — user profile + scan throttle + parlay leg frequency
|
||||
-- Idempotent. Safe to apply multiple times.
|
||||
|
||||
create extension if not exists "pgcrypto";
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- user_profiles
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
create table if not exists public.user_profiles (
|
||||
id uuid primary key references auth.users(id) on delete cascade,
|
||||
email text,
|
||||
tier text not null default 'free' check (tier in ('free', 'analyst', 'desk')),
|
||||
scan_count integer not null default 0,
|
||||
scan_reset_date date not null default current_date,
|
||||
subscription_start timestamptz,
|
||||
subscription_end timestamptz,
|
||||
subscription_status text not null default 'none' check (subscription_status in ('none','active','grace_period','expired','canceled')),
|
||||
cancel_at_period_end boolean not null default false,
|
||||
founder_pricing boolean not null default false,
|
||||
age_verified boolean not null default false,
|
||||
nexapay_customer_id text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_user_profiles_tier on public.user_profiles(tier);
|
||||
create index if not exists idx_user_profiles_subscription_end on public.user_profiles(subscription_end);
|
||||
|
||||
-- Auto-create profile on auth.users insert
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.user_profiles (id, email)
|
||||
values (new.id, new.email)
|
||||
on conflict (id) do nothing;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer set search_path = public;
|
||||
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
-- updated_at maintenance
|
||||
create or replace function public.touch_updated_at()
|
||||
returns trigger as $$
|
||||
begin
|
||||
new.updated_at := now();
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
drop trigger if exists set_user_profiles_updated_at on public.user_profiles;
|
||||
create trigger set_user_profiles_updated_at
|
||||
before update on public.user_profiles
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
-- RLS
|
||||
alter table public.user_profiles enable row level security;
|
||||
|
||||
drop policy if exists "user can read own profile" on public.user_profiles;
|
||||
create policy "user can read own profile"
|
||||
on public.user_profiles for select
|
||||
using (auth.uid() = id);
|
||||
|
||||
drop policy if exists "user can update own profile" on public.user_profiles;
|
||||
create policy "user can update own profile"
|
||||
on public.user_profiles for update
|
||||
using (auth.uid() = id)
|
||||
with check (auth.uid() = id);
|
||||
|
||||
-- service role bypasses RLS automatically; no explicit policy needed.
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- parlay_leg_frequency — track scan + parlay popularity per day
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
create table if not exists public.parlay_leg_frequency (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
player_name text not null,
|
||||
stat text not null,
|
||||
line numeric not null,
|
||||
over_under text not null check (over_under in ('over','under')),
|
||||
sport text not null check (sport in ('NBA','MLB','WNBA','NFL')),
|
||||
game_date date not null default current_date,
|
||||
scan_count integer not null default 0,
|
||||
parlay_count integer not null default 0,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (player_name, stat, line, over_under, sport, game_date)
|
||||
);
|
||||
|
||||
create index if not exists idx_plf_game_date_sport on public.parlay_leg_frequency(game_date, sport);
|
||||
create index if not exists idx_plf_parlay_rank on public.parlay_leg_frequency(game_date, parlay_count desc);
|
||||
create index if not exists idx_plf_scan_rank on public.parlay_leg_frequency(game_date, scan_count desc);
|
||||
|
||||
drop trigger if exists set_plf_updated_at on public.parlay_leg_frequency;
|
||||
create trigger set_plf_updated_at
|
||||
before update on public.parlay_leg_frequency
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
-- Public read; writes restricted to service role
|
||||
alter table public.parlay_leg_frequency enable row level security;
|
||||
|
||||
drop policy if exists "anyone can read parlay frequency" on public.parlay_leg_frequency;
|
||||
create policy "anyone can read parlay frequency"
|
||||
on public.parlay_leg_frequency for select
|
||||
using (true);
|
||||
|
||||
-- Atomic increment helper
|
||||
create or replace function public.increment_parlay_leg_frequency(
|
||||
p_player text,
|
||||
p_stat text,
|
||||
p_line numeric,
|
||||
p_dir text,
|
||||
p_sport text,
|
||||
p_scan_delta integer default 0,
|
||||
p_parlay_delta integer default 0
|
||||
) returns void as $$
|
||||
begin
|
||||
insert into public.parlay_leg_frequency
|
||||
(player_name, stat, line, over_under, sport, scan_count, parlay_count)
|
||||
values
|
||||
(p_player, p_stat, p_line, p_dir, p_sport, p_scan_delta, p_parlay_delta)
|
||||
on conflict (player_name, stat, line, over_under, sport, game_date)
|
||||
do update set
|
||||
scan_count = public.parlay_leg_frequency.scan_count + p_scan_delta,
|
||||
parlay_count = public.parlay_leg_frequency.parlay_count + p_parlay_delta,
|
||||
updated_at = now();
|
||||
end;
|
||||
$$ language plpgsql security definer set search_path = public;
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- scan_history — for ledger + per-user analytics
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
create table if not exists public.scan_history (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid references auth.users(id) on delete set null,
|
||||
sport text not null,
|
||||
player_name text not null,
|
||||
stat text not null,
|
||||
line numeric not null,
|
||||
direction text not null,
|
||||
grade text,
|
||||
projection numeric,
|
||||
confidence integer,
|
||||
factors jsonb,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_scan_history_user on public.scan_history(user_id, created_at desc);
|
||||
create index if not exists idx_scan_history_sport on public.scan_history(sport, created_at desc);
|
||||
|
||||
alter table public.scan_history enable row level security;
|
||||
|
||||
drop policy if exists "user reads own scans" on public.scan_history;
|
||||
create policy "user reads own scans"
|
||||
on public.scan_history for select
|
||||
using (auth.uid() = user_id);
|
||||
|
||||
drop policy if exists "user inserts own scans" on public.scan_history;
|
||||
create policy "user inserts own scans"
|
||||
on public.scan_history for insert
|
||||
with check (auth.uid() = user_id);
|
||||
@@ -0,0 +1,74 @@
|
||||
-- VYNDR Web — odds cache + waitlist signups
|
||||
-- Idempotent. Safe to apply multiple times.
|
||||
|
||||
create extension if not exists "pgcrypto";
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- odds_cache — TTL-backed mirror of upstream Odds API data
|
||||
-- Reduces calls to the rate-limited Odds API (500 req/month free tier).
|
||||
-- All Next.js route handlers must read from this table, never the
|
||||
-- upstream API directly.
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
create table if not exists public.odds_cache (
|
||||
cache_key text primary key,
|
||||
sport text not null,
|
||||
data_type text not null,
|
||||
payload jsonb not null,
|
||||
fetched_at timestamptz not null default now(),
|
||||
expires_at timestamptz not null default (now() + interval '5 minutes')
|
||||
);
|
||||
|
||||
create index if not exists idx_odds_cache_sport on public.odds_cache(sport);
|
||||
create index if not exists idx_odds_cache_expires on public.odds_cache(expires_at);
|
||||
|
||||
alter table public.odds_cache enable row level security;
|
||||
|
||||
-- Public read OK (cache contents are public market data anyway).
|
||||
-- Writes restricted to service role (bypasses RLS automatically).
|
||||
drop policy if exists "anyone can read odds cache" on public.odds_cache;
|
||||
create policy "anyone can read odds cache"
|
||||
on public.odds_cache for select
|
||||
using (true);
|
||||
|
||||
-- Cleanup helper — call from a cron or trigger if rows accumulate.
|
||||
create or replace function public.prune_expired_odds_cache()
|
||||
returns integer as $$
|
||||
declare
|
||||
deleted_count integer;
|
||||
begin
|
||||
delete from public.odds_cache
|
||||
where expires_at < now() - interval '1 hour';
|
||||
get diagnostics deleted_count = row_count;
|
||||
return deleted_count;
|
||||
end;
|
||||
$$ language plpgsql security definer set search_path = public;
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- waitlist_signups — capture email for upcoming launches
|
||||
-- Referenced by /api/waitlist (marketplace pre-launch lists).
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
create table if not exists public.waitlist_signups (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
email text not null,
|
||||
list text not null default 'general',
|
||||
source text,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (email, list)
|
||||
);
|
||||
|
||||
create index if not exists idx_waitlist_list on public.waitlist_signups(list);
|
||||
create index if not exists idx_waitlist_created on public.waitlist_signups(created_at desc);
|
||||
|
||||
alter table public.waitlist_signups enable row level security;
|
||||
-- Inserts/reads restricted to service role only. No public policies.
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- founder_pricing_seat_count — convenience view used by checkout
|
||||
-- to decide if the next paid user still gets the $9.99 lock-in.
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
create or replace view public.founder_pricing_seats as
|
||||
select
|
||||
count(*) filter (where founder_pricing) as taken,
|
||||
100 as cap,
|
||||
greatest(0, 100 - count(*) filter (where founder_pricing)) as remaining
|
||||
from public.user_profiles;
|
||||
@@ -0,0 +1,47 @@
|
||||
-- ---------------------------------------------------------------
|
||||
-- user_notifications: in-app alert center
|
||||
-- Surfaces rare grades, cascade events, steam moves, morning results.
|
||||
-- Read-tier RLS: a user can only ever see/update their own rows.
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL CHECK (type IN (
|
||||
'rare_grade', 'cascade', 'steam', 'morning_results',
|
||||
'line_movement', 'injury', 'system'
|
||||
)),
|
||||
title TEXT NOT NULL,
|
||||
body TEXT,
|
||||
link TEXT,
|
||||
read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notif_user_unread_created
|
||||
ON public.user_notifications (user_id, read, created_at DESC);
|
||||
|
||||
-- Trim noise: a soft cap of ~200 rows per user via periodic cleanup elsewhere.
|
||||
CREATE INDEX IF NOT EXISTS idx_notif_user_created
|
||||
ON public.user_notifications (user_id, created_at DESC);
|
||||
|
||||
ALTER TABLE public.user_notifications ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "Users read own notifications" ON public.user_notifications;
|
||||
CREATE POLICY "Users read own notifications"
|
||||
ON public.user_notifications
|
||||
FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
DROP POLICY IF EXISTS "Users update own notifications" ON public.user_notifications;
|
||||
CREATE POLICY "Users update own notifications"
|
||||
ON public.user_notifications
|
||||
FOR UPDATE
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Inserts/deletes are server-only (service role bypasses RLS).
|
||||
-- We do NOT grant INSERT/DELETE to anon or authenticated; the server inserts
|
||||
-- system notifications and the user only reads/marks-read via the policies above.
|
||||
|
||||
GRANT SELECT, UPDATE ON public.user_notifications TO authenticated;
|
||||
@@ -0,0 +1,161 @@
|
||||
-- ---------------------------------------------------------------
|
||||
-- 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;
|
||||
@@ -0,0 +1,73 @@
|
||||
-- ---------------------------------------------------------------
|
||||
-- 015 — Web push subscriptions + MFA tracking + grace period.
|
||||
--
|
||||
-- Three concerns bundled in one migration because they all surfaced
|
||||
-- in the Phase 5 PWA / payments / security pass:
|
||||
-- 1. push_subscriptions — per-user push endpoints from the browser
|
||||
-- 2. user_profiles.mfa_setup_prompted — track if we've already asked
|
||||
-- a paid user to enable MFA (so we only nag once)
|
||||
-- 3. user_profiles.grace_period_until — Stripe payment_failed grants
|
||||
-- a 48h grace before tier downgrades take effect
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
create table if not exists public.push_subscriptions (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
endpoint text not null,
|
||||
keys_p256dh text not null,
|
||||
keys_auth text not null,
|
||||
-- One row per (user, endpoint). A user installing the PWA on two
|
||||
-- devices gets two rows. Re-subscribing on the same device upserts.
|
||||
sport_preferences text[] not null default '{nba,mlb,wnba,nfl,nhl,ncaab,ncaafb}',
|
||||
notify_on_resolution boolean not null default true,
|
||||
notify_on_cascade boolean not null default true,
|
||||
notify_on_cheatsheet boolean not null default true,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (user_id, endpoint)
|
||||
);
|
||||
|
||||
create index if not exists idx_push_subscriptions_user on public.push_subscriptions(user_id);
|
||||
create index if not exists idx_push_subscriptions_sport on public.push_subscriptions using gin (sport_preferences);
|
||||
create index if not exists idx_push_subscriptions_resolution
|
||||
on public.push_subscriptions(notify_on_resolution) where notify_on_resolution = true;
|
||||
|
||||
drop trigger if exists set_push_subscriptions_updated_at on public.push_subscriptions;
|
||||
create trigger set_push_subscriptions_updated_at
|
||||
before update on public.push_subscriptions
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
alter table public.push_subscriptions enable row level security;
|
||||
|
||||
drop policy if exists "users manage own push subscriptions" on public.push_subscriptions;
|
||||
create policy "users manage own push subscriptions"
|
||||
on public.push_subscriptions for all
|
||||
using (auth.uid() = user_id)
|
||||
with check (auth.uid() = user_id);
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
-- user_profiles + users additions.
|
||||
--
|
||||
-- Two columns get added to BOTH tables because the codebase has two
|
||||
-- user-state tables in flight:
|
||||
-- public.users — Express backend / Stripe webhook writes here
|
||||
-- public.user_profiles — Next.js routes + AuthContext read here
|
||||
-- Unifying them is out of scope; keeping the columns in sync is the
|
||||
-- least-risk move.
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
alter table public.user_profiles
|
||||
add column if not exists mfa_setup_prompted boolean not null default false,
|
||||
add column if not exists grace_period_until timestamptz;
|
||||
|
||||
alter table public.users
|
||||
add column if not exists mfa_setup_prompted boolean not null default false,
|
||||
add column if not exists grace_period_until timestamptz;
|
||||
|
||||
create index if not exists idx_user_profiles_grace_period
|
||||
on public.user_profiles(grace_period_until)
|
||||
where grace_period_until is not null;
|
||||
|
||||
create index if not exists idx_users_grace_period
|
||||
on public.users(grace_period_until)
|
||||
where grace_period_until is not null;
|
||||
@@ -0,0 +1,145 @@
|
||||
-- ---------------------------------------------------------------
|
||||
-- 016 — Resolution infrastructure.
|
||||
--
|
||||
-- closing_lines : Pinnacle closing reference, captured at tip-off
|
||||
-- (CLV vs our graded line)
|
||||
-- player_id_map : ESPN ↔ MLB Stats API ↔ NBA API cross-references
|
||||
-- resolution_results : append-only per-prop outcome with full audit trail
|
||||
-- (DNP, corrections, closing line link, CLV)
|
||||
--
|
||||
-- Also retrofits grade_history for the resolution pipeline:
|
||||
-- + game_id (resolution route queries by this)
|
||||
-- + resolved_at (idempotency: resolved_at IS NULL = unresolved)
|
||||
-- + margin (actual_value - line for grading-loop analytics)
|
||||
-- + correction_* (audit trail for morning correction sweeps)
|
||||
-- + player_minutes / was_starter (DNP detection inputs)
|
||||
-- + result CHECK (relaxed to include 'void' for postponed games)
|
||||
--
|
||||
-- RLS posture: all three new tables are service-role-only (no public read).
|
||||
-- The Ledger queries grade_history directly; resolution_results is an
|
||||
-- internal audit table.
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.closing_lines (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
game_id text NOT NULL,
|
||||
sport text NOT NULL,
|
||||
player_name text NOT NULL,
|
||||
player_espn_id text,
|
||||
stat_type text NOT NULL,
|
||||
pinnacle_line numeric,
|
||||
pinnacle_over_odds integer,
|
||||
pinnacle_under_odds integer,
|
||||
fair_over_probability numeric,
|
||||
fair_under_probability numeric,
|
||||
captured_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (game_id, player_espn_id, stat_type)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_closing_lines_game ON public.closing_lines(game_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_closing_lines_sport_date ON public.closing_lines(sport, captured_at);
|
||||
|
||||
ALTER TABLE public.closing_lines ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
-- player_id_map: first introduced in migration 014 with a wider but
|
||||
-- differently-shaped column set. CREATE TABLE IF NOT EXISTS would no-op
|
||||
-- if 014 already created the table, leaving 016's columns missing —
|
||||
-- which would break populate-player-ids.js and oddsPapiAdapter.js.
|
||||
--
|
||||
-- Strategy: create the table if absent, then ALTER ADD COLUMN IF NOT
|
||||
-- EXISTS to bring 014's schema forward. The columns 014 has that 016
|
||||
-- doesn't (nba_id, nhl_id, nfl_gsis_id, ufc_id, headshot_url) stay
|
||||
-- intact — harmless extras.
|
||||
CREATE TABLE IF NOT EXISTS public.player_id_map (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
display_name text NOT NULL,
|
||||
normalized_name text NOT NULL,
|
||||
espn_id text NOT NULL UNIQUE,
|
||||
mlbam_id text,
|
||||
nba_api_id text,
|
||||
sport text NOT NULL,
|
||||
team_abbr text,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Bring forward 016's columns onto whatever shape 014 left behind.
|
||||
-- These are no-ops when 016 created the table from scratch.
|
||||
ALTER TABLE public.player_id_map
|
||||
ADD COLUMN IF NOT EXISTS normalized_name text,
|
||||
ADD COLUMN IF NOT EXISTS nba_api_id text,
|
||||
ADD COLUMN IF NOT EXISTS team_abbr text,
|
||||
ADD COLUMN IF NOT EXISTS espn_id text,
|
||||
ADD COLUMN IF NOT EXISTS mlbam_id text;
|
||||
|
||||
-- 014's UNIQUE(sport, display_name, team) does NOT cover espn_id alone,
|
||||
-- which the new code's upsert relies on. Add it idempotently via a
|
||||
-- unique index — does nothing if espn_id is already unique-enforced.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_player_id_espn_unique
|
||||
ON public.player_id_map(espn_id) WHERE espn_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_player_id_sport ON public.player_id_map(sport);
|
||||
CREATE INDEX IF NOT EXISTS idx_player_id_name ON public.player_id_map(normalized_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_player_id_mlbam ON public.player_id_map(mlbam_id) WHERE mlbam_id IS NOT NULL;
|
||||
|
||||
ALTER TABLE public.player_id_map ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.resolution_results (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
grade_id uuid NOT NULL,
|
||||
game_id text NOT NULL,
|
||||
sport text NOT NULL,
|
||||
player_espn_id text NOT NULL,
|
||||
player_name text NOT NULL,
|
||||
stat_type text NOT NULL,
|
||||
line numeric NOT NULL,
|
||||
direction text NOT NULL CHECK (direction IN ('over', 'under')),
|
||||
actual_value numeric NOT NULL,
|
||||
result text NOT NULL CHECK (result IN ('hit', 'miss', 'push', 'void')),
|
||||
margin numeric,
|
||||
player_minutes numeric,
|
||||
was_starter boolean,
|
||||
closing_line_id uuid REFERENCES public.closing_lines(id),
|
||||
clv numeric,
|
||||
resolved_at timestamptz NOT NULL DEFAULT now(),
|
||||
correction_note text,
|
||||
correction_original_value numeric,
|
||||
correction_original_result text
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_resolution_game ON public.resolution_results(game_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resolution_sport_date ON public.resolution_results(sport, resolved_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_resolution_player ON public.resolution_results(player_espn_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_resolution_result ON public.resolution_results(result);
|
||||
|
||||
ALTER TABLE public.resolution_results ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
-- grade_history retrofit. Columns confirmed missing by Session 6a audit.
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
ALTER TABLE public.grade_history
|
||||
ADD COLUMN IF NOT EXISTS game_id text,
|
||||
ADD COLUMN IF NOT EXISTS resolved_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS margin numeric,
|
||||
ADD COLUMN IF NOT EXISTS player_minutes numeric,
|
||||
ADD COLUMN IF NOT EXISTS was_starter boolean,
|
||||
ADD COLUMN IF NOT EXISTS closing_line_id uuid REFERENCES public.closing_lines(id),
|
||||
ADD COLUMN IF NOT EXISTS clv numeric,
|
||||
ADD COLUMN IF NOT EXISTS correction_note text,
|
||||
ADD COLUMN IF NOT EXISTS correction_original_value numeric,
|
||||
ADD COLUMN IF NOT EXISTS correction_original_result text;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_grade_history_game ON public.grade_history(game_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_grade_history_unresolved
|
||||
ON public.grade_history(game_id) WHERE resolved_at IS NULL;
|
||||
|
||||
-- Relax the result CHECK to include 'void' for postponed/canceled games.
|
||||
-- Drop-and-recreate is atomic inside the migration; production data with
|
||||
-- any of the legacy values (hit/miss/push/pending) revalidates cleanly.
|
||||
ALTER TABLE public.grade_history DROP CONSTRAINT IF EXISTS grade_history_result_check;
|
||||
ALTER TABLE public.grade_history
|
||||
ADD CONSTRAINT grade_history_result_check
|
||||
CHECK (result IS NULL OR result IN ('hit', 'miss', 'push', 'pending', 'void'));
|
||||
@@ -0,0 +1,117 @@
|
||||
-- ---------------------------------------------------------------
|
||||
-- 017 — Intelligence tables (Session 6b).
|
||||
--
|
||||
-- historical_props : ParlayAPI bulk archive (3.7M+ rows over time)
|
||||
-- line_snapshots : every SharpAPI pull stored for movement tracking
|
||||
-- ref_profiles : per-referee tendencies (foul rate, pace, bias)
|
||||
-- coach_profiles : per-coach system + pace preferences
|
||||
-- game_ref_assignments : game-day ref crew, with derived crew impact
|
||||
--
|
||||
-- All five tables are service-role-only — no RLS public read policies.
|
||||
-- Internal services read/write via getSupabaseServiceClient(); public
|
||||
-- consumers (Ledger, Pricing) read from grade_history / resolution_results.
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.historical_props (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport text NOT NULL,
|
||||
game_date date NOT NULL,
|
||||
player_name text NOT NULL,
|
||||
stat_type text NOT NULL,
|
||||
line numeric NOT NULL,
|
||||
closing_line numeric,
|
||||
result text CHECK (result IS NULL OR result IN ('over','under','push')),
|
||||
source text NOT NULL DEFAULT 'parlayapi',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_hist_props_player
|
||||
ON public.historical_props (player_name, stat_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_hist_props_sport_date
|
||||
ON public.historical_props (sport, game_date DESC);
|
||||
|
||||
ALTER TABLE public.historical_props ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.line_snapshots (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
game_id text NOT NULL,
|
||||
sport text NOT NULL,
|
||||
player_name text,
|
||||
player_id text,
|
||||
stat_type text NOT NULL,
|
||||
line numeric NOT NULL,
|
||||
over_odds integer,
|
||||
under_odds integer,
|
||||
book text,
|
||||
consensus_median numeric,
|
||||
snapshot_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
-- Partial index on (game_id, stat_type) is the access pattern for movement
|
||||
-- queries; full snapshot_at index covers retention sweeps (delete > 90d).
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshots_game
|
||||
ON public.line_snapshots (game_id, stat_type, snapshot_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshots_time
|
||||
ON public.line_snapshots (snapshot_at);
|
||||
|
||||
ALTER TABLE public.line_snapshots ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.ref_profiles (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
ref_name text NOT NULL UNIQUE,
|
||||
avg_fouls_per_game numeric,
|
||||
avg_free_throws_per_game numeric,
|
||||
pace_impact numeric, -- + = faster games
|
||||
home_whistle_bias numeric, -- + = favors home team
|
||||
games_reffed integer,
|
||||
last_updated timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ref_profiles_name ON public.ref_profiles (ref_name);
|
||||
|
||||
ALTER TABLE public.ref_profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.coach_profiles (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
coach_name text NOT NULL,
|
||||
team text NOT NULL,
|
||||
sport text NOT NULL,
|
||||
career_avg_pace numeric,
|
||||
current_team_pace numeric,
|
||||
pace_delta numeric, -- career_avg - current_team
|
||||
tenure_games integer NOT NULL DEFAULT 0,
|
||||
primary_player text,
|
||||
system_style text, -- 'half_court_iso','motion','transition','post_heavy'
|
||||
without_primary_style text,
|
||||
without_primary_pace_delta numeric,
|
||||
last_updated timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (coach_name, team, sport)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_coach_profiles_team
|
||||
ON public.coach_profiles (team, sport);
|
||||
|
||||
ALTER TABLE public.coach_profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.game_ref_assignments (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
game_id text NOT NULL UNIQUE,
|
||||
sport text NOT NULL,
|
||||
game_date date NOT NULL,
|
||||
ref1_name text,
|
||||
ref2_name text,
|
||||
ref3_name text,
|
||||
ref_crew_avg_fouls numeric, -- precomputed from ref_profiles
|
||||
ref_crew_pace_impact numeric,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_game_refs_game
|
||||
ON public.game_ref_assignments (game_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_game_refs_date
|
||||
ON public.game_ref_assignments (game_date);
|
||||
|
||||
ALTER TABLE public.game_ref_assignments ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,75 @@
|
||||
-- ---------------------------------------------------------------
|
||||
-- 018 — Engine 2 + learning loop tables (Session 6c).
|
||||
--
|
||||
-- grade_history retrofit : Engine 2 analysis columns
|
||||
-- engine1_weights : versioned factor weights for Engine 1
|
||||
-- accuracy_tracking : per-grade-tier hit rates with baseline lock
|
||||
--
|
||||
-- All three are service-role-only. The Ledger surface reads grade_history
|
||||
-- directly (it already had RLS configured in earlier migrations); the new
|
||||
-- engine2_* columns inherit that policy.
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
ALTER TABLE public.grade_history
|
||||
ADD COLUMN IF NOT EXISTS engine2_grade text,
|
||||
ADD COLUMN IF NOT EXISTS engine2_confidence numeric,
|
||||
ADD COLUMN IF NOT EXISTS engine2_narrative text,
|
||||
ADD COLUMN IF NOT EXISTS engine2_agrees boolean,
|
||||
ADD COLUMN IF NOT EXISTS engine2_key_factor text,
|
||||
ADD COLUMN IF NOT EXISTS engine2_trap_concern text,
|
||||
ADD COLUMN IF NOT EXISTS engine2_model text,
|
||||
ADD COLUMN IF NOT EXISTS engine2_latency_ms integer,
|
||||
ADD COLUMN IF NOT EXISTS engine2_analyzed_at timestamptz;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_grade_history_engine2_pending
|
||||
ON public.grade_history (id) WHERE engine2_analyzed_at IS NULL;
|
||||
|
||||
-- Session 7a additions: store Engine 1 factors (for the learning loop's
|
||||
-- weight adjuster) and the quantile-based P(Over) (for the UI + Engine 2).
|
||||
ALTER TABLE public.grade_history
|
||||
ADD COLUMN IF NOT EXISTS factors jsonb,
|
||||
ADD COLUMN IF NOT EXISTS p_over numeric;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.engine1_weights (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport text NOT NULL,
|
||||
stat_type text NOT NULL,
|
||||
factor_name text NOT NULL,
|
||||
weight numeric NOT NULL,
|
||||
previous_weight numeric,
|
||||
adjustment_reason text,
|
||||
resolved_grade_id uuid,
|
||||
version integer NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_weights_sport_stat
|
||||
ON public.engine1_weights (sport, stat_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_weights_version
|
||||
ON public.engine1_weights (sport, stat_type, factor_name, version DESC);
|
||||
|
||||
ALTER TABLE public.engine1_weights ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ---------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.accuracy_tracking (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport text NOT NULL,
|
||||
grade text NOT NULL,
|
||||
period text NOT NULL DEFAULT 'all_time',
|
||||
total_graded integer NOT NULL DEFAULT 0,
|
||||
total_hit integer NOT NULL DEFAULT 0,
|
||||
total_miss integer NOT NULL DEFAULT 0,
|
||||
total_push integer NOT NULL DEFAULT 0,
|
||||
total_void integer NOT NULL DEFAULT 0,
|
||||
hit_rate numeric,
|
||||
baseline_hit_rate numeric,
|
||||
baseline_locked boolean NOT NULL DEFAULT false,
|
||||
clv_avg numeric,
|
||||
engine2_agree_rate numeric,
|
||||
last_updated timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (sport, grade, period)
|
||||
);
|
||||
|
||||
ALTER TABLE public.accuracy_tracking ENABLE ROW LEVEL SECURITY;
|
||||
Reference in New Issue
Block a user