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.
115 lines
3.4 KiB
Python
115 lines
3.4 KiB
Python
"""SQLite schema for compact gameplay storage (mirrors postgresql_schema)."""
|
|
|
|
import sqlite3
|
|
|
|
BASE_TABLES = """
|
|
CREATE TABLE IF NOT EXISTS games (
|
|
game_id TEXT PRIMARY KEY,
|
|
started_at TEXT NOT NULL,
|
|
ended_at TEXT,
|
|
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 INTEGER NOT NULL DEFAULT 0,
|
|
final_turn INTEGER NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'running',
|
|
has_replay INTEGER NOT NULL DEFAULT 1,
|
|
quality_status TEXT NOT NULL DEFAULT 'retained',
|
|
quality_score INTEGER,
|
|
quality_tier TEXT,
|
|
quality_reasons_json TEXT,
|
|
gid INTEGER
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS game_snakes (
|
|
game_id TEXT NOT NULL,
|
|
snake_id TEXT NOT NULL,
|
|
snake_name TEXT,
|
|
is_you INTEGER NOT NULL DEFAULT 0,
|
|
customizations_json TEXT NOT NULL DEFAULT '{}',
|
|
gsid INTEGER,
|
|
PRIMARY KEY (game_id, snake_id),
|
|
FOREIGN KEY (game_id) REFERENCES games(game_id) ON DELETE CASCADE
|
|
);
|
|
"""
|
|
|
|
# Columns added over time; older databases get them via ALTER TABLE.
|
|
LEGACY_COLUMNS = (
|
|
("games", "your_snake_type", "TEXT"),
|
|
("games", "your_snake_version", "TEXT"),
|
|
("games", "game_type", "TEXT"),
|
|
("games", "winner_name", "TEXT"),
|
|
("games", "has_replay", "INTEGER NOT NULL DEFAULT 1"),
|
|
("games", "quality_status", "TEXT NOT NULL DEFAULT 'retained'"),
|
|
("games", "quality_score", "INTEGER"),
|
|
("games", "quality_tier", "TEXT"),
|
|
("games", "quality_reasons_json", "TEXT"),
|
|
("games", "gid", "INTEGER"),
|
|
("game_snakes", "customizations_json", "TEXT NOT NULL DEFAULT '{}'"),
|
|
("game_snakes", "gsid", "INTEGER"),
|
|
)
|
|
|
|
COMPACT_TABLES = """
|
|
CREATE UNIQUE INDEX IF NOT EXISTS games_gid_key ON games(gid);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS game_snakes_gsid_key ON game_snakes(gsid);
|
|
CREATE INDEX IF NOT EXISTS idx_games_status ON games(status);
|
|
|
|
UPDATE games
|
|
SET gid = rowid + (SELECT COALESCE(MAX(gid), 0) FROM games)
|
|
WHERE gid IS NULL;
|
|
|
|
UPDATE game_snakes
|
|
SET gsid = rowid + (SELECT COALESCE(MAX(gsid), 0) FROM game_snakes)
|
|
WHERE gsid IS NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS game_turns (
|
|
gid INTEGER NOT NULL REFERENCES games(gid) ON DELETE CASCADE,
|
|
turn INTEGER NOT NULL,
|
|
observed_at TEXT NOT NULL,
|
|
my_move INTEGER,
|
|
thinking_json TEXT,
|
|
food BLOB,
|
|
hazards BLOB,
|
|
PRIMARY KEY (gid, turn)
|
|
) WITHOUT ROWID;
|
|
|
|
CREATE TABLE IF NOT EXISTS snake_states (
|
|
gsid INTEGER NOT NULL REFERENCES game_snakes(gsid) ON DELETE CASCADE,
|
|
turn INTEGER NOT NULL,
|
|
health INTEGER,
|
|
latency INTEGER,
|
|
body BLOB NOT NULL,
|
|
PRIMARY KEY (gsid, turn)
|
|
) WITHOUT ROWID;
|
|
|
|
CREATE TABLE IF NOT EXISTS game_replays (
|
|
gid INTEGER PRIMARY KEY REFERENCES games(gid) ON DELETE CASCADE,
|
|
format INTEGER NOT NULL,
|
|
turn_count INTEGER NOT NULL,
|
|
data BLOB NOT NULL
|
|
);
|
|
|
|
DROP INDEX IF EXISTS idx_turns_game_turn;
|
|
DROP INDEX IF EXISTS idx_snake_turns_game_turn;
|
|
"""
|
|
|
|
def ensure_column(connection:sqlite3.Connection, table:str, column:str, column_type:str) -> None:
|
|
existing = {row[1] for row in connection.execute(f"PRAGMA table_info({table})")}
|
|
if column not in existing:
|
|
connection.execute(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}")
|
|
|
|
def create_schema(connection:sqlite3.Connection) -> None:
|
|
connection.executescript(BASE_TABLES)
|
|
for table, column, column_type in LEGACY_COLUMNS:
|
|
ensure_column(connection, table, column, column_type)
|
|
connection.executescript(COMPACT_TABLES)
|