package main import ( "context" "database/sql" ) const schemaSQL = ` -- ---- Phase 9 tables ---- CREATE TABLE IF NOT EXISTS predictions ( id BIGSERIAL PRIMARY KEY, match_id VARCHAR(32) NOT NULL REFERENCES matches(match_id), predictor_id VARCHAR(64) NOT NULL, predicted_bot VARCHAR(16) NOT NULL, confidence SMALLINT CHECK (confidence >= 1 AND confidence <= 100), correct BOOLEAN, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), resolved_at TIMESTAMPTZ, UNIQUE(match_id, predictor_id) ); CREATE INDEX IF NOT EXISTS idx_predictions_match ON predictions(match_id); CREATE INDEX IF NOT EXISTS idx_predictions_predictor ON predictions(predictor_id); CREATE TABLE IF NOT EXISTS predictor_stats ( predictor_id VARCHAR(64) PRIMARY KEY, correct INTEGER NOT NULL DEFAULT 0, incorrect INTEGER NOT NULL DEFAULT 0, streak INTEGER NOT NULL DEFAULT 0, best_streak INTEGER NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS series ( id BIGSERIAL PRIMARY KEY, bot_a_id VARCHAR(16) NOT NULL REFERENCES bots(bot_id), bot_b_id VARCHAR(16) NOT NULL REFERENCES bots(bot_id), format INTEGER NOT NULL DEFAULT 5, -- best of N (3, 5, 7...) a_wins INTEGER NOT NULL DEFAULT 0, b_wins INTEGER NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'active', winner_id VARCHAR(16), season_id BIGINT REFERENCES seasons(id), bracket_round VARCHAR(32), -- 'quarterfinal', 'semifinal', 'final' for championship bracket_position INTEGER, -- position within the bracket round (0-based) featured BOOLEAN NOT NULL DEFAULT FALSE, -- weekly featured series (Friday 20:00 UTC) created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_series_bots ON series(bot_a_id, bot_b_id); CREATE INDEX IF NOT EXISTS idx_series_status ON series(status); CREATE INDEX IF NOT EXISTS idx_series_season ON series(season_id); CREATE INDEX IF NOT EXISTS idx_series_bracket ON series(season_id, bracket_round) WHERE bracket_round IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_series_featured ON series(featured, created_at DESC) WHERE featured = TRUE; -- Add bracket columns if they don't exist (idempotent migration) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'series' AND column_name = 'bracket_round') THEN ALTER TABLE series ADD COLUMN bracket_round VARCHAR(32); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'series' AND column_name = 'bracket_position') THEN ALTER TABLE series ADD COLUMN bracket_position INTEGER; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'series' AND column_name = 'featured') THEN ALTER TABLE series ADD COLUMN featured BOOLEAN NOT NULL DEFAULT FALSE; END IF; END $$; -- Add missing foreign key constraints (CREATE TABLE IF NOT EXISTS doesn't add FKs to existing tables) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'replay_feedback_match_id_fkey') THEN ALTER TABLE replay_feedback ADD CONSTRAINT replay_feedback_match_id_fkey FOREIGN KEY (match_id) REFERENCES matches(match_id); END IF; END $$; CREATE TABLE IF NOT EXISTS series_games ( id BIGSERIAL PRIMARY KEY, series_id BIGINT NOT NULL REFERENCES series(id), match_id VARCHAR(32) REFERENCES matches(match_id), game_num INTEGER NOT NULL, winner_id VARCHAR(16), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_series_games_series ON series_games(series_id); CREATE TABLE IF NOT EXISTS seasons ( id BIGSERIAL PRIMARY KEY, name VARCHAR(64) NOT NULL, theme VARCHAR(128), rules_version VARCHAR(32) NOT NULL DEFAULT '1.0', status VARCHAR(16) NOT NULL DEFAULT 'active', champion_id VARCHAR(16), starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ends_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS season_snapshots ( id BIGSERIAL PRIMARY KEY, season_id BIGINT NOT NULL REFERENCES seasons(id), bot_id VARCHAR(16) NOT NULL REFERENCES bots(bot_id), rank INTEGER NOT NULL, rating DOUBLE PRECISION NOT NULL, wins INTEGER NOT NULL DEFAULT 0, losses INTEGER NOT NULL DEFAULT 0, recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_season_snapshots_season ON season_snapshots(season_id, rank); -- Map engagement scores (written by acb-mapgen or evolution pipeline) CREATE TABLE IF NOT EXISTS map_scores ( map_id VARCHAR(32) PRIMARY KEY, engagement DOUBLE PRECISION NOT NULL DEFAULT 0.0, symmetry_score DOUBLE PRECISION NOT NULL DEFAULT 0.0, wall_density DOUBLE PRECISION NOT NULL DEFAULT 0.0, last_used_at TIMESTAMPTZ, match_count INTEGER NOT NULL DEFAULT 0, avg_turns DOUBLE PRECISION, scored_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Map lifecycle management (§14.6 Map Evolution) CREATE TABLE IF NOT EXISTS maps ( map_id VARCHAR(32) PRIMARY KEY, player_count INTEGER NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', -- active, probation, retired, classic engagement DOUBLE PRECISION NOT NULL DEFAULT 0.0, wall_density DOUBLE PRECISION NOT NULL DEFAULT 0.0, energy_count INTEGER NOT NULL DEFAULT 0, grid_width INTEGER NOT NULL, grid_height INTEGER NOT NULL, map_json JSONB NOT NULL, -- Full map layout with walls, energy, cores created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), retired_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_maps_status ON maps(status, player_count); CREATE INDEX IF NOT EXISTS idx_maps_engagement ON maps(player_count, engagement DESC); -- User voting on maps (§14.6 Map Evolution) CREATE TABLE IF NOT EXISTS map_votes ( id BIGSERIAL PRIMARY KEY, map_id VARCHAR(32) NOT NULL REFERENCES maps(map_id) ON DELETE CASCADE, voter_id VARCHAR(64) NOT NULL, -- localStorage UUID vote SMALLINT NOT NULL, -- +1 or -1 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(map_id, voter_id) ); CREATE INDEX IF NOT EXISTS idx_map_votes_map ON map_votes(map_id); -- Positional fairness tracking (§14.6 Map Evolution) CREATE TABLE IF NOT EXISTS map_fairness ( map_id VARCHAR(32) NOT NULL REFERENCES maps(map_id) ON DELETE CASCADE, player_slot INTEGER NOT NULL, games INTEGER NOT NULL DEFAULT 0, wins INTEGER NOT NULL DEFAULT 0, last_check TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (map_id, player_slot) ); CREATE TABLE IF NOT EXISTS bots ( bot_id VARCHAR(16) PRIMARY KEY, name VARCHAR(32) UNIQUE NOT NULL, owner VARCHAR(128) NOT NULL, endpoint_url TEXT NOT NULL, shared_secret TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'pending', rating_mu DOUBLE PRECISION NOT NULL DEFAULT 1500.0, rating_phi DOUBLE PRECISION NOT NULL DEFAULT 350.0, rating_sigma DOUBLE PRECISION NOT NULL DEFAULT 0.06, evolved BOOLEAN NOT NULL DEFAULT FALSE, island VARCHAR(16), generation INTEGER, parent_ids JSONB, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_active TIMESTAMPTZ, consec_fails INTEGER NOT NULL DEFAULT 0, archetype VARCHAR(64), crash_strikes INTEGER NOT NULL DEFAULT 0, cooldown_until TIMESTAMPTZ, debug_public BOOLEAN NOT NULL DEFAULT FALSE ); -- Add debug_public column if it doesn't exist (idempotent migration) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'bots' AND column_name = 'debug_public') THEN ALTER TABLE bots ADD COLUMN debug_public BOOLEAN NOT NULL DEFAULT FALSE; END IF; END $$; CREATE TABLE IF NOT EXISTS matches ( match_id VARCHAR(32) PRIMARY KEY, map_id VARCHAR(32) NOT NULL, map_seed BIGINT, status VARCHAR(16) NOT NULL DEFAULT 'pending', winner INTEGER, condition VARCHAR(32), turn_count INTEGER, scores_json JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS match_participants ( match_id VARCHAR(32) NOT NULL REFERENCES matches(match_id), bot_id VARCHAR(16) NOT NULL REFERENCES bots(bot_id), player_slot INTEGER NOT NULL, score INTEGER, status VARCHAR(16), PRIMARY KEY (match_id, bot_id) ); CREATE TABLE IF NOT EXISTS jobs ( job_id VARCHAR(32) PRIMARY KEY, match_id VARCHAR(32) NOT NULL REFERENCES matches(match_id), status VARCHAR(16) NOT NULL DEFAULT 'pending', worker_id VARCHAR(64), config_json JSONB NOT NULL, claimed_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, heartbeat_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS rating_history ( bot_id VARCHAR(16) NOT NULL REFERENCES bots(bot_id), match_id VARCHAR(32) NOT NULL REFERENCES matches(match_id), rating DOUBLE PRECISION NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (bot_id, match_id) ); CREATE INDEX IF NOT EXISTS idx_rating_history_bot ON rating_history(bot_id, recorded_at); CREATE TABLE IF NOT EXISTS programs ( id BIGSERIAL PRIMARY KEY, code TEXT NOT NULL, language VARCHAR(32) NOT NULL, island VARCHAR(16) NOT NULL, generation INTEGER NOT NULL DEFAULT 0, parent_ids JSONB NOT NULL DEFAULT '[]', behavior_vector DOUBLE PRECISION[] NOT NULL DEFAULT '{}', fitness DOUBLE PRECISION NOT NULL DEFAULT 0.0, promoted BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_programs_island ON programs(island); CREATE INDEX IF NOT EXISTS idx_programs_island_fitness ON programs(island, fitness DESC); -- Curated playlist definitions (§14.4 Replay Playlists) CREATE TABLE IF NOT EXISTS playlists ( slug VARCHAR(64) PRIMARY KEY, title VARCHAR(128) NOT NULL, description TEXT NOT NULL DEFAULT '', category VARCHAR(32) NOT NULL DEFAULT 'featured', is_auto BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS playlist_matches ( playlist_slug VARCHAR(64) NOT NULL REFERENCES playlists(slug) ON DELETE CASCADE, match_id VARCHAR(32) NOT NULL REFERENCES matches(match_id), sort_order INTEGER NOT NULL DEFAULT 0, curation_tag TEXT NOT NULL DEFAULT '', added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (playlist_slug, match_id) ); CREATE INDEX IF NOT EXISTS idx_playlist_matches_playlist ON playlist_matches(playlist_slug, sort_order); -- Add combat_turns column to matches if it doesn't exist (idempotent migration) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'matches' AND column_name = 'combat_turns') THEN ALTER TABLE matches ADD COLUMN combat_turns INTEGER NOT NULL DEFAULT 0; END IF; END $$; -- Add enrichment_requested_at column to matches for idempotency (§13.3) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'matches' AND column_name = 'enrichment_requested_at') THEN ALTER TABLE matches ADD COLUMN enrichment_requested_at TIMESTAMPTZ; END IF; END $$; -- Community replay feedback (plan §13.6, §8.3) CREATE TABLE IF NOT EXISTS replay_feedback ( feedback_id VARCHAR(32) PRIMARY KEY, match_id VARCHAR(32) NOT NULL REFERENCES matches(match_id), turn INTEGER NOT NULL, type VARCHAR(16) NOT NULL CHECK (type IN ('insight', 'mistake', 'idea', 'highlight')), body TEXT NOT NULL, author VARCHAR(128) NOT NULL, upvotes INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_feedback_match ON replay_feedback(match_id, turn); -- Upvote deduplication: one upvote per visitor per feedback item CREATE TABLE IF NOT EXISTS feedback_upvotes ( feedback_id VARCHAR(32) NOT NULL REFERENCES replay_feedback(feedback_id) ON DELETE CASCADE, voter_id VARCHAR(36) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (feedback_id, voter_id) ); -- User-requested replay enrichment (plan §13.3) CREATE TABLE IF NOT EXISTS enrichment_requests ( request_id VARCHAR(32) PRIMARY KEY, match_id VARCHAR(32) NOT NULL REFERENCES matches(match_id), bot_id VARCHAR(16) NOT NULL REFERENCES bots(bot_id), status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), processed_at TIMESTAMPTZ, error_msg TEXT, UNIQUE(match_id, bot_id) ); CREATE INDEX IF NOT EXISTS idx_enrichment_requests_status ON enrichment_requests(status, requested_at); CREATE INDEX IF NOT EXISTS idx_enrichment_requests_bot ON enrichment_requests(bot_id, requested_at); ` func ensureSchema(ctx context.Context, db *sql.DB) error { _, err := db.ExecContext(ctx, schemaSQL) return err }