From 0afbb29e21fdae1067ecf776787992a4b8cb9abd Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:07:53 +0300 Subject: [PATCH] Optimize queries, fix isLooking tournament teams stale bug --- .../friends/FriendRepository.server.ts | 52 +++-- .../scrims/ScrimPostRepository.server.ts | 18 +- .../BracketRepository.server.ts | 7 + .../user-page/UserRepository.server.ts | 211 +++++++++++------- .../2026-09-08-sidebar-friend-lfg-activity.md | 4 + .../20260908124343-reset-stale-is-looking.ts | 12 + scripts/benchmark-db/cases.ts | 6 + 7 files changed, 198 insertions(+), 112 deletions(-) create mode 100644 changelog/2026-09-08-sidebar-friend-lfg-activity.md create mode 100644 migrations/20260908124343-reset-stale-is-looking.ts diff --git a/app/features/friends/FriendRepository.server.ts b/app/features/friends/FriendRepository.server.ts index 792258ecd..cde2ef1ae 100644 --- a/app/features/friends/FriendRepository.server.ts +++ b/app/features/friends/FriendRepository.server.ts @@ -70,31 +70,45 @@ function withLfgJoins>(qb: QB) { .leftJoin("TournamentTeamMember", (join) => join .onRef("TournamentTeamMember.userId", "=", "User.id") - .on("TournamentTeamMember.isLooking", "=", 1), + .on("TournamentTeamMember.isLooking", "=", 1) + .on((eb) => + eb.exists( + eb + .selectFrom("TournamentTeam as LookingTeam") + .innerJoin( + "Tournament as LookingTournament", + "LookingTournament.id", + "LookingTeam.tournamentId", + ) + .select("LookingTeam.id") + .whereRef( + "LookingTeam.id", + "=", + "TournamentTeamMember.tournamentTeamId", + ) + .where((innerEb) => + innerEb.or([ + innerEb( + sql`json_extract("LookingTournament"."settings", '$.regClosesAt')`, + "is", + null, + ), + innerEb( + sql`json_extract("LookingTournament"."settings", '$.regClosesAt')`, + ">", + nowTimestamp, + ), + ]), + ), + ), + ), ) .leftJoin( "TournamentTeam", "TournamentTeam.id", "TournamentTeamMember.tournamentTeamId", ) - .leftJoin("Tournament", (join) => - join - .onRef("Tournament.id", "=", "TournamentTeam.tournamentId") - .on((eb) => - eb.or([ - eb( - sql`json_extract("Tournament"."settings", '$.regClosesAt')`, - "is", - null, - ), - eb( - sql`json_extract("Tournament"."settings", '$.regClosesAt')`, - ">", - nowTimestamp, - ), - ]), - ), - ) + .leftJoin("Tournament", "Tournament.id", "TournamentTeam.tournamentId") .leftJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id") .leftJoin( "CalendarEventDate", diff --git a/app/features/scrims/ScrimPostRepository.server.ts b/app/features/scrims/ScrimPostRepository.server.ts index a7a3d3418..353d7d74a 100644 --- a/app/features/scrims/ScrimPostRepository.server.ts +++ b/app/features/scrims/ScrimPostRepository.server.ts @@ -715,16 +715,12 @@ export async function findUserScrims(userId: number): Promise { const rows = await baseFindQuery .where("ScrimPost.canceledAt", "is", null) .where(bookedStartsAt, ">=", now) - .where((eb) => - eb.or([ - eb.exists( - eb - .selectFrom("ScrimPostUser") - .select("ScrimPostUser.scrimPostId") - .whereRef("ScrimPostUser.scrimPostId", "=", "ScrimPost.id") - .where("ScrimPostUser.userId", "=", userId), - ), - eb.exists( + .where("ScrimPost.id", "in", (eb) => + eb + .selectFrom("ScrimPostUser") + .select("ScrimPostUser.scrimPostId") + .where("ScrimPostUser.userId", "=", userId) + .union( eb .selectFrom("ScrimPostRequest") .innerJoin( @@ -733,10 +729,8 @@ export async function findUserScrims(userId: number): Promise { "ScrimPostRequest.id", ) .select("ScrimPostRequest.scrimPostId") - .whereRef("ScrimPostRequest.scrimPostId", "=", "ScrimPost.id") .where("ScrimPostRequestUser.userId", "=", userId), ), - ]), ) .orderBy(bookedStartsAt, "asc") .execute(); diff --git a/app/features/tournament-bracket/BracketRepository.server.ts b/app/features/tournament-bracket/BracketRepository.server.ts index 13a575bab..a42f6a34a 100644 --- a/app/features/tournament-bracket/BracketRepository.server.ts +++ b/app/features/tournament-bracket/BracketRepository.server.ts @@ -163,6 +163,13 @@ export function insertBracket(args: { .returning(["id"]) .executeTakeFirstOrThrow(); + // no team can join once a bracket has started, so none is looking for members anymore + await trx + .updateTable("TournamentTeam") + .set({ isLooking: 0 }) + .where("tournamentId", "=", args.tournamentId) + .execute(); + if ( args.bracket.group.length === 0 || args.bracket.round.length === 0 || diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts index 4d20789ba..a6d13927a 100644 --- a/app/features/user-page/UserRepository.server.ts +++ b/app/features/user-page/UserRepository.server.ts @@ -762,7 +762,7 @@ const nameLikeExpr = (column: string, name: string) => { return sql`${sql.ref(column)} like ${pattern} escape '\\'`; }; -export function findResultsByUserId( +export async function findResultsByUserId( userId: number, { limit, @@ -773,95 +773,144 @@ export function findResultsByUserId( offset?: number; } = {}, ) { + const page = + limit !== undefined + ? await findResultPageKeys(userId, filters, { limit, offset }) + : null; + const calendarEventResultsQuery = baseCalendarEventResultsQuery( userId, filters, - ).select(({ eb, fn }) => [ - "CalendarEvent.id as eventId", - sql`null`.as("tournamentId"), - "CalendarEventResultTeam.placement", - "CalendarEvent.participantCount", - sql`null`.as("setResults"), - sql`null`.as("div"), - sql`null`.as("logoUrl"), - "CalendarEvent.name as eventName", - "CalendarEventResultTeam.id as teamId", - "CalendarEventResultTeam.name as teamName", - fn("iif", ["UserResultHighlight.userId", sql`1`, sql`0`]).as( - "isHighlight", - ), - sql`null`.as("tier"), - withMaxEventStartTime(eb), - jsonArrayFrom( - eb - .selectFrom("CalendarEventResultPlayer") - .leftJoin("User", "User.id", "CalendarEventResultPlayer.userId") - .select((mateEb) => [ - ...commonUserSelect(mateEb), - "CalendarEventResultPlayer.name", - ]) - .whereRef( - "CalendarEventResultPlayer.teamId", - "=", - "CalendarEventResultTeam.id", - ) - .where((mateEb) => - mateEb.or([ - mateEb("CalendarEventResultPlayer.userId", "is", null), - mateEb("CalendarEventResultPlayer.userId", "!=", userId), - ]), - ), - ).as("mates"), - ]); + ) + .$if(page !== null, (qb) => + qb.where("CalendarEventResultTeam.id", "in", page!.calendarEventTeamIds), + ) + .select(({ eb, fn }) => [ + "CalendarEvent.id as eventId", + sql`null`.as("tournamentId"), + "CalendarEventResultTeam.placement", + "CalendarEvent.participantCount", + sql`null`.as("setResults"), + sql`null`.as("div"), + sql`null`.as("logoUrl"), + "CalendarEvent.name as eventName", + "CalendarEventResultTeam.id as teamId", + "CalendarEventResultTeam.name as teamName", + fn("iif", [ + "UserResultHighlight.userId", + sql`1`, + sql`0`, + ]).as("isHighlight"), + sql`null`.as("tier"), + withMaxEventStartTime(eb), + jsonArrayFrom( + eb + .selectFrom("CalendarEventResultPlayer") + .leftJoin("User", "User.id", "CalendarEventResultPlayer.userId") + .select((mateEb) => [ + ...commonUserSelect(mateEb), + "CalendarEventResultPlayer.name", + ]) + .whereRef( + "CalendarEventResultPlayer.teamId", + "=", + "CalendarEventResultTeam.id", + ) + .where((mateEb) => + mateEb.or([ + mateEb("CalendarEventResultPlayer.userId", "is", null), + mateEb("CalendarEventResultPlayer.userId", "!=", userId), + ]), + ), + ).as("mates"), + ]); - const tournamentResultsQuery = baseTournamentResultsQuery( - userId, - filters, - ).select(({ eb }) => [ - sql`null`.as("eventId"), - "TournamentResult.tournamentId", - "TournamentResult.placement", - "TournamentResult.participantCount", - "TournamentResult.setResults", - "TournamentResult.div", - tournamentLogoOrNull(eb).as("logoUrl"), - "CalendarEvent.name as eventName", - "TournamentTeam.id as teamId", - "TournamentTeam.name as teamName", - "TournamentResult.isHighlight", - RESULT_TIER.as("tier"), - withMaxEventStartTime(eb), - jsonArrayFrom( - eb - .selectFrom("TournamentResult as TournamentResult2") - .innerJoin("User", "User.id", "TournamentResult2.userId") - .select((mateEb) => [ - ...commonUserSelect(mateEb), - sql`null`.as("name"), - ]) - .whereRef( - "TournamentResult2.tournamentTeamId", - "=", - "TournamentResult.tournamentTeamId", - ) - .where("TournamentResult2.userId", "!=", userId), - ).as("mates"), - ]); + const tournamentResultsQuery = baseTournamentResultsQuery(userId, filters) + .$if(page !== null, (qb) => + qb.where( + "TournamentResult.tournamentTeamId", + "in", + page!.tournamentTeamIds, + ), + ) + .select(({ eb }) => [ + sql`null`.as("eventId"), + "TournamentResult.tournamentId", + "TournamentResult.placement", + "TournamentResult.participantCount", + "TournamentResult.setResults", + "TournamentResult.div", + tournamentLogoOrNull(eb).as("logoUrl"), + "CalendarEvent.name as eventName", + "TournamentTeam.id as teamId", + "TournamentTeam.name as teamName", + "TournamentResult.isHighlight", + RESULT_TIER.as("tier"), + withMaxEventStartTime(eb), + jsonArrayFrom( + eb + .selectFrom("TournamentResult as TournamentResult2") + .innerJoin("User", "User.id", "TournamentResult2.userId") + .select((mateEb) => [ + ...commonUserSelect(mateEb), + sql`null`.as("name"), + ]) + .whereRef( + "TournamentResult2.tournamentTeamId", + "=", + "TournamentResult.tournamentTeamId", + ) + .where("TournamentResult2.userId", "!=", userId), + ).as("mates"), + ]); - let query = calendarEventResultsQuery + return calendarEventResultsQuery .unionAll(tournamentResultsQuery) .orderBy("startsAt", "desc") - .$narrowType<{ startsAt: NotNull }>(); + .$narrowType<{ startsAt: NotNull }>() + .execute(); +} - if (limit !== undefined) { - query = query.limit(limit); - } +/** + * Identities of the results on one page, newest first. Resolved on their own because the + * per-row columns of {@link findResultsByUserId} (mates, logos) would otherwise be computed + * for the user's every result before the sort and limit. + */ +async function findResultPageKeys( + userId: number, + filters: ResultsFilters, + { limit, offset }: { limit: number; offset?: number }, +) { + const rows = await baseCalendarEventResultsQuery(userId, filters) + .select((eb) => [ + sql`"CalendarEventResultTeam"."id"`.as( + "calendarEventTeamId", + ), + sql`null`.as("tournamentTeamId"), + withMaxEventStartTime(eb), + ]) + .unionAll( + baseTournamentResultsQuery(userId, filters).select((eb) => [ + sql`null`.as("calendarEventTeamId"), + sql`"TournamentResult"."tournamentTeamId"`.as( + "tournamentTeamId", + ), + withMaxEventStartTime(eb), + ]), + ) + .orderBy("startsAt", "desc") + .limit(limit) + .$if(offset !== undefined, (qb) => qb.offset(offset!)) + .execute(); - if (offset !== undefined) { - query = query.offset(offset); - } - - return query.execute(); + return { + calendarEventTeamIds: rows.flatMap((row) => + row.calendarEventTeamId !== null ? [row.calendarEventTeamId] : [], + ), + tournamentTeamIds: rows.flatMap((row) => + row.tournamentTeamId !== null ? [row.tournamentTeamId] : [], + ), + }; } export async function countResultsByUserId( diff --git a/changelog/2026-09-08-sidebar-friend-lfg-activity.md b/changelog/2026-09-08-sidebar-friend-lfg-activity.md new file mode 100644 index 000000000..562398759 --- /dev/null +++ b/changelog/2026-09-08-sidebar-friend-lfg-activity.md @@ -0,0 +1,4 @@ +--- +type: bug +--- +Friends looking for a pickup team in a tournament with open registration show up as such in the sidebar again diff --git a/migrations/20260908124343-reset-stale-is-looking.ts b/migrations/20260908124343-reset-stale-is-looking.ts new file mode 100644 index 000000000..694acf3d3 --- /dev/null +++ b/migrations/20260908124343-reset-stale-is-looking.ts @@ -0,0 +1,12 @@ +import { type Kysely, sql } from "kysely"; + +/** + * `TournamentTeam.isLooking` used to stay set after the tournament had started. Starting a + * bracket clears it from now on, so the sidebar friend activity no longer joins every past + * pickup registration. + */ +export async function up(db: Kysely): Promise { + await sql`update "TournamentTeam" set "isLooking" = 0 where "isLooking" = 1 and "tournamentId" in (select "tournamentId" from "TournamentStage")`.execute( + db, + ); +} diff --git a/scripts/benchmark-db/cases.ts b/scripts/benchmark-db/cases.ts index 92c0d1391..19197f47f 100644 --- a/scripts/benchmark-db/cases.ts +++ b/scripts/benchmark-db/cases.ts @@ -1368,6 +1368,12 @@ export function buildCases(fx: Fixtures): { add("UserRepository.findResultsByUserId", fx.heavyUser, (user) => UserRepository.findResultsByUserId(user.id, {}), ); + add("UserRepository.findResultsByUserId.page", fx.heavyUser, (user) => + UserRepository.findResultsByUserId(user.id, { limit: 25, offset: 25 }), + ); + add("UserRepository.findResultsByUserId.widget", fx.heavyUser, (user) => + UserRepository.findResultsByUserId(user.id, { limit: 3 }), + ); add("UserRepository.countResultsByUserId", fx.heavyUser, (user) => UserRepository.countResultsByUserId(user.id), );