mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-02 16:20:36 -05:00
Denormalize peak XP to SplatoonPlayer table (#2741)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -429,6 +429,8 @@ export interface SplatoonPlayer {
|
||||
id: GeneratedAlways<number>;
|
||||
splId: string;
|
||||
userId: number | null;
|
||||
/** Players best XP across both divisions. Denormalized for performance. */
|
||||
peakXp: number | null;
|
||||
}
|
||||
|
||||
export interface TaggedArt {
|
||||
|
||||
@@ -24,6 +24,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
await db
|
||||
.selectFrom("User")
|
||||
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
|
||||
.leftJoin("SplatoonPlayer", "SplatoonPlayer.userId", "User.id")
|
||||
.select(({ eb }) => [
|
||||
"User.id",
|
||||
"User.country",
|
||||
@@ -55,17 +56,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
.groupBy(["BadgeOwner.badgeId", "BadgeOwner.userId"])
|
||||
.whereRef("BadgeOwner.userId", "=", "User.id"),
|
||||
).as("badges"),
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.innerJoin(
|
||||
"XRankPlacement",
|
||||
"XRankPlacement.playerId",
|
||||
"SplatoonPlayer.id",
|
||||
)
|
||||
.select(["XRankPlacement.power"])
|
||||
.whereRef("SplatoonPlayer.userId", "=", "User.id"),
|
||||
).as("xRankPlacements"),
|
||||
"SplatoonPlayer.peakXp",
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TeamMemberWithSecondary")
|
||||
@@ -117,13 +108,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
tier: skill.tier,
|
||||
}
|
||||
: null,
|
||||
peakXp:
|
||||
user.xRankPlacements.length > 0
|
||||
? user.xRankPlacements.reduce((acc, cur) => {
|
||||
if (!cur.power) return acc;
|
||||
return Math.max(acc, cur.power);
|
||||
}, 0)
|
||||
: null,
|
||||
peakXp: user.peakXp,
|
||||
weaponPool: user.weapons.map((weapon) => ({
|
||||
id: weapon.weaponSplId,
|
||||
name: t(`weapons:MAIN_${weapon.weaponSplId}`),
|
||||
|
||||
@@ -17,12 +17,11 @@ const deleteBadgeOwnerStm = sql.prepare(/* sql */ `
|
||||
const userTopXPowersStm = sql.prepare(/* sql */ `
|
||||
select
|
||||
"SplatoonPlayer"."userId",
|
||||
max("XRankPlacement"."power") as "xPower"
|
||||
"SplatoonPlayer"."peakXp" as "xPower"
|
||||
from
|
||||
"SplatoonPlayer"
|
||||
left join "XRankPlacement" on "XRankPlacement"."playerId" = "SplatoonPlayer"."id"
|
||||
where "SplatoonPlayer"."userId" is not null
|
||||
group by "SplatoonPlayer"."userId"
|
||||
and "SplatoonPlayer"."peakXp" is not null
|
||||
`);
|
||||
|
||||
const addXPBadgeStm = sql.prepare(/* sql */ `
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { db } from "~/db/sql";
|
||||
import { dbReset } from "~/utils/Test";
|
||||
import * as XRankPlacementRepository from "./XRankPlacementRepository.server";
|
||||
|
||||
let placementCounter = 0;
|
||||
|
||||
const createSplatoonPlayer = async (splId: string) => {
|
||||
const result = await db
|
||||
.insertInto("SplatoonPlayer")
|
||||
.values({ splId })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return result.id;
|
||||
};
|
||||
|
||||
const createXRankPlacement = async ({
|
||||
playerId,
|
||||
power,
|
||||
}: {
|
||||
playerId: number;
|
||||
power: number;
|
||||
}) => {
|
||||
placementCounter++;
|
||||
|
||||
await db
|
||||
.insertInto("XRankPlacement")
|
||||
.values({
|
||||
playerId,
|
||||
power,
|
||||
badges: "[]",
|
||||
bannerSplId: 1,
|
||||
mode: "SZ",
|
||||
month: 1,
|
||||
year: 2024,
|
||||
name: "Test Player",
|
||||
nameDiscriminator: "0000",
|
||||
rank: placementCounter,
|
||||
region: "WEST",
|
||||
title: "Test",
|
||||
weaponSplId: 0,
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
|
||||
describe("refreshAllPeakXp", () => {
|
||||
beforeEach(() => {
|
||||
placementCounter = 0;
|
||||
dbReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("sets peakXp to max power for each player", async () => {
|
||||
const player1Id = await createSplatoonPlayer("player1");
|
||||
const player2Id = await createSplatoonPlayer("player2");
|
||||
|
||||
await createXRankPlacement({ playerId: player1Id, power: 2500 });
|
||||
await createXRankPlacement({ playerId: player1Id, power: 2700 });
|
||||
await createXRankPlacement({ playerId: player1Id, power: 2600 });
|
||||
|
||||
await createXRankPlacement({ playerId: player2Id, power: 3000 });
|
||||
await createXRankPlacement({ playerId: player2Id, power: 2800 });
|
||||
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
|
||||
const players = await db
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select(["id", "peakXp"])
|
||||
.orderBy("id", "asc")
|
||||
.execute();
|
||||
|
||||
expect(players[0].peakXp).toBe(2700);
|
||||
expect(players[1].peakXp).toBe(3000);
|
||||
});
|
||||
|
||||
test("sets peakXp to null for player with no placements", async () => {
|
||||
const playerId = await createSplatoonPlayer("player1");
|
||||
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
|
||||
const player = await db
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select("peakXp")
|
||||
.where("id", "=", playerId)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(player.peakXp).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -67,3 +67,15 @@ export async function monthYears() {
|
||||
export type FindPlacement = InferResult<
|
||||
ReturnType<typeof xRankPlacementsQueryBase>
|
||||
>[number];
|
||||
|
||||
export async function refreshAllPeakXp() {
|
||||
await db
|
||||
.updateTable("SplatoonPlayer")
|
||||
.set((eb) => ({
|
||||
peakXp: eb
|
||||
.selectFrom("XRankPlacement")
|
||||
.select((eb) => eb.fn.max("XRankPlacement.power").as("peakXp"))
|
||||
.whereRef("XRankPlacement.playerId", "=", "SplatoonPlayer.id"),
|
||||
}))
|
||||
.execute();
|
||||
}
|
||||
|
||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
21
migrations/115-splatoon-player-peak-xp.js
Normal file
21
migrations/115-splatoon-player-peak-xp.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
/*sql*/ `
|
||||
alter table "SplatoonPlayer"
|
||||
add column "peakXp" real
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
/*sql*/ `
|
||||
update "SplatoonPlayer"
|
||||
set "peakXp" = (
|
||||
select max("XRankPlacement"."power")
|
||||
from "XRankPlacement"
|
||||
where "XRankPlacement"."playerId" = "SplatoonPlayer"."id"
|
||||
)
|
||||
`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { sql } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { syncXPBadges } from "~/features/badges/queries/syncXPBadges.server";
|
||||
import * as BuildRepository from "~/features/builds/BuildRepository.server";
|
||||
import * as XRankPlacementRepository from "~/features/top-search/XRankPlacementRepository.server";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids";
|
||||
import invariant from "~/utils/invariant";
|
||||
@@ -53,6 +54,7 @@ async function main() {
|
||||
}
|
||||
|
||||
addPlacements(placements);
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
syncXPBadges();
|
||||
await BuildRepository.recalculateAllTop500();
|
||||
logger.info(`done reading in ${placements.length} placements`);
|
||||
|
||||
Reference in New Issue
Block a user