From 948050193292bca33a327e7b54321d1104c797cc Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Mon, 22 Jun 2026 20:32:22 -0700 Subject: [PATCH] Unify majors: score once, fan out across windows + tennis bracket EV Make a "major" (golf/tennis/CS2) scored once on its canonical tournament and fan out to every linked sports_season window and league. Fan-out & completion (app/services/sync-tournament-results.ts): - syncTournamentResults now marks each synced window's event complete (gated by markComplete), recalculates affected leagues, and counts recalc failures so a stale league can't hide behind a "completed" badge - syncMajorFromPrimaryEvent promotes a primary window's derived results to canonical tournament_results (deleting rows for dropped placements) and fans out to siblings; fanOutMajorIfPrimary guards on the primary - placement removals now propagate (stale rows reset to filler) Primary-event model (scoring_events.is_primary, migration 0122): - getPrimaryEventForTournament / isReadOnlySibling / ensurePrimaryEvent / setPrimaryEvent; event creation auto-seeds a primary for bracket majors; "Make primary" button on the tournament page - per-window event/bracket/cs2 pages are read-only for non-primary linked events (not-participating stays editable) Tennis Grand Slam bracket (tennis_128 template + TEMPLATE_ROUND_CONFIG): - bracket-scored qualifying major via the existing bracket pipeline - simulator conditions in-progress EV on the real bracket (honoring completed matches, walkover for withdrawals), QP derived from config, round structure read from the template; CS2 + tennis share resolveStructureSource Backfill (scripts/backfill-major-linking.ts): one-time idempotent reconcile of existing majors (link orphans, designate primary, promote canonical, sync). Co-Authored-By: Claude Opus 4.8 --- app/lib/bracket-templates.ts | 26 + .../scoring-event-tournament-helpers.test.ts | 54 +- app/models/scoring-calculator.ts | 10 + app/models/scoring-event.ts | 91 + ...sons.$id.events.$eventId.bracket.server.ts | 34 +- ...-seasons.$id.events.$eventId.cs2-setup.tsx | 20 +- ...orts-seasons.$id.events.$eventId.server.ts | 36 + ...min.sports-seasons.$id.events.$eventId.tsx | 18 +- .../admin.sports-seasons.$id.events.server.ts | 21 +- app/routes/admin.tournaments.$id.tsx | 214 +- ....$sportsSeasonId.events.$eventId.server.ts | 74 +- .../__tests__/sync-tournament-results.test.ts | 315 +- .../__tests__/cs-major-simulator.test.ts | 45 + .../__tests__/tennis-simulator.test.ts | 196 +- .../simulations/cs-major-simulator.ts | 83 +- app/services/simulations/shared-major.ts | 88 + app/services/simulations/tennis-simulator.ts | 290 +- app/services/sync-tournament-results.ts | 235 +- database/schema.ts | 5 + drizzle/0122_soft_longshot.sql | 2 + drizzle/meta/0122_snapshot.json | 6741 +++++++++++++++++ drizzle/meta/_journal.json | 7 + scripts/backfill-major-linking.ts | 234 + 23 files changed, 8689 insertions(+), 150 deletions(-) create mode 100644 app/services/simulations/shared-major.ts create mode 100644 drizzle/0122_soft_longshot.sql create mode 100644 drizzle/meta/0122_snapshot.json create mode 100644 scripts/backfill-major-linking.ts diff --git a/app/lib/bracket-templates.ts b/app/lib/bracket-templates.ts index a46d0aa..94b3892 100644 --- a/app/lib/bracket-templates.ts +++ b/app/lib/bracket-templates.ts @@ -455,6 +455,31 @@ export const DARTS_128: BracketTemplate = { ], }; +/** + * Tennis Grand Slam (128-player single-elimination draw) + * R128 → R64 → R32 → Round of 16 → Quarterfinals → Semifinals → Final + * + * Qualifying-points sport: QP is derived from how far each player advances + * (see TEMPLATE_ROUND_CONFIG["tennis_128"] in scoring-calculator.ts). Scoring + * begins at the Round of 16 — R16 losers share placements 9–16; earlier rounds + * award nothing. Seeding: top 32 fixed, remaining 96 randomly drawn. + */ +export const TENNIS_128: BracketTemplate = { + id: "tennis_128", + name: "Tennis Grand Slam (128 Players)", + totalTeams: 128, + scoringStartsAtRound: "Round of 16", + rounds: [ + { name: "Round of 128", matchCount: 64, feedsInto: "Round of 64", isScoring: false }, + { name: "Round of 64", matchCount: 32, feedsInto: "Round of 32", isScoring: false }, + { name: "Round of 32", matchCount: 16, feedsInto: "Round of 16", isScoring: false }, + { name: "Round of 16", matchCount: 8, feedsInto: "Quarterfinals", isScoring: true }, // losers share 9th–16th + { name: "Quarterfinals", matchCount: 4, feedsInto: "Semifinals", isScoring: true }, // losers share 5th–8th + { name: "Semifinals", matchCount: 2, feedsInto: "Final", isScoring: true }, // losers share 3rd–4th + { name: "Final", matchCount: 1, feedsInto: null, isScoring: true }, // winner 1st, loser 2nd + ], +}; + /** * NCAA March Madness (68 teams) * First Four (play-in) → Round of 64 → Round of 32 → Sweet Sixteen → Elite Eight → Final Four → Championship @@ -923,6 +948,7 @@ export const BRACKET_TEMPLATES: Record = { afl_10: AFL_10, fifa_48: FIFA_48, darts_128: DARTS_128, + tennis_128: TENNIS_128, cfp_12: CFP_12, nba_20: NBA_20, }; diff --git a/app/models/__tests__/scoring-event-tournament-helpers.test.ts b/app/models/__tests__/scoring-event-tournament-helpers.test.ts index 460a110..c680b43 100644 --- a/app/models/__tests__/scoring-event-tournament-helpers.test.ts +++ b/app/models/__tests__/scoring-event-tournament-helpers.test.ts @@ -1,11 +1,23 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const mockFindMany = vi.fn(); +const mockFindFirst = vi.fn(); +// Records each db.update(...).set(patch).where(pred) call for assertions. +const updateCalls: Array<{ patch: unknown; where: unknown }> = []; +const mockUpdate = vi.fn(() => ({ + set: (patch: unknown) => ({ + where: (where: unknown) => { + updateCalls.push({ patch, where }); + return Promise.resolve(); + }, + }), +})); const mockDb = { query: { - scoringEvents: { findMany: mockFindMany }, + scoringEvents: { findMany: mockFindMany, findFirst: mockFindFirst }, }, + update: mockUpdate, }; vi.mock("~/database/context", () => ({ @@ -14,8 +26,10 @@ vi.mock("~/database/context", () => ({ vi.mock("~/database/schema", () => ({ scoringEvents: { + id: "se.id", sportsSeasonId: "se.sports_season_id", tournamentId: "se.tournament_id", + isPrimary: "se.is_primary", }, })); @@ -23,11 +37,14 @@ vi.mock("drizzle-orm", () => ({ eq: (col: unknown, val: unknown) => ({ type: "eq", col, val }), and: (...args: unknown[]) => ({ type: "and", args }), isNotNull: (col: unknown) => ({ type: "isNotNull", col }), + asc: (col: unknown) => ({ type: "asc", col }), })); import { getTournamentsBySportsSeason, getSportsSeasonsByTournament, + ensurePrimaryEvent, + setPrimaryEvent, } from "../scoring-event"; const SEASON_ID = "ss-1"; @@ -39,6 +56,7 @@ const mockTournamentB = { id: TOURNAMENT_ID_B, name: "US Open", year: 2026 }; beforeEach(() => { vi.clearAllMocks(); + updateCalls.length = 0; }); describe("getTournamentsBySportsSeason", () => { @@ -116,3 +134,37 @@ describe("getSportsSeasonsByTournament", () => { ); }); }); + +describe("ensurePrimaryEvent", () => { + it("sets the event primary when the tournament has no primary yet", async () => { + mockFindFirst.mockResolvedValue(undefined); // no existing primary + const result = await ensurePrimaryEvent(TOURNAMENT_ID_A, "ev-1"); + expect(result).toBe("ev-1"); + expect(updateCalls).toHaveLength(1); + expect(updateCalls[0].patch).toMatchObject({ isPrimary: true }); + }); + + it("is idempotent: keeps the existing primary and writes nothing", async () => { + mockFindFirst.mockResolvedValue({ id: "ev-existing" }); + const result = await ensurePrimaryEvent(TOURNAMENT_ID_A, "ev-2"); + expect(result).toBe("ev-existing"); + expect(updateCalls).toHaveLength(0); + }); +}); + +describe("setPrimaryEvent", () => { + it("clears siblings then sets the chosen event primary", async () => { + mockFindFirst.mockResolvedValue({ id: "ev-3", tournamentId: TOURNAMENT_ID_A }); + await setPrimaryEvent("ev-3"); + // Two updates: clear-all (isPrimary:false) then set-one (isPrimary:true). + expect(updateCalls).toHaveLength(2); + expect(updateCalls[0].patch).toMatchObject({ isPrimary: false }); + expect(updateCalls[1].patch).toMatchObject({ isPrimary: true }); + }); + + it("throws when the event is not linked to a tournament", async () => { + mockFindFirst.mockResolvedValue({ id: "ev-4", tournamentId: null }); + await expect(setPrimaryEvent("ev-4")).rejects.toThrow(/not linked to a tournament/); + expect(updateCalls).toHaveLength(0); + }); +}); diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index 4b28bf6..6e6b556 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -112,6 +112,16 @@ const TEMPLATE_ROUND_CONFIG: Record> // 3rd place game finalizes both positions distinctly. "Third Place Game": { loserPosition: 4, loserIsPartial: false, winnerFloor: null, winnerPosition: 3 }, }, + tennis_128: { + // R16 losers share 9th–16th; winner advances to QF (floor 5th–8th). + "Round of 16": { loserPosition: 9, loserIsPartial: false, winnerFloor: 5 }, + // QF losers share 5th–8th; winner advances to SF (floor 3rd–4th). + Quarterfinals: { loserPosition: 5, loserIsPartial: false, winnerFloor: 3 }, + // SF losers share 3rd–4th; winner advances to Final (floor 2nd). + Semifinals: { loserPosition: 3, loserIsPartial: false, winnerFloor: 2 }, + // Final finalizes both: winner 1st, loser 2nd. + Final: { loserPosition: 2, loserIsPartial: false, winnerFloor: null }, + }, }; /** diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 80e45f4..1525e58 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -1067,3 +1067,94 @@ export async function getSportsSeasonsByTournament(tournamentId: string) { return true; }); } + +/** + * The primary scoring event for a tournament — the single window where the admin + * builds the bracket/stages and scoring happens; its results fan out to siblings. + * Returns the explicitly-flagged primary, falling back to the earliest-created + * linked event so callers (league/sim loaders) always have a structure source + * even before a primary is designated. + */ +export async function getPrimaryEventForTournament( + tournamentId: string, + providedDb?: ReturnType +) { + const db = providedDb || database(); + const primary = await db.query.scoringEvents.findFirst({ + where: and( + eq(schema.scoringEvents.tournamentId, tournamentId), + eq(schema.scoringEvents.isPrimary, true) + ), + }); + if (primary) return primary; + + return db.query.scoringEvents.findFirst({ + where: eq(schema.scoringEvents.tournamentId, tournamentId), + orderBy: asc(schema.scoringEvents.createdAt), + }); +} + +/** + * A tournament-linked, non-primary event is a read-only mirror: its results are + * owned by the shared major's primary window and fan out to it. Direct scoring + * on such an event must be rejected so windows can't diverge. Single source of + * truth for that predicate, shared by the admin route guards and the loaders. + */ +export function isReadOnlySibling(event: { + tournamentId: string | null; + isPrimary: boolean; +}): boolean { + return !!event.tournamentId && !event.isPrimary; +} + +/** + * Make `eventId` the single primary window for its tournament — set it primary + * and clear the flag on every sibling. Used by the "Make this the primary + * window" admin action and to seed a primary when the first bracket-major event + * is created. + */ +export async function setPrimaryEvent( + eventId: string, + providedDb?: ReturnType +) { + const db = providedDb || database(); + const event = await db.query.scoringEvents.findFirst({ + where: eq(schema.scoringEvents.id, eventId), + }); + if (!event?.tournamentId) { + throw new Error(`Event ${eventId} is not linked to a tournament`); + } + await db + .update(schema.scoringEvents) + .set({ isPrimary: false, updatedAt: new Date() }) + .where(eq(schema.scoringEvents.tournamentId, event.tournamentId)); + await db + .update(schema.scoringEvents) + .set({ isPrimary: true, updatedAt: new Date() }) + .where(eq(schema.scoringEvents.id, eventId)); +} + +/** + * Designate `eventId` as the tournament's primary window only if none is set + * yet. Idempotent: returns the existing primary's id when one already exists, so + * creating/linking additional windows never steals primary from the first. + */ +export async function ensurePrimaryEvent( + tournamentId: string, + eventId: string, + providedDb?: ReturnType +): Promise { + const db = providedDb || database(); + const existing = await db.query.scoringEvents.findFirst({ + where: and( + eq(schema.scoringEvents.tournamentId, tournamentId), + eq(schema.scoringEvents.isPrimary, true) + ), + }); + if (existing) return existing.id; + await db + .update(schema.scoringEvents) + .set({ isPrimary: true, updatedAt: new Date() }) + .where(eq(schema.scoringEvents.id, eventId)); + return eventId; +} diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index ee227ab..dd15217 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -2,7 +2,7 @@ import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.br import { findSportsSeasonById } from "~/models/sports-season"; import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; -import { getScoringEventById, updateScoringEvent } from "~/models/scoring-event"; +import { getScoringEventById, updateScoringEvent, isReadOnlySibling } from "~/models/scoring-event"; import { findPlayoffMatchesByEventId, generateBracketFromTemplate, @@ -61,6 +61,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq } from "drizzle-orm"; import { maybeResolveCompletedBracktForSportsSeason } from "~/services/brackt.server"; +import { fanOutMajorIfPrimary } from "~/services/sync-tournament-results"; export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); @@ -120,7 +121,13 @@ export async function loader({ params }: Route.LoaderArgs) { * fantasy placements come from finalizeQualifyingPoints across all majors. */ async function scoreQualifyingBracket( - event: { id: string; sportsSeasonId: string; name: string | null }, + event: { + id: string; + sportsSeasonId: string; + name: string | null; + isPrimary: boolean; + tournamentId: string | null; + }, db: ReturnType, recalcOptions?: Parameters[2] ): Promise { @@ -130,12 +137,28 @@ async function scoreQualifyingBracket( db, recalcOptions ?? { eventId: event.id, eventName: event.name ?? undefined } ); + // If this is the shared major's primary window, propagate to siblings. + // Mid-tournament (a single round): don't mark complete yet. + await fanOutMajorIfPrimary(event, { markComplete: false }); } export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); + // Brackets are built/scored only on the major's primary window. A + // tournament-linked, non-primary event is a read-only mirror that receives + // results via fan-out, so reject every mutating action here. + { + const ev = await getScoringEventById(params.eventId); + if (ev && isReadOnlySibling(ev)) { + return { + error: + "This bracket belongs to a shared major. Build and score it on the primary window (linked from Admin → Tournaments); results fan out here automatically.", + }; + } + } + if (intent === "generate-bracket") { const templateId = formData.get("templateId"); @@ -474,6 +497,9 @@ export async function action({ request, params }: Route.ActionArgs) { // qualifying majors, which are scored via processQualifyingBracketEvent above. if (!event.isQualifyingEvent) { await autoCompleteRoundIfDone(event.id, round, event.sportsSeasonId, db); + } else { + // Shared major primary window: propagate this round to siblings. + await fanOutMajorIfPrimary(event, { markComplete: false }); } } @@ -637,6 +663,8 @@ export async function action({ request, params }: Route.ActionArgs) { // skipDiscord: reprocess is a data-correction tool, not a result announcement. await recalculateAffectedLeagues(event.sportsSeasonId, db, { skipDiscord: true }); } + // Re-propagate corrected QP to sibling windows (data-correction; not final). + await fanOutMajorIfPrimary(event, { markComplete: false }); return { success: `Reprocessed qualifying bracket: cleared stale fantasy points and recomputed QP (${completed.length} completed match(es)).`, }; @@ -766,6 +794,8 @@ export async function action({ request, params }: Route.ActionArgs) { eventId: params.eventId, eventName: event.name ?? undefined, }); + // Finalize: propagate to siblings AND mark every window complete. + await fanOutMajorIfPrimary(event, { markComplete: true }); return { success: "Major bracket finalized — qualifying points awarded." }; } diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx index 76dff71..c9c99be 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx @@ -2,7 +2,7 @@ import { Form, useLoaderData, useActionData, useNavigation, useFetcher, Link } f import type { Route } from './+types/admin.sports-seasons.$id.events.$eventId.cs2-setup'; import { findSportsSeasonById } from '~/models/sports-season'; -import { getScoringEventById } from '~/models/scoring-event'; +import { getScoringEventById, isReadOnlySibling } from '~/models/scoring-event'; import { findParticipantsBySportsSeasonId } from '~/models/season-participant'; import { getCs2StageResultsForEvent, @@ -14,6 +14,7 @@ import { } from '~/models/cs2-major-stage'; import { findSeasonMatchesByScoringEventId } from '~/models/season-match'; import { syncMatches } from '~/services/match-sync'; +import { fanOutMajorIfPrimary } from '~/services/sync-tournament-results'; import { Button } from '~/components/ui/button'; import { Card, @@ -61,7 +62,20 @@ export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const intent = formData.get('intent') as string; + // CS2 stages are set up only on the major's primary window; a tournament-linked + // non-primary event is a read-only mirror fed by fan-out. + const guardEvent = await getScoringEventById(eventId); + if (guardEvent && isReadOnlySibling(guardEvent)) { + return { + success: false, + message: + 'This CS2 major is set up on its primary window; results fan out here automatically.', + }; + } + if (intent === 'reset') { + // Local recovery tool: clears only the primary's torn state. Siblings are not + // touched here — re-entering results re-propagates via the scoring fan-out. await resetCs2Event(eventId, params.id); return { success: true, message: 'Stage assignments and recorded results cleared.' }; } @@ -126,6 +140,10 @@ export async function action({ request, params }: Route.ActionArgs) { } await markCs2StageEliminations(eventId, eliminations); await assignCs2EliminationQP(eventId, params.id); + // Propagate the derived QP/placements to every sibling window (in-progress: + // don't mark complete — that happens when the bracket is finalized). + const event = await getScoringEventById(eventId); + if (event) await fanOutMajorIfPrimary(event, { markComplete: false }); return { success: true, message: `Marked ${eliminations.length} eliminations.` }; } catch (err) { return { success: false, message: err instanceof Error ? err.message : 'Failed to save eliminations.' }; diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts index 34cb0d0..6653bf1 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts @@ -6,6 +6,7 @@ import { getScoringEventById, completeScoringEvent, updateScoringEvent, + isReadOnlySibling, } from "~/models/scoring-event"; import { getEventResults, @@ -77,6 +78,9 @@ export async function loader({ params }: Route.LoaderArgs) { ) : new Set(); + // A tournament-linked event that is NOT the primary is a read-only mirror: + // scoring happens once on the canonical tournament (or its primary window) and + // fans out here. Surface this so the UI can point the admin to the right place. return { sportsSeason: sportsSeason as typeof sportsSeason & { sport: { id: string; name: string; type: string; slug: string; simulatorType: string | null }; @@ -89,13 +93,45 @@ export async function loader({ params }: Route.LoaderArgs) { qpConfig, qpStandings, notParticipatingIds, + isReadOnlySibling: isReadOnlySibling(event), + canonicalTournamentId: event.tournamentId, }; } +// Scoring intents that mutate this window's results directly. For a read-only +// sibling (tournament-linked, non-primary) these are rejected — score on the +// canonical tournament page instead, and the result fans out automatically. +// +// mark/unmark-not-participating are deliberately NOT here: they are per-window +// simulator inputs (exclude a participant from this window's EV draws), have no +// canonical-tournament equivalent, and must stay editable on every window. +const SCORING_INTENTS = new Set([ + "process-qp", + "complete", + "uncomplete", + "add-result", + "update-result", + "delete-result", + "update-standings", + "batch-add-results", +]); + export async function action({ request, params }: Route.ActionArgs) { const formData = await request.formData(); const intent = formData.get("intent"); + // Enforce single-source scoring: reject direct scoring on a read-only sibling. + if (typeof intent === "string" && SCORING_INTENTS.has(intent)) { + const ev = await getScoringEventById(params.eventId); + if (ev && isReadOnlySibling(ev)) { + return { + error: + "This event belongs to a shared major. Score it once on the tournament page (Admin → Tournaments) and it fans out to every window automatically.", + canonicalTournamentId: ev.tournamentId, + }; + } + } + if (intent === "mark-qualifying") { try { const event = await getScoringEventById(params.eventId); diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx index a529cd3..57db509 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx @@ -95,9 +95,24 @@ export default function EventResults({ loaderData, actionData, }: Route.ComponentProps) { - const { sportsSeason, event, participants, results, participantResults, seasonResults, notParticipatingIds } = loaderData; + const { sportsSeason, event, participants, results, participantResults, seasonResults, notParticipatingIds, isReadOnlySibling, canonicalTournamentId } = loaderData; const [hasChanges, setHasChanges] = useState(false); + // Banner shown on read-only sibling events: scoring lives on the canonical + // tournament page, and results fan out here automatically. + const readOnlySiblingBanner = isReadOnlySibling ? ( +
+ This event is part of a shared major. Scoring is done once on the{" "} + + tournament page + {" "} + and fans out to every window automatically — the controls here are read-only. +
+ ) : null; + const isBracketEvent = event.eventType === "playoff_game" || isBracketMajor(sportsSeason.sport?.simulatorType); // Create a map of participants with results for easy lookup @@ -218,6 +233,7 @@ export default function EventResults({ return (
+ {readOnlySiblingBanner}
-
-

{tournament.name}

- - {tournament.status} - +
+
+

{tournament.name}

+ + {tournament.status} + +
+ {primaryScoringHref && ( + + )}

{tournament.year} @@ -313,17 +390,36 @@ export default function AdminTournamentDetail({ {link.sportsSeason.scoringType.replace("_", " ")}

- - {link.sportsSeason.status} - +
+ {bracketMajor && + (link.isPrimary ? ( + Primary + ) : ( + + + + + + ))} + + {link.sportsSeason.status} + +
))}
@@ -382,17 +478,43 @@ export default function AdminTournamentDetail({ - ({ - id: p.id, - name: p.name, - }))} - sportsSeasonId="" - existingResultParticipantIds={ - new Set(results.map((r) => r.participantId)) - } - intent="batch-upsert-results" - /> + {bracketMajor ? ( + + + Scoring + + This is a bracket major. Results are derived from the + bracket/stage workflow on the primary window and fan out here + automatically. + + + + {primaryScoringHref ? ( + + ) : ( +

+ No primary window is set up yet for this tournament. +

+ )} +
+
+ ) : ( + ({ + id: p.id, + name: p.name, + }))} + sportsSeasonId="" + existingResultParticipantIds={ + new Set(results.map((r) => r.participantId)) + } + intent="batch-upsert-results" + /> + )}
diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server.ts b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server.ts index abcb194..b40feb0 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server.ts +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server.ts @@ -8,7 +8,8 @@ import { getUserDisplayName, } from "~/models"; import { getDraftPicks } from "~/models/draft-pick"; -import { getScoringEventById } from "~/models/scoring-event"; +import { getScoringEventById, getPrimaryEventForTournament, isReadOnlySibling } from "~/models/scoring-event"; +import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; import { getCs2StageResultsForEvent } from "~/models/cs2-major-stage"; import { findSeasonMatchesByScoringEventId } from "~/models/season-match"; import { getEventResults } from "~/models/event-result"; @@ -99,6 +100,55 @@ export async function loader({ params, request }: Route.LoaderArgs) { const simulatorType = sportsSeason.sport?.simulatorType ?? null; + // Shared-major fan-out for DISPLAY: a tournament-linked, non-primary event has + // no bracket/stage rows of its own — those live on the primary window. Read the + // structure from the primary, then re-key this league's ownership overlay from + // its own season_participant ids to the PRIMARY's season_participant ids (the + // ids the bracket structure uses) via the shared canonical participant. QP/ + // results branches keep using this window's local rows and are untouched. + let structureEventId = eventId; + let structureBracketTemplateId = scoringEvent.bracketTemplateId; + let bracketTeamOwnerships = teamOwnerships; + let bracketUserParticipantIds = userParticipantIds; + + if (isReadOnlySibling(scoringEvent) && scoringEvent.tournamentId) { + const primaryEvent = await getPrimaryEventForTournament(scoringEvent.tournamentId); + if (primaryEvent && primaryEvent.id !== eventId) { + structureEventId = primaryEvent.id; + structureBracketTemplateId = primaryEvent.bracketTemplateId; + + const [siblingParticipants, primaryParticipants] = await Promise.all([ + findParticipantsBySportsSeasonId(sportsSeasonId), + findParticipantsBySportsSeasonId(primaryEvent.sportsSeasonId), + ]); + // sibling season_participant id → canonical participant id + const siblingSpToCanonical = new Map( + siblingParticipants + .filter((p) => p.participantId) + .map((p) => [p.id, p.participantId as string]) + ); + // canonical participant id → primary season_participant id + const canonicalToPrimarySp = new Map( + primaryParticipants + .filter((p) => p.participantId) + .map((p) => [p.participantId as string, p.id]) + ); + const toPrimarySp = (siblingSpId: string): string | undefined => { + const canonical = siblingSpToCanonical.get(siblingSpId); + return canonical ? canonicalToPrimarySp.get(canonical) : undefined; + }; + + bracketTeamOwnerships = teamOwnerships.flatMap((o) => { + const primarySpId = toPrimarySp(o.participantId); + return primarySpId ? [{ ...o, participantId: primarySpId }] : []; + }); + bracketUserParticipantIds = userParticipantIds.flatMap((id) => { + const primarySpId = toPrimarySp(id); + return primarySpId ? [primarySpId] : []; + }); + } + } + type PlayoffMatchWithRelations = Omit & { createdAt: string; updatedAt: string; @@ -118,10 +168,10 @@ export async function loader({ params, request }: Route.LoaderArgs) { // CS2 Major — load Swiss stage data + bracket if (simulatorType === "cs2_major_qualifying_points") { const [cs2StageResults, seasonMatches, playoffMatchRows] = await Promise.all([ - getCs2StageResultsForEvent(eventId), - findSeasonMatchesByScoringEventId(eventId), + getCs2StageResultsForEvent(structureEventId), + findSeasonMatchesByScoringEventId(structureEventId), db.query.playoffMatches.findMany({ - where: eq(schema.playoffMatches.scoringEventId, eventId), + where: eq(schema.playoffMatches.scoringEventId, structureEventId), with: { participant1: true, participant2: true, winner: true, loser: true }, }), ]); @@ -131,7 +181,7 @@ export async function loader({ params, request }: Route.LoaderArgs) { createdAt: m.createdAt.toISOString(), updatedAt: m.updatedAt.toISOString(), })); - const templateId = scoringEvent.bracketTemplateId ?? undefined; + const templateId = structureBracketTemplateId ?? undefined; const template = templateId ? getBracketTemplate(templateId) : undefined; const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template); @@ -140,8 +190,8 @@ export async function loader({ params, request }: Route.LoaderArgs) { league, sportsSeason, scoringEvent, - teamOwnerships, - userParticipantIds, + teamOwnerships: bracketTeamOwnerships, + userParticipantIds: bracketUserParticipantIds, cs2StageResults, seasonMatches: seasonMatches.map((m) => ({ ...m, @@ -158,7 +208,7 @@ export async function loader({ params, request }: Route.LoaderArgs) { // Bracket events (tennis/golf bracket majors, soccer/UCL group+bracket) if (scoringEvent.eventType === "playoff_game") { const playoffMatchRows = await db.query.playoffMatches.findMany({ - where: eq(schema.playoffMatches.scoringEventId, eventId), + where: eq(schema.playoffMatches.scoringEventId, structureEventId), with: { participant1: true, participant2: true, winner: true, loser: true }, }); const playoffMatches: PlayoffMatchWithRelations[] = playoffMatchRows.map((m) => ({ @@ -166,13 +216,13 @@ export async function loader({ params, request }: Route.LoaderArgs) { createdAt: m.createdAt.toISOString(), updatedAt: m.updatedAt.toISOString(), })); - const templateId = scoringEvent.bracketTemplateId ?? undefined; + const templateId = structureBracketTemplateId ?? undefined; const template = templateId ? getBracketTemplate(templateId) : undefined; const playoffRounds = getOrderedRoundsFromMatches(playoffMatches, template); let groupStandings: GroupStandingData[] = []; if (template?.groupStage) { - const groups = await findGroupsByEventId(eventId); + const groups = await findGroupsByEventId(structureEventId); const matchesByGroupId = await findMatchesByGroupIds(groups.map((g) => g.id)); groupStandings = groups.map((group) => { const groupMatches = matchesByGroupId.get(group.id) ?? []; @@ -216,8 +266,8 @@ export async function loader({ params, request }: Route.LoaderArgs) { league, sportsSeason, scoringEvent, - teamOwnerships, - userParticipantIds, + teamOwnerships: bracketTeamOwnerships, + userParticipantIds: bracketUserParticipantIds, playoffMatches, playoffRounds, bracketTemplateId: templateId ?? null, diff --git a/app/services/__tests__/sync-tournament-results.test.ts b/app/services/__tests__/sync-tournament-results.test.ts index 60dd323..c0a6cdc 100644 --- a/app/services/__tests__/sync-tournament-results.test.ts +++ b/app/services/__tests__/sync-tournament-results.test.ts @@ -7,11 +7,38 @@ vi.mock("~/database/context", () => ({ vi.mock("~/models/scoring-calculator", () => ({ processQualifyingEvent: vi.fn(), + recalculateAffectedLeagues: vi.fn(), })); -import { syncTournamentResults } from "../sync-tournament-results"; +vi.mock("~/models/scoring-event", () => ({ + completeScoringEvent: vi.fn(), + getScoringEventById: vi.fn(), +})); + +vi.mock("~/models/event-result", () => ({ + getEventResults: vi.fn(), +})); + +vi.mock("~/models/tournament-result", () => ({ + upsertTournamentResult: vi.fn(), +})); + +vi.mock("~/lib/logger", () => ({ + logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +import { + syncTournamentResults, + syncMajorFromPrimaryEvent, +} from "../sync-tournament-results"; import { database } from "~/database/context"; -import { processQualifyingEvent } from "~/models/scoring-calculator"; +import { + processQualifyingEvent, + recalculateAffectedLeagues, +} from "~/models/scoring-calculator"; +import { completeScoringEvent, getScoringEventById } from "~/models/scoring-event"; +import { getEventResults } from "~/models/event-result"; +import { upsertTournamentResult } from "~/models/tournament-result"; // --------------------------------------------------------------------------- // Mock data types @@ -28,6 +55,7 @@ interface FakeScoringEvent { id: string; sportsSeasonId: string; tournamentId: string | null; + name?: string | null; } interface FakeSeasonParticipant { @@ -574,4 +602,287 @@ describe("syncTournamentResults", () => { expect(c?.placement).toBeNull(); expect(c?.qualifyingPointsAwarded).toBe("0"); }); + + // ------------------------------------------------------------------------- + // Test 7: Completion fan-out (the core "score once" fix) + // ------------------------------------------------------------------------- + + it("marks each synced window complete by default", async () => { + const state = seedBasicState({ + tournamentResults: [ + { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, + ], + scoringEvents: [ + { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, + { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, + ], + seasonParticipants: [ + { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, + { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, + ], + }); + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); + vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); + + await syncTournamentResults("t-1"); + + expect(completeScoringEvent).toHaveBeenCalledTimes(2); + expect(completeScoringEvent).toHaveBeenCalledWith("ev-A", db); + expect(completeScoringEvent).toHaveBeenCalledWith("ev-B", db); + }); + + it("does NOT mark windows complete when markComplete is false", async () => { + const state = seedBasicState({ + tournamentResults: [ + { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, + ], + scoringEvents: [{ id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }], + seasonParticipants: [ + { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, + ], + }); + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); + + await syncTournamentResults("t-1", { markComplete: false }); + + expect(completeScoringEvent).not.toHaveBeenCalled(); + }); + + // ------------------------------------------------------------------------- + // Test 8: League recalculation per synced window + // ------------------------------------------------------------------------- + + it("recalculates affected leagues once per cleanly-synced window", async () => { + const state = seedBasicState({ + tournamentResults: [ + { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, + ], + scoringEvents: [ + { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1", name: "Major A" }, + { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1", name: "Major B" }, + ], + seasonParticipants: [ + { id: "sp-AA", sportsSeasonId: "ss-A", participantId: "cp-A", name: "A" }, + { id: "sp-BA", sportsSeasonId: "ss-B", participantId: "cp-A", name: "A" }, + ], + }); + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); + vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); + + await syncTournamentResults("t-1"); + + expect(recalculateAffectedLeagues).toHaveBeenCalledTimes(2); + expect(recalculateAffectedLeagues).toHaveBeenCalledWith("ss-A", undefined, { + eventId: "ev-A", + eventName: "Major A", + skipDiscord: false, + }); + expect(recalculateAffectedLeagues).toHaveBeenCalledWith("ss-B", undefined, { + eventId: "ev-B", + eventName: "Major B", + skipDiscord: false, + }); + }); + + // ------------------------------------------------------------------------- + // Test 9: skipEventId excludes the primary window + // ------------------------------------------------------------------------- + + it("skips the primary window when skipEventId is given", async () => { + const state = seedBasicState({ + tournamentResults: [ + { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, + ], + scoringEvents: [ + { id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1" }, + { id: "ev-SIBLING", sportsSeasonId: "ss-S", tournamentId: "t-1" }, + ], + seasonParticipants: [ + { id: "sp-PA", sportsSeasonId: "ss-P", participantId: "cp-A", name: "A" }, + { id: "sp-SA", sportsSeasonId: "ss-S", participantId: "cp-A", name: "A" }, + ], + }); + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); + vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); + + const report = await syncTournamentResults("t-1", { + skipEventId: "ev-PRIMARY", + }); + + expect(report.windowsSynced).toBe(1); + expect(processQualifyingEvent).toHaveBeenCalledTimes(1); + expect(processQualifyingEvent).toHaveBeenCalledWith("ev-SIBLING", db); + // No event_results written for the skipped primary window. + expect( + state.eventResults.some((r) => r.scoringEventId === "ev-PRIMARY") + ).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// syncMajorFromPrimaryEvent: bracket/CS2 majors promote primary results to +// canonical tournament_results, then fan out to siblings (primary skipped). +// --------------------------------------------------------------------------- + +describe("syncMajorFromPrimaryEvent", () => { + it("promotes primary event_results to canonical and fans out to siblings", async () => { + const state = seedBasicState({ + scoringEvents: [ + { id: "ev-PRIMARY", sportsSeasonId: "ss-P", tournamentId: "t-1", name: "Major" }, + { id: "ev-SIBLING", sportsSeasonId: "ss-S", tournamentId: "t-1", name: "Major" }, + ], + seasonParticipants: [ + { id: "sp-PA", sportsSeasonId: "ss-P", participantId: "cp-A", name: "A" }, + { id: "sp-PB", sportsSeasonId: "ss-P", participantId: "cp-B", name: "B" }, + { id: "sp-SA", sportsSeasonId: "ss-S", participantId: "cp-A", name: "A" }, + { id: "sp-SB", sportsSeasonId: "ss-S", participantId: "cp-B", name: "B" }, + ], + }); + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); + vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); + + vi.mocked(getScoringEventById).mockResolvedValue({ + id: "ev-PRIMARY", + sportsSeasonId: "ss-P", + tournamentId: "t-1", + name: "Major", + } as never); + + // Primary window's derived results: cp-A 1st, cp-B 2nd. + vi.mocked(getEventResults).mockResolvedValue([ + { + placement: 1, + rawScore: "1", + notParticipating: false, + seasonParticipantId: "sp-PA", + seasonParticipant: { participantId: "cp-A" }, + }, + { + placement: 2, + rawScore: "2", + notParticipating: false, + seasonParticipantId: "sp-PB", + seasonParticipant: { participantId: "cp-B" }, + }, + ] as never); + + // upsertTournamentResult writes into our fake canonical table so the + // subsequent syncTournamentResults can read them back. + vi.mocked(upsertTournamentResult).mockImplementation( + async (data: { tournamentId: string; participantId: string; placement?: number | null; rawScore?: string | null }) => { + state.tournamentResults.push({ + tournamentId: data.tournamentId, + participantId: data.participantId, + placement: data.placement ?? null, + rawScore: data.rawScore ?? null, + }); + return data as never; + } + ); + + const report = await syncMajorFromPrimaryEvent("ev-PRIMARY"); + + // Two canonical results promoted from the primary. + expect(upsertTournamentResult).toHaveBeenCalledTimes(2); + expect(state.tournamentResults).toHaveLength(2); + + // Only the sibling window is synced (primary is skipped). + expect(report.windowsSynced).toBe(1); + expect( + state.eventResults.some((r) => r.scoringEventId === "ev-PRIMARY") + ).toBe(false); + const sib = state.eventResults.filter((r) => r.scoringEventId === "ev-SIBLING"); + expect(sib.find((r) => r.seasonParticipantId === "sp-SA")?.placement).toBe(1); + expect(sib.find((r) => r.seasonParticipantId === "sp-SB")?.placement).toBe(2); + + // In-progress fan-out: siblings are NOT marked complete by default. + expect(completeScoringEvent).not.toHaveBeenCalled(); + }); + + it("throws when the primary event is not linked to a tournament", async () => { + const db = makeFakeDb(seedBasicState()); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(getScoringEventById).mockResolvedValue({ + id: "ev-X", + sportsSeasonId: "ss-X", + tournamentId: null, + name: "Standalone", + } as never); + + await expect(syncMajorFromPrimaryEvent("ev-X")).rejects.toThrow( + /not linked to a tournament/ + ); + }); +}); + +// --------------------------------------------------------------------------- +// Regression: review fixes #1 (recalc failure counts) and #2 (stale removal) +// --------------------------------------------------------------------------- + +describe("syncTournamentResults — recalc failure & stale removal", () => { + it("counts a league-recalc failure as a failed window (so status can't show completed)", async () => { + const state = seedBasicState({ + tournamentResults: [ + { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: null }, + ], + scoringEvents: [{ id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }], + seasonParticipants: [ + { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, + ], + }); + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); + vi.mocked(recalculateAffectedLeagues).mockRejectedValue(new Error("recalc boom")); + + const report = await syncTournamentResults("t-1"); + + expect(report.windowsFailed).toBe(1); + expect(report.failures.some((f) => f.error.includes("League recalc failed"))).toBe(true); + }); + + it("resets a previously-placed participant to filler when dropped from canonical", async () => { + const state = seedBasicState({ + tournamentResults: [ + { tournamentId: "t-1", participantId: "cp-A", placement: 1, rawScore: "10" }, + ], + scoringEvents: [{ id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }], + seasonParticipants: [ + { id: "sp-A", sportsSeasonId: "ss-1", participantId: "cp-A", name: "A" }, + ], + }); + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + vi.mocked(completeScoringEvent).mockResolvedValue(undefined as never); + vi.mocked(recalculateAffectedLeagues).mockResolvedValue(undefined); + + await syncTournamentResults("t-1"); + expect(state.eventResults.find((r) => r.seasonParticipantId === "sp-A")?.placement).toBe(1); + + // Correction: cp-A is removed from the canonical results entirely. + state.tournamentResults.length = 0; + await syncTournamentResults("t-1"); + + const row = state.eventResults.find((r) => r.seasonParticipantId === "sp-A"); + expect(row?.placement).toBeNull(); + expect(row?.rawScore).toBeNull(); + expect(row?.qualifyingPointsAwarded).toBe("0"); + }); }); diff --git a/app/services/simulations/__tests__/cs-major-simulator.test.ts b/app/services/simulations/__tests__/cs-major-simulator.test.ts index 4a32d0e..9f06c7e 100644 --- a/app/services/simulations/__tests__/cs-major-simulator.test.ts +++ b/app/services/simulations/__tests__/cs-major-simulator.test.ts @@ -9,6 +9,7 @@ import { simulateOneMajor, reconcileAdvancers, } from "../cs-major-simulator"; +import { buildSpTranslator } from "../shared-major"; import type { AdvancedTeam, BracketMatchInput, SwissMatchInput, StageResultsMap } from "../cs-major-simulator"; // ─── Helpers ────────────────────────────────────────────────────────────────── @@ -840,3 +841,47 @@ describe("simulateOneMajor full conditioning", () => { } }); }); + +// ─── buildSpTranslator (shared-major id bridging) ─────────────────────────────── + +describe("buildSpTranslator", () => { + const primary = [ + { id: "P-1", participantId: "C-A" }, + { id: "P-2", participantId: "C-B" }, + { id: "P-3", participantId: "C-C" }, + ]; + const local = [ + { id: "L-1", participantId: "C-A" }, + { id: "L-2", participantId: "C-B" }, + { id: "L-3", participantId: "C-C" }, + ]; + + it("maps primary season_participant ids to the local window's via canonical", () => { + const tr = buildSpTranslator(primary, local); + expect(tr("P-1")).toBe("L-1"); + expect(tr("P-2")).toBe("L-2"); + expect(tr("P-3")).toBe("L-3"); + }); + + it("passes null/undefined through unchanged", () => { + const tr = buildSpTranslator(primary, local); + expect(tr(null)).toBeNull(); + }); + + it("passes ids through when no canonical link exists on the primary side", () => { + const tr = buildSpTranslator( + [{ id: "P-X", participantId: null }], + local + ); + expect(tr("P-X")).toBe("P-X"); + }); + + it("passes ids through when the local window has no counterpart", () => { + const tr = buildSpTranslator(primary, [ + { id: "L-1", participantId: "C-A" }, + // C-B and C-C absent from the local field + ]); + expect(tr("P-1")).toBe("L-1"); // bridged + expect(tr("P-2")).toBe("P-2"); // no local counterpart → unchanged + }); +}); diff --git a/app/services/simulations/__tests__/tennis-simulator.test.ts b/app/services/simulations/__tests__/tennis-simulator.test.ts index 3e16ba2..4d18fa8 100644 --- a/app/services/simulations/__tests__/tennis-simulator.test.ts +++ b/app/services/simulations/__tests__/tennis-simulator.test.ts @@ -1,5 +1,15 @@ import { describe, it, expect } from "vitest"; -import { eloWinProb, buildDraw, simulateMajor } from "../tennis-simulator"; +import { + eloWinProb, + buildDraw, + simulateMajor, + drawFromBracket, + buildHonoredMap, + DEFAULT_SLAM_QP, + type RealBracketMatch, +} from "../tennis-simulator"; +import { deriveBracketQualifyingStates, getRoundConfig } from "~/models/scoring-calculator"; +import { BRACKET_TEMPLATES } from "~/lib/bracket-templates"; // ─── eloWinProb ─────────────────────────────────────────────────────────────── @@ -337,3 +347,187 @@ describe("QP accumulation ranking (unit)", () => { expect(winRate).toBeGreaterThan(0.02); // > 2% (well above random 0.78%) }); }); + +// ─── drawFromBracket ──────────────────────────────────────────────────────────── + +describe("drawFromBracket", () => { + const ids = makeIds(128); + // 64 Round-of-128 matches: match N holds ids[2N-2], ids[2N-1]. + const fullR128 = Array.from({ length: 64 }, (_, i) => ({ + round: "Round of 128", + matchNumber: i + 1, + participant1Id: ids[i * 2], + participant2Id: ids[i * 2 + 1], + })); + + it("reconstructs a 128-slot draw in match order", () => { + const draw = drawFromBracket(fullR128, (id) => id); + expect(draw).not.toBeNull(); + expect(draw).toHaveLength(128); + expect(draw?.[0]).toBe("p001"); + expect(draw?.[1]).toBe("p002"); + expect(draw?.[127]).toBe("p128"); + }); + + it("returns null when the draw is not fully populated", () => { + const partial = fullR128.slice(0, 60); // only 60 of 64 matches + expect(drawFromBracket(partial, (id) => id)).toBeNull(); + const withNull = fullR128.map((m, i) => + i === 0 ? { ...m, participant1Id: null } : m + ); + expect(drawFromBracket(withNull, (id) => id)).toBeNull(); + }); + + it("applies the id translator", () => { + const draw = drawFromBracket(fullR128, (id) => (id ? `x-${id}` : id)); + expect(draw?.[0]).toBe("x-p001"); + }); +}); + +// ─── simulateMajor honoring a real bracket ────────────────────────────────────── + +describe("simulateMajor with realBracket", () => { + const ids = makeIds(128); + const eloMap = makeEloMap(ids, ids.map(() => 1500)); + + // A fully-decided bracket where the lower draw index always wins. Champion is + // therefore ids[0]; the result is deterministic regardless of RNG. + function decideBracket(draw: string[]): RealBracketMatch[] { + const rounds = [ + "Round of 128", "Round of 64", "Round of 32", "Round of 16", + "Quarterfinals", "Semifinals", "Final", + ]; + const order = new Map(draw.map((id, i) => [id, i])); + const matches: RealBracketMatch[] = []; + let current = [...draw]; + for (const round of rounds) { + const next: string[] = []; + for (let i = 0; i < current.length; i += 2) { + const p1 = current[i]; + const p2 = current[i + 1]; + const winner = (order.get(p1) ?? 0) < (order.get(p2) ?? 0) ? p1 : p2; + matches.push({ round, matchNumber: i / 2 + 1, winnerId: winner, isComplete: true }); + next.push(winner); + } + current = next; + } + return matches; + } + + it("honors a fully-decided bracket deterministically (champion = draw[0])", () => { + const draw = [...ids]; + const realBracket = decideBracket(draw); + const results = simulateMajor(draw, eloMap, "hard", { realBracket }); + const champ = results.find((r) => r.qp === DEFAULT_SLAM_QP.winner); + expect(champ?.participantId).toBe(draw[0]); + // Structural counts are unchanged by honoring. + expect(results.filter((r) => r.qp === DEFAULT_SLAM_QP.winner)).toHaveLength(1); + expect(results.filter((r) => r.qp === DEFAULT_SLAM_QP.finalist)).toHaveLength(1); + expect(results.filter((r) => r.qp === DEFAULT_SLAM_QP.sf)).toHaveLength(2); + expect(results.filter((r) => r.qp === DEFAULT_SLAM_QP.qf)).toHaveLength(4); + expect(results.filter((r) => r.qp === DEFAULT_SLAM_QP.r16)).toHaveLength(8); + }); + + it("honors a completed Final winner even against a heavy Elo underdog", () => { + // Underdog at draw[0] (low Elo) but the Final is recorded as their win. + const draw = [...ids]; + const skewed = makeEloMap(draw, draw.map((_, i) => (i === 0 ? 100 : 3000))); + const realBracket = decideBracket(draw); // draw[0] wins everything + const results = simulateMajor(draw, skewed, "hard", { realBracket }); + const champ = results.find((r) => r.qp === DEFAULT_SLAM_QP.winner); + expect(champ?.participantId).toBe(draw[0]); + }); + + it("uses config QP values from opts.qp", () => { + const draw = [...ids]; + const realBracket = decideBracket(draw); + const qp = { winner: 100, finalist: 70, sf: 40, qf: 20, r16: 5 }; + const results = simulateMajor(draw, eloMap, "hard", { realBracket, qp }); + expect(results.find((r) => r.participantId === draw[0])?.qp).toBe(100); + expect(results.filter((r) => r.qp === 70)).toHaveLength(1); + expect(results.filter((r) => r.qp === 5)).toHaveLength(8); + }); +}); + +// ─── tennis_128 bracket scoring (QP placement derivation) ─────────────────────── + +describe("tennis_128 qualifying bracket scoring", () => { + it("derives placements 1/2/3/5/9 from the tennis_128 round config", () => { + const template = BRACKET_TEMPLATES.tennis_128; + // A wins R16 → QF → SF → Final (champion). Losers exit at each scoring round. + const matches = [ + { round: "Round of 16", winnerId: "A", loserId: "I", participant1Id: "A", participant2Id: "I" }, + { round: "Quarterfinals", winnerId: "A", loserId: "E", participant1Id: "A", participant2Id: "E" }, + { round: "Semifinals", winnerId: "A", loserId: "C", participant1Id: "A", participant2Id: "C" }, + { round: "Final", winnerId: "A", loserId: "B", participant1Id: "A", participant2Id: "B" }, + ]; + const states = deriveBracketQualifyingStates( + matches, + template.rounds, + (round) => getRoundConfig(round, "tennis_128") + ); + expect(states.get("A")?.placement).toBe(1); // champion + expect(states.get("B")?.placement).toBe(2); // Final loser + expect(states.get("C")).toMatchObject({ placement: 3, tieCount: 2 }); // SF loser + expect(states.get("E")).toMatchObject({ placement: 5, tieCount: 4 }); // QF loser + expect(states.get("I")).toMatchObject({ placement: 9, tieCount: 8 }); // R16 loser + }); +}); + +// ─── exclusions (not-participating) + prebuilt honored map ────────────────────── + +describe("simulateMajor exclusions & prebuilt honored", () => { + const ids = makeIds(128); + + it("a withdrawn player walks over (earns 0 QP) even with a dominant Elo", () => { + // draw[0] would otherwise win almost everything; excluding them must zero them. + const dominant = makeEloMap(ids, ids.map((_, i) => (i === 0 ? 5000 : 1000))); + const excluded = new Set([ids[0]]); + for (let t = 0; t < 20; t++) { + const results = simulateMajor(ids, dominant, "hard", { excluded }); + expect(results.find((r) => r.participantId === ids[0])?.qp).toBe(0); + } + }); + + it("a completed match is still honored even if the winner is later excluded", () => { + // Fully-decided bracket says draw[0] wins; excluding them must NOT override a + // recorded real result (the match already happened). + function decide(draw: string[]): RealBracketMatch[] { + const rounds = [ + "Round of 128", "Round of 64", "Round of 32", "Round of 16", + "Quarterfinals", "Semifinals", "Final", + ]; + const order = new Map(draw.map((id, i) => [id, i])); + const matches: RealBracketMatch[] = []; + let cur = [...draw]; + for (const round of rounds) { + const next: string[] = []; + for (let i = 0; i < cur.length; i += 2) { + const w = (order.get(cur[i]) ?? 0) < (order.get(cur[i + 1]) ?? 0) ? cur[i] : cur[i + 1]; + matches.push({ round, matchNumber: i / 2 + 1, winnerId: w, isComplete: true }); + next.push(w); + } + cur = next; + } + return matches; + } + const honored = buildHonoredMap(decide(ids)); + const eloMap = makeEloMap(ids, ids.map(() => 1500)); + const results = simulateMajor(ids, eloMap, "hard", { + honored, + excluded: new Set([ids[0]]), + }); + expect(results.find((r) => r.qp === DEFAULT_SLAM_QP.winner)?.participantId).toBe(ids[0]); + }); + + it("prebuilt honored map produces the same champion as realBracket input", () => { + const realBracket: RealBracketMatch[] = [ + { round: "Final", matchNumber: 1, winnerId: null, isComplete: false }, + ]; + const honored = buildHonoredMap(realBracket); + // Empty honored (no completed matches) → no crash, full simulation runs. + const eloMap = makeEloMap(ids, ids.map(() => 1500)); + const results = simulateMajor(ids, eloMap, "hard", { honored }); + expect(results.filter((r) => r.qp === DEFAULT_SLAM_QP.winner)).toHaveLength(1); + }); +}); diff --git a/app/services/simulations/cs-major-simulator.ts b/app/services/simulations/cs-major-simulator.ts index b642960..861c704 100644 --- a/app/services/simulations/cs-major-simulator.ts +++ b/app/services/simulations/cs-major-simulator.ts @@ -46,11 +46,18 @@ import { database } from "~/database/context"; import { eq, and, inArray } from "drizzle-orm"; import * as schema from "~/database/schema"; import { getQPConfig } from "~/models/qualifying-points"; -import { getCs2StageResultsMapForEvent, computeStage3ExitQP as calcStage3ExitQP } from "~/models/cs2-major-stage"; +import { getCs2StageResultsMapForEvent, computeStage3ExitQP as calcStage3ExitQP, type Cs2StageResult } from "~/models/cs2-major-stage"; +import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; export { calcStage3ExitQP }; import { getExcludedByEventMap } from "~/models/event-result"; import { findPlayoffMatchesByEventId } from "~/models/playoff-match"; import { findSeasonMatchesByScoringEventId } from "~/models/season-match"; +import { + resolveStructureSource, + IDENTITY_TR, + type IdTranslator, + type StructureSource, +} from "./shared-major"; import type { Simulator, SimulationResult } from "./types"; // ─── Simulation parameters ──────────────────────────────────────────────────── @@ -611,6 +618,30 @@ export class CSMajorSimulator implements Simulator { ); } + // 4b. Shared-major structure resolution. A tournament-linked, non-primary + // event holds no bracket/stage/Swiss rows of its own — they live on the + // primary window. For each such event, read the structure from the primary + // and translate the primary window's season_participant ids into THIS + // window's ids via the shared canonical participant, so the simulation + // conditions on real in-progress results instead of re-simulating from + // scratch. (Provisional QP already fans out to event_results, so it is read + // locally below and needs no translation.) + const localParticipants = await findParticipantsBySportsSeasonId(sportsSeasonId); + // Composed translator (primary→local) cached per primary sports season — + // a window's majors may have primaries in several different windows. + const translatorByPrimarySeason = new Map(); + const structureByEventId = new Map( + await Promise.all( + events.map( + async (e) => + [ + e.id, + await resolveStructureSource(e, localParticipants, translatorByPrimarySeason), + ] as const + ) + ) + ); + // 5. For completed events, read actual QP from eventResults. const completedEventIds = events.filter((e) => e.isComplete).map((e) => e.id); const actualQPMap = new Map(participantIds.map((id) => [id, 0])); @@ -632,11 +663,21 @@ export class CSMajorSimulator implements Simulator { } } - // 6. Load stage results for all events in parallel. + // 6. Load stage results for all events in parallel (from the structure + // source — the primary window for siblings — translating participant ids). const stageResultEntries = await Promise.all( events.map(async (event) => { - const results = await getCs2StageResultsMapForEvent(event.id); - return [event.id, results] as const; + const { sourceId, tr } = structureByEventId.get(event.id) ?? { + sourceId: event.id, + tr: IDENTITY_TR, + }; + const results = await getCs2StageResultsMapForEvent(sourceId); + const translated = new Map(); + for (const [pid, r] of results) { + const t = tr(pid) ?? pid; + translated.set(t, { ...r, participantId: t }); + } + return [event.id, translated] as const; }) ); const eventStageResults = new Map( @@ -652,35 +693,49 @@ export class CSMajorSimulator implements Simulator { // 6b. For incomplete events, load already-known results so the simulation // conditions on them instead of re-simulating: the Champions Stage // bracket, the Swiss match results, and provisional recorded QP. + // Each entry carries its own translation fn (identity for primaries, primary + // →this-window for siblings) applied to every participant id read below. const [bracketEntries, swissEntries] = await Promise.all([ Promise.all( - incompleteEvents.map(async (e) => [e.id, await findPlayoffMatchesByEventId(e.id)] as const) + incompleteEvents.map(async (e) => { + const { sourceId, tr } = structureByEventId.get(e.id) ?? { + sourceId: e.id, + tr: IDENTITY_TR, + }; + return [e.id, tr, await findPlayoffMatchesByEventId(sourceId)] as const; + }) ), Promise.all( - incompleteEvents.map(async (e) => [e.id, await findSeasonMatchesByScoringEventId(e.id)] as const) + incompleteEvents.map(async (e) => { + const { sourceId, tr } = structureByEventId.get(e.id) ?? { + sourceId: e.id, + tr: IDENTITY_TR, + }; + return [e.id, tr, await findSeasonMatchesByScoringEventId(sourceId)] as const; + }) ), ]); const eventBracket = new Map( - bracketEntries.map(([id, matches]) => [ + bracketEntries.map(([id, tr, matches]) => [ id, matches.map((m) => ({ round: m.round, matchNumber: m.matchNumber, - participant1Id: m.participant1Id, - participant2Id: m.participant2Id, - winnerId: m.winnerId, + participant1Id: tr(m.participant1Id), + participant2Id: tr(m.participant2Id), + winnerId: tr(m.winnerId), isComplete: m.isComplete, })), ]) ); const eventSwiss = new Map( - swissEntries.map(([id, matches]) => [ + swissEntries.map(([id, tr, matches]) => [ id, matches.map((m) => ({ matchStage: m.matchStage, - participant1Id: m.participant1Id, - participant2Id: m.participant2Id, - winnerId: m.winnerId, + participant1Id: tr(m.participant1Id), + participant2Id: tr(m.participant2Id), + winnerId: tr(m.winnerId), })), ]) ); diff --git a/app/services/simulations/shared-major.ts b/app/services/simulations/shared-major.ts new file mode 100644 index 0000000..2d8b0c9 --- /dev/null +++ b/app/services/simulations/shared-major.ts @@ -0,0 +1,88 @@ +/** + * Shared helpers for "shared major" simulators (CS2, tennis). + * + * A major (a real tournament) can be linked to several sports_season windows. + * The bracket/stage structure is built and scored once on the PRIMARY window; + * sibling windows hold no structure of their own. When simulating a sibling, the + * simulator must read the structure from the primary and translate the primary + * window's season_participant ids into the local window's ids via the shared + * canonical participant. These helpers centralise that resolution + translation. + */ + +import { getPrimaryEventForTournament } from "~/models/scoring-event"; +import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; + +/** Maps a participant id from one window to another (or passes it through). */ +export type IdTranslator = (id: string | null) => string | null; + +/** Identity translation — used when structure lives in the event's own window. */ +export const IDENTITY_TR: IdTranslator = (id) => id; + +/** + * Build a translator that maps a PRIMARY window's season_participant id to the + * equivalent id in a LOCAL (sibling) window, bridging via the shared canonical + * participant. Ids that can't be bridged (no canonical link, or no local + * counterpart) pass through unchanged so structure is never silently dropped. + */ +export function buildSpTranslator( + primaryParticipants: Array<{ id: string; participantId: string | null }>, + localParticipants: Array<{ id: string; participantId: string | null }> +): IdTranslator { + const primaryToCanonical = new Map( + primaryParticipants + .filter((p) => p.participantId) + .map((p) => [p.id, p.participantId as string]) + ); + const canonicalToLocal = new Map( + localParticipants + .filter((p) => p.participantId) + .map((p) => [p.participantId as string, p.id]) + ); + return (id: string | null): string | null => { + if (!id) return id; + const canonical = primaryToCanonical.get(id); + if (!canonical) return id; + return canonicalToLocal.get(canonical) ?? id; + }; +} + +export interface StructureSource { + /** Event id whose bracket/stage rows hold this event's structure. */ + sourceId: string; + /** Translate the source window's participant ids into the local window's. */ + tr: IdTranslator; +} + +/** + * Resolve where an event's bracket/stage structure lives and how to map its + * participant ids into the local window. For a primary/standalone/non-linked + * event this is the event itself with identity translation. For a sibling + * (tournament-linked, non-primary) event, the structure is the primary event and + * ids are translated primary→local. + * + * `translatorCache` is keyed by primary sports_season id so a window's majors + * (whose primaries may live in different windows) each build their translator + * once. + */ +export async function resolveStructureSource( + event: { id: string; tournamentId: string | null; isPrimary: boolean }, + localParticipants: Array<{ id: string; participantId: string | null }>, + translatorCache: Map +): Promise { + if (!event.tournamentId || event.isPrimary) { + return { sourceId: event.id, tr: IDENTITY_TR }; + } + const primary = await getPrimaryEventForTournament(event.tournamentId); + if (!primary || primary.id === event.id) { + return { sourceId: event.id, tr: IDENTITY_TR }; + } + let tr = translatorCache.get(primary.sportsSeasonId); + if (!tr) { + const primaryParticipants = await findParticipantsBySportsSeasonId( + primary.sportsSeasonId + ); + tr = buildSpTranslator(primaryParticipants, localParticipants); + translatorCache.set(primary.sportsSeasonId, tr); + } + return { sourceId: primary.id, tr }; +} diff --git a/app/services/simulations/tennis-simulator.ts b/app/services/simulations/tennis-simulator.ts index 23e7f5f..eb8230e 100644 --- a/app/services/simulations/tennis-simulator.ts +++ b/app/services/simulations/tennis-simulator.ts @@ -42,6 +42,11 @@ import { eq, and, inArray } from "drizzle-orm"; import * as schema from "~/database/schema"; import { getSurfaceEloMap } from "~/models/surface-elo"; import { getExcludedByEventMap } from "~/models/event-result"; +import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; +import { findPlayoffMatchesByEventId } from "~/models/playoff-match"; +import { getQPConfig, calculateSplitQualifyingPoints } from "~/models/qualifying-points"; +import { resolveStructureSource, type IdTranslator } from "./shared-major"; +import { BRACKET_TEMPLATES } from "~/lib/bracket-templates"; import type { Simulator, SimulationResult } from "./types"; // ─── Simulation parameters ──────────────────────────────────────────────────── @@ -180,12 +185,89 @@ interface QPResult { qp: number; } +/** QP awarded at each scoring stage of a Grand Slam (tie-split pre-applied). */ +export interface SlamQP { + winner: number; + finalist: number; + sf: number; // each SF loser + qf: number; // each QF loser + r16: number; // each R16 loser +} + +/** + * Default QP — the historical hardcoded tie-split averages. Used when a season's + * QP config isn't supplied (e.g. unit tests). The live simulator derives these + * from getQPConfig so simulated QP matches what the bracket actually awards. + */ +export const DEFAULT_SLAM_QP: SlamQP = { + winner: QP_WINNER, + finalist: QP_FINALIST, + sf: QP_SF_LOSER, + qf: QP_QF_LOSER, + r16: QP_R16_LOSER, +}; + +/** A real (DB) bracket match used to condition an in-progress simulation. */ +export interface RealBracketMatch { + round: string; + matchNumber: number; + winnerId: string | null; + isComplete: boolean; +} + +// Round structure is derived from the tennis_128 template so the simulator and +// the scorer share one source of truth — renaming a round or changing the draw +// size in the template can't silently desync the bracket-conditioned sim. +const TENNIS_TEMPLATE = BRACKET_TEMPLATES.tennis_128; +const SLAM_ROUND_NAMES = TENNIS_TEMPLATE.rounds.map((r) => r.name); +const FIRST_ROUND = TENNIS_TEMPLATE.rounds[0]; +const FINAL_ROUND_NAME = SLAM_ROUND_NAMES[SLAM_ROUND_NAMES.length - 1]; +// Scoring rounds before the final, ordered final-adjacent first ([SF, QF, R16]), +// so loser QP tiers (sf, qf, r16) map positionally rather than by hardcoded name. +const NON_FINAL_SCORING_ROUNDS = TENNIS_TEMPLATE.rounds + .filter((r) => r.isScoring && r.name !== FINAL_ROUND_NAME) + .map((r) => r.name) + .toReversed(); + +/** Build the round→matchNumber→winnerId lookup honored during simulation. */ +export function buildHonoredMap( + realBracket: RealBracketMatch[] +): Map> { + const honored = new Map>(); + for (const m of realBracket) { + if (m.isComplete && m.winnerId) { + let r = honored.get(m.round); + if (!r) { r = new Map(); honored.set(m.round, r); } + r.set(m.matchNumber, m.winnerId); + } + } + return honored; +} + /** * Simulate one Grand Slam major and return QP earned per participant. - * The draw is a pre-shuffled 128-slot array from buildDraw(). + * The draw is a 128-slot array (from buildDraw, or from a real bracket's draw). + * + * When `opts.realBracket` is supplied, completed matches are HONORED (their + * winners advance instead of being re-simulated) and only undecided matches are + * played out — the tennis analog of CS2's bracket-conditioned simulation. The + * draw passed in must already reflect the real bracket's Round-of-128 ordering. + * * Exported for unit testing. */ -export function simulateMajor(draw: string[], eloMap: Map, surface: CourtSurface): QPResult[] { +export function simulateMajor( + draw: string[], + eloMap: Map, + surface: CourtSurface, + opts: { + realBracket?: RealBracketMatch[]; + honored?: Map>; + qp?: SlamQP; + excluded?: Set; + } = {} +): QPResult[] { + const qpValues = opts.qp ?? DEFAULT_SLAM_QP; + const excluded = opts.excluded; const qp = new Map(draw.map((id) => [id, 0])); const getElo = (id: string): number => { @@ -201,60 +283,83 @@ export function simulateMajor(draw: string[], eloMap: Map = {}; + const tiers = [qpValues.sf, qpValues.qf, qpValues.r16]; + NON_FINAL_SCORING_ROUNDS.forEach((name, i) => { + if (i < tiers.length) loserQpByRound[name] = tiers[i]; + }); + + let current = [...draw]; + for (const roundName of SLAM_ROUND_NAMES) { + const honoredRound = honored?.get(roundName); const next: string[] = []; for (let i = 0; i < current.length; i += 2) { - const { winner } = simMatch(current[i], current[i + 1]); + const p1 = current[i]; + const p2 = current[i + 1]; + const matchNumber = i / 2 + 1; + // Honor a completed match only if its winner is actually one of the two + // players at this slot (guards against inconsistent partial entry). + const forced = honoredRound?.get(matchNumber); + let winner: string; + let loser: string; + if (forced && (forced === p1 || forced === p2)) { + winner = forced; + loser = forced === p1 ? p2 : p1; + } else if (excluded && excluded.has(p1) !== excluded.has(p2)) { + // Exactly one player withdrew (not-participating) and the match isn't + // decided yet → the present player advances by walkover. + winner = excluded.has(p1) ? p2 : p1; + loser = winner === p1 ? p2 : p1; + } else { + ({ winner, loser } = simMatch(p1, p2)); + } + + if (roundName === FINAL_ROUND_NAME) { + qp.set(winner, (qp.get(winner) ?? 0) + qpValues.winner); + qp.set(loser, (qp.get(loser) ?? 0) + qpValues.finalist); + } else { + const lq = loserQpByRound[roundName]; + if (lq) qp.set(loser, (qp.get(loser) ?? 0) + lq); + } next.push(winner); } current = next; } - // R16: 8 matches, losers get 1.5 QP each. - { - const next: string[] = []; - for (let i = 0; i < current.length; i += 2) { - const { winner, loser } = simMatch(current[i], current[i + 1]); - qp.set(loser, (qp.get(loser) ?? 0) + QP_R16_LOSER); - next.push(winner); - } - current = next; - } - - // QF: 4 matches, losers get 4 QP each. - { - const next: string[] = []; - for (let i = 0; i < current.length; i += 2) { - const { winner, loser } = simMatch(current[i], current[i + 1]); - qp.set(loser, (qp.get(loser) ?? 0) + QP_QF_LOSER); - next.push(winner); - } - current = next; - } - - // SF: 2 matches, losers get 9 QP each. - { - const next: string[] = []; - for (let i = 0; i < current.length; i += 2) { - const { winner, loser } = simMatch(current[i], current[i + 1]); - qp.set(loser, (qp.get(loser) ?? 0) + QP_SF_LOSER); - next.push(winner); - } - current = next; - } - - // Final: 1 match. - const { winner: champion, loser: finalist } = simMatch(current[0], current[1]); - qp.set(champion, (qp.get(champion) ?? 0) + QP_WINNER); - qp.set(finalist, (qp.get(finalist) ?? 0) + QP_FINALIST); - return Array.from(qp.entries()).map(([participantId, qpVal]) => ({ participantId, qp: qpVal })); } +/** + * Reconstruct a 128-slot draw from a real bracket's Round-of-128 matches. + * Match N (1-indexed) occupies slots [2N-2, 2N-1]. Returns null if the draw is + * not fully populated (so the caller falls back to a synthetic seeded draw). + * `tr` maps the source window's participant ids into the local window's. + */ +export function drawFromBracket( + matches: Array<{ round: string; matchNumber: number; participant1Id: string | null; participant2Id: string | null }>, + tr: IdTranslator +): string[] | null { + // First round name + match count come from the template (single source). + const firstRoundMatches = FIRST_ROUND.matchCount; + const slotCount = firstRoundMatches * 2; + const r1 = matches.filter((m) => m.round === FIRST_ROUND.name); + if (r1.length !== firstRoundMatches) return null; + const slots: (string | null)[] = Array(slotCount).fill(null); + for (const m of r1) { + if (m.matchNumber < 1 || m.matchNumber > firstRoundMatches) return null; + slots[(m.matchNumber - 1) * 2] = tr(m.participant1Id); + slots[(m.matchNumber - 1) * 2 + 1] = tr(m.participant2Id); + } + if (slots.some((s) => s === null)) return null; + return slots as string[]; +} + // ─── Simulator ──────────────────────────────────────────────────────────────── export class TennisSimulator implements Simulator { @@ -327,6 +432,74 @@ export class TennisSimulator implements Simulator { // Load not-participating exclusions for each incomplete major. const excludedByEvent = await getExcludedByEventMap(incompleteMajors.map((e) => e.id)); + // Per-stage QP derived from the season's QP config (tie-split) so simulated QP + // matches what the bracket actually awards via processQualifyingBracketEvent. + // Falls back to the historical defaults if the season has no QP config. + const qpConfigArray = await getQPConfig(sportsSeasonId); + const qpMap = new Map( + qpConfigArray.map((c) => [c.placement, parseFloat(c.points)]) + ); + const slamQP: SlamQP = + qpConfigArray.length > 0 + ? { + winner: calculateSplitQualifyingPoints(1, 1, qpMap), + finalist: calculateSplitQualifyingPoints(2, 1, qpMap), + sf: calculateSplitQualifyingPoints(3, 2, qpMap), + qf: calculateSplitQualifyingPoints(5, 4, qpMap), + r16: calculateSplitQualifyingPoints(9, 8, qpMap), + } + : DEFAULT_SLAM_QP; + + // Precompute, once, how each incomplete major is simulated. If a real bracket + // exists (and is fully drawn), condition on it: a fixed draw from the bracket + // + completed matches honored (read from the primary window for siblings, with + // id translation). Otherwise fall back to a fresh synthetic seeded draw. + const localParticipants = await findParticipantsBySportsSeasonId(sportsSeasonId); + const translatorCache = new Map(); + interface MajorPlan { + eventId: string; + surface: CourtSurface; + fixedDraw: string[] | null; + // honored is built ONCE here (not per Monte Carlo iteration). + honored: Map> | null; + excluded: Set; + } + const majorPlans: MajorPlan[] = []; + for (const event of incompleteMajors) { + const surface = getSurfaceForEvent(event.name); + const excluded = excludedByEvent.get(event.id) ?? new Set(); + const { sourceId, tr } = await resolveStructureSource( + event, + localParticipants, + translatorCache + ); + const matches = await findPlayoffMatchesByEventId(sourceId); + let fixedDraw: string[] | null = null; + let honored: Map> | null = null; + if (matches.length > 0) { + fixedDraw = drawFromBracket( + matches.map((m) => ({ + round: m.round, + matchNumber: m.matchNumber, + participant1Id: m.participant1Id, + participant2Id: m.participant2Id, + })), + tr + ); + if (fixedDraw) { + honored = buildHonoredMap( + matches.map((m) => ({ + round: m.round, + matchNumber: m.matchNumber, + winnerId: tr(m.winnerId), + isComplete: m.isComplete, + })) + ); + } + } + majorPlans.push({ eventId: event.id, surface, fixedDraw, honored, excluded }); + } + // 5. Monte Carlo loop. // For each player, count how many times they finish 1st–8th by QP rank. const counts: number[][] = Array.from({ length: participantIds.length }, () => Array.from({ length: 8 }, () => 0)); @@ -337,15 +510,24 @@ export class TennisSimulator implements Simulator { const simQP = new Map(actualQPMap); // Simulate each incomplete major and accumulate QP. - for (const event of incompleteMajors) { - const surface = getSurfaceForEvent(event.name); - const excluded = excludedByEvent.get(event.id) ?? new Set(); - const activeIds = participantIds.filter((id) => !excluded.has(id)); - // Fall back to full participant list if too few remain after exclusions - // (buildDraw requires >= 128 players to fill all bracket slots). - const drawIds = activeIds.length >= 128 ? activeIds : participantIds; - const draw = buildDraw(drawIds, surfaceEloMap); - const results = simulateMajor(draw, surfaceEloMap, surface); + for (const plan of majorPlans) { + let results: QPResult[]; + if (plan.fixedDraw && plan.honored) { + // Real bracket: fixed draw, completed matches honored, withdrawn + // players walk over in undecided matches. + results = simulateMajor(plan.fixedDraw, surfaceEloMap, plan.surface, { + honored: plan.honored, + qp: slamQP, + excluded: plan.excluded, + }); + } else { + const activeIds = participantIds.filter((id) => !plan.excluded.has(id)); + // Fall back to full participant list if too few remain after exclusions + // (buildDraw requires >= 128 players to fill all bracket slots). + const drawIds = activeIds.length >= 128 ? activeIds : participantIds; + const draw = buildDraw(drawIds, surfaceEloMap); + results = simulateMajor(draw, surfaceEloMap, plan.surface, { qp: slamQP }); + } for (const { participantId, qp } of results) { simQP.set(participantId, (simQP.get(participantId) ?? 0) + qp); } diff --git a/app/services/sync-tournament-results.ts b/app/services/sync-tournament-results.ts index 786f913..df86c6a 100644 --- a/app/services/sync-tournament-results.ts +++ b/app/services/sync-tournament-results.ts @@ -1,7 +1,14 @@ -import { eq, and } from "drizzle-orm"; +import { eq, and, inArray } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { processQualifyingEvent } from "~/models/scoring-calculator"; +import { + processQualifyingEvent, + recalculateAffectedLeagues, +} from "~/models/scoring-calculator"; +import { completeScoringEvent, getScoringEventById } from "~/models/scoring-event"; +import { getEventResults } from "~/models/event-result"; +import { upsertTournamentResult } from "~/models/tournament-result"; +import { logger } from "~/lib/logger"; export interface SyncReport { tournamentId: string; @@ -14,6 +21,22 @@ export interface SyncReport { }>; } +export interface SyncOptions { + /** + * Mark each synced window's scoring_event as complete (is_complete + + * completed_at). Use for final results (golf batch entry, finalize-bracket). + * Leave false for mid-tournament fan-out (e.g. live bracket rounds) so a major + * isn't marked complete before it actually finishes. + * @default true + */ + markComplete?: boolean; + /** + * Skip the primary window when fanning out from a bracket/stage major — the + * primary is already scored in place, so re-running it would be wasted work. + */ + skipEventId?: string; +} + /** * Fan out canonical tournament_results to every scoring_event window that * points at this tournament. For each linked window: @@ -30,9 +53,11 @@ export interface SyncReport { * results is owned by processQualifyingEvent. */ export async function syncTournamentResults( - tournamentId: string + tournamentId: string, + options: SyncOptions = {} ): Promise { const db = database(); + const { markComplete = true, skipEventId } = options; const report: SyncReport = { tournamentId, @@ -48,10 +73,24 @@ export async function syncTournamentResults( .where(eq(schema.tournamentResults.tournamentId, tournamentId)); // 2. Load every scoring_event that points at this tournament. - const linkedEvents = await db + const allLinkedEvents = await db .select() .from(schema.scoringEvents) .where(eq(schema.scoringEvents.tournamentId, tournamentId)); + // The primary window (when fanning out from a bracket/stage major) is already + // scored in place — skip it so we don't redo its work. + const linkedEvents = skipEventId + ? allLinkedEvents.filter((ev) => ev.id !== skipEventId) + : allLinkedEvents; + + // Windows that synced cleanly — used to propagate to leagues after the loop + // (kept out of the per-window transaction so a recalc failure can't roll back + // the result write). + const syncedWindows: Array<{ + sportsSeasonId: string; + eventId: string; + eventName: string | null; + }> = []; // 3. Fan out — each event gets its own transaction. for (const ev of linkedEvents) { @@ -67,9 +106,11 @@ export async function syncTournamentResults( // upsert the matching event_results row. Only placement / rawScore // are copied; qualifying_points_awarded is left for // processQualifyingEvent to recompute. + const canonicalSpIds = new Set(); for (const cr of canonicalResults) { const sp = rosters.find((r) => r.participantId === cr.participantId); if (!sp) continue; + canonicalSpIds.add(sp.id); const [existing] = await tx .select() @@ -100,28 +141,56 @@ export async function syncTournamentResults( } } - // 3c. 0-QP filler pass: every roster member without an event_results - // row for this event gets one with placement=null, QP=0. This matches - // the behavior from the per-window batch-qualifying-results design. + // 3c. Reconcile every roster member NOT in the canonical results to a + // 0-QP filler row (placement=null, QP=0). This both creates missing rows + // AND resets rows for participants whose canonical placement was removed + // (a correction) — without this, a stale placement/rawScore would persist + // and keep fanning out. notParticipating markers are left untouched. const afterRows = await tx .select() .from(schema.eventResults) .where(eq(schema.eventResults.scoringEventId, ev.id)); - const covered = new Set(afterRows.map((r) => r.seasonParticipantId)); + const existingBySp = new Map(afterRows.map((r) => [r.seasonParticipantId, r])); for (const sp of rosters) { - if (covered.has(sp.id)) continue; - await tx.insert(schema.eventResults).values({ - scoringEventId: ev.id, - seasonParticipantId: sp.id, - placement: null, - qualifyingPointsAwarded: "0", - }); + if (canonicalSpIds.has(sp.id)) continue; + const existing = existingBySp.get(sp.id); + if (!existing) { + await tx.insert(schema.eventResults).values({ + scoringEventId: ev.id, + seasonParticipantId: sp.id, + placement: null, + qualifyingPointsAwarded: "0", + }); + } else if (!existing.notParticipating && existing.placement !== null) { + // Stale placement from a prior sync — reset to filler. + await tx + .update(schema.eventResults) + .set({ + placement: null, + rawScore: null, + qualifyingPointsAwarded: "0", + updatedAt: new Date(), + }) + .where(eq(schema.eventResults.id, existing.id)); + } } // 3d. Delegate to scoring engine inside the same transaction. await processQualifyingEvent(ev.id, tx); + + // 3e. Mark the window event complete (final-results sync only). This is + // what makes "score once" actually complete every window — without it, + // each sibling stayed "In Progress" and had to be completed by hand. + if (markComplete) { + await completeScoringEvent(ev.id, tx); + } }); report.windowsSynced += 1; + syncedWindows.push({ + sportsSeasonId: ev.sportsSeasonId, + eventId: ev.id, + eventName: ev.name, + }); } catch (e: unknown) { report.windowsFailed += 1; const msg = @@ -134,5 +203,141 @@ export async function syncTournamentResults( } } + // 4. Propagate to leagues for every cleanly-synced window. Done outside the + // per-window transactions: recalculateAffectedLeagues touches many fantasy + // seasons and must not roll back a committed result write if it fails. + for (const w of syncedWindows) { + try { + await recalculateAffectedLeagues(w.sportsSeasonId, undefined, { + eventId: w.eventId, + eventName: w.eventName ?? undefined, + // Mid-tournament fan-out (markComplete=false) updates standings silently; + // the primary window already announced the round. Only the final sync + // (completion) announces to each window's leagues. + skipDiscord: !markComplete, + }); + } catch (e: unknown) { + const msg = + e instanceof Error ? e.message : typeof e === "string" ? e : String(e); + // Count this as a failed window so the caller's status derivation + // (windowsFailed === 0 ? "completed") doesn't mask stale standings behind + // a green badge — the result write committed, but the league is not current. + report.windowsFailed += 1; + report.failures.push({ + scoringEventId: w.eventId, + sportsSeasonId: w.sportsSeasonId, + error: `League recalc failed: ${msg}`, + }); + } + } + return report; } + +/** + * Fan out a bracket/stage major (tennis, CS2) from its primary window. + * + * The admin builds and scores the bracket/Swiss stages on ONE window (the + * primary). Those derive per-participant placements into the primary's + * event_results. This reads them back, translates each window-scoped + * season_participant to its canonical participant, and persists them as the + * tournament's canonical tournament_results — the exact same shape golf produces + * by hand. It then runs the normal fan-out to every sibling window (skipping the + * primary, which is already scored). + * + * markComplete: pass false while a tournament is still in progress (live rounds) + * so siblings stay in sync without being marked finished; pass true on finalize. + */ +export async function syncMajorFromPrimaryEvent( + primaryEventId: string, + options: { markComplete?: boolean } = {} +): Promise { + const { markComplete = false } = options; + + const primaryEvent = await getScoringEventById(primaryEventId); + if (!primaryEvent) { + throw new Error(`Primary event ${primaryEventId} not found`); + } + if (!primaryEvent.tournamentId) { + throw new Error( + `Event ${primaryEventId} is not linked to a tournament; cannot fan out` + ); + } + + const tournamentId = primaryEvent.tournamentId; + + // Read the primary window's derived results and persist them as canonical + // tournament_results, keyed by canonical participant. Only real placements are + // promoted — unplaced/filler rows are reconstructed per-window by the fan-out. + const primaryResults = await getEventResults(primaryEventId); + const promotedCanonicalIds = new Set(); + for (const r of primaryResults) { + if (r.placement === null || r.notParticipating) continue; + const canonicalParticipantId = r.seasonParticipant?.participantId; + if (!canonicalParticipantId) { + logger.warn( + `[syncMajorFromPrimaryEvent] season_participant ${r.seasonParticipantId} has no canonical participant link; skipping` + ); + continue; + } + await upsertTournamentResult({ + tournamentId, + participantId: canonicalParticipantId, + placement: r.placement, + rawScore: r.rawScore, + }); + promotedCanonicalIds.add(canonicalParticipantId); + } + + // Remove canonical rows for participants no longer placed on the primary (a + // correction that dropped someone) so the stale placement stops fanning out. + const db = database(); + const existingCanonical = await db + .select({ participantId: schema.tournamentResults.participantId }) + .from(schema.tournamentResults) + .where(eq(schema.tournamentResults.tournamentId, tournamentId)); + const staleIds = existingCanonical + .map((r) => r.participantId) + .filter((id) => !promotedCanonicalIds.has(id)); + if (staleIds.length > 0) { + await db + .delete(schema.tournamentResults) + .where( + and( + eq(schema.tournamentResults.tournamentId, tournamentId), + inArray(schema.tournamentResults.participantId, staleIds) + ) + ); + } + + logger.log( + `[syncMajorFromPrimaryEvent] promoted ${promotedCanonicalIds.size} result(s) from primary ${primaryEventId} to tournament ${tournamentId}; removed ${staleIds.length} stale` + ); + + return syncTournamentResults(tournamentId, { + markComplete, + skipEventId: primaryEventId, + }); +} + +/** + * Convenience guard for route handlers: fan out a just-scored bracket/stage event + * to its sibling windows ONLY when it's the designated primary of a shared + * tournament. No-ops for standalone events (no tournament_id) or non-primary + * windows (which are read-only and never scored directly). Failures are logged + * but never thrown — fan-out must not break the admin's local scoring action. + */ +export async function fanOutMajorIfPrimary( + event: { id: string; isPrimary: boolean; tournamentId: string | null }, + options: { markComplete?: boolean } = {} +): Promise { + if (!event.isPrimary || !event.tournamentId) return; + try { + await syncMajorFromPrimaryEvent(event.id, options); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + logger.error( + `[fanOutMajorIfPrimary] fan-out failed for primary event ${event.id}: ${msg}` + ); + } +} diff --git a/database/schema.ts b/database/schema.ts index 8cbb2be..a96ef6b 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -643,6 +643,11 @@ export const scoringEvents = pgTable("scoring_events", { bracketRegionConfig: jsonb("bracket_region_config"), // BracketRegion[] | null isComplete: boolean("is_complete").notNull().default(false), completedAt: timestamp("completed_at"), + // For tournament-linked majors shared across windows: marks the single window + // where the admin does the actual scoring (bracket/stage/results). Scoring on + // the primary fans out to every sibling window. At most one true per + // tournament_id. NULL/false for golf (scored on the canonical tournament page). + isPrimary: boolean("is_primary").notNull().default(false), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }, (t) => ({ diff --git a/drizzle/0122_soft_longshot.sql b/drizzle/0122_soft_longshot.sql new file mode 100644 index 0000000..7edc430 --- /dev/null +++ b/drizzle/0122_soft_longshot.sql @@ -0,0 +1,2 @@ +ALTER TABLE "scoring_events" ADD COLUMN "is_primary" boolean DEFAULT false NOT NULL;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "tournament_groups_scoring_event_id_idx" ON "tournament_groups" USING btree ("scoring_event_id"); \ No newline at end of file diff --git a/drizzle/meta/0122_snapshot.json b/drizzle/meta/0122_snapshot.json new file mode 100644 index 0000000..e5a3e1c --- /dev/null +++ b/drizzle/meta/0122_snapshot.json @@ -0,0 +1,6741 @@ +{ + "id": "67a28e61-c2e6-4fd8-a224-9ebe658a21ec", + "prevId": "d52f9777-c0fe-4ecd-a6b9-ad9500c21913", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autodraft_settings": { + "name": "autodraft_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mode": { + "name": "mode", + "type": "autodraft_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'next_pick'" + }, + "queue_only": { + "name": "queue_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "autodraft_settings_season_id_seasons_id_fk": { + "name": "autodraft_settings_season_id_seasons_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autodraft_settings_team_id_teams_id_fk": { + "name": "autodraft_settings_team_id_teams_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioner_audit_log": { + "name": "commissioner_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "actor_display_name": { + "name": "actor_display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "audit_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "affected_team_ids": { + "name": "affected_team_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_season_id_idx": { + "name": "audit_log_season_id_idx", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "commissioner_audit_log_season_id_seasons_id_fk": { + "name": "commissioner_audit_log_season_id_seasons_id_fk", + "tableFrom": "commissioner_audit_log", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "commissioner_audit_log_league_id_leagues_id_fk": { + "name": "commissioner_audit_log_league_id_leagues_id_fk", + "tableFrom": "commissioner_audit_log", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cs2_major_stage_results": { + "name": "cs2_major_stage_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_entry": { + "name": "stage_entry", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stage_eliminated": { + "name": "stage_eliminated", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stage_eliminated_wins": { + "name": "stage_eliminated_wins", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_placement": { + "name": "final_placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cs2_major_stage_results_unique": { + "name": "cs2_major_stage_results_unique", + "columns": [ + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cs2_major_stage_results_scoring_event_id_scoring_events_id_fk": { + "name": "cs2_major_stage_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "cs2_major_stage_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cs2_major_stage_results_participant_id_season_participants_id_fk": { + "name": "cs2_major_stage_results_participant_id_season_participants_id_fk", + "tableFrom": "cs2_major_stage_results", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_picks": { + "name": "draft_picks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pick_number": { + "name": "pick_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pick_in_round": { + "name": "pick_in_round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picked_by_user_id": { + "name": "picked_by_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "picked_by_type": { + "name": "picked_by_type", + "type": "picked_by_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "time_used": { + "name": "time_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "draft_picks_season_pick_unique": { + "name": "draft_picks_season_pick_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pick_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "draft_picks_season_id_seasons_id_fk": { + "name": "draft_picks_season_id_seasons_id_fk", + "tableFrom": "draft_picks", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_team_id_teams_id_fk": { + "name": "draft_picks_team_id_teams_id_fk", + "tableFrom": "draft_picks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_participant_id_season_participants_id_fk": { + "name": "draft_picks_participant_id_season_participants_id_fk", + "tableFrom": "draft_picks", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_queue": { + "name": "draft_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_position": { + "name": "queue_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_queue_season_id_seasons_id_fk": { + "name": "draft_queue_season_id_seasons_id_fk", + "tableFrom": "draft_queue", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_team_id_teams_id_fk": { + "name": "draft_queue_team_id_teams_id_fk", + "tableFrom": "draft_queue", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_participant_id_season_participants_id_fk": { + "name": "draft_queue_participant_id_season_participants_id_fk", + "tableFrom": "draft_queue", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_slots": { + "name": "draft_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "draft_order": { + "name": "draft_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_slots_season_id_seasons_id_fk": { + "name": "draft_slots_season_id_seasons_id_fk", + "tableFrom": "draft_slots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_slots_team_id_teams_id_fk": { + "name": "draft_slots_team_id_teams_id_fk", + "tableFrom": "draft_slots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_timers": { + "name": "draft_timers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "time_remaining": { + "name": "time_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picks_expires_at": { + "name": "picks_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "picks_started_at": { + "name": "picks_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_timers_season_id_seasons_id_fk": { + "name": "draft_timers_season_id_seasons_id_fk", + "tableFrom": "draft_timers", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_timers_team_id_teams_id_fk": { + "name": "draft_timers_team_id_teams_id_fk", + "tableFrom": "draft_timers", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_results": { + "name": "event_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_participant_id": { + "name": "season_participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifying_points_awarded": { + "name": "qualifying_points_awarded", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "not_participating": { + "name": "not_participating", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_results_event_participant_unique": { + "name": "event_results_event_participant_unique", + "columns": [ + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_results_scoring_event_id_scoring_events_id_fk": { + "name": "event_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "event_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_results_season_participant_id_season_participants_id_fk": { + "name": "event_results_season_participant_id_season_participants_id_fk", + "tableFrom": "event_results", + "tableTo": "season_participants", + "columnsFrom": [ + "season_participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_stage_matches": { + "name": "group_stage_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_group_id": { + "name": "tournament_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant1_score": { + "name": "participant1_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "matchday": { + "name": "matchday", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "group_stage_matches_group_pair_unique": { + "name": "group_stage_matches_group_pair_unique", + "columns": [ + { + "expression": "tournament_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant1_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant2_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_stage_matches_tournament_group_id_tournament_groups_id_fk": { + "name": "group_stage_matches_tournament_group_id_tournament_groups_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "tournament_groups", + "columnsFrom": [ + "tournament_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_stage_matches_participant1_id_season_participants_id_fk": { + "name": "group_stage_matches_participant1_id_season_participants_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "group_stage_matches_participant2_id_season_participants_id_fk": { + "name": "group_stage_matches_participant2_id_season_participants_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_season_id": { + "name": "current_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_public_draft_board": { + "name": "is_public_draft_board", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discord_webhook_url": { + "name": "discord_webhook_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discord_picks_announcement_enabled": { + "name": "discord_picks_announcement_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.match_sub_games": { + "name": "match_sub_games", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_match_id": { + "name": "season_match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "game_number": { + "name": "game_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "game_label": { + "name": "game_label", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "participant1_score": { + "name": "participant1_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "match_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "external_game_id": { + "name": "external_game_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "match_sub_games_match_game_number_unique": { + "name": "match_sub_games_match_game_number_unique", + "columns": [ + { + "expression": "season_match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "game_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "match_sub_games_season_match_id_season_matches_id_fk": { + "name": "match_sub_games_season_match_id_season_matches_id_fk", + "tableFrom": "match_sub_games", + "tableTo": "season_matches", + "columnsFrom": [ + "season_match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "match_sub_games_winner_id_season_participants_id_fk": { + "name": "match_sub_games_winner_id_season_participants_id_fk", + "tableFrom": "match_sub_games", + "tableTo": "season_participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_ev_snapshots": { + "name": "participant_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "calculated_ev": { + "name": "calculated_ev", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_ev_snapshots_unique": { + "name": "participant_ev_snapshots_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_ev_snapshots_participant_id_season_participants_id_fk": { + "name": "participant_ev_snapshots_participant_id_season_participants_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk": { + "name": "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_golf_skills": { + "name": "participant_golf_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sg_total": { + "name": "sg_total", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "datagolf_rank": { + "name": "datagolf_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "masters_odds": { + "name": "masters_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "us_open_odds": { + "name": "us_open_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "open_championship_odds": { + "name": "open_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pga_championship_odds": { + "name": "pga_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_golf_skills_participant_unique": { + "name": "participant_golf_skills_participant_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_golf_skills_participant_id_participants_id_fk": { + "name": "participant_golf_skills_participant_id_participants_id_fk", + "tableFrom": "participant_golf_skills", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_season_results": { + "name": "participant_season_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_points": { + "name": "current_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_position": { + "name": "current_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "psr_participant_season_idx": { + "name": "psr_participant_season_idx", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_season_results_participant_id_season_participants_id_fk": { + "name": "participant_season_results_participant_id_season_participants_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_season_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_season_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_surface_elos": { + "name": "participant_surface_elos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "world_ranking": { + "name": "world_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "elo_hard": { + "name": "elo_hard", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "elo_clay": { + "name": "elo_clay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "elo_grass": { + "name": "elo_grass", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_surface_elos_participant_unique": { + "name": "participant_surface_elos_participant_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_surface_elos_participant_id_participants_id_fk": { + "name": "participant_surface_elos_participant_id_participants_id_fk", + "tableFrom": "participant_surface_elos", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participants": { + "name": "participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "external_key": { + "name": "external_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participants_sport_name_unique": { + "name": "participants_sport_name_unique", + "columns": [ + { + "expression": "sport_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participants_sport_id_sports_id_fk": { + "name": "participants_sport_id_sports_id_fk", + "tableFrom": "participants", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_standings_mappings": { + "name": "pending_standings_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_team_id": { + "name": "external_team_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "standing_data": { + "name": "standing_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "psm_season_external_id_idx": { + "name": "psm_season_external_id_idx", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_standings_mappings_sports_season_id_sports_seasons_id_fk": { + "name": "pending_standings_mappings_sports_season_id_sports_seasons_id_fk", + "tableFrom": "pending_standings_mappings", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_match_games": { + "name": "playoff_match_games", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "playoff_match_id": { + "name": "playoff_match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "game_number": { + "name": "game_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "playoff_match_game_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "participant1_score": { + "name": "participant1_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_match_games_playoff_match_id_playoff_matches_id_fk": { + "name": "playoff_match_games_playoff_match_id_playoff_matches_id_fk", + "tableFrom": "playoff_match_games", + "tableTo": "playoff_matches", + "columnsFrom": [ + "playoff_match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_match_games_winner_id_season_participants_id_fk": { + "name": "playoff_match_games_winner_id_season_participants_id_fk", + "tableFrom": "playoff_match_games", + "tableTo": "season_participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_match_odds": { + "name": "playoff_match_odds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "playoff_match_id": { + "name": "playoff_match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "moneyline_odds": { + "name": "moneyline_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "implied_probability": { + "name": "implied_probability", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "odds_source": { + "name": "odds_source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_match_odds_playoff_match_id_playoff_matches_id_fk": { + "name": "playoff_match_odds_playoff_match_id_playoff_matches_id_fk", + "tableFrom": "playoff_match_odds", + "tableTo": "playoff_matches", + "columnsFrom": [ + "playoff_match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_match_odds_participant_id_season_participants_id_fk": { + "name": "playoff_match_odds_participant_id_season_participants_id_fk", + "tableFrom": "playoff_match_odds", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_matches": { + "name": "playoff_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "match_number": { + "name": "match_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "loser_id": { + "name": "loser_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "participant1_score": { + "name": "participant1_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_scoring": { + "name": "is_scoring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "template_round": { + "name": "template_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "seed_info": { + "name": "seed_info", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "external_match_id": { + "name": "external_match_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_matches_scoring_event_id_scoring_events_id_fk": { + "name": "playoff_matches_scoring_event_id_scoring_events_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_matches_participant1_id_season_participants_id_fk": { + "name": "playoff_matches_participant1_id_season_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_participant2_id_season_participants_id_fk": { + "name": "playoff_matches_participant2_id_season_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_winner_id_season_participants_id_fk": { + "name": "playoff_matches_winner_id_season_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_loser_id_season_participants_id_fk": { + "name": "playoff_matches_loser_id_season_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "loser_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.qualifying_point_config": { + "name": "qualifying_point_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "qualifying_point_config_sports_season_id_sports_seasons_id_fk": { + "name": "qualifying_point_config_sports_season_id_sports_seasons_id_fk", + "tableFrom": "qualifying_point_config", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.regular_season_standings": { + "name": "regular_season_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "wins": { + "name": "wins", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "losses": { + "name": "losses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ot_losses": { + "name": "ot_losses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ties": { + "name": "ties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "table_points": { + "name": "table_points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "goals_for": { + "name": "goals_for", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "goals_against": { + "name": "goals_against", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "goal_difference": { + "name": "goal_difference", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "win_pct": { + "name": "win_pct", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "games_played": { + "name": "games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "games_back": { + "name": "games_back", + "type": "numeric(5, 1)", + "primaryKey": false, + "notNull": false + }, + "conference": { + "name": "conference", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "division": { + "name": "division", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "conference_rank": { + "name": "conference_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "division_rank": { + "name": "division_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "league_rank": { + "name": "league_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "streak": { + "name": "streak", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "last_ten": { + "name": "last_ten", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "home_record": { + "name": "home_record", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "away_record": { + "name": "away_record", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "external_team_id": { + "name": "external_team_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "srs": { + "name": "srs", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rss_participant_season_idx": { + "name": "rss_participant_season_idx", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "regular_season_standings_participant_id_season_participants_id_fk": { + "name": "regular_season_standings_participant_id_season_participants_id_fk", + "tableFrom": "regular_season_standings", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "regular_season_standings_sports_season_id_sports_seasons_id_fk": { + "name": "regular_season_standings_sports_season_id_sports_seasons_id_fk", + "tableFrom": "regular_season_standings", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scoring_events": { + "name": "scoring_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tournament_id": { + "name": "tournament_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "event_starts_at": { + "name": "event_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "playoff_round": { + "name": "playoff_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_qualifying_event": { + "name": "is_qualifying_event", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bracket_template_id": { + "name": "bracket_template_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "scoring_starts_at_round": { + "name": "scoring_starts_at_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "bracket_region_config": { + "name": "bracket_region_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scoring_events_sports_season_id_sports_seasons_id_fk": { + "name": "scoring_events_sports_season_id_sports_seasons_id_fk", + "tableFrom": "scoring_events", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scoring_events_tournament_id_tournaments_id_fk": { + "name": "scoring_events_tournament_id_tournaments_id_fk", + "tableFrom": "scoring_events", + "tableTo": "tournaments", + "columnsFrom": [ + "tournament_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scoring_events_qualifying_require_tournament": { + "name": "scoring_events_qualifying_require_tournament", + "value": "NOT \"scoring_events\".\"is_qualifying_event\" OR \"scoring_events\".\"tournament_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.season_matches": { + "name": "season_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant1_score": { + "name": "participant1_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "match_stage": { + "name": "match_stage", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "match_round": { + "name": "match_round", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "matchday": { + "name": "matchday", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_series": { + "name": "is_series", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "match_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "external_match_id": { + "name": "external_match_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_matches_external_id_unique": { + "name": "season_matches_external_id_unique", + "columns": [ + { + "expression": "external_match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"season_matches\".\"external_match_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "season_matches_sports_season_status_idx": { + "name": "season_matches_sports_season_status_idx", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "season_matches_sports_season_stage_round_idx": { + "name": "season_matches_sports_season_stage_round_idx", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_round", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "season_matches_scoring_event_idx": { + "name": "season_matches_scoring_event_idx", + "columns": [ + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_matches_sports_season_id_sports_seasons_id_fk": { + "name": "season_matches_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_matches", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_matches_scoring_event_id_scoring_events_id_fk": { + "name": "season_matches_scoring_event_id_scoring_events_id_fk", + "tableFrom": "season_matches", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_matches_participant1_id_season_participants_id_fk": { + "name": "season_matches_participant1_id_season_participants_id_fk", + "tableFrom": "season_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "season_matches_participant2_id_season_participants_id_fk": { + "name": "season_matches_participant2_id_season_participants_id_fk", + "tableFrom": "season_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "season_matches_winner_id_season_participants_id_fk": { + "name": "season_matches_winner_id_season_participants_id_fk", + "tableFrom": "season_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_expected_values": { + "name": "season_participant_expected_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "probability_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'manual'" + }, + "source_odds": { + "name": "source_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_elo": { + "name": "source_elo", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "world_ranking": { + "name": "world_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_ev_participant_season_unique": { + "name": "participant_ev_participant_season_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_participant_expected_values_participant_id_season_participants_id_fk": { + "name": "season_participant_expected_values_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_expected_values", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_expected_values_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_expected_values_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_expected_values", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_golf_skills": { + "name": "season_participant_golf_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sg_total": { + "name": "sg_total", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "datagolf_rank": { + "name": "datagolf_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "masters_odds": { + "name": "masters_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "us_open_odds": { + "name": "us_open_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "open_championship_odds": { + "name": "open_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pga_championship_odds": { + "name": "pga_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_participant_golf_skills_unique": { + "name": "season_participant_golf_skills_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_participant_golf_skills_participant_id_season_participants_id_fk": { + "name": "season_participant_golf_skills_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_golf_skills", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_golf_skills_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_golf_skills_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_golf_skills", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_qualifying_totals": { + "name": "season_participant_qualifying_totals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_qualifying_points": { + "name": "total_qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "events_scored": { + "name": "events_scored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "final_ranking": { + "name": "final_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_participant_qualifying_totals_participant_id_season_participants_id_fk": { + "name": "season_participant_qualifying_totals_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_qualifying_totals", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_qualifying_totals_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_qualifying_totals_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_qualifying_totals", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_results": { + "name": "season_participant_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "final_position": { + "name": "final_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_partial_score": { + "name": "is_partial_score", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qualifying_points": { + "name": "qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_participant_results_participant_id_season_participants_id_fk": { + "name": "season_participant_results_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_results", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_results_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_simulator_inputs": { + "name": "season_participant_simulator_inputs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_odds": { + "name": "source_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_elo": { + "name": "source_elo", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "world_ranking": { + "name": "world_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "projected_wins": { + "name": "projected_wins", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_table_points": { + "name": "projected_table_points", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "seed": { + "name": "seed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_participant_simulator_inputs_unique": { + "name": "season_participant_simulator_inputs_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "season_participant_simulator_inputs_season_idx": { + "name": "season_participant_simulator_inputs_season_idx", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_participant_simulator_inputs_participant_id_season_participants_id_fk": { + "name": "season_participant_simulator_inputs_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_simulator_inputs", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_simulator_inputs_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_simulator_inputs_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_simulator_inputs", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participants": { + "name": "season_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "vorp_value": { + "name": "vorp_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participants_sports_season_name_unique": { + "name": "participants_sports_season_name_unique", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_participants_sports_season_id_sports_seasons_id_fk": { + "name": "season_participants_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participants", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participants_participant_id_participants_id_fk": { + "name": "season_participants_participant_id_participants_id_fk", + "tableFrom": "season_participants", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_sports": { + "name": "season_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_sports_season_id_seasons_id_fk": { + "name": "season_sports_season_id_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_template_sports": { + "name": "season_template_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_template_sports_template_sport_unique": { + "name": "season_template_sports_template_sport_unique", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sport_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_template_sports_template_id_season_templates_id_fk": { + "name": "season_template_sports_template_id_season_templates_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_template_sports_sport_id_sports_id_fk": { + "name": "season_template_sports_sport_id_sports_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_templates": { + "name": "season_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_templates_name_unique": { + "name": "season_templates_name_unique", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "draft_rounds": { + "name": "draft_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "flex_spots": { + "name": "flex_spots", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "draft_date_time": { + "name": "draft_date_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auto_start_draft": { + "name": "auto_start_draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "draft_initial_time": { + "name": "draft_initial_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 120 + }, + "draft_increment_time": { + "name": "draft_increment_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "draft_timer_mode": { + "name": "draft_timer_mode", + "type": "draft_timer_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'chess_clock'" + }, + "current_pick_number": { + "name": "current_pick_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "draft_started_at": { + "name": "draft_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_completed_at": { + "name": "draft_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_paused": { + "name": "draft_paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "overnight_pause_mode": { + "name": "overnight_pause_mode", + "type": "overnight_pause_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "overnight_pause_start": { + "name": "overnight_pause_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "overnight_pause_end": { + "name": "overnight_pause_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "overnight_pause_timezone": { + "name": "overnight_pause_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "points_for_1st": { + "name": "points_for_1st", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "points_for_2nd": { + "name": "points_for_2nd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "points_for_3rd": { + "name": "points_for_3rd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "points_for_4th": { + "name": "points_for_4th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "points_for_5th": { + "name": "points_for_5th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_6th": { + "name": "points_for_6th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_7th": { + "name": "points_for_7th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "points_for_8th": { + "name": "points_for_8th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seasons_template_id_season_templates_id_fk": { + "name": "seasons_template_id_season_templates_id_fk", + "tableFrom": "seasons", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "seasons_invite_code_unique": { + "name": "seasons_invite_code_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.simulator_profiles": { + "name": "simulator_profiles", + "schema": "", + "columns": { + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": true, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_config": { + "name": "default_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports": { + "name": "sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sport_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "exclude_from_homepage": { + "name": "exclude_from_homepage", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sports_slug_unique": { + "name": "sports_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_season_simulator_configs": { + "name": "sports_season_simulator_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sports_season_simulator_config_unique": { + "name": "sports_season_simulator_config_unique", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sports_season_simulator_config_type_idx": { + "name": "sports_season_simulator_config_type_idx", + "columns": [ + { + "expression": "simulator_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sports_season_simulator_configs_sports_season_id_sports_seasons_id_fk": { + "name": "sports_season_simulator_configs_sports_season_id_sports_seasons_id_fk", + "tableFrom": "sports_season_simulator_configs", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_seasons": { + "name": "sports_seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fantasy_season_id": { + "name": "fantasy_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sports_season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'upcoming'" + }, + "external_season_id": { + "name": "external_season_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "scoring_type": { + "name": "scoring_type", + "type": "scoring_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "scoring_pattern": { + "name": "scoring_pattern", + "type": "scoring_pattern", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "total_majors": { + "name": "total_majors", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "majors_completed": { + "name": "majors_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "qualifying_points_finalized": { + "name": "qualifying_points_finalized", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "elo_calibration_exponent": { + "name": "elo_calibration_exponent", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "elo_min_rating": { + "name": "elo_min_rating", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1250 + }, + "elo_max_rating": { + "name": "elo_max_rating", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1750 + }, + "simulation_status": { + "name": "simulation_status", + "type": "simulation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "standings_last_changed_at": { + "name": "standings_last_changed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_simulated_at": { + "name": "last_simulated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_on": { + "name": "draft_on", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "draft_off": { + "name": "draft_off", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sports_seasons_sport_id_sports_id_fk": { + "name": "sports_seasons_sport_id_sports_id_fk", + "tableFrom": "sports_seasons", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sports_seasons_fantasy_season_id_seasons_id_fk": { + "name": "sports_seasons_fantasy_season_id_seasons_id_fk", + "tableFrom": "sports_seasons", + "tableTo": "seasons", + "columnsFrom": [ + "fantasy_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_ev_snapshots": { + "name": "team_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_ev_snapshots_unique": { + "name": "team_ev_snapshots_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_ev_snapshots_team_id_teams_id_fk": { + "name": "team_ev_snapshots_team_id_teams_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_ev_snapshots_season_id_seasons_id_fk": { + "name": "team_ev_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_score_events": { + "name": "team_score_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scoring_event_name": { + "name": "scoring_event_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "sport_name": { + "name": "sport_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "match_id": { + "name": "match_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant_ids": { + "name": "participant_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "points_delta": { + "name": "points_delta", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_score_events_match_unique": { + "name": "team_score_events_match_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "match_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_score_events_event_unique": { + "name": "team_score_events_event_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "match_id IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_score_events_team_id_teams_id_fk": { + "name": "team_score_events_team_id_teams_id_fk", + "tableFrom": "team_score_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_score_events_season_id_seasons_id_fk": { + "name": "team_score_events_season_id_seasons_id_fk", + "tableFrom": "team_score_events", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_score_events_scoring_event_id_scoring_events_id_fk": { + "name": "team_score_events_scoring_event_id_scoring_events_id_fk", + "tableFrom": "team_score_events", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "team_score_events_match_id_playoff_matches_id_fk": { + "name": "team_score_events_match_id_playoff_matches_id_fk", + "tableFrom": "team_score_events", + "tableTo": "playoff_matches", + "columnsFrom": [ + "match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_sport_scores": { + "name": "team_sport_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "participants_completed": { + "name": "participants_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_total": { + "name": "participants_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_sport_scores_team_id_teams_id_fk": { + "name": "team_sport_scores_team_id_teams_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_sport_scores_sports_season_id_sports_seasons_id_fk": { + "name": "team_sport_scores_sports_season_id_sports_seasons_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings": { + "name": "team_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_rank": { + "name": "current_rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_rank": { + "name": "previous_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_team_id_teams_id_fk": { + "name": "team_standings_team_id_teams_id_fk", + "tableFrom": "team_standings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_season_id_seasons_id_fk": { + "name": "team_standings_season_id_seasons_id_fk", + "tableFrom": "team_standings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings_snapshots": { + "name": "team_standings_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_standings_snapshots_unique": { + "name": "team_standings_snapshots_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_standings_snapshots_team_id_teams_id_fk": { + "name": "team_standings_snapshots_team_id_teams_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_snapshots_season_id_seasons_id_fk": { + "name": "team_standings_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "flag_config": { + "name": "flag_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "avatar_type": { + "name": "avatar_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'owner'" + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "teams_season_id_lower_name_unique": { + "name": "teams_season_id_lower_name_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_group_members": { + "name": "tournament_group_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_group_id": { + "name": "tournament_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tournament_group_members_tournament_group_id_tournament_groups_id_fk": { + "name": "tournament_group_members_tournament_group_id_tournament_groups_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "tournament_groups", + "columnsFrom": [ + "tournament_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tournament_group_members_participant_id_season_participants_id_fk": { + "name": "tournament_group_members_participant_id_season_participants_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_groups": { + "name": "tournament_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tournament_groups_scoring_event_id_idx": { + "name": "tournament_groups_scoring_event_id_idx", + "columns": [ + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tournament_groups_scoring_event_id_scoring_events_id_fk": { + "name": "tournament_groups_scoring_event_id_scoring_events_id_fk", + "tableFrom": "tournament_groups", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_results": { + "name": "tournament_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_id": { + "name": "tournament_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tournament_results_tournament_participant_unique": { + "name": "tournament_results_tournament_participant_unique", + "columns": [ + { + "expression": "tournament_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tournament_results_tournament_id_tournaments_id_fk": { + "name": "tournament_results_tournament_id_tournaments_id_fk", + "tableFrom": "tournament_results", + "tableTo": "tournaments", + "columnsFrom": [ + "tournament_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tournament_results_participant_id_participants_id_fk": { + "name": "tournament_results_participant_id_participants_id_fk", + "tableFrom": "tournament_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournaments": { + "name": "tournaments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "tournament_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "external_key": { + "name": "external_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tournaments_sport_name_year_unique": { + "name": "tournaments_sport_name_year_unique", + "columns": [ + { + "expression": "sport_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tournaments_sport_id_sports_id_fk": { + "name": "tournaments_sport_id_sports_id_fk", + "tableFrom": "tournaments", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "flag_config": { + "name": "flag_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_avatar_url": { + "name": "custom_avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "avatar_type": { + "name": "avatar_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'flag'" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "discord_ping_enabled": { + "name": "discord_ping_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "draft_email_notifications_enabled": { + "name": "draft_email_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_data_request_at": { + "name": "last_data_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_username_lower_unique": { + "name": "users_username_lower_unique", + "columns": [ + { + "expression": "lower(\"username\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.watchlist": { + "name": "watchlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "watchlist_season_team_participant_unique": { + "name": "watchlist_season_team_participant_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "watchlist_season_id_seasons_id_fk": { + "name": "watchlist_season_id_seasons_id_fk", + "tableFrom": "watchlist", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "watchlist_team_id_teams_id_fk": { + "name": "watchlist_team_id_teams_id_fk", + "tableFrom": "watchlist", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "watchlist_participant_id_season_participants_id_fk": { + "name": "watchlist_participant_id_season_participants_id_fk", + "tableFrom": "watchlist", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.audit_action": { + "name": "audit_action", + "schema": "public", + "values": [ + "league_settings_changed", + "draft_settings_changed", + "scoring_rules_changed", + "sports_changed", + "draft_order_set", + "draft_order_randomized", + "draft_started", + "draft_paused", + "draft_resumed", + "draft_reset", + "draft_rollback", + "force_autopick", + "force_manual_pick", + "draft_pick_changed", + "time_bank_edited", + "brackt_resolved" + ] + }, + "public.autodraft_mode": { + "name": "autodraft_mode", + "schema": "public", + "values": [ + "next_pick", + "while_on" + ] + }, + "public.draft_timer_mode": { + "name": "draft_timer_mode", + "schema": "public", + "values": [ + "chess_clock", + "standard" + ] + }, + "public.event_type": { + "name": "event_type", + "schema": "public", + "values": [ + "playoff_game", + "major_tournament", + "final_standings", + "schedule_event" + ] + }, + "public.match_status": { + "name": "match_status", + "schema": "public", + "values": [ + "scheduled", + "in_progress", + "complete", + "canceled", + "postponed" + ] + }, + "public.overnight_pause_mode": { + "name": "overnight_pause_mode", + "schema": "public", + "values": [ + "none", + "league", + "per_user" + ] + }, + "public.picked_by_type": { + "name": "picked_by_type", + "schema": "public", + "values": [ + "owner", + "commissioner", + "admin", + "auto" + ] + }, + "public.playoff_match_game_status": { + "name": "playoff_match_game_status", + "schema": "public", + "values": [ + "scheduled", + "complete", + "postponed" + ] + }, + "public.probability_source": { + "name": "probability_source", + "schema": "public", + "values": [ + "manual", + "futures_odds", + "elo_simulation", + "performance_model" + ] + }, + "public.scoring_pattern": { + "name": "scoring_pattern", + "schema": "public", + "values": [ + "playoff_bracket", + "season_standings", + "qualifying_points" + ] + }, + "public.scoring_type": { + "name": "scoring_type", + "schema": "public", + "values": [ + "playoffs", + "regular_season", + "majors" + ] + }, + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + }, + "public.simulation_status": { + "name": "simulation_status", + "schema": "public", + "values": [ + "idle", + "running", + "failed" + ] + }, + "public.simulator_type": { + "name": "simulator_type", + "schema": "public", + "values": [ + "f1_standings", + "indycar_standings", + "golf_qualifying_points", + "playoff_bracket", + "ucl_bracket", + "ncaam_bracket", + "ncaaw_bracket", + "nba_bracket", + "nhl_bracket", + "nfl_bracket", + "afl_bracket", + "epl_standings", + "snooker_bracket", + "tennis_qualifying_points", + "mlb_bracket", + "wnba_bracket", + "world_cup", + "darts_bracket", + "cs2_major_qualifying_points", + "ncaa_football_bracket", + "llws_bracket", + "college_hockey_bracket", + "brackt", + "nll_bracket", + "mls_bracket" + ] + }, + "public.sport_type": { + "name": "sport_type", + "schema": "public", + "values": [ + "team", + "individual" + ] + }, + "public.sports_season_status": { + "name": "sports_season_status", + "schema": "public", + "values": [ + "upcoming", + "active", + "completed" + ] + }, + "public.tournament_status": { + "name": "tournament_status", + "schema": "public", + "values": [ + "scheduled", + "in_progress", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index d31049a..8640b1f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -855,6 +855,13 @@ "when": 1781296473277, "tag": "0121_even_gambit", "breakpoints": true + }, + { + "idx": 122, + "version": "7", + "when": 1782103950846, + "tag": "0122_soft_longshot", + "breakpoints": true } ] } \ No newline at end of file diff --git a/scripts/backfill-major-linking.ts b/scripts/backfill-major-linking.ts new file mode 100644 index 0000000..be9313a --- /dev/null +++ b/scripts/backfill-major-linking.ts @@ -0,0 +1,234 @@ +/** + * Backfill: unify existing majors for "score once, fan out everywhere". + * + * Existing data predates the primary-event model and the completion fan-out, so + * it's in three inconsistent states. This script reconciles them idempotently: + * + * 1. Link orphaned qualifying events (tournament_id IS NULL) to a canonical + * tournament via extractTournamentIdentity + upsertTournament. Same-major + * windows collapse onto one canonical row (unique sportId+name+year). + * 2. Designate a primary scoring_event per BRACKET major (tennis/CS2) — the + * window holding the actual bracket/stage/results work. Golf is left with no + * primary (it's scored on the canonical tournament page). + * 3. Backfill canonical tournament_results from the chosen source window's + * event_results (placement → canonical participant), and set tournament + * status from whether that window is complete. + * 4. Reconcile every sibling window via syncTournamentResults, then recompute + * each sports_season's majors_completed from its completed qualifying events. + * + * Safe to re-run. Validate on a DB snapshot first. Reads DATABASE_URL. + * + * npx tsx scripts/backfill-major-linking.ts # apply + * npx tsx scripts/backfill-major-linking.ts --dry # report only + */ + +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { and, eq, isNull } from "drizzle-orm"; +import * as schema from "../database/schema.js"; +import { DatabaseContext, database } from "../database/context.js"; +import { extractTournamentIdentity } from "../app/lib/tournament-identity.js"; +import { isBracketMajor } from "../app/lib/event-utils.js"; +import { upsertTournament, updateTournamentStatus } from "../app/models/tournament.js"; +import { upsertTournamentResult } from "../app/models/tournament-result.js"; +import { getEventResults } from "../app/models/event-result.js"; +import { findPlayoffMatchesByEventId } from "../app/models/playoff-match.js"; +import { getCs2StageResultsMapForEvent } from "../app/models/cs2-major-stage.js"; +import { syncTournamentResults, syncMajorFromPrimaryEvent } from "../app/services/sync-tournament-results.js"; + +const DRY = process.argv.includes("--dry"); +const log = (...a: unknown[]) => console.log(...a); + +async function run() { + const db = database(); + + // ── Step 1: link orphaned qualifying events ─────────────────────────────── + const orphans = await db.query.scoringEvents.findMany({ + where: and( + eq(schema.scoringEvents.isQualifyingEvent, true), + isNull(schema.scoringEvents.tournamentId) + ), + with: { sportsSeason: true }, + }); + log(`\n[1] Orphaned qualifying events (no tournament link): ${orphans.length}`); + for (const ev of orphans) { + try { + const identity = extractTournamentIdentity({ + name: ev.name, + eventDate: ev.eventDate, + eventType: ev.eventType, + }); + log(` • "${ev.name}" → ${identity.name} ${identity.year}`); + if (!DRY) { + const tournament = await upsertTournament({ + sportId: ev.sportsSeason.sportId, + name: identity.name, + year: identity.year, + startsAt: ev.eventStartsAt ?? null, + }); + await db + .update(schema.scoringEvents) + .set({ tournamentId: tournament.id, updatedAt: new Date() }) + .where(eq(schema.scoringEvents.id, ev.id)); + } + } catch (e) { + log(` ! could not parse "${ev.name}" — link by hand: ${(e as Error).message}`); + } + } + + // ── Gather all tournaments that now have linked events ───────────────────── + const linkedEvents = await db.query.scoringEvents.findMany({ + where: eq(schema.scoringEvents.isQualifyingEvent, true), + with: { sportsSeason: { with: { sport: true } } }, + }); + const byTournament = new Map(); + for (const ev of linkedEvents) { + if (!ev.tournamentId) continue; + const arr = byTournament.get(ev.tournamentId) ?? []; + arr.push(ev); + byTournament.set(ev.tournamentId, arr); + } + log(`\n[2-4] Tournaments with linked events: ${byTournament.size}`); + + // How "scored" a window is, used to pick the source/primary window. + async function scoredWeight(eventId: string): Promise { + const [results, matches, stages] = await Promise.all([ + getEventResults(eventId), + findPlayoffMatchesByEventId(eventId), + getCs2StageResultsMapForEvent(eventId), + ]); + const placed = results.filter((r) => r.placement !== null).length; + return placed + matches.length + stages.size; + } + + const touchedSportsSeasons = new Set(); + + for (const [tournamentId, events] of byTournament) { + const simulatorType = events[0]?.sportsSeason?.sport?.simulatorType ?? null; + const bracketMajor = isBracketMajor(simulatorType); + + // Choose the source window: most-scored, tie-break earliest created. + const weighted = await Promise.all( + events.map(async (e) => ({ e, w: await scoredWeight(e.id) })) + ); + weighted.sort( + (a, b) => + b.w - a.w || + a.e.createdAt.getTime() - b.e.createdAt.getTime() + ); + const source = weighted[0]?.e; + if (!source) continue; + + const tName = `${source.name} [${bracketMajor ? simulatorType : "golf/placement"}]`; + log(`\n ${tName}: ${events.length} window(s), source=${source.id} (weight ${weighted[0].w})`); + + // Step 2: designate primary for bracket majors only. + if (bracketMajor && !DRY) { + for (const { e } of weighted) { + const shouldBePrimary = e.id === source.id; + if (e.isPrimary !== shouldBePrimary) { + await db + .update(schema.scoringEvents) + .set({ isPrimary: shouldBePrimary, updatedAt: new Date() }) + .where(eq(schema.scoringEvents.id, e.id)); + } + } + log(` primary → ${source.id}`); + } + + // Steps 3+4: backfill canonical results and reconcile siblings. + const isComplete = source.isComplete; + if (bracketMajor) { + // Bracket majors go through the exact live path: promote the primary's + // derived results to canonical (deleting any stale rows) and fan out to + // siblings, skipping the primary which is scored in place. + if (!DRY) { + const report = await syncMajorFromPrimaryEvent(source.id, { + markComplete: isComplete, + }); + await updateTournamentStatus( + tournamentId, + report.windowsFailed === 0 && isComplete ? "completed" : "in_progress" + ); + log( + ` synced via primary: ${report.windowsSynced} ok, ${report.windowsFailed} failed` + ); + } else { + log(` (dry) would sync via primary ${source.id}`); + } + } else { + // Golf/placement: promote the source window's placements to canonical, + // then fan to every window (no primary to skip). + const sourceResults = await getEventResults(source.id); + let promoted = 0; + for (const r of sourceResults) { + if (r.placement === null || r.notParticipating) continue; + const canonical = r.seasonParticipant?.participantId; + if (!canonical) continue; + if (!DRY) { + await upsertTournamentResult({ + tournamentId, + participantId: canonical, + placement: r.placement, + rawScore: r.rawScore, + }); + } + promoted += 1; + } + log(` canonical results promoted: ${promoted}`); + if (!DRY && promoted > 0) { + const report = await syncTournamentResults(tournamentId, { + markComplete: isComplete, + }); + await updateTournamentStatus( + tournamentId, + report.windowsFailed === 0 && isComplete ? "completed" : "in_progress" + ); + log(` synced: ${report.windowsSynced} ok, ${report.windowsFailed} failed`); + } + } + + for (const e of events) touchedSportsSeasons.add(e.sportsSeasonId); + } + + // ── Recompute majors_completed from completed qualifying events ──────────── + log(`\n[5] Recomputing majors_completed for ${touchedSportsSeasons.size} sports season(s)`); + for (const sportsSeasonId of touchedSportsSeasons) { + const completed = await db.query.scoringEvents.findMany({ + where: and( + eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId), + eq(schema.scoringEvents.isQualifyingEvent, true), + eq(schema.scoringEvents.isComplete, true) + ), + }); + if (!DRY) { + await db + .update(schema.sportsSeasons) + .set({ majorsCompleted: completed.length, updatedAt: new Date() }) + .where(eq(schema.sportsSeasons.id, sportsSeasonId)); + } + log(` • ${sportsSeasonId}: majorsCompleted = ${completed.length}`); + } + + log(`\nDone${DRY ? " (dry run — no writes)" : ""}.`); +} + +async function main() { + const dbUrl = process.env.DATABASE_URL; + if (!dbUrl) { + console.error("ERROR: DATABASE_URL is required"); + process.exit(1); + } + const client = postgres(dbUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + await DatabaseContext.run(db, run); + } finally { + await client.end(); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +});