Build and Push Docker Container / build-and-push (push) Successful in 3m52s
- Add compact storage package: codec, records, hydrate, replay blobs, cache. - Running games write game_turns and snake_states keyed by integer ids. - Encode moves as 1-byte codes and bodies as head plus step codes. - Store food and hazards only when they changed since the previous turn. - Strip reasoning fields that duplicate stored values and restore on read. - Pack finished games into one zstd blob per game in game_replays. - Merge late turns into packed blobs during the stale-game sweep. - Extract SQLite and PostgreSQL schemas into dedicated modules. - Add read/import game bundle and list_game_ids to the backend template. - Add legacy readers and converter for the old turns/snake_turns layout. - Add migrate_gameplay_to_compact.py with in-place or cross-DB migration. - Verify every migrated game and keep legacy rows on failure. - Support --since, --minimum-quality and guarded --drop-legacy. - Add just recipe migrate-gameplay-postgres. - Rewrite merge script to copy game by game via backend bundles. - Move benchmark state loading into benchmark_states for both backends. - Update PostgreSQL cleanup script to the new replay tables. - Drop sqlite-zstd build stages from the Dockerfile. - Remove old migration scripts, normalized_turn and their tests. - Add tests for compact storage, schema, legacy migration and PostgreSQL. - Document the compact layout and migration workflow in the README.
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""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",
|
|
)
|