137 lines
6.1 KiB
SQL
137 lines
6.1 KiB
SQL
-- +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;
|