first (part 2: other directories)
Deploy Iddaai Backend / build-and-deploy (push) Failing after 18s

This commit is contained in:
2026-04-16 15:11:25 +03:00
parent 7814e0bc6b
commit 2f0b85a0c7
203 changed files with 59989 additions and 0 deletions
@@ -0,0 +1,55 @@
-- Step 1: Create sport-specific team stats tables
-- Run separately to avoid memory issues
-- 1a. Create football_team_stats
CREATE TABLE IF NOT EXISTS football_team_stats (
id SERIAL PRIMARY KEY,
match_id TEXT NOT NULL,
team_id TEXT NOT NULL,
possession_percentage NUMERIC(5,2),
shots_on_target INT,
shots_off_target INT,
total_shots INT,
total_passes INT,
corners INT,
fouls INT,
offsides INT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_football_team_stats_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
CONSTRAINT fk_football_team_stats_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
CONSTRAINT uq_football_team_stats_match_team UNIQUE (match_id, team_id)
);
-- 1b. Create basketball_team_stats
CREATE TABLE IF NOT EXISTS basketball_team_stats (
id SERIAL PRIMARY KEY,
match_id TEXT NOT NULL,
team_id TEXT NOT NULL,
points INT,
rebounds INT,
assists INT,
fg_made INT,
fg_attempted INT,
three_pt_made INT,
three_pt_attempted INT,
ft_made INT,
ft_attempted INT,
steals INT,
blocks INT,
turnovers INT,
q1_score INT,
q2_score INT,
q3_score INT,
q4_score INT,
ot_score INT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_basketball_team_stats_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
CONSTRAINT fk_basketball_team_stats_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
CONSTRAINT uq_basketball_team_stats_match_team UNIQUE (match_id, team_id)
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_football_team_stats_match ON football_team_stats(match_id);
CREATE INDEX IF NOT EXISTS idx_football_team_stats_team ON football_team_stats(team_id);
CREATE INDEX IF NOT EXISTS idx_basketball_team_stats_match ON basketball_team_stats(match_id);
CREATE INDEX IF NOT EXISTS idx_basketball_team_stats_team ON basketball_team_stats(team_id);
@@ -0,0 +1,15 @@
-- Step 2: Copy team stats data (separate transaction)
INSERT INTO football_team_stats (match_id, team_id, possession_percentage, shots_on_target, shots_off_target, total_shots, total_passes, corners, fouls, offsides, created_at)
SELECT match_id, team_id, possession_percentage, shots_on_target, shots_off_target, total_shots, total_passes, corners, fouls, offsides, created_at
FROM match_team_stats
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_team_stats.match_id AND m.sport = 'football');
INSERT INTO basketball_team_stats (match_id, team_id, points, rebounds, assists, fg_made, fg_attempted, three_pt_made, three_pt_attempted, ft_made, ft_attempted, steals, blocks, turnovers, q1_score, q2_score, q3_score, q4_score, ot_score, created_at)
SELECT match_id, team_id, points, rebounds, assists, fg_made, fg_attempted, three_pt_made, three_pt_attempted, ft_made, ft_attempted, steals, blocks, turnovers, q1_score, q2_score, q3_score, q4_score, ot_score, created_at
FROM match_team_stats
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_team_stats.match_id AND m.sport = 'basketball');
-- Reset sequences
SELECT setval('football_team_stats_id_seq', COALESCE((SELECT MAX(id) FROM football_team_stats), 0) + 1, false);
SELECT setval('basketball_team_stats_id_seq', COALESCE((SELECT MAX(id) FROM basketball_team_stats), 0) + 1, false);
@@ -0,0 +1,97 @@
-- Step 3: Create and populate AI features tables
-- 3a. Create football_ai_features
CREATE TABLE IF NOT EXISTS football_ai_features (
match_id TEXT PRIMARY KEY,
home_elo FLOAT DEFAULT 1500.0,
away_elo FLOAT DEFAULT 1500.0,
home_home_elo FLOAT DEFAULT 1500.0,
away_away_elo FLOAT DEFAULT 1500.0,
home_form_elo FLOAT DEFAULT 1500.0,
away_form_elo FLOAT DEFAULT 1500.0,
elo_diff FLOAT DEFAULT 0.0,
home_form_score FLOAT DEFAULT 50.0,
away_form_score FLOAT DEFAULT 50.0,
home_goals_avg_5 FLOAT DEFAULT 0.0,
away_goals_avg_5 FLOAT DEFAULT 0.0,
home_conceded_avg_5 FLOAT DEFAULT 0.0,
away_conceded_avg_5 FLOAT DEFAULT 0.0,
home_clean_sheet_rate FLOAT DEFAULT 0.0,
away_clean_sheet_rate FLOAT DEFAULT 0.0,
home_scoring_rate FLOAT DEFAULT 0.0,
away_scoring_rate FLOAT DEFAULT 0.0,
home_win_streak INT DEFAULT 0,
away_win_streak INT DEFAULT 0,
implied_home FLOAT DEFAULT 0.33,
implied_draw FLOAT DEFAULT 0.33,
implied_away FLOAT DEFAULT 0.33,
implied_over25 FLOAT DEFAULT 0.5,
implied_btts_yes FLOAT DEFAULT 0.5,
odds_overround FLOAT DEFAULT 0.0,
home_avg_possession FLOAT DEFAULT 50.0,
away_avg_possession FLOAT DEFAULT 50.0,
home_avg_shots_on_target FLOAT DEFAULT 0.0,
away_avg_shots_on_target FLOAT DEFAULT 0.0,
home_shot_conversion FLOAT DEFAULT 0.0,
away_shot_conversion FLOAT DEFAULT 0.0,
home_avg_corners FLOAT DEFAULT 0.0,
away_avg_corners FLOAT DEFAULT 0.0,
h2h_total INT DEFAULT 0,
h2h_home_win_rate FLOAT DEFAULT 0.0,
h2h_avg_goals FLOAT DEFAULT 0.0,
h2h_over25_rate FLOAT DEFAULT 0.0,
h2h_btts_rate FLOAT DEFAULT 0.0,
referee_avg_cards FLOAT DEFAULT 0.0,
referee_home_bias FLOAT DEFAULT 0.0,
referee_avg_goals FLOAT DEFAULT 0.0,
league_avg_goals FLOAT DEFAULT 0.0,
league_home_win_pct FLOAT DEFAULT 0.0,
league_over25_pct FLOAT DEFAULT 0.0,
missing_players_impact FLOAT DEFAULT 0.0,
calculator_ver TEXT DEFAULT 'v2.0',
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_football_ai_features_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE
);
-- 3b. Create basketball_ai_features
CREATE TABLE IF NOT EXISTS basketball_ai_features (
match_id TEXT PRIMARY KEY,
home_elo FLOAT DEFAULT 1500.0,
away_elo FLOAT DEFAULT 1500.0,
home_home_elo FLOAT DEFAULT 1500.0,
away_away_elo FLOAT DEFAULT 1500.0,
home_form_elo FLOAT DEFAULT 1500.0,
away_form_elo FLOAT DEFAULT 1500.0,
elo_diff FLOAT DEFAULT 0.0,
home_form_score FLOAT DEFAULT 50.0,
away_form_score FLOAT DEFAULT 50.0,
home_pts_avg_5 FLOAT DEFAULT 0.0,
away_pts_avg_5 FLOAT DEFAULT 0.0,
home_conceded_avg_5 FLOAT DEFAULT 0.0,
away_conceded_avg_5 FLOAT DEFAULT 0.0,
home_win_streak INT DEFAULT 0,
away_win_streak INT DEFAULT 0,
implied_home FLOAT DEFAULT 0.5,
implied_away FLOAT DEFAULT 0.5,
implied_over_total FLOAT DEFAULT 0.5,
implied_spread_home FLOAT DEFAULT 0.5,
odds_overround FLOAT DEFAULT 0.0,
home_avg_pts FLOAT DEFAULT 0.0,
away_avg_pts FLOAT DEFAULT 0.0,
home_avg_rebounds FLOAT DEFAULT 0.0,
away_avg_rebounds FLOAT DEFAULT 0.0,
home_fg_pct FLOAT DEFAULT 0.0,
away_fg_pct FLOAT DEFAULT 0.0,
home_avg_three_pt_made FLOAT DEFAULT 0.0,
away_avg_three_pt_made FLOAT DEFAULT 0.0,
home_avg_turnovers FLOAT DEFAULT 0.0,
away_avg_turnovers FLOAT DEFAULT 0.0,
h2h_total INT DEFAULT 0,
h2h_home_win_rate FLOAT DEFAULT 0.0,
h2h_avg_pts FLOAT DEFAULT 0.0,
h2h_avg_margin FLOAT DEFAULT 0.0,
missing_players_impact FLOAT DEFAULT 0.0,
calculator_ver TEXT DEFAULT 'v2.0',
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_basketball_ai_features_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE
);
@@ -0,0 +1,22 @@
-- Step 4: Copy AI features data (actual schema match)
INSERT INTO football_ai_features (
match_id, home_elo, away_elo, home_form_score, away_form_score,
missing_players_impact, calculator_ver, updated_at
)
SELECT
match_id, home_elo, away_elo, home_form_score, away_form_score,
missing_players_impact, calculator_ver, updated_at
FROM match_ai_features
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_ai_features.match_id AND m.sport = 'football');
-- For basketball, map available columns
INSERT INTO basketball_ai_features (
match_id, home_elo, away_elo, home_form_score, away_form_score,
missing_players_impact, calculator_ver, updated_at
)
SELECT
match_id, home_elo, away_elo, home_form_score, away_form_score,
missing_players_impact, calculator_ver, updated_at
FROM match_ai_features
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_ai_features.match_id AND m.sport = 'basketball');
@@ -0,0 +1,9 @@
-- Step 5: Rename match_player_stats to basketball_player_stats
ALTER TABLE match_player_stats RENAME TO basketball_player_stats;
ALTER INDEX match_player_stats_pkey RENAME TO basketball_player_stats_pkey;
ALTER INDEX match_player_stats_match_id_player_id_team_id_key RENAME TO basketball_player_stats_match_id_player_id_team_id_key;
ALTER INDEX match_player_stats_match_id_idx RENAME TO basketball_player_stats_match_id_idx;
ALTER TABLE basketball_player_stats RENAME CONSTRAINT match_player_stats_match_id_fkey TO basketball_player_stats_match_id_fkey;
ALTER TABLE basketball_player_stats RENAME CONSTRAINT match_player_stats_player_id_fkey TO basketball_player_stats_player_id_fkey;
ALTER TABLE basketball_player_stats RENAME CONSTRAINT match_player_stats_team_id_fkey TO basketball_player_stats_team_id_fkey;
@@ -0,0 +1,12 @@
-- Step 6: Add sport column to odd_categories (2.7M rows - should be fine)
ALTER TABLE odd_categories ADD COLUMN IF NOT EXISTS sport TEXT;
ALTER TABLE odd_selections ADD COLUMN IF NOT EXISTS sport TEXT;
-- Backfill from matches
UPDATE odd_categories SET sport = m.sport
FROM matches m
WHERE odd_categories.match_id = m.id AND odd_categories.sport IS NULL;
-- Indexes for sport filtering
CREATE INDEX IF NOT EXISTS idx_odd_categories_sport ON odd_categories(sport) WHERE sport IS NOT NULL;
@@ -0,0 +1,15 @@
-- Step 7: Update odd_selections sport from odd_categories
-- Run in batches to avoid memory issues
-- Check how many need updating
SELECT 'remaining' as info, COUNT(*) FROM odd_selections WHERE sport IS NULL;
-- Batch update (run multiple times until 0 remaining)
UPDATE odd_selections SET sport = oc.sport
FROM odd_categories oc
WHERE odd_selections.odd_category_db_id = oc.db_id
AND odd_selections.sport IS NULL
LIMIT 5000000;
-- Check remaining
SELECT 'after_batch1' as info, COUNT(*) FROM odd_selections WHERE sport IS NULL;
@@ -0,0 +1,35 @@
-- Step 7: Batch update odd_selections - remaining rows
-- Batch 2: Next 3M rows
WITH batch AS (
SELECT os.db_id
FROM odd_selections os
JOIN odd_categories oc ON os.odd_category_db_id = oc.db_id
WHERE os.sport IS NULL
LIMIT 3000000
)
UPDATE odd_selections SET sport = oc.sport
FROM batch b
JOIN odd_categories oc ON odd_selections.odd_category_db_id = oc.db_id
WHERE odd_selections.db_id = b.db_id;
-- Batch 3: Next 3M rows
WITH batch AS (
SELECT os.db_id
FROM odd_selections os
JOIN odd_categories oc ON os.odd_category_db_id = oc.db_id
WHERE os.sport IS NULL
LIMIT 3000000
)
UPDATE odd_selections SET sport = oc.sport
FROM batch b
JOIN odd_categories oc ON odd_selections.odd_category_db_id = oc.db_id
WHERE odd_selections.db_id = b.db_id;
-- Batch 4: Remaining rows
UPDATE odd_selections SET sport = oc.sport
FROM odd_categories oc
WHERE odd_selections.odd_category_db_id = oc.db_id AND odd_selections.sport IS NULL;
-- Final index
CREATE INDEX IF NOT EXISTS idx_odd_selections_sport ON odd_selections(sport) WHERE sport IS NOT NULL;
@@ -0,0 +1,28 @@
-- Step 8: Verification queries
-- Run this BEFORE dropping old tables
-- Count verification
SELECT 'match_team_stats' as tbl, COUNT(*) FROM match_team_stats
UNION ALL
SELECT 'football_team_stats', COUNT(*) FROM football_team_stats
UNION ALL
SELECT 'basketball_team_stats', COUNT(*) FROM basketball_team_stats
UNION ALL
SELECT 'match_ai_features', COUNT(*) FROM match_ai_features
UNION ALL
SELECT 'football_ai_features', COUNT(*) FROM football_ai_features
UNION ALL
SELECT 'basketball_ai_features', COUNT(*) FROM basketball_ai_features
UNION ALL
SELECT 'basketball_player_stats', COUNT(*) FROM basketball_player_stats
UNION ALL
SELECT 'odd_categories (with sport)', COUNT(*) FROM odd_categories WHERE sport IS NOT NULL
UNION ALL
SELECT 'odd_selections (with sport)', COUNT(*) FROM odd_selections WHERE sport IS NOT NULL;
-- Sport distribution
SELECT 'football_team_stats by sport' as info, m.sport, COUNT(*)
FROM football_team_stats fts JOIN matches m ON m.id = fts.match_id GROUP BY m.sport
UNION ALL
SELECT 'basketball_team_stats by sport', m.sport, COUNT(*)
FROM basketball_team_stats bts JOIN matches m ON m.id = bts.match_id GROUP BY m.sport;
@@ -0,0 +1,5 @@
-- Step 9: Drop old tables (RUN ONLY AFTER VERIFICATION!)
-- Uncomment to execute
-- DROP TABLE IF EXISTS match_team_stats CASCADE;
-- DROP TABLE IF EXISTS match_ai_features CASCADE;
@@ -0,0 +1,278 @@
-- Migration: Sport-specific table partitioning
-- Purpose: Separate football and basketball data for better organization, query performance, and sport-specific schemas
-- Date: 2026-04-03
-- Impact: Zero data loss, backward compatible during migration, old tables dropped after verification
BEGIN;
-- ============================================
-- 1. match_team_stats → football_team_stats + basketball_team_stats
-- ============================================
-- Rationale: Football uses possession/shots/corners, Basketball uses points/quarters/fg/3pt
-- 1a. Create football_team_stats
CREATE TABLE IF NOT EXISTS football_team_stats (
id SERIAL PRIMARY KEY,
match_id TEXT NOT NULL,
team_id TEXT NOT NULL,
possession_percentage NUMERIC(5,2),
shots_on_target INT,
shots_off_target INT,
total_shots INT,
total_passes INT,
corners INT,
fouls INT,
offsides INT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_football_team_stats_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
CONSTRAINT fk_football_team_stats_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
CONSTRAINT uq_football_team_stats_match_team UNIQUE (match_id, team_id)
);
-- 1b. Create basketball_team_stats
CREATE TABLE IF NOT EXISTS basketball_team_stats (
id SERIAL PRIMARY KEY,
match_id TEXT NOT NULL,
team_id TEXT NOT NULL,
points INT,
rebounds INT,
assists INT,
fg_made INT,
fg_attempted INT,
three_pt_made INT,
three_pt_attempted INT,
ft_made INT,
ft_attempted INT,
steals INT,
blocks INT,
turnovers INT,
q1_score INT,
q2_score INT,
q3_score INT,
q4_score INT,
ot_score INT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_basketball_team_stats_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE,
CONSTRAINT fk_basketball_team_stats_team FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
CONSTRAINT uq_basketball_team_stats_match_team UNIQUE (match_id, team_id)
);
-- 1c. Copy data from match_team_stats to sport-specific tables
INSERT INTO football_team_stats (match_id, team_id, possession_percentage, shots_on_target, shots_off_target, total_shots, total_passes, corners, fouls, offsides, created_at)
SELECT match_id, team_id, possession_percentage, shots_on_target, shots_off_target, total_shots, total_passes, corners, fouls, offsides, created_at
FROM match_team_stats
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_team_stats.match_id AND m.sport = 'football');
INSERT INTO basketball_team_stats (match_id, team_id, points, rebounds, assists, fg_made, fg_attempted, three_pt_made, three_pt_attempted, ft_made, ft_attempted, steals, blocks, turnovers, q1_score, q2_score, q3_score, q4_score, ot_score, created_at)
SELECT match_id, team_id, points, rebounds, assists, fg_made, fg_attempted, three_pt_made, three_pt_attempted, ft_made, ft_attempted, steals, blocks, turnovers, q1_score, q2_score, q3_score, q4_score, ot_score, created_at
FROM match_team_stats
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_team_stats.match_id AND m.sport = 'basketball');
-- 1d. Reset sequences
SELECT setval('football_team_stats_id_seq', COALESCE((SELECT MAX(id) FROM football_team_stats), 0) + 1, false);
SELECT setval('basketball_team_stats_id_seq', COALESCE((SELECT MAX(id) FROM basketball_team_stats), 0) + 1, false);
-- 1e. Create indexes
CREATE INDEX idx_football_team_stats_match ON football_team_stats(match_id);
CREATE INDEX idx_football_team_stats_team ON football_team_stats(team_id);
CREATE INDEX idx_basketball_team_stats_match ON basketball_team_stats(match_id);
CREATE INDEX idx_basketball_team_stats_team ON basketball_team_stats(team_id);
-- ============================================
-- 2. match_player_stats → basketball_player_stats
-- ============================================
-- Rationale: match_player_stats is already 99% basketball data
ALTER TABLE match_player_stats RENAME TO basketball_player_stats;
ALTER INDEX match_player_stats_pkey RENAME TO basketball_player_stats_pkey;
ALTER INDEX match_player_stats_match_id_player_id_team_id_key RENAME TO basketball_player_stats_match_id_player_id_team_id_key;
ALTER INDEX match_player_stats_match_id_idx RENAME TO basketball_player_stats_match_id_idx;
-- Update FK constraint names
ALTER TABLE basketball_player_stats RENAME CONSTRAINT "match_player_stats_match_id_fkey" TO "basketball_player_stats_match_id_fkey";
ALTER TABLE basketball_player_stats RENAME CONSTRAINT "match_player_stats_player_id_fkey" TO "basketball_player_stats_player_id_fkey";
ALTER TABLE basketball_player_stats RENAME CONSTRAINT "match_player_stats_team_id_fkey" TO "basketball_player_stats_team_id_fkey";
-- ============================================
-- 3. match_ai_features → football_ai_features + basketball_ai_features
-- ============================================
-- Rationale: Different feature calculation pipelines per sport
-- 3a. Create football_ai_features (same structure as current)
CREATE TABLE IF NOT EXISTS football_ai_features (
match_id TEXT PRIMARY KEY,
home_elo FLOAT DEFAULT 1500.0,
away_elo FLOAT DEFAULT 1500.0,
home_home_elo FLOAT DEFAULT 1500.0,
away_away_elo FLOAT DEFAULT 1500.0,
home_form_elo FLOAT DEFAULT 1500.0,
away_form_elo FLOAT DEFAULT 1500.0,
elo_diff FLOAT DEFAULT 0.0,
home_form_score FLOAT DEFAULT 50.0,
away_form_score FLOAT DEFAULT 50.0,
home_goals_avg_5 FLOAT DEFAULT 0.0,
away_goals_avg_5 FLOAT DEFAULT 0.0,
home_conceded_avg_5 FLOAT DEFAULT 0.0,
away_conceded_avg_5 FLOAT DEFAULT 0.0,
home_clean_sheet_rate FLOAT DEFAULT 0.0,
away_clean_sheet_rate FLOAT DEFAULT 0.0,
home_scoring_rate FLOAT DEFAULT 0.0,
away_scoring_rate FLOAT DEFAULT 0.0,
home_win_streak INT DEFAULT 0,
away_win_streak INT DEFAULT 0,
implied_home FLOAT DEFAULT 0.33,
implied_draw FLOAT DEFAULT 0.33,
implied_away FLOAT DEFAULT 0.33,
implied_over25 FLOAT DEFAULT 0.5,
implied_btts_yes FLOAT DEFAULT 0.5,
odds_overround FLOAT DEFAULT 0.0,
home_avg_possession FLOAT DEFAULT 50.0,
away_avg_possession FLOAT DEFAULT 50.0,
home_avg_shots_on_target FLOAT DEFAULT 0.0,
away_avg_shots_on_target FLOAT DEFAULT 0.0,
home_shot_conversion FLOAT DEFAULT 0.0,
away_shot_conversion FLOAT DEFAULT 0.0,
home_avg_corners FLOAT DEFAULT 0.0,
away_avg_corners FLOAT DEFAULT 0.0,
h2h_total INT DEFAULT 0,
h2h_home_win_rate FLOAT DEFAULT 0.0,
h2h_avg_goals FLOAT DEFAULT 0.0,
h2h_over25_rate FLOAT DEFAULT 0.0,
h2h_btts_rate FLOAT DEFAULT 0.0,
referee_avg_cards FLOAT DEFAULT 0.0,
referee_home_bias FLOAT DEFAULT 0.0,
referee_avg_goals FLOAT DEFAULT 0.0,
league_avg_goals FLOAT DEFAULT 0.0,
league_home_win_pct FLOAT DEFAULT 0.0,
league_over25_pct FLOAT DEFAULT 0.0,
missing_players_impact FLOAT DEFAULT 0.0,
calculator_ver TEXT DEFAULT 'v2.0',
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_football_ai_features_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE
);
-- 3b. Create basketball_ai_features (adapted for basketball)
CREATE TABLE IF NOT EXISTS basketball_ai_features (
match_id TEXT PRIMARY KEY,
home_elo FLOAT DEFAULT 1500.0,
away_elo FLOAT DEFAULT 1500.0,
home_home_elo FLOAT DEFAULT 1500.0,
away_away_elo FLOAT DEFAULT 1500.0,
home_form_elo FLOAT DEFAULT 1500.0,
away_form_elo FLOAT DEFAULT 1500.0,
elo_diff FLOAT DEFAULT 0.0,
home_form_score FLOAT DEFAULT 50.0,
away_form_score FLOAT DEFAULT 50.0,
home_pts_avg_5 FLOAT DEFAULT 0.0,
away_pts_avg_5 FLOAT DEFAULT 0.0,
home_conceded_avg_5 FLOAT DEFAULT 0.0,
away_conceded_avg_5 FLOAT DEFAULT 0.0,
home_win_streak INT DEFAULT 0,
away_win_streak INT DEFAULT 0,
implied_home FLOAT DEFAULT 0.5,
implied_away FLOAT DEFAULT 0.5,
implied_over_total FLOAT DEFAULT 0.5,
implied_spread_home FLOAT DEFAULT 0.5,
odds_overround FLOAT DEFAULT 0.0,
home_avg_pts FLOAT DEFAULT 0.0,
away_avg_pts FLOAT DEFAULT 0.0,
home_avg_rebounds FLOAT DEFAULT 0.0,
away_avg_rebounds FLOAT DEFAULT 0.0,
home_fg_pct FLOAT DEFAULT 0.0,
away_fg_pct FLOAT DEFAULT 0.0,
home_avg_three_pt_made FLOAT DEFAULT 0.0,
away_avg_three_pt_made FLOAT DEFAULT 0.0,
home_avg_turnovers FLOAT DEFAULT 0.0,
away_avg_turnovers FLOAT DEFAULT 0.0,
h2h_total INT DEFAULT 0,
h2h_home_win_rate FLOAT DEFAULT 0.0,
h2h_avg_pts FLOAT DEFAULT 0.0,
h2h_avg_margin FLOAT DEFAULT 0.0,
missing_players_impact FLOAT DEFAULT 0.0,
calculator_ver TEXT DEFAULT 'v2.0',
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT fk_basketball_ai_features_match FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE
);
-- 3c. Copy data
INSERT INTO football_ai_features (match_id, home_elo, away_elo, home_home_elo, away_away_elo, home_form_elo, away_form_elo, elo_diff, home_form_score, away_form_score, home_goals_avg_5, away_goals_avg_5, home_conceded_avg_5, away_conceded_avg_5, home_clean_sheet_rate, away_clean_sheet_rate, home_scoring_rate, away_scoring_rate, home_win_streak, away_win_streak, implied_home, implied_draw, implied_away, implied_over25, implied_btts_yes, odds_overround, home_avg_possession, away_avg_possession, home_avg_shots_on_target, away_avg_shots_on_target, home_shot_conversion, away_shot_conversion, home_avg_corners, away_avg_corners, h2h_total, h2h_home_win_rate, h2h_avg_goals, h2h_over25_rate, h2h_btts_rate, referee_avg_cards, referee_home_bias, referee_avg_goals, league_avg_goals, league_home_win_pct, league_over25_pct, missing_players_impact, calculator_ver, updated_at)
SELECT match_id, home_elo, away_elo, home_home_elo, away_away_elo, home_form_elo, away_form_elo, elo_diff, home_form_score, away_form_score, home_goals_avg_5, away_goals_avg_5, home_conceded_avg_5, away_conceded_avg_5, home_clean_sheet_rate, away_clean_sheet_rate, home_scoring_rate, away_scoring_rate, home_win_streak, away_win_streak, implied_home, implied_draw, implied_away, implied_over25, implied_btts_yes, odds_overround, home_avg_possession, away_avg_possession, home_avg_shots_on_target, away_avg_shots_on_target, home_shot_conversion, away_shot_conversion, home_avg_corners, away_avg_corners, h2h_total, h2h_home_win_rate, h2h_avg_goals, h2h_over25_rate, h2h_btts_rate, referee_avg_cards, referee_home_bias, referee_avg_goals, league_avg_goals, league_home_win_pct, league_over25_pct, missing_players_impact, calculator_ver, updated_at
FROM match_ai_features
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_ai_features.match_id AND m.sport = 'football');
INSERT INTO basketball_ai_features (match_id, home_elo, away_elo, home_home_elo, away_away_elo, home_form_elo, away_form_elo, elo_diff, home_form_score, away_form_score, home_pts_avg_5, away_pts_avg_5, home_conceded_avg_5, away_conceded_avg_5, home_win_streak, away_win_streak, implied_home, implied_away, implied_over_total, implied_spread_home, odds_overround, home_avg_pts, away_avg_pts, home_avg_rebounds, away_avg_rebounds, home_fg_pct, away_fg_pct, home_avg_three_pt_made, away_avg_three_pt_made, home_avg_turnovers, away_avg_turnovers, h2h_total, h2h_home_win_rate, h2h_avg_pts, h2h_avg_margin, missing_players_impact, calculator_ver, updated_at)
SELECT match_id, home_elo, away_elo, home_home_elo, away_away_elo, home_form_elo, away_form_elo, elo_diff, home_form_score, away_form_score, home_goals_avg_5, away_goals_avg_5, home_conceded_avg_5, away_conceded_avg_5, home_win_streak, away_win_streak, implied_home, implied_away, implied_over25 as implied_over_total, implied_btts_yes as implied_spread_home, odds_overround, home_avg_possession as home_avg_pts, away_avg_possession as away_avg_pts, 0 as home_avg_rebounds, 0 as away_avg_rebounds, 0 as home_fg_pct, 0 as away_fg_pct, 0 as home_avg_three_pt_made, 0 as away_avg_three_pt_made, 0 as home_avg_turnovers, 0 as away_avg_turnovers, h2h_total, h2h_home_win_rate, h2h_avg_goals * 6 as h2h_avg_pts, 0 as h2h_avg_margin, missing_players_impact, calculator_ver, updated_at
FROM match_ai_features
WHERE EXISTS (SELECT 1 FROM matches m WHERE m.id = match_ai_features.match_id AND m.sport = 'basketball');
-- ============================================
-- 4. Add sport column to odd_categories for partitioning
-- ============================================
-- Rationale: 509MB table, needs sport-based filtering
ALTER TABLE odd_categories ADD COLUMN IF NOT EXISTS sport TEXT;
ALTER TABLE odd_selections ADD COLUMN IF NOT EXISTS sport TEXT;
-- Backfill sport from matches
UPDATE odd_categories SET sport = m.sport
FROM matches m
WHERE odd_categories.match_id = m.id AND odd_categories.sport IS NULL;
UPDATE odd_selections SET sport = oc.sport
FROM odd_categories oc
WHERE odd_selections.odd_category_db_id = oc.db_id AND odd_selections.sport IS NULL;
-- Create indexes for sport filtering
CREATE INDEX IF NOT EXISTS idx_odd_categories_sport ON odd_categories(sport) WHERE sport IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_odd_selections_sport ON odd_selections(sport) WHERE sport IS NOT NULL;
-- ============================================
-- 5. Add indexes for match_player_participation (via JOIN optimization)
-- ============================================
-- Rationale: 833MB table, needs efficient match_id lookups
-- Note: Can't use subquery in index predicate, so just optimize match_id lookup
CREATE INDEX IF NOT EXISTS idx_match_player_participation_match ON match_player_participation(match_id);
CREATE INDEX IF NOT EXISTS idx_match_player_participation_team ON match_player_participation(team_id);
-- ============================================
-- 6. Verification queries (run these before dropping old tables)
-- ============================================
-- DO $$
-- DECLARE
-- v_old_stats INT;
-- v_football_stats INT;
-- v_basketball_stats INT;
-- v_old_ai INT;
-- v_football_ai INT;
-- v_basketball_ai INT;
-- BEGIN
-- SELECT COUNT(*) INTO v_old_stats FROM match_team_stats;
-- SELECT COUNT(*) INTO v_football_stats FROM football_team_stats;
-- SELECT COUNT(*) INTO v_basketball_stats FROM basketball_team_stats;
--
-- SELECT COUNT(*) INTO v_old_ai FROM match_ai_features;
-- SELECT COUNT(*) INTO v_football_ai FROM football_ai_features;
-- SELECT COUNT(*) INTO v_basketball_ai FROM basketball_ai_features;
--
-- RAISE NOTICE '=== VERIFICATION ===';
-- RAISE NOTICE 'match_team_stats: % = football: % + basketball: %', v_old_stats, v_football_stats, v_basketball_stats;
-- RAISE NOTICE 'match_ai_features: % = football: % + basketball: %', v_old_ai, v_football_ai, v_basketball_ai;
--
-- IF v_old_stats != v_football_stats + v_basketball_stats THEN
-- RAISE EXCEPTION 'Data mismatch in team stats!';
-- END IF;
--
-- IF v_old_ai != v_football_ai + v_basketball_ai THEN
-- RAISE EXCEPTION 'Data mismatch in AI features!';
-- END IF;
--
-- RAISE NOTICE '✅ VERIFICATION PASSED - Safe to drop old tables';
-- END $$;
-- ============================================
-- 7. Drop old tables (UNCOMMENT AFTER VERIFICATION)
-- ============================================
-- DROP TABLE IF EXISTS match_team_stats CASCADE;
-- DROP TABLE IF EXISTS match_ai_features CASCADE;
COMMIT;