diff --git a/README.md b/README.md index e54fac72..ad230a1a 100644 --- a/README.md +++ b/README.md @@ -928,7 +928,11 @@ streaks. A match counts once it has ended (the match in progress is on the live result (win, loss, draw) is read from the match's winner and final scores against the side the player played; a match with no winner and nobody scoring, or one abandoned by a restart, has no result. Playtime and seed time come from player sessions; kills per hour and cash per minute leave -seed time out; cash is summed over sessions, each banked across its matches like kills, so money +seed time out. They also leave out idle time, a player's time on in a match before any side had +scored (the match waiting for its players), so hours spent seeding are not counted as played on a +server without a Seeding reward; as both count the same wait, they leave out whichever of the two +is longer. Idle time is kept from this release on, so earlier matches have none. Cash +is summed over sessions, each banked across its matches like kills, so money spent in a match lowers it. Cash per minute divides that cash by the whole time of the sessions it came from: a session that began before a range or season counts all of its cash there, so all of its time too, though Playtime shows only the time inside. Names link to the dossier. diff --git a/drizzle/0051_idle_seconds.sql b/drizzle/0051_idle_seconds.sql new file mode 100644 index 00000000..dd0f7728 --- /dev/null +++ b/drizzle/0051_idle_seconds.sql @@ -0,0 +1,247 @@ +-- Idle time: of a player's time on in a match, the part before any side had scored, while the match +-- waited for its players (the game's MatchState.PreMatch.WaitingForPlayers). Kills per hour leaves +-- it out, as it leaves out seed time, so a player who seeds for hours before a match goes live is +-- not ranked as if those hours were played. The worker tallies it beside `seconds` (tallyLook); +-- lines written before this have 0, which is what they counted before. +-- +-- The totals and day rows carry the lines' sum like the other line columns. The functions below are +-- 0038's player_totals_recount and 0039's player_days_recount, player_lines_cleared, +-- player_totals_match_ended, player_days_rebuild and player_totals_rebuild, each with idle_seconds +-- added and nothing else changed. Every existing line has 0, so the rows need no rebuild: their 0 is already the sum. +ALTER TABLE "match_players" ADD COLUMN "idle_seconds" integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "player_days" ADD COLUMN "idle_seconds" bigint DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE "player_totals" ADD COLUMN "idle_seconds" bigint DEFAULT 0 NOT NULL;--> statement-breakpoint +CREATE OR REPLACE FUNCTION "player_totals_recount"(sid text, pids text[]) RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + PERFORM player_totals_lock(sid); + INSERT INTO player_totals AS t (server_id, steam_id, sessions, seconds, seed_seconds, cash, last_seen, + matches, kills, deaths, headshots, team_kills, suicides, vehicle_kills, + kill_streak, death_streak, wins, losses, draws, idle_seconds) + SELECT sid, p.steam_id, s.sessions, s.seconds, s.seed_seconds, s.cash, s.last_seen, + l.matches, l.kills, l.deaths, l.headshots, l.team_kills, l.suicides, l.vehicle_kills, + l.kill_streak, l.death_streak, l.wins, l.losses, l.draws, l.idle_seconds + FROM (SELECT DISTINCT unnest(pids) AS steam_id) p + CROSS JOIN LATERAL ( + SELECT COUNT(*)::int AS sessions, + COALESCE(SUM(EXTRACT(EPOCH FROM (ps.left_at - ps.joined_at))), 0) AS seconds, + COALESCE(SUM(ps.seed_seconds), 0) AS seed_seconds, COALESCE(SUM(ps.cash), 0) AS cash, + MAX(ps.last_seen) AS last_seen + FROM player_sessions ps + WHERE ps.steam_id = p.steam_id AND ps.server_id = sid AND ps.left_at IS NOT NULL) s + CROSS JOIN LATERAL ( + SELECT COUNT(*)::int AS matches, COALESCE(SUM(mp.kills), 0) AS kills, COALESCE(SUM(mp.deaths), 0) AS deaths, + COALESCE(SUM(mp.headshots), 0) AS headshots, COALESCE(SUM(mp.team_kills), 0) AS team_kills, + COALESCE(SUM(mp.suicides), 0) AS suicides, COALESCE(SUM(mp.vehicle_kills), 0) AS vehicle_kills, + COALESCE(MAX(mp.kill_streak), 0) AS kill_streak, COALESCE(MAX(mp.death_streak), 0) AS death_streak, + (COUNT(*) FILTER (WHERE x.r = 'win'))::int AS wins, + (COUNT(*) FILTER (WHERE x.r = 'loss'))::int AS losses, + (COUNT(*) FILTER (WHERE x.r = 'draw'))::int AS draws, + COALESCE(SUM(mp.idle_seconds), 0) AS idle_seconds + FROM match_players mp + JOIN matches m ON m.id = mp.match_id AND m.server_id = mp.server_id + CROSS JOIN LATERAL (SELECT match_result(m.final_scores, m.winner, mp.faction) AS r) x + WHERE mp.steam_id = p.steam_id AND mp.server_id = sid AND m.ended_at IS NOT NULL) l + ON CONFLICT (server_id, steam_id) DO UPDATE SET + sessions = EXCLUDED.sessions, seconds = EXCLUDED.seconds, seed_seconds = EXCLUDED.seed_seconds, + cash = EXCLUDED.cash, last_seen = EXCLUDED.last_seen, matches = EXCLUDED.matches, kills = EXCLUDED.kills, + deaths = EXCLUDED.deaths, headshots = EXCLUDED.headshots, team_kills = EXCLUDED.team_kills, + suicides = EXCLUDED.suicides, vehicle_kills = EXCLUDED.vehicle_kills, kill_streak = EXCLUDED.kill_streak, + death_streak = EXCLUDED.death_streak, wins = EXCLUDED.wins, losses = EXCLUDED.losses, draws = EXCLUDED.draws, + idle_seconds = EXCLUDED.idle_seconds; + DELETE FROM player_totals WHERE server_id = sid AND steam_id = ANY (pids) AND sessions = 0 AND matches = 0; +END $$;--> statement-breakpoint +CREATE OR REPLACE FUNCTION "player_days_recount"(sid text, pids text[], days date[]) RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + PERFORM player_totals_lock(sid); + INSERT INTO player_days AS t (server_id, day, steam_id, seconds, crossings, sessions, seed_seconds, cash, last_seen, + matches, kills, deaths, headshots, team_kills, suicides, vehicle_kills, + kill_streak, death_streak, wins, losses, draws, idle_seconds) + WITH k AS (SELECT DISTINCT u.p, u.d FROM unnest(pids, days) AS u(p, d) WHERE u.d IS NOT NULL), + c AS ( + SELECT DISTINCT ps.id, ps.steam_id, ps.joined_at, ps.left_at, ps.seed_seconds, ps.cash, ps.last_seen + FROM k JOIN player_sessions ps ON ps.steam_id = k.p AND ps.server_id = sid AND ps.left_at IS NOT NULL + AND ps.left_at >= k.d::timestamp AT TIME ZONE 'UTC' + AND LEAST(ps.joined_at, ps.left_at) < (k.d + 1)::timestamp AT TIME ZONE 'UTC'), + s AS ( + SELECT c.steam_id AS p, x.day AS d, SUM(x.seconds) AS seconds, + array_agg(c.joined_at ORDER BY c.joined_at) FILTER (WHERE x.crossing) AS crossings, + (COUNT(*) FILTER (WHERE x.closes))::int AS sessions, + COALESCE(SUM(c.seed_seconds) FILTER (WHERE x.closes), 0) AS seed_seconds, + COALESCE(SUM(c.cash) FILTER (WHERE x.closes), 0) AS cash, + MAX(c.last_seen) FILTER (WHERE x.closes) AS last_seen + FROM c CROSS JOIN LATERAL ( + SELECT p.day, p.seconds, p.crossing, p.day = (c.left_at AT TIME ZONE 'UTC')::date AS closes + FROM session_day_parts(c.joined_at, c.left_at) p) x + JOIN k ON k.p = c.steam_id AND k.d = x.day + GROUP BY 1, 2), + l AS ( + SELECT k.p, k.d, COUNT(*)::int AS matches, SUM(mp.kills) AS kills, SUM(mp.deaths) AS deaths, + SUM(mp.headshots) AS headshots, SUM(mp.team_kills) AS team_kills, SUM(mp.suicides) AS suicides, + SUM(mp.vehicle_kills) AS vehicle_kills, MAX(mp.kill_streak) AS kill_streak, + MAX(mp.death_streak) AS death_streak, + (COUNT(*) FILTER (WHERE x.r = 'win'))::int AS wins, + (COUNT(*) FILTER (WHERE x.r = 'loss'))::int AS losses, + (COUNT(*) FILTER (WHERE x.r = 'draw'))::int AS draws, + SUM(mp.idle_seconds) AS idle_seconds + FROM k JOIN match_players mp ON mp.steam_id = k.p AND mp.server_id = sid + JOIN matches m ON m.id = mp.match_id AND m.server_id = mp.server_id + AND m.ended_at >= k.d::timestamp AT TIME ZONE 'UTC' AND m.ended_at < (k.d + 1)::timestamp AT TIME ZONE 'UTC' + CROSS JOIN LATERAL (SELECT match_result(m.final_scores, m.winner, mp.faction) AS r) x + GROUP BY k.p, k.d) + SELECT sid, k.d, k.p, COALESCE(s.seconds, 0), s.crossings, COALESCE(s.sessions, 0), COALESCE(s.seed_seconds, 0), + COALESCE(s.cash, 0), s.last_seen, COALESCE(l.matches, 0), COALESCE(l.kills, 0), COALESCE(l.deaths, 0), + COALESCE(l.headshots, 0), COALESCE(l.team_kills, 0), COALESCE(l.suicides, 0), COALESCE(l.vehicle_kills, 0), + COALESCE(l.kill_streak, 0), COALESCE(l.death_streak, 0), COALESCE(l.wins, 0), COALESCE(l.losses, 0), + COALESCE(l.draws, 0), COALESCE(l.idle_seconds, 0) + FROM k LEFT JOIN s ON s.p = k.p AND s.d = k.d LEFT JOIN l ON l.p = k.p AND l.d = k.d + ON CONFLICT (server_id, day, steam_id) DO UPDATE SET + seconds = EXCLUDED.seconds, crossings = EXCLUDED.crossings, sessions = EXCLUDED.sessions, + seed_seconds = EXCLUDED.seed_seconds, cash = EXCLUDED.cash, last_seen = EXCLUDED.last_seen, + matches = EXCLUDED.matches, kills = EXCLUDED.kills, deaths = EXCLUDED.deaths, headshots = EXCLUDED.headshots, + team_kills = EXCLUDED.team_kills, suicides = EXCLUDED.suicides, vehicle_kills = EXCLUDED.vehicle_kills, + kill_streak = EXCLUDED.kill_streak, death_streak = EXCLUDED.death_streak, wins = EXCLUDED.wins, + losses = EXCLUDED.losses, draws = EXCLUDED.draws, idle_seconds = EXCLUDED.idle_seconds; + DELETE FROM player_days t USING (SELECT DISTINCT u.p, u.d FROM unnest(pids, days) AS u(p, d)) k + WHERE t.server_id = sid AND t.steam_id = k.p AND t.day = k.d + AND t.sessions = 0 AND t.matches = 0 AND t.seconds = 0 AND t.crossings IS NULL; +END $$;--> statement-breakpoint +CREATE OR REPLACE FUNCTION "player_lines_cleared"(sid text) RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + PERFORM player_totals_lock(sid); + UPDATE player_totals SET matches = 0, kills = 0, deaths = 0, headshots = 0, team_kills = 0, suicides = 0, + vehicle_kills = 0, kill_streak = 0, death_streak = 0, wins = 0, losses = 0, draws = 0, idle_seconds = 0 + WHERE server_id = sid AND matches > 0; + DELETE FROM player_totals WHERE server_id = sid AND sessions = 0 AND matches = 0; + UPDATE player_days SET matches = 0, kills = 0, deaths = 0, headshots = 0, team_kills = 0, suicides = 0, + vehicle_kills = 0, kill_streak = 0, death_streak = 0, wins = 0, losses = 0, draws = 0, idle_seconds = 0 + WHERE server_id = sid AND matches > 0; + DELETE FROM player_days + WHERE server_id = sid AND sessions = 0 AND matches = 0 AND seconds = 0 AND crossings IS NULL; +END $$;--> statement-breakpoint +CREATE OR REPLACE FUNCTION "player_totals_match_ended"() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + PERFORM player_totals_lock(NEW.server_id); + INSERT INTO player_totals AS t (server_id, steam_id, matches, kills, deaths, headshots, team_kills, suicides, + vehicle_kills, kill_streak, death_streak, wins, losses, draws, idle_seconds) + SELECT mp.server_id, mp.steam_id, 1, mp.kills, mp.deaths, mp.headshots, mp.team_kills, mp.suicides, + mp.vehicle_kills, mp.kill_streak, mp.death_streak, + CASE WHEN x.r = 'win' THEN 1 ELSE 0 END, CASE WHEN x.r = 'loss' THEN 1 ELSE 0 END, + CASE WHEN x.r = 'draw' THEN 1 ELSE 0 END, mp.idle_seconds + FROM match_players mp + CROSS JOIN LATERAL (SELECT match_result(NEW.final_scores, NEW.winner, mp.faction) AS r) x + WHERE mp.match_id = NEW.id AND mp.server_id = NEW.server_id + ON CONFLICT (server_id, steam_id) DO UPDATE SET + matches = t.matches + EXCLUDED.matches, kills = t.kills + EXCLUDED.kills, deaths = t.deaths + EXCLUDED.deaths, + headshots = t.headshots + EXCLUDED.headshots, team_kills = t.team_kills + EXCLUDED.team_kills, + suicides = t.suicides + EXCLUDED.suicides, vehicle_kills = t.vehicle_kills + EXCLUDED.vehicle_kills, + kill_streak = CASE WHEN t.matches = 0 THEN EXCLUDED.kill_streak ELSE GREATEST(t.kill_streak, EXCLUDED.kill_streak) END, + death_streak = CASE WHEN t.matches = 0 THEN EXCLUDED.death_streak ELSE GREATEST(t.death_streak, EXCLUDED.death_streak) END, + wins = t.wins + EXCLUDED.wins, losses = t.losses + EXCLUDED.losses, draws = t.draws + EXCLUDED.draws, + idle_seconds = t.idle_seconds + EXCLUDED.idle_seconds; + INSERT INTO player_days AS t (server_id, day, steam_id, matches, kills, deaths, headshots, team_kills, suicides, + vehicle_kills, kill_streak, death_streak, wins, losses, draws, idle_seconds) + SELECT mp.server_id, (NEW.ended_at AT TIME ZONE 'UTC')::date, mp.steam_id, 1, mp.kills, mp.deaths, mp.headshots, + mp.team_kills, mp.suicides, mp.vehicle_kills, mp.kill_streak, mp.death_streak, + CASE WHEN x.r = 'win' THEN 1 ELSE 0 END, CASE WHEN x.r = 'loss' THEN 1 ELSE 0 END, + CASE WHEN x.r = 'draw' THEN 1 ELSE 0 END, mp.idle_seconds + FROM match_players mp + CROSS JOIN LATERAL (SELECT match_result(NEW.final_scores, NEW.winner, mp.faction) AS r) x + WHERE mp.match_id = NEW.id AND mp.server_id = NEW.server_id + ON CONFLICT (server_id, day, steam_id) DO UPDATE SET + matches = t.matches + EXCLUDED.matches, kills = t.kills + EXCLUDED.kills, deaths = t.deaths + EXCLUDED.deaths, + headshots = t.headshots + EXCLUDED.headshots, team_kills = t.team_kills + EXCLUDED.team_kills, + suicides = t.suicides + EXCLUDED.suicides, vehicle_kills = t.vehicle_kills + EXCLUDED.vehicle_kills, + kill_streak = CASE WHEN t.matches = 0 THEN EXCLUDED.kill_streak ELSE GREATEST(t.kill_streak, EXCLUDED.kill_streak) END, + death_streak = CASE WHEN t.matches = 0 THEN EXCLUDED.death_streak ELSE GREATEST(t.death_streak, EXCLUDED.death_streak) END, + wins = t.wins + EXCLUDED.wins, losses = t.losses + EXCLUDED.losses, draws = t.draws + EXCLUDED.draws, + idle_seconds = t.idle_seconds + EXCLUDED.idle_seconds; + RETURN NEW; +END $$;--> statement-breakpoint +CREATE OR REPLACE FUNCTION "player_days_rebuild"() RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + IF EXISTS (SELECT 1 FROM player_sessions WHERE left_at IS NOT NULL AND last_seen IS DISTINCT FROM left_at) THEN + RAISE EXCEPTION 'a closed player session must have last_seen equal to left_at (migration 0039): player_days not built'; + END IF; + DELETE FROM player_days; + INSERT INTO player_days (server_id, day, steam_id, seconds, crossings, sessions, seed_seconds, cash, last_seen, + matches, kills, deaths, headshots, team_kills, suicides, vehicle_kills, + kill_streak, death_streak, wins, losses, draws, idle_seconds) + WITH results AS ( + SELECT f.match_id, f.faction, match_result(m.final_scores, m.winner, f.faction) AS r + FROM (SELECT DISTINCT match_id, faction FROM match_players WHERE faction IS NOT NULL) f + JOIN matches m ON m.id = f.match_id + WHERE m.ended_at IS NOT NULL), + s AS ( + SELECT ps.server_id, x.day, ps.steam_id, SUM(x.seconds) AS seconds, + array_agg(ps.joined_at ORDER BY ps.joined_at) FILTER (WHERE x.crossing) AS crossings, + (COUNT(*) FILTER (WHERE x.closes))::int AS sessions, + COALESCE(SUM(ps.seed_seconds) FILTER (WHERE x.closes), 0) AS seed_seconds, + COALESCE(SUM(ps.cash) FILTER (WHERE x.closes), 0) AS cash, + MAX(ps.last_seen) FILTER (WHERE x.closes) AS last_seen + FROM player_sessions ps CROSS JOIN LATERAL ( + SELECT p.day, p.seconds, p.crossing, p.day = (ps.left_at AT TIME ZONE 'UTC')::date AS closes + FROM session_day_parts(ps.joined_at, ps.left_at) p) x + WHERE ps.left_at IS NOT NULL + GROUP BY 1, 2, 3), + l AS ( + SELECT mp.server_id, (m.ended_at AT TIME ZONE 'UTC')::date AS day, mp.steam_id, COUNT(*)::int AS matches, + SUM(mp.kills) AS kills, SUM(mp.deaths) AS deaths, SUM(mp.headshots) AS headshots, + SUM(mp.team_kills) AS team_kills, SUM(mp.suicides) AS suicides, SUM(mp.vehicle_kills) AS vehicle_kills, + MAX(mp.kill_streak) AS kill_streak, MAX(mp.death_streak) AS death_streak, + (COUNT(*) FILTER (WHERE x.r = 'win'))::int AS wins, + (COUNT(*) FILTER (WHERE x.r = 'loss'))::int AS losses, + (COUNT(*) FILTER (WHERE x.r = 'draw'))::int AS draws, + SUM(mp.idle_seconds) AS idle_seconds + FROM match_players mp + JOIN matches m ON m.id = mp.match_id AND m.server_id = mp.server_id + LEFT JOIN results x ON x.match_id = mp.match_id AND x.faction = mp.faction + WHERE m.ended_at IS NOT NULL + GROUP BY 1, 2, 3) + SELECT COALESCE(s.server_id, l.server_id), COALESCE(s.day, l.day), COALESCE(s.steam_id, l.steam_id), + COALESCE(s.seconds, 0), s.crossings, COALESCE(s.sessions, 0), COALESCE(s.seed_seconds, 0), + COALESCE(s.cash, 0), s.last_seen, COALESCE(l.matches, 0), COALESCE(l.kills, 0), COALESCE(l.deaths, 0), + COALESCE(l.headshots, 0), COALESCE(l.team_kills, 0), COALESCE(l.suicides, 0), COALESCE(l.vehicle_kills, 0), + COALESCE(l.kill_streak, 0), COALESCE(l.death_streak, 0), COALESCE(l.wins, 0), COALESCE(l.losses, 0), + COALESCE(l.draws, 0), COALESCE(l.idle_seconds, 0) + FROM s FULL JOIN l ON l.server_id = s.server_id AND l.day = s.day AND l.steam_id = s.steam_id; +END $$;--> statement-breakpoint +CREATE OR REPLACE FUNCTION "player_totals_rebuild"() RETURNS void LANGUAGE plpgsql AS $$ +BEGIN + DELETE FROM player_totals; + INSERT INTO player_totals (server_id, steam_id, sessions, seconds, seed_seconds, cash, last_seen, + matches, kills, deaths, headshots, team_kills, suicides, vehicle_kills, + kill_streak, death_streak, wins, losses, draws, idle_seconds) + WITH results AS ( + SELECT f.match_id, f.faction, match_result(m.final_scores, m.winner, f.faction) AS r + FROM (SELECT DISTINCT match_id, faction FROM match_players WHERE faction IS NOT NULL) f + JOIN matches m ON m.id = f.match_id + WHERE m.ended_at IS NOT NULL), + s AS ( + SELECT server_id, steam_id, COUNT(*)::int AS sessions, + SUM(EXTRACT(EPOCH FROM (left_at - joined_at))) AS seconds, + SUM(seed_seconds) AS seed_seconds, SUM(cash) AS cash, MAX(last_seen) AS last_seen + FROM player_sessions WHERE left_at IS NOT NULL + GROUP BY server_id, steam_id), + l AS ( + SELECT mp.server_id, mp.steam_id, COUNT(*)::int AS matches, SUM(mp.kills) AS kills, SUM(mp.deaths) AS deaths, + SUM(mp.headshots) AS headshots, SUM(mp.team_kills) AS team_kills, SUM(mp.suicides) AS suicides, + SUM(mp.vehicle_kills) AS vehicle_kills, MAX(mp.kill_streak) AS kill_streak, + MAX(mp.death_streak) AS death_streak, + (COUNT(*) FILTER (WHERE x.r = 'win'))::int AS wins, + (COUNT(*) FILTER (WHERE x.r = 'loss'))::int AS losses, + (COUNT(*) FILTER (WHERE x.r = 'draw'))::int AS draws, + SUM(mp.idle_seconds) AS idle_seconds + FROM match_players mp + JOIN matches m ON m.id = mp.match_id AND m.server_id = mp.server_id + LEFT JOIN results x ON x.match_id = mp.match_id AND x.faction = mp.faction + WHERE m.ended_at IS NOT NULL + GROUP BY mp.server_id, mp.steam_id) + SELECT COALESCE(s.server_id, l.server_id), COALESCE(s.steam_id, l.steam_id), + COALESCE(s.sessions, 0), COALESCE(s.seconds, 0), COALESCE(s.seed_seconds, 0), COALESCE(s.cash, 0), s.last_seen, + COALESCE(l.matches, 0), COALESCE(l.kills, 0), COALESCE(l.deaths, 0), COALESCE(l.headshots, 0), + COALESCE(l.team_kills, 0), COALESCE(l.suicides, 0), COALESCE(l.vehicle_kills, 0), + COALESCE(l.kill_streak, 0), COALESCE(l.death_streak, 0), + COALESCE(l.wins, 0), COALESCE(l.losses, 0), COALESCE(l.draws, 0), COALESCE(l.idle_seconds, 0) + FROM s FULL JOIN l ON l.server_id = s.server_id AND l.steam_id = s.steam_id; + PERFORM player_days_rebuild(); +END $$; diff --git a/drizzle/meta/0051_snapshot.json b/drizzle/meta/0051_snapshot.json new file mode 100644 index 00000000..b8e33ab5 --- /dev/null +++ b/drizzle/meta/0051_snapshot.json @@ -0,0 +1,5761 @@ +{ + "id": "09975601-4e40-44ca-9753-6e9a8b2cd387", + "prevId": "9de65adc-6a94-4870-a19b-b960ffed63ae", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_issuer_account_id_uidx": { + "name": "account_issuer_account_id_uidx", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hint": { + "name": "hint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "server_ids": { + "name": "server_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_keys_org_idx": { + "name": "api_keys_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_org_id_organizations_id_fk": { + "name": "api_keys_org_id_organizations_id_fk", + "tableFrom": "api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_keys_created_by_user_id_fk": { + "name": "api_keys_created_by_user_id_fk", + "tableFrom": "api_keys", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "ts": { + "name": "ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_name": { + "name": "server_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "audit_ts_idx": { + "name": "audit_ts_idx", + "columns": [ + { + "expression": "ts", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_server_idx": { + "name": "audit_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_org_idx": { + "name": "audit_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_actor_idx": { + "name": "audit_actor_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_action_idx": { + "name": "audit_action_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_target_idx": { + "name": "audit_target_idx", + "columns": [ + { + "expression": "target", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"target\" <> ''", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.json_webhook_posts": { + "name": "json_webhook_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "not_before": { + "name": "not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "done_at": { + "name": "done_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "json_webhook_posts_event_idx": { + "name": "json_webhook_posts_event_idx", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "json_webhook_posts_next_idx": { + "name": "json_webhook_posts_next_idx", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"json_webhook_posts\".\"state\" in ('pending', 'sending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "json_webhook_posts_open_idx": { + "name": "json_webhook_posts_open_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"json_webhook_posts\".\"state\" in ('pending', 'sending')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "json_webhook_posts_webhook_id_json_webhooks_id_fk": { + "name": "json_webhook_posts_webhook_id_json_webhooks_id_fk", + "tableFrom": "json_webhook_posts", + "tableTo": "json_webhooks", + "columnsFrom": [ + "webhook_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.json_webhooks": { + "name": "json_webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url_enc": { + "name": "url_enc", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url_hint": { + "name": "url_hint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "secret_enc": { + "name": "secret_enc", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "server_ids": { + "name": "server_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_private": { + "name": "allow_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_sent_at": { + "name": "last_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status": { + "name": "last_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "json_webhooks_org_idx": { + "name": "json_webhooks_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "json_webhooks_org_id_organizations_id_fk": { + "name": "json_webhooks_org_id_organizations_id_fk", + "tableFrom": "json_webhooks", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kills": { + "name": "kills", + "schema": "", + "columns": { + "ts": { + "name": "ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_id": { + "name": "match_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_row": { + "name": "match_row", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event_time": { + "name": "event_time", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "map": { + "name": "map", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "killer_steam_id": { + "name": "killer_steam_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "killer_name": { + "name": "killer_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "killer_faction": { + "name": "killer_faction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "victim_steam_id": { + "name": "victim_steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "victim_faction": { + "name": "victim_faction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cause": { + "name": "cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "distance_m": { + "name": "distance_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "headshot": { + "name": "headshot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suicide": { + "name": "suicide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "team_kill": { + "name": "team_kill", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kills_event_idx": { + "name": "kills_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kills_server_ts_idx": { + "name": "kills_server_ts_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ts", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kills_killer_idx": { + "name": "kills_killer_idx", + "columns": [ + { + "expression": "killer_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ts", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kills_victim_idx": { + "name": "kills_victim_idx", + "columns": [ + { + "expression": "victim_steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ts", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.list_entries": { + "name": "list_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by_name": { + "name": "added_by_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "added_at": { + "name": "added_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_by": { + "name": "removed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "removed_by_name": { + "name": "removed_by_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "removal": { + "name": "removal", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "list_entries_active_uidx": { + "name": "list_entries_active_uidx", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"list_entries\".\"removed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "list_entries_list_idx": { + "name": "list_entries_list_idx", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "removed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "list_entries_steam_idx": { + "name": "list_entries_steam_idx", + "columns": [ + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "list_entries_expiry_idx": { + "name": "list_entries_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"list_entries\".\"removed_at\" is null and \"list_entries\".\"expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "list_entries_list_id_lists_id_fk": { + "name": "list_entries_list_id_lists_id_fk", + "tableFrom": "list_entries", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lists": { + "name": "lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Default'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "on_from": { + "name": "on_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "on_until": { + "name": "on_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "every_server": { + "name": "every_server", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "lists_org_kind_name_uidx": { + "name": "lists_org_kind_name_uidx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"lists\".\"server_id\" is null and \"lists\".\"archived_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "lists_org_default_uidx": { + "name": "lists_org_default_uidx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"lists\".\"is_default\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "lists_server_kind_uidx": { + "name": "lists_server_kind_uidx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"lists\".\"server_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "lists_org_id_organizations_id_fk": { + "name": "lists_org_id_organizations_id_fk", + "tableFrom": "lists", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "lists_server_id_servers_id_fk": { + "name": "lists_server_id_servers_id_fk", + "tableFrom": "lists", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "lists_share_token_unique": { + "name": "lists_share_token_unique", + "nullsNotDistinct": false, + "columns": [ + "share_token" + ] + } + }, + "policies": {}, + "checkConstraints": { + "lists_group_kind": { + "name": "lists_group_kind", + "value": "\"lists\".\"server_id\" is not null or \"lists\".\"is_default\" or \"lists\".\"kind\" = 'reserve'" + }, + "lists_group_shape": { + "name": "lists_group_shape", + "value": "(\"lists\".\"server_id\" is null and not \"lists\".\"is_default\") or (\"lists\".\"on_from\" is null and \"lists\".\"on_until\" is null and \"lists\".\"every_server\" and \"lists\".\"archived_at\" is null)" + }, + "lists_window": { + "name": "lists_window", + "value": "\"lists\".\"on_from\" is null or \"lists\".\"on_until\" is null or \"lists\".\"on_from\" < \"lists\".\"on_until\"" + } + }, + "isRLSEnabled": false + }, + "public.login_attempts": { + "name": "login_attempts", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_at": { + "name": "first_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_events": { + "name": "match_events", + "schema": "", + "columns": { + "ts": { + "name": "ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_time": { + "name": "event_time", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "match_id": { + "name": "match_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "map": { + "name": "map", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "players": { + "name": "players", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "winning_faction": { + "name": "winning_faction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scores": { + "name": "scores", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "match_events_event_idx": { + "name": "match_events_event_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "match_events_server_ts_idx": { + "name": "match_events_server_ts_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ts", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_players": { + "name": "match_players", + "schema": "", + "columns": { + "match_id": { + "name": "match_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "faction": { + "name": "faction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "seconds": { + "name": "seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "kills": { + "name": "kills", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deaths": { + "name": "deaths", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cash_delta": { + "name": "cash_delta", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "headshots": { + "name": "headshots", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "team_kills": { + "name": "team_kills", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "suicides": { + "name": "suicides", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "vehicle_kills": { + "name": "vehicle_kills", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "longest_m": { + "name": "longest_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kill_streak": { + "name": "kill_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "death_streak": { + "name": "death_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idle_seconds": { + "name": "idle_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "match_players_server_idx": { + "name": "match_players_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "match_players_steam_idx": { + "name": "match_players_steam_idx", + "columns": [ + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "match_players_match_id_steam_id_pk": { + "name": "match_players_match_id_steam_id_pk", + "columns": [ + "match_id", + "steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.matches": { + "name": "matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "map": { + "name": "map", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "experiences": { + "name": "experiences", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "peak_players": { + "name": "peak_players", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "final_scores": { + "name": "final_scores", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "winner": { + "name": "winner", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "matches_server_idx": { + "name": "matches_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "matches_open_idx": { + "name": "matches_open_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"matches\".\"ended_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "matches_server_ended_idx": { + "name": "matches_server_ended_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_ban_reasons": { + "name": "org_ban_reasons", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_ban_reasons_org_id_organizations_id_fk": { + "name": "org_ban_reasons_org_id_organizations_id_fk", + "tableFrom": "org_ban_reasons", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_invites": { + "name": "org_invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "org_role": { + "name": "org_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "server_role_id": { + "name": "server_role_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uses": { + "name": "uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_invites_org_idx": { + "name": "org_invites_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_invites_org_id_organizations_id_fk": { + "name": "org_invites_org_id_organizations_id_fk", + "tableFrom": "org_invites", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_invites_server_role_id_org_roles_id_fk": { + "name": "org_invites_server_role_id_org_roles_id_fk", + "tableFrom": "org_invites", + "tableTo": "org_roles", + "columnsFrom": [ + "server_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "org_invites_token_unique": { + "name": "org_invites_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_members": { + "name": "org_members", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invite_id": { + "name": "invite_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_members_user_idx": { + "name": "org_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_members_org_id_organizations_id_fk": { + "name": "org_members_org_id_organizations_id_fk", + "tableFrom": "org_members", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_members_user_id_user_id_fk": { + "name": "org_members_user_id_user_id_fk", + "tableFrom": "org_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "org_members_org_id_user_id_pk": { + "name": "org_members_org_id_user_id_pk", + "columns": [ + "org_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_roles": { + "name": "org_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "builtin": { + "name": "builtin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_roles_builtin_uidx": { + "name": "org_roles_builtin_uidx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "builtin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"org_roles\".\"builtin\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_roles_name_uidx": { + "name": "org_roles_name_uidx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_roles_org_id_organizations_id_fk": { + "name": "org_roles_org_id_organizations_id_fk", + "tableFrom": "org_roles", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_saved_messages": { + "name": "org_saved_messages", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "org_saved_messages_org_id_organizations_id_fk": { + "name": "org_saved_messages_org_id_organizations_id_fk", + "tableFrom": "org_saved_messages", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_saved_messages_updated_by_user_id_fk": { + "name": "org_saved_messages_updated_by_user_id_fk", + "tableFrom": "org_saved_messages", + "tableTo": "user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "server_limit": { + "name": "server_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_reason": { + "name": "suspended_reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "members_reserved": { + "name": "members_reserved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allow_public_status": { + "name": "allow_public_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "allow_public_leaderboards": { + "name": "allow_public_leaderboards", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "discord_invite_url": { + "name": "discord_invite_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "ban_message": { + "name": "ban_message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'{reason}'" + }, + "board_opens": { + "name": "board_opens", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'official'" + }, + "board_hidden": { + "name": "board_hidden", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox": { + "name": "outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_name": { + "name": "trigger_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "trigger_kind": { + "name": "trigger_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ok_message": { + "name": "ok_message", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "not_before": { + "name": "not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "done_at": { + "name": "done_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_dedupe_idx": { + "name": "outbox_dedupe_idx", + "columns": [ + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "not_before", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox\".\"state\" in ('pending', 'sending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_server_idx": { + "name": "outbox_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outbox_server_id_servers_id_fk": { + "name": "outbox_server_id_servers_id_fk", + "tableFrom": "outbox", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_user_id_idx": { + "name": "passkey_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credential_id_idx": { + "name": "passkey_credential_id_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_aliases": { + "name": "player_aliases", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "holder": { + "name": "holder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen": { + "name": "first_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "player_aliases_steam_idx": { + "name": "player_aliases_steam_idx", + "columns": [ + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "player_aliases_server_id_steam_id_name_pk": { + "name": "player_aliases_server_id_steam_id_name_pk", + "columns": [ + "server_id", + "steam_id", + "name" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_days": { + "name": "player_days", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seconds": { + "name": "seconds", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "crossings": { + "name": "crossings", + "type": "timestamp with time zone[]", + "primaryKey": false, + "notNull": false + }, + "sessions": { + "name": "sessions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seed_seconds": { + "name": "seed_seconds", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cash": { + "name": "cash", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "matches": { + "name": "matches", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "kills": { + "name": "kills", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deaths": { + "name": "deaths", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "headshots": { + "name": "headshots", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "team_kills": { + "name": "team_kills", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "suicides": { + "name": "suicides", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "vehicle_kills": { + "name": "vehicle_kills", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "kill_streak": { + "name": "kill_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "death_streak": { + "name": "death_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "wins": { + "name": "wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "losses": { + "name": "losses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "draws": { + "name": "draws", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idle_seconds": { + "name": "idle_seconds", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "player_days_server_id_day_steam_id_pk": { + "name": "player_days_server_id_day_steam_id_pk", + "columns": [ + "server_id", + "day", + "steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_marks": { + "name": "player_marks", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watched": { + "name": "watched", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_name": { + "name": "updated_by_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "player_marks_org_id_organizations_id_fk": { + "name": "player_marks_org_id_organizations_id_fk", + "tableFrom": "player_marks", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "player_marks_org_id_steam_id_pk": { + "name": "player_marks_org_id_steam_id_pk", + "columns": [ + "org_id", + "steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_notes": { + "name": "player_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "player_notes_idx": { + "name": "player_notes_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "player_notes_org_id_organizations_id_fk": { + "name": "player_notes_org_id_organizations_id_fk", + "tableFrom": "player_notes", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_sessions": { + "name": "player_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "faction": { + "name": "faction", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "left_at": { + "name": "left_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "kills": { + "name": "kills", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deaths": { + "name": "deaths", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cash": { + "name": "cash", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seed_seconds": { + "name": "seed_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "player_sessions_open_idx": { + "name": "player_sessions_open_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "left_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "player_sessions_steam_idx": { + "name": "player_sessions_steam_idx", + "columns": [ + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "joined_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.player_totals": { + "name": "player_totals", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sessions": { + "name": "sessions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seconds": { + "name": "seconds", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "seed_seconds": { + "name": "seed_seconds", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cash": { + "name": "cash", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_seen": { + "name": "last_seen", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "matches": { + "name": "matches", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "kills": { + "name": "kills", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deaths": { + "name": "deaths", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "headshots": { + "name": "headshots", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "team_kills": { + "name": "team_kills", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "suicides": { + "name": "suicides", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "vehicle_kills": { + "name": "vehicle_kills", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "kill_streak": { + "name": "kill_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "death_streak": { + "name": "death_streak", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "wins": { + "name": "wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "losses": { + "name": "losses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "draws": { + "name": "draws", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idle_seconds": { + "name": "idle_seconds", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "player_totals_server_id_steam_id_pk": { + "name": "player_totals_server_id_steam_id_pk", + "columns": [ + "server_id", + "steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sample_map_rollups": { + "name": "sample_map_rollups", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "map": { + "name": "map", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secs": { + "name": "secs", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sample_map_rollups_server_id_bucket_map_pk": { + "name": "sample_map_rollups_server_id_bucket_map_pk", + "columns": [ + "server_id", + "bucket", + "map" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sample_rollups": { + "name": "sample_rollups", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "samples": { + "name": "samples", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ok_samples": { + "name": "ok_samples", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "up_s": { + "name": "up_s", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "down_s": { + "name": "down_s", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "player_s": { + "name": "player_s", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_players": { + "name": "max_players", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_cap": { + "name": "max_cap", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sample_rollups_server_id_bucket_pk": { + "name": "sample_rollups_server_id_bucket_pk", + "columns": [ + "server_id", + "bucket" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.samples": { + "name": "samples", + "schema": "", + "columns": { + "ts": { + "name": "ts", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "player_count": { + "name": "player_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_players": { + "name": "max_players", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "map": { + "name": "map", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "experiences": { + "name": "experiences", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "match_seconds": { + "name": "match_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scores": { + "name": "scores", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cash": { + "name": "cash", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "samples_server_ts_idx": { + "name": "samples_server_ts_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ts", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "seasons_org_start_uidx": { + "name": "seasons_org_start_uidx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "seasons_org_id_organizations_id_fk": { + "name": "seasons_org_id_organizations_id_fk", + "tableFrom": "seasons", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_bans": { + "name": "server_bans", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "banned_by": { + "name": "banned_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "banned_at_utc": { + "name": "banned_at_utc", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "server_bans_steam_idx": { + "name": "server_bans_steam_idx", + "columns": [ + { + "expression": "steam_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "server_bans_server_id_servers_id_fk": { + "name": "server_bans_server_id_servers_id_fk", + "tableFrom": "server_bans", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "server_bans_server_id_steam_id_pk": { + "name": "server_bans_server_id_steam_id_pk", + "columns": [ + "server_id", + "steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_grants": { + "name": "server_grants", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_by": { + "name": "granted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "server_grants_user_idx": { + "name": "server_grants_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "server_grants_role_idx": { + "name": "server_grants_role_idx", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "server_grants_server_id_servers_id_fk": { + "name": "server_grants_server_id_servers_id_fk", + "tableFrom": "server_grants", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_grants_user_id_user_id_fk": { + "name": "server_grants_user_id_user_id_fk", + "tableFrom": "server_grants", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_grants_role_id_org_roles_id_fk": { + "name": "server_grants_role_id_org_roles_id_fk", + "tableFrom": "server_grants", + "tableTo": "org_roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "server_grants_server_id_user_id_pk": { + "name": "server_grants_server_id_user_id_pk", + "columns": [ + "server_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_list_state": { + "name": "server_list_state", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_list_id": { + "name": "source_list_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "server_list_state_source_idx": { + "name": "server_list_state_source_idx", + "columns": [ + { + "expression": "source_list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "server_list_state_server_id_servers_id_fk": { + "name": "server_list_state_server_id_servers_id_fk", + "tableFrom": "server_list_state", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_list_state_source_list_id_lists_id_fk": { + "name": "server_list_state_source_list_id_lists_id_fk", + "tableFrom": "server_list_state", + "tableTo": "lists", + "columnsFrom": [ + "source_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "server_list_state_server_id_kind_steam_id_pk": { + "name": "server_list_state_server_id_kind_steam_id_pk", + "columns": [ + "server_id", + "kind", + "steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_list_sync": { + "name": "server_list_sync", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "server_list_sync_server_id_servers_id_fk": { + "name": "server_list_sync_server_id_servers_id_fk", + "tableFrom": "server_list_sync", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_lists": { + "name": "server_lists", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "list_id": { + "name": "list_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "server_lists_list_idx": { + "name": "server_lists_list_idx", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "server_lists_server_id_servers_id_fk": { + "name": "server_lists_server_id_servers_id_fk", + "tableFrom": "server_lists", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_lists_list_id_lists_id_fk": { + "name": "server_lists_list_id_lists_id_fk", + "tableFrom": "server_lists", + "tableTo": "lists", + "columnsFrom": [ + "list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "server_lists_server_id_list_id_pk": { + "name": "server_lists_server_id_list_id_pk", + "columns": [ + "server_id", + "list_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_live": { + "name": "server_live", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "build": { + "name": "build", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "game_server_id": { + "name": "game_server_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reserved_slots": { + "name": "reserved_slots", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "restart_time_utc": { + "name": "restart_time_utc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "restart_time_utc_file": { + "name": "restart_time_utc_file", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "players": { + "name": "players", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "player_count": { + "name": "player_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status_at": { + "name": "status_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "players_at": { + "name": "players_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feed_at": { + "name": "feed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "match_state": { + "name": "match_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "server_live_server_id_servers_id_fk": { + "name": "server_live_server_id_servers_id_fk", + "tableFrom": "server_live", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_reserved": { + "name": "server_reserved", + "schema": "", + "columns": { + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "configured": { + "name": "configured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "live": { + "name": "live", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "server_reserved_server_id_servers_id_fk": { + "name": "server_reserved_server_id_servers_id_fk", + "tableFrom": "server_reserved", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "server_reserved_server_id_steam_id_pk": { + "name": "server_reserved_server_id_steam_id_pk", + "columns": [ + "server_id", + "steam_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.servers": { + "name": "servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheme": { + "name": "scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'http'" + }, + "password_enc": { + "name": "password_enc", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_private": { + "name": "allow_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feed_token_enc": { + "name": "feed_token_enc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feed_token_hash": { + "name": "feed_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_status": { + "name": "public_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "public_leaderboards": { + "name": "public_leaderboards", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "public_kills": { + "name": "public_kills", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "restart_schedule": { + "name": "restart_schedule", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "servers_org_id_organizations_id_fk": { + "name": "servers_org_id_organizations_id_fk", + "tableFrom": "servers", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "servers_feed_token_hash_unique": { + "name": "servers_feed_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "feed_token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.steam_profiles": { + "name": "steam_profiles", + "schema": "", + "columns": { + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "persona": { + "name": "persona", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "profile_url": { + "name": "profile_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "account_created_at": { + "name": "account_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "vac_bans": { + "name": "vac_bans", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "game_bans": { + "name": "game_bans", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "days_since_last_ban": { + "name": "days_since_last_ban", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "community_banned": { + "name": "community_banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "economy_ban": { + "name": "economy_ban", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "friends_state": { + "name": "friends_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "friends_total": { + "name": "friends_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "friends_checked": { + "name": "friends_checked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "banned_friends": { + "name": "banned_friends", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "friends_checked_at": { + "name": "friends_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.triggers": { + "name": "triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "fire_count": { + "name": "fire_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "triggers_server_idx": { + "name": "triggers_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "triggers_server_id_servers_id_fk": { + "name": "triggers_server_id_servers_id_fk", + "tableFrom": "triggers", + "tableTo": "servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "two_factor_user_id_idx": { + "name": "two_factor_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "two_factor_secret_idx": { + "name": "two_factor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "must_change_password": { + "name": "must_change_password", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "steam_id": { + "name": "steam_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_org_id": { + "name": "default_org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auth_complete": { + "name": "auth_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auth_grace_started_at": { + "name": "auth_grace_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recovery_key_hash": { + "name": "recovery_key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recovery_key_at": { + "name": "recovery_key_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_default_org_id_organizations_id_fk": { + "name": "user_default_org_id_organizations_id_fk", + "tableFrom": "user", + "tableTo": "organizations", + "columnsFrom": [ + "default_org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "user_steam_id_unique": { + "name": "user_steam_id_unique", + "nullsNotDistinct": false, + "columns": [ + "steam_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhooks": { + "name": "webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "url_enc": { + "name": "url_enc", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url_hint": { + "name": "url_hint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "trigger_kinds": { + "name": "trigger_kinds", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "server_ids": { + "name": "server_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status_enabled": { + "name": "status_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status_style": { + "name": "status_style", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'banner'" + }, + "status_interval_s": { + "name": "status_interval_s", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 60 + }, + "link_status": { + "name": "link_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "link_leaderboard": { + "name": "link_leaderboard", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "link_panel": { + "name": "link_panel", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status_messages": { + "name": "status_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status_sent_at": { + "name": "status_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sent_at": { + "name": "last_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status": { + "name": "last_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhooks_org_idx": { + "name": "webhooks_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhooks_org_id_organizations_id_fk": { + "name": "webhooks_org_id_organizations_id_fk", + "tableFrom": "webhooks", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.worker_ownership": { + "name": "worker_ownership", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f634c598..c47231b1 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -358,6 +358,13 @@ "when": 1792156935138, "tag": "0050_org_saved_messages", "breakpoints": true + }, + { + "idx": 51, + "version": "7", + "when": 1792283936471, + "tag": "0051_idle_seconds", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/components/LeaderboardTable.svelte b/src/lib/components/LeaderboardTable.svelte index 126d3d9d..d8eabfb9 100644 --- a/src/lib/components/LeaderboardTable.svelte +++ b/src/lib/components/LeaderboardTable.svelte @@ -249,7 +249,9 @@ {#if show('deaths')}{fmtNum(r.deaths)}{/if} {#if show('kd')}{ratio(kdRatio(r.kills, r.deaths))}{/if} {#if show('perHour')} - {ratio(perHour(r.kills, r.minutes, r.seedMinutes), 1)} + {ratio(perHour(r.kills, r.minutes, r.seedMinutes, r.idleMinutes), 1)} {/if} {#if show('headshots')}{r.headshots}{/if} {#if show('teamKills')} @@ -262,7 +264,9 @@ {#if show('winRate')}{pct(winRate(r.wins, r.losses, r.draws))}{/if} {#if show('cash')}{fmtCash(r.cash)}{/if} {#if show('cashPerMin')} - {fmtCash(perMinute(r.cash, r.cashMinutes, r.seedMinutes))} + {fmtCash(perMinute(r.cash, r.cashMinutes, r.seedMinutes, r.idleMinutes))} {/if} {#if show('lastSeen')} diff --git a/src/lib/leaderboard.test.ts b/src/lib/leaderboard.test.ts index b695d606..3897960d 100644 --- a/src/lib/leaderboard.test.ts +++ b/src/lib/leaderboard.test.ts @@ -87,6 +87,14 @@ describe('ratios with zero denominators', () => { expect(perHour(30, 90, 30)).toBe(30); expect(perHour(30, 90, 90)).toBeNull(); }); + test('kills per hour leaves out the wait for players, seed time or idle time, whichever is longer', () => { + expect(perHour(30, 90, 0, 30)).toBe(30); + // the two mostly count the same minutes, so they are not added up + expect(perHour(30, 90, 30, 30)).toBe(30); + expect(perHour(30, 90, 10, 30)).toBe(30); + expect(perHour(30, 90, 30, 10)).toBe(30); + expect(perHour(30, 90, 0, 90)).toBeNull(); + }); test('cash per minute needs playtime, and seed time is not playtime', () => { expect(perMinute(900, 90)).toBe(10); expect(perMinute(900, 0)).toBeNull(); @@ -94,6 +102,12 @@ describe('ratios with zero denominators', () => { expect(perMinute(900, 90, 30)).toBe(15); expect(perMinute(900, 90, 90)).toBeNull(); }); + test('cash per minute leaves out the wait for players as kills per hour does', () => { + expect(perMinute(900, 90, 0, 30)).toBe(15); + expect(perMinute(900, 90, 30, 30)).toBe(15); + expect(perMinute(900, 90, 10, 30)).toBe(15); + expect(perMinute(900, 90, 0, 90)).toBeNull(); + }); test('win rate needs a match with a result', () => { expect(winRate(3, 1, 0)).toBe(0.75); expect(winRate(1, 1, 2)).toBe(0.25); @@ -180,6 +194,7 @@ describe('metricValue', () => { minutes: 120, cashMinutes: 120, seedMinutes: 45, + idleMinutes: 0, kills: 40, deaths: 0, headshots: 5, diff --git a/src/lib/leaderboard.ts b/src/lib/leaderboard.ts index 90add6a3..abcede70 100644 --- a/src/lib/leaderboard.ts +++ b/src/lib/leaderboard.ts @@ -101,6 +101,8 @@ export interface BoardRow { cashMinutes: number; /** minutes on with the server low, as the Seeding reward counts them (0 without a rule) */ seedMinutes: number; + /** minutes on in matches before any side had scored, the match waiting for its players */ + idleMinutes: number; kills: number; deaths: number; headshots: number; @@ -245,16 +247,33 @@ export const rangeStart = (range: FixedRange, now = Date.now()): Date | null => export const kdRatio = (kills: number, deaths: number): number | null => deaths > 0 ? kills / deaths : kills > 0 ? kills : null; -/** Kills per hour of playtime, seed time left out; nothing without playtime beyond it. */ -export const perHour = (kills: number, minutes: number, seedMinutes = 0): number | null => - minutes - seedMinutes > 0 ? kills / ((minutes - seedMinutes) / 60) : null; +/** + * Kills per hour of playtime, the wait for players left out: seed time and idle time both count + * it, mostly over the same minutes, so the larger of the two goes. Nothing without playtime beyond it. + */ +export const perHour = ( + kills: number, + minutes: number, + seedMinutes = 0, + idleMinutes = 0 +): number | null => { + const played = minutes - Math.max(seedMinutes, idleMinutes); + return played > 0 ? kills / (played / 60) : null; +}; /** - * Cash per minute of the time it was made in (a board row's cashMinutes), seed time left out as - * for kills per hour; nothing without such time. + * Cash per minute of the time it was made in (a board row's cashMinutes), the wait for players + * left out as for kills per hour; nothing without such time. */ -export const perMinute = (cash: number, minutes: number, seedMinutes = 0): number | null => - minutes - seedMinutes > 0 ? cash / (minutes - seedMinutes) : null; +export const perMinute = ( + cash: number, + minutes: number, + seedMinutes = 0, + idleMinutes = 0 +): number | null => { + const played = minutes - Math.max(seedMinutes, idleMinutes); + return played > 0 ? cash / played : null; +}; /** Wins over the matches that had a result; nothing without one. */ export const winRate = (wins: number, losses: number, draws: number): number | null => { @@ -332,7 +351,7 @@ export function metricValue(row: BoardRow, metric: BoardMetric): number | null { case 'kd': return kdRatio(row.kills, row.deaths); case 'perHour': - return perHour(row.kills, row.minutes, row.seedMinutes); + return perHour(row.kills, row.minutes, row.seedMinutes, row.idleMinutes); case 'playtime': return row.minutes; case 'seeded': @@ -346,7 +365,7 @@ export function metricValue(row: BoardRow, metric: BoardMetric): number | null { case 'cash': return row.cash; case 'cashPerMin': - return perMinute(row.cash, row.cashMinutes, row.seedMinutes); + return perMinute(row.cash, row.cashMinutes, row.seedMinutes, row.idleMinutes); } } diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 38891313..008d4185 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -709,7 +709,9 @@ export const matchPlayers = pgTable( /** null without a feed or a distance */ longestM: real('longest_m'), killStreak: integer('kill_streak').notNull().default(0), - deathStreak: integer('death_streak').notNull().default(0) + deathStreak: integer('death_streak').notNull().default(0), + /** of `seconds`, the time on before any side had scored: the match waiting for its players */ + idleSeconds: integer('idle_seconds').notNull().default(0) }, (t) => [ primaryKey({ columns: [t.matchId, t.steamId] }), @@ -753,7 +755,9 @@ export const playerTotals = pgTable( deathStreak: integer('death_streak').notNull().default(0), wins: integer('wins').notNull().default(0), losses: integer('losses').notNull().default(0), - draws: integer('draws').notNull().default(0) + draws: integer('draws').notNull().default(0), + /** the lines' idle_seconds */ + idleSeconds: bigint('idle_seconds', { mode: 'number' }).notNull().default(0) }, (t) => [primaryKey({ columns: [t.serverId, t.steamId] })] ); @@ -796,7 +800,9 @@ export const playerDays = pgTable( deathStreak: integer('death_streak').notNull().default(0), wins: integer('wins').notNull().default(0), losses: integer('losses').notNull().default(0), - draws: integer('draws').notNull().default(0) + draws: integer('draws').notNull().default(0), + /** the idle_seconds of lines of matches that ended on the day */ + idleSeconds: bigint('idle_seconds', { mode: 'number' }).notNull().default(0) }, (t) => [primaryKey({ columns: [t.serverId, t.day, t.steamId] })] ); diff --git a/src/lib/server/leaderboards.ts b/src/lib/server/leaderboards.ts index f81ca4af..a922b96a 100644 --- a/src/lib/server/leaderboards.ts +++ b/src/lib/server/leaderboards.ts @@ -93,24 +93,24 @@ export const rangeBase = (ids: string[], from: Date) => sql` parts AS ( SELECT d.steam_id, d.sessions AS n, d.seconds AS sec, d.seconds AS cs, d.seed_seconds AS seed, d.cash, d.last_seen AS ls, d.matches AS m, d.kills, d.deaths, d.headshots, d.team_kills, d.suicides, - d.vehicle_kills, d.kill_streak, d.death_streak, d.wins, d.losses, d.draws + d.vehicle_kills, d.kill_streak, d.death_streak, d.wins, d.losses, d.draws, d.idle_seconds AS idle FROM player_days d, edge WHERE d.server_id IN ${ids} AND d.day >= edge.d1 UNION ALL SELECT d.steam_id, 0, EXTRACT(EPOCH FROM (edge.m1 - GREATEST(j, edge.f))), EXTRACT(EPOCH FROM (edge.m1 - j)), - 0, 0, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 FROM player_days d CROSS JOIN edge CROSS JOIN LATERAL unnest(d.crossings) j WHERE d.server_id IN ${ids} AND d.day = edge.d1 AND d.crossings IS NOT NULL UNION ALL SELECT s.steam_id, 1, EXTRACT(EPOCH FROM (COALESCE(s.left_at, now()) - GREATEST(s.joined_at, edge.f))), EXTRACT(EPOCH FROM (COALESCE(s.left_at, now()) - s.joined_at)), - s.seed_seconds, s.cash, s.last_seen, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + s.seed_seconds, s.cash, s.last_seen, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 FROM player_sessions s, edge WHERE s.server_id IN ${ids} AND s.last_seen >= edge.f AND (s.left_at IS NULL OR (s.left_at >= edge.f AND s.left_at < edge.m1)) UNION ALL SELECT p.steam_id, 0, 0, 0, 0, 0, NULL, 1, p.kills, p.deaths, p.headshots, p.team_kills, p.suicides, p.vehicle_kills, p.kill_streak, p.death_streak, CASE WHEN x.r = 'win' THEN 1 ELSE 0 END, - CASE WHEN x.r = 'loss' THEN 1 ELSE 0 END, CASE WHEN x.r = 'draw' THEN 1 ELSE 0 END + CASE WHEN x.r = 'loss' THEN 1 ELSE 0 END, CASE WHEN x.r = 'draw' THEN 1 ELSE 0 END, p.idle_seconds FROM edge, matches mt JOIN match_players p ON p.match_id = mt.id AND p.server_id = mt.server_id CROSS JOIN LATERAL (SELECT match_result(mt.final_scores, mt.winner, p.faction) AS r) x @@ -121,7 +121,7 @@ export const rangeBase = (ids: string[], from: Date) => sql` SUM(headshots) AS headshots, SUM(team_kills) AS team_kills, SUM(suicides) AS suicides, SUM(vehicle_kills) AS vehicle_kills, MAX(kill_streak) FILTER (WHERE m > 0) AS kill_streak, MAX(death_streak) FILTER (WHERE m > 0) AS death_streak, - SUM(wins) AS wins, SUM(losses) AS losses, SUM(draws) AS draws + SUM(wins) AS wins, SUM(losses) AS losses, SUM(draws) AS draws, SUM(idle) AS idle FROM parts GROUP BY steam_id HAVING SUM(n) > 0 OR SUM(m) > 0), base AS ( SELECT steam_id, @@ -137,7 +137,8 @@ export const rangeBase = (ids: string[], from: Date) => sql` CASE WHEN m > 0 THEN kill_streak ELSE 0 END AS kill_streak, CASE WHEN m > 0 THEN death_streak ELSE 0 END AS death_streak, CASE WHEN m > 0 THEN m ELSE 0 END AS matches, CASE WHEN m > 0 THEN wins ELSE 0 END AS wins, - CASE WHEN m > 0 THEN losses ELSE 0 END AS losses, CASE WHEN m > 0 THEN draws ELSE 0 END AS draws + CASE WHEN m > 0 THEN losses ELSE 0 END AS losses, CASE WHEN m > 0 THEN draws ELSE 0 END AS draws, + CASE WHEN m > 0 THEN idle / 60.0 ELSE 0 END AS idle_minutes FROM player)`; /** @@ -167,12 +168,12 @@ export const seasonBase = (ids: string[], from: Date, to: Date) => sql` parts AS ( SELECT d.steam_id, d.sessions AS n, d.seconds AS sec, d.seconds AS cs, d.seed_seconds AS seed, d.cash, d.matches AS m, d.kills, d.deaths, d.headshots, d.team_kills, d.suicides, d.vehicle_kills, - d.kill_streak, d.death_streak, d.wins, d.losses, d.draws + d.kill_streak, d.death_streak, d.wins, d.losses, d.draws, d.idle_seconds AS idle FROM player_days d, edge WHERE d.server_id IN ${ids} AND d.day >= edge.d1 AND d.day < edge.dt UNION ALL SELECT d.steam_id, 0, EXTRACT(EPOCH FROM (LEAST(edge.m1, edge.t) - GREATEST(j, edge.f))), CASE WHEN edge.late THEN EXTRACT(EPOCH FROM (edge.m1 - j)) ELSE 0 END, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 FROM player_days d CROSS JOIN edge CROSS JOIN LATERAL unnest(d.crossings) j WHERE d.server_id IN ${ids} AND d.day = edge.d1 AND d.crossings IS NOT NULL AND j < edge.t UNION ALL @@ -180,14 +181,14 @@ export const seasonBase = (ids: string[], from: Date, to: Date) => sql` EXTRACT(EPOCH FROM (LEAST(s.left_at, edge.t) - GREATEST(s.joined_at, edge.f))), CASE WHEN s.left_at < edge.t THEN EXTRACT(EPOCH FROM (s.left_at - s.joined_at)) ELSE 0 END, CASE WHEN s.left_at < edge.t THEN s.seed_seconds ELSE 0 END, - CASE WHEN s.left_at < edge.t THEN s.cash ELSE 0 END, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + CASE WHEN s.left_at < edge.t THEN s.cash ELSE 0 END, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 FROM player_sessions s, edge WHERE s.server_id IN ${ids} AND s.left_at >= edge.f AND s.left_at < edge.m1 AND (s.left_at < edge.t OR s.joined_at < edge.t) UNION ALL SELECT d.steam_id, 0, EXTRACT(EPOCH FROM (edge.t - GREATEST(j, edge.mt))), -GREATEST(EXTRACT(EPOCH FROM (edge.mt - j)), 0), 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 FROM player_days d CROSS JOIN edge CROSS JOIN LATERAL unnest(d.crossings) j WHERE edge.late AND d.server_id IN ${ids} AND d.day = edge.dt + 1 AND d.crossings IS NOT NULL AND j < edge.t @@ -197,20 +198,20 @@ export const seasonBase = (ids: string[], from: Date, to: Date) => sql` CASE WHEN s.left_at < edge.t THEN EXTRACT(EPOCH FROM (s.left_at - GREATEST(s.joined_at, edge.mt))) ELSE -GREATEST(EXTRACT(EPOCH FROM (edge.mt - s.joined_at)), 0) END, CASE WHEN s.left_at < edge.t THEN s.seed_seconds ELSE 0 END, - CASE WHEN s.left_at < edge.t THEN s.cash ELSE 0 END, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + CASE WHEN s.left_at < edge.t THEN s.cash ELSE 0 END, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 FROM player_sessions s, edge WHERE edge.late AND s.server_id IN ${ids} AND s.left_at >= edge.mt AND s.left_at < edge.mn AND (s.left_at < edge.t OR s.joined_at < edge.t) UNION ALL SELECT s.steam_id, 1, EXTRACT(EPOCH FROM (LEAST(now(), edge.t) - GREATEST(s.joined_at, edge.f))), - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 FROM player_sessions s, edge WHERE s.server_id IN ${ids} AND s.left_at IS NULL AND s.last_seen >= edge.f AND s.joined_at < edge.t UNION ALL SELECT p.steam_id, 0, 0, 0, 0, 0, 1, p.kills, p.deaths, p.headshots, p.team_kills, p.suicides, p.vehicle_kills, p.kill_streak, p.death_streak, CASE WHEN x.r = 'win' THEN 1 ELSE 0 END, - CASE WHEN x.r = 'loss' THEN 1 ELSE 0 END, CASE WHEN x.r = 'draw' THEN 1 ELSE 0 END + CASE WHEN x.r = 'loss' THEN 1 ELSE 0 END, CASE WHEN x.r = 'draw' THEN 1 ELSE 0 END, p.idle_seconds FROM edge, matches mt JOIN match_players p ON p.match_id = mt.id AND p.server_id = mt.server_id CROSS JOIN LATERAL (SELECT match_result(mt.final_scores, mt.winner, p.faction) AS r) x @@ -223,7 +224,7 @@ export const seasonBase = (ids: string[], from: Date, to: Date) => sql` SUM(team_kills) AS team_kills, SUM(suicides) AS suicides, SUM(vehicle_kills) AS vehicle_kills, MAX(kill_streak) FILTER (WHERE m > 0) AS kill_streak, MAX(death_streak) FILTER (WHERE m > 0) AS death_streak, - SUM(wins) AS wins, SUM(losses) AS losses, SUM(draws) AS draws + SUM(wins) AS wins, SUM(losses) AS losses, SUM(draws) AS draws, SUM(idle) AS idle FROM parts GROUP BY steam_id HAVING SUM(n) > 0 OR SUM(sec) <> 0 OR SUM(m) > 0), base AS ( SELECT steam_id, @@ -239,7 +240,8 @@ export const seasonBase = (ids: string[], from: Date, to: Date) => sql` CASE WHEN m > 0 THEN kill_streak ELSE 0 END AS kill_streak, CASE WHEN m > 0 THEN death_streak ELSE 0 END AS death_streak, CASE WHEN m > 0 THEN m ELSE 0 END AS matches, CASE WHEN m > 0 THEN wins ELSE 0 END AS wins, - CASE WHEN m > 0 THEN losses ELSE 0 END AS losses, CASE WHEN m > 0 THEN draws ELSE 0 END AS draws + CASE WHEN m > 0 THEN losses ELSE 0 END AS losses, CASE WHEN m > 0 THEN draws ELSE 0 END AS draws, + CASE WHEN m > 0 THEN idle / 60.0 ELSE 0 END AS idle_minutes FROM player)`; /** @@ -267,7 +269,8 @@ export const totalsBase = (ids: string[], steamIds: string[] | null = null) => s SUM(kills) AS kills, SUM(deaths) AS deaths, SUM(headshots) AS headshots, SUM(team_kills) AS team_kills, SUM(suicides) AS suicides, SUM(vehicle_kills) AS vehicle_kills, MAX(kill_streak) AS kill_streak, MAX(death_streak) AS death_streak, - SUM(wins) AS wins, SUM(losses) AS losses, SUM(draws) AS draws + SUM(wins) AS wins, SUM(losses) AS losses, SUM(draws) AS draws, + SUM(idle_seconds) / 60.0 AS idle_minutes FROM player_totals WHERE server_id IN ${ids} AND matches > 0 ${steamIds === null ? sql`` : sql`AND steam_id IN ${steamIds}`} GROUP BY steam_id), @@ -280,7 +283,8 @@ export const totalsBase = (ids: string[], steamIds: string[] | null = null) => s COALESCE(mt.suicides, 0) AS suicides, COALESCE(mt.vehicle_kills, 0) AS vehicle_kills, COALESCE(mt.kill_streak, 0) AS kill_streak, COALESCE(mt.death_streak, 0) AS death_streak, COALESCE(mt.matches, 0) AS matches, COALESCE(mt.wins, 0) AS wins, - COALESCE(mt.losses, 0) AS losses, COALESCE(mt.draws, 0) AS draws + COALESCE(mt.losses, 0) AS losses, COALESCE(mt.draws, 0) AS draws, + COALESCE(mt.idle_minutes, 0) AS idle_minutes FROM sess FULL JOIN mt USING (steam_id))`; /** All recorded games on these servers, batched for the connected-player risk badges. */ @@ -349,14 +353,18 @@ const METRIC_SQL: Record> = { kills: sql`kills`, deaths: sql`deaths`, kd: sql`CASE WHEN deaths > 0 THEN kills::float / deaths WHEN kills > 0 THEN kills::float ELSE NULL END`, - perHour: sql`CASE WHEN minutes - seed_minutes > 0 THEN kills::float / ((minutes - seed_minutes) / 60) ELSE NULL END`, + // as perHour and perMinute in $lib/leaderboard: seed time and idle time both count the wait for + // players, so the larger goes + perHour: sql`CASE WHEN minutes - GREATEST(seed_minutes, idle_minutes) > 0 + THEN kills::float / ((minutes - GREATEST(seed_minutes, idle_minutes)) / 60) ELSE NULL END`, playtime: sql`minutes`, seeded: sql`seed_minutes`, matches: sql`matches`, wins: sql`wins`, winRate: sql`CASE WHEN wins + losses + draws > 0 THEN wins::float / (wins + losses + draws) ELSE NULL END`, cash: sql`cash`, - cashPerMin: sql`CASE WHEN cash_minutes - seed_minutes > 0 THEN cash::float / (cash_minutes - seed_minutes) ELSE NULL END` + cashPerMin: sql`CASE WHEN cash_minutes - GREATEST(seed_minutes, idle_minutes) > 0 + THEN cash::float / (cash_minutes - GREATEST(seed_minutes, idle_minutes)) ELSE NULL END` }; interface BaseRow extends Record { @@ -365,6 +373,7 @@ interface BaseRow extends Record { minutes: string; cashMinutes: string; seedMinutes: string; + idleMinutes: string; cash: string; lastSeen: Date | null; kills: string; @@ -427,7 +436,7 @@ async function boardSlice( ORDER BY ${METRIC_SQL[q.sort]} ${order}, kills DESC, steam_id LIMIT ${limit} OFFSET ${offset}) SELECT r.steam_id AS "steamId", r.minutes, r.cash_minutes AS "cashMinutes", r.seed_minutes AS "seedMinutes", - r.cash, r.last_seen AS "lastSeen", + r.idle_minutes AS "idleMinutes", r.cash, r.last_seen AS "lastSeen", r.kills, r.headshots, r.team_kills AS "teamKills", r.deaths, r.suicides, r.vehicle_kills AS "vehicleKills", r.kill_streak AS "killStreak", r.death_streak AS "deathStreak", r.matches, r.wins, r.losses, r.draws, r.total, @@ -642,6 +651,7 @@ const shapeRow = (r: BaseRow, rank: number): BoardRow => ({ minutes: Math.round(num(r.minutes)), cashMinutes: Math.round(num(r.cashMinutes)), seedMinutes: Math.round(num(r.seedMinutes)), + idleMinutes: Math.round(num(r.idleMinutes)), kills: num(r.kills), deaths: num(r.deaths), headshots: num(r.headshots), diff --git a/src/lib/server/match-players.test.ts b/src/lib/server/match-players.test.ts index 796dc97d..f2e17f20 100644 --- a/src/lib/server/match-players.test.ts +++ b/src/lib/server/match-players.test.ts @@ -21,8 +21,20 @@ const player = (steamId: string, kills: number, deaths: number, cash = 0): Playe const TEAMS = ['Red', 'Blue', 'Green']; // Each look's gap is the time since the look before it, as the worker measures it. let prevNow = 0; -const look = (t: Tallies, players: Player[], now: number, stayed: string[] = []) => { - tallyLook(t, players, { now, gapMs: now - prevNow, stayed: new Set(stayed), teams: TEAMS }); +const look = ( + t: Tallies, + players: Player[], + now: number, + stayed: string[] = [], + scored?: boolean +) => { + tallyLook(t, players, { + now, + gapMs: now - prevNow, + stayed: new Set(stayed), + teams: TEAMS, + scored + }); prevNow = now; }; @@ -36,6 +48,7 @@ describe('a match tally', () => { name: 'a', faction: 'Red', seconds: 2, + idleSeconds: 0, kills: 5, deaths: 2, cashDelta: 150 @@ -64,6 +77,15 @@ describe('a match tally', () => { expect(t.get('b')!.ms).toBe(0); }); + test('time on before any side has scored is idle time too; unknown scores make none', () => { + const t: Tallies = new Map(); + look(t, [player('a', 0, 0)], 1000, [], false); + look(t, [player('a', 0, 0)], 61_000, ['a'], false); // waiting for players + look(t, [player('a', 2, 0)], 91_000, ['a'], true); // a side has scored + look(t, [player('a', 2, 0)], 101_000, ['a']); // no status to read the scores from + expect(tallyRow(t.get('a')!)).toMatchObject({ seconds: 100, idleSeconds: 60 }); + }); + test('keeps the last side that was a team, and the last name', () => { const t: Tallies = new Map(); look(t, [{ ...player('a', 0, 0), faction: 'White', name: 'a' }], 1000); @@ -83,7 +105,16 @@ describe('closing a match', () => { look(t, [player('a', 6, 3, 400)], 9000, ['a']); const { rows, carried } = closeTallies(t, 8000); expect(rows).toEqual([ - { steamId: 'a', name: 'a', faction: 'Red', seconds: 8, kills: 10, deaths: 4, cashDelta: 300 } + { + steamId: 'a', + name: 'a', + faction: 'Red', + seconds: 8, + idleSeconds: 0, + kills: 10, + deaths: 4, + cashDelta: 300 + } ]); expect(carried.size).toBe(0); }); @@ -96,7 +127,16 @@ describe('closing a match', () => { look(t, [player('a', 1, 0, 50)], 7000, ['a']); const { rows, carried } = closeTallies(t, 6000); expect(rows).toEqual([ - { steamId: 'a', name: 'a', faction: 'Red', seconds: 6, kills: 9, deaths: 4, cashDelta: 200 } + { + steamId: 'a', + name: 'a', + faction: 'Red', + seconds: 6, + idleSeconds: 0, + kills: 9, + deaths: 4, + cashDelta: 200 + } ]); const next = carried.get('a')!; expect(next.banked).toEqual({ kills: 0, deaths: 0, cash: 0 }); diff --git a/src/lib/server/match-players.ts b/src/lib/server/match-players.ts index 31404532..1b3f37f1 100644 --- a/src/lib/server/match-players.ts +++ b/src/lib/server/match-players.ts @@ -35,6 +35,8 @@ export interface MatchTally { droppedAt: number; /** time on during the match: the gaps between trusted looks the player was on for */ ms: number; + /** of `ms`, the gaps when no side had scored yet: the match waiting for its players */ + idleMs: number; lastSeen: number; /** the row is due at the next match stage: the player left (their part is then safe from a * worker restart), or the row is being closed */ @@ -49,6 +51,8 @@ export interface TallyRow { name: string; faction: string | null; seconds: number; + /** of `seconds`, the time on before any side had scored */ + idleSeconds: number; kills: number; deaths: number; /** the cash earned over the match: each run of the counters' last look less its first */ @@ -57,13 +61,20 @@ export interface TallyRow { /** * Applies one look at the player list. `stayed` names the players who were also on at the - * previous look, when that look was recent enough to trust: they earn `gapMs` of time on. + * previous look, when that look was recent enough to trust: they earn `gapMs` of time on, and of + * idle time too while no side has scored (`scored` false: the match is waiting for its players). * Everyone on the list gets a tally; a newcomer starts from the counters as they stand. */ export function tallyLook( tallies: Tallies, players: Player[], - opts: { now: number; gapMs: number; stayed: ReadonlySet; teams?: readonly string[] } + opts: { + now: number; + gapMs: number; + stayed: ReadonlySet; + teams?: readonly string[]; + scored?: boolean; + } ): void { for (const p of players) { if (!p.steamId) continue; @@ -79,6 +90,7 @@ export function tallyLook( cashLast: p.cash, droppedAt: 0, ms: 0, + idleMs: 0, lastSeen: opts.now, dirty: false }); @@ -95,7 +107,10 @@ export function tallyLook( t.cashLast = p.cash; t.name = p.name; if (isTeam(p.faction, opts.teams)) t.faction = p.faction; - if (opts.stayed.has(p.steamId)) t.ms += opts.gapMs; + if (opts.stayed.has(p.steamId)) { + t.ms += opts.gapMs; + if (opts.scored === false) t.idleMs += opts.gapMs; + } t.lastSeen = opts.now; } } @@ -106,6 +121,7 @@ export const tallyRow = (t: MatchTally): TallyRow => ({ name: t.name, faction: t.faction, seconds: Math.round(t.ms / 1000), + idleSeconds: Math.round(t.idleMs / 1000), kills: t.banked.kills + t.last.kills, deaths: t.banked.deaths + t.last.deaths, cashDelta: t.banked.cash + t.cashLast - t.cashFirst @@ -135,6 +151,7 @@ export function closeTallies( name: t.name, faction: t.faction, seconds: Math.round(t.ms / 1000), + idleSeconds: Math.round(t.idleMs / 1000), kills: t.banked.kills, deaths: t.banked.deaths, cashDelta: t.banked.cash @@ -145,6 +162,7 @@ export function closeTallies( last: { ...t.last }, droppedAt: 0, ms: 0, + idleMs: 0, dirty: false }); } @@ -160,22 +178,27 @@ export async function writeMatchPlayers( ): Promise { if (!rows.length) return; await db.execute(sql` - INSERT INTO match_players (match_id, server_id, steam_id, name, faction, seconds, kills, deaths, cash_delta) - SELECT ${matchId}::bigint, ${serverId}, v.steam_id, v.name, v.faction, v.seconds, v.kills, v.deaths, v.cash_delta + INSERT INTO match_players (match_id, server_id, steam_id, name, faction, seconds, idle_seconds, kills, deaths, + cash_delta) + SELECT ${matchId}::bigint, ${serverId}, v.steam_id, v.name, v.faction, v.seconds, v.idle_seconds, v.kills, + v.deaths, v.cash_delta FROM jsonb_to_recordset(${json( rows.map((r) => ({ steam_id: r.steamId, name: r.name, faction: r.faction, seconds: r.seconds, + idle_seconds: r.idleSeconds, kills: r.kills, deaths: r.deaths, cash_delta: r.cashDelta })) - )}) AS v(steam_id text, name text, faction text, seconds int, kills int, deaths int, cash_delta int) + )}) AS v(steam_id text, name text, faction text, seconds int, idle_seconds int, kills int, + deaths int, cash_delta int) ON CONFLICT (match_id, steam_id) DO UPDATE SET name = EXCLUDED.name, faction = EXCLUDED.faction, seconds = EXCLUDED.seconds, - kills = EXCLUDED.kills, deaths = EXCLUDED.deaths, cash_delta = EXCLUDED.cash_delta`); + idle_seconds = EXCLUDED.idle_seconds, kills = EXCLUDED.kills, deaths = EXCLUDED.deaths, + cash_delta = EXCLUDED.cash_delta`); } /** One kill of the match, in match order, as the feed pass reads it. */ diff --git a/src/lib/server/observe.ts b/src/lib/server/observe.ts index c6747dfb..8134654b 100644 --- a/src/lib/server/observe.ts +++ b/src/lib/server/observe.ts @@ -742,13 +742,15 @@ export async function observeServer(env: Env, m: ServerMemory, kinds: ObserveKin // Memory follows every player observation; the database only when something is due. for (const { player: p, session: s } of diff.stayed) followPlayer(s, p, started, teams); // The match tallies follow the same look: everyone on the list, time for those on since the - // previous trusted look, and a leaver's row is due at the next match stage. + // previous trusted look (idle while no side has scored), and a leaver's row is due at the next + // match stage. if (players) { tallyLook(m.tallies, players, { now: started, gapMs, stayed: joinsTrusted ? new Set(diff.stayed.map((x) => x.player.steamId)) : new Set(), - teams + teams, + scored: m.status ? m.status.scores.some((s) => s.score > 0) : undefined }); for (const s of diff.left) { const t = m.tallies.get(s.steamId); diff --git a/src/routes/api/servers/[id]/leaderboard/export/+server.ts b/src/routes/api/servers/[id]/leaderboard/export/+server.ts index 9d48da3e..2de3a368 100644 --- a/src/routes/api/servers/[id]/leaderboard/export/+server.ts +++ b/src/routes/api/servers/[id]/leaderboard/export/+server.ts @@ -32,7 +32,7 @@ const COLUMNS: [string, (r: BoardRow) => unknown][] = [ ['kills', (r) => r.kills], ['deaths', (r) => r.deaths], ['kd', (r) => round(kdRatio(r.kills, r.deaths), 2)], - ['kills_per_hour', (r) => round(perHour(r.kills, r.minutes, r.seedMinutes), 1)], + ['kills_per_hour', (r) => round(perHour(r.kills, r.minutes, r.seedMinutes, r.idleMinutes), 1)], ['headshots', (r) => r.headshots], ['team_kills', (r) => r.teamKills], ['suicides', (r) => r.suicides], @@ -45,7 +45,7 @@ const COLUMNS: [string, (r: BoardRow) => unknown][] = [ ['draws', (r) => r.draws], ['win_pct', (r) => percent(winRate(r.wins, r.losses, r.draws))], ['cash', (r) => r.cash], - ['cash_per_min', (r) => round(perMinute(r.cash, r.cashMinutes, r.seedMinutes), 1)], + ['cash_per_min', (r) => round(perMinute(r.cash, r.cashMinutes, r.seedMinutes, r.idleMinutes), 1)], ['last_seen', (r) => r.lastSeen] ]; diff --git a/src/test/db.ts b/src/test/db.ts index a191e0f0..2f2642a6 100644 --- a/src/test/db.ts +++ b/src/test/db.ts @@ -8,6 +8,8 @@ // Without it the database suites are skipped (CI sets it, and fails when it is missing). import { SQL } from 'bun'; import { randomBytes } from 'node:crypto'; +import { sql as query } from 'drizzle-orm'; +import type { DbOrTx } from '$lib/server/db'; import type { Env } from '$lib/server/env'; const PREFIX = 'warcon_test_'; @@ -19,6 +21,23 @@ if (!base && process.env.CI) /** False when there is no test database to use; suites pass it to describe.skipIf. */ export const hasTestDb = !!base; +/** + * Match lines for a database migrated only part of the way, with just the columns each one gives: + * Drizzle's insert names every column the schema has, and one a later migration adds is not there + * yet. + */ +export async function insertEarlyLines(db: DbOrTx, rows: Record[]): Promise { + for (const r of rows) { + const columns = Object.keys(r).map((k) => + query.identifier(k.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)) + ); + const values = Object.values(r).map((v) => query`${v}`); + await db.execute( + query`INSERT INTO match_players (${query.join(columns, query`, `)}) VALUES (${query.join(values, query`, `)})` + ); + } +} + type Shared = { env: Promise; name: string }; // One database per `bun test` process, shared by every file: the module registry is per file for // mocked modules, so the handle lives on globalThis. diff --git a/src/test/match-feed-backfill.test.ts b/src/test/match-feed-backfill.test.ts index b6697a27..e62b2d72 100644 --- a/src/test/match-feed-backfill.test.ts +++ b/src/test/match-feed-backfill.test.ts @@ -14,7 +14,7 @@ import { join } from 'node:path'; import { and, asc, eq, inArray, sql } from 'drizzle-orm'; import { connect, runMigrations, type Db } from '$lib/server/db'; import { kills, matches, matchPlayers, playerSessions } from '$lib/server/db/schema'; -import { hasTestDb } from './db'; +import { hasTestDb, insertEarlyLines } from './db'; import { oracleBase, oracleRangeBase, rangeRows, totalsRows } from './totals-oracle'; const FEED = '0047_match_players_from_feed'; @@ -367,15 +367,13 @@ describe.skipIf(!hasTestDb)('the match rows from the feed', () => { session(B, R, -0.9, -0.1, 'White', 3, 1) ]); await run(db, '0032_match_players_backfill'); - await db - .insert(matchPlayers) - .values([ - worker(m[14].id, A, 9, 9, 700), - worker(m[15].id, A, 1, 0, 0), - { ...worker(m[13].id, A, 6, 2, 0), steamId: WK }, - worker(o[1].id, B, 1, 0, 0), - worker(o[2].id, B, 4, 4, 300) - ]); + await insertEarlyLines(db, [ + worker(m[14].id, A, 9, 9, 700), + worker(m[15].id, A, 1, 0, 0), + { ...worker(m[13].id, A, 6, 2, 0), steamId: WK }, + worker(o[1].id, B, 1, 0, 0), + worker(o[2].id, B, 4, 4, 300) + ]); await db.insert(playerSessions).values(session(A, WK, 9.5, 11.5, 'Lonestar', 6, 2)); await runMigrations(db, 'drizzle'); }, 60_000); @@ -574,7 +572,7 @@ describe.skipIf(!hasTestDb)( session(S, B2, 2.2, 2.8, 'Lonestar', 1, 0) ]); await run(db, '0032_match_players_backfill'); - await db.insert(matchPlayers).values(worker(k[3].id, S, 3, 3, 200)); + await insertEarlyLines(db, [worker(k[3].id, S, 3, 3, 200)]); await runMigrations(db, 'drizzle'); }, 60_000); diff --git a/src/test/match-players.test.ts b/src/test/match-players.test.ts index 3a0f914d..726a4078 100644 --- a/src/test/match-players.test.ts +++ b/src/test/match-players.test.ts @@ -38,19 +38,46 @@ describe.skipIf(!hasTestDb)('match rows', () => { test('a write sets the row; a second write of the same player replaces it', async () => { await writeMatchPlayers(env.db, serverId, matchId, [ - { steamId: A, name: 'a', faction: 'Red', seconds: 60, kills: 3, deaths: 1, cashDelta: 150 }, - { steamId: B, name: 'b', faction: 'Blue', seconds: 30, kills: 0, deaths: 2, cashDelta: -20 } + { + steamId: A, + name: 'a', + faction: 'Red', + seconds: 60, + idleSeconds: 40, + kills: 3, + deaths: 1, + cashDelta: 150 + }, + { + steamId: B, + name: 'b', + faction: 'Blue', + seconds: 30, + idleSeconds: 30, + kills: 0, + deaths: 2, + cashDelta: -20 + } ]); await writeMatchPlayers(env.db, serverId, matchId, [ - { steamId: A, name: 'a2', faction: 'Red', seconds: 600, kills: 12, deaths: 4, cashDelta: 900 } + { + steamId: A, + name: 'a2', + faction: 'Red', + seconds: 600, + idleSeconds: 120, + kills: 12, + deaths: 4, + cashDelta: 900 + } ]); const rows = await rowsOf(); - expect(rows.map((r) => [r.steamId, r.name, r.seconds, r.kills, r.deaths, r.cashDelta])).toEqual( - [ - [A, 'a2', 600, 12, 4, 900], - [B, 'b', 30, 0, 2, -20] - ] - ); + expect( + rows.map((r) => [r.steamId, r.name, r.seconds, r.idleSeconds, r.kills, r.deaths, r.cashDelta]) + ).toEqual([ + [A, 'a2', 600, 120, 12, 4, 900], + [B, 'b', 30, 30, 0, 2, -20] + ]); expect(rows[0].serverId).toBe(serverId); expect(rows[0].headshots).toBe(0); }); diff --git a/src/test/player-totals-writes.test.ts b/src/test/player-totals-writes.test.ts index d5153bb4..25e75e73 100644 --- a/src/test/player-totals-writes.test.ts +++ b/src/test/player-totals-writes.test.ts @@ -33,7 +33,7 @@ import { import { writeMatchPlayers } from '$lib/server/match-players'; import { purgeServerStats } from '$lib/server/stats'; import { lockTotals } from '$lib/server/totals'; -import { hasTestDb, testEnv } from './db'; +import { hasTestDb, insertEarlyLines, testEnv } from './db'; import { seedWorld, type World } from './world'; import { oracleBase, oracleRangeBase, rangeRows, totalsRows } from './totals-oracle'; @@ -239,8 +239,26 @@ describe.skipIf(!hasTestDb)('player totals under every kind of write', () => { await exact([A]); // an upsert of an ended match's lines fires both statement triggers await writeMatchPlayers(env.db, A, m, [ - { steamId: P, name: 'p', faction: 'Valkyra', seconds: 1, kills: 1, deaths: 1, cashDelta: 0 }, - { steamId: R, name: 'r', faction: 'White', seconds: 1, kills: 2, deaths: 0, cashDelta: 0 } + { + steamId: P, + name: 'p', + faction: 'Valkyra', + seconds: 1, + idleSeconds: 0, + kills: 1, + deaths: 1, + cashDelta: 0 + }, + { + steamId: R, + name: 'r', + faction: 'White', + seconds: 1, + idleSeconds: 0, + kills: 2, + deaths: 0, + cashDelta: 0 + } ]); await exact([A]); // a line moved to another player, a match moved to another server, a line deleted @@ -531,6 +549,7 @@ describe.skipIf(!hasTestDb)('player totals under every kind of write', () => { name: 'q', faction: 'Lonestar', seconds: 9, + idleSeconds: 0, kills: 7, deaths: 1, cashDelta: 5 @@ -543,6 +562,7 @@ describe.skipIf(!hasTestDb)('player totals under every kind of write', () => { name: 'q', faction: 'Lonestar', seconds: 9, + idleSeconds: 0, kills: 7, deaths: 1, cashDelta: 5 @@ -552,6 +572,7 @@ describe.skipIf(!hasTestDb)('player totals under every kind of write', () => { name: 'r', faction: 'Valkyra', seconds: 9, + idleSeconds: 0, kills: 1, deaths: 4, cashDelta: 0 @@ -574,6 +595,7 @@ describe.skipIf(!hasTestDb)('player totals under every kind of write', () => { name: 'q', faction: 'Lonestar', seconds: 9, + idleSeconds: 0, kills: 7, deaths: 1, cashDelta: 5 @@ -811,16 +833,14 @@ describe.skipIf(!hasTestDb)('player totals under every kind of write', () => { deaths: 1, killStreak: kills }); - await db - .insert(matchPlayers) - .values([ - l(won.id, 'a', P, 'Valkyra', 5), - l(won.id, 'a', Q, 'White', 2), - l(drawn.id, 'a', P, 'Lonestar', 3), - l(drawn.id, 'a', 'lines-only-player', 'Valkyra', 7), - l(abandoned.id, 'b', P, 'Valkyra', 4), - l(open.id, 'a', Q, 'Lonestar', 9) - ]); + await insertEarlyLines(db, [ + l(won.id, 'a', P, 'Valkyra', 5), + l(won.id, 'a', Q, 'White', 2), + l(drawn.id, 'a', P, 'Lonestar', 3), + l(drawn.id, 'a', 'lines-only-player', 'Valkyra', 7), + l(abandoned.id, 'b', P, 'Valkyra', 4), + l(open.id, 'a', Q, 'Lonestar', 9) + ]); // a negative streak merged the 0038 way where 0038 is in: a session closed first (the // pair's row has no match yet), then a match ended with the player's line at -1 const [late] = await db @@ -834,9 +854,9 @@ describe.skipIf(!hasTestDb)('player totals under every kind of write', () => { .insert(matches) .values({ serverId: 'a', startedAt: at(4.6), map: 'Europe' }) .returning({ id: matches.id }); - await db - .insert(matchPlayers) - .values({ ...l(negative.id, 'a', S, 'Valkyra', 0), killStreak: -1, deathStreak: -2 }); + await insertEarlyLines(db, [ + { ...l(negative.id, 'a', S, 'Valkyra', 0), killStreak: -1, deathStreak: -2 } + ]); await db.execute(sql` UPDATE matches SET ended_at = ${at(5.5)}, final_scores = ${JSON.stringify(scores)}::jsonb, winner = 'Valkyra' diff --git a/src/test/player-totals.test.ts b/src/test/player-totals.test.ts index 21666ca7..1eb587a7 100644 --- a/src/test/player-totals.test.ts +++ b/src/test/player-totals.test.ts @@ -292,7 +292,9 @@ describe.skipIf(!hasTestDb)('player totals against the reads they replace', () = faction: r.pick(FACTIONS), kills: r.int(20), deaths: r.int(20), - killStreak: r.int(6) + killStreak: r.int(6), + // fixed, not drawn: another draw would change every step after it + idleSeconds: 420 }) .onConflictDoNothing(); return; @@ -352,7 +354,8 @@ describe.skipIf(!hasTestDb)('player totals against the reads they replace', () = name: 'odd', faction: i ? 'Lonestar' : 'White', kills: r.int(9), - deaths: r.int(9) + deaths: r.int(9), + idleSeconds: 90 })) ); return; @@ -570,6 +573,7 @@ describe.skipIf(!hasTestDb)('player totals against the reads they replace', () = minutes: Math.round(Number(o.minutes)), cashMinutes: Math.round(Number(o.cashMinutes)), seedMinutes: Math.round(Number(o.seedMinutes)), + idleMinutes: Math.round(Number(o.idleMinutes)), kills: Number(o.kills), deaths: Number(o.deaths), headshots: Number(o.headshots), @@ -658,7 +662,7 @@ describe.skipIf(!hasTestDb)('player totals against the reads they replace', () = (SELECT array_agg(c ORDER BY c) FROM unnest(d.crossings) c), d.sessions, d.seed_seconds, d.cash, d.last_seen, d.matches, d.kills, d.deaths, d.headshots, d.team_kills, d.suicides, d.vehicle_kills, d.kill_streak, d.death_streak, d.wins, - d.losses, d.draws)::text AS row + d.losses, d.draws, d.idle_seconds)::text AS row FROM player_days d WHERE server_id IN ${sets()[4]} ORDER BY server_id, day, steam_id`)) as { row: string; }[]; diff --git a/src/test/totals-oracle.ts b/src/test/totals-oracle.ts index 1fd9f288..606c6422 100644 --- a/src/test/totals-oracle.ts +++ b/src/test/totals-oracle.ts @@ -22,7 +22,7 @@ const lines = ( lines AS ( SELECT p.steam_id, p.match_id, p.server_id, p.faction, p.seconds, p.kills, p.deaths, p.cash_delta, p.headshots, p.team_kills, p.suicides, p.vehicle_kills, p.longest_m, p.kill_streak, p.death_streak, - m.started_at, m.ended_at, m.map, + p.idle_seconds, m.started_at, m.ended_at, m.map, CASE WHEN p.faction IS NULL THEN NULL WHEN jsonb_typeof(m.final_scores) = 'array' AND jsonb_array_length(m.final_scores) > 0 AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(m.final_scores) e WHERE e->>'name' = p.faction) @@ -57,7 +57,8 @@ const base = (ids: string[], from: Date, steamIds: string[] | null = null) => sq MAX(kill_streak) AS kill_streak, MAX(death_streak) AS death_streak, COUNT(*) FILTER (WHERE result = 'win') AS wins, COUNT(*) FILTER (WHERE result = 'loss') AS losses, - COUNT(*) FILTER (WHERE result = 'draw') AS draws + COUNT(*) FILTER (WHERE result = 'draw') AS draws, + SUM(idle_seconds) / 60.0 AS idle_minutes FROM lines GROUP BY steam_id), base AS ( SELECT steam_id, @@ -68,7 +69,8 @@ const base = (ids: string[], from: Date, steamIds: string[] | null = null) => sq COALESCE(mt.suicides, 0) AS suicides, COALESCE(mt.vehicle_kills, 0) AS vehicle_kills, COALESCE(mt.kill_streak, 0) AS kill_streak, COALESCE(mt.death_streak, 0) AS death_streak, COALESCE(mt.matches, 0) AS matches, COALESCE(mt.wins, 0) AS wins, - COALESCE(mt.losses, 0) AS losses, COALESCE(mt.draws, 0) AS draws + COALESCE(mt.losses, 0) AS losses, COALESCE(mt.draws, 0) AS draws, + COALESCE(mt.idle_minutes, 0) AS idle_minutes FROM sess FULL JOIN mt USING (steam_id))`; /** @@ -96,7 +98,8 @@ const seasonSums = (ids: string[], from: Date, to: Date) => sql` MAX(kill_streak) AS kill_streak, MAX(death_streak) AS death_streak, COUNT(*) FILTER (WHERE result = 'win') AS wins, COUNT(*) FILTER (WHERE result = 'loss') AS losses, - COUNT(*) FILTER (WHERE result = 'draw') AS draws + COUNT(*) FILTER (WHERE result = 'draw') AS draws, + SUM(idle_seconds) / 60.0 AS idle_minutes FROM lines GROUP BY steam_id), base AS ( SELECT steam_id, @@ -108,21 +111,24 @@ const seasonSums = (ids: string[], from: Date, to: Date) => sql` COALESCE(mt.suicides, 0) AS suicides, COALESCE(mt.vehicle_kills, 0) AS vehicle_kills, COALESCE(mt.kill_streak, 0) AS kill_streak, COALESCE(mt.death_streak, 0) AS death_streak, COALESCE(mt.matches, 0) AS matches, COALESCE(mt.wins, 0) AS wins, - COALESCE(mt.losses, 0) AS losses, COALESCE(mt.draws, 0) AS draws + COALESCE(mt.losses, 0) AS losses, COALESCE(mt.draws, 0) AS draws, + COALESCE(mt.idle_minutes, 0) AS idle_minutes FROM sess FULL JOIN mt USING (steam_id))`; const METRIC_SQL: Record = { kills: sql`kills`, deaths: sql`deaths`, kd: sql`CASE WHEN deaths > 0 THEN kills::float / deaths WHEN kills > 0 THEN kills::float ELSE NULL END`, - perHour: sql`CASE WHEN minutes - seed_minutes > 0 THEN kills::float / ((minutes - seed_minutes) / 60) ELSE NULL END`, + perHour: sql`CASE WHEN minutes - GREATEST(seed_minutes, idle_minutes) > 0 + THEN kills::float / ((minutes - GREATEST(seed_minutes, idle_minutes)) / 60) ELSE NULL END`, playtime: sql`minutes`, seeded: sql`seed_minutes`, matches: sql`matches`, wins: sql`wins`, winRate: sql`CASE WHEN wins + losses + draws > 0 THEN wins::float / (wins + losses + draws) ELSE NULL END`, cash: sql`cash`, - cashPerMin: sql`CASE WHEN cash_minutes - seed_minutes > 0 THEN cash::float / (cash_minutes - seed_minutes) ELSE NULL END` + cashPerMin: sql`CASE WHEN cash_minutes - GREATEST(seed_minutes, idle_minutes) > 0 + THEN cash::float / (cash_minutes - GREATEST(seed_minutes, idle_minutes)) ELSE NULL END` }; /** Every all-time base row over these servers (or these players), each column as text. */ @@ -147,7 +153,7 @@ export async function oracleRangeBase( SELECT steam_id, minutes::text, trim_scale(cash_minutes)::text AS cash_minutes, seed_minutes::text, cash::text, last_seen::text, kills::text, deaths::text, headshots::text, team_kills::text, suicides::text, vehicle_kills::text, kill_streak::text, - death_streak::text, matches::text, wins::text, losses::text, draws::text + death_streak::text, matches::text, wins::text, losses::text, draws::text, idle_minutes::text FROM base ORDER BY steam_id`)) as Record[]; } @@ -177,7 +183,7 @@ export async function oracleBoard( r.last_seen AS "lastSeen", r.kills::text, r.headshots::text, r.team_kills::text AS "teamKills", r.deaths::text, r.suicides::text, r.vehicle_kills::text AS "vehicleKills", r.kill_streak::text AS "killStreak", r.death_streak::text AS "deathStreak", r.matches::text, - r.wins::text, r.losses::text, r.draws::text, r.total::text, + r.wins::text, r.losses::text, r.draws::text, r.idle_minutes::text AS "idleMinutes", r.total::text, (SELECT name FROM player_sessions ps WHERE ps.steam_id = r.steam_id AND ps.server_id IN ${ids} ORDER BY ps.joined_at DESC, ps.id DESC LIMIT 1) AS name FROM page r`)) as Record[]; @@ -230,7 +236,7 @@ export async function totalsRows( SELECT steam_id, minutes::text, trim_scale(cash_minutes)::text AS cash_minutes, seed_minutes::text, cash::text, last_seen::text, kills::text, deaths::text, headshots::text, team_kills::text, suicides::text, vehicle_kills::text, kill_streak::text, - death_streak::text, matches::text, wins::text, losses::text, draws::text + death_streak::text, matches::text, wins::text, losses::text, draws::text, idle_minutes::text FROM base ORDER BY steam_id`)) as Record[]; } @@ -247,7 +253,7 @@ export async function oracleSeasonBase( SELECT steam_id, minutes::text, trim_scale(cash_minutes)::text AS cash_minutes, seed_minutes::text, cash::text, last_seen::text, kills::text, deaths::text, headshots::text, team_kills::text, suicides::text, vehicle_kills::text, kill_streak::text, - death_streak::text, matches::text, wins::text, losses::text, draws::text + death_streak::text, matches::text, wins::text, losses::text, draws::text, idle_minutes::text FROM base ORDER BY steam_id`)) as Record[]; } @@ -263,7 +269,7 @@ export async function rangeRows( SELECT steam_id, minutes::text, trim_scale(cash_minutes)::text AS cash_minutes, seed_minutes::text, cash::text, last_seen::text, kills::text, deaths::text, headshots::text, team_kills::text, suicides::text, vehicle_kills::text, kill_streak::text, - death_streak::text, matches::text, wins::text, losses::text, draws::text + death_streak::text, matches::text, wins::text, losses::text, draws::text, idle_minutes::text FROM base ORDER BY steam_id`)) as Record[]; } @@ -280,6 +286,6 @@ export async function seasonRows( SELECT steam_id, minutes::text, trim_scale(cash_minutes)::text AS cash_minutes, seed_minutes::text, cash::text, last_seen::text, kills::text, deaths::text, headshots::text, team_kills::text, suicides::text, vehicle_kills::text, kill_streak::text, - death_streak::text, matches::text, wins::text, losses::text, draws::text + death_streak::text, matches::text, wins::text, losses::text, draws::text, idle_minutes::text FROM base ORDER BY steam_id`)) as Record[]; } diff --git a/src/test/unstorable-text.test.ts b/src/test/unstorable-text.test.ts index d7c3ad0f..e940a7cc 100644 --- a/src/test/unstorable-text.test.ts +++ b/src/test/unstorable-text.test.ts @@ -201,6 +201,7 @@ describe.skipIf(!hasTestDb)('text Postgres refuses, from a caller or the game', name: `Half${HIGH}`, faction: 'Red', seconds: 60, + idleSeconds: 0, kills: 1, deaths: 0, cashDelta: 0 @@ -210,6 +211,7 @@ describe.skipIf(!hasTestDb)('text Postgres refuses, from a caller or the game', name: `N${NUL}l`, faction: 'Blue', seconds: 30, + idleSeconds: 0, kills: 0, deaths: 1, cashDelta: 0 @@ -319,7 +321,7 @@ describe.skipIf(!hasTestDb)('text Postgres refuses, from a caller or the game', .insert(matches) .values({ serverId: w.server.id, startedAt: new Date(), map: 'Zestafona', peakPlayers: 2 }) .returning({ id: matches.id }); - const line = { faction: 'Red', seconds: 1, kills: 0, deaths: 0, cashDelta: 0 }; + const line = { faction: 'Red', seconds: 1, idleSeconds: 0, kills: 0, deaths: 0, cashDelta: 0 }; await writeMatchPlayers(env.db, w.server.id, match.id, [ { ...line, steamId: '76561198000000501', name: sqlish }, { ...line, steamId: '76561198000000502', name: jsonish }