mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-02 16:20:36 -05:00
Further db optimizations
This commit is contained in:
@@ -85,19 +85,18 @@ export async function findAllOpenRoomIdsByUserId(
|
||||
.where("GroupMember.createdAt", ">", joinedSince)
|
||||
.where(openRoom("GroupMatch.chatRoomId"))
|
||||
.execute(),
|
||||
tournamentTeamIds.length === 0
|
||||
? []
|
||||
: db
|
||||
.selectFrom("TournamentMatch")
|
||||
.select("TournamentMatch.chatRoomId as id")
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb(opponentTeamId("opponentOne"), "in", tournamentTeamIds),
|
||||
eb(opponentTeamId("opponentTwo"), "in", tournamentTeamIds),
|
||||
]),
|
||||
)
|
||||
.where(openRoom("TournamentMatch.chatRoomId"))
|
||||
.execute(),
|
||||
// a column per query so each opponent's expression index is used directly,
|
||||
// mirroring the alpha/bravo split above
|
||||
...(["opponentOne", "opponentTwo"] as const).map((column) =>
|
||||
tournamentTeamIds.length === 0
|
||||
? []
|
||||
: db
|
||||
.selectFrom("TournamentMatch")
|
||||
.select("TournamentMatch.chatRoomId as id")
|
||||
.where(opponentTeamId(column), "in", tournamentTeamIds)
|
||||
.where(openRoom("TournamentMatch.chatRoomId"))
|
||||
.execute(),
|
||||
),
|
||||
db
|
||||
.selectFrom("TournamentTeamMember")
|
||||
.innerJoin(
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
commonUserSelect,
|
||||
concatUserSubmittedImagePrefix,
|
||||
groupMemberOfSeasonSql,
|
||||
jsonArrayFrom,
|
||||
latestSkillPerSeason,
|
||||
skillCountsAsSeasonSet,
|
||||
@@ -208,6 +209,7 @@ export async function hasEnoughSqMatchesByUserId(userId: number) {
|
||||
.onRef("Skill.userId", "=", "GroupMember.userId"),
|
||||
)
|
||||
.where("GroupMember.userId", "=", userId)
|
||||
.where(groupMemberOfSeasonSql(season.nth))
|
||||
.where(
|
||||
"GroupMatch.createdAt",
|
||||
">",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { addHours } from "date-fns";
|
||||
import { addHours, subDays } from "date-fns";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { list, nthToDateRange, nthToReportingDateRange } from "./Seasons";
|
||||
import {
|
||||
list,
|
||||
nthToDateRange,
|
||||
nthToGroupMembershipDateRange,
|
||||
nthToReportingDateRange,
|
||||
} from "./Seasons";
|
||||
|
||||
describe("nthToDateRange()", () => {
|
||||
test("returns the date range for an existing season", () => {
|
||||
@@ -33,3 +38,15 @@ describe("nthToReportingDateRange()", () => {
|
||||
expect(() => nthToReportingDateRange(list.length)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("nthToGroupMembershipDateRange()", () => {
|
||||
test("starts a week before the season, covering groups formed before it", () => {
|
||||
const { starts } = nthToGroupMembershipDateRange(0);
|
||||
expect(starts).toEqual(subDays(list[0].starts, 7));
|
||||
});
|
||||
|
||||
test("ends when reporting ends", () => {
|
||||
const { ends } = nthToGroupMembershipDateRange(0);
|
||||
expect(ends).toEqual(nthToReportingDateRange(0).ends);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { addHours } from "date-fns";
|
||||
import { addHours, subDays } from "date-fns";
|
||||
import { Config } from "~/config";
|
||||
import { IS_E2E_TEST_RUN } from "~/utils/e2e";
|
||||
|
||||
/** How long past a season's end its matches can still resolve: 24h stale match routine after a buzzer-beater creation, plus up to an hour of scheduling lag. */
|
||||
const REPORTING_GRACE_HOURS = 25;
|
||||
|
||||
/**
|
||||
* How far before a season's reporting range the `GroupMember` rows of its matches can have been created.
|
||||
* No membership in the database has ever preceded its season's start (90+ days of margin), so this is
|
||||
* insurance for a group formed just before a boundary rather than a bound on how long a group lives.
|
||||
*/
|
||||
const GROUP_LIFETIME_MAX_DAYS = 7;
|
||||
|
||||
/** Seasons (`nth` from 0) with their start and end dates. Outside production the list is a test set that keeps a season always open. */
|
||||
export const list =
|
||||
// when we do pnpm run setup NODE_ENV is not set -> use test seasons
|
||||
@@ -171,6 +178,19 @@ export function nthToReportingDateRange(nth: number) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When the members of the season's SendouQ matches joined their groups: the reporting range
|
||||
* widened by {@link GROUP_LIFETIME_MAX_DAYS}.
|
||||
*/
|
||||
export function nthToGroupMembershipDateRange(nth: number) {
|
||||
const { starts, ends } = nthToReportingDateRange(nth);
|
||||
|
||||
return {
|
||||
starts: subDays(starts, GROUP_LIFETIME_MAX_DAYS),
|
||||
ends,
|
||||
};
|
||||
}
|
||||
|
||||
/** Numbers of seasons started by `date` (default now), newest first; `[0]` if none have. */
|
||||
export function allStarted(date = new Date()) {
|
||||
const startedSeasons = list.filter((s) => date >= s.starts);
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
StageId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { groupMemberOfSeasonSql } from "~/utils/kysely.server";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
|
||||
export async function upsertOwn({
|
||||
@@ -302,6 +303,7 @@ export async function findAllWeaponUsageStats({
|
||||
.as("weaponUserGroupId"),
|
||||
])
|
||||
.where("GroupMember.userId", "=", userId)
|
||||
.where(groupMemberOfSeasonSql(season))
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("GroupMatch.alphaGroupId", "=", eb.ref("GroupMember.groupId")),
|
||||
|
||||
@@ -31,6 +31,7 @@ import { invariant } from "~/utils/invariant";
|
||||
import {
|
||||
commonUserSelect,
|
||||
concatUserSubmittedImagePrefix,
|
||||
groupMemberOfSeasonSql,
|
||||
jsonArrayFrom,
|
||||
jsonObjectFrom,
|
||||
matchProfileWeapons,
|
||||
@@ -700,6 +701,7 @@ export async function findSeasonCanceledMatchesByUserId({
|
||||
).as("cancelReports"),
|
||||
])
|
||||
.where("GroupMember.userId", "=", userId)
|
||||
.where(groupMemberOfSeasonSql(season))
|
||||
.where("GroupMatch.createdAt", ">=", dateToDatabaseTimestamp(starts))
|
||||
.where("GroupMatch.createdAt", "<=", dateToDatabaseTimestamp(ends))
|
||||
.orderBy("GroupMatch.createdAt", "desc")
|
||||
|
||||
@@ -159,6 +159,7 @@ export async function findPlacementsByUserId(
|
||||
return result.length ? result : null;
|
||||
}
|
||||
|
||||
/** Every month with placements, newest first. */
|
||||
export async function findAllMonthYears() {
|
||||
return await db
|
||||
.selectFrom("XRankPlacement")
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as TournamentFactory from "~/db/seed/factories/TournamentFactory";
|
||||
import * as TournamentTeamFactory from "~/db/seed/factories/TournamentTeamFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import type { TournamentSettings } from "~/db/tables-json";
|
||||
import { userChannel } from "~/features/events/events-types";
|
||||
import {
|
||||
abortSubscriptions,
|
||||
@@ -12,6 +13,7 @@ import { resolveMatchMapList } from "~/features/tournament-match/core/mapList.se
|
||||
import { reportScore } from "~/features/tournament-match/core/reportScore.server";
|
||||
import * as TournamentMatchRepository from "~/features/tournament-match/TournamentMatchRepository.server";
|
||||
import { invariant } from "~/utils/invariant";
|
||||
import * as BracketRepository from "./BracketRepository.server";
|
||||
import * as Engine from "./core/engine";
|
||||
import { executeBracketOperation } from "./core/executeBracketOperation.server";
|
||||
import {
|
||||
@@ -29,12 +31,24 @@ afterEach(() => {
|
||||
abortSubscriptions();
|
||||
});
|
||||
|
||||
const setupStartedMatch = async () => {
|
||||
/** KOs are only reported in round robin brackets. */
|
||||
const ROUND_ROBIN: TournamentSettings["bracketProgression"] = [
|
||||
{
|
||||
name: "Groups",
|
||||
type: "round_robin",
|
||||
requiresCheckIn: false,
|
||||
settings: { teamsPerGroup: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
const setupStartedMatch = async (
|
||||
overrides?: Partial<Parameters<typeof TournamentFactory.create>[0]>,
|
||||
) => {
|
||||
const authorId = users.id(1);
|
||||
const teamAlphaUserIds = [users.id(2), users.id(3), users.id(4), users.id(5)];
|
||||
const teamBravoUserIds = [users.id(6), users.id(7), users.id(8), users.id(9)];
|
||||
|
||||
const tournament = await TournamentFactory.create({ authorId });
|
||||
const tournament = await TournamentFactory.create({ authorId, ...overrides });
|
||||
for (const memberUserIds of [teamAlphaUserIds, teamBravoUserIds]) {
|
||||
await TournamentTeamFactory.create(
|
||||
{ tournamentId: tournament.id, memberUserIds },
|
||||
@@ -63,9 +77,15 @@ const setupStartedMatch = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
type GameResult = { winner: "one" | "two"; ko: boolean };
|
||||
|
||||
/** Reports every game of the match through `reportScore` until the set is over. */
|
||||
const playOutMatch = async (
|
||||
setup: Awaited<ReturnType<typeof setupStartedMatch>>,
|
||||
resultOfGame: (position: number) => GameResult = () => ({
|
||||
winner: "one",
|
||||
ko: false,
|
||||
}),
|
||||
) => {
|
||||
let position = 0;
|
||||
let setOver = false;
|
||||
@@ -77,6 +97,9 @@ const playOutMatch = async (
|
||||
);
|
||||
invariant(matchRow, "Match not found");
|
||||
invariant(matchRow.opponentOne?.id, "Match has no first opponent");
|
||||
invariant(matchRow.opponentTwo?.id, "Match has no second opponent");
|
||||
|
||||
const { winner, ko } = resultOfGame(position);
|
||||
|
||||
const reported = await reportScore({
|
||||
match: matchRow,
|
||||
@@ -84,8 +107,9 @@ const playOutMatch = async (
|
||||
mapList: await resolveMatchMapList({ match: matchRow, tournament }),
|
||||
user: { id: setup.authorId },
|
||||
position,
|
||||
winnerTeamId: matchRow.opponentOne.id,
|
||||
ko: false,
|
||||
winnerTeamId:
|
||||
winner === "one" ? matchRow.opponentOne.id : matchRow.opponentTwo.id,
|
||||
ko,
|
||||
});
|
||||
invariant(reported, `Game ${position} was already reported`);
|
||||
|
||||
@@ -134,6 +158,45 @@ describe("BracketRepository.applyMatchChanges", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("BracketRepository.findByTournamentId", () => {
|
||||
test("counts each opponent's KO wins into totalKos", async () => {
|
||||
const setup = await setupStartedMatch({ bracketProgression: ROUND_ROBIN });
|
||||
const kosOfGame: Array<GameResult> = [
|
||||
{ winner: "two", ko: true },
|
||||
{ winner: "one", ko: true },
|
||||
{ winner: "one", ko: true },
|
||||
];
|
||||
|
||||
await playOutMatch(setup, (position) => kosOfGame[position]);
|
||||
|
||||
const { match } = await BracketRepository.findByTournamentId(
|
||||
setup.tournamentId,
|
||||
);
|
||||
const playedMatch = match.find(
|
||||
(bracketMatch) => bracketMatch.id === setup.matchId,
|
||||
);
|
||||
|
||||
expect(playedMatch?.opponent1?.totalKos).toBe(2);
|
||||
expect(playedMatch?.opponent2?.totalKos).toBe(1);
|
||||
});
|
||||
|
||||
test("does not count games won without a KO", async () => {
|
||||
const setup = await setupStartedMatch({ bracketProgression: ROUND_ROBIN });
|
||||
|
||||
await playOutMatch(setup);
|
||||
|
||||
const { match } = await BracketRepository.findByTournamentId(
|
||||
setup.tournamentId,
|
||||
);
|
||||
const playedMatch = match.find(
|
||||
(bracketMatch) => bracketMatch.id === setup.matchId,
|
||||
);
|
||||
|
||||
expect(playedMatch?.opponent1?.totalKos).toBe(0);
|
||||
expect(playedMatch?.opponent2?.totalKos).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const roomById = (id: number) =>
|
||||
db
|
||||
.selectFrom("ChatRoom")
|
||||
|
||||
@@ -93,11 +93,6 @@ export async function findByTournamentId(
|
||||
"TournamentStage.id",
|
||||
"TournamentMatch.stageId",
|
||||
)
|
||||
.leftJoin(
|
||||
"TournamentMatchGameResult",
|
||||
"TournamentMatch.id",
|
||||
"TournamentMatchGameResult.matchId",
|
||||
)
|
||||
.select([
|
||||
"TournamentMatch.id",
|
||||
"TournamentMatch.stageId",
|
||||
@@ -111,7 +106,6 @@ export async function findByTournamentId(
|
||||
serializedOpponentWithKos("opponentTwo").as("opponent2"),
|
||||
])
|
||||
.where("TournamentStage.tournamentId", "=", tournamentId)
|
||||
.groupBy("TournamentMatch.id")
|
||||
.orderBy("TournamentMatch.stageId", "asc")
|
||||
.orderBy("TournamentMatch.id", "asc"),
|
||||
).as("match"),
|
||||
@@ -121,20 +115,23 @@ export async function findByTournamentId(
|
||||
return { stage, group, round, match };
|
||||
}
|
||||
|
||||
/** Opponent JSON with `totalKos` summed over the match's game results, `null` for BYEs. */
|
||||
/** Opponent JSON with `totalKos` counted over the match's game results, `null` for BYEs. */
|
||||
function serializedOpponentWithKos(
|
||||
column: "opponentOne" | "opponentTwo",
|
||||
): RawBuilder<ParticipantResult | null> {
|
||||
const opponent = kyselySql.ref(`TournamentMatch.${column}`);
|
||||
|
||||
// a correlated count is answered by the (matchId, winnerTeamId, ko) index alone; the
|
||||
// left join + group by it replaces read every game result row of the tournament
|
||||
return kyselySql<ParticipantResult | null>`json_set(
|
||||
${kyselySql.ref(`TournamentMatch.${column}`)},
|
||||
${opponent},
|
||||
'$.totalKos',
|
||||
sum(
|
||||
case
|
||||
when "TournamentMatchGameResult"."ko" = 1
|
||||
and "TournamentMatchGameResult"."winnerTeamId" = ${kyselySql.ref(`TournamentMatch.${column}`)} ->> '$.id'
|
||||
then 1
|
||||
else 0
|
||||
end
|
||||
(
|
||||
select count(*)
|
||||
from "TournamentMatchGameResult"
|
||||
where "TournamentMatchGameResult"."matchId" = "TournamentMatch"."id"
|
||||
and "TournamentMatchGameResult"."winnerTeamId" = ${opponent} ->> '$.id'
|
||||
and "TournamentMatchGameResult"."ko" = 1
|
||||
)
|
||||
)`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as R from "remeda";
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import * as CalendarEventFactory from "~/db/seed/factories/CalendarEventFactory";
|
||||
import * as CalendarEventResultFactory from "~/db/seed/factories/CalendarEventResultFactory";
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from "~/utils/dates";
|
||||
import * as TournamentOrganizationRepository from "./TournamentOrganizationRepository.server";
|
||||
import { seedOrgEventWithParticipants } from "./test-utils";
|
||||
import { TOURNAMENT_SERIES_EVENTS_PER_PAGE } from "./tournament-organization-constants";
|
||||
|
||||
const users = UserFactory.pool();
|
||||
|
||||
@@ -110,6 +112,76 @@ describe("findEventsByMonth", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("findPaginatedEventsBySeries", () => {
|
||||
const NEWEST_EVENT_STARTED_AT = 1_700_000_000;
|
||||
const DAY_IN_SECONDS = 60 * 60 * 24;
|
||||
const EVENT_COUNT = TOURNAMENT_SERIES_EVENTS_PER_PAGE + 1;
|
||||
|
||||
beforeEach(async () => {
|
||||
await users.create(1);
|
||||
});
|
||||
|
||||
/** One event per day going back from the newest, so `Low Ink #0` is the newest of the series. */
|
||||
const seedSeries = async (organizationId: number) => {
|
||||
for (const nth of R.range(0, EVENT_COUNT)) {
|
||||
await CalendarEventFactory.create({
|
||||
authorId: users.id(1),
|
||||
organizationId,
|
||||
name: `Low Ink #${nth}`,
|
||||
startTimes: [NEWEST_EVENT_STARTED_AT - nth * DAY_IN_SECONDS],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const eventsOnPage = (organizationId: number, page: number) =>
|
||||
TournamentOrganizationRepository.findPaginatedEventsBySeries({
|
||||
organizationId,
|
||||
substringMatches: ["Low Ink"],
|
||||
page,
|
||||
});
|
||||
|
||||
test("pages the events of the series newest first", async () => {
|
||||
const org = await TournamentOrganizationFactory.create({
|
||||
ownerId: users.id(1),
|
||||
});
|
||||
await seedSeries(org.id);
|
||||
|
||||
const firstPage = await eventsOnPage(org.id, 1);
|
||||
const secondPage = await eventsOnPage(org.id, 2);
|
||||
|
||||
expect(firstPage.map((event) => event.name)).toEqual(
|
||||
R.range(0, TOURNAMENT_SERIES_EVENTS_PER_PAGE).map(
|
||||
(nth) => `Low Ink #${nth}`,
|
||||
),
|
||||
);
|
||||
expect(secondPage.map((event) => event.name)).toEqual([
|
||||
`Low Ink #${EVENT_COUNT - 1}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("leaves out the events of other series", async () => {
|
||||
const org = await TournamentOrganizationFactory.create({
|
||||
ownerId: users.id(1),
|
||||
});
|
||||
await CalendarEventFactory.create({
|
||||
authorId: users.id(1),
|
||||
organizationId: org.id,
|
||||
name: "Low Ink #1",
|
||||
startTimes: [NEWEST_EVENT_STARTED_AT],
|
||||
});
|
||||
await CalendarEventFactory.create({
|
||||
authorId: users.id(1),
|
||||
organizationId: org.id,
|
||||
name: "Paddling Pool #1",
|
||||
startTimes: [NEWEST_EVENT_STARTED_AT],
|
||||
});
|
||||
|
||||
const events = await eventsOnPage(org.id, 1);
|
||||
|
||||
expect(events.map((event) => event.name)).toEqual(["Low Ink #1"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("countActiveParticipants", () => {
|
||||
const WINDOW_START = 1_700_000_000;
|
||||
const WINDOW_END = WINDOW_START + 60 * 60 * 24 * 31;
|
||||
|
||||
@@ -437,22 +437,14 @@ export function findAllUnfinalizedEvents(organizationId: number) {
|
||||
.execute();
|
||||
}
|
||||
|
||||
const findSeriesEventsBaseQuery = ({
|
||||
organizationId,
|
||||
substringMatches,
|
||||
}: {
|
||||
organizationId: number;
|
||||
substringMatches: string[];
|
||||
}) =>
|
||||
findEventsBaseQuery(organizationId)
|
||||
.where((eb) =>
|
||||
eb.or(
|
||||
substringMatches.map((match) =>
|
||||
eb("CalendarEvent.name", "like", `%${match}%`),
|
||||
),
|
||||
const nameMatchesSeries =
|
||||
(substringMatches: string[]) =>
|
||||
(eb: ExpressionBuilder<DB, "CalendarEvent">) =>
|
||||
eb.or(
|
||||
substringMatches.map((match) =>
|
||||
eb("CalendarEvent.name", "like", `%${match}%`),
|
||||
),
|
||||
)
|
||||
.orderBy("CalendarEventDate.startsAt", "desc");
|
||||
);
|
||||
|
||||
export async function findPaginatedEventsBySeries({
|
||||
organizationId,
|
||||
@@ -463,12 +455,27 @@ export async function findPaginatedEventsBySeries({
|
||||
substringMatches: string[];
|
||||
page: number;
|
||||
}) {
|
||||
const events = await findSeriesEventsBaseQuery({
|
||||
organizationId,
|
||||
substringMatches,
|
||||
})
|
||||
// the page is resolved by id first: with the limit on the full read, the winners
|
||||
// of every event of the series would be aggregated before it applies
|
||||
const pageEventIds = db
|
||||
.selectFrom("CalendarEvent")
|
||||
.innerJoin(
|
||||
"CalendarEventDate",
|
||||
"CalendarEventDate.eventId",
|
||||
"CalendarEvent.id",
|
||||
)
|
||||
.select("CalendarEvent.id")
|
||||
.where("CalendarEvent.organizationId", "=", organizationId)
|
||||
.where("CalendarEvent.hidden", "=", 0)
|
||||
.where(nameMatchesSeries(substringMatches))
|
||||
.groupBy("CalendarEvent.id")
|
||||
.orderBy(({ fn }) => fn.min("CalendarEventDate.startsAt"), "desc")
|
||||
.limit(TOURNAMENT_SERIES_EVENTS_PER_PAGE)
|
||||
.offset((page - 1) * TOURNAMENT_SERIES_EVENTS_PER_PAGE)
|
||||
.offset((page - 1) * TOURNAMENT_SERIES_EVENTS_PER_PAGE);
|
||||
|
||||
const events = await findEventsBaseQuery(organizationId)
|
||||
.where("CalendarEvent.id", "in", pageEventIds)
|
||||
.orderBy("CalendarEventDate.startsAt", "desc")
|
||||
.execute();
|
||||
|
||||
return events.map(mapEvent);
|
||||
@@ -500,13 +507,7 @@ export async function findAllEventsBySeries({
|
||||
])
|
||||
.where("CalendarEvent.organizationId", "=", organizationId)
|
||||
.where("CalendarEvent.hidden", "=", 0)
|
||||
.where((eb) =>
|
||||
eb.or(
|
||||
substringMatches.map((match) =>
|
||||
eb("CalendarEvent.name", "like", `%${match}%`),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where(nameMatchesSeries(substringMatches))
|
||||
.groupBy("CalendarEvent.id")
|
||||
.orderBy("CalendarEventDate.startsAt", "desc")
|
||||
.execute();
|
||||
|
||||
@@ -203,13 +203,7 @@ export async function findById(id: number) {
|
||||
"=",
|
||||
"TournamentTeamMember.userId",
|
||||
)
|
||||
.on(
|
||||
"SeedingSkill.type",
|
||||
"=",
|
||||
sql<
|
||||
Tables["SeedingSkill"]["type"]
|
||||
> /*sql*/`case when json_extract("Tournament"."settings", '$.isRanked') = 1 then 'RANKED' else 'UNRANKED' end`,
|
||||
),
|
||||
.on("SeedingSkill.type", "=", seedingSkillType(id)),
|
||||
)
|
||||
.select(({ fn }) =>
|
||||
fn.avg<number>("SeedingSkill.ordinal").as("v"),
|
||||
@@ -500,7 +494,6 @@ export type TeamFull = Unwrapped<typeof findTeamsFullByTournamentId>;
|
||||
export async function findTeamsFullByTournamentId(tournamentId: number) {
|
||||
const teams = await db
|
||||
.selectFrom("TournamentTeam")
|
||||
.innerJoin("Tournament", "Tournament.id", "TournamentTeam.tournamentId")
|
||||
.leftJoin(
|
||||
"UserSubmittedImage as PickupAvatar",
|
||||
"TournamentTeam.avatarImgId",
|
||||
@@ -528,13 +521,7 @@ export async function findTeamsFullByTournamentId(tournamentId: number) {
|
||||
.leftJoin("SeedingSkill", (join) =>
|
||||
join
|
||||
.onRef("User.id", "=", "SeedingSkill.userId")
|
||||
.on(
|
||||
"SeedingSkill.type",
|
||||
"=",
|
||||
sql<
|
||||
Tables["SeedingSkill"]["type"]
|
||||
> /*sql*/`case when json_extract("Tournament"."settings", '$.isRanked') = 1 then 'RANKED' else 'UNRANKED' end`,
|
||||
),
|
||||
.on("SeedingSkill.type", "=", seedingSkillType(tournamentId)),
|
||||
)
|
||||
.select((memberEb) => [
|
||||
...commonUserSelect(memberEb, {
|
||||
@@ -1885,3 +1872,10 @@ async function trophyTier(
|
||||
|
||||
return tournament?.tier ?? null;
|
||||
}
|
||||
|
||||
/** Which seeding skill the tournament ranks by, resolved once: inline in the join it parsed the settings JSON per member row. */
|
||||
function seedingSkillType(tournamentId: number) {
|
||||
return sql<
|
||||
Tables["SeedingSkill"]["type"]
|
||||
>`(select case when json_extract("settings", '$.isRanked') = 1 then 'RANKED' else 'UNRANKED' end from "Tournament" where "id" = ${tournamentId})`;
|
||||
}
|
||||
|
||||
@@ -359,6 +359,71 @@ describe("TournamentTeamRepository", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAllRegistrationsByUserIds", () => {
|
||||
const WINDOW_STARTS_AT = 1_700_000_000;
|
||||
const DAY_IN_SECONDS = 60 * 60 * 24;
|
||||
const WINDOW_ENDS_AT = WINDOW_STARTS_AT + 7 * DAY_IN_SECONDS;
|
||||
|
||||
const registerAt = async (startTime: number) => {
|
||||
const tournament = await TournamentFactory.create({
|
||||
authorId: organizerId(),
|
||||
startTimes: [startTime],
|
||||
});
|
||||
await TournamentTeamFactory.create({
|
||||
tournamentId: tournament.id,
|
||||
memberUserIds: [ownerId(), memberId()],
|
||||
});
|
||||
|
||||
return tournament.id;
|
||||
};
|
||||
|
||||
const registrationsInWindow = (excludeTournamentId?: number) =>
|
||||
TournamentTeamRepository.findAllRegistrationsByUserIds({
|
||||
userIds: [memberId()],
|
||||
startsAt: WINDOW_STARTS_AT,
|
||||
endsAt: WINDOW_ENDS_AT,
|
||||
excludeTournamentId,
|
||||
});
|
||||
|
||||
test("leaves out the registrations starting outside the window", async () => {
|
||||
await registerAt(WINDOW_STARTS_AT + DAY_IN_SECONDS);
|
||||
await registerAt(WINDOW_STARTS_AT - DAY_IN_SECONDS);
|
||||
await registerAt(WINDOW_ENDS_AT + DAY_IN_SECONDS);
|
||||
|
||||
const registrations = await registrationsInWindow();
|
||||
|
||||
expect(registrations).toHaveLength(1);
|
||||
expect(registrations[0].userId).toBe(memberId());
|
||||
expect(registrations[0].startsAt).toBe(WINDOW_STARTS_AT + DAY_IN_SECONDS);
|
||||
});
|
||||
|
||||
test("leaves out the excluded tournament", async () => {
|
||||
const excludedTournamentId = await registerAt(
|
||||
WINDOW_STARTS_AT + DAY_IN_SECONDS,
|
||||
);
|
||||
await registerAt(WINDOW_STARTS_AT + 2 * DAY_IN_SECONDS);
|
||||
|
||||
const registrations = await registrationsInWindow(excludedTournamentId);
|
||||
|
||||
expect(registrations).toHaveLength(1);
|
||||
expect(registrations[0].startsAt).toBe(
|
||||
WINDOW_STARTS_AT + 2 * DAY_IN_SECONDS,
|
||||
);
|
||||
});
|
||||
|
||||
test("returns nothing when no user ids are given", async () => {
|
||||
await registerAt(WINDOW_STARTS_AT + DAY_IN_SECONDS);
|
||||
|
||||
expect(
|
||||
await TournamentTeamRepository.findAllRegistrationsByUserIds({
|
||||
userIds: [],
|
||||
startsAt: WINDOW_STARTS_AT,
|
||||
endsAt: WINDOW_ENDS_AT,
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findRecentlyPlayedMapsByIds", () => {
|
||||
test("leaves out the games of the match the maps are resolved for", async () => {
|
||||
// an in-progress set's map list is regenerated when its cache entry is lost, so counting its
|
||||
|
||||
@@ -982,43 +982,48 @@ export function findAllRegistrationsByUserIds({
|
||||
}) {
|
||||
if (userIds.length === 0) return Promise.resolve([]);
|
||||
|
||||
return db
|
||||
.selectFrom("TournamentTeamMember")
|
||||
.innerJoin(
|
||||
"TournamentTeam",
|
||||
"TournamentTeam.id",
|
||||
"TournamentTeamMember.tournamentTeamId",
|
||||
)
|
||||
.innerJoin("Tournament", "Tournament.id", "TournamentTeam.tournamentId")
|
||||
.innerJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id")
|
||||
.innerJoin(
|
||||
"CalendarEventDate",
|
||||
"CalendarEventDate.eventId",
|
||||
"CalendarEvent.id",
|
||||
)
|
||||
.select((eb) => [
|
||||
"TournamentTeamMember.userId",
|
||||
"CalendarEvent.name",
|
||||
"CalendarEvent.organizationId",
|
||||
"CalendarEventDate.startsAt",
|
||||
"Tournament.settings",
|
||||
eb
|
||||
.selectFrom("TournamentTeam as RegisteredTeam")
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.whereRef("RegisteredTeam.tournamentId", "=", "Tournament.id")
|
||||
.where("RegisteredTeam.isPlaceholder", "=", 0)
|
||||
.as("teamCount"),
|
||||
])
|
||||
.$narrowType<{ teamCount: NotNull }>()
|
||||
.where("TournamentTeamMember.userId", "in", userIds)
|
||||
.where("TournamentTeam.droppedOut", "=", 0)
|
||||
.where("CalendarEvent.hidden", "=", 0)
|
||||
.where("CalendarEventDate.startsAt", ">=", startsAt)
|
||||
.where("CalendarEventDate.startsAt", "<=", endsAt)
|
||||
.$if(typeof excludeTournamentId === "number", (qb) =>
|
||||
qb.where("Tournament.id", "!=", excludeTournamentId!),
|
||||
)
|
||||
.execute();
|
||||
return (
|
||||
db
|
||||
.selectFrom("CalendarEventDate")
|
||||
// cross join pins the join order: the date window is indexed and far narrower
|
||||
// than the users' registration histories the planner walks otherwise
|
||||
.crossJoin("CalendarEvent")
|
||||
.innerJoin("Tournament", "Tournament.id", "CalendarEvent.tournamentId")
|
||||
.innerJoin(
|
||||
"TournamentTeam",
|
||||
"TournamentTeam.tournamentId",
|
||||
"Tournament.id",
|
||||
)
|
||||
.innerJoin(
|
||||
"TournamentTeamMember",
|
||||
"TournamentTeamMember.tournamentTeamId",
|
||||
"TournamentTeam.id",
|
||||
)
|
||||
.select((eb) => [
|
||||
"TournamentTeamMember.userId",
|
||||
"CalendarEvent.name",
|
||||
"CalendarEvent.organizationId",
|
||||
"CalendarEventDate.startsAt",
|
||||
"Tournament.settings",
|
||||
eb
|
||||
.selectFrom("TournamentTeam as RegisteredTeam")
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.whereRef("RegisteredTeam.tournamentId", "=", "Tournament.id")
|
||||
.where("RegisteredTeam.isPlaceholder", "=", 0)
|
||||
.as("teamCount"),
|
||||
])
|
||||
.$narrowType<{ teamCount: NotNull }>()
|
||||
.whereRef("CalendarEvent.id", "=", "CalendarEventDate.eventId")
|
||||
.where("TournamentTeamMember.userId", "in", userIds)
|
||||
.where("TournamentTeam.droppedOut", "=", 0)
|
||||
.where("CalendarEvent.hidden", "=", 0)
|
||||
.where("CalendarEventDate.startsAt", ">=", startsAt)
|
||||
.where("CalendarEventDate.startsAt", "<=", endsAt)
|
||||
.$if(typeof excludeTournamentId === "number", (qb) =>
|
||||
qb.where("Tournament.id", "!=", excludeTournamentId!),
|
||||
)
|
||||
.execute()
|
||||
);
|
||||
}
|
||||
|
||||
/** Invite code of one team, the secret the tournament layout data does not carry. */
|
||||
|
||||
@@ -126,6 +126,31 @@ describe("findVods", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("countVods", () => {
|
||||
beforeEach(async () => {
|
||||
await users.create(5);
|
||||
await seedVodsOfEveryFilter();
|
||||
});
|
||||
|
||||
test.each([
|
||||
["by weapon", () => ({ weapon: 1000 as const }), 2],
|
||||
["by mode", () => ({ mode: "SZ" as const }), 2],
|
||||
["by stageId", () => ({ stageId: 1 as const }), 2],
|
||||
["by type", () => ({ type: "CAST" as const }), 1],
|
||||
["by user", () => ({ userId: users.id(1) }), 1],
|
||||
["without filters", () => ({}), 3],
|
||||
])(
|
||||
"agrees with the rows findVods returns (%s)",
|
||||
async (_why, filters, expected) => {
|
||||
const rows = await VodRepository.findVods({ ...filters(), limit: 100 });
|
||||
const count = await VodRepository.countVods(filters());
|
||||
|
||||
expect(rows).toHaveLength(expected);
|
||||
expect(count).toBe(rows.length);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("findVodById", () => {
|
||||
beforeEach(async () => {
|
||||
await users.create(5);
|
||||
@@ -418,3 +443,27 @@ describe("deleteById", () => {
|
||||
expect(secondResult).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/** One vod per `countVods` filter case, each matching some of the filters but never all of them. */
|
||||
async function seedVodsOfEveryFilter() {
|
||||
await VodFactory.create({
|
||||
submitterUserId: users.id(1),
|
||||
type: "TOURNAMENT",
|
||||
pov: { type: "USER", userId: users.id(1) },
|
||||
matches: [{ mode: "SZ", stageId: 1, startsAt: "0:00", weapons: [1000] }],
|
||||
});
|
||||
await VodFactory.create({
|
||||
submitterUserId: users.id(1),
|
||||
type: "CAST",
|
||||
matches: [
|
||||
{ mode: "SZ", stageId: 2, startsAt: "0:00", weapons: [2000] },
|
||||
{ mode: "TC", stageId: 1, startsAt: "5:00", weapons: [1000] },
|
||||
],
|
||||
});
|
||||
await VodFactory.create({
|
||||
submitterUserId: users.id(2),
|
||||
type: "SCRIM",
|
||||
pov: { type: "USER", userId: users.id(2) },
|
||||
matches: [{ mode: "TC", stageId: 3, startsAt: "0:00", weapons: [0] }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { sql } from "kysely";
|
||||
import {
|
||||
type Expression,
|
||||
type ExpressionBuilder,
|
||||
type SelectQueryBuilder,
|
||||
type SqlBool,
|
||||
sql,
|
||||
} from "kysely";
|
||||
import * as R from "remeda";
|
||||
import { db } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
@@ -30,31 +36,24 @@ export async function findByUserId(userId: Tables["User"]["id"], limit = 100) {
|
||||
return findVods({ userId, limit });
|
||||
}
|
||||
|
||||
export async function findVods({
|
||||
weapon,
|
||||
mode,
|
||||
stageId,
|
||||
type,
|
||||
userId,
|
||||
limit = VODS_PAGE_BATCH_SIZE,
|
||||
offset = 0,
|
||||
}: {
|
||||
type VodFilters = {
|
||||
weapon?: MainWeaponId;
|
||||
mode?: ModeShort;
|
||||
stageId?: StageId;
|
||||
type?: Tables["Video"]["type"];
|
||||
userId?: number;
|
||||
};
|
||||
|
||||
/** Page of the vods matching the filters, newest first, with the weapons and players of their matching match rows. */
|
||||
export async function findVods({
|
||||
limit = VODS_PAGE_BATCH_SIZE,
|
||||
offset = 0,
|
||||
...filters
|
||||
}: VodFilters & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
let query = db
|
||||
.selectFrom("Video")
|
||||
.leftJoin("VideoMatch", "VideoMatch.videoId", "Video.id")
|
||||
.leftJoin(
|
||||
"VideoMatchPlayer",
|
||||
"VideoMatch.id",
|
||||
"VideoMatchPlayer.videoMatchId",
|
||||
)
|
||||
const result = await vodsWithMatches()
|
||||
.selectAll("Video")
|
||||
.select(({ fn, ref, eb }) => [
|
||||
sql<
|
||||
@@ -72,32 +71,20 @@ export async function findVods({
|
||||
.select((playerEb) => commonUserSelect(playerEb))
|
||||
.whereRef("User.id", "=", "VideoMatchPlayer.playerUserId"),
|
||||
).as("players"),
|
||||
]);
|
||||
if (userId) {
|
||||
query = query.where("VideoMatchPlayer.playerUserId", "=", userId);
|
||||
} else {
|
||||
if (type) {
|
||||
query = query.where("Video.type", "=", type);
|
||||
}
|
||||
if (mode) {
|
||||
query = query.where("VideoMatch.mode", "=", mode);
|
||||
}
|
||||
if (stageId) {
|
||||
query = query.where("VideoMatch.stageId", "=", stageId);
|
||||
}
|
||||
}
|
||||
if (weapon) {
|
||||
query = query.where(
|
||||
"VideoMatchPlayer.weaponSplId",
|
||||
])
|
||||
.where(vodFilters(filters))
|
||||
// the page is resolved by id first: with the limit on this read, the aggregates
|
||||
// of every matching vod would be computed before it applies
|
||||
.where(
|
||||
"Video.id",
|
||||
"in",
|
||||
weaponIdToArrayWithAlts(weapon),
|
||||
);
|
||||
}
|
||||
const result = await query
|
||||
filteredVideoIds(filters)
|
||||
.orderBy("Video.youtubePublishedAt", "desc")
|
||||
.limit(limit)
|
||||
.offset(offset),
|
||||
)
|
||||
.groupBy("Video.id")
|
||||
.orderBy("Video.youtubePublishedAt", "desc")
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.execute();
|
||||
|
||||
const vods = result.map((value) => {
|
||||
@@ -110,50 +97,13 @@ export async function findVods({
|
||||
return vods;
|
||||
}
|
||||
|
||||
export async function countVods({
|
||||
weapon,
|
||||
mode,
|
||||
stageId,
|
||||
type,
|
||||
userId,
|
||||
}: {
|
||||
weapon?: MainWeaponId;
|
||||
mode?: ModeShort;
|
||||
stageId?: StageId;
|
||||
type?: Tables["Video"]["type"];
|
||||
userId?: number;
|
||||
}) {
|
||||
let query = db
|
||||
.selectFrom("Video")
|
||||
.leftJoin("VideoMatch", "VideoMatch.videoId", "Video.id")
|
||||
.leftJoin(
|
||||
"VideoMatchPlayer",
|
||||
"VideoMatch.id",
|
||||
"VideoMatchPlayer.videoMatchId",
|
||||
)
|
||||
.select(({ fn }) => fn.count<number>("Video.id").distinct().as("count"));
|
||||
if (userId) {
|
||||
query = query.where("VideoMatchPlayer.playerUserId", "=", userId);
|
||||
} else {
|
||||
if (type) {
|
||||
query = query.where("Video.type", "=", type);
|
||||
}
|
||||
if (mode) {
|
||||
query = query.where("VideoMatch.mode", "=", mode);
|
||||
}
|
||||
if (stageId) {
|
||||
query = query.where("VideoMatch.stageId", "=", stageId);
|
||||
}
|
||||
}
|
||||
if (weapon) {
|
||||
query = query.where(
|
||||
"VideoMatchPlayer.weaponSplId",
|
||||
"in",
|
||||
weaponIdToArrayWithAlts(weapon),
|
||||
);
|
||||
}
|
||||
/** How many vods match the filters. */
|
||||
export async function countVods(filters: VodFilters) {
|
||||
const result = await db
|
||||
.selectFrom(filteredVideoIds(filters).as("filtered"))
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const result = await query.executeTakeFirstOrThrow();
|
||||
return result.count;
|
||||
}
|
||||
|
||||
@@ -346,3 +296,91 @@ async function save(
|
||||
export function deleteById(id: number) {
|
||||
return db.deleteFrom("UnvalidatedVideo").where("id", "=", id).execute();
|
||||
}
|
||||
|
||||
const vodsWithMatches = () =>
|
||||
db
|
||||
.selectFrom("Video")
|
||||
.leftJoin("VideoMatch", "VideoMatch.videoId", "Video.id")
|
||||
.leftJoin(
|
||||
"VideoMatchPlayer",
|
||||
"VideoMatch.id",
|
||||
"VideoMatchPlayer.videoMatchId",
|
||||
);
|
||||
|
||||
type VodsWithMatchesDB =
|
||||
ReturnType<typeof vodsWithMatches> extends SelectQueryBuilder<
|
||||
infer JoinedDB,
|
||||
any,
|
||||
any
|
||||
>
|
||||
? JoinedDB
|
||||
: never;
|
||||
|
||||
type VodsTables = "Video" | "VideoMatch" | "VideoMatchPlayer";
|
||||
|
||||
/** Conditions the filters put on the match rows. `userId` makes the vod's own filters moot: it is the user's vods regardless. */
|
||||
function vodFilters({ weapon, mode, stageId, type, userId }: VodFilters) {
|
||||
return (eb: ExpressionBuilder<VodsWithMatchesDB, VodsTables>) => {
|
||||
const conditions: Expression<SqlBool>[] = [];
|
||||
if (userId) {
|
||||
conditions.push(eb("VideoMatchPlayer.playerUserId", "=", userId));
|
||||
} else {
|
||||
if (type) {
|
||||
conditions.push(eb("Video.type", "=", type));
|
||||
}
|
||||
if (mode) {
|
||||
conditions.push(eb("VideoMatch.mode", "=", mode));
|
||||
}
|
||||
if (stageId) {
|
||||
conditions.push(eb("VideoMatch.stageId", "=", stageId));
|
||||
}
|
||||
}
|
||||
if (weapon) {
|
||||
conditions.push(
|
||||
eb(
|
||||
"VideoMatchPlayer.weaponSplId",
|
||||
"in",
|
||||
weaponIdToArrayWithAlts(weapon),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return eb.and(conditions);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids of the vods matching the filters, joined only as far as a filter reads: inner joins
|
||||
* let a player level filter start from the player rows' index instead of walking every vod.
|
||||
*/
|
||||
function filteredVideoIds(filters: VodFilters) {
|
||||
const { type, userId, mode, stageId, weapon } = filters;
|
||||
const filtersPlayers = Boolean(userId || weapon);
|
||||
const filtersMatches = !filtersPlayers && Boolean(mode || stageId);
|
||||
|
||||
return db
|
||||
.selectFrom("Video")
|
||||
.select("Video.id")
|
||||
.distinct()
|
||||
.$if(Boolean(type) && !userId, (qb) => qb.where("Video.type", "=", type!))
|
||||
.$if(filtersPlayers, (qb) =>
|
||||
qb
|
||||
.innerJoin("VideoMatch", "VideoMatch.videoId", "Video.id")
|
||||
.innerJoin(
|
||||
"VideoMatchPlayer",
|
||||
"VideoMatch.id",
|
||||
"VideoMatchPlayer.videoMatchId",
|
||||
)
|
||||
.where(vodFilters(filters)),
|
||||
)
|
||||
.$if(filtersMatches, (qb) =>
|
||||
qb
|
||||
.innerJoin("VideoMatch", "VideoMatch.videoId", "Video.id")
|
||||
.leftJoin(
|
||||
"VideoMatchPlayer",
|
||||
"VideoMatch.id",
|
||||
"VideoMatchPlayer.videoMatchId",
|
||||
)
|
||||
.where(vodFilters(filters)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type Expression,
|
||||
type ExpressionBuilder,
|
||||
type RawBuilder,
|
||||
type SqlBool,
|
||||
sql,
|
||||
} from "kysely";
|
||||
import type {
|
||||
@@ -19,6 +20,8 @@ import {
|
||||
} from "~/db/json-selections";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables } from "~/db/tables";
|
||||
import * as Seasons from "~/features/mmr/core/Seasons";
|
||||
import { dateToDatabaseTimestamp } from "./dates";
|
||||
import { IS_E2E_TEST_RUN } from "./e2e";
|
||||
import { safeNumberParse } from "./number";
|
||||
|
||||
@@ -51,6 +54,16 @@ export function peakXpOverallSql<T extends number | null = number | null>() {
|
||||
return sql<T>`"SplatoonPlayer"."peakXp" ->> '$.overall'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter keeping only the `GroupMember` rows that can belong to season `nth`'s matches.
|
||||
* `"GroupMember"` must be in scope at the call site.
|
||||
*/
|
||||
export function groupMemberOfSeasonSql(nth: number) {
|
||||
const { starts, ends } = Seasons.nthToGroupMembershipDateRange(nth);
|
||||
|
||||
return sql<SqlBool>`"GroupMember"."createdAt" between ${dateToDatabaseTimestamp(starts)} and ${dateToDatabaseTimestamp(ends)}`;
|
||||
}
|
||||
|
||||
type CommonUserSelectOptions = {
|
||||
alias?: string;
|
||||
prefix?: string;
|
||||
|
||||
4
changelog/2026-09-08-faster-db-queries.md
Normal file
4
changelog/2026-09-08-faster-db-queries.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
type: feature
|
||||
---
|
||||
Faster page loads across the site (db optimization)
|
||||
45
migrations/20260908152750-query-indexes.ts
Normal file
45
migrations/20260908152750-query-indexes.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Kysely } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
// date window reads (calendar, running tournaments, availability) scanned every event
|
||||
await trx.schema
|
||||
.createIndex("calendar_event_date_starts_at")
|
||||
.on("CalendarEventDate")
|
||||
.column("startsAt")
|
||||
.execute();
|
||||
|
||||
// the month list skips through this instead of reading every placement
|
||||
await trx.schema
|
||||
.createIndex("xrank_placement_year_month")
|
||||
.on("XRankPlacement")
|
||||
.columns(["year", "month"])
|
||||
.execute();
|
||||
|
||||
// covers the per match KO count of the bracket read, replacing the matchId only index
|
||||
await trx.schema
|
||||
.dropIndex("tournament_match_game_result_match_id")
|
||||
.execute();
|
||||
await trx.schema
|
||||
.createIndex("tournament_match_game_result_match_id_winner_team_id_ko")
|
||||
.on("TournamentMatchGameResult")
|
||||
.columns(["matchId", "winnerTeamId", "ko"])
|
||||
.execute();
|
||||
|
||||
// the vods weapon filter walked every player row
|
||||
await trx.schema
|
||||
.createIndex("video_match_player_weapon_spl_id")
|
||||
.on("VideoMatchPlayer")
|
||||
.column("weaponSplId")
|
||||
.execute();
|
||||
|
||||
// almost no event has a trophy, so to the planner the full index looked as bad as scanning every event
|
||||
await trx.schema.dropIndex("calendar_event_trophy_id").execute();
|
||||
await trx.schema
|
||||
.createIndex("calendar_event_trophy_id")
|
||||
.on("CalendarEvent")
|
||||
.column("trophyId")
|
||||
.where("trophyId", "is not", null)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user