}
+ onPress={() => handleToggle(false)}
+ className={styles.collapseButton}
+ aria-label={t("q:match.actions.reportWeapons")}
+ />
+ )}
{pastReported.length > 0 ? (
{pastReported.map((weaponId, i) => (
diff --git a/app/db/tables.ts b/app/db/tables.ts
index 2679d14e1..b00787aa3 100644
--- a/app/db/tables.ts
+++ b/app/db/tables.ts
@@ -453,8 +453,14 @@ export interface PlusVotingResult {
passedVoting: DBBoolean;
}
+// xxx: unify SendouQ side to also key on (matchId, mapIndex). Replace
+// groupMatchMapId with groupMatchId, make mapIndex non-null, and have a
+// single CHECK that exactly one of groupMatchId / tournamentMatchId is set.
+// Backfill (groupMatchId, mapIndex) from GroupMatchMap.matchId/index.
export interface ReportedWeapon {
groupMatchMapId: number | null;
+ tournamentMatchId: number | null;
+ mapIndex: number | null;
userId: number;
weaponSplId: MainWeaponId;
}
diff --git a/app/features/sendouq-match/ReportedWeaponRepository.server.ts b/app/features/sendouq-match/ReportedWeaponRepository.server.ts
index 4423274c3..df1db2221 100644
--- a/app/features/sendouq-match/ReportedWeaponRepository.server.ts
+++ b/app/features/sendouq-match/ReportedWeaponRepository.server.ts
@@ -1,6 +1,9 @@
import type { NotNull, Transaction } from "kysely";
import { db } from "~/db/sql";
import type { DB, TablesInsertable } from "~/db/tables";
+import * as Seasons from "~/features/mmr/core/Seasons";
+import type { MainWeaponId } from "~/modules/in-game-lists/types";
+import { dateToDatabaseTimestamp } from "~/utils/dates";
export function createMany(
weapons: TablesInsertable["ReportedWeapon"][],
@@ -106,3 +109,143 @@ export async function findByMatchId(matchId: number) {
return rows;
}
+
+export async function upsertOneTournament({
+ tournamentMatchId,
+ mapIndex,
+ userId,
+ weaponSplId,
+}: TablesInsertable["ReportedWeapon"] & {
+ tournamentMatchId: number;
+ mapIndex: number;
+}) {
+ await db
+ .deleteFrom("ReportedWeapon")
+ .where("tournamentMatchId", "=", tournamentMatchId)
+ .where("mapIndex", "=", mapIndex)
+ .where("userId", "=", userId)
+ .execute();
+
+ await db
+ .insertInto("ReportedWeapon")
+ .values({ tournamentMatchId, mapIndex, userId, weaponSplId })
+ .execute();
+}
+
+export async function deleteByUserMapIndexTournament({
+ tournamentMatchId,
+ userId,
+ mapIndex,
+}: {
+ tournamentMatchId: number;
+ userId: number;
+ mapIndex: number;
+}) {
+ await db
+ .deleteFrom("ReportedWeapon")
+ .where("tournamentMatchId", "=", tournamentMatchId)
+ .where("mapIndex", "=", mapIndex)
+ .where("userId", "=", userId)
+ .execute();
+}
+
+export async function findByTournamentMatchId(matchId: number) {
+ const rows = await db
+ .selectFrom("ReportedWeapon")
+ .select([
+ "ReportedWeapon.tournamentMatchId",
+ "ReportedWeapon.mapIndex",
+ "ReportedWeapon.weaponSplId",
+ "ReportedWeapon.userId",
+ ])
+ .where("ReportedWeapon.tournamentMatchId", "=", matchId)
+ .orderBy("ReportedWeapon.mapIndex", "asc")
+ .orderBy("ReportedWeapon.userId", "asc")
+ .$narrowType<{ tournamentMatchId: NotNull; mapIndex: NotNull }>()
+ .execute();
+
+ if (rows.length === 0) return null;
+
+ return rows;
+}
+
+/**
+ * Aggregates a user's reported weapons across both SendouQ matches and
+ * finalized tournaments that fall within the given season's date range.
+ */
+export async function seasonReportedWeaponsByUserId({
+ userId,
+ season,
+}: {
+ userId: number;
+ season: number;
+}): Promise
> {
+ const { starts, ends } = Seasons.nthToDateRange(season);
+ const startsTs = dateToDatabaseTimestamp(starts);
+ const endsTs = dateToDatabaseTimestamp(ends);
+
+ const sendouqWeapons = db
+ .selectFrom("ReportedWeapon")
+ .innerJoin(
+ "GroupMatchMap",
+ "GroupMatchMap.id",
+ "ReportedWeapon.groupMatchMapId",
+ )
+ .innerJoin("GroupMatch", "GroupMatch.id", "GroupMatchMap.matchId")
+ .select(({ fn }) => [
+ "ReportedWeapon.weaponSplId",
+ fn.countAll().as("count"),
+ ])
+ .where("ReportedWeapon.userId", "=", userId)
+ .where("GroupMatch.createdAt", ">=", startsTs)
+ .where("GroupMatch.createdAt", "<=", endsTs)
+ .groupBy("ReportedWeapon.weaponSplId");
+
+ const tournamentWeapons = db
+ .selectFrom("ReportedWeapon")
+ .innerJoin(
+ "TournamentMatch",
+ "TournamentMatch.id",
+ "ReportedWeapon.tournamentMatchId",
+ )
+ .innerJoin(
+ "TournamentStage",
+ "TournamentStage.id",
+ "TournamentMatch.stageId",
+ )
+ .innerJoin("Tournament", "Tournament.id", "TournamentStage.tournamentId")
+ .innerJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id")
+ .innerJoin(
+ (eb) =>
+ eb
+ .selectFrom("CalendarEventDate")
+ .select(({ fn }) => [
+ "CalendarEventDate.eventId",
+ fn.min("CalendarEventDate.startTime").as("startTime"),
+ ])
+ .groupBy("CalendarEventDate.eventId")
+ .as("EventStartTime"),
+ (join) => join.onRef("EventStartTime.eventId", "=", "CalendarEvent.id"),
+ )
+ .select(({ fn }) => [
+ "ReportedWeapon.weaponSplId",
+ fn.countAll().as("count"),
+ ])
+ .where("ReportedWeapon.userId", "=", userId)
+ .where("Tournament.isFinalized", "=", 1)
+ .where("EventStartTime.startTime", ">=", startsTs)
+ .where("EventStartTime.startTime", "<=", endsTs)
+ .groupBy("ReportedWeapon.weaponSplId");
+
+ const rows = await db
+ .selectFrom(sendouqWeapons.unionAll(tournamentWeapons).as("merged"))
+ .select(({ fn }) => [
+ "merged.weaponSplId",
+ fn.sum("merged.count").as("count"),
+ ])
+ .groupBy("merged.weaponSplId")
+ .orderBy("count", "desc")
+ .execute();
+
+ return rows;
+}
diff --git a/app/features/sendouq/queries/seasonReportedWeaponsByUserId.server.ts b/app/features/sendouq/queries/seasonReportedWeaponsByUserId.server.ts
deleted file mode 100644
index 89642ac27..000000000
--- a/app/features/sendouq/queries/seasonReportedWeaponsByUserId.server.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { sql } from "~/db/sql";
-import * as Seasons from "~/features/mmr/core/Seasons";
-import type { MainWeaponId } from "~/modules/in-game-lists/types";
-import { dateToDatabaseTimestamp } from "~/utils/dates";
-
-const stm = sql.prepare(/* sql */ `
- select
- "ReportedWeapon"."weaponSplId",
- count(*) as "count"
- from
- "ReportedWeapon"
- left join "GroupMatchMap" on "GroupMatchMap"."id" = "ReportedWeapon"."groupMatchMapId"
- left join "GroupMatch" on "GroupMatch"."id" = "GroupMatchMap"."matchId"
- where
- "ReportedWeapon"."userId" = @userId
- and "GroupMatch"."createdAt" between @starts and @ends
- group by "ReportedWeapon"."weaponSplId"
- order by "count" desc
-`);
-
-export function seasonReportedWeaponsByUserId({
- userId,
- season,
-}: {
- userId: number;
- season: number;
-}) {
- const { starts, ends } = Seasons.nthToDateRange(season);
-
- return stm.all({
- userId,
- starts: dateToDatabaseTimestamp(starts),
- ends: dateToDatabaseTimestamp(ends),
- }) as Array<{ weaponSplId: MainWeaponId; count: number }>;
-}
diff --git a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts
index 6bb6f7c78..f9ca57d89 100644
--- a/app/features/tournament-bracket/tournament-bracket-schemas.server.ts
+++ b/app/features/tournament-bracket/tournament-bracket-schemas.server.ts
@@ -9,6 +9,7 @@ import {
numericEnum,
safeJSONParse,
stageId,
+ weaponSplId,
} from "~/utils/zod";
import { TOURNAMENT } from "../tournament/tournament-constants";
import * as PickBan from "./core/PickBan";
@@ -102,6 +103,15 @@ export const matchSchema = z.union([
_action: _action("END_SET"),
winnerTeamId: z.preprocess(nullLiteraltoNull, id.nullable()),
}),
+ z.object({
+ _action: _action("REPORT_WEAPON"),
+ weaponSplId,
+ mapIndex: z.coerce.number().int().nonnegative(),
+ }),
+ z.object({
+ _action: _action("UNDO_WEAPON_REPORT"),
+ mapIndex: z.coerce.number().int().nonnegative(),
+ }),
]);
export const bracketIdx = z.coerce.number().int().min(0).max(100);
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 c0df3b420..6d31a4853 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
@@ -3,6 +3,7 @@ import { sql } from "~/db/sql";
import { TournamentMatchStatus } from "~/db/tables";
import { requireUser } from "~/features/auth/core/user.server";
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
+import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { endDroppedTeamMatches } from "~/features/tournament/tournament-utils.server";
@@ -736,6 +737,37 @@ export const action: ActionFunction = async ({ params, request }) => {
break;
}
+ case "REPORT_WEAPON": {
+ const isMemberOfATeamInTheMatch = match.players.some(
+ (p) => p.id === user.id,
+ );
+ errorToastIfFalsy(isMemberOfATeamInTheMatch, "Unauthorized");
+ errorToastIfFalsy(!tournament.ctx.isFinalized, "Tournament is finalized");
+
+ await ReportedWeaponRepository.upsertOneTournament({
+ tournamentMatchId: matchId,
+ mapIndex: data.mapIndex,
+ userId: user.id,
+ weaponSplId: data.weaponSplId,
+ });
+
+ break;
+ }
+ case "UNDO_WEAPON_REPORT": {
+ const isMemberOfATeamInTheMatch = match.players.some(
+ (p) => p.id === user.id,
+ );
+ errorToastIfFalsy(isMemberOfATeamInTheMatch, "Unauthorized");
+ errorToastIfFalsy(!tournament.ctx.isFinalized, "Tournament is finalized");
+
+ await ReportedWeaponRepository.deleteByUserMapIndexTournament({
+ tournamentMatchId: matchId,
+ userId: user.id,
+ mapIndex: data.mapIndex,
+ });
+
+ break;
+ }
default: {
assertUnreachable(data);
}
diff --git a/app/features/tournament-match/components/TournamentMatchActionTab.tsx b/app/features/tournament-match/components/TournamentMatchActionTab.tsx
index d7f3ff947..64b86d4c7 100644
--- a/app/features/tournament-match/components/TournamentMatchActionTab.tsx
+++ b/app/features/tournament-match/components/TournamentMatchActionTab.tsx
@@ -3,9 +3,21 @@ import { Undo2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useFetcher } from "react-router";
import { SendouButton } from "~/components/elements/Button";
+import { SendouTabPanel } from "~/components/elements/Tabs";
import { MatchActionTab } from "~/components/match-page/MatchActionTab";
+import { TAB_KEYS } from "~/components/match-page/MatchTabs";
+import {
+ WeaponReporter,
+ type WeaponReporterProps,
+} from "~/components/match-page/WeaponReporter";
+import { useUser } from "~/features/auth/core/user";
+import { useRecentlyReportedWeapons } from "~/features/sendouq/q-hooks";
import { useTournament } from "~/features/tournament/routes/to.$id";
-import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
+import type {
+ MainWeaponId,
+ ModeShort,
+ StageId,
+} from "~/modules/in-game-lists/types";
import { databaseTimestampToJavascriptTimestamp } from "~/utils/dates";
import type { CommonUser } from "~/utils/kysely.server";
import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server";
@@ -17,14 +29,28 @@ export function TournamentMatchActionTab({
ownTeamId,
}: {
data: TournamentMatchLoaderData;
- currentMap: { stageId: StageId; mode: ModeShort };
+ currentMap?: { stageId: StageId; mode: ModeShort };
ownTeamId: number;
}) {
const { t } = useTranslation(["q"]);
const tournament = useTournament();
+ const user = useUser();
const reportFetcher = useFetcher();
const undoFetcher = useFetcher();
+ const weaponReport = useTournamentWeaponReport({
+ data,
+ viewerUserId: user?.id,
+ });
+
+ if (!currentMap) {
+ return (
+
+ {weaponReport ? : null}
+
+ );
+ }
+
const opponentOneId = data.match.opponentOne!.id!;
const opponentTwoId = data.match.opponentTwo!.id!;
@@ -129,10 +155,74 @@ export function TournamentMatchActionTab({
{t("q:match.undoReport")}
}
+ weaponReport={weaponReport ?? undefined}
/>
);
}
+function useTournamentWeaponReport({
+ data,
+ viewerUserId,
+}: {
+ data: TournamentMatchLoaderData;
+ viewerUserId: number | undefined;
+}): WeaponReporterProps | null {
+ const weaponFetcher = useFetcher();
+ const { recentlyReportedWeapons, addRecentlyReportedWeapon } =
+ useRecentlyReportedWeapons();
+
+ if (viewerUserId === undefined) return null;
+
+ const isParticipant = data.match.players.some((p) => p.id === viewerUserId);
+ if (!isParticipant) return null;
+
+ const playOrderMaps = (data.mapList ?? []).filter(
+ (m) => !m.bannedByTournamentTeamId,
+ );
+ const reportedCount = data.results.length;
+ const weaponReportMaps = playOrderMaps
+ .slice(0, reportedCount + 1)
+ .map((m) => ({ stageId: m.stageId, mode: m.mode }));
+
+ if (weaponReportMaps.length === 0) return null;
+
+ const pastReported: MainWeaponId[] = data.reportedWeapons
+ ? data.reportedWeapons
+ .filter((w) => w.userId === viewerUserId)
+ .map((w) => w.weaponSplId)
+ : [];
+
+ return {
+ maps: weaponReportMaps,
+ pastReported,
+ quickSelectWeaponIds: recentlyReportedWeapons,
+ isSubmitting: weaponFetcher.state !== "idle",
+ onSubmit: (weaponSplId) => {
+ addRecentlyReportedWeapon(weaponSplId);
+ const mapIndex = pastReported.length;
+ weaponFetcher.submit(
+ {
+ _action: "REPORT_WEAPON",
+ weaponSplId: String(weaponSplId),
+ mapIndex: String(mapIndex),
+ },
+ { method: "post" },
+ );
+ },
+ onUndo: () => {
+ const mapIndex = pastReported.length - 1;
+ if (mapIndex < 0) return;
+ weaponFetcher.submit(
+ {
+ _action: "UNDO_WEAPON_REPORT",
+ mapIndex: String(mapIndex),
+ },
+ { method: "post" },
+ );
+ },
+ };
+}
+
function buildSetEndingData({
teams,
scores,
diff --git a/app/features/tournament-match/components/TournamentMatchTabs.tsx b/app/features/tournament-match/components/TournamentMatchTabs.tsx
index 83edbeb07..82f26d9f8 100644
--- a/app/features/tournament-match/components/TournamentMatchTabs.tsx
+++ b/app/features/tournament-match/components/TournamentMatchTabs.tsx
@@ -86,9 +86,14 @@ export function TournamentMatchTabs({
const hasReportedMaps = data.results.length > 0;
const hasPickBanEvents = data.pickBanEventCount > 0;
+ const isParticipant = data.match.players.some((p) => p.id === user?.id);
+ const canReportWeapons =
+ isParticipant && !tournament.ctx.isFinalized && hasReportedMaps;
+
const tabs = resolveVisibleTabs({
matchIsOver: data.matchIsOver,
canReportScore,
+ canReportWeapons,
canJoin: data.canJoin,
hasCurrentMap: Boolean(currentMap),
hasMissingActiveRoster,
@@ -136,13 +141,13 @@ export function TournamentMatchTabs({
teams={pickBanTeams}
turnOfResult={turnOfResult}
/>
- ) : currentMap ? (
+ ) : (
- ) : null
+ )
) : null}
{tabs.includes("admin") ? : null}
@@ -192,10 +197,24 @@ function resolveTimelineMaps(
customUrl: u.customUrl,
}));
- return data.results.map((result) => {
+ 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);
+
+ const weaponFor = (userId: number) =>
+ data.reportedWeapons?.find(
+ (w) => w.mapIndex === mapIndex && w.userId === userId,
+ )?.weaponSplId ?? null;
+
+ const alphaWeapons = alphaRoster.map((u) => weaponFor(u.id));
+ const bravoWeapons = bravoRoster.map((u) => weaponFor(u.id));
+ const hasAnyWeapon =
+ alphaWeapons.some((w) => w !== null) ||
+ bravoWeapons.some((w) => w !== null);
+
return {
stageId: result.stageId,
mode: result.mode,
@@ -205,9 +224,12 @@ function resolveTimelineMaps(
? ("ALPHA" as const)
: ("BRAVO" as const),
rosters: {
- alpha: resolveRoster(result.participants, opponentOneId),
- bravo: resolveRoster(result.participants, opponentTwoId),
+ alpha: alphaRoster,
+ bravo: bravoRoster,
},
+ weapons: hasAnyWeapon
+ ? { alpha: alphaWeapons, bravo: bravoWeapons }
+ : undefined,
points: hasPoints
? ([result.opponentOnePoints, result.opponentTwoPoints] as [
number,
@@ -485,6 +507,7 @@ function TournamentMatchRosterTab({
function resolveVisibleTabs({
matchIsOver,
canReportScore,
+ canReportWeapons,
canJoin,
hasCurrentMap,
hasMissingActiveRoster,
@@ -496,6 +519,7 @@ function resolveVisibleTabs({
}: {
matchIsOver: boolean;
canReportScore: boolean;
+ canReportWeapons: boolean;
canJoin: boolean;
hasCurrentMap: boolean;
hasMissingActiveRoster: boolean;
@@ -517,7 +541,8 @@ function resolveVisibleTabs({
if (
!leagueRoundLocked &&
(isPickBanStep ||
- (canReportScore && hasCurrentMap && !hasMissingActiveRoster))
+ (canReportScore && hasCurrentMap && !hasMissingActiveRoster) ||
+ canReportWeapons)
) {
tabs.push("action");
}
diff --git a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts
index 4fa7c2388..b93b192dc 100644
--- a/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts
+++ b/app/features/tournament-match/loaders/to.$id.matches.$mid.server.ts
@@ -4,6 +4,7 @@ import { getUser } from "~/features/auth/core/user.server";
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
import { chatAccessible } from "~/features/chat/chat-utils";
import * as RoomLinkRepository from "~/features/chat/RoomLinkRepository.server";
+import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { isLeagueRoundLocked } from "~/features/tournament/tournament-utils";
@@ -53,6 +54,9 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const results = findResultsByMatchId(matchId);
+ const reportedWeapons =
+ await ReportedWeaponRepository.findByTournamentMatchId(matchId);
+
const matchIsOver =
match.opponentOne?.result === "win" || match.opponentTwo?.result === "win";
@@ -219,6 +223,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
return {
match: hasPermsToSeeChat ? match : { ...match, chatCode: undefined },
results,
+ reportedWeapons,
mapList,
matchIsOver,
endedEarly,
diff --git a/app/features/user-page/loaders/u.$identifier.seasons.server.ts b/app/features/user-page/loaders/u.$identifier.seasons.server.ts
index bf92700b1..1262dae74 100644
--- a/app/features/user-page/loaders/u.$identifier.seasons.server.ts
+++ b/app/features/user-page/loaders/u.$identifier.seasons.server.ts
@@ -4,10 +4,10 @@ import * as LeaderboardRepository from "~/features/leaderboards/LeaderboardRepos
import * as SkillRepository from "~/features/mmr/SkillRepository.server";
import { userSkills as _userSkills } from "~/features/mmr/tiered.server";
import { seasonMapWinrateByUserId } from "~/features/sendouq/queries/seasonMapWinrateByUserId.server";
-import { seasonReportedWeaponsByUserId } from "~/features/sendouq/queries/seasonReportedWeaponsByUserId.server";
import { seasonSetWinrateByUserId } from "~/features/sendouq/queries/seasonSetWinrateByUserId.server";
import { seasonStagesByUserId } from "~/features/sendouq/queries/seasonStagesByUserId.server";
import { seasonsMatesEnemiesByUserId } from "~/features/sendouq/queries/seasonsMatesEnemiesByUserId.server";
+import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import type { SerializeFrom } from "~/utils/remix";
@@ -91,7 +91,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
: null,
weapons:
info === "weapons"
- ? seasonReportedWeaponsByUserId({ season, userId: user.id })
+ ? await ReportedWeaponRepository.seasonReportedWeaponsByUserId({ season, userId: user.id })
: null,
players:
info === "enemies" || info === "mates"
diff --git a/db-test.sqlite3 b/db-test.sqlite3
index 54cd492f1..f12b1b373 100644
Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ
diff --git a/migrations/134-match-page.js b/migrations/134-match-page.js
index d77e663ef..9d6b64f96 100644
--- a/migrations/134-match-page.js
+++ b/migrations/134-match-page.js
@@ -135,6 +135,53 @@ export function up(db) {
/* sql */ `create index group_match_continue_vote_group_id on "GroupMatchContinueVote"("groupId")`,
).run();
+ db.prepare(
+ /* sql */ `
+ create table "ReportedWeapon_new" (
+ "groupMatchMapId" integer,
+ "tournamentMatchId" integer,
+ "mapIndex" integer,
+ "weaponSplId" integer not null,
+ "userId" integer not null,
+ foreign key ("groupMatchMapId") references "GroupMatchMap"("id") on delete restrict,
+ foreign key ("tournamentMatchId") references "TournamentMatch"("id") on delete cascade,
+ foreign key ("userId") references "User"("id") on delete restrict,
+ unique("groupMatchMapId", "userId") on conflict rollback,
+ unique("tournamentMatchId", "mapIndex", "userId") on conflict rollback,
+ check (
+ ("groupMatchMapId" is not null and "tournamentMatchId" is null and "mapIndex" is null)
+ or
+ ("groupMatchMapId" is null and "tournamentMatchId" is not null and "mapIndex" is not null)
+ )
+ ) strict
+ `,
+ ).run();
+
+ db.prepare(
+ /* sql */ `
+ insert into "ReportedWeapon_new" (
+ "groupMatchMapId", "tournamentMatchId", "mapIndex", "weaponSplId", "userId"
+ )
+ select "groupMatchMapId", null, null, "weaponSplId", "userId"
+ from "ReportedWeapon"
+ `,
+ ).run();
+
+ db.prepare(/* sql */ `drop table "ReportedWeapon"`).run();
+ db.prepare(
+ /* sql */ `alter table "ReportedWeapon_new" rename to "ReportedWeapon"`,
+ ).run();
+
+ db.prepare(
+ /* sql */ `create index reported_weapon_group_match_map_id on "ReportedWeapon"("groupMatchMapId")`,
+ ).run();
+ db.prepare(
+ /* sql */ `create index reported_weapon_tournament_match_id on "ReportedWeapon"("tournamentMatchId")`,
+ ).run();
+ db.prepare(
+ /* sql */ `create index reported_weapon_user_id on "ReportedWeapon"("userId")`,
+ ).run();
+
db.pragma("foreign_key_check");
})();