"""PostgreSQL schema for compact gameplay storage. Finished games live in ``game_replays`` as one zstd blob per game. Running games use ``game_turns`` and ``snake_states`` keyed by small integer ids (``games.gid`` / ``game_snakes.gsid``) instead of repeating text ids per row. """ TABLES_DDL = """ CREATE TABLE IF NOT EXISTS games ( game_id TEXT PRIMARY KEY, started_at TIMESTAMPTZ NOT NULL, ended_at TIMESTAMPTZ, width INTEGER, height INTEGER, source TEXT, map_name TEXT, ruleset_name TEXT, ruleset_version TEXT, your_snake_id TEXT, your_snake_name TEXT, your_snake_type TEXT, your_snake_version TEXT, game_type TEXT, winner_name TEXT, winner_you BOOLEAN NOT NULL DEFAULT FALSE, final_turn INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'running', has_replay BOOLEAN NOT NULL DEFAULT TRUE, quality_status TEXT NOT NULL DEFAULT 'retained', quality_score INTEGER, quality_tier TEXT, quality_reasons JSONB ); CREATE TABLE IF NOT EXISTS game_snakes ( game_id TEXT NOT NULL REFERENCES games(game_id) ON DELETE CASCADE, snake_id TEXT NOT NULL, snake_name TEXT, is_you BOOLEAN NOT NULL DEFAULT FALSE, customizations JSONB NOT NULL DEFAULT '{}'::jsonb, PRIMARY KEY (game_id, snake_id) ); ALTER TABLE games ADD COLUMN IF NOT EXISTS game_type TEXT; ALTER TABLE games ADD COLUMN IF NOT EXISTS your_snake_type TEXT; ALTER TABLE games ADD COLUMN IF NOT EXISTS your_snake_version TEXT; ALTER TABLE games ADD COLUMN IF NOT EXISTS winner_name TEXT; ALTER TABLE games ADD COLUMN IF NOT EXISTS has_replay BOOLEAN NOT NULL DEFAULT TRUE; ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_status TEXT NOT NULL DEFAULT 'retained'; ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_score INTEGER; ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_tier TEXT; ALTER TABLE games ADD COLUMN IF NOT EXISTS quality_reasons JSONB; ALTER TABLE game_snakes ADD COLUMN IF NOT EXISTS customizations JSONB NOT NULL DEFAULT '{}'::jsonb; ALTER TABLE games ADD COLUMN IF NOT EXISTS gid INTEGER GENERATED BY DEFAULT AS IDENTITY; ALTER TABLE game_snakes ADD COLUMN IF NOT EXISTS gsid INTEGER GENERATED BY DEFAULT AS IDENTITY; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'games_gid_key') THEN ALTER TABLE games ADD CONSTRAINT games_gid_key UNIQUE (gid); END IF; IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'game_snakes_gsid_key') THEN ALTER TABLE game_snakes ADD CONSTRAINT game_snakes_gsid_key UNIQUE (gsid); END IF; END $$; CREATE TABLE IF NOT EXISTS game_turns ( gid INTEGER NOT NULL REFERENCES games(gid) ON DELETE CASCADE, turn INTEGER NOT NULL, observed_at TIMESTAMPTZ NOT NULL, my_move SMALLINT, thinking JSONB, food BYTEA, hazards BYTEA, PRIMARY KEY (gid, turn) ); CREATE TABLE IF NOT EXISTS snake_states ( gsid INTEGER NOT NULL REFERENCES game_snakes(gsid) ON DELETE CASCADE, turn INTEGER NOT NULL, health SMALLINT, latency SMALLINT, body BYTEA NOT NULL, PRIMARY KEY (gsid, turn) ); CREATE TABLE IF NOT EXISTS game_replays ( gid INTEGER PRIMARY KEY REFERENCES games(gid) ON DELETE CASCADE, format SMALLINT NOT NULL, turn_count INTEGER NOT NULL, data BYTEA NOT NULL ); -- Replay blobs are already zstd-compressed; skip TOAST compression attempts. ALTER TABLE game_replays ALTER COLUMN data SET STORAGE EXTERNAL; CREATE INDEX IF NOT EXISTS idx_games_status ON games(status); """ # Superseded by the UNIQUE constraints of the legacy turns/snake_turns tables. # CONCURRENTLY avoids blocking writes; each must run outside a transaction. DROP_REDUNDANT_INDEXES = ( "DROP INDEX CONCURRENTLY IF EXISTS idx_turns_game_turn", "DROP INDEX CONCURRENTLY IF EXISTS idx_snake_turns_game_turn", )