mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
178 lines
6.8 KiB
SQL
178 lines
6.8 KiB
SQL
-- Cosmic Clash matchmaking control plane, migration 0001.
|
|
-- PostgreSQL is the durable authority. Redis indexes are rebuildable and do
|
|
-- not appear in this schema or in any ownership constraint.
|
|
|
|
CREATE TABLE identities (
|
|
player_id TEXT PRIMARY KEY,
|
|
steam_id TEXT NOT NULL UNIQUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
banned_until TIMESTAMPTZ,
|
|
ban_reason TEXT
|
|
);
|
|
|
|
CREATE TABLE sessions (
|
|
session_id TEXT PRIMARY KEY,
|
|
player_id TEXT NOT NULL REFERENCES identities(player_id),
|
|
token_digest BYTEA NOT NULL UNIQUE,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
revoked_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE idempotency_keys (
|
|
scope TEXT NOT NULL,
|
|
idempotency_key TEXT NOT NULL CHECK (char_length(idempotency_key) BETWEEN 16 AND 128),
|
|
payload_digest BYTEA NOT NULL,
|
|
result JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (scope, idempotency_key)
|
|
);
|
|
|
|
CREATE TABLE queue_tickets (
|
|
ticket_id TEXT PRIMARY KEY,
|
|
player_id TEXT NOT NULL REFERENCES identities(player_id),
|
|
playlist TEXT NOT NULL CHECK (playlist IN ('casual', 'ranked')),
|
|
state TEXT NOT NULL CHECK (state IN ('QUEUED', 'PROPOSED', 'ACCEPTED', 'ALLOCATING', 'PROCESS_READY', 'ASSIGNMENT_READY', 'ASSIGNED', 'CONNECTING', 'LIVE', 'RESULT_PENDING', 'COMPLETED', 'CANCELLED', 'EXPIRED', 'FAILED')),
|
|
client_build TEXT NOT NULL,
|
|
protocol_version INTEGER NOT NULL CHECK (protocol_version > 0),
|
|
enqueued_at TIMESTAMPTZ NOT NULL,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
revision BIGINT NOT NULL DEFAULT 0 CHECK (revision >= 0),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE UNIQUE INDEX queue_tickets_one_active_per_player
|
|
ON queue_tickets (player_id)
|
|
WHERE state IN ('QUEUED', 'PROPOSED', 'ACCEPTED', 'ALLOCATING', 'PROCESS_READY', 'ASSIGNMENT_READY', 'ASSIGNED', 'CONNECTING', 'LIVE', 'RESULT_PENDING');
|
|
|
|
CREATE TABLE proposals (
|
|
proposal_id TEXT PRIMARY KEY,
|
|
playlist TEXT NOT NULL CHECK (playlist IN ('casual', 'ranked')),
|
|
state TEXT NOT NULL CHECK (state IN ('OPEN', 'ACCEPTED', 'DECLINED', 'EXPIRED', 'CANCELLED')),
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
revision BIGINT NOT NULL DEFAULT 0 CHECK (revision >= 0),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE proposal_participants (
|
|
proposal_id TEXT NOT NULL REFERENCES proposals(proposal_id),
|
|
player_id TEXT NOT NULL REFERENCES identities(player_id),
|
|
ticket_id TEXT NOT NULL REFERENCES queue_tickets(ticket_id),
|
|
response TEXT NOT NULL CHECK (response IN ('PENDING', 'ACCEPTED', 'DECLINED', 'TIMED_OUT')),
|
|
responded_at TIMESTAMPTZ,
|
|
PRIMARY KEY (proposal_id, player_id),
|
|
UNIQUE (proposal_id, ticket_id)
|
|
);
|
|
|
|
CREATE TABLE matches (
|
|
match_id TEXT PRIMARY KEY,
|
|
playlist TEXT NOT NULL CHECK (playlist IN ('casual', 'ranked')),
|
|
state TEXT NOT NULL CHECK (state IN ('ALLOCATING', 'PROCESS_READY', 'ASSIGNMENT_READY', 'ASSIGNED', 'CONNECTING', 'LIVE', 'RESULT_PENDING', 'COMPLETED', 'CANCELLED', 'FAILED')),
|
|
region TEXT NOT NULL CHECK (region IN ('EU', 'NA')),
|
|
protocol_version INTEGER NOT NULL CHECK (protocol_version > 0),
|
|
server_id TEXT,
|
|
revision BIGINT NOT NULL DEFAULT 0 CHECK (revision >= 0),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
completed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE TABLE match_participants (
|
|
match_id TEXT NOT NULL REFERENCES matches(match_id),
|
|
player_id TEXT NOT NULL REFERENCES identities(player_id),
|
|
ticket_id TEXT NOT NULL REFERENCES queue_tickets(ticket_id),
|
|
slot INTEGER NOT NULL CHECK (slot BETWEEN 0 AND 5),
|
|
team INTEGER NOT NULL CHECK (team IN (0, 1)),
|
|
connection_generation BIGINT NOT NULL DEFAULT 0 CHECK (connection_generation >= 0),
|
|
connected_at TIMESTAMPTZ,
|
|
abandoned_at TIMESTAMPTZ,
|
|
participation_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
PRIMARY KEY (match_id, player_id),
|
|
UNIQUE (match_id, slot)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX match_participants_one_active_match
|
|
ON match_participants (player_id)
|
|
WHERE participation_active;
|
|
|
|
CREATE TABLE ratings (
|
|
player_id TEXT PRIMARY KEY REFERENCES identities(player_id),
|
|
rating DOUBLE PRECISION NOT NULL DEFAULT 1500,
|
|
deviation DOUBLE PRECISION NOT NULL DEFAULT 350,
|
|
volatility DOUBLE PRECISION NOT NULL DEFAULT 0.06,
|
|
ranked_games INTEGER NOT NULL DEFAULT 0 CHECK (ranked_games >= 0),
|
|
revision BIGINT NOT NULL DEFAULT 0 CHECK (revision >= 0),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE seasons (
|
|
season_id TEXT PRIMARY KEY,
|
|
playlist TEXT NOT NULL CHECK (playlist = 'ranked'),
|
|
starts_at TIMESTAMPTZ NOT NULL,
|
|
ends_at TIMESTAMPTZ NOT NULL CHECK (ends_at > starts_at),
|
|
rolled_over_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE TABLE ranked_season_rollovers (
|
|
player_id TEXT NOT NULL REFERENCES identities(player_id),
|
|
season_id TEXT NOT NULL REFERENCES seasons(season_id),
|
|
rating DOUBLE PRECISION NOT NULL,
|
|
deviation DOUBLE PRECISION NOT NULL,
|
|
volatility DOUBLE PRECISION NOT NULL,
|
|
ranked_games INTEGER NOT NULL CHECK (ranked_games >= 0),
|
|
rolled_over_at TIMESTAMPTZ NOT NULL,
|
|
PRIMARY KEY (player_id, season_id)
|
|
);
|
|
|
|
CREATE TABLE penalties (
|
|
penalty_id TEXT PRIMARY KEY,
|
|
player_id TEXT NOT NULL REFERENCES identities(player_id),
|
|
match_id TEXT REFERENCES matches(match_id),
|
|
playlist TEXT NOT NULL CHECK (playlist IN ('casual', 'ranked')),
|
|
kind TEXT NOT NULL,
|
|
starts_at TIMESTAMPTZ NOT NULL,
|
|
ends_at TIMESTAMPTZ NOT NULL CHECK (ends_at > starts_at),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE result_receipts (
|
|
result_id TEXT PRIMARY KEY,
|
|
match_id TEXT NOT NULL UNIQUE REFERENCES matches(match_id),
|
|
result_nonce TEXT NOT NULL UNIQUE,
|
|
payload_digest BYTEA NOT NULL,
|
|
integrity_state TEXT NOT NULL CHECK (integrity_state IN ('CERTIFIED', 'SUPPRESSED', 'REVIEW')),
|
|
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
committed_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE TABLE outbox (
|
|
event_id TEXT PRIMARY KEY,
|
|
aggregate_type TEXT NOT NULL,
|
|
aggregate_id TEXT NOT NULL,
|
|
revision BIGINT NOT NULL CHECK (revision >= 0),
|
|
event_type TEXT NOT NULL,
|
|
payload JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
published_at TIMESTAMPTZ,
|
|
UNIQUE (aggregate_type, aggregate_id, revision)
|
|
);
|
|
|
|
CREATE TABLE audit_events (
|
|
audit_id BIGSERIAL PRIMARY KEY,
|
|
actor_type TEXT NOT NULL CHECK (actor_type IN ('PLAYER', 'SERVER', 'SYSTEM', 'ADMIN')),
|
|
actor_id TEXT,
|
|
action TEXT NOT NULL,
|
|
aggregate_type TEXT NOT NULL,
|
|
aggregate_id TEXT NOT NULL,
|
|
request_id TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX queue_tickets_candidate_order
|
|
ON queue_tickets (playlist, enqueued_at, ticket_id)
|
|
WHERE state = 'QUEUED';
|
|
|
|
CREATE INDEX outbox_unpublished_order
|
|
ON outbox (created_at, event_id)
|
|
WHERE published_at IS NULL;
|