From d0c456cf3db8b558d325d3f46467523b7d553df4 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 25 Jul 2026 08:09:08 +0300 Subject: [PATCH] point -> ko DB migration --- app/components/match-page/MatchActionTab.tsx | 6 +-- app/components/match-page/MatchTimeline.tsx | 17 +++---- app/db/tables.ts | 4 +- .../routes/sendouq.match.$matchId.ts | 2 +- .../api-public/routes/tournament-match.$id.ts | 10 ++-- app/features/api-public/schema.ts | 4 +- .../routes/match-page-test.tsx | 2 +- .../BracketRepository.server.ts | 9 +--- .../tournament-bracket-schemas.server.ts | 1 - .../TournamentMatchRepository.server.ts | 6 +-- .../actions/to.$id.matches.$mid.server.ts | 47 +++++------------- .../components/TournamentMatchActionTab.tsx | 8 +-- .../components/TournamentMatchAdminTab.tsx | 7 +-- .../components/TournamentMatchTabs.tsx | 10 +--- .../insertTournamentMatchGameResult.server.ts | 4 +- .../queries/updateMatchGameResultKo.server.ts | 17 +++++++ .../updateMatchGameResultPoints.server.ts | 20 -------- .../routes/to.$id.matches.$mid.test.ts | 3 +- db-test.sqlite3 | Bin 1470464 -> 1470464 bytes ...159-tournament-match-nullable-opponents.js | 20 ++++++++ 20 files changed, 77 insertions(+), 120 deletions(-) create mode 100644 app/features/tournament-match/queries/updateMatchGameResultKo.server.ts delete mode 100644 app/features/tournament-match/queries/updateMatchGameResultPoints.server.ts diff --git a/app/components/match-page/MatchActionTab.tsx b/app/components/match-page/MatchActionTab.tsx index 791fcac32..fe6702496 100644 --- a/app/components/match-page/MatchActionTab.tsx +++ b/app/components/match-page/MatchActionTab.tsx @@ -219,11 +219,7 @@ function SetEndingConfirmation({ timestamp: Date.now(), winner: winnerSide, rosters: setEnding.currentRosters, - points: withKo - ? isKo - ? [winnerSide === "ALPHA" ? 100 : 0, winnerSide === "BRAVO" ? 100 : 0] - : [0, 0] - : undefined, + ko: withKo ? isKo : undefined, }; const updatedScore = { diff --git a/app/components/match-page/MatchTimeline.tsx b/app/components/match-page/MatchTimeline.tsx index fe8d8e40d..6427d40c7 100644 --- a/app/components/match-page/MatchTimeline.tsx +++ b/app/components/match-page/MatchTimeline.tsx @@ -48,8 +48,8 @@ export interface TimelineMap { alpha: Array; bravo: Array; }; - /** Optional point values [alpha, bravo] */ - points?: [number, number]; + /** Whether the game ended in a knockout. Undefined if not collected. */ + ko?: boolean; /** Side that picked this map (counterpick / postGame map PICK). Renders a click indicator next to that side's WIN/LOSS label. */ pickedBy?: MatchSide; } @@ -210,15 +210,12 @@ function TimelineHeader({ function TimelineMapRow({ map }: { map: TimelineMap }) { const { t } = useTranslation(["game-misc"]); - const alphaPoints = map.points?.[0]; - const bravoPoints = map.points?.[1]; - return (
@@ -242,7 +239,7 @@ function TimelineMapRow({ map }: { map: TimelineMap }) {
@@ -253,12 +250,12 @@ function TimelineMapRow({ map }: { map: TimelineMap }) { function SideResult({ result, - points, + isKo, weapons, isPicked, }: { result: "WIN" | "LOSS"; - points?: number; + isKo?: boolean; weapons?: Array; isPicked?: boolean; }) { @@ -288,7 +285,7 @@ function SideResult({ ? t("q:match.timeline.win") : t("q:match.timeline.loss")} - {points === 100 ? ( + {isKo ? ( {t("q:match.action.ko")} ) : null}
diff --git a/app/db/tables.ts b/app/db/tables.ts index 04877f05e..0cd53a7a7 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -692,6 +692,8 @@ export interface TournamentMatchPickBanEvent { export interface TournamentMatchGameResult { createdAt: Generated; id: GeneratedAlways; + /** Whether the game ended in a knockout. `null` if not collected for this bracket. */ + ko: DBBoolean | null; matchId: number; mode: ModeShort; number: number; @@ -699,8 +701,6 @@ export interface TournamentMatchGameResult { source: string; stageId: StageId; winnerTeamId: number; - opponentOnePoints: number | null; - opponentTwoPoints: number | null; } export interface TournamentMatchGameResultParticipant { diff --git a/app/features/api-public/routes/sendouq.match.$matchId.ts b/app/features/api-public/routes/sendouq.match.$matchId.ts index cebda392e..2a60a0d4b 100644 --- a/app/features/api-public/routes/sendouq.match.$matchId.ts +++ b/app/features/api-public/routes/sendouq.match.$matchId.ts @@ -58,7 +58,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { ? (map.source as MapListMap["source"]) : Number(map.source), participatedUserIds: null, - points: null, + ko: null, })), teamAlpha: { id: match.groupAlpha.id, diff --git a/app/features/api-public/routes/tournament-match.$id.ts b/app/features/api-public/routes/tournament-match.$id.ts index 7634c8a4a..7bdfd174c 100644 --- a/app/features/api-public/routes/tournament-match.$id.ts +++ b/app/features/api-public/routes/tournament-match.$id.ts @@ -52,8 +52,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { "TournamentMatchGameResult.mode", "TournamentMatchGameResult.winnerTeamId", "TournamentMatchGameResult.source", - "TournamentMatchGameResult.opponentOnePoints", - "TournamentMatchGameResult.opponentTwoPoints", + "TournamentMatchGameResult.ko", jsonArrayFrom( innerEb .selectFrom("TournamentMatchGameResultParticipant") @@ -106,10 +105,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { participatedUserIds: playedMap.participants.map((p) => p.userId), winnerTeamId: playedMap.winnerTeamId, source: parseSource(playedMap.source), - points: - playedMap.opponentOnePoints && playedMap.opponentTwoPoints - ? [playedMap.opponentOnePoints, playedMap.opponentTwoPoints] - : null, + ko: playedMap.ko !== null ? Boolean(playedMap.ko) : null, })); } @@ -147,7 +143,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { participatedUserIds: null, winnerTeamId: null, source: mapListMap.source, - points: null, + ko: null, }; }); }; diff --git a/app/features/api-public/schema.ts b/app/features/api-public/schema.ts index 1a3c9000a..e2ad37c34 100644 --- a/app/features/api-public/schema.ts +++ b/app/features/api-public/schema.ts @@ -526,8 +526,8 @@ export type MapListMap = { | "ROLL"; winnerTeamId: number | null; participatedUserIds: Array | null; - /** (round robin only) points of the match used for tiebreaker purposes. e.g. [100, 0] indicates a knockout. */ - points: [number, number] | null; + /** (round robin only) whether the map ended in a knockout. `null` if not tracked. */ + ko: boolean | null; }; type TournamentMatchTeam = { diff --git a/app/features/match-page-test/routes/match-page-test.tsx b/app/features/match-page-test/routes/match-page-test.tsx index a270abf8a..2938f2f79 100644 --- a/app/features/match-page-test/routes/match-page-test.tsx +++ b/app/features/match-page-test/routes/match-page-test.tsx @@ -688,7 +688,7 @@ export default function MatchPageTestRoute() { mode: "RM", timestamp: 1712856200, winner: "ALPHA", - points: [100, 42], + ko: true, weapons: { alpha: [40, null, 1100, 3040], bravo: [null, 210, null, 4010], diff --git a/app/features/tournament-bracket/BracketRepository.server.ts b/app/features/tournament-bracket/BracketRepository.server.ts index 839227eb8..a5d0943e7 100644 --- a/app/features/tournament-bracket/BracketRepository.server.ts +++ b/app/features/tournament-bracket/BracketRepository.server.ts @@ -128,18 +128,13 @@ export async function findByTournamentId( function serializedOpponentWithKos( column: "opponentOne" | "opponentTwo", ): RawBuilder { - const [winnerPoints, loserPoints] = - column === "opponentOne" - ? (["opponentOnePoints", "opponentTwoPoints"] as const) - : (["opponentTwoPoints", "opponentOnePoints"] as const); - return kyselySql`json_set( json_remove(${kyselySql.ref(`TournamentMatch.${column}`)}, '$.totalPoints'), '$.totalKos', sum( case - when ${kyselySql.ref(`TournamentMatchGameResult.${winnerPoints}`)} = 100 - and ${kyselySql.ref(`TournamentMatchGameResult.${loserPoints}`)} = 0 + when "TournamentMatchGameResult"."ko" = 1 + and "TournamentMatchGameResult"."winnerTeamId" = ${kyselySql.ref(`TournamentMatch.${column}`)} ->> '$.id' then 1 else 0 end diff --git a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts index a34bd7cb3..d903b234d 100644 --- a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts +++ b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts @@ -31,7 +31,6 @@ const reportedMatchPosition = z.preprocess( .max(Math.max(...TOURNAMENT.AVAILABLE_BEST_OF) - 1), ); -// TODO: KO is stored as points (100-0, 0-100 or 0-0). If we decide that this KO only approach is solid then we can do a proper data model migration const ko = z.preprocess(safeJSONParse, z.boolean().nullish()); export const matchSchema = z.union([ z.object({ diff --git a/app/features/tournament-match/TournamentMatchRepository.server.ts b/app/features/tournament-match/TournamentMatchRepository.server.ts index fcfd7245d..e09154de8 100644 --- a/app/features/tournament-match/TournamentMatchRepository.server.ts +++ b/app/features/tournament-match/TournamentMatchRepository.server.ts @@ -105,8 +105,7 @@ export function findResultById(id: number) { .select([ "TournamentMatchGameResult.id", "TournamentMatchGameResult.matchId", - "TournamentMatchGameResult.opponentOnePoints", - "TournamentMatchGameResult.opponentTwoPoints", + "TournamentMatchGameResult.ko", "TournamentMatchGameResult.winnerTeamId", ]) .where("TournamentMatchGameResult.id", "=", id) @@ -123,8 +122,7 @@ export function findResultsByMatchId(matchId: number) { "TournamentMatchGameResult.mode", "TournamentMatchGameResult.source", "TournamentMatchGameResult.createdAt", - "TournamentMatchGameResult.opponentOnePoints", - "TournamentMatchGameResult.opponentTwoPoints", + "TournamentMatchGameResult.ko", jsonArrayFrom( eb .selectFrom("TournamentMatchGameResultParticipant") diff --git a/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts b/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts index 37e229263..f7187667f 100644 --- a/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts +++ b/app/features/tournament-match/actions/to.$id.matches.$mid.server.ts @@ -42,7 +42,7 @@ import { resolveMapList } from "../core/mapList.server"; import { deleteParticipantsByMatchGameResultId } from "../queries/deleteParticipantsByMatchGameResultId.server"; import { insertTournamentMatchGameResult } from "../queries/insertTournamentMatchGameResult.server"; import { insertTournamentMatchGameResultParticipant } from "../queries/insertTournamentMatchGameResultParticipant.server"; -import { updateMatchGameResultPoints } from "../queries/updateMatchGameResultPoints.server"; +import { updateMatchGameResultKo } from "../queries/updateMatchGameResultKo.server"; import type { FindMatchById } from "../TournamentMatchRepository.server"; import { matchIsLocked, @@ -158,8 +158,6 @@ export const action: ActionFunction = async ({ params, request }) => { ]; invariant(currentMap, "Can't resolve current map"); - const winnerSide = data.winnerTeamId === match.opponentOne.id ? 0 : 1; - const bracket = tournament.bracketByIdx( tournament.matchIdToBracketIdx(match.id)!, )!; @@ -168,10 +166,6 @@ export const action: ActionFunction = async ({ params, request }) => { "KO status is required for this bracket", ); - const points = bracket.collectsKos - ? koToPoints({ ko: Boolean(data.ko), winnerSide }) - : null; - const teamOneRoster = tournamentTeamToActiveRosterUserIds( tournament.teamById(match.opponentOne.id!)!, tournament.minMembersPerTeam, @@ -210,8 +204,7 @@ export const action: ActionFunction = async ({ params, request }) => { winnerTeamId: data.winnerTeamId, number: data.position + 1, source: String(currentMap.source), - opponentOnePoints: points?.[0] ?? null, - opponentTwoPoints: points?.[1] ?? null, + ko: bracket.collectsKos ? Number(Boolean(data.ko)) : null, }); for (const userId of teamOneRoster) { @@ -390,12 +383,15 @@ export const action: ActionFunction = async ({ params, request }) => { "Invalid roster", ); - const hadKoRecorded = typeof result.opponentOnePoints === "number"; - const hasKoSubmitted = typeof data.ko === "boolean"; - errorToastIfFalsy(hadKoRecorded === hasKoSubmitted, "KO status mismatch"); + const bracket = tournament.bracketByIdx( + tournament.matchIdToBracketIdx(match.id)!, + )!; + errorToastIfFalsy( + !bracket.collectsKos || typeof data.ko === "boolean", + "KO status is required for this bracket", + ); - const wasKo = - result.opponentOnePoints === 100 || result.opponentTwoPoints === 100; + const wasKo = Boolean(result.ko); if (typeof data.ko === "boolean" && data.ko !== wasKo) { // changing the KO status at this point could retroactively change who advanced from the group errorToastIfFalsy( @@ -406,15 +402,9 @@ export const action: ActionFunction = async ({ params, request }) => { sql.transaction(() => { if (typeof data.ko === "boolean") { - const points = koToPoints({ - ko: data.ko, - winnerSide: result.winnerTeamId === match.opponentOne!.id ? 0 : 1, - }); - - updateMatchGameResultPoints({ + updateMatchGameResultKo({ matchGameResultId: result.id, - opponentOnePoints: points[0], - opponentTwoPoints: points[1], + ko: data.ko, }); } @@ -853,19 +843,6 @@ export const action: ActionFunction = async ({ params, request }) => { return null; }; -/** KO status is stored as points: the KO winner gets 100 and the loser 0, a game that was not a KO is stored as 0-0. */ -function koToPoints({ - ko, - winnerSide, -}: { - ko: boolean; - winnerSide: 0 | 1; -}): [number, number] { - if (!ko) return [0, 0]; - - return winnerSide === 0 ? [100, 0] : [0, 100]; -} - function canReportTournamentScore({ match, isMemberOfATeamInTheMatch, diff --git a/app/features/tournament-match/components/TournamentMatchActionTab.tsx b/app/features/tournament-match/components/TournamentMatchActionTab.tsx index 745b0003e..bc5602414 100644 --- a/app/features/tournament-match/components/TournamentMatchActionTab.tsx +++ b/app/features/tournament-match/components/TournamentMatchActionTab.tsx @@ -271,13 +271,7 @@ function buildSetEndingData({ alpha: alphaParticipants, bravo: bravoParticipants, }, - points: - result.opponentOnePoints != null && result.opponentTwoPoints != null - ? ([result.opponentOnePoints, result.opponentTwoPoints] as [ - number, - number, - ]) - : undefined, + ko: result.ko != null ? Boolean(result.ko) : undefined, }; }); diff --git a/app/features/tournament-match/components/TournamentMatchAdminTab.tsx b/app/features/tournament-match/components/TournamentMatchAdminTab.tsx index 2cab7693b..dda2ab14b 100644 --- a/app/features/tournament-match/components/TournamentMatchAdminTab.tsx +++ b/app/features/tournament-match/components/TournamentMatchAdminTab.tsx @@ -399,8 +399,7 @@ function EditReportedScoreRow({ previousFetcherStateRef.current = fetcher.state; }, [fetcher.state, fetcher.data]); - const isKo = - result.opponentOnePoints === 100 || result.opponentTwoPoints === 100; + const isKo = Boolean(result.ko); if (!editing) { return ( @@ -471,9 +470,7 @@ function EditReportedScoreForm({ .map((p) => p.userId), ]; }); - const [isKO, setIsKO] = React.useState( - result.opponentOnePoints === 100 || result.opponentTwoPoints === 100, - ); + const [isKO, setIsKO] = React.useState(Boolean(result.ko)); const formValid = checkedPlayers.every( (team) => team.length === minMembersPerTeam, diff --git a/app/features/tournament-match/components/TournamentMatchTabs.tsx b/app/features/tournament-match/components/TournamentMatchTabs.tsx index 2ad98bf0e..8d1f28790 100644 --- a/app/features/tournament-match/components/TournamentMatchTabs.tsx +++ b/app/features/tournament-match/components/TournamentMatchTabs.tsx @@ -153,9 +153,6 @@ function resolveTimelineMaps( })); return data.results.map((result, mapIndex) => { - const hasPoints = - result.opponentOnePoints !== null && result.opponentTwoPoints !== null; - const alphaRoster = resolveRoster(result.participants, opponentOneId); const bravoRoster = resolveRoster(result.participants, opponentTwoId); @@ -185,12 +182,7 @@ function resolveTimelineMaps( weapons: hasAnyWeapon ? { alpha: alphaWeapons, bravo: bravoWeapons } : undefined, - points: hasPoints - ? ([result.opponentOnePoints, result.opponentTwoPoints] as [ - number, - number, - ]) - : undefined, + ko: result.ko != null ? Boolean(result.ko) : undefined, }; }); } diff --git a/app/features/tournament-match/queries/insertTournamentMatchGameResult.server.ts b/app/features/tournament-match/queries/insertTournamentMatchGameResult.server.ts index d2cf4f5ad..ecb773b8d 100644 --- a/app/features/tournament-match/queries/insertTournamentMatchGameResult.server.ts +++ b/app/features/tournament-match/queries/insertTournamentMatchGameResult.server.ts @@ -3,9 +3,9 @@ import type { Tables } from "~/db/tables"; const stm = sql.prepare(/* sql */ ` insert into "TournamentMatchGameResult" - ("matchId", "stageId", "mode", "winnerTeamId", "reporterId", "number", "source", "opponentOnePoints", "opponentTwoPoints") + ("matchId", "stageId", "mode", "winnerTeamId", "reporterId", "number", "source", "ko") values - (@matchId, @stageId, @mode, @winnerTeamId, @reporterId, @number, @source, @opponentOnePoints, @opponentTwoPoints) + (@matchId, @stageId, @mode, @winnerTeamId, @reporterId, @number, @source, @ko) returning * `); diff --git a/app/features/tournament-match/queries/updateMatchGameResultKo.server.ts b/app/features/tournament-match/queries/updateMatchGameResultKo.server.ts new file mode 100644 index 000000000..2a1344eb8 --- /dev/null +++ b/app/features/tournament-match/queries/updateMatchGameResultKo.server.ts @@ -0,0 +1,17 @@ +import { sql } from "~/db/sql"; + +const stm = sql.prepare(/* sql */ ` + update "TournamentMatchGameResult" + set "ko" = @ko + where "id" = @id +`); + +export function updateMatchGameResultKo({ + matchGameResultId, + ko, +}: { + matchGameResultId: number; + ko: boolean; +}) { + stm.run({ id: matchGameResultId, ko: Number(ko) }); +} diff --git a/app/features/tournament-match/queries/updateMatchGameResultPoints.server.ts b/app/features/tournament-match/queries/updateMatchGameResultPoints.server.ts deleted file mode 100644 index ac009f2fa..000000000 --- a/app/features/tournament-match/queries/updateMatchGameResultPoints.server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { sql } from "~/db/sql"; - -const stm = sql.prepare(/* sql */ ` - update "TournamentMatchGameResult" - set "opponentOnePoints" = @opponentOnePoints, - "opponentTwoPoints" = @opponentTwoPoints - where "id" = @id -`); - -export function updateMatchGameResultPoints({ - matchGameResultId, - opponentOnePoints, - opponentTwoPoints, -}: { - matchGameResultId: number; - opponentOnePoints: number; - opponentTwoPoints: number; -}) { - stm.run({ id: matchGameResultId, opponentOnePoints, opponentTwoPoints }); -} diff --git a/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts b/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts index 2cf636a32..cb2a6d278 100644 --- a/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts +++ b/app/features/tournament-match/routes/to.$id.matches.$mid.test.ts @@ -130,8 +130,7 @@ describe("Tournament match page", () => { ), "Result participants should only include active roster user ids", ).toBeTruthy(); - expect(result.opponentOnePoints).toBe(null); - expect(result.opponentTwoPoints).toBe(null); + expect(result.ko).toBe(null); expect(result.winnerTeamId).toBe(1); }); diff --git a/db-test.sqlite3 b/db-test.sqlite3 index b22347788eb1a34d708efa2fd95a01a09c7be818..fb83dc0448e2de8b56bc9a021cbeb1bce00c175f 100644 GIT binary patch delta 264 zcmZp85ZUk`a)Pv=00RSKIuNrlFfcFyk=sNaV@83-gw_Pc)&!>31m@NRmaPe_3*U1x zKVaZslw)9@vz_Y$>osO>-c!tb*v0Mb8M`Z|Z`dm!GX213R<7-wUsxlVUE7=+nZa^B zq9ut*IjJG}rA2v(xv6<2zKJDmYEa4UXe^SPFiAyJQ>QXAW#>A6dH3phcAkGvL@2Y>O`R`rXo{8~Zu^NzF5QWoRGSiKLk)_=yVjP_ z&``J(=++npIYmxkQ$Yvu*k&eK>2_tG0-lwL|?)%gyRFex(mf{_zB9u{{^NPY}BpNZpCue zOHR4EhTWXLjO7WeNOikm;pj4&=|SVlM;ddzqw~)cP8n0 zlGl@wbf#P_IYoQBl1V$2eJ7j7QC^SZ!*xjN+228Y%j>H+yjLUmJAn&)E$B;VJcBA} xhwK)OF{Ds^1Q}n(aC+3A1Mw0ck%^xuBtRC3N`gcqArdAL5+$)ge;(gz`~l8pfWZI& diff --git a/migrations/159-tournament-match-nullable-opponents.js b/migrations/159-tournament-match-nullable-opponents.js index 021d55447..1d7c56a9b 100644 --- a/migrations/159-tournament-match-nullable-opponents.js +++ b/migrations/159-tournament-match-nullable-opponents.js @@ -110,6 +110,26 @@ export function up(db) { `, ).run(); + db.prepare( + /* sql */ `alter table "TournamentMatchGameResult" add "ko" integer`, + ).run(); + + db.prepare( + /* sql */ ` + update "TournamentMatchGameResult" + set "ko" = 1 + where ("opponentOnePoints" = 100 and "opponentTwoPoints" = 0) + or ("opponentOnePoints" = 0 and "opponentTwoPoints" = 100) + `, + ).run(); + + db.prepare( + /* sql */ `alter table "TournamentMatchGameResult" drop column "opponentOnePoints"`, + ).run(); + db.prepare( + /* sql */ `alter table "TournamentMatchGameResult" drop column "opponentTwoPoints"`, + ).run(); + db.pragma("foreign_key_check"); })();