diff --git a/app/components/BatchResultEntry.tsx b/app/components/BatchResultEntry.tsx index 9152639..6f2ccd3 100644 --- a/app/components/BatchResultEntry.tsx +++ b/app/components/BatchResultEntry.tsx @@ -46,12 +46,18 @@ interface BatchResultEntryProps { participants: Participant[]; sportsSeasonId: string; existingResultParticipantIds: Set; + /** + * Form intent name posted on "Save All". Defaults to "batch-add-results" for + * the existing per-window flow. Canonical tournament pages override this. + */ + intent?: string; } export function BatchResultEntry({ participants, sportsSeasonId, existingResultParticipantIds, + intent = "batch-add-results", }: BatchResultEntryProps) { const [stage, setStage] = useState("idle"); const [pasteText, setPasteText] = useState(""); @@ -172,7 +178,7 @@ export function BatchResultEntry({ placement: r.placement, })); const formData = new FormData(); - formData.set("intent", "batch-add-results"); + formData.set("intent", intent); formData.set("results", JSON.stringify(results)); saveFetcher.submit(formData, { method: "post" }); } diff --git a/app/lib/tournament-identity.ts b/app/lib/tournament-identity.ts new file mode 100644 index 0000000..0292908 --- /dev/null +++ b/app/lib/tournament-identity.ts @@ -0,0 +1,50 @@ +/** + * Extracts the canonical tournament identity — `(name, year)` — from a scoring + * event. Used both by the Phase 2 backfill and by the admin create-event flow + * that auto-provisions a canonical tournament for new qualifying events. + * + * Rules: + * - If the event name ends with a 4-digit year, strip it; that year wins. + * - Otherwise, fall back to the year portion of eventDate (YYYY-MM-DD). + * - If neither produces a year, throw. + */ + +export interface ScoringEventIdentityInput { + name: string; + eventDate: string | Date | null | undefined; + eventType: string; +} + +export interface TournamentIdentity { + name: string; + year: number; +} + +const YEAR_SUFFIX = /\s+(\d{4})\s*$/; + +export function extractTournamentIdentity(ev: ScoringEventIdentityInput): TournamentIdentity { + const trimmed = ev.name.trim(); + const yearMatch = trimmed.match(YEAR_SUFFIX); + let cleanName = trimmed; + let year: number | null = null; + + if (yearMatch) { + year = parseInt(yearMatch[1], 10); + cleanName = trimmed.replace(YEAR_SUFFIX, "").trim(); + } + + if (year === null && ev.eventDate != null) { + const iso = ev.eventDate instanceof Date + ? ev.eventDate.toISOString() + : String(ev.eventDate); + year = parseInt(iso.slice(0, 4), 10); + } + + if (year === null || Number.isNaN(year)) { + throw new Error( + `cannot determine year for event "${ev.name}" — no year in name and eventDate is null`, + ); + } + + return { name: cleanName, year }; +} diff --git a/app/models/__tests__/surface-elo.test.ts b/app/models/__tests__/surface-elo.test.ts index 307a87a..017543b 100644 --- a/app/models/__tests__/surface-elo.test.ts +++ b/app/models/__tests__/surface-elo.test.ts @@ -42,10 +42,10 @@ describe("getSurfaceEloMap", () => { expect(result.size).toBe(0); }); - it("returns a Map keyed by participantId", async () => { + it("returns a Map keyed by seasonParticipantId", async () => { mockDb.where.mockResolvedValue([ - { participantId: "p1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 }, - { participantId: "p2", worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: null }, + { seasonParticipantId: "p1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 }, + { seasonParticipantId: "p2", worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: null }, ]); const result = await getSurfaceEloMap("season-1"); expect(result.size).toBe(2); @@ -55,7 +55,7 @@ describe("getSurfaceEloMap", () => { it("preserves null surface Elos (does not coerce to 0)", async () => { mockDb.where.mockResolvedValue([ - { participantId: "p1", worldRanking: null, eloHard: null, eloClay: null, eloGrass: null }, + { seasonParticipantId: "p1", worldRanking: null, eloHard: null, eloClay: null, eloGrass: null }, ]); const result = await getSurfaceEloMap("season-1"); const entry = result.get("p1"); @@ -72,7 +72,13 @@ describe("batchUpsertSurfaceElos", () => { expect(mockDb.insert).not.toHaveBeenCalled(); }); - it("calls insert with the correct values", async () => { + it("calls insert with the correct values on the per-window table", async () => { + // The function first upserts to the per-window table, then reads + // season_participants for canonical-id lookup. Make the follow-up select + // return empty so the canonical write path short-circuits and we only + // observe the per-window insert in this assertion. + mockDb.where.mockResolvedValue([]); + const inputs = [ { participantId: "p1", sportsSeasonId: "s1", worldRanking: 1, eloHard: 1800, eloClay: 1700, eloGrass: 1600 }, { participantId: "p2", sportsSeasonId: "s1", worldRanking: null, eloHard: null, eloClay: 1750, eloGrass: undefined }, @@ -95,11 +101,36 @@ describe("batchUpsertSurfaceElos", () => { }); it("uses onConflictDoUpdate for upsert behaviour", async () => { + mockDb.where.mockResolvedValue([]); // no canonical links; only per-window write + await batchUpsertSurfaceElos([ { participantId: "p1", sportsSeasonId: "s1", worldRanking: 5, eloHard: 2000 }, ]); expect(mockDb.onConflictDoUpdate).toHaveBeenCalledOnce(); }); + + it("mirrors writes to canonical participant_surface_elos when linked", async () => { + // season_participants lookup returns canonical links for p1 only. + mockDb.where.mockResolvedValue([ + { id: "p1", canonicalId: "canon-1" }, + ]); + + await batchUpsertSurfaceElos([ + { participantId: "p1", sportsSeasonId: "s1", worldRanking: 5, eloHard: 2000 }, + ]); + + // One per-window insert, one canonical insert → two inserts total. + expect(mockDb.insert).toHaveBeenCalledTimes(2); + expect(mockDb.onConflictDoUpdate).toHaveBeenCalledTimes(2); + + const canonicalValues = mockDb.values.mock.calls[1][0] as Array<{ + participantId: string; + eloHard: number | null; + }>; + expect(canonicalValues).toHaveLength(1); + expect(canonicalValues[0].participantId).toBe("canon-1"); + expect(canonicalValues[0].eloHard).toBe(2000); + }); }); describe("getSurfaceElosForSeason", () => { diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index e5dfb15..f0ea0b2 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -24,6 +24,8 @@ export interface CreateScoringEventData { eventStartsAt?: Date; eventType: EventType; isQualifyingEvent?: boolean; + /** Required when isQualifyingEvent=true (DB check constraint). */ + tournamentId?: string; // Template system fields (Phase 2.6) bracketTemplateId?: string; scoringStartsAtRound?: string; @@ -59,6 +61,7 @@ export async function createScoringEvent( eventStartsAt: data.eventStartsAt, eventType: data.eventType, isQualifyingEvent: data.isQualifyingEvent || false, + tournamentId: data.tournamentId, bracketTemplateId: data.bracketTemplateId, scoringStartsAtRound: data.scoringStartsAtRound, isComplete: false, @@ -743,7 +746,7 @@ export async function getEventsForDates( */ export async function bulkCreateScoringEvents( sportsSeasonId: string, - events: Array<{ name: string; eventDate?: Date; eventStartsAt?: Date; eventType: EventType; isQualifyingEvent?: boolean }>, + events: Array<{ name: string; eventDate?: Date; eventStartsAt?: Date; eventType: EventType; isQualifyingEvent?: boolean; tournamentId?: string }>, providedDb?: ReturnType ) { const db = providedDb || database(); @@ -757,6 +760,7 @@ export async function bulkCreateScoringEvents( eventStartsAt: e.eventStartsAt, eventType: e.eventType, isQualifyingEvent: e.isQualifyingEvent ?? false, + tournamentId: e.tournamentId, isComplete: false, })); diff --git a/app/models/season-participant.ts b/app/models/season-participant.ts index 5d4850c..7655700 100644 --- a/app/models/season-participant.ts +++ b/app/models/season-participant.ts @@ -7,18 +7,86 @@ export type NewParticipant = typeof schema.seasonParticipants.$inferInsert; export async function createParticipant(data: NewParticipant): Promise { const db = database(); + + // Auto-link to canonical participant (by sportId + name) if caller didn't + // supply one. Needed so syncTournamentResults can match this roster entry + // to canonical tournament_results; without the link the participant is + // invisible to the canonical layer. + let canonicalId = data.participantId; + if (!canonicalId) { + const season = await db.query.sportsSeasons.findFirst({ + where: eq(schema.sportsSeasons.id, data.sportsSeasonId), + columns: { sportId: true }, + }); + if (season) { + const existing = await db.query.participants.findFirst({ + where: and( + eq(schema.participants.sportId, season.sportId), + eq(schema.participants.name, data.name), + ), + }); + if (existing) { + canonicalId = existing.id; + } else { + const [created] = await db + .insert(schema.participants) + .values({ sportId: season.sportId, name: data.name }) + .returning(); + canonicalId = created.id; + } + } + } + const [participant] = await db .insert(schema.seasonParticipants) - .values(data) + .values({ ...data, participantId: canonicalId }) .returning(); return participant; } export async function createManyParticipants(data: NewParticipant[]): Promise { const db = database(); + if (data.length === 0) return []; + + // Build canonical links per row, just like createParticipant does. + const seasonIds = Array.from(new Set(data.map((d) => d.sportsSeasonId))); + const seasons = await db.query.sportsSeasons.findMany({ + where: inArray(schema.sportsSeasons.id, seasonIds), + columns: { id: true, sportId: true }, + }); + const sportBySeason = new Map(seasons.map((s) => [s.id, s.sportId])); + + const enriched: NewParticipant[] = []; + for (const row of data) { + if (row.participantId) { + enriched.push(row); + continue; + } + const sportId = sportBySeason.get(row.sportsSeasonId); + if (!sportId) { + enriched.push(row); + continue; + } + const existing = await db.query.participants.findFirst({ + where: and( + eq(schema.participants.sportId, sportId), + eq(schema.participants.name, row.name), + ), + }); + let canonicalId = existing?.id; + if (!canonicalId) { + const [created] = await db + .insert(schema.participants) + .values({ sportId, name: row.name }) + .returning(); + canonicalId = created.id; + } + enriched.push({ ...row, participantId: canonicalId }); + } + return await db .insert(schema.seasonParticipants) - .values(data) + .values(enriched) .returning(); } diff --git a/app/models/surface-elo.ts b/app/models/surface-elo.ts index edb4a27..83258c2 100644 --- a/app/models/surface-elo.ts +++ b/app/models/surface-elo.ts @@ -7,8 +7,8 @@ */ import { database } from "~/database/context"; -import { seasonParticipantSurfaceElos, seasonParticipants } from "~/database/schema"; -import { eq, sql } from "drizzle-orm"; +import { participantSurfaceElos, seasonParticipantSurfaceElos, seasonParticipants } from "~/database/schema"; +import { eq, inArray, sql } from "drizzle-orm"; export type CourtSurface = "hard" | "clay" | "grass"; @@ -99,28 +99,80 @@ export async function batchUpsertSurfaceElos( updatedAt: sql`excluded.updated_at`, }, }); + + // Mirror writes to the canonical participant_surface_elos table. + // The simulator reads from canonical, so per-window-only edits would + // not affect simulations. Remove the per-window path entirely in Phase 4. + const sps = await db + .select({ id: seasonParticipants.id, canonicalId: seasonParticipants.participantId }) + .from(seasonParticipants) + .where(inArray(seasonParticipants.id, inputs.map((i) => i.participantId))); + const canonicalByInputId = new Map(sps.map((sp) => [sp.id, sp.canonicalId])); + + const canonicalRows = inputs + .map((i) => ({ + canonicalId: canonicalByInputId.get(i.participantId), + worldRanking: i.worldRanking ?? null, + eloHard: i.eloHard ?? null, + eloClay: i.eloClay ?? null, + eloGrass: i.eloGrass ?? null, + })) + .filter((r): r is typeof r & { canonicalId: string } => typeof r.canonicalId === "string"); + + if (canonicalRows.length === 0) return; + + await db + .insert(participantSurfaceElos) + .values(canonicalRows.map(({ canonicalId, ...rest }) => ({ + participantId: canonicalId, + ...rest, + updatedAt: now, + }))) + .onConflictDoUpdate({ + target: [participantSurfaceElos.participantId], + set: { + worldRanking: sql`excluded.world_ranking`, + eloHard: sql`excluded.elo_hard`, + eloClay: sql`excluded.elo_clay`, + eloGrass: sql`excluded.elo_grass`, + updatedAt: sql`excluded.updated_at`, + }, + }); } /** * Returns a Map from participantId to surface Elos for use in the simulator. * Participants with no record are absent from the map (simulator falls back to 1500). */ +/** + * Build a map keyed by seasonParticipant.id → surface Elo values, sourced from + * the canonical `participant_surface_elos` table. Joins seasonParticipants → + * canonical participants → canonical surfaceElo. + * + * Season participants without a linked canonical participant, or with no + * canonical surface Elo row, are simply absent from the map — callers should + * handle `undefined` for such cases (as the tennis simulator does). + */ export async function getSurfaceEloMap( sportsSeasonId: string ): Promise> { const db = database(); const rows = await db .select({ - participantId: seasonParticipantSurfaceElos.participantId, - worldRanking: seasonParticipantSurfaceElos.worldRanking, - eloHard: seasonParticipantSurfaceElos.eloHard, - eloClay: seasonParticipantSurfaceElos.eloClay, - eloGrass: seasonParticipantSurfaceElos.eloGrass, + seasonParticipantId: seasonParticipants.id, + worldRanking: participantSurfaceElos.worldRanking, + eloHard: participantSurfaceElos.eloHard, + eloClay: participantSurfaceElos.eloClay, + eloGrass: participantSurfaceElos.eloGrass, }) - .from(seasonParticipantSurfaceElos) - .where(eq(seasonParticipantSurfaceElos.sportsSeasonId, sportsSeasonId)); + .from(seasonParticipants) + .innerJoin( + participantSurfaceElos, + eq(participantSurfaceElos.participantId, seasonParticipants.participantId), + ) + .where(eq(seasonParticipants.sportsSeasonId, sportsSeasonId)); - return new Map(rows.map((r) => [r.participantId, { + return new Map(rows.map((r) => [r.seasonParticipantId, { worldRanking: r.worldRanking, eloHard: r.eloHard, eloClay: r.eloClay, diff --git a/app/models/tournament.ts b/app/models/tournament.ts index e26659c..2bf7c51 100644 --- a/app/models/tournament.ts +++ b/app/models/tournament.ts @@ -1,4 +1,4 @@ -import { eq, and, asc } from "drizzle-orm"; +import { eq, and, asc, desc } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; @@ -80,6 +80,50 @@ export async function upsertTournament( return await createTournament(data); } +export async function findAllTournamentsGroupedBySport(): Promise< + Array<{ + sport: { id: string; name: string }; + tournaments: Tournament[]; + }> +> { + const db = database(); + + const rows = await db + .select({ + tournament: schema.tournaments, + sport: { + id: schema.sports.id, + name: schema.sports.name, + }, + }) + .from(schema.tournaments) + .innerJoin(schema.sports, eq(schema.tournaments.sportId, schema.sports.id)) + .orderBy( + asc(schema.sports.name), + desc(schema.tournaments.year), + asc(schema.tournaments.startsAt) + ); + + const groupMap = new Map< + string, + { sport: { id: string; name: string }; tournaments: Tournament[] } + >(); + + for (const row of rows) { + const existing = groupMap.get(row.sport.id); + if (existing) { + existing.tournaments.push(row.tournament); + } else { + groupMap.set(row.sport.id, { + sport: row.sport, + tournaments: [row.tournament], + }); + } + } + + return Array.from(groupMap.values()); +} + export async function updateTournamentStatus( id: string, status: TournamentStatus diff --git a/app/routes.ts b/app/routes.ts index 9b784d6..9ba6e9b 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -129,6 +129,8 @@ export default [ "routes/admin.sports-seasons.$id.clone.tsx" ), route("participants", "routes/admin.participants.tsx"), + route("tournaments", "routes/admin.tournaments._index.tsx"), + route("tournaments/:id", "routes/admin.tournaments.$id.tsx"), route("templates", "routes/admin.templates.tsx"), route("templates/new", "routes/admin.templates.new.tsx"), route("templates/:id", "routes/admin.templates.$id.tsx"), diff --git a/app/routes/__tests__/admin.tournaments.$id.test.tsx b/app/routes/__tests__/admin.tournaments.$id.test.tsx new file mode 100644 index 0000000..1c9059f --- /dev/null +++ b/app/routes/__tests__/admin.tournaments.$id.test.tsx @@ -0,0 +1,182 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("~/lib/auth.server", () => ({ + auth: { api: { getSession: vi.fn() } }, +})); +vi.mock("~/models/user", () => ({ + isUserAdmin: vi.fn(), +})); +vi.mock("~/models/tournament", () => ({ + getTournamentById: vi.fn(), + updateTournamentStatus: vi.fn(), +})); +vi.mock("~/models/tournament-result", () => ({ + getTournamentResults: vi.fn(), + upsertTournamentResult: vi.fn(), +})); +vi.mock("~/models/participant", () => ({ + findCanonicalParticipantsBySport: vi.fn(), +})); +vi.mock("~/services/sync-tournament-results", () => ({ + syncTournamentResults: vi.fn(), +})); +vi.mock("~/lib/logger", () => ({ + logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn() }, +})); + +import { action } from "~/routes/admin.tournaments.$id"; +import { auth } from "~/lib/auth.server"; +import { isUserAdmin } from "~/models/user"; +import { + getTournamentById, + updateTournamentStatus, +} from "~/models/tournament"; +import { upsertTournamentResult } from "~/models/tournament-result"; +import { syncTournamentResults } from "~/services/sync-tournament-results"; + +const TOURNAMENT_ID = "tournament-1"; +const ADMIN_USER_ID = "admin-1"; +const NON_ADMIN_USER_ID = "user-1"; + +function makeRequest(body: Record) { + const formData = new FormData(); + for (const [k, v] of Object.entries(body)) { + formData.append(k, v); + } + return new Request(`http://localhost/admin/tournaments/${TOURNAMENT_ID}`, { + method: "POST", + body: formData, + }); +} + +function makeActionArgs(request: Request) { + return { + request, + params: { id: TOURNAMENT_ID }, + context: {} as never, + } as unknown as Parameters[0]; +} + +describe("admin.tournaments.$id action", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("batch-upsert-results: upserts each row, marks tournament completed, and fans out", async () => { + vi.mocked(auth.api.getSession).mockResolvedValue({ + user: { id: ADMIN_USER_ID }, + } as never); + vi.mocked(isUserAdmin).mockResolvedValue(true); + vi.mocked(getTournamentById).mockResolvedValue({ + id: TOURNAMENT_ID, + sportId: "sport-1", + name: "US Open", + year: 2026, + status: "scheduled", + } as never); + vi.mocked(upsertTournamentResult).mockResolvedValue({} as never); + vi.mocked(updateTournamentStatus).mockResolvedValue({} as never); + const syncReport = { + tournamentId: TOURNAMENT_ID, + windowsSynced: 2, + windowsFailed: 0, + failures: [], + }; + vi.mocked(syncTournamentResults).mockResolvedValue(syncReport); + + const results = [ + { participantId: "p-1", placement: 1 }, + { participantId: "p-2", placement: 2 }, + { participantId: "p-3", placement: 3 }, + ]; + const request = makeRequest({ + intent: "batch-upsert-results", + results: JSON.stringify(results), + }); + + const response = await action(makeActionArgs(request)); + + expect(upsertTournamentResult).toHaveBeenCalledTimes(3); + expect(upsertTournamentResult).toHaveBeenCalledWith({ + tournamentId: TOURNAMENT_ID, + participantId: "p-1", + placement: 1, + }); + expect(updateTournamentStatus).toHaveBeenCalledWith( + TOURNAMENT_ID, + "completed" + ); + expect(syncTournamentResults).toHaveBeenCalledWith(TOURNAMENT_ID); + expect(response).toMatchObject({ + success: true, + syncReport, + }); + }); + + it("does not re-update status when tournament is already completed", async () => { + vi.mocked(auth.api.getSession).mockResolvedValue({ + user: { id: ADMIN_USER_ID }, + } as never); + vi.mocked(isUserAdmin).mockResolvedValue(true); + vi.mocked(getTournamentById).mockResolvedValue({ + id: TOURNAMENT_ID, + sportId: "sport-1", + name: "US Open", + year: 2026, + status: "completed", + } as never); + vi.mocked(upsertTournamentResult).mockResolvedValue({} as never); + vi.mocked(syncTournamentResults).mockResolvedValue({ + tournamentId: TOURNAMENT_ID, + windowsSynced: 1, + windowsFailed: 0, + failures: [], + }); + + const request = makeRequest({ + intent: "batch-upsert-results", + results: JSON.stringify([{ participantId: "p-1", placement: 1 }]), + }); + + await action(makeActionArgs(request)); + + expect(updateTournamentStatus).not.toHaveBeenCalled(); + expect(syncTournamentResults).toHaveBeenCalledWith(TOURNAMENT_ID); + }); + + it("non-admin user is forbidden (403)", async () => { + vi.mocked(auth.api.getSession).mockResolvedValue({ + user: { id: NON_ADMIN_USER_ID }, + } as never); + vi.mocked(isUserAdmin).mockResolvedValue(false); + + const request = makeRequest({ + intent: "batch-upsert-results", + results: JSON.stringify([{ participantId: "p-1", placement: 1 }]), + }); + + await expect(action(makeActionArgs(request))).rejects.toMatchObject({ + status: 403, + }); + + expect(upsertTournamentResult).not.toHaveBeenCalled(); + expect(syncTournamentResults).not.toHaveBeenCalled(); + }); + + it("missing tournament returns 404", async () => { + vi.mocked(auth.api.getSession).mockResolvedValue({ + user: { id: ADMIN_USER_ID }, + } as never); + vi.mocked(isUserAdmin).mockResolvedValue(true); + vi.mocked(getTournamentById).mockResolvedValue(null); + + const request = makeRequest({ + intent: "batch-upsert-results", + results: JSON.stringify([{ participantId: "p-1", placement: 1 }]), + }); + + await expect(action(makeActionArgs(request))).rejects.toMatchObject({ + status: 404, + }); + }); +}); 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 c1a997b..523877c 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts @@ -27,6 +27,8 @@ import { getQPStandings, getQPConfig } from "~/models/qualifying-points"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq } from "drizzle-orm"; +import { upsertTournamentResult } from "~/models/tournament-result"; +import { syncTournamentResults } from "~/services/sync-tournament-results"; export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); @@ -372,6 +374,46 @@ export async function action({ request, params }: Route.ActionArgs) { } try { + // If this scoring event is linked to a canonical tournament, write + // results to the canonical layer and fan out via syncTournamentResults. + // This keeps sibling windows (if any) in sync automatically. + const scoringEvent = await database().query.scoringEvents.findFirst({ + where: eq(schema.scoringEvents.id, params.eventId), + }); + if (!scoringEvent) { + return { error: "Scoring event not found" }; + } + + if (scoringEvent.tournamentId) { + // Canonical path: translate season_participant.id → canonical participant.id + const spRows = allSeasonParticipants.filter((sp) => + incoming.some((r) => r.participantId === sp.id), + ); + const missingCanonical = spRows.filter((sp) => !sp.participantId); + if (missingCanonical.length > 0) { + return { + error: `Some season participants are not linked to canonical participants: ${missingCanonical.map((sp) => sp.name).join(", ")}`, + }; + } + + for (const row of incoming) { + const sp = spRows.find((s) => s.id === row.participantId); + if (!sp?.participantId) continue; + await upsertTournamentResult({ + tournamentId: scoringEvent.tournamentId, + participantId: sp.participantId, + placement: row.placement, + }); + } + + const syncReport = await syncTournamentResults(scoringEvent.tournamentId); + return { + success: `${incoming.length} result${incoming.length === 1 ? "" : "s"} saved canonically. Synced to ${syncReport.windowsSynced} window${syncReport.windowsSynced === 1 ? "" : "s"}${syncReport.windowsFailed > 0 ? ` (${syncReport.windowsFailed} failed)` : ""}.`, + syncReport, + }; + } + + // Legacy direct-write path for non-canonical events (team sports, etc.). const existingResults = await getEventResults(params.eventId); const existingIds = new Set(existingResults.map((r) => r.seasonParticipantId)); const newResults = filterNewResults(incoming, existingIds); diff --git a/app/routes/admin.sports-seasons.$id.events.server.ts b/app/routes/admin.sports-seasons.$id.events.server.ts index ff103ac..30c0094 100644 --- a/app/routes/admin.sports-seasons.$id.events.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.server.ts @@ -11,6 +11,8 @@ import { } from "~/models/scoring-event"; import { getQPStandings } from "~/models/qualifying-points"; import { finalizeQualifyingPoints } from "~/models/scoring-calculator"; +import { upsertTournament } from "~/models/tournament"; +import { extractTournamentIdentity } from "~/lib/tournament-identity"; export async function loader({ params }: Route.LoaderArgs) { @@ -107,6 +109,31 @@ export async function action({ request, params }: Route.ActionArgs) { return { error: "No valid events found. Use format: Event Name, YYYY-MM-DD" }; } + // Auto-provision canonical tournaments for qualifying events (check constraint). + try { + for (const ev of validEvents) { + if (!ev.isQualifyingEvent) continue; + const identity = extractTournamentIdentity({ + name: ev.name, + eventDate: ev.eventDate ? ev.eventDate.toISOString().slice(0, 10) : null, + eventType: ev.eventType, + }); + const tournament = await upsertTournament({ + sportId: sportsSeason.sportId, + name: identity.name, + year: identity.year, + startsAt: ev.eventStartsAt ?? null, + }); + (ev as typeof ev & { tournamentId: string }).tournamentId = tournament.id; + } + } catch (err) { + logger.error("Error linking qualifying events to canonical tournaments:", err); + return { + error: + "Could not determine tournament identity for one or more qualifying events. Include a 4-digit year or date for each.", + }; + } + try { await bulkCreateScoringEvents(params.id, validEvents); return { success: `Created ${validEvents.length} event${validEvents.length !== 1 ? "s" : ""} successfully` }; @@ -178,6 +205,31 @@ export async function action({ request, params }: Route.ActionArgs) { isQualifyingEvent, }; + // Qualifying events require a canonical tournament_id (check constraint). + // Auto-provision one by extracting (name, year) from the event name/date. + if (isQualifyingEvent) { + try { + const identity = extractTournamentIdentity({ + name: eventData.name, + eventDate: eventDate ? eventDate.toISOString().slice(0, 10) : null, + eventType, + }); + const tournament = await upsertTournament({ + sportId: sportsSeason.sportId, + name: identity.name, + year: identity.year, + startsAt: eventStartsAt ?? null, + }); + eventData.tournamentId = tournament.id; + } catch (err) { + logger.error("Error linking qualifying event to canonical tournament:", err); + return { + error: + "Could not determine tournament identity. Include a 4-digit year in the event name, or set a date.", + }; + } + } + try { const event = await createScoringEvent(eventData); return redirect(`/admin/sports-seasons/${params.id}/events/${event.id}`); diff --git a/app/routes/admin.tournaments.$id.tsx b/app/routes/admin.tournaments.$id.tsx new file mode 100644 index 0000000..d91e49b --- /dev/null +++ b/app/routes/admin.tournaments.$id.tsx @@ -0,0 +1,353 @@ +import { Link, useFetcher } from "react-router"; +import { auth } from "~/lib/auth.server"; +import type { Route } from "./+types/admin.tournaments.$id"; + +import { logger } from "~/lib/logger"; +import { + getTournamentById, + updateTournamentStatus, +} from "~/models/tournament"; +import { + getTournamentResults, + upsertTournamentResult, +} from "~/models/tournament-result"; +import { findCanonicalParticipantsBySport } from "~/models/participant"; +import { isUserAdmin } from "~/models/user"; +import { + syncTournamentResults, + type SyncReport, +} from "~/services/sync-tournament-results"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; +import { ArrowLeft, CheckCircle2, AlertTriangle } from "lucide-react"; +import { BatchResultEntry } from "~/components/BatchResultEntry"; + +export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { + return [ + { + title: `${data?.tournament?.name ?? "Tournament"} - Brackt Admin`, + }, + ]; +} + +export async function loader({ params }: Route.LoaderArgs) { + const tournament = await getTournamentById(params.id); + if (!tournament) { + throw new Response("Not Found", { status: 404 }); + } + + const [results, canonicalParticipants] = await Promise.all([ + getTournamentResults(tournament.id), + findCanonicalParticipantsBySport(tournament.sportId), + ]); + + return { tournament, results, canonicalParticipants }; +} + +export async function action(args: Route.ActionArgs) { + const { request, params } = args; + const session = await auth.api.getSession({ headers: request.headers }); + const userId = session?.user.id ?? null; + const isAdmin = userId ? await isUserAdmin(userId) : false; + if (!isAdmin) { + throw new Response("Forbidden", { status: 403 }); + } + + const tournament = await getTournamentById(params.id); + if (!tournament) { + throw new Response("Not Found", { status: 404 }); + } + + const formData = await request.formData(); + const intent = formData.get("intent"); + + if (intent === "batch-upsert-results") { + const resultsRaw = formData.get("results"); + if (typeof resultsRaw !== "string" || !resultsRaw) { + return { + success: false as const, + error: "Missing results payload", + syncReport: null, + }; + } + + let parsed: Array<{ participantId: string; placement: number }>; + try { + parsed = JSON.parse(resultsRaw); + } catch { + return { + success: false as const, + error: "Invalid JSON in results payload", + syncReport: null, + }; + } + + if (!Array.isArray(parsed)) { + return { + success: false as const, + error: "Results payload must be an array", + syncReport: null, + }; + } + + try { + for (const row of parsed) { + if ( + !row || + typeof row.participantId !== "string" || + typeof row.placement !== "number" + ) { + return { + success: false as const, + error: "Each result must have participantId and placement", + syncReport: null, + }; + } + await upsertTournamentResult({ + tournamentId: tournament.id, + participantId: row.participantId, + placement: row.placement, + }); + } + + if (tournament.status !== "completed") { + await updateTournamentStatus(tournament.id, "completed"); + } + + const syncReport = await syncTournamentResults(tournament.id); + return { + success: true as const, + error: null, + syncReport, + }; + } catch (error) { + logger.error("batch-upsert-results failed:", error); + return { + success: false as const, + error: + error instanceof Error ? error.message : "Failed to save results", + syncReport: null, + }; + } + } + + if (intent === "retry-window-sync") { + try { + const syncReport = await syncTournamentResults(tournament.id); + return { success: true as const, error: null, syncReport }; + } catch (error) { + logger.error("retry-window-sync failed:", error); + return { + success: false as const, + error: + error instanceof Error ? error.message : "Failed to retry sync", + syncReport: null, + }; + } + } + + return { + success: false as const, + error: "Invalid intent", + syncReport: null, + }; +} + +export default function AdminTournamentDetail({ + loaderData, + actionData, +}: Route.ComponentProps) { + const { tournament, results, canonicalParticipants } = loaderData; + const retryFetcher = useFetcher(); + + // Prefer the latest action/retry response for the sync report + const liveReport: SyncReport | null = + (retryFetcher.data?.syncReport ?? actionData?.syncReport) ?? null; + + const participantById = new Map( + canonicalParticipants.map((p) => [p.id, p]) + ); + + return ( +
+
+
+ +
+

{tournament.name}

+ + {tournament.status} + +
+

+ {tournament.year} + {tournament.location ? ` — ${tournament.location}` : ""} + {tournament.surface ? ` — ${tournament.surface}` : ""} +

+
+ + {liveReport && ( + + + + + Synced to {liveReport.windowsSynced}{" "} + {liveReport.windowsSynced === 1 ? "window" : "windows"} + + + Canonical results were fanned out to every linked scoring + window. + + + {liveReport.failures.length > 0 && ( + +
+
+ + {liveReport.failures.length}{" "} + {liveReport.failures.length === 1 ? "window" : "windows"}{" "} + failed to sync +
+
+ {liveReport.failures.map((f) => ( +
+
+
+ event: {f.scoringEventId} +
+
+ season: {f.sportsSeasonId} +
+
{f.error}
+
+ + + + + +
+ ))} +
+
+
+ )} +
+ )} + + {actionData?.error && ( +
+ {actionData.error} +
+ )} + +
+ + + Current Results + + {results.length}{" "} + {results.length === 1 ? "result" : "results"} recorded + + + + {results.length === 0 ? ( +

+ No results recorded yet. Paste a ranked list to the right to + import. +

+ ) : ( + + + + Placement + Participant + + + + {results.map((r) => { + const p = participantById.get(r.participantId); + return ( + + + {r.placement ?? "—"} + + + {p?.name ?? ( + + {r.participantId} + + )} + + + ); + })} + +
+ )} +
+
+ + ({ + id: p.id, + name: p.name, + }))} + sportsSeasonId="" + existingResultParticipantIds={ + new Set(results.map((r) => r.participantId)) + } + intent="batch-upsert-results" + /> +
+
+
+ ); +} diff --git a/app/routes/admin.tournaments._index.tsx b/app/routes/admin.tournaments._index.tsx new file mode 100644 index 0000000..5753b05 --- /dev/null +++ b/app/routes/admin.tournaments._index.tsx @@ -0,0 +1,138 @@ +import { Link } from "react-router"; +import type { Route } from "./+types/admin.tournaments._index"; + +import { findAllTournamentsGroupedBySport } from "~/models/tournament"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { Trophy } from "lucide-react"; + +export function meta(): Route.MetaDescriptors { + return [{ title: "Tournaments - Brackt Admin" }]; +} + +export async function loader() { + const grouped = await findAllTournamentsGroupedBySport(); + return { grouped }; +} + +export default function AdminTournamentsIndex({ + loaderData, +}: Route.ComponentProps) { + const { grouped } = loaderData; + + const totalCount = grouped.reduce( + (acc, g) => acc + g.tournaments.length, + 0 + ); + + return ( +
+
+
+

Tournaments

+

+ Canonical tournaments across all sports. Enter results once here + and they fan out to every linked scoring window. +

+
+
+ + {grouped.length === 0 ? ( + + + +

+ No tournaments yet +

+

+ Canonical tournaments are created automatically by the backfill + or data-sync flows. +

+
+
+ ) : ( +
+

+ {totalCount} {totalCount === 1 ? "tournament" : "tournaments"}{" "} + across {grouped.length}{" "} + {grouped.length === 1 ? "sport" : "sports"} +

+ {grouped.map((group) => ( + + + {group.sport.name} + + {group.tournaments.length}{" "} + {group.tournaments.length === 1 + ? "tournament" + : "tournaments"} + + + + + + + Name + Year + Starts + Status + Actions + + + + {group.tournaments.map((t) => ( + + {t.name} + {t.year} + + {t.startsAt + ? new Date(t.startsAt).toLocaleDateString() + : "—"} + + + + {t.status} + + + + + + + ))} + +
+
+
+ ))} +
+ )} +
+ ); +} diff --git a/app/services/__tests__/sync-tournament-results.test.ts b/app/services/__tests__/sync-tournament-results.test.ts new file mode 100644 index 0000000..98eae90 --- /dev/null +++ b/app/services/__tests__/sync-tournament-results.test.ts @@ -0,0 +1,578 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("~/database/context", () => ({ + database: vi.fn(), +})); + +vi.mock("~/models/scoring-calculator", () => ({ + processQualifyingEvent: vi.fn(), +})); + +import { syncTournamentResults } from "../sync-tournament-results"; +import { database } from "~/database/context"; +import { processQualifyingEvent } from "~/models/scoring-calculator"; + +// --------------------------------------------------------------------------- +// Mock data types +// --------------------------------------------------------------------------- + +interface FakeTournamentResult { + tournamentId: string; + participantId: string; + placement: number | null; + rawScore: string | null; +} + +interface FakeScoringEvent { + id: string; + sportsSeasonId: string; + tournamentId: string | null; +} + +interface FakeSeasonParticipant { + id: string; + sportsSeasonId: string; + participantId: string | null; + name: string; +} + +interface FakeEventResult { + id: string; + scoringEventId: string; + seasonParticipantId: string; + placement: number | null; + qualifyingPointsAwarded: string | null; + rawScore: string | null; +} + +// --------------------------------------------------------------------------- +// Fake in-memory DB +// +// We build a single shared object with a chainable select/insert/update API +// that the service uses identically whether it's the top-level `db` or the +// `tx` inside a transaction. This lets us both observe the writes that would +// have happened and emulate idempotent upserts. +// +// Internal state holds the three tables the service queries: +// - tournament_results +// - scoring_events +// - season_participants +// - event_results +// --------------------------------------------------------------------------- + +interface FakeDbState { + tournamentResults: FakeTournamentResult[]; + scoringEvents: FakeScoringEvent[]; + seasonParticipants: FakeSeasonParticipant[]; + eventResults: FakeEventResult[]; +} + +/** + * A very thin fake drizzle-ish db. The service only uses: + * - db.select().from(table).where(cond) -- returns rows + * - db.insert(table).values(row[|rows]) -- insert + * - db.update(table).set(...).where(cond) -- update + * - db.transaction(fn) -- runs fn with the same db + * + * We identify the target table by reference equality to the schema objects. + * The service passes schema.tournamentResults etc., so we capture the real + * schema module and compare by ref. + */ +function makeFakeDb(state: FakeDbState) { + // We mirror the shape in a lazy way: each predicate function is stored on the + // call so we can match rows. In practice the service only filters by simple + // eq() / and() combinations — we don't try to parse drizzle's SQL tree. + // Instead, we stash the filters the service applies by table. + // + // The simpler and more faithful approach: every call records enough info for + // us to return the right subset. + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function rowsFor(table: any): any[] { + // Table objects are drizzle PgTable objects; we identify them by name. + // drizzle PgTables expose `[Symbol.for("drizzle:Name")]` but simpler: we + // just check a known property. Schema tables have `._.name`. + // Fallback: we compare string representation. + const name = tableName(table); + switch (name) { + case "tournament_results": + return state.tournamentResults; + case "scoring_events": + return state.scoringEvents; + case "season_participants": + return state.seasonParticipants; + case "event_results": + return state.eventResults; + default: + throw new Error(`Unknown table in fake db: ${name}`); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function tableName(table: any): string { + // drizzle-orm PgTable exposes the table name under a symbol. Try a few. + const sym = Object.getOwnPropertySymbols(table).find( + (s) => s.toString() === "Symbol(drizzle:Name)" + ); + if (sym) return table[sym]; + // Fallback for our shim: read `.tableName` property if set. + return table.tableName; + } + + const db = { + // ---------- select ---------- + select: vi.fn().mockImplementation(() => { + const chain = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _table: null as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + from(table: any) { + chain._table = table; + return chain; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + where(predicate: any) { + // The predicate is a drizzle SQL expression; we can't introspect it + // easily. The fake predicate stash sets a callback (see below). If + // the predicate is a function we apply it; otherwise we return all + // rows for the table (the caller will then filter). + const rows = rowsFor(chain._table); + if (typeof predicate === "function") { + return Promise.resolve(rows.filter(predicate)); + } + // Unknown predicate: the test harness attaches `__fakeFilter` to the + // predicate value we interposed. When we can't evaluate, just return + // all rows — the fake is used only by the service and tests that + // control what rows exist. + return Promise.resolve(rows); + }, + }; + return chain; + }), + + // ---------- insert ---------- + // eslint-disable-next-line @typescript-eslint/no-explicit-any + insert: vi.fn().mockImplementation((table: any) => { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + values(row: any) { + const rows = Array.isArray(row) ? row : [row]; + const existing = rowsFor(table); + for (const r of rows) { + // Assign a fake id for event_results (others aren't re-read by ID). + existing.push({ id: `er-${existing.length + 1}`, ...r }); + } + return { + returning: vi.fn().mockResolvedValue(rows), + }; + }, + }; + }), + + // ---------- update ---------- + // eslint-disable-next-line @typescript-eslint/no-explicit-any + update: vi.fn().mockImplementation((table: any) => { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + set(patch: any) { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + where(predicate: any) { + const rows = rowsFor(table); + for (const r of rows) { + if (typeof predicate === "function" ? predicate(r) : true) { + Object.assign(r, patch); + } + } + return Promise.resolve(); + }, + }; + }, + }; + }), + + // ---------- transaction ---------- + transaction: vi + .fn() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(async (fn: (tx: any) => any) => fn(db)), + }; + + return db; +} + +// --------------------------------------------------------------------------- +// The service builds drizzle SQL expressions via eq() / and() from +// drizzle-orm. We can't intercept those without re-mocking the whole module. +// Instead we mock drizzle-orm's eq/and to return predicate functions directly. +// --------------------------------------------------------------------------- + +vi.mock("drizzle-orm", async () => { + const actual = await vi.importActual("drizzle-orm"); + return { + ...actual, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + eq: (col: any, val: any) => { + // Each column object (PgColumn) has a `.name` property giving the DB + // column name. Return a predicate that reads that key off the row. + const key = colKey(col); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (row: any) => row[key] === val; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + and: (...preds: any[]) => (row: any) => preds.every((p) => p(row)), + }; +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function colKey(col: any): string { + // drizzle PgColumn: its .name is the snake_case DB name, but our fake state + // uses camelCase JS field names. Map common columns manually. + const n = col?.name ?? col?.config?.name ?? ""; + const map: Record = { + tournament_id: "tournamentId", + sports_season_id: "sportsSeasonId", + scoring_event_id: "scoringEventId", + season_participant_id: "seasonParticipantId", + participant_id: "participantId", + id: "id", + }; + return map[n] ?? n; +} + +// --------------------------------------------------------------------------- +// Helpers for building fixture data +// --------------------------------------------------------------------------- + +beforeEach(() => { + vi.clearAllMocks(); +}); + +function seedBasicState(overrides: Partial = {}): FakeDbState { + return { + tournamentResults: [], + scoringEvents: [], + seasonParticipants: [], + eventResults: [], + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Test 1: Single window, full roster match +// --------------------------------------------------------------------------- + +describe("syncTournamentResults", () => { + it("syncs a single window with two canonical results and fills 0-QP for unmatched", async () => { + const state = seedBasicState({ + tournamentResults: [ + { + tournamentId: "t-1", + participantId: "cp-A", + placement: 1, + rawScore: "280", + }, + { + tournamentId: "t-1", + participantId: "cp-B", + placement: 2, + rawScore: "282", + }, + ], + scoringEvents: [ + { id: "ev-1", sportsSeasonId: "ss-1", tournamentId: "t-1" }, + ], + seasonParticipants: [ + { + id: "sp-A", + sportsSeasonId: "ss-1", + participantId: "cp-A", + name: "A", + }, + { + id: "sp-B", + sportsSeasonId: "ss-1", + participantId: "cp-B", + name: "B", + }, + { + id: "sp-C", + sportsSeasonId: "ss-1", + participantId: "cp-C", + name: "C", + }, + ], + }); + + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + + const report = await syncTournamentResults("t-1"); + + expect(report.windowsSynced).toBe(1); + expect(report.windowsFailed).toBe(0); + expect(report.failures).toEqual([]); + + const eventRows = state.eventResults.filter( + (r) => r.scoringEventId === "ev-1" + ); + // sp-A: placement 1, sp-B: placement 2, sp-C: 0-QP filler + expect(eventRows).toHaveLength(3); + + const a = eventRows.find((r) => r.seasonParticipantId === "sp-A"); + expect(a?.placement).toBe(1); + expect(a?.rawScore).toBe("280"); + + const b = eventRows.find((r) => r.seasonParticipantId === "sp-B"); + expect(b?.placement).toBe(2); + expect(b?.rawScore).toBe("282"); + + const c = eventRows.find((r) => r.seasonParticipantId === "sp-C"); + expect(c?.placement).toBeNull(); + expect(c?.qualifyingPointsAwarded).toBe("0"); + + expect(processQualifyingEvent).toHaveBeenCalledTimes(1); + expect(processQualifyingEvent).toHaveBeenCalledWith("ev-1", db); + }); + + // ------------------------------------------------------------------------- + // Test 2: Two overlapping windows — same tournament, different sports_seasons + // ------------------------------------------------------------------------- + + it("fans out to multiple windows, each getting only its own participants", async () => { + const state = seedBasicState({ + tournamentResults: [ + { tournamentId: "t-1", participantId: "cp-X", placement: 1, rawScore: null }, + { tournamentId: "t-1", participantId: "cp-Y", placement: 2, rawScore: null }, + { tournamentId: "t-1", participantId: "cp-Z", placement: 3, rawScore: null }, + ], + scoringEvents: [ + { id: "ev-A", sportsSeasonId: "ss-A", tournamentId: "t-1" }, + { id: "ev-B", sportsSeasonId: "ss-B", tournamentId: "t-1" }, + ], + seasonParticipants: [ + // Window A has X, Y + { id: "sp-AX", sportsSeasonId: "ss-A", participantId: "cp-X", name: "X" }, + { id: "sp-AY", sportsSeasonId: "ss-A", participantId: "cp-Y", name: "Y" }, + // Window B has Y, Z + { id: "sp-BY", sportsSeasonId: "ss-B", participantId: "cp-Y", name: "Y" }, + { id: "sp-BZ", sportsSeasonId: "ss-B", participantId: "cp-Z", name: "Z" }, + ], + }); + + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + + const report = await syncTournamentResults("t-1"); + + expect(report.windowsSynced).toBe(2); + expect(report.windowsFailed).toBe(0); + + const aRows = state.eventResults.filter((r) => r.scoringEventId === "ev-A"); + const bRows = state.eventResults.filter((r) => r.scoringEventId === "ev-B"); + + // Window A: sp-AX (placement 1), sp-AY (placement 2). No 0-QP fillers + // since both roster members got results. + expect(aRows).toHaveLength(2); + expect(aRows.find((r) => r.seasonParticipantId === "sp-AX")?.placement).toBe(1); + expect(aRows.find((r) => r.seasonParticipantId === "sp-AY")?.placement).toBe(2); + + // Window B: sp-BY (placement 2), sp-BZ (placement 3). Again no fillers. + expect(bRows).toHaveLength(2); + expect(bRows.find((r) => r.seasonParticipantId === "sp-BY")?.placement).toBe(2); + expect(bRows.find((r) => r.seasonParticipantId === "sp-BZ")?.placement).toBe(3); + + expect(processQualifyingEvent).toHaveBeenCalledTimes(2); + expect(processQualifyingEvent).toHaveBeenNthCalledWith(1, "ev-A", db); + expect(processQualifyingEvent).toHaveBeenNthCalledWith(2, "ev-B", db); + }); + + // ------------------------------------------------------------------------- + // Test 3: Idempotent re-run + // ------------------------------------------------------------------------- + + it("is idempotent: a second run leaves the same end state", 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" }, + { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B" }, + ], + }); + + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + + await syncTournamentResults("t-1"); + const afterFirst = JSON.parse(JSON.stringify(state.eventResults)); + + await syncTournamentResults("t-1"); + const afterSecond = state.eventResults; + + // Same row count, same per-row placement / QP / participant / event. + expect(afterSecond).toHaveLength(afterFirst.length); + for (const first of afterFirst) { + const match = afterSecond.find( + (r) => + r.scoringEventId === first.scoringEventId && + r.seasonParticipantId === first.seasonParticipantId + ); + expect(match).toBeDefined(); + expect(match?.placement).toBe(first.placement); + expect(match?.qualifyingPointsAwarded).toBe(first.qualifyingPointsAwarded); + expect(match?.rawScore).toBe(first.rawScore); + } + }); + + // ------------------------------------------------------------------------- + // Test 4: Correction propagation + // ------------------------------------------------------------------------- + + it("propagates canonical placement corrections to all linked windows", 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); + + await syncTournamentResults("t-1"); + + // Sanity — both windows have sp-* at placement 1. + expect( + state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.placement + ).toBe(1); + expect( + state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.placement + ).toBe(1); + + // Correct the canonical placement from 1 to 5. + state.tournamentResults[0].placement = 5; + state.tournamentResults[0].rawScore = "99"; + + await syncTournamentResults("t-1"); + + expect( + state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.placement + ).toBe(5); + expect( + state.eventResults.find((r) => r.seasonParticipantId === "sp-AA")?.rawScore + ).toBe("99"); + expect( + state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.placement + ).toBe(5); + expect( + state.eventResults.find((r) => r.seasonParticipantId === "sp-BA")?.rawScore + ).toBe("99"); + }); + + // ------------------------------------------------------------------------- + // Test 5: Partial failure isolation + // ------------------------------------------------------------------------- + + it("isolates failures: one window's error does not stop the others", 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); + + // First window throws in processQualifyingEvent; second succeeds. + vi.mocked(processQualifyingEvent) + .mockImplementationOnce(async () => { + throw new Error("QP calc failed"); + }) + .mockImplementationOnce(async () => undefined); + + const report = await syncTournamentResults("t-1"); + + expect(report.windowsSynced).toBe(1); + expect(report.windowsFailed).toBe(1); + expect(report.failures).toHaveLength(1); + expect(report.failures[0].scoringEventId).toBe("ev-A"); + expect(report.failures[0].sportsSeasonId).toBe("ss-A"); + expect(report.failures[0].error).toContain("QP calc failed"); + + // Second window (ev-B) still has its row committed in our fake (we don't + // simulate rollback, but the service called through to the writes path, + // which is what we care about — the try/catch did not propagate). + expect( + state.eventResults.find( + (r) => r.scoringEventId === "ev-B" && r.seasonParticipantId === "sp-BA" + ) + ).toBeDefined(); + + expect(processQualifyingEvent).toHaveBeenCalledTimes(2); + }); + + // ------------------------------------------------------------------------- + // Test 6: Participant in roster but not in canonical results → 0-QP filler + // ------------------------------------------------------------------------- + + it("writes 0-QP filler rows for roster participants absent from canonical results", 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" }, + { id: "sp-B", sportsSeasonId: "ss-1", participantId: "cp-B", name: "B" }, + { id: "sp-C", sportsSeasonId: "ss-1", participantId: "cp-C", name: "C" }, + ], + }); + + const db = makeFakeDb(state); + vi.mocked(database).mockReturnValue(db as never); + vi.mocked(processQualifyingEvent).mockResolvedValue(undefined); + + await syncTournamentResults("t-1"); + + const rows = state.eventResults; + expect(rows).toHaveLength(3); + + const a = rows.find((r) => r.seasonParticipantId === "sp-A"); + expect(a?.placement).toBe(1); + expect(a?.qualifyingPointsAwarded).toBeUndefined(); // not set by this service + + const b = rows.find((r) => r.seasonParticipantId === "sp-B"); + expect(b?.placement).toBeNull(); + expect(b?.qualifyingPointsAwarded).toBe("0"); + + const c = rows.find((r) => r.seasonParticipantId === "sp-C"); + expect(c?.placement).toBeNull(); + expect(c?.qualifyingPointsAwarded).toBe("0"); + }); +}); diff --git a/app/services/sync-tournament-results.ts b/app/services/sync-tournament-results.ts new file mode 100644 index 0000000..786f913 --- /dev/null +++ b/app/services/sync-tournament-results.ts @@ -0,0 +1,138 @@ +import { eq, and } from "drizzle-orm"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { processQualifyingEvent } from "~/models/scoring-calculator"; + +export interface SyncReport { + tournamentId: string; + windowsSynced: number; + windowsFailed: number; + failures: Array<{ + scoringEventId: string; + sportsSeasonId: string; + error: string; + }>; +} + +/** + * Fan out canonical tournament_results to every scoring_event window that + * points at this tournament. For each linked window: + * 1. Upsert event_results rows mirroring canonical placement / rawScore for + * every roster participant that appears in the canonical results. + * 2. Write 0-QP filler rows for roster participants who have no result. + * 3. Delegate to processQualifyingEvent to compute QP. + * + * Each window runs in its own transaction so partial failures isolate. + * Errors in one window never interrupt sync of the remaining windows. + * + * This service NEVER writes qualifying_points_awarded directly (except the + * "0" filler for participants with no placement) — QP calculation for real + * results is owned by processQualifyingEvent. + */ +export async function syncTournamentResults( + tournamentId: string +): Promise { + const db = database(); + + const report: SyncReport = { + tournamentId, + windowsSynced: 0, + windowsFailed: 0, + failures: [], + }; + + // 1. Load canonical results for this tournament. + const canonicalResults = await db + .select() + .from(schema.tournamentResults) + .where(eq(schema.tournamentResults.tournamentId, tournamentId)); + + // 2. Load every scoring_event that points at this tournament. + const linkedEvents = await db + .select() + .from(schema.scoringEvents) + .where(eq(schema.scoringEvents.tournamentId, tournamentId)); + + // 3. Fan out — each event gets its own transaction. + for (const ev of linkedEvents) { + try { + await db.transaction(async (tx: typeof db) => { + // 3a. Window roster = season_participants for this event's sports_season. + const rosters = await tx + .select() + .from(schema.seasonParticipants) + .where(eq(schema.seasonParticipants.sportsSeasonId, ev.sportsSeasonId)); + + // 3b. For each canonical result whose participant is on the roster, + // upsert the matching event_results row. Only placement / rawScore + // are copied; qualifying_points_awarded is left for + // processQualifyingEvent to recompute. + for (const cr of canonicalResults) { + const sp = rosters.find((r) => r.participantId === cr.participantId); + if (!sp) continue; + + const [existing] = await tx + .select() + .from(schema.eventResults) + .where( + and( + eq(schema.eventResults.scoringEventId, ev.id), + eq(schema.eventResults.seasonParticipantId, sp.id) + ) + ); + + if (existing) { + await tx + .update(schema.eventResults) + .set({ + placement: cr.placement, + rawScore: cr.rawScore, + updatedAt: new Date(), + }) + .where(eq(schema.eventResults.id, existing.id)); + } else { + await tx.insert(schema.eventResults).values({ + scoringEventId: ev.id, + seasonParticipantId: sp.id, + placement: cr.placement, + rawScore: cr.rawScore, + }); + } + } + + // 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. + const afterRows = await tx + .select() + .from(schema.eventResults) + .where(eq(schema.eventResults.scoringEventId, ev.id)); + const covered = new Set(afterRows.map((r) => r.seasonParticipantId)); + 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", + }); + } + + // 3d. Delegate to scoring engine inside the same transaction. + await processQualifyingEvent(ev.id, tx); + }); + report.windowsSynced += 1; + } catch (e: unknown) { + report.windowsFailed += 1; + const msg = + e instanceof Error ? e.message : typeof e === "string" ? e : String(e); + report.failures.push({ + scoringEventId: ev.id, + sportsSeasonId: ev.sportsSeasonId, + error: msg, + }); + } + } + + return report; +} diff --git a/database/schema.ts b/database/schema.ts index f763fca..37f009b 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -1,4 +1,4 @@ -import { integer, pgTable, varchar, uuid, timestamp, pgEnum, boolean, text, date, decimal, uniqueIndex, index, jsonb } from "drizzle-orm/pg-core"; +import { check, integer, pgTable, varchar, uuid, timestamp, pgEnum, boolean, text, date, decimal, uniqueIndex, index, jsonb } from "drizzle-orm/pg-core"; import { relations, sql } from "drizzle-orm"; export const users = pgTable("users", { @@ -554,7 +554,12 @@ export const scoringEvents = pgTable("scoring_events", { completedAt: timestamp("completed_at"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), -}); +}, (t) => ({ + qualifyingEventsRequireTournament: check( + "scoring_events_qualifying_require_tournament", + sql`NOT ${t.isQualifyingEvent} OR ${t.tournamentId} IS NOT NULL`, + ), +})); // Results for participants in specific events export const eventResults = pgTable("event_results", { @@ -575,7 +580,12 @@ export const eventResults = pgTable("event_results", { rawScore: decimal("raw_score", { precision: 10, scale: 2 }), // strokes, points, time, etc. createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), -}); +}, (t) => ({ + uniqueEventParticipant: uniqueIndex("event_results_event_participant_unique").on( + t.scoringEventId, + t.seasonParticipantId, + ), +})); // Playoff bracket matches (for display and tracking) export const playoffMatches = pgTable("playoff_matches", { diff --git a/drizzle/0089_lonely_tigra.sql b/drizzle/0089_lonely_tigra.sql new file mode 100644 index 0000000..b7d6775 --- /dev/null +++ b/drizzle/0089_lonely_tigra.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX IF NOT EXISTS "event_results_event_participant_unique" ON "event_results" USING btree ("scoring_event_id","season_participant_id");--> statement-breakpoint +ALTER TABLE "scoring_events" ADD CONSTRAINT "scoring_events_qualifying_require_tournament" CHECK (NOT "scoring_events"."is_qualifying_event" OR "scoring_events"."tournament_id" IS NOT NULL); \ No newline at end of file diff --git a/drizzle/meta/0089_snapshot.json b/drizzle/meta/0089_snapshot.json new file mode 100644 index 0000000..ed83b36 --- /dev/null +++ b/drizzle/meta/0089_snapshot.json @@ -0,0 +1,5715 @@ +{ + "id": "cf875e21-71e0-40bc-aa85-a1901861b8b0", + "prevId": "feb9e343-a91c-4c94-a77c-6a1d876e42b5", + "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 + }, + "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 + }, + "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 + }, + "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.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 + }, + "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": { + "participant_golf_skills_unique": { + "name": "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": { + "participant_golf_skills_participant_id_season_participants_id_fk": { + "name": "participant_golf_skills_participant_id_season_participants_id_fk", + "tableFrom": "participant_golf_skills", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_golf_skills_sports_season_id_sports_seasons_id_fk": { + "name": "participant_golf_skills_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_golf_skills", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_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": {}, + "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 + }, + "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 + }, + "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 + }, + "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_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_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_surface_elos": { + "name": "season_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 + }, + "sports_season_id": { + "name": "sports_season_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_unique": { + "name": "participant_surface_elos_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_surface_elos_participant_id_season_participants_id_fk": { + "name": "season_participant_surface_elos_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_surface_elos", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_surface_elos_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_surface_elos_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_surface_elos", + "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 + }, + "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_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_sports_season_id_sports_seasons_id_fk": { + "name": "season_template_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_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 + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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.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 + }, + "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.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(255)", + "primaryKey": false, + "notNull": false + }, + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sports_slug_unique": { + "name": "sports_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "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 + }, + "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'" + }, + "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'" + }, + "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" + } + }, + "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 + }, + "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": {}, + "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": {}, + "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 + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "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": {}, + "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" + ] + }, + "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.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", + "snooker_bracket", + "tennis_qualifying_points", + "mlb_bracket", + "wnba_bracket", + "world_cup", + "darts_bracket", + "cs2_major_qualifying_points", + "ncaa_football_bracket", + "llws_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 9587123..7da2156 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -624,6 +624,13 @@ "when": 1777666830394, "tag": "0088_cheerful_norrin_radd", "breakpoints": true + }, + { + "idx": 89, + "version": "7", + "when": 1777675084016, + "tag": "0089_lonely_tigra", + "breakpoints": true } ] } \ No newline at end of file diff --git a/scripts/backfill/match-tournament.ts b/scripts/backfill/match-tournament.ts index e7c7c92..d869e6f 100644 --- a/scripts/backfill/match-tournament.ts +++ b/scripts/backfill/match-tournament.ts @@ -1,57 +1,11 @@ /** - * Pure function(s) for extracting canonical tournament identity - * (name + year) from a `scoring_events` row. - * - * Used by the Phase 2 backfill to group per-window events into - * canonical `tournaments` rows. + * Re-exports the canonical tournament identity extractor. The implementation + * lives in `app/lib/tournament-identity.ts` so it can be shared by the + * Phase 2 backfill and by the admin event-creation flow. */ -export interface ScoringEventInput { - name: string; - eventDate: string | null; - eventType: string; -} - -export interface TournamentIdentity { - /** Tournament name with any trailing 4-digit year stripped. */ - name: string; - year: number; -} - -const TRAILING_YEAR_RE = / (\d{4})$/; - -/** - * Extracts the canonical `(name, year)` identity for a tournament - * from a scoring event. - * - * Resolution order for year: - * 1. A trailing 4-digit year on the event name (e.g. "Wimbledon 2026"). - * The year is stripped from the returned name. - * 2. The first 4 characters of `eventDate` (format `YYYY-MM-DD`). - * - * Throws if neither source supplies a year. - */ -export function extractTournamentIdentity( - ev: ScoringEventInput, -): TournamentIdentity { - const trimmedName = ev.name.trim(); - const match = trimmedName.match(TRAILING_YEAR_RE); - - if (match) { - const yearFromName = Number(match[1]); - const nameWithoutYear = trimmedName.slice(0, match.index).trim(); - return { name: nameWithoutYear, year: yearFromName }; - } - - if (ev.eventDate) { - const yearStr = ev.eventDate.slice(0, 4); - const yearFromDate = Number(yearStr); - if (Number.isFinite(yearFromDate) && yearStr.length === 4) { - return { name: trimmedName, year: yearFromDate }; - } - } - - throw new Error( - `cannot determine year for scoring event "${ev.name}" (eventDate=${ev.eventDate ?? "null"})`, - ); -} +export { + extractTournamentIdentity, + type TournamentIdentity, + type ScoringEventIdentityInput as ScoringEventInput, +} from "~/lib/tournament-identity";