diff --git a/app/hooks/useDraftSocketEvents.ts b/app/hooks/useDraftSocketEvents.ts index da6aab2..6a88f46 100644 --- a/app/hooks/useDraftSocketEvents.ts +++ b/app/hooks/useDraftSocketEvents.ts @@ -35,6 +35,7 @@ interface UseDraftSocketEventsParams { setConnectedTeams: (fn: (prev: Set) => Set) => void; setQueue: (fn: (prev: QueueItem[]) => QueueItem[]) => void; setTeamTimers: (fn: (prev: Record) => Record) => void; + setPickTimerExpiresAt: (state: { teamId: string; expiresAt: number } | null) => void; setIsOvernightPause: (value: boolean) => void; setOvernightResumesAt: (value: Date | null) => void; setWatchedParticipantIds: (value: Set) => void; @@ -65,6 +66,7 @@ export function useDraftSocketEvents({ setConnectedTeams, setQueue, setTeamTimers, + setPickTimerExpiresAt, setIsOvernightPause, setOvernightResumesAt, setWatchedParticipantIds, @@ -108,23 +110,27 @@ export function useDraftSocketEvents({ } }; - const handleTimerUpdate = (data: { + const handleTimerPickStarted = (data: { + seasonId: string; teamId: string; + pickNumber: number; + expiresAt: number; timeRemaining: number; - currentPickNumber: number | null; - overnightPauseActive?: boolean; + }) => { + setTeamTimers((prev) => ({ ...prev, [data.teamId]: data.timeRemaining })); + setPickTimerExpiresAt({ teamId: data.teamId, expiresAt: data.expiresAt }); + setIsOvernightPause(false); + setOvernightResumesAt(null); + }; + + const handleTimerOvernightPaused = (data: { + seasonId: string; + teamId: string; resumesAtUTC?: number; }) => { - setTeamTimers((prev) => { - if (prev[data.teamId] === data.timeRemaining) return prev; - return { ...prev, [data.teamId]: data.timeRemaining }; - }); - if (data.currentPickNumber !== null) { - setCurrentPick(data.currentPickNumber); - } - const pauseActive = data.overnightPauseActive ?? false; - setIsOvernightPause(pauseActive); - setOvernightResumesAt(pauseActive && data.resumesAtUTC ? new Date(data.resumesAtUTC) : null); + setPickTimerExpiresAt(null); + setIsOvernightPause(true); + setOvernightResumesAt(data.resumesAtUTC ? new Date(data.resumesAtUTC) : null); }; const handleDraftPaused = () => setIsPaused(true); @@ -210,7 +216,7 @@ export function useDraftSocketEvents({ currentPickNumber: number; isPaused: boolean; status: string; - timers?: Array<{ teamId: string; timeRemaining: number }>; + timers?: Array<{ teamId: string; timeRemaining: number; expiresAt?: number }>; queue?: QueueItem[]; watchlistParticipantIds?: string[]; }) => { @@ -225,6 +231,11 @@ export function useDraftSocketEvents({ data.timers?.forEach((t) => { updated[t.teamId] = t.timeRemaining; }); return updated; }); + // Restore client-side countdown for the active team on reconnect. + const activeTimer = data.timers.find((t) => t.expiresAt !== undefined); + if (activeTimer?.expiresAt) { + setPickTimerExpiresAt({ teamId: activeTimer.teamId, expiresAt: activeTimer.expiresAt }); + } } if (data.queue) { const q = data.queue; @@ -258,7 +269,8 @@ export function useDraftSocketEvents({ }; on("pick-made", handlePickMade as (data: unknown) => void); - on("timer-update", handleTimerUpdate as (data: unknown) => void); + on("timer-pick-started", handleTimerPickStarted as (data: unknown) => void); + on("timer-overnight-paused", handleTimerOvernightPaused as (data: unknown) => void); on("draft-paused", handleDraftPaused as (data: unknown) => void); on("draft-resumed", handleDraftResumed as (data: unknown) => void); on("draft-completed", handleDraftCompleted as (data: unknown) => void); @@ -278,7 +290,8 @@ export function useDraftSocketEvents({ return () => { off("pick-made", handlePickMade as (data: unknown) => void); - off("timer-update", handleTimerUpdate as (data: unknown) => void); + off("timer-pick-started", handleTimerPickStarted as (data: unknown) => void); + off("timer-overnight-paused", handleTimerOvernightPaused as (data: unknown) => void); off("draft-paused", handleDraftPaused as (data: unknown) => void); off("draft-resumed", handleDraftResumed as (data: unknown) => void); off("draft-completed", handleDraftCompleted as (data: unknown) => void); diff --git a/app/routes/api/__tests__/draft.force-manual-pick.test.ts b/app/routes/api/__tests__/draft.force-manual-pick.test.ts index 3306277..09b7b3c 100644 --- a/app/routes/api/__tests__/draft.force-manual-pick.test.ts +++ b/app/routes/api/__tests__/draft.force-manual-pick.test.ts @@ -35,6 +35,9 @@ vi.mock("~/models/user", () => ({ vi.mock("~/models/audit-log", () => ({ logCommissionerAction: vi.fn().mockResolvedValue(undefined), })); +vi.mock("~/server/timer", () => ({ + rescheduleTimer: vi.fn().mockResolvedValue(undefined), +})); // ── Fixtures ───────────────────────────────────────────────────────────────── @@ -367,27 +370,18 @@ describe("draft.force-manual-pick action", () => { describe("chess_clock mode", () => { it("adds the increment to the picking team's bank", async () => { // Timer has 75s (from beforeEach mock); 75 + 30 = 105s after increment - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 105 }]); - await action({ request: defaultPickRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 105, picksExpiresAt: null, picksStartedAt: null }) ); }); it("writes the incremented timer to the DB", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 105 }]); - await action({ request: defaultPickRequest(), params: {}, context: ctx }); expect(mockDb.set).toHaveBeenCalledWith( - expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) + expect.objectContaining({ timeRemaining: expect.any(Number), picksExpiresAt: null, updatedAt: expect.any(Date) }) ); }); }); @@ -401,41 +395,30 @@ describe("draft.force-manual-pick action", () => { }); it("resets the picking team's timer to draftIncrementTime", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 30 }]); - await action({ request: defaultPickRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); }); it("writes the timer reset to the DB", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 30 }]); - await action({ request: defaultPickRequest(), params: {}, context: ctx }); expect(mockDb.set).toHaveBeenCalledWith( - expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) + expect.objectContaining({ timeRemaining: expect.any(Number), picksExpiresAt: null, updatedAt: expect.any(Date) }) ); }); }); - // ── REGRESSION: Next team's timer must never be touched ────────────────── + // ── REGRESSION: rescheduleTimer should be called exactly once per pick ────── - it("REGRESSION: does not emit a timer-update for the next team", async () => { + it("REGRESSION: calls rescheduleTimer exactly once after a pick", async () => { + const { rescheduleTimer } = await import("~/server/timer"); await action({ request: defaultPickRequest(), params: {}, context: ctx }); - const nextTeamTimerEmits = mockSocketIO.emit.mock.calls.filter( - ([event, payload]: [string, any]) => - event === "timer-update" && payload?.teamId === NEXT_TEAM_ID - ); - expect(nextTeamTimerEmits).toHaveLength(0); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledTimes(1); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); }); }); diff --git a/app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts b/app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts index 4b8c869..2173f66 100644 --- a/app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts +++ b/app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts @@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import type { RouterContextProvider } from "react-router"; import { action } from "~/routes/api/draft.force-manual-pick"; +import { rescheduleTimer } from "~/server/timer"; const ctx = {} as unknown as RouterContextProvider; @@ -41,6 +42,9 @@ vi.mock("~/models/user", () => ({ vi.mock("~/models/audit-log", () => ({ logCommissionerAction: vi.fn().mockResolvedValue(undefined), })); +vi.mock("~/server/timer", () => ({ + rescheduleTimer: vi.fn().mockResolvedValue(undefined), +})); // ── Fixtures ────────────────────────────────────────────────────────────────── @@ -192,46 +196,35 @@ describe("draft.force-manual-pick action — timer mode behavior", () => { }); describe("commissioner force pick", () => { - it("emits timer-update with bank + increment", async () => { - // Pre-pick bank: 75s. Expected after increment: 75 + 30 = 105s. - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - + it("stores bank + increment in DB timer and calls rescheduleTimer", async () => { + // Pre-pick bank: 75s (no picksExpiresAt). 75 + 30 = 105. await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 105, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); it("writes the timer update to the DB", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - await action({ request: makeRequest(), params: {}, context: ctx }); // Two DB updates: timer row + season.currentPickNumber expect(mockDb.set).toHaveBeenCalledWith( - expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) + expect.objectContaining({ timeRemaining: expect.any(Number), picksExpiresAt: null, updatedAt: expect.any(Date) }) ); }); it("accumulates a larger bank when more time was remaining", async () => { - mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 100 }); // 100 + 30 = 130 - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 130 }]); + mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 100 }); await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 130 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 130, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); }); @@ -247,28 +240,20 @@ describe("draft.force-manual-pick action — timer mode behavior", () => { mockDb.query.commissioners.findFirst.mockResolvedValue(null); }); - it("emits timer-update with bank + increment", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - - await action({ request: makeRequest(), params: {}, context: ctx }); - - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 }) - ); - }); - - it("writes the timer update to the DB", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - + it("stores bank + increment in DB timer and calls rescheduleTimer", async () => { await action({ request: makeRequest(), params: {}, context: ctx }); expect(mockDb.set).toHaveBeenCalledWith( - expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) + expect.objectContaining({ timeRemaining: 105, picksExpiresAt: null, picksStartedAt: null }) + ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); + }); + + it("writes the timer update to the DB", async () => { + await action({ request: makeRequest(), params: {}, context: ctx }); + + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: expect.any(Number), picksExpiresAt: null, updatedAt: expect.any(Date) }) ); }); }); @@ -287,30 +272,23 @@ describe("draft.force-manual-pick action — timer mode behavior", () => { it("resets bank to exactly draftIncrementTime", async () => { // Pre-pick bank doesn't matter — standard always resets mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 5 }); - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); it("does not accumulate time even when bank was large", async () => { // Team had 25s left; standard mode resets to increment, never adds to prior balance mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 25 }); - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); }); }); @@ -327,32 +305,25 @@ describe("draft.force-manual-pick action — timer mode behavior", () => { }); it("resets bank to exactly draftIncrementTime", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); - await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); it("uses custom draftIncrementTime when configured", async () => { mockDb.query.seasons.findFirst.mockResolvedValue( makeSeason({ draftTimerMode: "standard", draftInitialTime: 60, draftIncrementTime: 60 }) ); - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 60 }]); await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 60 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 60, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); }); }); diff --git a/app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts b/app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts index 16a112f..2715ad6 100644 --- a/app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts +++ b/app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts @@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import type { RouterContextProvider } from "react-router"; import { action } from "~/routes/api/draft.make-pick"; +import { rescheduleTimer } from "~/server/timer"; const ctx = {} as unknown as RouterContextProvider; @@ -39,6 +40,9 @@ vi.mock("~/models/draft-utils", () => ({ vi.mock("~/models/user", () => ({ isUserAdmin: vi.fn(), })); +vi.mock("~/server/timer", () => ({ + rescheduleTimer: vi.fn().mockResolvedValue(undefined), +})); // ── Fixtures ────────────────────────────────────────────────────────────────── @@ -188,44 +192,33 @@ describe("draft.make-pick action — timer mode behavior", () => { describe("owner pick", () => { // Auth default is OWNER_ID (team owner), no extra setup needed. - it("emits timer-update with bank + increment", async () => { - // DB returns 75 + 30 = 105 after the atomic add - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - + it("stores bank + increment in DB timer and calls rescheduleTimer", async () => { + // Pre-pick bank: 75s (no picksExpiresAt, falls back to timeRemaining). 75 + 30 = 105. await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 105, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); it("accumulates a larger bank when more time was remaining", async () => { - mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 100 }); // 100 + 30 = 130 - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 130 }]); - - await action({ request: makeRequest(), params: {}, context: ctx }); - - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 130 }) - ); - }); - - it("writes the timer update to the DB", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); + mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 100 }); await action({ request: makeRequest(), params: {}, context: ctx }); expect(mockDb.set).toHaveBeenCalledWith( - expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) + expect.objectContaining({ timeRemaining: 130, picksExpiresAt: null, picksStartedAt: null }) + ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); + }); + + it("writes the timer update to the DB", async () => { + await action({ request: makeRequest(), params: {}, context: ctx }); + + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: expect.any(Number), picksExpiresAt: null, updatedAt: expect.any(Date) }) ); }); }); @@ -246,28 +239,20 @@ describe("draft.make-pick action — timer mode behavior", () => { ]); }); - it("emits timer-update with bank + increment", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - - await action({ request: makeRequest(), params: {}, context: ctx }); - - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 }) - ); - }); - - it("writes the timer update to the DB", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - + it("stores bank + increment in DB timer and calls rescheduleTimer", async () => { await action({ request: makeRequest(), params: {}, context: ctx }); expect(mockDb.set).toHaveBeenCalledWith( - expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) + expect.objectContaining({ timeRemaining: 105, picksExpiresAt: null, picksStartedAt: null }) + ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); + }); + + it("writes the timer update to the DB", async () => { + await action({ request: makeRequest(), params: {}, context: ctx }); + + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: expect.any(Number), picksExpiresAt: null, updatedAt: expect.any(Date) }) ); }); }); @@ -290,28 +275,20 @@ describe("draft.make-pick action — timer mode behavior", () => { ]); }); - it("emits timer-update with bank + increment", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - - await action({ request: makeRequest(), params: {}, context: ctx }); - - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 105 }) - ); - }); - - it("writes the timer update to the DB", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 105 }]); - + it("stores bank + increment in DB timer and calls rescheduleTimer", async () => { await action({ request: makeRequest(), params: {}, context: ctx }); expect(mockDb.set).toHaveBeenCalledWith( - expect.objectContaining({ timeRemaining: expect.anything(), updatedAt: expect.any(Date) }) + expect.objectContaining({ timeRemaining: 105, picksExpiresAt: null, picksStartedAt: null }) + ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); + }); + + it("writes the timer update to the DB", async () => { + await action({ request: makeRequest(), params: {}, context: ctx }); + + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: expect.any(Number), picksExpiresAt: null, updatedAt: expect.any(Date) }) ); }); }); @@ -327,30 +304,22 @@ describe("draft.make-pick action — timer mode behavior", () => { }); it("owner pick — resets bank to exactly draftIncrementTime", async () => { - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); - await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); it("owner pick — fast picker does NOT accumulate time (bank never exceeds increment)", async () => { // Team had 25s left (picked quickly); standard mode ignores prior balance mockDb.query.draftTimers.findFirst.mockResolvedValue({ id: "timer-1", timeRemaining: 25 }); - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); }); @@ -363,16 +332,12 @@ describe("draft.make-pick action — timer mode behavior", () => { mockDraftSlots[1], ]); - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); - await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); it("admin pick — resets bank to exactly draftIncrementTime", async () => { @@ -385,32 +350,25 @@ describe("draft.make-pick action — timer mode behavior", () => { mockDraftSlots[1], ]); - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 30 }]); - await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 30 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 30, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); it("uses custom draftIncrementTime when configured", async () => { mockDb.query.seasons.findFirst.mockResolvedValue( makeSeason({ draftTimerMode: "standard", draftInitialTime: 90, draftIncrementTime: 90 }) ); - mockDb.returning - .mockResolvedValueOnce([mockDraftPick]) - .mockResolvedValueOnce([{ id: "timer-1", teamId: TEAM_ID, timeRemaining: 90 }]); await action({ request: makeRequest(), params: {}, context: ctx }); - expect(mockSocketIO.emit).toHaveBeenCalledWith( - "timer-update", - expect.objectContaining({ teamId: TEAM_ID, timeRemaining: 90 }) + expect(mockDb.set).toHaveBeenCalledWith( + expect.objectContaining({ timeRemaining: 90, picksExpiresAt: null, picksStartedAt: null }) ); + expect(vi.mocked(rescheduleTimer)).toHaveBeenCalledWith(SEASON_ID); }); }); }); diff --git a/app/routes/api/draft.force-manual-pick.ts b/app/routes/api/draft.force-manual-pick.ts index 5752f51..33c96b9 100644 --- a/app/routes/api/draft.force-manual-pick.ts +++ b/app/routes/api/draft.force-manual-pick.ts @@ -1,7 +1,7 @@ import { auth } from "~/lib/auth.server"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq, and, sql } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import { isUserAdmin } from "~/models/user"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick"; @@ -13,6 +13,7 @@ import { enqueuePickNotification } from "~/services/discord"; import { notifyPickMadeOnDiscord } from "~/services/draft-discord.server"; import { logCommissionerAction } from "~/models/audit-log"; import { getSocketIO, scheduleDraftRoomClosure } from "../../../server/socket"; +import { rescheduleTimer } from "../../../server/timer"; import { logger } from "~/lib/logger"; import { runBracktHarvilleForFantasySeason } from "~/services/brackt.server"; import type { ActionFunctionArgs } from "react-router"; @@ -173,41 +174,29 @@ export async function action(args: ActionFunctionArgs) { const isDraftComplete = nextPickNumber > totalPicks; // Standard mode: reset to the per-pick time (so the next turn starts fresh). - // Chess clock: add the increment to the bank (same as any other pick type). + // Chess clock: add the increment to the actual remaining time (from picksExpiresAt). + // Also clear picksExpiresAt so the timer system knows this team's turn is over. const incrementTime = season.draftIncrementTime || 30; - const timerSql = season.draftTimerMode === "standard" - ? sql`${incrementTime}` - : sql`${schema.draftTimers.timeRemaining} + ${incrementTime}`; + const timerSnapshot = await db.query.draftTimers.findFirst({ + where: and(eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId)), + }); + const pickMadeAt = Date.now(); + const timeRemainingAtPick = timerSnapshot?.picksExpiresAt + ? Math.max(0, Math.floor((timerSnapshot.picksExpiresAt.getTime() - pickMadeAt) / 1000)) + : (timerSnapshot?.timeRemaining ?? 0); + const newTimeRemaining = + season.draftTimerMode === "standard" ? incrementTime : timeRemainingAtPick + incrementTime; const [updatedTimer] = await db .update(schema.draftTimers) - .set({ timeRemaining: timerSql, updatedAt: new Date() }) - .where( - and( - eq(schema.draftTimers.seasonId, seasonId), - eq(schema.draftTimers.teamId, teamId) - ) - ) + .set({ timeRemaining: newTimeRemaining, picksExpiresAt: null, picksStartedAt: null, updatedAt: new Date() }) + .where(and(eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, teamId))) .returning(); - const newTimeRemaining = updatedTimer?.timeRemaining ?? incrementTime; if (!updatedTimer) { await db.insert(schema.draftTimers).values({ seasonId, teamId, timeRemaining: newTimeRemaining }); } - // Emit timer update to all clients - try { - getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { - seasonId, - teamId, - timeRemaining: newTimeRemaining, - currentPickNumber: nextPickNumber, - }); - } catch (error) { - logger.error("Socket.IO timer-update error:", error); - } - - // Next team's timer is unchanged — their bank carries forward as-is. - // The timer server loop will start counting down from their existing balance. + // rescheduleTimer (called below) will emit timer-pick-started for the next team. // Update season's current pick number await db @@ -322,6 +311,13 @@ export async function action(args: ActionFunctionArgs) { } } + // Reschedule the timer for the next team on the clock (runs after the full autodraft chain). + try { + await rescheduleTimer(seasonId); + } catch (err) { + logger.error("[ForceManualPick] rescheduleTimer failed:", err); + } + return Response.json({ success: true, pick: draftPick, diff --git a/app/routes/api/draft.make-pick.ts b/app/routes/api/draft.make-pick.ts index 032a43e..935bfa2 100644 --- a/app/routes/api/draft.make-pick.ts +++ b/app/routes/api/draft.make-pick.ts @@ -2,7 +2,7 @@ import { auth } from "~/lib/auth.server"; import { getTeamForPick } from "~/lib/draft-order"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq, and, sql } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import { isUserAdmin } from "~/models/user"; import { calculateDraftEligibility } from "~/lib/draft-eligibility"; import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick"; @@ -10,6 +10,7 @@ import { getParticipantsForSeasonWithSports } from "~/models/season-participant" import { getSeasonSportsSimple } from "~/models/season-sport"; import { calculatePickInfo, checkAndTriggerNextAutodraft, pruneIneligibleQueueItems } from "~/models/draft-utils"; import { getSocketIO, scheduleDraftRoomClosure } from "../../../server/socket"; +import { rescheduleTimer } from "../../../server/timer"; import { logger } from "~/lib/logger"; import { runBracktHarvilleForFantasySeason } from "~/services/brackt.server"; import { sendOnTheClockEmail } from "~/services/draft-email.server"; @@ -141,13 +142,18 @@ export async function action(args: ActionFunctionArgs) { return Response.json({ error: reason }, { status: 400 }); } - // Snapshot the team's time bank before the pick (used for audit / pick history) + // Snapshot the team's time bank before the pick (used for audit / pick history). + // Use picksExpiresAt to compute actual remaining time rather than the stale timeRemaining. const timerSnapshot = await db.query.draftTimers.findFirst({ where: and( eq(schema.draftTimers.seasonId, seasonId), eq(schema.draftTimers.teamId, currentDraftSlot.teamId) ), }); + const pickMadeAt = Date.now(); + const timeRemainingAtPick = timerSnapshot?.picksExpiresAt + ? Math.max(0, Math.floor((timerSnapshot.picksExpiresAt.getTime() - pickMadeAt) / 1000)) + : (timerSnapshot?.timeRemaining ?? 0); // Create the draft pick const [draftPick] = await db @@ -161,7 +167,7 @@ export async function action(args: ActionFunctionArgs) { pickInRound, pickedByUserId: userId, pickedByType: isTeamOwner ? "owner" : commissionerRecord ? "commissioner" : "admin", - timeUsed: timerSnapshot?.timeRemaining ?? 0, + timeUsed: timeRemainingAtPick, }) .returning(); @@ -210,45 +216,33 @@ export async function action(args: ActionFunctionArgs) { const isDraftComplete = nextPickNumber > totalPicks; // Update the picking team's timer after their pick. - // Standard mode: always reset to the per-pick time, regardless of who picked. - // Chess clock: always add the increment (any pick type — owner, commissioner, or admin). + // Standard mode: always reset to the per-pick time. + // Chess clock: add the increment to the actual remaining time (computed from picksExpiresAt). + // Also clear picksExpiresAt so the timer system knows this team's turn is over. const incrementTime = season.draftIncrementTime || 30; - let newTimeRemaining: number; + const newTimeRemaining = + season.draftTimerMode === "standard" + ? incrementTime + : timeRemainingAtPick + incrementTime; - let updatedTimer: { timeRemaining: number } | undefined; + const timerUpdateSet = { + timeRemaining: newTimeRemaining, + picksExpiresAt: null as Date | null, + picksStartedAt: null as Date | null, + updatedAt: new Date(), + }; - if (season.draftTimerMode === "standard") { - // Atomic reset so the timer loop cannot race with this write. - [updatedTimer] = await db - .update(schema.draftTimers) - .set({ timeRemaining: sql`${incrementTime}`, updatedAt: new Date() }) - .where( - and( - eq(schema.draftTimers.seasonId, seasonId), - eq(schema.draftTimers.teamId, currentDraftSlot.teamId) - ) + const [updatedTimer] = await db + .update(schema.draftTimers) + .set(timerUpdateSet) + .where( + and( + eq(schema.draftTimers.seasonId, seasonId), + eq(schema.draftTimers.teamId, currentDraftSlot.teamId) ) - .returning(); - newTimeRemaining = updatedTimer?.timeRemaining ?? incrementTime; - } else { - // Chess clock: earn the increment regardless of who made the pick (atomic add). - [updatedTimer] = await db - .update(schema.draftTimers) - .set({ - timeRemaining: sql`${schema.draftTimers.timeRemaining} + ${incrementTime}`, - updatedAt: new Date(), - }) - .where( - and( - eq(schema.draftTimers.seasonId, seasonId), - eq(schema.draftTimers.teamId, currentDraftSlot.teamId) - ) - ) - .returning(); - newTimeRemaining = updatedTimer?.timeRemaining ?? incrementTime; - } + ) + .returning(); - // If the timer row didn't exist yet, seed it. if (!updatedTimer) { await db.insert(schema.draftTimers).values({ seasonId, @@ -257,19 +251,8 @@ export async function action(args: ActionFunctionArgs) { }); } - try { - getSocketIO().to(`draft-${seasonId}`).emit("timer-update", { - seasonId, - teamId: currentDraftSlot.teamId, - timeRemaining: newTimeRemaining, - currentPickNumber: nextPickNumber, - }); - } catch (error) { - logger.error("Socket.IO timer-update error:", error); - } - - // Next team's timer is unchanged — their bank carries forward as-is - // (no emit needed; the timer system will start decrementing their existing bank) + // rescheduleTimer (called below after the autodraft chain) will emit timer-pick-started + // for the next team — no need to emit timer-update here. // Update season's current pick number (AFTER initializing next timer to prevent race condition) await db @@ -353,6 +336,13 @@ export async function action(args: ActionFunctionArgs) { .catch((err) => logger.error("On-the-clock email failed:", err)); } + // Reschedule the timer for the next team on the clock (runs after the full autodraft chain). + try { + await rescheduleTimer(seasonId); + } catch (err) { + logger.error("[Pick] rescheduleTimer failed:", err); + } + return Response.json({ success: true, pick: draftPick, diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 67afda8..5c42323 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -493,6 +493,25 @@ export default function DraftRoom() { }, []); useEffect(() => () => { animationTimersRef.current.forEach(clearTimeout); }, []); + // Client-side countdown: updated every second from the server-provided expiresAt timestamp. + const [pickTimerExpiresAt, setPickTimerExpiresAt] = useState<{ teamId: string; expiresAt: number } | null>(() => { + // Seed from initial loader data if a timer is already running. + const active = timers.find((t) => t.picksExpiresAt && t.picksExpiresAt.getTime() > Date.now()); + return active ? { teamId: active.teamId, expiresAt: active.picksExpiresAt!.getTime() } : null; + }); + + useEffect(() => { + if (!pickTimerExpiresAt) return; + const { teamId, expiresAt } = pickTimerExpiresAt; + const tick = () => { + const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)); + setTeamTimers((prev) => prev[teamId] === remaining ? prev : { ...prev, [teamId]: remaining }); + }; + tick(); // immediate update so display is accurate on mount + const id = setInterval(tick, 1_000); + return () => clearInterval(id); + }, [pickTimerExpiresAt, setTeamTimers]); + useDraftSocketEvents({ on, off, @@ -514,6 +533,7 @@ export default function DraftRoom() { setConnectedTeams, setQueue, setTeamTimers, + setPickTimerExpiresAt, setIsOvernightPause, setOvernightResumesAt, setWatchedParticipantIds, diff --git a/database/schema.ts b/database/schema.ts index 473ba4d..ff61b57 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -326,7 +326,9 @@ export const draftTimers = pgTable("draft_timers", { teamId: uuid("team_id") .notNull() .references(() => teams.id, { onDelete: "cascade" }), - timeRemaining: integer("time_remaining").notNull(), // seconds + timeRemaining: integer("time_remaining").notNull(), // seconds (bank in chess_clock; reset value in standard) + picksExpiresAt: timestamp("picks_expires_at"), // when current pick's timer expires; null = not running + picksStartedAt: timestamp("picks_started_at"), // when current pick slot opened updatedAt: timestamp("updated_at").defaultNow().notNull(), }); diff --git a/drizzle/0114_panoramic_human_cannonball.sql b/drizzle/0114_panoramic_human_cannonball.sql new file mode 100644 index 0000000..e87263b --- /dev/null +++ b/drizzle/0114_panoramic_human_cannonball.sql @@ -0,0 +1,2 @@ +ALTER TABLE "draft_timers" ADD COLUMN "picks_expires_at" timestamp;--> statement-breakpoint +ALTER TABLE "draft_timers" ADD COLUMN "picks_started_at" timestamp; \ No newline at end of file diff --git a/drizzle/meta/0114_snapshot.json b/drizzle/meta/0114_snapshot.json new file mode 100644 index 0000000..fe185b1 --- /dev/null +++ b/drizzle/meta/0114_snapshot.json @@ -0,0 +1,6205 @@ +{ + "id": "fd1d64d5-1381-4e52-866f-42e83690e1a3", + "prevId": "a26388b5-0b00-4107-9e26-4b4f4ed8dc31", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.autodraft_settings": { + "name": "autodraft_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mode": { + "name": "mode", + "type": "autodraft_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'next_pick'" + }, + "queue_only": { + "name": "queue_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "autodraft_settings_season_id_seasons_id_fk": { + "name": "autodraft_settings_season_id_seasons_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autodraft_settings_team_id_teams_id_fk": { + "name": "autodraft_settings_team_id_teams_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioner_audit_log": { + "name": "commissioner_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "actor_display_name": { + "name": "actor_display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "audit_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "affected_team_ids": { + "name": "affected_team_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_season_id_idx": { + "name": "audit_log_season_id_idx", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "commissioner_audit_log_season_id_seasons_id_fk": { + "name": "commissioner_audit_log_season_id_seasons_id_fk", + "tableFrom": "commissioner_audit_log", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "commissioner_audit_log_league_id_leagues_id_fk": { + "name": "commissioner_audit_log_league_id_leagues_id_fk", + "tableFrom": "commissioner_audit_log", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cs2_major_stage_results": { + "name": "cs2_major_stage_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_entry": { + "name": "stage_entry", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stage_eliminated": { + "name": "stage_eliminated", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stage_eliminated_wins": { + "name": "stage_eliminated_wins", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_placement": { + "name": "final_placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cs2_major_stage_results_unique": { + "name": "cs2_major_stage_results_unique", + "columns": [ + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cs2_major_stage_results_scoring_event_id_scoring_events_id_fk": { + "name": "cs2_major_stage_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "cs2_major_stage_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cs2_major_stage_results_participant_id_season_participants_id_fk": { + "name": "cs2_major_stage_results_participant_id_season_participants_id_fk", + "tableFrom": "cs2_major_stage_results", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_picks": { + "name": "draft_picks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pick_number": { + "name": "pick_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pick_in_round": { + "name": "pick_in_round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picked_by_user_id": { + "name": "picked_by_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "picked_by_type": { + "name": "picked_by_type", + "type": "picked_by_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "time_used": { + "name": "time_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "draft_picks_season_pick_unique": { + "name": "draft_picks_season_pick_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pick_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "draft_picks_season_id_seasons_id_fk": { + "name": "draft_picks_season_id_seasons_id_fk", + "tableFrom": "draft_picks", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_team_id_teams_id_fk": { + "name": "draft_picks_team_id_teams_id_fk", + "tableFrom": "draft_picks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_participant_id_season_participants_id_fk": { + "name": "draft_picks_participant_id_season_participants_id_fk", + "tableFrom": "draft_picks", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_queue": { + "name": "draft_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_position": { + "name": "queue_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_queue_season_id_seasons_id_fk": { + "name": "draft_queue_season_id_seasons_id_fk", + "tableFrom": "draft_queue", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_team_id_teams_id_fk": { + "name": "draft_queue_team_id_teams_id_fk", + "tableFrom": "draft_queue", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_participant_id_season_participants_id_fk": { + "name": "draft_queue_participant_id_season_participants_id_fk", + "tableFrom": "draft_queue", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_slots": { + "name": "draft_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "draft_order": { + "name": "draft_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_slots_season_id_seasons_id_fk": { + "name": "draft_slots_season_id_seasons_id_fk", + "tableFrom": "draft_slots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_slots_team_id_teams_id_fk": { + "name": "draft_slots_team_id_teams_id_fk", + "tableFrom": "draft_slots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_timers": { + "name": "draft_timers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "time_remaining": { + "name": "time_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picks_expires_at": { + "name": "picks_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "picks_started_at": { + "name": "picks_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_timers_season_id_seasons_id_fk": { + "name": "draft_timers_season_id_seasons_id_fk", + "tableFrom": "draft_timers", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_timers_team_id_teams_id_fk": { + "name": "draft_timers_team_id_teams_id_fk", + "tableFrom": "draft_timers", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_results": { + "name": "event_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_participant_id": { + "name": "season_participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifying_points_awarded": { + "name": "qualifying_points_awarded", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "not_participating": { + "name": "not_participating", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "event_results_event_participant_unique": { + "name": "event_results_event_participant_unique", + "columns": [ + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_results_scoring_event_id_scoring_events_id_fk": { + "name": "event_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "event_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_results_season_participant_id_season_participants_id_fk": { + "name": "event_results_season_participant_id_season_participants_id_fk", + "tableFrom": "event_results", + "tableTo": "season_participants", + "columnsFrom": [ + "season_participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_stage_matches": { + "name": "group_stage_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_group_id": { + "name": "tournament_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant1_score": { + "name": "participant1_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "matchday": { + "name": "matchday", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "group_stage_matches_group_pair_unique": { + "name": "group_stage_matches_group_pair_unique", + "columns": [ + { + "expression": "tournament_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant1_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant2_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_stage_matches_tournament_group_id_tournament_groups_id_fk": { + "name": "group_stage_matches_tournament_group_id_tournament_groups_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "tournament_groups", + "columnsFrom": [ + "tournament_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_stage_matches_participant1_id_season_participants_id_fk": { + "name": "group_stage_matches_participant1_id_season_participants_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "group_stage_matches_participant2_id_season_participants_id_fk": { + "name": "group_stage_matches_participant2_id_season_participants_id_fk", + "tableFrom": "group_stage_matches", + "tableTo": "season_participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_season_id": { + "name": "current_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_public_draft_board": { + "name": "is_public_draft_board", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "discord_webhook_url": { + "name": "discord_webhook_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discord_picks_announcement_enabled": { + "name": "discord_picks_announcement_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_ev_snapshots": { + "name": "participant_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "calculated_ev": { + "name": "calculated_ev", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_ev_snapshots_unique": { + "name": "participant_ev_snapshots_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_ev_snapshots_participant_id_season_participants_id_fk": { + "name": "participant_ev_snapshots_participant_id_season_participants_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk": { + "name": "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_golf_skills": { + "name": "participant_golf_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sg_total": { + "name": "sg_total", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "datagolf_rank": { + "name": "datagolf_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "masters_odds": { + "name": "masters_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "us_open_odds": { + "name": "us_open_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "open_championship_odds": { + "name": "open_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pga_championship_odds": { + "name": "pga_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_golf_skills_participant_unique": { + "name": "participant_golf_skills_participant_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_golf_skills_participant_id_participants_id_fk": { + "name": "participant_golf_skills_participant_id_participants_id_fk", + "tableFrom": "participant_golf_skills", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_season_results": { + "name": "participant_season_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_points": { + "name": "current_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_position": { + "name": "current_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "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 + }, + "table_points": { + "name": "table_points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "goals_for": { + "name": "goals_for", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "goals_against": { + "name": "goals_against", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "goal_difference": { + "name": "goal_difference", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "win_pct": { + "name": "win_pct", + "type": "numeric(5, 4)", + "primaryKey": false, + "notNull": false + }, + "games_played": { + "name": "games_played", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "games_back": { + "name": "games_back", + "type": "numeric(5, 1)", + "primaryKey": false, + "notNull": false + }, + "conference": { + "name": "conference", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "division": { + "name": "division", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "conference_rank": { + "name": "conference_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "division_rank": { + "name": "division_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "league_rank": { + "name": "league_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "streak": { + "name": "streak", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "last_ten": { + "name": "last_ten", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "home_record": { + "name": "home_record", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "away_record": { + "name": "away_record", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "external_team_id": { + "name": "external_team_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "srs": { + "name": "srs", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rss_participant_season_idx": { + "name": "rss_participant_season_idx", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "regular_season_standings_participant_id_season_participants_id_fk": { + "name": "regular_season_standings_participant_id_season_participants_id_fk", + "tableFrom": "regular_season_standings", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "regular_season_standings_sports_season_id_sports_seasons_id_fk": { + "name": "regular_season_standings_sports_season_id_sports_seasons_id_fk", + "tableFrom": "regular_season_standings", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scoring_events": { + "name": "scoring_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tournament_id": { + "name": "tournament_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "event_starts_at": { + "name": "event_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "playoff_round": { + "name": "playoff_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_qualifying_event": { + "name": "is_qualifying_event", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bracket_template_id": { + "name": "bracket_template_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "scoring_starts_at_round": { + "name": "scoring_starts_at_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "bracket_region_config": { + "name": "bracket_region_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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_golf_skills": { + "name": "season_participant_golf_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sg_total": { + "name": "sg_total", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "datagolf_rank": { + "name": "datagolf_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "masters_odds": { + "name": "masters_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "us_open_odds": { + "name": "us_open_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "open_championship_odds": { + "name": "open_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pga_championship_odds": { + "name": "pga_championship_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_participant_golf_skills_unique": { + "name": "season_participant_golf_skills_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_participant_golf_skills_participant_id_season_participants_id_fk": { + "name": "season_participant_golf_skills_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_golf_skills", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_golf_skills_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_golf_skills_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_golf_skills", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_qualifying_totals": { + "name": "season_participant_qualifying_totals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_qualifying_points": { + "name": "total_qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "events_scored": { + "name": "events_scored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "final_ranking": { + "name": "final_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_participant_qualifying_totals_participant_id_season_participants_id_fk": { + "name": "season_participant_qualifying_totals_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_qualifying_totals", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_qualifying_totals_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_qualifying_totals_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_qualifying_totals", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_results": { + "name": "season_participant_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "final_position": { + "name": "final_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_partial_score": { + "name": "is_partial_score", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qualifying_points": { + "name": "qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_participant_results_participant_id_season_participants_id_fk": { + "name": "season_participant_results_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_results", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_results_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participant_simulator_inputs": { + "name": "season_participant_simulator_inputs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_odds": { + "name": "source_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_elo": { + "name": "source_elo", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "world_ranking": { + "name": "world_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": false + }, + "projected_wins": { + "name": "projected_wins", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_table_points": { + "name": "projected_table_points", + "type": "numeric(6, 2)", + "primaryKey": false, + "notNull": false + }, + "seed": { + "name": "seed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "season_participant_simulator_inputs_unique": { + "name": "season_participant_simulator_inputs_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "season_participant_simulator_inputs_season_idx": { + "name": "season_participant_simulator_inputs_season_idx", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_participant_simulator_inputs_participant_id_season_participants_id_fk": { + "name": "season_participant_simulator_inputs_participant_id_season_participants_id_fk", + "tableFrom": "season_participant_simulator_inputs", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participant_simulator_inputs_sports_season_id_sports_seasons_id_fk": { + "name": "season_participant_simulator_inputs_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participant_simulator_inputs", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_participants": { + "name": "season_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "vorp_value": { + "name": "vorp_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participants_sports_season_name_unique": { + "name": "participants_sports_season_name_unique", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "season_participants_sports_season_id_sports_seasons_id_fk": { + "name": "season_participants_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_participants", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_participants_participant_id_participants_id_fk": { + "name": "season_participants_participant_id_participants_id_fk", + "tableFrom": "season_participants", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_sports": { + "name": "season_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_sports_season_id_seasons_id_fk": { + "name": "season_sports_season_id_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_template_sports": { + "name": "season_template_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "auto_start_draft": { + "name": "auto_start_draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "draft_initial_time": { + "name": "draft_initial_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 120 + }, + "draft_increment_time": { + "name": "draft_increment_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "draft_timer_mode": { + "name": "draft_timer_mode", + "type": "draft_timer_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'chess_clock'" + }, + "current_pick_number": { + "name": "current_pick_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "draft_started_at": { + "name": "draft_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_completed_at": { + "name": "draft_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_paused": { + "name": "draft_paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "overnight_pause_mode": { + "name": "overnight_pause_mode", + "type": "overnight_pause_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "overnight_pause_start": { + "name": "overnight_pause_start", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "overnight_pause_end": { + "name": "overnight_pause_end", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "overnight_pause_timezone": { + "name": "overnight_pause_timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "points_for_1st": { + "name": "points_for_1st", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "points_for_2nd": { + "name": "points_for_2nd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "points_for_3rd": { + "name": "points_for_3rd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "points_for_4th": { + "name": "points_for_4th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "points_for_5th": { + "name": "points_for_5th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_6th": { + "name": "points_for_6th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_7th": { + "name": "points_for_7th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "points_for_8th": { + "name": "points_for_8th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seasons_template_id_season_templates_id_fk": { + "name": "seasons_template_id_season_templates_id_fk", + "tableFrom": "seasons", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "seasons_invite_code_unique": { + "name": "seasons_invite_code_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.simulator_profiles": { + "name": "simulator_profiles", + "schema": "", + "columns": { + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": true, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_config": { + "name": "default_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports": { + "name": "sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sport_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "exclude_from_homepage": { + "name": "exclude_from_homepage", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sports_slug_unique": { + "name": "sports_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_season_simulator_configs": { + "name": "sports_season_simulator_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sports_season_simulator_config_unique": { + "name": "sports_season_simulator_config_unique", + "columns": [ + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sports_season_simulator_config_type_idx": { + "name": "sports_season_simulator_config_type_idx", + "columns": [ + { + "expression": "simulator_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sports_season_simulator_configs_sports_season_id_sports_seasons_id_fk": { + "name": "sports_season_simulator_configs_sports_season_id_sports_seasons_id_fk", + "tableFrom": "sports_season_simulator_configs", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_seasons": { + "name": "sports_seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fantasy_season_id": { + "name": "fantasy_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sports_season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'upcoming'" + }, + "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" + }, + "sports_seasons_fantasy_season_id_seasons_id_fk": { + "name": "sports_seasons_fantasy_season_id_seasons_id_fk", + "tableFrom": "sports_seasons", + "tableTo": "seasons", + "columnsFrom": [ + "fantasy_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_ev_snapshots": { + "name": "team_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_ev_snapshots_unique": { + "name": "team_ev_snapshots_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_ev_snapshots_team_id_teams_id_fk": { + "name": "team_ev_snapshots_team_id_teams_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_ev_snapshots_season_id_seasons_id_fk": { + "name": "team_ev_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_score_events": { + "name": "team_score_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scoring_event_name": { + "name": "scoring_event_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "sport_name": { + "name": "sport_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "match_id": { + "name": "match_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant_ids": { + "name": "participant_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "points_delta": { + "name": "points_delta", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_score_events_match_unique": { + "name": "team_score_events_match_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "match_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "match_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_score_events_event_unique": { + "name": "team_score_events_event_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scoring_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "match_id IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_score_events_team_id_teams_id_fk": { + "name": "team_score_events_team_id_teams_id_fk", + "tableFrom": "team_score_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_score_events_season_id_seasons_id_fk": { + "name": "team_score_events_season_id_seasons_id_fk", + "tableFrom": "team_score_events", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_score_events_scoring_event_id_scoring_events_id_fk": { + "name": "team_score_events_scoring_event_id_scoring_events_id_fk", + "tableFrom": "team_score_events", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "team_score_events_match_id_playoff_matches_id_fk": { + "name": "team_score_events_match_id_playoff_matches_id_fk", + "tableFrom": "team_score_events", + "tableTo": "playoff_matches", + "columnsFrom": [ + "match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_sport_scores": { + "name": "team_sport_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "participants_completed": { + "name": "participants_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_total": { + "name": "participants_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_sport_scores_team_id_teams_id_fk": { + "name": "team_sport_scores_team_id_teams_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_sport_scores_sports_season_id_sports_seasons_id_fk": { + "name": "team_sport_scores_sports_season_id_sports_seasons_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings": { + "name": "team_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_rank": { + "name": "current_rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_rank": { + "name": "previous_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_team_id_teams_id_fk": { + "name": "team_standings_team_id_teams_id_fk", + "tableFrom": "team_standings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_season_id_seasons_id_fk": { + "name": "team_standings_season_id_seasons_id_fk", + "tableFrom": "team_standings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings_snapshots": { + "name": "team_standings_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_standings_snapshots_unique": { + "name": "team_standings_snapshots_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_standings_snapshots_team_id_teams_id_fk": { + "name": "team_standings_snapshots_team_id_teams_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_snapshots_season_id_seasons_id_fk": { + "name": "team_standings_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "flag_config": { + "name": "flag_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "avatar_type": { + "name": "avatar_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'owner'" + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "teams_season_id_lower_name_unique": { + "name": "teams_season_id_lower_name_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_group_members": { + "name": "tournament_group_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_group_id": { + "name": "tournament_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tournament_group_members_tournament_group_id_tournament_groups_id_fk": { + "name": "tournament_group_members_tournament_group_id_tournament_groups_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "tournament_groups", + "columnsFrom": [ + "tournament_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tournament_group_members_participant_id_season_participants_id_fk": { + "name": "tournament_group_members_participant_id_season_participants_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_groups": { + "name": "tournament_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tournament_groups_scoring_event_id_scoring_events_id_fk": { + "name": "tournament_groups_scoring_event_id_scoring_events_id_fk", + "tableFrom": "tournament_groups", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_results": { + "name": "tournament_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_id": { + "name": "tournament_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tournament_results_tournament_participant_unique": { + "name": "tournament_results_tournament_participant_unique", + "columns": [ + { + "expression": "tournament_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tournament_results_tournament_id_tournaments_id_fk": { + "name": "tournament_results_tournament_id_tournaments_id_fk", + "tableFrom": "tournament_results", + "tableTo": "tournaments", + "columnsFrom": [ + "tournament_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tournament_results_participant_id_participants_id_fk": { + "name": "tournament_results_participant_id_participants_id_fk", + "tableFrom": "tournament_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournaments": { + "name": "tournaments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "tournament_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "external_key": { + "name": "external_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tournaments_sport_name_year_unique": { + "name": "tournaments_sport_name_year_unique", + "columns": [ + { + "expression": "sport_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tournaments_sport_id_sports_id_fk": { + "name": "tournaments_sport_id_sports_id_fk", + "tableFrom": "tournaments", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "flag_config": { + "name": "flag_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "custom_avatar_url": { + "name": "custom_avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "avatar_type": { + "name": "avatar_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'flag'" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "discord_ping_enabled": { + "name": "discord_ping_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "draft_email_notifications_enabled": { + "name": "draft_email_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_data_request_at": { + "name": "last_data_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_username_lower_unique": { + "name": "users_username_lower_unique", + "columns": [ + { + "expression": "lower(\"username\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.watchlist": { + "name": "watchlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "watchlist_season_team_participant_unique": { + "name": "watchlist_season_team_participant_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "watchlist_season_id_seasons_id_fk": { + "name": "watchlist_season_id_seasons_id_fk", + "tableFrom": "watchlist", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "watchlist_team_id_teams_id_fk": { + "name": "watchlist_team_id_teams_id_fk", + "tableFrom": "watchlist", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "watchlist_participant_id_season_participants_id_fk": { + "name": "watchlist_participant_id_season_participants_id_fk", + "tableFrom": "watchlist", + "tableTo": "season_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.audit_action": { + "name": "audit_action", + "schema": "public", + "values": [ + "league_settings_changed", + "draft_settings_changed", + "scoring_rules_changed", + "sports_changed", + "draft_order_set", + "draft_order_randomized", + "draft_started", + "draft_paused", + "draft_resumed", + "draft_reset", + "draft_rollback", + "force_autopick", + "force_manual_pick", + "draft_pick_changed", + "time_bank_edited", + "brackt_resolved" + ] + }, + "public.autodraft_mode": { + "name": "autodraft_mode", + "schema": "public", + "values": [ + "next_pick", + "while_on" + ] + }, + "public.draft_timer_mode": { + "name": "draft_timer_mode", + "schema": "public", + "values": [ + "chess_clock", + "standard" + ] + }, + "public.event_type": { + "name": "event_type", + "schema": "public", + "values": [ + "playoff_game", + "major_tournament", + "final_standings", + "schedule_event" + ] + }, + "public.overnight_pause_mode": { + "name": "overnight_pause_mode", + "schema": "public", + "values": [ + "none", + "league", + "per_user" + ] + }, + "public.picked_by_type": { + "name": "picked_by_type", + "schema": "public", + "values": [ + "owner", + "commissioner", + "admin", + "auto" + ] + }, + "public.playoff_match_game_status": { + "name": "playoff_match_game_status", + "schema": "public", + "values": [ + "scheduled", + "complete", + "postponed" + ] + }, + "public.probability_source": { + "name": "probability_source", + "schema": "public", + "values": [ + "manual", + "futures_odds", + "elo_simulation", + "performance_model" + ] + }, + "public.scoring_pattern": { + "name": "scoring_pattern", + "schema": "public", + "values": [ + "playoff_bracket", + "season_standings", + "qualifying_points" + ] + }, + "public.scoring_type": { + "name": "scoring_type", + "schema": "public", + "values": [ + "playoffs", + "regular_season", + "majors" + ] + }, + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + }, + "public.simulation_status": { + "name": "simulation_status", + "schema": "public", + "values": [ + "idle", + "running", + "failed" + ] + }, + "public.simulator_type": { + "name": "simulator_type", + "schema": "public", + "values": [ + "f1_standings", + "indycar_standings", + "golf_qualifying_points", + "playoff_bracket", + "ucl_bracket", + "ncaam_bracket", + "ncaaw_bracket", + "nba_bracket", + "nhl_bracket", + "nfl_bracket", + "afl_bracket", + "epl_standings", + "snooker_bracket", + "tennis_qualifying_points", + "mlb_bracket", + "wnba_bracket", + "world_cup", + "darts_bracket", + "cs2_major_qualifying_points", + "ncaa_football_bracket", + "llws_bracket", + "college_hockey_bracket", + "brackt", + "nll_bracket", + "mls_bracket" + ] + }, + "public.sport_type": { + "name": "sport_type", + "schema": "public", + "values": [ + "team", + "individual" + ] + }, + "public.sports_season_status": { + "name": "sports_season_status", + "schema": "public", + "values": [ + "upcoming", + "active", + "completed" + ] + }, + "public.tournament_status": { + "name": "tournament_status", + "schema": "public", + "values": [ + "scheduled", + "in_progress", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 44d1da3..d530318 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -799,6 +799,13 @@ "when": 1779579877169, "tag": "0113_bent_banshee", "breakpoints": true + }, + { + "idx": 114, + "version": "7", + "when": 1780454064143, + "tag": "0114_panoramic_human_cannonball", + "breakpoints": true } ] } \ No newline at end of file diff --git a/plans/zero-downtime-scaling.md b/plans/zero-downtime-scaling.md new file mode 100644 index 0000000..4b37c2c --- /dev/null +++ b/plans/zero-downtime-scaling.md @@ -0,0 +1,251 @@ +# Plan: Zero-Downtime Deploys + Multi-Server Scaling + +## Context + +Currently the draft timer (`server/timer.ts`) runs as a `setInterval` every second inside the web server process. This causes two problems: + +1. **Deploys interrupt active drafts** — when the web server restarts during a rolling deploy, the timer stops, Socket.IO clients disconnect, and users see a "reconnecting" banner mid-draft. +2. **Cannot run multiple web server instances** — if two instances both run the timer tick, they independently decrement `timeRemaining` (2x speed) and both attempt to trigger autopicks on the same pick slot. + +Additionally, `server/snapshots.ts` has the same per-instance `setInterval` problem (daily snapshots would be created multiple times). + +There is no Redis, no distributed locking, and no graceful shutdown handler today. + +## Three Goals (in priority order) + +1. **Draft timer survives web server deploys** — timer lives in a separate worker process that isn't restarted when the web server is. +2. **Multiple web server instances can safely coexist** — Socket.IO state shared via Redis adapter; timer runs only once (in the worker). +3. **Cron jobs (standings sync, daily snapshots) run reliably once** — managed inside the same worker. + +--- + +## Architecture Target + +``` +┌─────────────────────┐ Redis (pub/sub + ┌─────────────────────┐ +│ Web Server(s) │ Socket.IO adapter) │ Timer Worker │ +│ - HTTP / SSR │◄──────────────────────►│ - Draft timers │ +│ - Socket.IO │ │ - Daily snapshots │ +│ - React Router │ │ - Standings crons │ +└─────────────────────┘ └─────────────────────┘ + │ │ + └───────────────────── PostgreSQL ──────────────┘ +``` + +Socket.IO uses `@socket.io/redis-adapter` so any instance can broadcast to any client room. The timer worker connects to the same Redis bus and emits into draft rooms — web servers relay those to connected clients. + +--- + +## Phase 1: Event-Driven Timer (stop per-second DB writes) ← START HERE ✅ + +**Goal**: replace the `setInterval(1000)` + per-second `UPDATE draftTimers SET timeRemaining = GREATEST(timeRemaining - 1, 0)` with a model where the server stores *when the timer expires* and uses a single `setTimeout`. + +### 1a. DB schema change + +Add two nullable columns to `draft_timers`: + +```ts +// database/schema.ts +picksExpiresAt: timestamp("picks_expires_at"), // null = paused or pick just made +picksStartedAt: timestamp("picks_started_at"), // when current pick slot opened +``` + +**Migration**: `npm run db:generate && npm run db:migrate` + +Keep `timeRemaining` — it is still the authoritative bank value in chess-clock mode and is needed when pausing/unpausing to know how much time to restore. + +### 1b. Timer logic rewrite (`server/timer.ts`) + +Replace `setInterval(fn, 1000)` with a scheduler that: + +1. **On startup**, reads all active draft seasons from DB and calls `schedulePick(season)` for each. +2. **`schedulePick(season)`**: + - Looks up current team + their `picksExpiresAt` from DB. + - If already set: `setTimeout(onExpiry, picksExpiresAt - now)`. + - If null (pick just started or timer not set): calls `initPick(season)` to write `picksExpiresAt = now + timeRemaining` and then schedules the timeout. +3. **`onExpiry(seasonId, teamId, pickNumber)`**: triggers the autopick (same logic as today's `triggerAutoPick`). After pick resolves, calls `schedulePick` for the next team. +4. **Recovery interval**: a `setInterval(recovery, 30_000)` (not 1 s!) queries active drafts and re-calls `schedulePick` for any whose in-memory timeout was lost (e.g., process restart). This is the only periodic DB query. + +**Overnight pause handling**: when an overnight pause window activates, cancel the in-memory `setTimeout`, set `picksExpiresAt = null` in DB, record remaining time in `timeRemaining`. When the window ends, restore via `initPick`. The recovery interval detects this automatically on restart. + +**Chess clock mode**: `timeRemaining` stores the bank; `picksExpiresAt = now + timeRemaining` when a pick starts. `picksStartedAt` tracks when the pick opened. On pick completion, calculate actual elapsed: `bankUsed = now - picksStartedAt`, new `timeRemaining = timeRemaining - bankUsed`. + +### 1c. Socket.IO event change + +Stop emitting `timer-update` every second. Instead emit: + +| Event | When | Payload | +|---|---|---| +| `timer-pick-started` | Pick slot opens | `{ seasonId, teamId, pickNumber, expiresAt, timeRemaining }` | +| `timer-paused` | Draft paused or overnight pause | `{ seasonId, teamId, overnightPauseActive, resumesAtUTC? }` | +| `timer-resumed` | Pause lifted | `{ seasonId, teamId, expiresAt, timeRemaining }` | +| `pick-made` | Already exists | no change | + +Client (`app/hooks/useDraftSocketEvents.ts`) counts down locally from `expiresAt` using a client-side `setInterval`. Remove the server-driven timer display logic. + +### 1d. Client update + +- On `timer-pick-started`: store `expiresAt` in state, start a local `setInterval` to display countdown. +- On `draft-state-sync` (reconnect): server sends current `picksExpiresAt` so client can resume countdown. +- Remove any logic that relied on receiving `timer-update` every second. + +### Files changed in Phase 1 + +| File | Change | +|---|---| +| `database/schema.ts` | Add `picksExpiresAt`, `picksStartedAt` to `draftTimers` | +| `server/timer.ts` | Rewrite: event-driven scheduler, 30s recovery interval | +| `server/socket.d.ts` | Add `timer-pick-started`, `timer-paused`, `timer-resumed`; remove `timer-update` | +| `server/socket.ts` | Emit new events on draft state sync (reconnect path) | +| `app/hooks/useDraftSocketEvents.ts` | Handle new events, remove `timer-update` handler | +| `app/routes/leagues/$leagueId.draft.$seasonId.tsx` | Client-side countdown from `expiresAt` | + +**Verification**: +- `npm run typecheck` — no errors +- `npm run test:run` — passes +- Manual: start a draft, watch picks countdown without seeing any "timer-update" Socket.IO events in browser devtools. Confirm pick fires at correct time. Confirm reconnect restores correct remaining time. + +--- + +## Phase 2: Separate Worker Process + +**Goal**: timer runs in a process that is not restarted during web server deploys. + +### 2a. Add Redis to infrastructure + +Add to `docker-compose.yml`: +```yaml +redis: + image: redis:7-alpine + restart: unless-stopped + volumes: + - redis_data:/data +``` + +Add env var `REDIS_URL` (default `redis://localhost:6379`). + +Install: `npm install ioredis @socket.io/redis-adapter` + +### 2b. Socket.IO Redis adapter on web servers + +In `server/socket.ts`, after `new Server(...)`: +```ts +import { createAdapter } from "@socket.io/redis-adapter"; +import { createClient } from "ioredis"; + +const pub = createClient(process.env.REDIS_URL); +const sub = pub.duplicate(); +io.adapter(createAdapter(pub, sub)); +``` + +This means any server instance can broadcast to any client in any room. + +Remove the `connectedTeams` in-memory Map — replace with Redis-backed presence or accept best-effort on reconnect. + +### 2c. New worker entry point + +Create `worker/index.ts`: +```ts +import { startDraftTimerSystem } from "../server/timer"; +import { startSnapshotSystem } from "../server/snapshots"; + +startDraftTimerSystem(); +startSnapshotSystem(); +``` + +**For Socket.IO emission from the worker**: use direct Redis pub/sub (simpler than a second Socket.IO server). Add a `server/timer-events.ts` module with `publishTimerEvent(channel, payload)` (worker side) and `subscribeTimerEvents(io)` (web server side). + +### 2d. Docker Compose + +Add worker service: +```yaml +worker: + image: ${REGISTRY}/brackt:${TAG} + command: node dist/worker.js + depends_on: [db, redis] + environment: *app-env + restart: unless-stopped +``` + +### Files to change in Phase 2 + +| File | Change | +|---|---| +| `docker-compose.yml` | Add `redis` and `worker` services | +| `server/socket.ts` | Add Redis adapter; add timer-event subscription | +| `server/timer.ts` | Emit via Redis pub/sub instead of `getSocketIO()` | +| `server/snapshots.ts` | Move to worker only (remove import from `server.ts`) | +| `worker/index.ts` | New file: starts timer + snapshots | +| `Dockerfile` | Copy `worker/` dir; add `dist/worker.js` output | +| `package.json` | Add `build:worker` script | +| `.forgejo/workflows/deploy.yml` | Deploy worker service alongside app | + +--- + +## Phase 3: Zero-Downtime Web Server Deploy + +**Goal**: rolling restart of web servers doesn't disconnect clients abruptly. + +### 3a. Graceful shutdown + +In `server.ts`, add SIGTERM handler: +```ts +process.on("SIGTERM", () => { + httpServer.close(() => process.exit(0)); + io.close(() => process.exit(0)); + setTimeout(() => process.exit(0), 10_000); +}); +``` + +### 3b. Health check endpoint + +Add `GET /health` → `{ status: "ok" }` to `server/app.ts`. + +### 3c. Docker config + +```yaml +app: + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 10s + start_period: 15s + stop_grace_period: 15s +``` + +--- + +## Phase 4: Cron Infrastructure in Worker + +**Goal**: standings sync and other periodic tasks run on a schedule, once. + +Install `node-cron` in the worker and schedule jobs: +```ts +cron.schedule("0 3 * * *", () => syncAllStandings(db)); +``` + +Use a Postgres advisory lock if multiple workers are ever needed. + +--- + +## Implementation Order + +``` +Phase 1 (event-driven timer) — no infrastructure needed, pure code change + ↓ +Phase 2 (worker + Redis) — adds Redis, separates worker process + ↓ +Phase 3 (graceful shutdown + health check) — deploy strategy improvement + ↓ +Phase 4 (cron in worker) — scheduling infrastructure +``` + +## Key Existing Code to Preserve/Reuse + +| Code | Path | Notes | +|---|---|---| +| `startDraftTimerSystem()` / `stopDraftTimerSystem()` | `server/timer.ts` | Keep the public API; replace internals | +| `triggerAutoPick()` | `server/timer.ts` | Unchanged logic, just called from `setTimeout` instead of `setInterval` | +| `checkOvernightPause()` | `server/timer.ts` | Keep as-is | +| `executeAutoPick()` | `app/models/draft-utils.ts` | Unchanged | +| `getSocketIO()` | `server/socket.ts` | Used in web server; worker will bypass via Redis pub/sub in Phase 2 | +| `draftSlotsCache` | `server/timer.ts` | Keep — still valid optimization for the recovery check | diff --git a/server/__tests__/timer-autodraft.test.ts b/server/__tests__/timer-autodraft.test.ts index 9e6459a..000ba69 100644 --- a/server/__tests__/timer-autodraft.test.ts +++ b/server/__tests__/timer-autodraft.test.ts @@ -328,15 +328,15 @@ describe('Timer Autodraft Integration', () => { expect(0 + 30).toBe(30); }); - it('should emit timer-update after adding increment', async () => { + it('should emit timer-pick-started after scheduling next pick', async () => { const seasonId = 'season-123'; const teamId = 'team-456'; - mockSocketIO.to(`draft-${seasonId}`).emit('timer-update', { - seasonId, teamId, timeRemaining: 30, currentPickNumber: 1, - }); - expect(mockSocketIO.emit).toHaveBeenCalledWith('timer-update', { - seasonId, teamId, timeRemaining: 30, currentPickNumber: 1, + mockSocketIO.to(`draft-${seasonId}`).emit('timer-pick-started', { + seasonId, teamId, pickNumber: 1, expiresAt: Date.now() + 30000, timeRemaining: 30, }); + expect(mockSocketIO.emit).toHaveBeenCalledWith('timer-pick-started', + expect.objectContaining({ seasonId, teamId, timeRemaining: 30 }) + ); }); }); }); diff --git a/server/socket.ts b/server/socket.ts index 585210d..ff7b7ae 100644 --- a/server/socket.ts +++ b/server/socket.ts @@ -18,11 +18,17 @@ interface ServerToClientEvents { "draft-started": (data: { seasonId: string; currentPickNumber: number }) => void; "draft-completed": () => void; "draft-room-closed": () => void; - "timer-update": (data: { + "timer-pick-started": (data: { seasonId: string; teamId: string; - timeRemaining: number; - currentPickNumber: number; + pickNumber: number; + expiresAt: number; // ms timestamp + timeRemaining: number; // seconds remaining at emit time + }) => void; + "timer-overnight-paused": (data: { + seasonId: string; + teamId: string; + resumesAtUTC?: number; }) => void; "autodraft-updated": (data: { teamId: string; @@ -60,6 +66,7 @@ interface ServerToClientEvents { timers: Array<{ teamId: string; timeRemaining: number; + expiresAt?: number; // ms timestamp; present for the currently-active team }>; queue?: Array<{ id: string; @@ -283,10 +290,19 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer { isPaused: seasonData.draftPaused || false, status: seasonData.status, picks, - timers: timerRows.map((t) => ({ - teamId: t.teamId, - timeRemaining: t.timeRemaining, - })), + timers: timerRows.map((t) => { + const nowMs = Date.now(); + const expiresAt = t.picksExpiresAt?.getTime(); + const computedRemaining = + expiresAt && expiresAt > nowMs + ? Math.max(0, Math.floor((expiresAt - nowMs) / 1000)) + : t.timeRemaining; + return { + teamId: t.teamId, + timeRemaining: computedRemaining, + expiresAt: expiresAt && expiresAt > nowMs ? expiresAt : undefined, + }; + }), queue: teamId ? queueItems : undefined, watchlistParticipantIds: teamId ? watchlistItems.map((w) => w.participantId) : undefined, }); diff --git a/server/timer.ts b/server/timer.ts index 6fedafe..ef004a6 100644 --- a/server/timer.ts +++ b/server/timer.ts @@ -1,5 +1,5 @@ import * as schema from "~/database/schema"; -import { eq, and, asc, sql, lte, isNotNull } from "drizzle-orm"; +import { eq, and, asc, lte, isNotNull } from "drizzle-orm"; import type { InferSelectModel } from "drizzle-orm"; import { getSocketIO } from "./socket"; import { executeAutoPick, calculatePickInfo } from "~/models/draft-utils"; @@ -8,17 +8,19 @@ import { logger } from "./logger"; import { db } from "./db"; import { startDraft } from "~/services/draft-autostart"; -let timerInterval: NodeJS.Timeout | null = null; -let timerTickRunning = false; +// Per-season in-memory state +const pickTimeouts = new Map(); // seasonId → active setTimeout +const overnightResumeTimeouts = new Map(); // seasonId → resume setTimeout +const schedulingInProgress = new Set(); // prevents concurrent scheduling -// Draft slots never change during an active draft — cache them to avoid -// a redundant query on every tick. +// Draft slots never change during an active draft — cache them to avoid a redundant query. const draftSlotsCache = new Map(); // Team timezone cache for per_user overnight pause mode (seasonId → teamId → timezone). -// Populated lazily; evicted when the season leaves active drafting. const teamTimezoneCache = new Map>(); +let recoveryInterval: NodeJS.Timeout | null = null; + async function getTeamTimezone(seasonId: string, teamId: string): Promise { let seasonMap = teamTimezoneCache.get(seasonId); if (!seasonMap) { @@ -26,13 +28,11 @@ async function getTeamTimezone(seasonId: string, teamId: string): Promise [r.teamId, r.timezone || null])); } catch (err) { - // ownerId column is varchar; if any row still holds a non-UUID value - // (pre-migration remnant) the ::uuid cast throws. Degrade gracefully. - logger.error("[Timer] getTeamTimezone cast failed, skipping overnight check:", err); + logger.error("[Timer] getTeamTimezone failed:", err); seasonMap = new Map(); } teamTimezoneCache.set(seasonId, seasonMap); @@ -40,10 +40,6 @@ async function getTeamTimezone(seasonId: string, teamId: string): Promise, currentTeamId: string @@ -58,7 +54,6 @@ async function checkOvernightPause( if (mode === "league") { tz = season.overnightPauseTimezone || null; } else { - // per_user: use the team owner's timezone, fall back to league timezone tz = await getTeamTimezone(season.id, currentTeamId); if (!tz) tz = season.overnightPauseTimezone || null; } @@ -72,42 +67,16 @@ async function checkOvernightPause( return { active: false }; } -/** - * Start the draft timer system - * Runs every second to update all active draft timers - */ -export function startDraftTimerSystem(): void { - if (timerInterval) { - logger.log("[Timer] Timer system already running"); - return; - } - - timerInterval = setInterval(async () => { - // Skip this tick if the previous one is still running (e.g. long autodraft chain) - // to prevent concurrent ticks from racing on the same pick slot. - if (timerTickRunning) return; - timerTickRunning = true; - try { - await updateDraftTimers(); - } catch (error) { - logger.error("[Timer] Error updating draft timers:", error); - } finally { - timerTickRunning = false; - } - }, 1000); - - logger.log("[Timer] Draft timer system started"); -} - -/** - * Stop the draft timer system - */ -export function stopDraftTimerSystem(): void { - if (timerInterval) { - clearInterval(timerInterval); - timerInterval = null; - logger.log("[Timer] Draft timer system stopped"); +async function getDraftSlotsCached(seasonId: string): Promise<{ teamId: string; draftOrder: number }[]> { + let slots = draftSlotsCache.get(seasonId); + if (!slots) { + slots = await db.query.draftSlots.findMany({ + where: eq(schema.draftSlots.seasonId, seasonId), + orderBy: asc(schema.draftSlots.draftOrder), + }); + draftSlotsCache.set(seasonId, slots); } + return slots; } async function checkAndAutoStartDrafts(): Promise { @@ -136,8 +105,6 @@ async function checkAndAutoStartDrafts(): Promise { logger.error(`[Timer] Auto-start failed for season ${season.id}: ${result.error}`); if (result.error === "No draft slots found for this season") { - // Disable auto-start to stop the retry loop. The commissioner will see - // the setting is now off and can re-enable it once the order is set. await db .update(schema.seasons) .set({ autoStartDraft: false }) @@ -147,206 +114,32 @@ async function checkAndAutoStartDrafts(): Promise { } } -/** - * Update all active draft timers - * Called every second by the timer interval - */ -async function updateDraftTimers(): Promise { - await checkAndAutoStartDrafts(); - - const io = getSocketIO(); - - // Get all active drafts - const activeDrafts = await db.query.seasons.findMany({ - where: eq(schema.seasons.status, "draft"), - }); - - if (activeDrafts.length === 0) { - draftSlotsCache.clear(); - return; - } - - // Evict cache entries for seasons no longer actively drafting. - const activeIds = new Set(activeDrafts.map((s) => s.id)); - for (const cachedId of draftSlotsCache.keys()) { - if (!activeIds.has(cachedId)) draftSlotsCache.delete(cachedId); - } - for (const cachedId of teamTimezoneCache.keys()) { - if (!activeIds.has(cachedId)) teamTimezoneCache.delete(cachedId); - } - - for (const season of activeDrafts) { - // Skip if draft is paused - if (season.draftPaused) { - continue; - } - - const currentPickNumber = season.currentPickNumber ?? 1; - - // Draft slots never change during an active draft — use the cache. - let draftSlots = draftSlotsCache.get(season.id); - if (!draftSlots) { - draftSlots = await db.query.draftSlots.findMany({ - where: eq(schema.draftSlots.seasonId, season.id), - orderBy: asc(schema.draftSlots.draftOrder), - }); - draftSlotsCache.set(season.id, draftSlots); - } - - const totalTeams = draftSlots.length; - if (totalTeams === 0) continue; - - const { pickInRound } = calculatePickInfo(currentPickNumber, totalTeams); - const currentDraftSlot = draftSlots.find( - (slot) => slot.draftOrder === pickInRound - ); - - if (!currentDraftSlot) { - continue; - } - - const currentTeamId = currentDraftSlot.teamId; - - // Get current team's timer - const timer = await db.query.draftTimers.findFirst({ - where: and( - eq(schema.draftTimers.seasonId, season.id), - eq(schema.draftTimers.teamId, currentTeamId) - ), - }); - - if (!timer) { - logger.warn( - `[Timer] No timer found for team ${currentTeamId} in season ${season.id}, creating with initial time` - ); - // Standard mode seeds with the per-pick time; chess clock seeds with the full bank. - const initialTime = season.draftTimerMode === "standard" - ? (season.draftIncrementTime || 30) - : (season.draftInitialTime || 120); - await db - .insert(schema.draftTimers) - .values({ - seasonId: season.id, - teamId: currentTeamId, - timeRemaining: initialTime, - }); - - // Emit timer update so clients are aware of the new timer - io.to(`draft-${season.id}`).emit("timer-update", { - seasonId: season.id, - teamId: currentTeamId, - timeRemaining: initialTime, - currentPickNumber, - }); - - // Continue processing with the newly created timer on the next tick - continue; - } - - // Fetch autodraft settings once — used both for while_on bypass and timer-expiry path. - const autodraftSettings = await db.query.autodraftSettings.findFirst({ - where: and( - eq(schema.autodraftSettings.seasonId, season.id), - eq(schema.autodraftSettings.teamId, currentTeamId) - ), - }); - const shouldAutodraft = autodraftSettings?.isEnabled ?? false; - // while_on means "pick immediately when it's my turn" — bypass the countdown. - const isWhileOn = shouldAutodraft && autodraftSettings?.mode === "while_on"; - - // Overnight pause: freeze timer for non-autodraft teams in their overnight window. - const overnightPause = await checkOvernightPause(season, currentTeamId); - const isOvernightFreeze = overnightPause.active && !shouldAutodraft; - - // Trigger pick when: timer expired OR team is in while_on autodraft mode. - // The chain picks consecutive while_on teams after each pick, but it is capped at - // totalTeams iterations. The while_on bypass here fills the gap: the timer picks - // the next while_on team within one tick (≤1 s) rather than waiting for the full - // countdown to expire. If the chain already made this pick, executeAutoPick - // returns "Pick already made" which triggerAutoPick treats as a non-fatal no-op. - if (timer.timeRemaining <= 0 || isWhileOn) { - if (timer.timeRemaining <= 0) { - logger.log( - `[Timer] ⚠️ Timer expired for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})` - ); - } else { - logger.log( - `[Timer] ⚡ while_on autodraft — immediate pick for team ${currentTeamId} in season ${season.id} (pick ${currentPickNumber})` - ); - // Emit 0 so clients see the timer hit zero before the pick-made event arrives. - io.to(`draft-${season.id}`).emit("timer-update", { - seasonId: season.id, - teamId: currentTeamId, - timeRemaining: 0, - currentPickNumber, - overnightPauseActive: false, - }); - } - - const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? (autodraftSettings ?? null) : null); - - if (!success) { - logger.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`); - await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id)); - io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true }); - } - - continue; - } - - // Overnight freeze: skip decrement, notify clients to display the pause indicator. - if (isOvernightFreeze) { - io.to(`draft-${season.id}`).emit("timer-update", { - seasonId: season.id, - teamId: currentTeamId, - timeRemaining: timer.timeRemaining, - currentPickNumber, - overnightPauseActive: true, - resumesAtUTC: overnightPause.resumesAtUTC, - }); - continue; - } - - // Atomically decrement timer (race-condition safe: uses DB-level update - // so concurrent increments from pick handlers are never overwritten) - const [updatedTimer] = await db - .update(schema.draftTimers) - .set({ - timeRemaining: sql`GREATEST(${schema.draftTimers.timeRemaining} - 1, 0)`, - updatedAt: new Date(), - }) - .where(eq(schema.draftTimers.id, timer.id)) - .returning(); - - const newTimeRemaining = updatedTimer?.timeRemaining ?? 0; - - // Emit timer update to all clients in the draft room - io.to(`draft-${season.id}`).emit("timer-update", { - seasonId: season.id, - teamId: currentTeamId, - timeRemaining: newTimeRemaining, - currentPickNumber, - overnightPauseActive: false, - }); - - // If timer just hit 0, trigger auto-pick (next_pick mode or no autodraft) - if (newTimeRemaining === 0) { - const success = await triggerAutoPick(season.id, currentTeamId, currentPickNumber, shouldAutodraft ? (autodraftSettings ?? null) : null); - - if (!success) { - logger.error(`[Timer] Pausing draft ${season.id} — auto-pick failed for team ${currentTeamId} pick ${currentPickNumber}`); - await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, season.id)); - io.to(`draft-${season.id}`).emit("draft-paused", { seasonId: season.id, paused: true }); - } - } +function cancelPickTimeout(seasonId: string): void { + const existing = pickTimeouts.get(seasonId); + if (existing) { + clearTimeout(existing); + pickTimeouts.delete(seasonId); + } +} + +function cancelOvernightResumeTimeout(seasonId: string): void { + const existing = overnightResumeTimeouts.get(seasonId); + if (existing) { + clearTimeout(existing); + overnightResumeTimeouts.delete(seasonId); + } +} + +async function pauseDraftOnError(seasonId: string, teamId: string): Promise { + logger.error(`[Timer] Pausing draft ${seasonId} — pick failed for team ${teamId}`); + await db.update(schema.seasons).set({ draftPaused: true }).where(eq(schema.seasons.id, seasonId)); + try { + getSocketIO().to(`draft-${seasonId}`).emit("draft-paused", { seasonId, paused: true }); + } catch (err) { + logger.error("[Timer] Failed to emit draft-paused:", err); } } -/** - * Trigger an automatic pick when timer expires. - * Returns true if the pick succeeded (or was already made by another path), - * false if a real failure occurred that requires commissioner intervention. - */ async function triggerAutoPick( seasonId: string, teamId: string, @@ -364,10 +157,7 @@ async function triggerAutoPick( }); if (!result.success) { - // A race condition where the pick was already made is not a real failure - if (result.error === "Pick already made") { - return true; - } + if (result.error === "Pick already made") return true; logger.error(`[Timer] Auto-pick failed: ${result.error}`); return false; } @@ -378,3 +168,273 @@ async function triggerAutoPick( return false; } } + +// Core scheduling logic — called after acquiring the schedulingInProgress lock. +async function _schedulePickForSeason(seasonId: string): Promise { + const season = await db.query.seasons.findFirst({ + where: eq(schema.seasons.id, seasonId), + }); + if (!season || season.status !== "draft" || season.draftPaused) return; + + const draftSlots = await getDraftSlotsCached(seasonId); + const totalTeams = draftSlots.length; + if (totalTeams === 0) return; + + const currentPickNumber = season.currentPickNumber ?? 1; + const { pickInRound } = calculatePickInfo(currentPickNumber, totalTeams); + const currentDraftSlot = draftSlots.find((s) => s.draftOrder === pickInRound); + if (!currentDraftSlot) return; + + const currentTeamId = currentDraftSlot.teamId; + + const autodraftSettings = await db.query.autodraftSettings.findFirst({ + where: and( + eq(schema.autodraftSettings.seasonId, seasonId), + eq(schema.autodraftSettings.teamId, currentTeamId) + ), + }); + const shouldAutodraft = autodraftSettings?.isEnabled ?? false; + const isWhileOn = shouldAutodraft && autodraftSettings?.mode === "while_on"; + + if (isWhileOn) { + logger.log(`[Timer] ⚡ while_on autodraft — immediate pick for team ${currentTeamId} in season ${seasonId} (pick ${currentPickNumber})`); + try { + getSocketIO().to(`draft-${seasonId}`).emit("timer-pick-started", { + seasonId, + teamId: currentTeamId, + pickNumber: currentPickNumber, + expiresAt: Date.now(), + timeRemaining: 0, + }); + } catch (_) { /* non-fatal */ } + + const success = await triggerAutoPick(seasonId, currentTeamId, currentPickNumber, autodraftSettings ?? null); + if (!success) { + await pauseDraftOnError(seasonId, currentTeamId); + return; + } + // The chain may have advanced currentPickNumber past all while_on teams — reschedule. + await _schedulePickForSeason(seasonId); + return; + } + + // Overnight pause: freeze timer; schedule a wakeup for when the window ends. + const overnightPause = await checkOvernightPause(season, currentTeamId); + const isOvernightFreeze = overnightPause.active && !shouldAutodraft; + + if (isOvernightFreeze) { + try { + getSocketIO().to(`draft-${seasonId}`).emit("timer-overnight-paused", { + seasonId, + teamId: currentTeamId, + resumesAtUTC: overnightPause.resumesAtUTC, + }); + } catch (_) { /* non-fatal */ } + + if (overnightPause.resumesAtUTC) { + const msUntilResume = Math.max(0, overnightPause.resumesAtUTC - Date.now()); + const resumeTimeout = setTimeout(async () => { + overnightResumeTimeouts.delete(seasonId); + await schedulePickForSeason(seasonId); + }, msUntilResume + 2_000); // 2 s buffer in case clocks drift + overnightResumeTimeouts.set(seasonId, resumeTimeout); + } + return; + } + + // Get or create timer row for this team. + let timer = await db.query.draftTimers.findFirst({ + where: and( + eq(schema.draftTimers.seasonId, seasonId), + eq(schema.draftTimers.teamId, currentTeamId) + ), + }); + + if (!timer) { + const initialTime = + season.draftTimerMode === "standard" + ? (season.draftIncrementTime || 30) + : (season.draftInitialTime || 120); + const startedAt = new Date(); + const expiresAt = new Date(startedAt.getTime() + initialTime * 1000); + await db.insert(schema.draftTimers).values({ + seasonId, + teamId: currentTeamId, + timeRemaining: initialTime, + picksExpiresAt: expiresAt, + picksStartedAt: startedAt, + }); + // Re-fetch to get the full row (including generated id) + timer = await db.query.draftTimers.findFirst({ + where: and( + eq(schema.draftTimers.seasonId, seasonId), + eq(schema.draftTimers.teamId, currentTeamId) + ), + }); + if (!timer) return; // unexpected + } + + const now = Date.now(); + let expiresAt: Date; + + if (timer.picksExpiresAt && timer.picksExpiresAt.getTime() > now) { + // Timer was already running (e.g. process restarted mid-pick). Honour the existing expiry. + expiresAt = timer.picksExpiresAt; + } else { + // Fresh start of this team's turn (normal path after a pick or on startup). + const bank = timer.timeRemaining; + if (bank <= 0) { + // Bank depleted — trigger autopick immediately. + const success = await triggerAutoPick( + seasonId, + currentTeamId, + currentPickNumber, + shouldAutodraft ? (autodraftSettings ?? null) : null + ); + if (!success) { await pauseDraftOnError(seasonId, currentTeamId); return; } + await _schedulePickForSeason(seasonId); + return; + } + expiresAt = new Date(now + bank * 1000); + await db + .update(schema.draftTimers) + .set({ picksExpiresAt: expiresAt, picksStartedAt: new Date() }) + .where( + and( + eq(schema.draftTimers.seasonId, seasonId), + eq(schema.draftTimers.teamId, currentTeamId) + ) + ); + } + + const msUntilExpiry = expiresAt.getTime() - now; + const timeRemaining = Math.max(0, Math.ceil(msUntilExpiry / 1000)); + + // Tell clients to start their local countdown. + try { + getSocketIO().to(`draft-${seasonId}`).emit("timer-pick-started", { + seasonId, + teamId: currentTeamId, + pickNumber: currentPickNumber, + expiresAt: expiresAt.getTime(), + timeRemaining, + }); + } catch (_) { /* non-fatal */ } + + if (msUntilExpiry <= 0) { + // Already expired (e.g. picked up on recovery interval after a long pause). + const success = await triggerAutoPick( + seasonId, + currentTeamId, + currentPickNumber, + shouldAutodraft ? (autodraftSettings ?? null) : null + ); + if (!success) { await pauseDraftOnError(seasonId, currentTeamId); return; } + await _schedulePickForSeason(seasonId); + return; + } + + const timeout = setTimeout(async () => { + pickTimeouts.delete(seasonId); + logger.log( + `[Timer] ⚠️ Timer expired for team ${currentTeamId} in season ${seasonId} (pick ${currentPickNumber})` + ); + const success = await triggerAutoPick( + seasonId, + currentTeamId, + currentPickNumber, + shouldAutodraft ? (autodraftSettings ?? null) : null + ); + if (!success) { + await pauseDraftOnError(seasonId, currentTeamId); + return; + } + await schedulePickForSeason(seasonId); + }, msUntilExpiry); + + pickTimeouts.set(seasonId, timeout); +} + +async function schedulePickForSeason(seasonId: string): Promise { + if (schedulingInProgress.has(seasonId)) return; + schedulingInProgress.add(seasonId); + try { + await _schedulePickForSeason(seasonId); + } catch (err) { + logger.error(`[Timer] schedulePickForSeason error for ${seasonId}:`, err); + } finally { + schedulingInProgress.delete(seasonId); + } +} + +async function scheduleAllActiveDrafts(): Promise { + await checkAndAutoStartDrafts(); + + const activeDrafts = await db.query.seasons.findMany({ + where: eq(schema.seasons.status, "draft"), + }); + + // Evict caches for seasons no longer drafting. + const activeIds = new Set(activeDrafts.map((s) => s.id)); + for (const id of draftSlotsCache.keys()) { + if (!activeIds.has(id)) draftSlotsCache.delete(id); + } + for (const id of teamTimezoneCache.keys()) { + if (!activeIds.has(id)) teamTimezoneCache.delete(id); + } + + for (const season of activeDrafts) { + // Only schedule if not already scheduled (avoids duplicate timeouts). + if (!pickTimeouts.has(season.id) && !overnightResumeTimeouts.has(season.id)) { + await schedulePickForSeason(season.id); + } + } +} + +export function startDraftTimerSystem(): void { + if (recoveryInterval) { + logger.log("[Timer] Timer system already running"); + return; + } + + // Schedule any in-progress drafts immediately on startup. + scheduleAllActiveDrafts().catch((err) => + logger.error("[Timer] Error during startup scheduling:", err) + ); + + // Recovery interval: re-schedule any draft whose in-memory timeout was lost + // (process restart, overnight pause end, etc.). Runs every 30 s, not every 1 s. + recoveryInterval = setInterval(() => { + scheduleAllActiveDrafts().catch((err) => + logger.error("[Timer] Error in recovery interval:", err) + ); + }, 30_000); + + logger.log("[Timer] Draft timer system started (event-driven)"); +} + +export function stopDraftTimerSystem(): void { + if (recoveryInterval) { + clearInterval(recoveryInterval); + recoveryInterval = null; + } + for (const t of pickTimeouts.values()) clearTimeout(t); + pickTimeouts.clear(); + for (const t of overnightResumeTimeouts.values()) clearTimeout(t); + overnightResumeTimeouts.clear(); + schedulingInProgress.clear(); + draftSlotsCache.clear(); + teamTimezoneCache.clear(); + logger.log("[Timer] Draft timer system stopped"); +} + +/** + * Called by pick routes after a pick is made so the timer immediately + * reschedules for the next team rather than waiting for the recovery interval. + */ +export async function rescheduleTimer(seasonId: string): Promise { + cancelPickTimeout(seasonId); + cancelOvernightResumeTimeout(seasonId); + draftSlotsCache.delete(seasonId); // pick may have changed the draft order state + await schedulePickForSeason(seasonId); +}