-- +goose Up -- Initial Game Master PostgreSQL schema. -- -- Four tables cover the durable surface of the service: -- * runtime_records — one row per game with the latest known runtime -- status, scheduling state, and engine health summary; -- * engine_versions — the deployable engine version registry consumed -- by Lobby's start flow and the GM admin/patch flow; -- * player_mappings — the (game_id, user_id) → (race_name, -- engine_player_uuid) projection installed at register-runtime; -- * operation_log — append-only audit of every register-runtime, -- turn-generation, force-next-turn, banish, stop, patch, and -- engine-version mutation GM performed. -- -- Schema and the matching `gamemasterservice` role are provisioned -- outside this script (in tests via cmd/jetgen/main.go::provisionRoleAndSchema; -- in production via an ops init script). This migration runs as the -- schema owner with `search_path=gamemaster` and only contains DDL for -- the service-owned tables and indexes. ARCHITECTURE.md §Database topology -- mandates that the per-service role's grants stay restricted to its own -- schema; consequently this file deliberately deviates from PLAN.md -- Stage 09's literal `CREATE SCHEMA IF NOT EXISTS gamemaster;` instruction. -- runtime_records holds one durable record per game with the latest -- known runtime status, scheduling state, and engine health summary. -- The status enum is enforced by a CHECK so domain code can rely on it -- without reading every callsite. The composite (status, -- next_generation_at) index drives the scheduler ticker scan that -- selects `status='running' AND next_generation_at <= now()` once per -- second. next_generation_at is nullable: a row enters with -- status='starting' and a null tick, and only acquires a tick when the -- register-runtime CAS flips it to 'running'. CREATE TABLE runtime_records ( game_id text PRIMARY KEY, status text NOT NULL, engine_endpoint text NOT NULL, current_image_ref text NOT NULL, current_engine_version text NOT NULL, turn_schedule text NOT NULL, current_turn integer NOT NULL DEFAULT 0, next_generation_at timestamptz, skip_next_tick boolean NOT NULL DEFAULT false, engine_health text NOT NULL DEFAULT '', created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, started_at timestamptz, stopped_at timestamptz, finished_at timestamptz, CONSTRAINT runtime_records_status_chk CHECK (status IN ( 'starting', 'running', 'generation_in_progress', 'generation_failed', 'stopped', 'engine_unreachable', 'finished' )) ); CREATE INDEX runtime_records_status_next_gen_idx ON runtime_records (status, next_generation_at); -- engine_versions is the deployable engine version registry. Each row -- ties a semver string to a Docker reference and a free-form options -- document; the status enum gates the start flow (active versions are -- accepted by Lobby's resolve, deprecated versions are rejected on new -- starts but remain valid for already-running games). `options` is -- jsonb: v1 stores it verbatim and never element-filters. CREATE TABLE engine_versions ( version text PRIMARY KEY, image_ref text NOT NULL, options jsonb NOT NULL DEFAULT '{}'::jsonb, status text NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, CONSTRAINT engine_versions_status_chk CHECK (status IN ('active', 'deprecated')) ); -- player_mappings carries the (game_id, user_id) → (race_name, -- engine_player_uuid) projection installed at register-runtime. The -- composite primary key both serves the lookups by (game_id, user_id) -- on every command/order/report request and as a leftmost-prefix index -- for the per-game roster reads (`WHERE game_id = $1`). The partial -- UNIQUE index on (game_id, race_name) enforces the one-race-per-game -- invariant at the storage boundary. CREATE TABLE player_mappings ( game_id text NOT NULL, user_id text NOT NULL, race_name text NOT NULL, engine_player_uuid text NOT NULL, created_at timestamptz NOT NULL, PRIMARY KEY (game_id, user_id) ); CREATE UNIQUE INDEX player_mappings_game_race_uniq ON player_mappings (game_id, race_name); -- operation_log is an append-only audit of every operation Game Master -- performed against a game's runtime or against the engine version -- registry. The (game_id, started_at DESC) index drives audit reads -- from the GM/Admin REST surface. finished_at is nullable for in-flight -- rows even though the service layer always finalises the row. The -- op_kind / op_source / outcome enums are enforced by CHECK constraints -- to keep the audit schema honest without a separate Go validator. CREATE TABLE operation_log ( id bigserial PRIMARY KEY, game_id text NOT NULL, op_kind text NOT NULL, op_source text NOT NULL, source_ref text NOT NULL DEFAULT '', outcome text NOT NULL, error_code text NOT NULL DEFAULT '', error_message text NOT NULL DEFAULT '', started_at timestamptz NOT NULL, finished_at timestamptz, CONSTRAINT operation_log_op_kind_chk CHECK (op_kind IN ( 'register_runtime', 'turn_generation', 'force_next_turn', 'banish', 'stop', 'patch', 'engine_version_create', 'engine_version_update', 'engine_version_deprecate', 'engine_version_delete' )), CONSTRAINT operation_log_op_source_chk CHECK (op_source IN ( 'gateway_player', 'lobby_internal', 'admin_rest' )), CONSTRAINT operation_log_outcome_chk CHECK (outcome IN ('success', 'failure')) ); CREATE INDEX operation_log_game_started_idx ON operation_log (game_id, started_at DESC); -- +goose Down DROP TABLE IF EXISTS operation_log; DROP TABLE IF EXISTS player_mappings; DROP TABLE IF EXISTS engine_versions; DROP TABLE IF EXISTS runtime_records;