131 lines
5.7 KiB
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');
|