-- 20260717000000_schema.sql — Consolidated schema for x0gp catalog, races, and drivers. -- --------------------------------------------------------------------------- -- Tracks & Waypoints -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS tracks ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', author_id TEXT NOT NULL DEFAULT 'system', visibility TEXT NOT NULL DEFAULT 'system' CHECK (visibility IN ('system', 'public', 'private')), scale INT NOT NULL DEFAULT 27, length_m DOUBLE PRECISION NOT NULL DEFAULT 0, width_m DOUBLE PRECISION NOT NULL DEFAULT 0, lane_width_m DOUBLE PRECISION NOT NULL DEFAULT 0.20, surface TEXT NOT NULL DEFAULT 'carpet' CHECK (surface IN ('carpet', 'tile', 'wood', 'paper', 'mixed')), best_lap_ms BIGINT NOT NULL DEFAULT 0, best_lap_holder TEXT NOT NULL DEFAULT '', bounds_min_x DOUBLE PRECISION NOT NULL DEFAULT 0, bounds_min_y DOUBLE PRECISION NOT NULL DEFAULT 0, bounds_max_x DOUBLE PRECISION NOT NULL DEFAULT 0, bounds_max_y DOUBLE PRECISION NOT NULL DEFAULT 0, created_ms BIGINT NOT NULL, updated_ms BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS tracks_visibility_idx ON tracks (visibility); CREATE INDEX IF NOT EXISTS tracks_author_idx ON tracks (author_id); CREATE TABLE IF NOT EXISTS track_waypoints ( track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, seq INT NOT NULL, x DOUBLE PRECISION NOT NULL, y DOUBLE PRECISION NOT NULL, heading_rad DOUBLE PRECISION NOT NULL, speed_ms DOUBLE PRECISION NOT NULL, curvature DOUBLE PRECISION NOT NULL, PRIMARY KEY (track_id, seq) ); CREATE TABLE IF NOT EXISTS track_tags ( track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, tag TEXT NOT NULL, PRIMARY KEY (track_id, tag) ); CREATE INDEX IF NOT EXISTS track_tags_tag_idx ON track_tags (tag); -- --------------------------------------------------------------------------- -- Cars -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS cars ( id TEXT PRIMARY KEY, name TEXT NOT NULL, owner_id TEXT NOT NULL DEFAULT 'system', visibility TEXT NOT NULL DEFAULT 'system' CHECK (visibility IN ('system', 'public', 'private')), scale INT NOT NULL DEFAULT 27, length_mm DOUBLE PRECISION NOT NULL, width_mm DOUBLE PRECISION NOT NULL, height_mm DOUBLE PRECISION NOT NULL DEFAULT 0, weight_g DOUBLE PRECISION NOT NULL DEFAULT 0, chassis_model TEXT NOT NULL DEFAULT '', chassis_material TEXT NOT NULL DEFAULT '', chassis_printed BOOLEAN NOT NULL DEFAULT FALSE, chassis_wheelbase_mm DOUBLE PRECISION NOT NULL DEFAULT 0, chassis_track_mm DOUBLE PRECISION NOT NULL DEFAULT 0, motor_kind TEXT NOT NULL DEFAULT 'brushed', motor_class TEXT NOT NULL DEFAULT '', motor_kv INT NOT NULL DEFAULT 0, motor_power_w DOUBLE PRECISION NOT NULL DEFAULT 0, battery_voltage_v DOUBLE PRECISION NOT NULL DEFAULT 0, battery_capacity_mah INT NOT NULL DEFAULT 0, battery_cells INT NOT NULL DEFAULT 0, battery_chemistry TEXT NOT NULL DEFAULT 'li-po', drive TEXT NOT NULL DEFAULT '2WD' CHECK (drive IN ('2WD', '4WD')), top_speed_ms DOUBLE PRECISION NOT NULL DEFAULT 0, color_hex TEXT NOT NULL DEFAULT '#ff6b1a', avatar_url TEXT NOT NULL DEFAULT '', active BOOLEAN NOT NULL DEFAULT TRUE, total_distance_m DOUBLE PRECISION NOT NULL DEFAULT 0, total_races INT NOT NULL DEFAULT 0, total_laps INT NOT NULL DEFAULT 0, best_lap_ms BIGINT NOT NULL DEFAULT 0, created_ms BIGINT NOT NULL, updated_ms BIGINT NOT NULL, device_id INT ); CREATE INDEX IF NOT EXISTS cars_visibility_idx ON cars (visibility); CREATE INDEX IF NOT EXISTS cars_owner_idx ON cars (owner_id); -- --------------------------------------------------------------------------- -- Clans & Drivers -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS clans ( id TEXT PRIMARY KEY, tag TEXT NOT NULL UNIQUE, name TEXT NOT NULL, avatar_url TEXT NOT NULL DEFAULT '', created_ms BIGINT NOT NULL, updated_ms BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS clans_tag_idx ON clans (tag); CREATE TABLE IF NOT EXISTS drivers ( id TEXT PRIMARY KEY, nickname TEXT NOT NULL UNIQUE, name TEXT NOT NULL, avatar_url TEXT NOT NULL DEFAULT '', clan_id TEXT REFERENCES clans(id) ON DELETE SET NULL, created_ms BIGINT NOT NULL, updated_ms BIGINT NOT NULL ); CREATE INDEX IF NOT EXISTS drivers_nickname_idx ON drivers (nickname); CREATE INDEX IF NOT EXISTS drivers_clan_idx ON drivers (clan_id); -- --------------------------------------------------------------------------- -- Races & Race Drivers -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS races ( id TEXT PRIMARY KEY, name TEXT NOT NULL, track_id TEXT NOT NULL DEFAULT '', max_cars INT NOT NULL, laps INT NOT NULL, time_limit_s INT NOT NULL, status TEXT NOT NULL DEFAULT 'lobby' CHECK (status IN ('lobby', 'countdown', 'racing', 'finished', 'cancelled')), created_ms BIGINT NOT NULL, started_ms BIGINT NOT NULL DEFAULT 0, finished_ms BIGINT NOT NULL DEFAULT 0, duration_ms BIGINT NOT NULL DEFAULT 0, total_laps INT NOT NULL DEFAULT 0, total_drivers INT NOT NULL DEFAULT 0, updated_ms BIGINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS races_status_finished_idx ON races (status, finished_ms DESC, id DESC); CREATE INDEX IF NOT EXISTS races_started_idx ON races (status, started_ms DESC, id DESC); CREATE INDEX IF NOT EXISTS races_track_idx ON races (track_id); CREATE INDEX IF NOT EXISTS races_status_idx ON races (status); CREATE TABLE IF NOT EXISTS race_drivers ( race_id TEXT NOT NULL REFERENCES races(id) ON DELETE CASCADE, driver_id TEXT NOT NULL, slot INT NOT NULL DEFAULT 0, joined_ms BIGINT NOT NULL, total_time_ms BIGINT NOT NULL DEFAULT 0, best_lap_ms BIGINT NOT NULL DEFAULT 0, position INT, PRIMARY KEY (race_id, driver_id) ); CREATE INDEX IF NOT EXISTS race_drivers_driver_idx ON race_drivers (driver_id); CREATE INDEX IF NOT EXISTS race_drivers_podium_idx ON race_drivers (race_id, total_time_ms ASC, position ASC) WHERE total_time_ms > 0; -- --------------------------------------------------------------------------- -- Race Plans & Queue -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS race_plans ( id TEXT PRIMARY KEY, name TEXT NOT NULL, track_id TEXT NOT NULL DEFAULT 'default', max_cars INT NOT NULL, laps INT NOT NULL DEFAULT 0, time_limit_s INT NOT NULL DEFAULT 0, start_at_ms BIGINT NOT NULL, interval_s INT NOT NULL DEFAULT 0, -- 0 = single-shot count INT NOT NULL DEFAULT 0, -- 0 = repeat forever enabled BOOLEAN NOT NULL DEFAULT TRUE, created_ms BIGINT NOT NULL, updated_ms BIGINT NOT NULL, next_fire_ms BIGINT NOT NULL, -- last materialised fire time fires_done INT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS race_plans_start_idx ON race_plans (start_at_ms ASC, id ASC); CREATE INDEX IF NOT EXISTS race_plans_enabled_idx ON race_plans (enabled, next_fire_ms); CREATE TABLE IF NOT EXISTS race_queue ( driver_id TEXT NOT NULL, race_id TEXT NOT NULL, plan_id TEXT NOT NULL DEFAULT '', enqueued_ms BIGINT NOT NULL, PRIMARY KEY (driver_id, race_id) ); CREATE INDEX IF NOT EXISTS race_queue_race_idx ON race_queue (race_id); CREATE INDEX IF NOT EXISTS race_queue_driver_idx ON race_queue (driver_id, enqueued_ms); -- --------------------------------------------------------------------------- -- Lobby Drivers -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS lobby_drivers ( driver_id TEXT PRIMARY KEY, name TEXT NOT NULL DEFAULT '', nickname TEXT NOT NULL DEFAULT '', avatar_url TEXT NOT NULL DEFAULT '', clan_id TEXT NOT NULL DEFAULT '', clan_tag TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'idle' CHECK (status IN ('idle', 'racing', 'offline')), current_race_id TEXT NOT NULL DEFAULT '', connected_ms BIGINT NOT NULL, last_seen_ms BIGINT NOT NULL, car_id TEXT ); CREATE INDEX IF NOT EXISTS lobby_drivers_status_idx ON lobby_drivers (status); -- --------------------------------------------------------------------------- -- Track Calendar -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS track_calendar ( id SERIAL PRIMARY KEY, track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE, start_at TIMESTAMP WITH TIME ZONE NOT NULL, end_at TIMESTAMP WITH TIME ZONE NOT NULL, CHECK (start_at < end_at) ); CREATE INDEX IF NOT EXISTS track_calendar_dates_idx ON track_calendar (start_at, end_at); -- --------------------------------------------------------------------------- -- Go Migration Metadata (Bridges Go server startup migrations and Supabase) -- --------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now() ); INSERT INTO schema_migrations (version) VALUES ('001_init.sql'), ('002_seed.sql'), ('003_f1_seed.sql'), ('004_monaco_rescale.sql'), ('005_races.sql'), ('006_drop_host.sql'), ('007_podium.sql'), ('008_drivers_clans.sql'), ('009_live_persistence.sql'), ('010_unify_races.sql'), ('011_drop_driver_ids.sql'), ('012_normalize_race_results.sql'), ('013_driver_device.sql'), ('014_track_calendar.sql'), ('015_driver_car_id.sql') ON CONFLICT (version) DO NOTHING;