Files
Stat-Dashboard/mockups/db/001_initial.sql

131 lines
5.7 KiB
SQL

CREATE SCHEMA IF NOT EXISTS analytics;
SET search_path TO analytics, public;
CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE games (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
roblox_experience_id bigint,
timezone text NOT NULL DEFAULT 'UTC',
properties jsonb NOT NULL DEFAULT '{}',
sample_key text UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE players (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
roblox_user_id bigint NOT NULL,
display_name text,
first_seen timestamptz NOT NULL,
last_seen timestamptz,
metadata jsonb NOT NULL DEFAULT '{}',
UNIQUE (game_id, id), UNIQUE (game_id, roblox_user_id)
);
CREATE TABLE sessions (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
player_id bigint NOT NULL,
started_at timestamptz NOT NULL,
ended_at timestamptz,
place_name text,
stats jsonb NOT NULL DEFAULT '{}',
UNIQUE (game_id, id), UNIQUE (game_id, player_id, id),
FOREIGN KEY (game_id, player_id) REFERENCES players(game_id, id),
CHECK (ended_at IS NULL OR ended_at >= started_at)
);
CREATE TABLE player_snapshots (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
player_id bigint NOT NULL,
captured_at timestamptz NOT NULL,
data jsonb NOT NULL,
FOREIGN KEY (game_id, player_id) REFERENCES players(game_id, id),
CHECK (jsonb_typeof(data) = 'object')
);
CREATE TABLE activity_definitions (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
key text NOT NULL,
name text NOT NULL,
start_event text NOT NULL,
end_event text,
configuration jsonb NOT NULL DEFAULT '{}',
UNIQUE (game_id, id), UNIQUE (game_id, key)
);
CREATE TABLE activity_runs (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
definition_id bigint NOT NULL,
player_id bigint NOT NULL,
session_id bigint,
started_at timestamptz NOT NULL,
ended_at timestamptz,
outcome text,
step_count integer,
prompt_count integer,
captured_values jsonb NOT NULL DEFAULT '{}',
FOREIGN KEY (game_id, definition_id) REFERENCES activity_definitions(game_id, id),
FOREIGN KEY (game_id, player_id) REFERENCES players(game_id, id),
FOREIGN KEY (game_id, player_id, session_id) REFERENCES sessions(game_id, player_id, id),
CHECK (ended_at IS NULL OR ended_at >= started_at)
);
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
category text NOT NULL DEFAULT 'events',
name text NOT NULL,
player_id bigint,
session_id bigint,
occurred_at timestamptz NOT NULL,
properties jsonb NOT NULL DEFAULT '{}',
FOREIGN KEY (game_id, player_id) REFERENCES players(game_id, id),
FOREIGN KEY (game_id, player_id, session_id) REFERENCES sessions(game_id, player_id, id),
CHECK (session_id IS NULL OR player_id IS NOT NULL)
);
CREATE TABLE game_metrics (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
game_id bigint NOT NULL REFERENCES games(id),
category text NOT NULL,
name text NOT NULL,
recorded_at timestamptz NOT NULL,
value double precision,
properties jsonb NOT NULL DEFAULT '{}',
UNIQUE (game_id, category, name, recorded_at)
);
CREATE TABLE player_fields (
game_id bigint NOT NULL REFERENCES games(id),
key text NOT NULL,
label text NOT NULL,
format text NOT NULL DEFAULT 'number',
source text NOT NULL DEFAULT 'snapshot',
PRIMARY KEY (game_id, key)
);
CREATE TABLE column_templates (
game_id bigint NOT NULL REFERENCES games(id),
key text NOT NULL,
label text NOT NULL,
columns jsonb NOT NULL,
PRIMARY KEY (game_id, key)
);
CREATE INDEX sessions_game_time ON sessions(game_id, started_at DESC);
CREATE INDEX sessions_player_time ON sessions(game_id, player_id, started_at DESC);
CREATE INDEX snapshots_player_latest ON player_snapshots(game_id, player_id, captured_at DESC, id DESC);
CREATE INDEX events_game_time ON events(game_id, occurred_at DESC);
CREATE INDEX events_player_time ON events(game_id, player_id, occurred_at DESC);
CREATE INDEX metrics_game_category_time ON game_metrics(game_id, category, name, recorded_at DESC);
CREATE INDEX runs_game_definition ON activity_runs(game_id, definition_id, started_at DESC);
CREATE INDEX runs_player ON activity_runs(game_id, player_id, started_at DESC);
CREATE VIEW player_statistics AS
WITH session_totals AS (
SELECT game_id, player_id, count(*)::int AS session_count,
sum(extract(epoch FROM ended_at-started_at))::double precision AS playtime,
avg(extract(epoch FROM ended_at-started_at))::double precision AS average_session,
percentile_cont(.5) WITHIN GROUP (ORDER BY extract(epoch FROM ended_at-started_at))::double precision AS median_session,
count(DISTINCT (started_at AT TIME ZONE 'UTC')::date) FILTER (WHERE started_at >= now()-interval '30 days')::int AS active_days
FROM sessions GROUP BY game_id, player_id
)
SELECT p.*, latest.data AS latest_data, latest.captured_at AS snapshot_at,
coalesce(s.session_count,0) AS session_count, s.playtime, s.average_session, s.median_session, coalesce(s.active_days,0) AS active_days
FROM players p LEFT JOIN session_totals s ON s.game_id=p.game_id AND s.player_id=p.id
LEFT JOIN LATERAL (SELECT data,captured_at FROM player_snapshots ps WHERE ps.game_id=p.game_id AND ps.player_id=p.id ORDER BY captured_at DESC,id DESC LIMIT 1) latest ON true;
INSERT INTO schema_migrations(version) VALUES ('001_initial');