Canonical tournament layer: schema + backfill (1/2) (#365)
* refactor(schema): rename per-window tables to season_* prefix Renames participants, participant_expected_values, participant_qualifying_totals, participant_results, participant_surface_elos to season_* prefixed names. Renames event_results.participant_id to season_participant_id. Phase 1a of canonical tournament layer migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor: rename participant.ts model file to season-participant.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(models): update model layer to use renamed schema exports Updated all model files to use the renamed schema exports from Task 1: - participants → seasonParticipants - participantExpectedValues → seasonParticipantExpectedValues - participantQualifyingTotals → seasonParticipantQualifyingTotals - participantResults → seasonParticipantResults - participantSurfaceElos → seasonParticipantSurfaceElos - eventResults.participantId → eventResults.seasonParticipantId - db.query relation accessors updated - Relation field .participant → .seasonParticipant where applicable - Import paths updated: ./participant → ./season-participant Files updated (14 model files + 3 test files): - draft-pick.ts - draft-utils.ts - event-result.ts - group-stage-match.ts - participant-result.ts - qualifying-points.ts - scoring-calculator.ts - scoring-event.ts - sports-season.ts - surface-elo.ts - team-score-events.ts - cs2-major-stage.ts - golf-skills.ts - participant-expected-value.ts - __tests__/sports-season.clone.test.ts - __tests__/auto-pick.test.ts - __tests__/executeAutoPick.timer.test.ts Typecheck errors decreased: 779 → 499 (280 fewer) All model file errors related to renamed schemas resolved. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(routes): update route layer to use renamed schema exports - Update model import from ~/models/participant to ~/models/season-participant - Rename schema.participants to schema.seasonParticipants - Rename schema.participantResults to schema.seasonParticipantResults - Rename db.query.participants to db.query.seasonParticipants - Update 9 route files and 1 test file Affected files: - admin.sports-seasons.$id.events.$eventId.bracket.server.ts - admin.sports-seasons.$id.participants.tsx - api/draft.force-manual-pick.ts - api/draft.make-pick.ts - api/draft.replace-pick.ts - api/seasons.$seasonId.draft.ts - leagues/$leagueId.draft-board.$seasonId.tsx - leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts - admin/__tests__/sports-seasons-participants.test.ts Error count reduced from 499 to 453 (46 errors fixed). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(routes): update route files for schema rename Update route imports from ~/models/participant to ~/models/season-participant and fix references to .participant/.participantId on event results to use .seasonParticipant/.seasonParticipantId after schema rename. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(services): update simulators and services for renamed schema Update all simulators, services, and server files to use renamed schema tables: - participants → seasonParticipants - participantExpectedValues → seasonParticipantExpectedValues - participantResults → seasonParticipantResults - eventResults.participantId → eventResults.seasonParticipantId Files updated: - 20 sport simulators (NBA, NHL, NFL, MLB, etc.) - probability-updater.ts - standings-sync/index.ts - sports-data-sync.server.ts - server/socket.ts Typecheck errors reduced from 365 to 0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * migration: rename per-window tables to season_* prefix * fix(tests): update mock query keys after participants table rename Change mock db.query.participants to db.query.seasonParticipants in test files to match the schema rename from commit66145a9. This fixes "Cannot read properties of undefined (reading 'findFirst'/'findMany')" errors that occurred when production code queries db.query.seasonParticipants but test mocks only defined the old participants key. Files updated: - app/services/simulations/__tests__/world-cup-simulator.test.ts - app/routes/api/__tests__/draft.force-manual-pick.test.ts - app/routes/api/__tests__/draft.force-manual-pick.timer-mode.test.ts - app/routes/api/__tests__/draft.make-pick.timer-mode.test.ts - server/__tests__/timer-autodraft.test.ts - app/models/__tests__/team-score-events.test.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(tests): update remaining mock paths and keys after schema rename * fix(tests): final two mock stragglers after schema rename - draft-pick.test.ts: assertion on db.query.participantQualifyingTotals - process-match-result.test.ts: mock key participants → seasonParticipants Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: add post-phase1a baseline capture (temp, for diff verification) * chore: capture pre-migration baselines * chore: remove post-phase1a capture helper after verification * schema: add canonical tournament & participant tables Adds tournaments, participants (canonical), tournament_results, and participant_surface_elos (canonical). Adds nullable tournament_id to scoring_events and nullable participant_id to season_participants. Phase 1b of canonical tournament layer migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(models): add canonical tournament, participant, result, surface-elo models Adds CRUD modules for the canonical tables created in commit775b905. Each module mirrors existing app/models conventions (database() from ~/database/context, schema from ~/database/schema, mock-based tests). Key implementation notes: - participant.ts exports use "Canonical" prefix (CanonicalParticipant, createCanonicalParticipant, etc.) to avoid collision with existing season-participant.ts exports - All four models include comprehensive unit tests following the audit-log.test.ts pattern - Tests use mocked db responses (no real database access) - Upsert functions use onConflictDoUpdate for appropriate unique constraints Part of Phase 1b of canonical tournament layer migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * migration: create canonical tables, add nullable FKs * scripts: add extractTournamentIdentity helper for backfill Pure function that derives canonical (name, year) identity from a scoring_events row, stripping trailing 4-digit years from the name or falling back to eventDate. Used by the Phase 2 backfill to group per-window events into canonical tournaments. * scripts: add backfill orchestrator for canonical layer Populates canonical tournaments, participants, tournament_results, and participant_surface_elos from per-window data for qualifying-points sports. Skips already-linked rows, is idempotent, and supports dry-run mode. Critical invariants enforced by the implementation: - qualifying_points_awarded is never copied to tournament_results - season_participant_qualifying_totals is never touched - conflicting surface-Elo values between windows raise a loud error (recorded in report.errors) rather than overwriting * scripts: add backfill CLI with dry-run default Wires backfill-canonical-layer.ts to a CLI entry point exposed as `npm run backfill:canonical`. Defaults to --dry-run; requires --apply to actually write. Supports --sport=<uuid> to limit to a single sport. Exits 2 if the backfill reports errors (e.g., surface-Elo conflicts). * fix(backfill-cli): wrap runBackfill in DatabaseContext.run The orchestrator uses database() from ~/database/context, which requires AsyncLocalStorage to be populated. Wrap the CLI invocation with DatabaseContext.run(db, ...) using server/db's cached connection pool. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(backfill-cli): exit 0 on success so pg pool doesn't block The cached postgres connection pool keeps the Node event loop open after main() returns. Explicit process.exit(0) on success mirrors the pattern in scripts/capture-baseline.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Chris Parsons <chrisp@extrahop.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a068dcc6e7
commit
2848231235
94 changed files with 13936 additions and 734 deletions
|
|
@ -8,7 +8,7 @@ vi.mock("~/models/draft-pick", () => ({
|
|||
isParticipantDrafted: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("~/models/participant", () => ({
|
||||
vi.mock("~/models/season-participant", () => ({
|
||||
getParticipantsForSeasonWithSports: vi.fn(),
|
||||
}));
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ import {
|
|||
getTeamDraftPicksWithSports,
|
||||
isParticipantDrafted,
|
||||
} from "~/models/draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/season-participant";
|
||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||
import { getTeamQueue, getAllQueuesForSeason } from "~/models/draft-queue";
|
||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||
|
|
@ -49,7 +49,7 @@ const ALL_TEAM_IDS = [TEAM_ID, "team-2"];
|
|||
function makeMockDb(overrides: Record<string, unknown> = {}) {
|
||||
const mockDb: Record<string, unknown> = {
|
||||
query: {
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
|
|
@ -71,7 +71,7 @@ function makeMockDb(overrides: Record<string, unknown> = {}) {
|
|||
// 3. select().from().where().orderBy() → top participant → [{id, ...}]
|
||||
function makeMockDbWithEvParticipant(participantId: string) {
|
||||
return {
|
||||
query: { participants: { findMany: vi.fn().mockResolvedValue([]) } },
|
||||
query: { seasonParticipants: { findMany: vi.fn().mockResolvedValue([]) } },
|
||||
select: vi.fn().mockReturnThis(),
|
||||
from: vi.fn().mockReturnThis(),
|
||||
innerJoin: vi.fn().mockReturnThis(),
|
||||
|
|
@ -148,7 +148,7 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
|
||||
const mockDb = makeMockDb({
|
||||
query: {
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-1",
|
||||
|
|
@ -188,7 +188,7 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
||||
const mockDb = makeMockDb({
|
||||
query: {
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-1",
|
||||
|
|
@ -233,7 +233,7 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
const mockWhere = vi.fn().mockResolvedValue(undefined);
|
||||
const mockDb = makeMockDb({
|
||||
query: {
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-snooker",
|
||||
|
|
@ -280,7 +280,7 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
// 4. select().from(participants).where().orderBy() — participant query (chain)
|
||||
const mockDb = {
|
||||
query: {
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-snooker",
|
||||
|
|
@ -330,7 +330,7 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
|
||||
const mockDb = makeMockDb({
|
||||
query: {
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-1",
|
||||
|
|
@ -373,7 +373,7 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => {
|
|||
|
||||
const mockDb = makeMockDb({
|
||||
query: {
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "p-1",
|
||||
|
|
|
|||
100
app/models/__tests__/canonical-surface-elo.test.ts
Normal file
100
app/models/__tests__/canonical-surface-elo.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
upsertCanonicalSurfaceElo,
|
||||
getCanonicalSurfaceElo,
|
||||
} from "../canonical-surface-elo";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const PARTICIPANT_ID = "participant-1";
|
||||
|
||||
const SAMPLE_ELO = {
|
||||
id: "elo-1",
|
||||
participantId: PARTICIPANT_ID,
|
||||
worldRanking: 1,
|
||||
eloHard: 2400,
|
||||
eloClay: 2350,
|
||||
eloGrass: 2300,
|
||||
updatedAt: new Date("2026-01-28T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeUpsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("upsertCanonicalSurfaceElo", () => {
|
||||
it("inserts or updates surface elo data", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeUpsertDb(SAMPLE_ELO) as never);
|
||||
|
||||
const result = await upsertCanonicalSurfaceElo({
|
||||
participantId: PARTICIPANT_ID,
|
||||
worldRanking: 1,
|
||||
eloHard: 2400,
|
||||
eloClay: 2350,
|
||||
eloGrass: 2300,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_ELO);
|
||||
});
|
||||
|
||||
it("uses onConflictDoUpdate on participantId", async () => {
|
||||
const mockDb = makeUpsertDb(SAMPLE_ELO);
|
||||
vi.mocked(database).mockReturnValue(mockDb as never);
|
||||
|
||||
await upsertCanonicalSurfaceElo({
|
||||
participantId: PARTICIPANT_ID,
|
||||
worldRanking: 1,
|
||||
});
|
||||
|
||||
const onConflictCall = (mockDb.insert as ReturnType<typeof vi.fn>)
|
||||
.mock.results[0].value.values.mock.results[0].value.onConflictDoUpdate;
|
||||
|
||||
expect(onConflictCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCanonicalSurfaceElo", () => {
|
||||
it("returns elo data when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_ELO]) as never);
|
||||
|
||||
const result = await getCanonicalSurfaceElo(PARTICIPANT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_ELO);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getCanonicalSurfaceElo("nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -45,7 +45,7 @@ function makeDb(opts: MakeDbOpts = {}) {
|
|||
scoringEvents: {
|
||||
findMany: vi.fn().mockResolvedValue(scoringEvents),
|
||||
},
|
||||
participantQualifyingTotals: {
|
||||
seasonParticipantQualifyingTotals: {
|
||||
findMany: vi.fn().mockResolvedValue(qualifyingTotals),
|
||||
},
|
||||
},
|
||||
|
|
@ -220,6 +220,6 @@ describe("getDraftedParticipantsWithPoints", () => {
|
|||
|
||||
await getDraftedParticipantsWithPoints("team-1", "season-1", db);
|
||||
|
||||
expect(db.query.participantQualifyingTotals.findMany).not.toHaveBeenCalled();
|
||||
expect(db.query.seasonParticipantQualifyingTotals.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ vi.mock("~/models/draft-pick", () => ({
|
|||
isParticipantDrafted: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("~/models/participant", () => ({
|
||||
vi.mock("~/models/season-participant", () => ({
|
||||
getParticipantsForSeasonWithSports: vi.fn(),
|
||||
}));
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ vi.mock("~/lib/draft-eligibility", () => ({
|
|||
vi.mock("~/database/context");
|
||||
|
||||
import { getDraftPicksWithSports, getTeamDraftPicksWithSports, isParticipantDrafted } from "~/models/draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/season-participant";
|
||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||
import { getTeamQueue, getAllQueuesForSeason } from "~/models/draft-queue";
|
||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||
|
|
@ -127,7 +127,7 @@ function makeMockDb(seasonOverrides: Record<string, unknown> = {}) {
|
|||
draftPicks: { findFirst: vi.fn().mockResolvedValue(null) },
|
||||
seasons: { findFirst: vi.fn().mockResolvedValue(makeSeason(seasonOverrides)) },
|
||||
draftSlots: { findMany: vi.fn().mockResolvedValue(mockDraftSlots) },
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
// findMany: used by autoPickForTeam queue path
|
||||
findMany: vi.fn().mockResolvedValue([mockParticipantForQueue]),
|
||||
// findFirst: used by executeAutoPick to fetch full participant details
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ vi.mock("~/database/context", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("~/database/schema", () => ({
|
||||
participants: { id: "id" },
|
||||
participantExpectedValues: {
|
||||
seasonParticipants: { id: "id" },
|
||||
seasonParticipantExpectedValues: {
|
||||
participantId: "participantId",
|
||||
sportsSeasonId: "sportsSeasonId",
|
||||
},
|
||||
|
|
|
|||
152
app/models/__tests__/participant.test.ts
Normal file
152
app/models/__tests__/participant.test.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
createCanonicalParticipant,
|
||||
getCanonicalParticipantById,
|
||||
findCanonicalParticipantsBySport,
|
||||
findCanonicalParticipantBySportName,
|
||||
upsertCanonicalParticipantBySportName,
|
||||
} from "../participant";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const SPORT_ID = "sport-1";
|
||||
const PARTICIPANT_ID = "participant-1";
|
||||
|
||||
const SAMPLE_PARTICIPANT = {
|
||||
id: PARTICIPANT_ID,
|
||||
sportId: SPORT_ID,
|
||||
name: "Novak Djokovic",
|
||||
externalKey: "djokovic-n",
|
||||
metadata: { atp_id: "12345" },
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeInsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
orderBy: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createCanonicalParticipant", () => {
|
||||
it("inserts a participant and returns it", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeInsertDb(SAMPLE_PARTICIPANT) as never);
|
||||
|
||||
const result = await createCanonicalParticipant({
|
||||
sportId: SPORT_ID,
|
||||
name: "Novak Djokovic",
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCanonicalParticipantById", () => {
|
||||
it("returns the participant when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_PARTICIPANT]) as never);
|
||||
|
||||
const result = await getCanonicalParticipantById(PARTICIPANT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getCanonicalParticipantById("nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCanonicalParticipantsBySport", () => {
|
||||
it("returns participants ordered by name", async () => {
|
||||
const participants = [SAMPLE_PARTICIPANT];
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb(participants) as never);
|
||||
|
||||
const result = await findCanonicalParticipantsBySport(SPORT_ID);
|
||||
|
||||
expect(result).toEqual(participants);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCanonicalParticipantBySportName", () => {
|
||||
it("returns the participant when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_PARTICIPANT]) as never);
|
||||
|
||||
const result = await findCanonicalParticipantBySportName(SPORT_ID, "Novak Djokovic");
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await findCanonicalParticipantBySportName(SPORT_ID, "Roger Federer");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("upsertCanonicalParticipantBySportName", () => {
|
||||
it("returns existing participant if found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_PARTICIPANT]) as never);
|
||||
|
||||
const result = await upsertCanonicalParticipantBySportName(SPORT_ID, "Novak Djokovic");
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("creates new participant if not found", async () => {
|
||||
let callCount = 0;
|
||||
vi.mocked(database).mockImplementation(() => {
|
||||
callCount++;
|
||||
return (callCount === 1 ? makeSelectDb([]) : makeInsertDb(SAMPLE_PARTICIPANT)) as never;
|
||||
});
|
||||
|
||||
const result = await upsertCanonicalParticipantBySportName(SPORT_ID, "Novak Djokovic");
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("passes extra fields when creating", async () => {
|
||||
let callCount = 0;
|
||||
vi.mocked(database).mockImplementation(() => {
|
||||
callCount++;
|
||||
return (callCount === 1 ? makeSelectDb([]) : makeInsertDb(SAMPLE_PARTICIPANT)) as never;
|
||||
});
|
||||
|
||||
const result = await upsertCanonicalParticipantBySportName(
|
||||
SPORT_ID,
|
||||
"Novak Djokovic",
|
||||
{ externalKey: "djokovic-n" }
|
||||
);
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
});
|
||||
|
|
@ -42,7 +42,7 @@ function makeDb(existingResult?: { id: string; isPartialScore: boolean }) {
|
|||
insert: vi.fn().mockReturnValue({ values: insertValues }),
|
||||
update: vi.fn().mockReturnValue({ set: updateSet }),
|
||||
query: {
|
||||
participantResults: { findFirst },
|
||||
seasonParticipantResults: { findFirst },
|
||||
seasonSports: {
|
||||
findMany: vi.fn().mockResolvedValue([]), // empty → standings loop exits immediately
|
||||
},
|
||||
|
|
@ -430,10 +430,10 @@ describe("processPlayoffEvent - NBA Play-In Round 1 loserAdvances fix", () => {
|
|||
playoffMatches: {
|
||||
findMany: vi.fn().mockResolvedValue(matches),
|
||||
},
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
participantResults: {
|
||||
seasonParticipantResults: {
|
||||
findFirst: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
seasons: {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ vi.mock("~/database/context", () => ({
|
|||
|
||||
vi.mock("~/database/schema", () => ({
|
||||
sportsSeasons: { id: "ss.id", sportId: "ss.sport_id" },
|
||||
participants: { sportsSeasonId: "p.sports_season_id" },
|
||||
seasonParticipants: { sportsSeasonId: "p.sports_season_id" },
|
||||
scoringEvents: { sportsSeasonId: "se.sports_season_id" },
|
||||
participantExpectedValues: { sportsSeasonId: "pev.sports_season_id" },
|
||||
seasonParticipantExpectedValues: { sportsSeasonId: "pev.sports_season_id" },
|
||||
}));
|
||||
|
||||
vi.mock("drizzle-orm", () => ({
|
||||
|
|
@ -91,16 +91,16 @@ function makeMockDb({
|
|||
const db: any = {
|
||||
query: {
|
||||
sportsSeasons: { findFirst: vi.fn().mockResolvedValue(ss) },
|
||||
participants: { findMany: vi.fn().mockResolvedValue(participants) },
|
||||
seasonParticipants: { findMany: vi.fn().mockResolvedValue(participants) },
|
||||
scoringEvents: { findMany: vi.fn().mockResolvedValue(scoringEvents) },
|
||||
participantExpectedValues: { findMany: vi.fn().mockResolvedValue(sourceEvRows) },
|
||||
seasonParticipantExpectedValues: { findMany: vi.fn().mockResolvedValue(sourceEvRows) },
|
||||
},
|
||||
insert: vi.fn().mockImplementation((table: object) => {
|
||||
let key: string;
|
||||
let returnRows: object[];
|
||||
if (table === schema.sportsSeasons) {
|
||||
key = "sportsSeasons"; returnRows = [insertedSeason];
|
||||
} else if (table === schema.participants) {
|
||||
} else if (table === schema.seasonParticipants) {
|
||||
key = "participants"; returnRows = newParticipantRows;
|
||||
} else if (table === schema.scoringEvents) {
|
||||
key = "scoringEvents"; returnRows = [];
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function makeDb(opts: MakeDbOpts = {}) {
|
|||
teamScoreEvents: {
|
||||
findMany: vi.fn().mockResolvedValue(scoreEventRows),
|
||||
},
|
||||
participants: {
|
||||
seasonParticipants: {
|
||||
findMany: vi.fn().mockResolvedValue(participantRows),
|
||||
},
|
||||
},
|
||||
|
|
@ -268,7 +268,7 @@ describe("getRecentTeamScoreEvents", () => {
|
|||
it("does not query participants when rows array is empty", async () => {
|
||||
const { db } = makeDb({ scoreEventRows: [] });
|
||||
await getRecentTeamScoreEvents("season-1", 10, db);
|
||||
expect(db.query.participants.findMany).not.toHaveBeenCalled();
|
||||
expect(db.query.seasonParticipants.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns mapped entries with resolved participant names", async () => {
|
||||
|
|
|
|||
114
app/models/__tests__/tournament-result.test.ts
Normal file
114
app/models/__tests__/tournament-result.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
upsertTournamentResult,
|
||||
getTournamentResults,
|
||||
getTournamentResultByParticipant,
|
||||
} from "../tournament-result";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const TOURNAMENT_ID = "tournament-1";
|
||||
const PARTICIPANT_ID = "participant-1";
|
||||
|
||||
const SAMPLE_RESULT = {
|
||||
id: "result-1",
|
||||
tournamentId: TOURNAMENT_ID,
|
||||
participantId: PARTICIPANT_ID,
|
||||
placement: 1,
|
||||
rawScore: "7500.00",
|
||||
createdAt: new Date("2026-01-28T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-28T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeUpsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
orderBy: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("upsertTournamentResult", () => {
|
||||
it("inserts or updates a result", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeUpsertDb(SAMPLE_RESULT) as never);
|
||||
|
||||
const result = await upsertTournamentResult({
|
||||
tournamentId: TOURNAMENT_ID,
|
||||
participantId: PARTICIPANT_ID,
|
||||
placement: 1,
|
||||
rawScore: "7500.00",
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_RESULT);
|
||||
});
|
||||
|
||||
it("uses onConflictDoUpdate on tournamentId and participantId", async () => {
|
||||
const mockDb = makeUpsertDb(SAMPLE_RESULT);
|
||||
vi.mocked(database).mockReturnValue(mockDb as never);
|
||||
|
||||
await upsertTournamentResult({
|
||||
tournamentId: TOURNAMENT_ID,
|
||||
participantId: PARTICIPANT_ID,
|
||||
placement: 1,
|
||||
});
|
||||
|
||||
const onConflictCall = (mockDb.insert as ReturnType<typeof vi.fn>)
|
||||
.mock.results[0].value.values.mock.results[0].value.onConflictDoUpdate;
|
||||
|
||||
expect(onConflictCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTournamentResults", () => {
|
||||
it("returns results ordered by placement", async () => {
|
||||
const results = [SAMPLE_RESULT];
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb(results) as never);
|
||||
|
||||
const result = await getTournamentResults(TOURNAMENT_ID);
|
||||
|
||||
expect(result).toEqual(results);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTournamentResultByParticipant", () => {
|
||||
it("returns the result when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_RESULT]) as never);
|
||||
|
||||
const result = await getTournamentResultByParticipant(TOURNAMENT_ID, PARTICIPANT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_RESULT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getTournamentResultByParticipant(TOURNAMENT_ID, "nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
174
app/models/__tests__/tournament.test.ts
Normal file
174
app/models/__tests__/tournament.test.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
createTournament,
|
||||
getTournamentById,
|
||||
findTournamentsBySport,
|
||||
findTournamentBySportNameYear,
|
||||
upsertTournament,
|
||||
updateTournamentStatus,
|
||||
} from "../tournament";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const SPORT_ID = "sport-1";
|
||||
const TOURNAMENT_ID = "tournament-1";
|
||||
|
||||
const SAMPLE_TOURNAMENT = {
|
||||
id: TOURNAMENT_ID,
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
startsAt: new Date("2026-01-15T00:00:00Z"),
|
||||
endsAt: new Date("2026-01-28T00:00:00Z"),
|
||||
surface: "hard",
|
||||
location: "Melbourne",
|
||||
status: "scheduled" as const,
|
||||
externalKey: "ao-2026",
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeInsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
orderBy: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeUpdateDb(returnValue: object) {
|
||||
return {
|
||||
update: vi.fn().mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createTournament", () => {
|
||||
it("inserts a tournament and returns it", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeInsertDb(SAMPLE_TOURNAMENT) as never);
|
||||
|
||||
const result = await createTournament({
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTournamentById", () => {
|
||||
it("returns the tournament when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
||||
|
||||
const result = await getTournamentById(TOURNAMENT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getTournamentById("nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findTournamentsBySport", () => {
|
||||
it("returns tournaments ordered by year and date", async () => {
|
||||
const tournaments = [SAMPLE_TOURNAMENT];
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb(tournaments) as never);
|
||||
|
||||
const result = await findTournamentsBySport(SPORT_ID);
|
||||
|
||||
expect(result).toEqual(tournaments);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findTournamentBySportNameYear", () => {
|
||||
it("returns the tournament when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
||||
|
||||
const result = await findTournamentBySportNameYear(SPORT_ID, "Australian Open", 2026);
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await findTournamentBySportNameYear(SPORT_ID, "Wimbledon", 2026);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("upsertTournament", () => {
|
||||
it("returns existing tournament if found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
||||
|
||||
const result = await upsertTournament({
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
|
||||
it("creates new tournament if not found", async () => {
|
||||
let callCount = 0;
|
||||
vi.mocked(database).mockImplementation(() => {
|
||||
callCount++;
|
||||
return (callCount === 1 ? makeSelectDb([]) : makeInsertDb(SAMPLE_TOURNAMENT)) as never;
|
||||
});
|
||||
|
||||
const result = await upsertTournament({
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateTournamentStatus", () => {
|
||||
it("updates status and returns the tournament", async () => {
|
||||
const updated = { ...SAMPLE_TOURNAMENT, status: "in_progress" as const };
|
||||
vi.mocked(database).mockReturnValue(makeUpdateDb(updated) as never);
|
||||
|
||||
const result = await updateTournamentStatus(TOURNAMENT_ID, "in_progress");
|
||||
|
||||
expect(result.status).toBe("in_progress");
|
||||
});
|
||||
});
|
||||
43
app/models/canonical-surface-elo.ts
Normal file
43
app/models/canonical-surface-elo.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type CanonicalSurfaceElo = typeof schema.participantSurfaceElos.$inferSelect;
|
||||
export type NewCanonicalSurfaceElo = typeof schema.participantSurfaceElos.$inferInsert;
|
||||
|
||||
export async function upsertCanonicalSurfaceElo(
|
||||
data: NewCanonicalSurfaceElo
|
||||
): Promise<CanonicalSurfaceElo> {
|
||||
if (!data.participantId) {
|
||||
throw new Error("participantId is required");
|
||||
}
|
||||
|
||||
const db = database();
|
||||
const [result] = await db
|
||||
.insert(schema.participantSurfaceElos)
|
||||
.values(data)
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.participantSurfaceElos.participantId],
|
||||
set: {
|
||||
worldRanking: data.worldRanking,
|
||||
eloHard: data.eloHard,
|
||||
eloClay: data.eloClay,
|
||||
eloGrass: data.eloGrass,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getCanonicalSurfaceElo(
|
||||
participantId: string
|
||||
): Promise<CanonicalSurfaceElo | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.participantSurfaceElos)
|
||||
.where(eq(schema.participantSurfaceElos.participantId, participantId))
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
*/
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import { cs2MajorStageResults, participants } from "~/database/schema";
|
||||
import { cs2MajorStageResults, seasonParticipants } from "~/database/schema";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
|
||||
export interface Cs2StageResult {
|
||||
|
|
@ -56,12 +56,12 @@ export async function getCs2StageResultsForEvent(
|
|||
finalPlacement: cs2MajorStageResults.finalPlacement,
|
||||
createdAt: cs2MajorStageResults.createdAt,
|
||||
updatedAt: cs2MajorStageResults.updatedAt,
|
||||
participantName: participants.name,
|
||||
participantName: seasonParticipants.name,
|
||||
})
|
||||
.from(cs2MajorStageResults)
|
||||
.innerJoin(participants, eq(cs2MajorStageResults.participantId, participants.id))
|
||||
.innerJoin(seasonParticipants, eq(cs2MajorStageResults.participantId, seasonParticipants.id))
|
||||
.where(eq(cs2MajorStageResults.scoringEventId, scoringEventId))
|
||||
.orderBy(cs2MajorStageResults.stageEntry, participants.name);
|
||||
.orderBy(cs2MajorStageResults.stageEntry, seasonParticipants.name);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ export async function clearCs2StageAssignments(
|
|||
|
||||
/**
|
||||
* Mark teams as eliminated from a specific stage.
|
||||
* Records stageEliminated and stageEliminatedWins for the specified participants.
|
||||
* Records stageEliminated and stageEliminatedWins for the specified seasonParticipants.
|
||||
* Teams NOT in eliminatedEntries that are in this stage are implicitly considered
|
||||
* to have advanced (stageEliminated remains null).
|
||||
*/
|
||||
|
|
@ -166,7 +166,7 @@ export async function markCs2StageEliminations(
|
|||
}
|
||||
|
||||
/**
|
||||
* Set final placements for all participants in a CS2 Major event.
|
||||
* Set final placements for all seasonParticipants in a CS2 Major event.
|
||||
* Called after the Champions Stage is complete.
|
||||
* placements: Map from participantId to final placement (1–32)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -78,14 +78,14 @@ export async function getDraftedParticipantsBySportsSeason(
|
|||
|
||||
const results = await db
|
||||
.select({
|
||||
sportsSeasonId: schema.participants.sportsSeasonId,
|
||||
participantId: schema.participants.id,
|
||||
participantName: schema.participants.name,
|
||||
sportsSeasonId: schema.seasonParticipants.sportsSeasonId,
|
||||
participantId: schema.seasonParticipants.id,
|
||||
participantName: schema.seasonParticipants.name,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(
|
||||
schema.participants,
|
||||
eq(schema.draftPicks.participantId, schema.participants.id)
|
||||
schema.seasonParticipants,
|
||||
eq(schema.draftPicks.participantId, schema.seasonParticipants.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
|
|
@ -182,10 +182,10 @@ export async function getDraftedParticipantsWithPoints(
|
|||
// Batch-fetch QP totals for qualifying_points participants
|
||||
const qpMap = new Map<string, number>(); // participantId → totalQP
|
||||
if (qpParticipantIds.size > 0 && qpSeasonIds.size > 0) {
|
||||
const totals = await db.query.participantQualifyingTotals.findMany({
|
||||
const totals = await db.query.seasonParticipantQualifyingTotals.findMany({
|
||||
where: and(
|
||||
inArray(schema.participantQualifyingTotals.participantId, [...qpParticipantIds]),
|
||||
inArray(schema.participantQualifyingTotals.sportsSeasonId, [...qpSeasonIds])
|
||||
inArray(schema.seasonParticipantQualifyingTotals.participantId, [...qpParticipantIds]),
|
||||
inArray(schema.seasonParticipantQualifyingTotals.sportsSeasonId, [...qpSeasonIds])
|
||||
),
|
||||
columns: { participantId: true, totalQualifyingPoints: true },
|
||||
});
|
||||
|
|
@ -245,19 +245,19 @@ export async function getDraftPicksWithSports(seasonId: string, providedDb?: Ret
|
|||
id: schema.draftPicks.id,
|
||||
teamId: schema.draftPicks.teamId,
|
||||
pickNumber: schema.draftPicks.pickNumber,
|
||||
participantId: schema.participants.id,
|
||||
participantName: schema.participants.name,
|
||||
participantId: schema.seasonParticipants.id,
|
||||
participantName: schema.seasonParticipants.name,
|
||||
sportId: schema.sports.id,
|
||||
sportName: schema.sports.name,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(
|
||||
schema.participants,
|
||||
eq(schema.draftPicks.participantId, schema.participants.id)
|
||||
schema.seasonParticipants,
|
||||
eq(schema.draftPicks.participantId, schema.seasonParticipants.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sportsSeasons,
|
||||
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sports,
|
||||
|
|
@ -289,19 +289,19 @@ export async function getTeamDraftPicksWithSports(teamId: string, seasonId: stri
|
|||
id: schema.draftPicks.id,
|
||||
teamId: schema.draftPicks.teamId,
|
||||
pickNumber: schema.draftPicks.pickNumber,
|
||||
participantId: schema.participants.id,
|
||||
participantName: schema.participants.name,
|
||||
participantId: schema.seasonParticipants.id,
|
||||
participantName: schema.seasonParticipants.name,
|
||||
sportId: schema.sports.id,
|
||||
sportName: schema.sports.name,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(
|
||||
schema.participants,
|
||||
eq(schema.draftPicks.participantId, schema.participants.id)
|
||||
schema.seasonParticipants,
|
||||
eq(schema.draftPicks.participantId, schema.seasonParticipants.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sportsSeasons,
|
||||
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sports,
|
||||
|
|
@ -338,13 +338,13 @@ export async function getDraftPicksForSeason(seasonId: string) {
|
|||
pickInRound: schema.draftPicks.pickInRound,
|
||||
timeUsed: schema.draftPicks.timeUsed,
|
||||
team: schema.teams,
|
||||
participant: schema.participants,
|
||||
participant: schema.seasonParticipants,
|
||||
sport: schema.sports,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
|
||||
.innerJoin(schema.participants, eq(schema.draftPicks.participantId, schema.participants.id))
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.seasonParticipants, eq(schema.draftPicks.participantId, schema.seasonParticipants.id))
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(eq(schema.draftPicks.seasonId, seasonId))
|
||||
.orderBy(asc(schema.draftPicks.pickNumber));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { logger } from "~/lib/logger";
|
|||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { getTeamQueue, getAllQueuesForSeason } from "./draft-queue";
|
||||
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "./participant";
|
||||
import { getParticipantsForSeasonWithSports } from "./season-participant";
|
||||
import { getSeasonSportsSimple } from "./season-sport";
|
||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||
import { getSocketIO, scheduleDraftRoomClosure } from "../../server/socket";
|
||||
|
|
@ -118,8 +118,8 @@ export async function autoPickForTeam(
|
|||
|
||||
// Get participant details for queue items to check eligibility
|
||||
const queueParticipantIds = queue.map((item) => item.participantId);
|
||||
const queueParticipants = await db.query.participants.findMany({
|
||||
where: inArray(schema.participants.id, queueParticipantIds),
|
||||
const queueParticipants = await db.query.seasonParticipants.findMany({
|
||||
where: inArray(schema.seasonParticipants.id, queueParticipantIds),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
|
|
@ -264,17 +264,17 @@ export async function getTopAvailableParticipant(
|
|||
for (const sportsSeasonId of sportsSeasonIds) {
|
||||
let participantQuery = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (draftedIds.length > 0) {
|
||||
participantQuery = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.from(schema.seasonParticipants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
||||
notInArray(schema.participants.id, draftedIds)
|
||||
eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
|
||||
notInArray(schema.seasonParticipants.id, draftedIds)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -299,21 +299,21 @@ export async function getTopAvailableParticipant(
|
|||
// Single sport season
|
||||
let query = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]))
|
||||
.orderBy(desc(schema.participants.vorpValue), schema.participants.name);
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0]))
|
||||
.orderBy(desc(schema.seasonParticipants.vorpValue), schema.seasonParticipants.name);
|
||||
|
||||
if (draftedIds.length > 0) {
|
||||
query = db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.from(schema.seasonParticipants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]),
|
||||
notInArray(schema.participants.id, draftedIds)
|
||||
eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0]),
|
||||
notInArray(schema.seasonParticipants.id, draftedIds)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(schema.participants.vorpValue), schema.participants.name);
|
||||
.orderBy(desc(schema.seasonParticipants.vorpValue), schema.seasonParticipants.name);
|
||||
}
|
||||
|
||||
const [topParticipant] = await query;
|
||||
|
|
@ -471,7 +471,7 @@ export async function executeAutoPick(params: {
|
|||
success: boolean;
|
||||
error?: string;
|
||||
pick?: InferSelectModel<typeof schema.draftPicks>;
|
||||
participant?: InferSelectModel<typeof schema.participants> & {
|
||||
participant?: InferSelectModel<typeof schema.seasonParticipants> & {
|
||||
sportsSeason: InferSelectModel<typeof schema.sportsSeasons> & {
|
||||
sport: InferSelectModel<typeof schema.sports>;
|
||||
};
|
||||
|
|
@ -581,8 +581,8 @@ export async function executeAutoPick(params: {
|
|||
}
|
||||
|
||||
// Get participant details
|
||||
const participantToPick = await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, participantId),
|
||||
const participantToPick = await db.query.seasonParticipants.findFirst({
|
||||
where: eq(schema.seasonParticipants.id, participantId),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export async function createEventResult(
|
|||
.insert(schema.eventResults)
|
||||
.values({
|
||||
scoringEventId: data.scoringEventId,
|
||||
participantId: data.participantId,
|
||||
seasonParticipantId: data.participantId,
|
||||
placement: data.placement,
|
||||
qualifyingPointsAwarded: data.qualifyingPointsAwarded?.toString(),
|
||||
eliminated: data.eliminated,
|
||||
|
|
@ -57,7 +57,7 @@ export async function createEventResultsBulk(
|
|||
.values(
|
||||
results.map((r) => ({
|
||||
scoringEventId: r.scoringEventId,
|
||||
participantId: r.participantId,
|
||||
seasonParticipantId: r.participantId,
|
||||
placement: r.placement,
|
||||
qualifyingPointsAwarded: r.qualifyingPointsAwarded?.toString(),
|
||||
eliminated: r.eliminated,
|
||||
|
|
@ -81,7 +81,7 @@ export async function getEventResultById(
|
|||
return await db.query.eventResults.findFirst({
|
||||
where: eq(schema.eventResults.id, resultId),
|
||||
with: {
|
||||
participant: {
|
||||
seasonParticipant: {
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
|
|
@ -108,7 +108,7 @@ export async function getEventResults(
|
|||
where: eq(schema.eventResults.scoringEventId, eventId),
|
||||
orderBy: schema.eventResults.placement,
|
||||
with: {
|
||||
participant: {
|
||||
seasonParticipant: {
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
|
|
@ -131,7 +131,7 @@ export async function getParticipantEventResults(
|
|||
const db = providedDb || database();
|
||||
|
||||
return await db.query.eventResults.findMany({
|
||||
where: eq(schema.eventResults.participantId, participantId),
|
||||
where: eq(schema.eventResults.seasonParticipantId, participantId),
|
||||
with: {
|
||||
scoringEvent: true,
|
||||
},
|
||||
|
|
@ -203,7 +203,7 @@ export async function hasParticipantResult(
|
|||
const result = await db.query.eventResults.findFirst({
|
||||
where: and(
|
||||
eq(schema.eventResults.scoringEventId, eventId),
|
||||
eq(schema.eventResults.participantId, participantId)
|
||||
eq(schema.eventResults.seasonParticipantId, participantId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -226,10 +226,10 @@ export async function getEventResultsForParticipants(
|
|||
return await db.query.eventResults.findMany({
|
||||
where: and(
|
||||
eq(schema.eventResults.scoringEventId, eventId),
|
||||
inArray(schema.eventResults.participantId, participantIds)
|
||||
inArray(schema.eventResults.seasonParticipantId, participantIds)
|
||||
),
|
||||
with: {
|
||||
participant: {
|
||||
seasonParticipant: {
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import { participantGolfSkills, participants } from "~/database/schema";
|
||||
import { participantGolfSkills, seasonParticipants } from "~/database/schema";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
|
||||
export interface GolfSkillsRecord {
|
||||
|
|
@ -40,7 +40,7 @@ export interface GolfSkillsInput {
|
|||
|
||||
/**
|
||||
* Get all golf skill records for a sports season, joined with participant names.
|
||||
* Returns one record per participant (participants with no record are excluded).
|
||||
* Returns one record per participant (seasonParticipants with no record are excluded).
|
||||
*/
|
||||
export async function getGolfSkillsForSeason(
|
||||
sportsSeasonId: string
|
||||
|
|
@ -58,12 +58,12 @@ export async function getGolfSkillsForSeason(
|
|||
openChampionshipOdds: participantGolfSkills.openChampionshipOdds,
|
||||
pgaChampionshipOdds: participantGolfSkills.pgaChampionshipOdds,
|
||||
updatedAt: participantGolfSkills.updatedAt,
|
||||
participantName: participants.name,
|
||||
participantName: seasonParticipants.name,
|
||||
})
|
||||
.from(participantGolfSkills)
|
||||
.innerJoin(participants, eq(participantGolfSkills.participantId, participants.id))
|
||||
.innerJoin(seasonParticipants, eq(participantGolfSkills.participantId, seasonParticipants.id))
|
||||
.where(eq(participantGolfSkills.sportsSeasonId, sportsSeasonId))
|
||||
.orderBy(participants.name);
|
||||
.orderBy(seasonParticipants.name);
|
||||
|
||||
return rows.map((r) => ({
|
||||
...r,
|
||||
|
|
@ -94,7 +94,7 @@ export async function getGolfSkillsMap(
|
|||
}
|
||||
|
||||
/**
|
||||
* Upsert golf skill ratings for a batch of participants.
|
||||
* Upsert golf skill ratings for a batch of seasonParticipants.
|
||||
* Uses INSERT … ON CONFLICT DO UPDATE so all columns are overwritten atomically.
|
||||
*/
|
||||
export async function batchUpsertGolfSkills(
|
||||
|
|
|
|||
|
|
@ -334,8 +334,8 @@ export async function getUpcomingGroupStageMatchesForParticipants(
|
|||
const allParticipantIds = [
|
||||
...new Set(rows.flatMap((r) => [r.participant1Id, r.participant2Id])),
|
||||
];
|
||||
const participantRows = await db.query.participants.findMany({
|
||||
where: inArray(schema.participants.id, allParticipantIds),
|
||||
const participantRows = await db.query.seasonParticipants.findMany({
|
||||
where: inArray(schema.seasonParticipants.id, allParticipantIds),
|
||||
});
|
||||
const participantMap = new Map(participantRows.map((p) => [p.id, p]));
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export * from "./team";
|
|||
export * from "./commissioner";
|
||||
export * from "./sport";
|
||||
export * from "./sports-season";
|
||||
export * from "./participant";
|
||||
export * from "./season-participant";
|
||||
export * from "./season-template";
|
||||
export * from "./season-template-sport";
|
||||
export * from "./season-sport";
|
||||
|
|
@ -18,3 +18,7 @@ export * from "./draft-timer";
|
|||
export * from "./draft-utils";
|
||||
export * from "./watchlist";
|
||||
export * from "./audit-log";
|
||||
export * from "./tournament";
|
||||
export * from "./participant";
|
||||
export * from "./tournament-result";
|
||||
export * from "./canonical-surface-elo";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* Model for Participant Expected Values
|
||||
*
|
||||
* Manages probability distributions and calculated EVs for participants
|
||||
* Manages probability distributions and calculated EVs for seasonParticipants
|
||||
* in sports seasons.
|
||||
*/
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import { participantExpectedValues, participants } from "~/database/schema";
|
||||
import { seasonParticipantExpectedValues, seasonParticipants } from "~/database/schema";
|
||||
import { eq, and, count, sql } from "drizzle-orm";
|
||||
import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator";
|
||||
import { calculateEV, normalizeProbabilities, calculateReplacementLevel, calculateVORP } from "~/services/ev-calculator";
|
||||
|
|
@ -53,9 +53,9 @@ export interface UpdateProbabilityInput {
|
|||
* Recalculate and persist VORP for every participant in a sports season.
|
||||
*
|
||||
* VORP = participant EV − replacement level EV
|
||||
* Replacement level = average EV of participants ranked 12th–14th in this season.
|
||||
* Replacement level = average EV of seasonParticipants ranked 12th–14th in this season.
|
||||
*
|
||||
* Call this after any operation that changes EVs for participants in the season.
|
||||
* Call this after any operation that changes EVs for seasonParticipants in the season.
|
||||
*/
|
||||
export async function syncVorpForSeason(sportsSeasonId: string): Promise<void> {
|
||||
const db = database();
|
||||
|
|
@ -72,12 +72,12 @@ export async function syncVorpForSeason(sportsSeasonId: string): Promise<void> {
|
|||
|
||||
const now = new Date();
|
||||
|
||||
// Build a single CASE expression to update all participants in one query
|
||||
// Build a single CASE expression to update all seasonParticipants in one query
|
||||
// instead of N individual UPDATE statements.
|
||||
const vorpCaseExpr = sql`CASE ${sql.join(
|
||||
sorted.map((ev) => {
|
||||
const vorp = calculateVORP(parseFloat(ev.expectedValue), replacementLevel);
|
||||
return sql`WHEN ${participants.id} = ${ev.participantId} THEN ${vorp.toFixed(4)}::numeric`;
|
||||
return sql`WHEN ${seasonParticipants.id} = ${ev.participantId} THEN ${vorp.toFixed(4)}::numeric`;
|
||||
}),
|
||||
sql` `
|
||||
)} END`;
|
||||
|
|
@ -85,9 +85,9 @@ export async function syncVorpForSeason(sportsSeasonId: string): Promise<void> {
|
|||
const ids = sorted.map((ev) => ev.participantId);
|
||||
|
||||
await db
|
||||
.update(participants)
|
||||
.update(seasonParticipants)
|
||||
.set({ vorpValue: vorpCaseExpr, updatedAt: now })
|
||||
.where(sql`${participants.id} = ANY(${sql`ARRAY[${sql.join(ids.map((id) => sql`${id}`), sql`, `)}]::uuid[]`})`);
|
||||
.where(sql`${seasonParticipants.id} = ANY(${sql`ARRAY[${sql.join(ids.map((id) => sql`${id}`), sql`, `)}]::uuid[]`})`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -118,11 +118,11 @@ export async function upsertParticipantEV(
|
|||
// Check if record exists
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(participantExpectedValues)
|
||||
.from(seasonParticipantExpectedValues)
|
||||
.where(
|
||||
and(
|
||||
eq(participantExpectedValues.participantId, participantId),
|
||||
eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
eq(seasonParticipantExpectedValues.participantId, participantId),
|
||||
eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
@ -134,7 +134,7 @@ export async function upsertParticipantEV(
|
|||
if (existing.length > 0) {
|
||||
// Update existing
|
||||
const updated = await db
|
||||
.update(participantExpectedValues)
|
||||
.update(seasonParticipantExpectedValues)
|
||||
.set({
|
||||
probFirst: probabilities.probFirst.toString(),
|
||||
probSecond: probabilities.probSecond.toString(),
|
||||
|
|
@ -150,14 +150,14 @@ export async function upsertParticipantEV(
|
|||
calculatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(participantExpectedValues.id, existing[0].id))
|
||||
.where(eq(seasonParticipantExpectedValues.id, existing[0].id))
|
||||
.returning();
|
||||
|
||||
result = updated[0];
|
||||
} else {
|
||||
// Create new
|
||||
const created = await db
|
||||
.insert(participantExpectedValues)
|
||||
.insert(seasonParticipantExpectedValues)
|
||||
.values({
|
||||
participantId,
|
||||
sportsSeasonId,
|
||||
|
|
@ -180,13 +180,13 @@ export async function upsertParticipantEV(
|
|||
result = created[0];
|
||||
}
|
||||
|
||||
// Sync calculated EV to participants table for draft room ranking
|
||||
// Sync calculated EV to seasonParticipants table for draft room ranking
|
||||
await db
|
||||
.update(participants)
|
||||
.update(seasonParticipants)
|
||||
.set({ expectedValue: expectedValue.toString(), updatedAt: now })
|
||||
.where(eq(participants.id, participantId));
|
||||
.where(eq(seasonParticipants.id, participantId));
|
||||
|
||||
// Recalculate VORP for all participants in this season (replacement level may have shifted)
|
||||
// Recalculate VORP for all seasonParticipants in this season (replacement level may have shifted)
|
||||
await syncVorpForSeason(sportsSeasonId);
|
||||
|
||||
return result;
|
||||
|
|
@ -209,7 +209,7 @@ export async function upsertParticipantEVWithNormalization(
|
|||
|
||||
export async function countAllParticipantEVs(): Promise<number> {
|
||||
const db = database();
|
||||
const result = await db.select({ value: count() }).from(participantExpectedValues);
|
||||
const result = await db.select({ value: count() }).from(seasonParticipantExpectedValues);
|
||||
return result[0].value;
|
||||
}
|
||||
|
||||
|
|
@ -223,11 +223,11 @@ export async function getParticipantEV(
|
|||
const db = database();
|
||||
const result = await db
|
||||
.select()
|
||||
.from(participantExpectedValues)
|
||||
.from(seasonParticipantExpectedValues)
|
||||
.where(
|
||||
and(
|
||||
eq(participantExpectedValues.participantId, participantId),
|
||||
eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
eq(seasonParticipantExpectedValues.participantId, participantId),
|
||||
eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
@ -244,8 +244,8 @@ export async function getAllParticipantEVsForSeason(
|
|||
const db = database();
|
||||
return db
|
||||
.select()
|
||||
.from(participantExpectedValues)
|
||||
.where(eq(participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(seasonParticipantExpectedValues)
|
||||
.where(eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -257,21 +257,21 @@ export async function deleteParticipantEV(
|
|||
): Promise<void> {
|
||||
const db = database();
|
||||
await db
|
||||
.delete(participantExpectedValues)
|
||||
.delete(seasonParticipantExpectedValues)
|
||||
.where(
|
||||
and(
|
||||
eq(participantExpectedValues.participantId, participantId),
|
||||
eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
eq(seasonParticipantExpectedValues.participantId, participantId),
|
||||
eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
)
|
||||
);
|
||||
|
||||
// Reset this participant's EV and VORP to 0 (no longer ranked)
|
||||
await db
|
||||
.update(participants)
|
||||
.update(seasonParticipants)
|
||||
.set({ expectedValue: "0", vorpValue: "0", updatedAt: new Date() })
|
||||
.where(eq(participants.id, participantId));
|
||||
.where(eq(seasonParticipants.id, participantId));
|
||||
|
||||
// Recalculate VORP for remaining participants — replacement level may have shifted
|
||||
// Recalculate VORP for remaining seasonParticipants — replacement level may have shifted
|
||||
await syncVorpForSeason(sportsSeasonId);
|
||||
}
|
||||
|
||||
|
|
@ -301,12 +301,12 @@ export async function batchUpsertParticipantEVs(
|
|||
const expectedValue = calculateEV(probabilities, scoringRules);
|
||||
|
||||
const existing = await tx
|
||||
.select({ id: participantExpectedValues.id })
|
||||
.from(participantExpectedValues)
|
||||
.select({ id: seasonParticipantExpectedValues.id })
|
||||
.from(seasonParticipantExpectedValues)
|
||||
.where(
|
||||
and(
|
||||
eq(participantExpectedValues.participantId, participantId),
|
||||
eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
eq(seasonParticipantExpectedValues.participantId, participantId),
|
||||
eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
|
@ -330,24 +330,24 @@ export async function batchUpsertParticipantEVs(
|
|||
|
||||
if (existing.length > 0) {
|
||||
const [updated] = await tx
|
||||
.update(participantExpectedValues)
|
||||
.update(seasonParticipantExpectedValues)
|
||||
// Only overwrite sourceOdds if explicitly provided; preserve existing value otherwise
|
||||
.set(sourceOdds !== undefined ? { ...baseValues, sourceOdds } : baseValues)
|
||||
.where(eq(participantExpectedValues.id, existing[0].id))
|
||||
.where(eq(seasonParticipantExpectedValues.id, existing[0].id))
|
||||
.returning();
|
||||
result = updated;
|
||||
} else {
|
||||
const [created] = await tx
|
||||
.insert(participantExpectedValues)
|
||||
.insert(seasonParticipantExpectedValues)
|
||||
.values({ participantId, sportsSeasonId, ...baseValues, sourceOdds: sourceOdds ?? null })
|
||||
.returning();
|
||||
result = created;
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(participants)
|
||||
.update(seasonParticipants)
|
||||
.set({ expectedValue: expectedValue.toString(), updatedAt: now })
|
||||
.where(eq(participants.id, participantId));
|
||||
.where(eq(seasonParticipants.id, participantId));
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
|
@ -361,7 +361,7 @@ export async function batchUpsertParticipantEVs(
|
|||
}
|
||||
|
||||
/**
|
||||
* Save American odds for a batch of participants without touching probabilities or EV.
|
||||
* Save American odds for a batch of seasonParticipants without touching probabilities or EV.
|
||||
* Used by the futures-odds admin page to persist odds before running the full simulation.
|
||||
*/
|
||||
export async function batchSaveSourceOdds(
|
||||
|
|
@ -374,24 +374,24 @@ export async function batchSaveSourceOdds(
|
|||
|
||||
for (const { participantId, sportsSeasonId, sourceOdds } of inputs) {
|
||||
const existing = await tx
|
||||
.select({ id: participantExpectedValues.id })
|
||||
.from(participantExpectedValues)
|
||||
.select({ id: seasonParticipantExpectedValues.id })
|
||||
.from(seasonParticipantExpectedValues)
|
||||
.where(
|
||||
and(
|
||||
eq(participantExpectedValues.participantId, participantId),
|
||||
eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
eq(seasonParticipantExpectedValues.participantId, participantId),
|
||||
eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
await tx
|
||||
.update(participantExpectedValues)
|
||||
.update(seasonParticipantExpectedValues)
|
||||
.set({ sourceOdds, updatedAt: now })
|
||||
.where(eq(participantExpectedValues.id, existing[0].id));
|
||||
.where(eq(seasonParticipantExpectedValues.id, existing[0].id));
|
||||
} else {
|
||||
// Insert a stub record — probabilities/EV will be filled in by the simulator
|
||||
await tx.insert(participantExpectedValues).values({
|
||||
await tx.insert(seasonParticipantExpectedValues).values({
|
||||
participantId,
|
||||
sportsSeasonId,
|
||||
probFirst: "0",
|
||||
|
|
@ -414,7 +414,7 @@ export async function batchSaveSourceOdds(
|
|||
}
|
||||
|
||||
/**
|
||||
* Persist raw Elo ratings (and optional world rankings) for a batch of participants.
|
||||
* Persist raw Elo ratings (and optional world rankings) for a batch of seasonParticipants.
|
||||
* Used by Elo-based simulators like snooker_bracket and darts_bracket.
|
||||
* Creates stub records if none exist; does not touch probabilities or EV.
|
||||
* Uses INSERT ... ON CONFLICT DO UPDATE to avoid N+1 queries.
|
||||
|
|
@ -427,7 +427,7 @@ export async function batchSaveSourceElos(
|
|||
const now = new Date();
|
||||
|
||||
await db
|
||||
.insert(participantExpectedValues)
|
||||
.insert(seasonParticipantExpectedValues)
|
||||
.values(
|
||||
inputs.map(({ participantId, sportsSeasonId, sourceElo, worldRanking }) => ({
|
||||
participantId,
|
||||
|
|
@ -449,7 +449,7 @@ export async function batchSaveSourceElos(
|
|||
}))
|
||||
)
|
||||
.onConflictDoUpdate({
|
||||
target: [participantExpectedValues.participantId, participantExpectedValues.sportsSeasonId],
|
||||
target: [seasonParticipantExpectedValues.participantId, seasonParticipantExpectedValues.sportsSeasonId],
|
||||
set: {
|
||||
sourceElo: sql`excluded.source_elo`,
|
||||
worldRanking: sql`excluded.world_ranking`,
|
||||
|
|
@ -499,26 +499,26 @@ export async function recalculateEV(
|
|||
const db = database();
|
||||
const now = new Date();
|
||||
const updated = await db
|
||||
.update(participantExpectedValues)
|
||||
.update(seasonParticipantExpectedValues)
|
||||
.set({
|
||||
expectedValue: newEV.toString(),
|
||||
calculatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(participantExpectedValues.id, existing.id))
|
||||
.where(eq(seasonParticipantExpectedValues.id, existing.id))
|
||||
.returning();
|
||||
|
||||
// Sync recalculated EV to participants table
|
||||
// Sync recalculated EV to seasonParticipants table
|
||||
await db
|
||||
.update(participants)
|
||||
.update(seasonParticipants)
|
||||
.set({ expectedValue: newEV.toString(), updatedAt: now })
|
||||
.where(eq(participants.id, participantId));
|
||||
.where(eq(seasonParticipants.id, participantId));
|
||||
|
||||
return updated[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate EVs for all participants in a sports season
|
||||
* Recalculate EVs for all seasonParticipants in a sports season
|
||||
* Used when scoring rules change
|
||||
*/
|
||||
export async function recalculateAllEVsForSeason(
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@ import { eq, and } from "drizzle-orm";
|
|||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type ParticipantResult = typeof schema.participantResults.$inferSelect;
|
||||
export type ParticipantResult = typeof schema.seasonParticipantResults.$inferSelect;
|
||||
export type ParticipantResultWithParticipant = ParticipantResult & {
|
||||
participant: { id: string; name: string } | null;
|
||||
};
|
||||
export type NewParticipantResult = typeof schema.participantResults.$inferInsert;
|
||||
export type NewParticipantResult = typeof schema.seasonParticipantResults.$inferInsert;
|
||||
|
||||
export async function createParticipantResult(
|
||||
data: NewParticipantResult
|
||||
): Promise<ParticipantResult> {
|
||||
const db = database();
|
||||
const [result] = await db
|
||||
.insert(schema.participantResults)
|
||||
.insert(schema.seasonParticipantResults)
|
||||
.values(data)
|
||||
.returning();
|
||||
return result;
|
||||
|
|
@ -24,7 +24,7 @@ export async function createManyParticipantResults(
|
|||
): Promise<ParticipantResult[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.insert(schema.participantResults)
|
||||
.insert(schema.seasonParticipantResults)
|
||||
.values(data)
|
||||
.returning();
|
||||
}
|
||||
|
|
@ -33,8 +33,8 @@ export async function findParticipantResultById(
|
|||
id: string
|
||||
): Promise<ParticipantResult | undefined> {
|
||||
const db = database();
|
||||
return await db.query.participantResults.findFirst({
|
||||
where: eq(schema.participantResults.id, id),
|
||||
return await db.query.seasonParticipantResults.findFirst({
|
||||
where: eq(schema.seasonParticipantResults.id, id),
|
||||
with: {
|
||||
participant: true,
|
||||
sportsSeason: {
|
||||
|
|
@ -50,8 +50,8 @@ export async function findParticipantResultByParticipantId(
|
|||
participantId: string
|
||||
): Promise<ParticipantResult | undefined> {
|
||||
const db = database();
|
||||
return await db.query.participantResults.findFirst({
|
||||
where: eq(schema.participantResults.participantId, participantId),
|
||||
return await db.query.seasonParticipantResults.findFirst({
|
||||
where: eq(schema.seasonParticipantResults.participantId, participantId),
|
||||
with: {
|
||||
participant: true,
|
||||
sportsSeason: {
|
||||
|
|
@ -67,8 +67,8 @@ export async function findParticipantResultsBySportsSeasonId(
|
|||
sportsSeasonId: string
|
||||
): Promise<ParticipantResultWithParticipant[]> {
|
||||
const db = database();
|
||||
return await db.query.participantResults.findMany({
|
||||
where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
||||
return await db.query.seasonParticipantResults.findMany({
|
||||
where: eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId),
|
||||
orderBy: (results, { asc }) => [asc(results.finalPosition)],
|
||||
with: {
|
||||
participant: true,
|
||||
|
|
@ -82,16 +82,16 @@ export async function updateParticipantResult(
|
|||
): Promise<ParticipantResult> {
|
||||
const db = database();
|
||||
const [result] = await db
|
||||
.update(schema.participantResults)
|
||||
.update(schema.seasonParticipantResults)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(schema.participantResults.id, id))
|
||||
.where(eq(schema.seasonParticipantResults.id, id))
|
||||
.returning();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteParticipantResult(id: string): Promise<void> {
|
||||
const db = database();
|
||||
await db.delete(schema.participantResults).where(eq(schema.participantResults.id, id));
|
||||
await db.delete(schema.seasonParticipantResults).where(eq(schema.seasonParticipantResults.id, id));
|
||||
}
|
||||
|
||||
export async function deleteParticipantResultsBySportsSeasonId(
|
||||
|
|
@ -99,8 +99,8 @@ export async function deleteParticipantResultsBySportsSeasonId(
|
|||
): Promise<void> {
|
||||
const db = database();
|
||||
await db
|
||||
.delete(schema.participantResults)
|
||||
.where(eq(schema.participantResults.sportsSeasonId, sportsSeasonId));
|
||||
.delete(schema.seasonParticipantResults)
|
||||
.where(eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,10 +117,10 @@ export async function setParticipantResult(
|
|||
const db = database();
|
||||
|
||||
// Check if result already exists
|
||||
const existing = await db.query.participantResults.findFirst({
|
||||
const existing = await db.query.seasonParticipantResults.findFirst({
|
||||
where: and(
|
||||
eq(schema.participantResults.participantId, participantId),
|
||||
eq(schema.participantResults.sportsSeasonId, sportsSeasonId)
|
||||
eq(schema.seasonParticipantResults.participantId, participantId),
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { eq, inArray, count, and, sql, asc, desc } from "drizzle-orm";
|
||||
import { eq, and, asc } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type Participant = typeof schema.participants.$inferSelect;
|
||||
export type NewParticipant = typeof schema.participants.$inferInsert;
|
||||
export type CanonicalParticipant = typeof schema.participants.$inferSelect;
|
||||
export type NewCanonicalParticipant = typeof schema.participants.$inferInsert;
|
||||
|
||||
export async function createParticipant(data: NewParticipant): Promise<Participant> {
|
||||
export async function createCanonicalParticipant(
|
||||
data: NewCanonicalParticipant
|
||||
): Promise<CanonicalParticipant> {
|
||||
const db = database();
|
||||
const [participant] = await db
|
||||
.insert(schema.participants)
|
||||
|
|
@ -14,187 +16,61 @@ export async function createParticipant(data: NewParticipant): Promise<Participa
|
|||
return participant;
|
||||
}
|
||||
|
||||
export async function createManyParticipants(data: NewParticipant[]): Promise<Participant[]> {
|
||||
export async function getCanonicalParticipantById(
|
||||
id: string
|
||||
): Promise<CanonicalParticipant | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.id, id))
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findCanonicalParticipantsBySport(
|
||||
sportId: string
|
||||
): Promise<CanonicalParticipant[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.insert(schema.participants)
|
||||
.values(data)
|
||||
.returning();
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportId, sportId))
|
||||
.orderBy(asc(schema.participants.name));
|
||||
}
|
||||
|
||||
export async function findParticipantById(id: string): Promise<Participant | undefined> {
|
||||
const db = database();
|
||||
return await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, id),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
sport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function findParticipantByName(
|
||||
sportsSeasonId: string,
|
||||
export async function findCanonicalParticipantBySportName(
|
||||
sportId: string,
|
||||
name: string
|
||||
): Promise<Participant | undefined> {
|
||||
): Promise<CanonicalParticipant | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
||||
sql`lower(${schema.participants.name}) = lower(${name})`
|
||||
eq(schema.participants.sportId, sportId),
|
||||
eq(schema.participants.name, name)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return results[0];
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findParticipantsBySportsSeasonId(sportsSeasonId: string): Promise<Participant[]> {
|
||||
const db = database();
|
||||
return await db.query.participants.findMany({
|
||||
where: eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
||||
orderBy: (participants, { asc: orderAsc }) => [orderAsc(participants.name)],
|
||||
});
|
||||
}
|
||||
export async function upsertCanonicalParticipantBySportName(
|
||||
sportId: string,
|
||||
name: string,
|
||||
extra?: Partial<NewCanonicalParticipant>
|
||||
): Promise<CanonicalParticipant> {
|
||||
const existing = await findCanonicalParticipantBySportName(sportId, name);
|
||||
|
||||
export async function findParticipantsByExternalId(
|
||||
externalId: string
|
||||
): Promise<Participant[]> {
|
||||
const db = database();
|
||||
return await db.query.participants.findMany({
|
||||
where: eq(schema.participants.externalId, externalId),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
sport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateParticipant(
|
||||
id: string,
|
||||
data: Partial<NewParticipant>
|
||||
): Promise<Participant> {
|
||||
const db = database();
|
||||
const [participant] = await db
|
||||
.update(schema.participants)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(schema.participants.id, id))
|
||||
.returning();
|
||||
return participant;
|
||||
}
|
||||
|
||||
export async function countAllParticipants(): Promise<number> {
|
||||
const db = database();
|
||||
const result = await db.select({ value: count() }).from(schema.participants);
|
||||
return result[0].value;
|
||||
}
|
||||
|
||||
export async function deleteParticipant(id: string): Promise<void> {
|
||||
const db = database();
|
||||
await db.delete(schema.participants).where(eq(schema.participants.id, id));
|
||||
}
|
||||
|
||||
export async function copyParticipantsFromSeason(
|
||||
sourceSportsSeasonId: string,
|
||||
targetSportsSeasonId: string
|
||||
): Promise<Participant[]> {
|
||||
// Get all participants from source season
|
||||
const sourceParticipants = await findParticipantsBySportsSeasonId(sourceSportsSeasonId);
|
||||
|
||||
// Insert them into the target season
|
||||
if (sourceParticipants.length === 0) {
|
||||
return [];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return await createManyParticipants(
|
||||
sourceParticipants.map((p) => ({
|
||||
sportsSeasonId: targetSportsSeasonId,
|
||||
name: p.name,
|
||||
shortName: p.shortName,
|
||||
externalId: p.externalId,
|
||||
expectedValue: p.expectedValue,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all participants for a season with sport information
|
||||
* Used for draft eligibility calculations
|
||||
*/
|
||||
export async function getParticipantsForSeasonWithSports(seasonId: string, providedDb?: ReturnType<typeof database>) {
|
||||
const db = providedDb || database();
|
||||
|
||||
// First get all sports seasons for this season
|
||||
const seasonSports = await db.query.seasonSports.findMany({
|
||||
where: eq(schema.seasonSports.seasonId, seasonId),
|
||||
return await createCanonicalParticipant({
|
||||
sportId,
|
||||
name,
|
||||
...extra,
|
||||
});
|
||||
|
||||
if (seasonSports.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
|
||||
|
||||
// Get all participants for these sports seasons with sport info
|
||||
const participants = await db
|
||||
.select({
|
||||
id: schema.participants.id,
|
||||
name: schema.participants.name,
|
||||
sport: {
|
||||
id: schema.sports.id,
|
||||
name: schema.sports.name,
|
||||
},
|
||||
})
|
||||
.from(schema.participants)
|
||||
.innerJoin(
|
||||
schema.sportsSeasons,
|
||||
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sports,
|
||||
eq(schema.sportsSeasons.sportId, schema.sports.id)
|
||||
)
|
||||
.where(
|
||||
sportsSeasonIds.length === 1
|
||||
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
|
||||
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
|
||||
);
|
||||
|
||||
return participants;
|
||||
}
|
||||
|
||||
export async function getDraftParticipants(seasonId: string) {
|
||||
const db = database();
|
||||
|
||||
const seasonSports = await db.query.seasonSports.findMany({
|
||||
where: eq(schema.seasonSports.seasonId, seasonId),
|
||||
});
|
||||
|
||||
const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
|
||||
if (sportsSeasonIds.length === 0) return [];
|
||||
|
||||
return await db
|
||||
.select({
|
||||
id: schema.participants.id,
|
||||
name: schema.participants.name,
|
||||
vorpValue: schema.participants.vorpValue,
|
||||
sport: schema.sports,
|
||||
})
|
||||
.from(schema.participants)
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(
|
||||
sportsSeasonIds.length === 1
|
||||
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
|
||||
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds)
|
||||
)
|
||||
.orderBy(desc(schema.participants.vorpValue), asc(schema.participants.name));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,16 +144,16 @@ export async function getOrCreateParticipantQPTotal(
|
|||
) {
|
||||
const db = providedDb || database();
|
||||
|
||||
let total = await db.query.participantQualifyingTotals.findFirst({
|
||||
let total = await db.query.seasonParticipantQualifyingTotals.findFirst({
|
||||
where: and(
|
||||
eq(schema.participantQualifyingTotals.participantId, participantId),
|
||||
eq(schema.participantQualifyingTotals.sportsSeasonId, sportsSeasonId)
|
||||
eq(schema.seasonParticipantQualifyingTotals.participantId, participantId),
|
||||
eq(schema.seasonParticipantQualifyingTotals.sportsSeasonId, sportsSeasonId)
|
||||
),
|
||||
});
|
||||
|
||||
if (!total) {
|
||||
const [created] = await db
|
||||
.insert(schema.participantQualifyingTotals)
|
||||
.insert(schema.seasonParticipantQualifyingTotals)
|
||||
.values({
|
||||
participantId,
|
||||
sportsSeasonId,
|
||||
|
|
@ -184,12 +184,12 @@ export async function addQualifyingPoints(
|
|||
|
||||
// Update with new points (do NOT increment eventsScored here)
|
||||
const [updated] = await db
|
||||
.update(schema.participantQualifyingTotals)
|
||||
.update(schema.seasonParticipantQualifyingTotals)
|
||||
.set({
|
||||
totalQualifyingPoints: (parseFloat(total.totalQualifyingPoints) + pointsToAdd).toString(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.participantQualifyingTotals.id, total.id))
|
||||
.where(eq(schema.seasonParticipantQualifyingTotals.id, total.id))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
|
|
@ -208,7 +208,7 @@ export async function recalculateParticipantQP(
|
|||
|
||||
// Get all event results for this participant in this sports season
|
||||
const eventResults = await db.query.eventResults.findMany({
|
||||
where: eq(schema.eventResults.participantId, participantId),
|
||||
where: eq(schema.eventResults.seasonParticipantId, participantId),
|
||||
with: {
|
||||
scoringEvent: true,
|
||||
},
|
||||
|
|
@ -238,13 +238,13 @@ export async function recalculateParticipantQP(
|
|||
|
||||
// Update with recalculated values
|
||||
await db
|
||||
.update(schema.participantQualifyingTotals)
|
||||
.update(schema.seasonParticipantQualifyingTotals)
|
||||
.set({
|
||||
totalQualifyingPoints: totalQP.toString(),
|
||||
eventsScored,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.participantQualifyingTotals.id, total.id));
|
||||
.where(eq(schema.seasonParticipantQualifyingTotals.id, total.id));
|
||||
|
||||
return { totalQP, eventsScored };
|
||||
}
|
||||
|
|
@ -271,9 +271,9 @@ export async function getQPStandings(
|
|||
) {
|
||||
const db = providedDb || database();
|
||||
|
||||
return await db.query.participantQualifyingTotals.findMany({
|
||||
where: eq(schema.participantQualifyingTotals.sportsSeasonId, sportsSeasonId),
|
||||
orderBy: [desc(sql`CAST(${schema.participantQualifyingTotals.totalQualifyingPoints} AS DECIMAL)`)],
|
||||
return await db.query.seasonParticipantQualifyingTotals.findMany({
|
||||
where: eq(schema.seasonParticipantQualifyingTotals.sportsSeasonId, sportsSeasonId),
|
||||
orderBy: [desc(sql`CAST(${schema.seasonParticipantQualifyingTotals.totalQualifyingPoints} AS DECIMAL)`)],
|
||||
with: {
|
||||
participant: true,
|
||||
},
|
||||
|
|
@ -325,9 +325,9 @@ export async function updateFinalRankings(
|
|||
// Update all rankings
|
||||
for (const update of updates) {
|
||||
await db
|
||||
.update(schema.participantQualifyingTotals)
|
||||
.update(schema.seasonParticipantQualifyingTotals)
|
||||
.set({ finalRanking: update.finalRanking, updatedAt: new Date() })
|
||||
.where(eq(schema.participantQualifyingTotals.id, update.id));
|
||||
.where(eq(schema.seasonParticipantQualifyingTotals.id, update.id));
|
||||
}
|
||||
|
||||
return updates;
|
||||
|
|
@ -343,6 +343,6 @@ export async function resetQualifyingPoints(
|
|||
const db = providedDb || database();
|
||||
|
||||
await db
|
||||
.delete(schema.participantQualifyingTotals)
|
||||
.where(eq(schema.participantQualifyingTotals.sportsSeasonId, sportsSeasonId));
|
||||
.delete(schema.seasonParticipantQualifyingTotals)
|
||||
.where(eq(schema.seasonParticipantQualifyingTotals.sportsSeasonId, sportsSeasonId));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,8 +201,8 @@ export async function processPlayoffEvent(
|
|||
|
||||
// PHASE 5.3: Handle teams that didn't make the playoffs
|
||||
// Get all participants in the sports season
|
||||
const allParticipants = await db.query.participants.findMany({
|
||||
where: eq(schema.participants.sportsSeasonId, event.sportsSeasonId),
|
||||
const allParticipants = await db.query.seasonParticipants.findMany({
|
||||
where: eq(schema.seasonParticipants.sportsSeasonId, event.sportsSeasonId),
|
||||
});
|
||||
|
||||
// Get all participant IDs that are in ANY playoff match (winners or losers)
|
||||
|
|
@ -222,10 +222,10 @@ export async function processPlayoffEvent(
|
|||
for (const participant of allParticipants) {
|
||||
if (!participantsInBracket.has(participant.id)) {
|
||||
// Check if they already have a result (don't overwrite)
|
||||
const existingResult = await db.query.participantResults.findFirst({
|
||||
const existingResult = await db.query.seasonParticipantResults.findFirst({
|
||||
where: and(
|
||||
eq(schema.participantResults.participantId, participant.id),
|
||||
eq(schema.participantResults.sportsSeasonId, event.sportsSeasonId)
|
||||
eq(schema.seasonParticipantResults.participantId, participant.id),
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, event.sportsSeasonId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -506,10 +506,10 @@ async function upsertParticipantResult(
|
|||
db: ReturnType<typeof database>,
|
||||
isPartialScore = false
|
||||
): Promise<number | null> {
|
||||
const existing = await db.query.participantResults.findFirst({
|
||||
const existing = await db.query.seasonParticipantResults.findFirst({
|
||||
where: and(
|
||||
eq(schema.participantResults.participantId, participantId),
|
||||
eq(schema.participantResults.sportsSeasonId, sportsSeasonId)
|
||||
eq(schema.seasonParticipantResults.participantId, participantId),
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -520,16 +520,16 @@ async function upsertParticipantResult(
|
|||
if (!existing.isPartialScore && isPartialScore) return null;
|
||||
|
||||
await db
|
||||
.update(schema.participantResults)
|
||||
.update(schema.seasonParticipantResults)
|
||||
.set({
|
||||
finalPosition,
|
||||
isPartialScore,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.participantResults.id, existing.id));
|
||||
.where(eq(schema.seasonParticipantResults.id, existing.id));
|
||||
return existing.finalPosition ?? 0;
|
||||
} else {
|
||||
await db.insert(schema.participantResults).values({
|
||||
await db.insert(schema.seasonParticipantResults).values({
|
||||
participantId,
|
||||
sportsSeasonId,
|
||||
finalPosition,
|
||||
|
|
@ -651,7 +651,7 @@ export async function processQualifyingEvent(
|
|||
|
||||
// Recalculate totals for all participants from scratch
|
||||
// This is the source of truth - sums all their event_results
|
||||
const participantIds = new Set(results.map(r => r.participantId));
|
||||
const participantIds = new Set(results.map(r => r.seasonParticipantId));
|
||||
for (const participantId of participantIds) {
|
||||
await recalculateParticipantQP(participantId, event.sportsSeasonId, db);
|
||||
}
|
||||
|
|
@ -752,10 +752,10 @@ export async function finalizeQualifyingPoints(
|
|||
if (currentPlacement <= standings.length) {
|
||||
for (let i = currentPlacement - 1; i < standings.length; i++) {
|
||||
const standing = standings[i];
|
||||
const hasResult = await db.query.participantResults.findFirst({
|
||||
const hasResult = await db.query.seasonParticipantResults.findFirst({
|
||||
where: and(
|
||||
eq(schema.participantResults.participantId, standing.participantId),
|
||||
eq(schema.participantResults.sportsSeasonId, sportsSeasonId)
|
||||
eq(schema.seasonParticipantResults.participantId, standing.participantId),
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -1388,11 +1388,11 @@ export async function recalculateAffectedLeagues(
|
|||
.filter((id): id is string => id !== null);
|
||||
const finalizedLoserIds = new Set<string>();
|
||||
if (loserParticipantIds.length > 0) {
|
||||
const loserResults = await db.query.participantResults.findMany({
|
||||
const loserResults = await db.query.seasonParticipantResults.findMany({
|
||||
where: and(
|
||||
inArray(schema.participantResults.participantId, loserParticipantIds),
|
||||
eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
||||
eq(schema.participantResults.isPartialScore, false)
|
||||
inArray(schema.seasonParticipantResults.participantId, loserParticipantIds),
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId),
|
||||
eq(schema.seasonParticipantResults.isPartialScore, false)
|
||||
),
|
||||
});
|
||||
for (const r of loserResults) finalizedLoserIds.add(r.participantId);
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export async function getScoringEventsForSportsSeason(
|
|||
with: {
|
||||
eventResults: {
|
||||
with: {
|
||||
participant: true,
|
||||
seasonParticipant: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -224,7 +224,7 @@ export async function deleteScoringEvent(
|
|||
const results = await db.query.eventResults.findMany({
|
||||
where: eq(schema.eventResults.scoringEventId, eventId),
|
||||
});
|
||||
affectedParticipantIds = results.map((r) => r.participantId);
|
||||
affectedParticipantIds = results.map((r) => r.seasonParticipantId);
|
||||
wasQPProcessed = results.some(
|
||||
(r) => r.qualifyingPointsAwarded && parseFloat(r.qualifyingPointsAwarded) > 0
|
||||
);
|
||||
|
|
@ -233,8 +233,8 @@ export async function deleteScoringEvent(
|
|||
await db.transaction(async (tx) => {
|
||||
if (event.eventType === "playoff_game") {
|
||||
await tx
|
||||
.delete(schema.participantResults)
|
||||
.where(eq(schema.participantResults.sportsSeasonId, event.sportsSeasonId));
|
||||
.delete(schema.seasonParticipantResults)
|
||||
.where(eq(schema.seasonParticipantResults.sportsSeasonId, event.sportsSeasonId));
|
||||
}
|
||||
// Cascades: playoffMatches, eventResults, tournamentGroups/Members
|
||||
await tx.delete(schema.scoringEvents).where(eq(schema.scoringEvents.id, eventId));
|
||||
|
|
|
|||
200
app/models/season-participant.ts
Normal file
200
app/models/season-participant.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { eq, inArray, count, and, sql, asc, desc } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type Participant = typeof schema.seasonParticipants.$inferSelect;
|
||||
export type NewParticipant = typeof schema.seasonParticipants.$inferInsert;
|
||||
|
||||
export async function createParticipant(data: NewParticipant): Promise<Participant> {
|
||||
const db = database();
|
||||
const [participant] = await db
|
||||
.insert(schema.seasonParticipants)
|
||||
.values(data)
|
||||
.returning();
|
||||
return participant;
|
||||
}
|
||||
|
||||
export async function createManyParticipants(data: NewParticipant[]): Promise<Participant[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.insert(schema.seasonParticipants)
|
||||
.values(data)
|
||||
.returning();
|
||||
}
|
||||
|
||||
export async function findParticipantById(id: string): Promise<Participant | undefined> {
|
||||
const db = database();
|
||||
return await db.query.seasonParticipants.findFirst({
|
||||
where: eq(schema.seasonParticipants.id, id),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
sport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function findParticipantByName(
|
||||
sportsSeasonId: string,
|
||||
name: string
|
||||
): Promise<Participant | undefined> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.seasonParticipants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
|
||||
sql`lower(${schema.seasonParticipants.name}) = lower(${name})`
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return results[0];
|
||||
}
|
||||
|
||||
export async function findParticipantsBySportsSeasonId(sportsSeasonId: string): Promise<Participant[]> {
|
||||
const db = database();
|
||||
return await db.query.seasonParticipants.findMany({
|
||||
where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
|
||||
orderBy: (participants, { asc: orderAsc }) => [orderAsc(participants.name)],
|
||||
});
|
||||
}
|
||||
|
||||
export async function findParticipantsByExternalId(
|
||||
externalId: string
|
||||
): Promise<Participant[]> {
|
||||
const db = database();
|
||||
return await db.query.seasonParticipants.findMany({
|
||||
where: eq(schema.seasonParticipants.externalId, externalId),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
sport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateParticipant(
|
||||
id: string,
|
||||
data: Partial<NewParticipant>
|
||||
): Promise<Participant> {
|
||||
const db = database();
|
||||
const [participant] = await db
|
||||
.update(schema.seasonParticipants)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(schema.seasonParticipants.id, id))
|
||||
.returning();
|
||||
return participant;
|
||||
}
|
||||
|
||||
export async function countAllParticipants(): Promise<number> {
|
||||
const db = database();
|
||||
const result = await db.select({ value: count() }).from(schema.seasonParticipants);
|
||||
return result[0].value;
|
||||
}
|
||||
|
||||
export async function deleteParticipant(id: string): Promise<void> {
|
||||
const db = database();
|
||||
await db.delete(schema.seasonParticipants).where(eq(schema.seasonParticipants.id, id));
|
||||
}
|
||||
|
||||
export async function copyParticipantsFromSeason(
|
||||
sourceSportsSeasonId: string,
|
||||
targetSportsSeasonId: string
|
||||
): Promise<Participant[]> {
|
||||
// Get all participants from source season
|
||||
const sourceParticipants = await findParticipantsBySportsSeasonId(sourceSportsSeasonId);
|
||||
|
||||
// Insert them into the target season
|
||||
if (sourceParticipants.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await createManyParticipants(
|
||||
sourceParticipants.map((p) => ({
|
||||
sportsSeasonId: targetSportsSeasonId,
|
||||
name: p.name,
|
||||
shortName: p.shortName,
|
||||
externalId: p.externalId,
|
||||
expectedValue: p.expectedValue,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all participants for a season with sport information
|
||||
* Used for draft eligibility calculations
|
||||
*/
|
||||
export async function getParticipantsForSeasonWithSports(seasonId: string, providedDb?: ReturnType<typeof database>) {
|
||||
const db = providedDb || database();
|
||||
|
||||
// First get all sports seasons for this season
|
||||
const seasonSports = await db.query.seasonSports.findMany({
|
||||
where: eq(schema.seasonSports.seasonId, seasonId),
|
||||
});
|
||||
|
||||
if (seasonSports.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
|
||||
|
||||
// Get all participants for these sports seasons with sport info
|
||||
const participants = await db
|
||||
.select({
|
||||
id: schema.seasonParticipants.id,
|
||||
name: schema.seasonParticipants.name,
|
||||
sport: {
|
||||
id: schema.sports.id,
|
||||
name: schema.sports.name,
|
||||
},
|
||||
})
|
||||
.from(schema.seasonParticipants)
|
||||
.innerJoin(
|
||||
schema.sportsSeasons,
|
||||
eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sports,
|
||||
eq(schema.sportsSeasons.sportId, schema.sports.id)
|
||||
)
|
||||
.where(
|
||||
sportsSeasonIds.length === 1
|
||||
? eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0])
|
||||
: inArray(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds)
|
||||
);
|
||||
|
||||
return participants;
|
||||
}
|
||||
|
||||
export async function getDraftParticipants(seasonId: string) {
|
||||
const db = database();
|
||||
|
||||
const seasonSports = await db.query.seasonSports.findMany({
|
||||
where: eq(schema.seasonSports.seasonId, seasonId),
|
||||
});
|
||||
|
||||
const sportsSeasonIds = seasonSports.map((ss) => ss.sportsSeasonId);
|
||||
if (sportsSeasonIds.length === 0) return [];
|
||||
|
||||
return await db
|
||||
.select({
|
||||
id: schema.seasonParticipants.id,
|
||||
name: schema.seasonParticipants.name,
|
||||
vorpValue: schema.seasonParticipants.vorpValue,
|
||||
sport: schema.sports,
|
||||
})
|
||||
.from(schema.seasonParticipants)
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(
|
||||
sportsSeasonIds.length === 1
|
||||
? eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds[0])
|
||||
: inArray(schema.seasonParticipants.sportsSeasonId, sportsSeasonIds)
|
||||
)
|
||||
.orderBy(desc(schema.seasonParticipants.vorpValue), asc(schema.seasonParticipants.name));
|
||||
}
|
||||
|
|
@ -211,8 +211,8 @@ export async function cloneSportsSeason(
|
|||
};
|
||||
|
||||
// Read all source data before writing anything
|
||||
const sourceParticipants = await db.query.participants.findMany({
|
||||
where: eq(schema.participants.sportsSeasonId, sourceId),
|
||||
const sourceParticipants = await db.query.seasonParticipants.findMany({
|
||||
where: eq(schema.seasonParticipants.sportsSeasonId, sourceId),
|
||||
});
|
||||
|
||||
const sourceEvents = await db.query.scoringEvents.findMany({
|
||||
|
|
@ -225,8 +225,8 @@ export async function cloneSportsSeason(
|
|||
: null;
|
||||
|
||||
// Read source futures odds and Elo ratings
|
||||
const sourceEvRows = await db.query.participantExpectedValues.findMany({
|
||||
where: eq(schema.participantExpectedValues.sportsSeasonId, sourceId),
|
||||
const sourceEvRows = await db.query.seasonParticipantExpectedValues.findMany({
|
||||
where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sourceId),
|
||||
});
|
||||
|
||||
// Build lookup: (externalId ?? name) → source EV, only for rows that have
|
||||
|
|
@ -251,7 +251,7 @@ export async function cloneSportsSeason(
|
|||
const newParticipants =
|
||||
sourceParticipants.length > 0
|
||||
? await tx
|
||||
.insert(schema.participants)
|
||||
.insert(schema.seasonParticipants)
|
||||
.values(
|
||||
sourceParticipants.map((p: typeof sourceParticipants[number]) => ({
|
||||
sportsSeasonId: newSeason.id,
|
||||
|
|
@ -288,7 +288,7 @@ export async function cloneSportsSeason(
|
|||
.filter((r): r is NonNullable<typeof r> => r !== null);
|
||||
|
||||
if (evRows.length > 0) {
|
||||
await tx.insert(schema.participantExpectedValues).values(evRows);
|
||||
await tx.insert(schema.seasonParticipantExpectedValues).values(evRows);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import { participantSurfaceElos, participants } from "~/database/schema";
|
||||
import { seasonParticipantSurfaceElos, seasonParticipants } from "~/database/schema";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
|
||||
export type CourtSurface = "hard" | "clay" | "grass";
|
||||
|
|
@ -38,7 +38,7 @@ export interface SurfaceEloInput {
|
|||
|
||||
/**
|
||||
* Get all surface Elo records for a sports season, joined with participant names.
|
||||
* Returns one record per participant (participants with no Elo record are excluded).
|
||||
* Returns one record per participant (seasonParticipants with no Elo record are excluded).
|
||||
*/
|
||||
export async function getSurfaceElosForSeason(
|
||||
sportsSeasonId: string
|
||||
|
|
@ -46,26 +46,26 @@ export async function getSurfaceElosForSeason(
|
|||
const db = database();
|
||||
const rows = await db
|
||||
.select({
|
||||
id: participantSurfaceElos.id,
|
||||
participantId: participantSurfaceElos.participantId,
|
||||
sportsSeasonId: participantSurfaceElos.sportsSeasonId,
|
||||
worldRanking: participantSurfaceElos.worldRanking,
|
||||
eloHard: participantSurfaceElos.eloHard,
|
||||
eloClay: participantSurfaceElos.eloClay,
|
||||
eloGrass: participantSurfaceElos.eloGrass,
|
||||
updatedAt: participantSurfaceElos.updatedAt,
|
||||
participantName: participants.name,
|
||||
id: seasonParticipantSurfaceElos.id,
|
||||
participantId: seasonParticipantSurfaceElos.participantId,
|
||||
sportsSeasonId: seasonParticipantSurfaceElos.sportsSeasonId,
|
||||
worldRanking: seasonParticipantSurfaceElos.worldRanking,
|
||||
eloHard: seasonParticipantSurfaceElos.eloHard,
|
||||
eloClay: seasonParticipantSurfaceElos.eloClay,
|
||||
eloGrass: seasonParticipantSurfaceElos.eloGrass,
|
||||
updatedAt: seasonParticipantSurfaceElos.updatedAt,
|
||||
participantName: seasonParticipants.name,
|
||||
})
|
||||
.from(participantSurfaceElos)
|
||||
.innerJoin(participants, eq(participantSurfaceElos.participantId, participants.id))
|
||||
.where(eq(participantSurfaceElos.sportsSeasonId, sportsSeasonId))
|
||||
.orderBy(participants.name);
|
||||
.from(seasonParticipantSurfaceElos)
|
||||
.innerJoin(seasonParticipants, eq(seasonParticipantSurfaceElos.participantId, seasonParticipants.id))
|
||||
.where(eq(seasonParticipantSurfaceElos.sportsSeasonId, sportsSeasonId))
|
||||
.orderBy(seasonParticipants.name);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert surface Elo ratings for a batch of participants.
|
||||
* Upsert surface Elo ratings for a batch of seasonParticipants.
|
||||
* Uses INSERT ... ON CONFLICT DO UPDATE so all three surface columns are
|
||||
* overwritten atomically — the admin always submits all three values.
|
||||
*/
|
||||
|
|
@ -77,7 +77,7 @@ export async function batchUpsertSurfaceElos(
|
|||
const now = new Date();
|
||||
|
||||
await db
|
||||
.insert(participantSurfaceElos)
|
||||
.insert(seasonParticipantSurfaceElos)
|
||||
.values(
|
||||
inputs.map(({ participantId, sportsSeasonId, worldRanking, eloHard, eloClay, eloGrass }) => ({
|
||||
participantId,
|
||||
|
|
@ -90,7 +90,7 @@ export async function batchUpsertSurfaceElos(
|
|||
}))
|
||||
)
|
||||
.onConflictDoUpdate({
|
||||
target: [participantSurfaceElos.participantId, participantSurfaceElos.sportsSeasonId],
|
||||
target: [seasonParticipantSurfaceElos.participantId, seasonParticipantSurfaceElos.sportsSeasonId],
|
||||
set: {
|
||||
worldRanking: sql`excluded.world_ranking`,
|
||||
eloHard: sql`excluded.elo_hard`,
|
||||
|
|
@ -111,14 +111,14 @@ export async function getSurfaceEloMap(
|
|||
const db = database();
|
||||
const rows = await db
|
||||
.select({
|
||||
participantId: participantSurfaceElos.participantId,
|
||||
worldRanking: participantSurfaceElos.worldRanking,
|
||||
eloHard: participantSurfaceElos.eloHard,
|
||||
eloClay: participantSurfaceElos.eloClay,
|
||||
eloGrass: participantSurfaceElos.eloGrass,
|
||||
participantId: seasonParticipantSurfaceElos.participantId,
|
||||
worldRanking: seasonParticipantSurfaceElos.worldRanking,
|
||||
eloHard: seasonParticipantSurfaceElos.eloHard,
|
||||
eloClay: seasonParticipantSurfaceElos.eloClay,
|
||||
eloGrass: seasonParticipantSurfaceElos.eloGrass,
|
||||
})
|
||||
.from(participantSurfaceElos)
|
||||
.where(eq(participantSurfaceElos.sportsSeasonId, sportsSeasonId));
|
||||
.from(seasonParticipantSurfaceElos)
|
||||
.where(eq(seasonParticipantSurfaceElos.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
return new Map(rows.map((r) => [r.participantId, {
|
||||
worldRanking: r.worldRanking,
|
||||
|
|
|
|||
|
|
@ -230,8 +230,8 @@ export async function getRecentTeamScoreEvents(
|
|||
|
||||
const participantNameById = new Map<string, string>();
|
||||
if (allParticipantIds.length > 0) {
|
||||
const participantRows = await db.query.participants.findMany({
|
||||
where: inArray(schema.participants.id, allParticipantIds),
|
||||
const participantRows = await db.query.seasonParticipants.findMany({
|
||||
where: inArray(schema.seasonParticipants.id, allParticipantIds),
|
||||
columns: { id: true, name: true },
|
||||
});
|
||||
for (const p of participantRows) {
|
||||
|
|
|
|||
61
app/models/tournament-result.ts
Normal file
61
app/models/tournament-result.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { eq, and, asc } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type TournamentResult = typeof schema.tournamentResults.$inferSelect;
|
||||
export type NewTournamentResult = typeof schema.tournamentResults.$inferInsert;
|
||||
|
||||
export async function upsertTournamentResult(
|
||||
data: NewTournamentResult
|
||||
): Promise<TournamentResult> {
|
||||
if (!data.tournamentId || !data.participantId) {
|
||||
throw new Error("tournamentId and participantId are required");
|
||||
}
|
||||
|
||||
const db = database();
|
||||
const [result] = await db
|
||||
.insert(schema.tournamentResults)
|
||||
.values(data)
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.tournamentResults.tournamentId,
|
||||
schema.tournamentResults.participantId,
|
||||
],
|
||||
set: {
|
||||
placement: data.placement,
|
||||
rawScore: data.rawScore,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getTournamentResults(
|
||||
tournamentId: string
|
||||
): Promise<TournamentResult[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.select()
|
||||
.from(schema.tournamentResults)
|
||||
.where(eq(schema.tournamentResults.tournamentId, tournamentId))
|
||||
.orderBy(asc(schema.tournamentResults.placement));
|
||||
}
|
||||
|
||||
export async function getTournamentResultByParticipant(
|
||||
tournamentId: string,
|
||||
participantId: string
|
||||
): Promise<TournamentResult | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.tournamentResults)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.tournamentResults.tournamentId, tournamentId),
|
||||
eq(schema.tournamentResults.participantId, participantId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
94
app/models/tournament.ts
Normal file
94
app/models/tournament.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { eq, and, asc } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type Tournament = typeof schema.tournaments.$inferSelect;
|
||||
export type NewTournament = typeof schema.tournaments.$inferInsert;
|
||||
export type TournamentStatus = Tournament["status"];
|
||||
|
||||
export async function createTournament(
|
||||
data: NewTournament
|
||||
): Promise<Tournament> {
|
||||
const db = database();
|
||||
const [tournament] = await db
|
||||
.insert(schema.tournaments)
|
||||
.values(data)
|
||||
.returning();
|
||||
return tournament;
|
||||
}
|
||||
|
||||
export async function getTournamentById(
|
||||
id: string
|
||||
): Promise<Tournament | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.tournaments)
|
||||
.where(eq(schema.tournaments.id, id))
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findTournamentsBySport(
|
||||
sportId: string
|
||||
): Promise<Tournament[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.select()
|
||||
.from(schema.tournaments)
|
||||
.where(eq(schema.tournaments.sportId, sportId))
|
||||
.orderBy(asc(schema.tournaments.year), asc(schema.tournaments.startsAt));
|
||||
}
|
||||
|
||||
export async function findTournamentBySportNameYear(
|
||||
sportId: string,
|
||||
name: string,
|
||||
year: number
|
||||
): Promise<Tournament | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.tournaments)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.tournaments.sportId, sportId),
|
||||
eq(schema.tournaments.name, name),
|
||||
eq(schema.tournaments.year, year)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function upsertTournament(
|
||||
data: NewTournament
|
||||
): Promise<Tournament> {
|
||||
if (!data.sportId || !data.name || !data.year) {
|
||||
throw new Error("sportId, name, and year are required for upsert");
|
||||
}
|
||||
|
||||
const existing = await findTournamentBySportNameYear(
|
||||
data.sportId,
|
||||
data.name,
|
||||
data.year
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return await createTournament(data);
|
||||
}
|
||||
|
||||
export async function updateTournamentStatus(
|
||||
id: string,
|
||||
status: TournamentStatus
|
||||
): Promise<Tournament> {
|
||||
const db = database();
|
||||
const [tournament] = await db
|
||||
.update(schema.tournaments)
|
||||
.set({ status, updatedAt: new Date() })
|
||||
.where(eq(schema.tournaments.id, id))
|
||||
.returning();
|
||||
return tournament;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { logger } from "~/lib/logger";
|
|||
import { findAllSports } from "~/models/sport";
|
||||
import { findAllSportsSeasons } from "~/models/sports-season";
|
||||
import { findAllSeasonTemplates } from "~/models/season-template";
|
||||
import { countAllParticipants } from "~/models/participant";
|
||||
import { countAllParticipants } from "~/models/season-participant";
|
||||
import { countAllParticipantEVs } from "~/models/participant-expected-value";
|
||||
import { importSportsDataFromJSON } from "~/utils/sports-data-sync.server";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Route } from './+types/admin.sports-seasons.$id.elo-ratings';
|
|||
|
||||
import { logger } from '~/lib/logger';
|
||||
import { findSportsSeasonById, updateSportsSeason } from '~/models/sports-season';
|
||||
import { findParticipantsBySportsSeasonId } from '~/models/participant';
|
||||
import { findParticipantsBySportsSeasonId } from '~/models/season-participant';
|
||||
import {
|
||||
getAllParticipantEVsForSeason,
|
||||
batchSaveSourceElos,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId.bracket";
|
||||
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/season-participant";
|
||||
import { getScoringEventById, updateScoringEvent } from "~/models/scoring-event";
|
||||
import {
|
||||
findPlayoffMatchesByEventId,
|
||||
|
|
@ -595,9 +595,9 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
// the "never un-finalize" guard in upsertParticipantResult.
|
||||
const db = database();
|
||||
await db
|
||||
.delete(schema.participantResults)
|
||||
.delete(schema.seasonParticipantResults)
|
||||
.where(
|
||||
eq(schema.participantResults.sportsSeasonId, event.sportsSeasonId)
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, event.sportsSeasonId)
|
||||
);
|
||||
|
||||
// Replay each completed match in bracket order (earlier rounds first).
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Route } from './+types/admin.sports-seasons.$id.events.$eventId.cs
|
|||
|
||||
import { findSportsSeasonById } from '~/models/sports-season';
|
||||
import { getScoringEventById } from '~/models/scoring-event';
|
||||
import { findParticipantsBySportsSeasonId } from '~/models/participant';
|
||||
import { findParticipantsBySportsSeasonId } from '~/models/season-participant';
|
||||
import {
|
||||
getCs2StageResultsForEvent,
|
||||
upsertCs2StageAssignments,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Route } from "./+types/admin.sports-seasons.$id.events.$eventId";
|
||||
import { logger } from "~/lib/logger";
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId, createParticipant } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId, createParticipant } from "~/models/season-participant";
|
||||
import {
|
||||
getScoringEventById,
|
||||
completeScoringEvent,
|
||||
|
|
@ -134,7 +134,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
// This marks them as "competed, earned nothing" so simulations skip this event.
|
||||
const allParticipants = await findParticipantsBySportsSeasonId(params.id);
|
||||
const existingResults = await getEventResults(params.eventId);
|
||||
const existingIds = new Set(existingResults.map((r) => r.participantId));
|
||||
const existingIds = new Set(existingResults.map((r) => r.seasonParticipantId));
|
||||
const missingIds = findParticipantsWithoutResults(
|
||||
allParticipants.map((p) => p.id),
|
||||
existingIds
|
||||
|
|
@ -373,7 +373,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
|
||||
try {
|
||||
const existingResults = await getEventResults(params.eventId);
|
||||
const existingIds = new Set(existingResults.map((r) => r.participantId));
|
||||
const existingIds = new Set(existingResults.map((r) => r.seasonParticipantId));
|
||||
const newResults = filterNewResults(incoming, existingIds);
|
||||
|
||||
if (newResults.length > 0) {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export default function EventResults({
|
|||
|
||||
// Create a map of participants with results for easy lookup
|
||||
const participantResultsMap = new Map(
|
||||
results.map((r: { participant: { id: string } }) => [r.participant.id, r])
|
||||
results.map((r: { seasonParticipant: { id: string } }) => [r.seasonParticipant.id, r])
|
||||
);
|
||||
|
||||
// Get participants without results
|
||||
|
|
@ -419,7 +419,7 @@ export default function EventResults({
|
|||
participants={participants}
|
||||
sportsSeasonId={sportsSeason.id}
|
||||
existingResultParticipantIds={
|
||||
new Set(results.map((r: { participant: { id: string } }) => r.participant.id))
|
||||
new Set(results.map((r: { seasonParticipant: { id: string } }) => r.seasonParticipant.id))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -576,7 +576,7 @@ export default function EventResults({
|
|||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{result.participant.name}</TableCell>
|
||||
<TableCell>{result.seasonParticipant.name}</TableCell>
|
||||
{event.isQualifyingEvent && event.isComplete && (
|
||||
<TableCell className="text-right">
|
||||
{result.qualifyingPointsAwarded ? (
|
||||
|
|
@ -620,7 +620,7 @@ export default function EventResults({
|
|||
variant="ghost"
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
if (!confirm(`Delete result for ${result.participant.name}?`)) {
|
||||
if (!confirm(`Delete result for ${result.seasonParticipant.name}?`)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Route } from "./+types/admin.sports-seasons.$id.expected-values";
|
||||
import { logger } from "~/lib/logger";
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/season-participant";
|
||||
import {
|
||||
batchUpsertParticipantEVs,
|
||||
getAllParticipantEVsForSeason
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Route } from './+types/admin.sports-seasons.$id.futures-odds';
|
|||
|
||||
import { logger } from '~/lib/logger';
|
||||
import { findSportsSeasonById, updateSportsSeason } from '~/models/sports-season';
|
||||
import { findParticipantsBySportsSeasonId } from '~/models/participant';
|
||||
import { findParticipantsBySportsSeasonId } from '~/models/season-participant';
|
||||
import { getAllParticipantEVsForSeason, batchSaveSourceOdds, batchUpsertParticipantEVs } from '~/models/participant-expected-value';
|
||||
import { batchUpsertParticipantEvSnapshots } from '~/models/ev-snapshot';
|
||||
import { getSimulator, type SimulatorType } from '~/services/simulations/registry';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Route } from './+types/admin.sports-seasons.$id.golf-skills';
|
|||
|
||||
import { logger } from '~/lib/logger';
|
||||
import { findSportsSeasonById, updateSportsSeason } from '~/models/sports-season';
|
||||
import { findParticipantsBySportsSeasonId, findParticipantByName, createParticipant } from '~/models/participant';
|
||||
import { findParticipantsBySportsSeasonId, findParticipantByName, createParticipant } from '~/models/season-participant';
|
||||
import { batchUpsertParticipantEVs } from '~/models/participant-expected-value';
|
||||
import { batchUpsertParticipantEvSnapshots } from '~/models/ev-snapshot';
|
||||
import { getGolfSkillsForSeason, batchUpsertGolfSkills } from '~/models/golf-skills';
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
createParticipant,
|
||||
deleteParticipant,
|
||||
updateParticipant,
|
||||
} from "~/models/participant";
|
||||
} from "~/models/season-participant";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Route } from "./+types/admin.sports-seasons.$id.regular-standings"
|
|||
|
||||
import { logger } from "~/lib/logger";
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/season-participant";
|
||||
import { getRegularSeasonStandings, upsertManualStanding } from "~/models/regular-season-standings";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import type { Route } from "./+types/admin.sports-seasons.$id.simulate";
|
|||
import { findSportsSeasonById, updateSportsSeason } from "~/models/sports-season";
|
||||
import { batchUpsertParticipantEVs } from "~/models/participant-expected-value";
|
||||
import { batchUpsertParticipantEvSnapshots } from "~/models/ev-snapshot";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/season-participant";
|
||||
import { getSimulator, type SimulatorType } from "~/services/simulations/registry";
|
||||
import { calculateEV } from "~/services/ev-calculator";
|
||||
import { DEFAULT_SCORING_RULES } from "~/lib/scoring-types";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Route } from "./+types/admin.sports-seasons.$id.standings";
|
|||
|
||||
import { logger } from "~/lib/logger";
|
||||
import { findSportsSeasonById } from "~/models/sports-season";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId } from "~/models/season-participant";
|
||||
import { getSeasonResults, upsertSeasonResultsBulk } from "~/models/participant-season-result";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Route } from './+types/admin.sports-seasons.$id.surface-elo';
|
|||
|
||||
import { logger } from '~/lib/logger';
|
||||
import { findSportsSeasonById, updateSportsSeason } from '~/models/sports-season';
|
||||
import { findParticipantsBySportsSeasonId, findParticipantByName, createParticipant } from '~/models/participant';
|
||||
import { findParticipantsBySportsSeasonId, findParticipantByName, createParticipant } from '~/models/season-participant';
|
||||
import {
|
||||
batchUpsertParticipantEVs,
|
||||
} from '~/models/participant-expected-value';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
getPendingStandingsMappings,
|
||||
deletePendingStandingsMapping,
|
||||
} from "~/models/pending-standings-mappings";
|
||||
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/season-participant";
|
||||
import { getLastSyncedAt, upsertRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/models/participant", () => ({
|
||||
vi.mock("~/models/season-participant", () => ({
|
||||
findParticipantsBySportsSeasonId: vi.fn(),
|
||||
findParticipantByName: vi.fn(),
|
||||
createParticipant: vi.fn(),
|
||||
|
|
@ -19,7 +19,7 @@ vi.mock("~/lib/logger", () => ({
|
|||
import {
|
||||
findParticipantByName,
|
||||
updateParticipant,
|
||||
} from "~/models/participant";
|
||||
} from "~/models/season-participant";
|
||||
import { action } from "~/routes/admin.sports-seasons.$id.participants";
|
||||
|
||||
const mockParams = { id: "season-1" };
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ vi.mock("~/models/draft-pick", () => ({
|
|||
getDraftPicksWithSports: vi.fn(),
|
||||
getTeamDraftPicksWithSports: vi.fn(),
|
||||
}));
|
||||
vi.mock("~/models/participant", () => ({
|
||||
vi.mock("~/models/season-participant", () => ({
|
||||
getParticipantsForSeasonWithSports: vi.fn(),
|
||||
}));
|
||||
vi.mock("~/models/season-sport", () => ({
|
||||
|
|
@ -138,7 +138,7 @@ describe("draft.force-manual-pick action", () => {
|
|||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
|
||||
|
||||
const { getParticipantsForSeasonWithSports } = await import("~/models/participant");
|
||||
const { getParticipantsForSeasonWithSports } = await import("~/models/season-participant");
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
||||
|
||||
const { getSeasonSportsSimple } = await import("~/models/season-sport");
|
||||
|
|
@ -159,7 +159,7 @@ describe("draft.force-manual-pick action", () => {
|
|||
seasons: { findFirst: vi.fn() },
|
||||
commissioners: { findFirst: vi.fn() },
|
||||
draftPicks: { findFirst: vi.fn() },
|
||||
participants: { findFirst: vi.fn() },
|
||||
seasonParticipants: { findFirst: vi.fn() },
|
||||
draftSlots: { findMany: vi.fn() },
|
||||
draftTimers: { findFirst: vi.fn() },
|
||||
},
|
||||
|
|
@ -178,7 +178,7 @@ describe("draft.force-manual-pick action", () => {
|
|||
userId: COMMISSIONER_ID,
|
||||
});
|
||||
mockDb.query.draftPicks.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.participants.findFirst.mockResolvedValue(mockParticipant);
|
||||
mockDb.query.seasonParticipants.findFirst.mockResolvedValue(mockParticipant);
|
||||
mockDb.query.draftSlots.findMany.mockResolvedValue(mockDraftSlots);
|
||||
mockDb.query.draftTimers.findFirst.mockResolvedValue({
|
||||
id: "timer-1",
|
||||
|
|
@ -277,7 +277,7 @@ describe("draft.force-manual-pick action", () => {
|
|||
});
|
||||
|
||||
it("returns 404 when the participant does not exist", async () => {
|
||||
mockDb.query.participants.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.seasonParticipants.findFirst.mockResolvedValue(null);
|
||||
|
||||
const response = await action({ request: defaultPickRequest(), params: {}, context: ctx });
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ vi.mock("~/models/draft-pick", () => ({
|
|||
getDraftPicksWithSports: vi.fn(),
|
||||
getTeamDraftPicksWithSports: vi.fn(),
|
||||
}));
|
||||
vi.mock("~/models/participant", () => ({
|
||||
vi.mock("~/models/season-participant", () => ({
|
||||
getParticipantsForSeasonWithSports: vi.fn(),
|
||||
}));
|
||||
vi.mock("~/models/season-sport", () => ({
|
||||
|
|
@ -141,7 +141,7 @@ describe("draft.force-manual-pick action — timer mode behavior", () => {
|
|||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
|
||||
|
||||
const { getParticipantsForSeasonWithSports } = await import("~/models/participant");
|
||||
const { getParticipantsForSeasonWithSports } = await import("~/models/season-participant");
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
||||
|
||||
const { getSeasonSportsSimple } = await import("~/models/season-sport");
|
||||
|
|
@ -163,7 +163,7 @@ describe("draft.force-manual-pick action — timer mode behavior", () => {
|
|||
findFirst: vi.fn().mockResolvedValue({ id: "c-1", userId: COMMISSIONER_ID }),
|
||||
},
|
||||
draftPicks: { findFirst: vi.fn().mockResolvedValue(null) },
|
||||
participants: { findFirst: vi.fn().mockResolvedValue(mockParticipant) },
|
||||
seasonParticipants: { findFirst: vi.fn().mockResolvedValue(mockParticipant) },
|
||||
draftSlots: { findMany: vi.fn().mockResolvedValue(mockDraftSlots) },
|
||||
draftTimers: {
|
||||
findFirst: vi.fn().mockResolvedValue({ id: "timer-1", seasonId: SEASON_ID, teamId: TEAM_ID, timeRemaining: 75 }),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ vi.mock("~/models/draft-pick", () => ({
|
|||
getDraftPicksWithSports: vi.fn(),
|
||||
getTeamDraftPicksWithSports: vi.fn(),
|
||||
}));
|
||||
vi.mock("~/models/participant", () => ({
|
||||
vi.mock("~/models/season-participant", () => ({
|
||||
getParticipantsForSeasonWithSports: vi.fn(),
|
||||
}));
|
||||
vi.mock("~/models/season-sport", () => ({
|
||||
|
|
@ -138,7 +138,7 @@ describe("draft.make-pick action — timer mode behavior", () => {
|
|||
vi.mocked(getDraftPicksWithSports).mockResolvedValue([]);
|
||||
vi.mocked(getTeamDraftPicksWithSports).mockResolvedValue([]);
|
||||
|
||||
const { getParticipantsForSeasonWithSports } = await import("~/models/participant");
|
||||
const { getParticipantsForSeasonWithSports } = await import("~/models/season-participant");
|
||||
vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([]);
|
||||
|
||||
const { getSeasonSportsSimple } = await import("~/models/season-sport");
|
||||
|
|
@ -158,7 +158,7 @@ describe("draft.make-pick action — timer mode behavior", () => {
|
|||
seasons: { findFirst: vi.fn() },
|
||||
commissioners: { findFirst: vi.fn().mockResolvedValue(null) },
|
||||
draftPicks: { findFirst: vi.fn().mockResolvedValue(null) },
|
||||
participants: { findFirst: vi.fn().mockResolvedValue(mockParticipant) },
|
||||
seasonParticipants: { findFirst: vi.fn().mockResolvedValue(mockParticipant) },
|
||||
draftSlots: { findMany: vi.fn().mockResolvedValue(mockDraftSlots) },
|
||||
draftTimers: { findFirst: vi.fn().mockResolvedValue({ id: "timer-1", timeRemaining: 75 }) },
|
||||
draftQueue: { findMany: vi.fn().mockResolvedValue([]) },
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { eq, and, sql } from "drizzle-orm";
|
|||
import { isUserAdmin } from "~/models/user";
|
||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||
import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/season-participant";
|
||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||
import { calculatePickInfo, checkAndTriggerNextAutodraft } from "~/models/draft-utils";
|
||||
import { logCommissionerAction } from "~/models/audit-log";
|
||||
|
|
@ -87,8 +87,8 @@ export async function action(args: ActionFunctionArgs) {
|
|||
}
|
||||
|
||||
// Get participant details
|
||||
const participant = await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, participantId),
|
||||
const participant = await db.query.seasonParticipants.findFirst({
|
||||
where: eq(schema.seasonParticipants.id, participantId),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { eq, and, sql } from "drizzle-orm";
|
|||
import { isUserAdmin } from "~/models/user";
|
||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||
import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||
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";
|
||||
|
|
@ -96,8 +96,8 @@ export async function action(args: ActionFunctionArgs) {
|
|||
}
|
||||
|
||||
// Get participant details
|
||||
const participant = await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, participantId),
|
||||
const participant = await db.query.seasonParticipants.findFirst({
|
||||
where: eq(schema.seasonParticipants.id, participantId),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { eq, and } from "drizzle-orm";
|
|||
import { isCommissioner } from "~/models/commissioner";
|
||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
||||
import { getDraftPicksWithSports, getTeamDraftPicksWithSports } from "~/models/draft-pick";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/participant";
|
||||
import { getParticipantsForSeasonWithSports } from "~/models/season-participant";
|
||||
import { getSeasonSportsSimple } from "~/models/season-sport";
|
||||
import { logCommissionerAction } from "~/models/audit-log";
|
||||
import { getSocketIO } from "../../../server/socket";
|
||||
|
|
@ -78,8 +78,8 @@ export async function action(args: ActionFunctionArgs) {
|
|||
}
|
||||
}
|
||||
|
||||
const participant = await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, participantId),
|
||||
const participant = await db.query.seasonParticipants.findFirst({
|
||||
where: eq(schema.seasonParticipants.id, participantId),
|
||||
with: {
|
||||
sportsSeason: { with: { sport: true } },
|
||||
},
|
||||
|
|
@ -128,8 +128,8 @@ export async function action(args: ActionFunctionArgs) {
|
|||
}
|
||||
|
||||
// Fetch old participant name for the audit log (before overwriting the pick)
|
||||
const oldParticipant = await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, oldParticipantId),
|
||||
const oldParticipant = await db.query.seasonParticipants.findFirst({
|
||||
where: eq(schema.seasonParticipants.id, oldParticipantId),
|
||||
});
|
||||
|
||||
// Update the pick in-place
|
||||
|
|
|
|||
|
|
@ -51,13 +51,13 @@ export async function loader({ params }: LoaderFunctionArgs) {
|
|||
round: schema.draftPicks.round,
|
||||
teamName: schema.teams.name,
|
||||
teamOwnerId: schema.teams.ownerId,
|
||||
participantName: schema.participants.name,
|
||||
participantName: schema.seasonParticipants.name,
|
||||
sport: schema.sports.name,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
|
||||
.innerJoin(schema.participants, eq(schema.draftPicks.participantId, schema.participants.id))
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.seasonParticipants, eq(schema.draftPicks.participantId, schema.seasonParticipants.id))
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(eq(schema.draftPicks.seasonId, seasonId))
|
||||
.orderBy(schema.draftPicks.pickNumber);
|
||||
|
|
|
|||
|
|
@ -76,19 +76,19 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
round: schema.draftPicks.round,
|
||||
pickInRound: schema.draftPicks.pickInRound,
|
||||
team: schema.teams,
|
||||
participant: schema.participants,
|
||||
participant: schema.seasonParticipants,
|
||||
sport: schema.sports,
|
||||
scoringPattern: schema.sportsSeasons.scoringPattern,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
|
||||
.innerJoin(
|
||||
schema.participants,
|
||||
eq(schema.draftPicks.participantId, schema.participants.id)
|
||||
schema.seasonParticipants,
|
||||
eq(schema.draftPicks.participantId, schema.seasonParticipants.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sportsSeasons,
|
||||
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
)
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(eq(schema.draftPicks.seasonId, seasonId))
|
||||
|
|
@ -104,13 +104,13 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
const results = await db
|
||||
.select({
|
||||
participantId: schema.participantResults.participantId,
|
||||
sportsSeasonId: schema.participantResults.sportsSeasonId,
|
||||
finalPosition: schema.participantResults.finalPosition,
|
||||
isPartialScore: schema.participantResults.isPartialScore,
|
||||
participantId: schema.seasonParticipantResults.participantId,
|
||||
sportsSeasonId: schema.seasonParticipantResults.sportsSeasonId,
|
||||
finalPosition: schema.seasonParticipantResults.finalPosition,
|
||||
isPartialScore: schema.seasonParticipantResults.isPartialScore,
|
||||
})
|
||||
.from(schema.participantResults)
|
||||
.where(inArray(schema.participantResults.participantId, participantIds));
|
||||
.from(schema.seasonParticipantResults)
|
||||
.where(inArray(schema.seasonParticipantResults.participantId, participantIds));
|
||||
|
||||
const resultByParticipant = new Map(results.map((r) => [r.participantId, r]));
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { getTeamWatchlist } from "~/models/watchlist";
|
|||
import { findSeasonWithTeamsAndLeague } from "~/models/season";
|
||||
import { findDraftSlotsBySeasonId } from "~/models/draft-slot";
|
||||
import { getDraftPicksForSeason } from "~/models/draft-pick";
|
||||
import { getDraftParticipants } from "~/models/participant";
|
||||
import { getDraftParticipants } from "~/models/season-participant";
|
||||
import { getSeasonTimers } from "~/models/draft-timer";
|
||||
import { getSeasonAutodraftSettings } from "~/models/autodraft-settings";
|
||||
import { hasCommissionerRecord } from "~/models/commissioner";
|
||||
|
|
|
|||
|
|
@ -233,15 +233,15 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
|
||||
// Fetch group-stage losers and all participant results for bracket scoring
|
||||
const [eliminatedResults, allResults] = await Promise.all([
|
||||
db.query.participantResults.findMany({
|
||||
db.query.seasonParticipantResults.findMany({
|
||||
where: and(
|
||||
eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
||||
eq(schema.participantResults.finalPosition, 0)
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId),
|
||||
eq(schema.seasonParticipantResults.finalPosition, 0)
|
||||
),
|
||||
with: { participant: true },
|
||||
}),
|
||||
db.query.participantResults.findMany({
|
||||
where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId),
|
||||
db.query.seasonParticipantResults.findMany({
|
||||
where: eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -252,8 +252,8 @@ export async function previewProbabilityUpdate(
|
|||
const results = await findParticipantResultsBySportsSeasonId(sportsSeasonId);
|
||||
|
||||
// Get all existing EVs with participant names
|
||||
const existingEVs = await db.query.participantExpectedValues.findMany({
|
||||
where: eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId),
|
||||
const existingEVs = await db.query.seasonParticipantExpectedValues.findMany({
|
||||
where: eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId),
|
||||
with: {
|
||||
participant: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ import { database } from "~/database/context";
|
|||
|
||||
const mockDb = {
|
||||
query: {
|
||||
participants: { findMany: vi.fn() },
|
||||
seasonParticipants: { findMany: vi.fn() },
|
||||
scoringEvents: { findFirst: vi.fn() },
|
||||
tournamentGroups: { findMany: vi.fn() },
|
||||
playoffMatches: { findMany: vi.fn() },
|
||||
|
|
@ -98,7 +98,7 @@ const mockDb = {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.mocked(database).mockReturnValue(mockDb as never);
|
||||
mockDb.query.participants.findMany.mockReset();
|
||||
mockDb.query.seasonParticipants.findMany.mockReset();
|
||||
mockDb.query.scoringEvents.findFirst.mockReset();
|
||||
mockDb.query.tournamentGroups.findMany.mockReset();
|
||||
mockDb.query.playoffMatches.findMany.mockReset();
|
||||
|
|
@ -109,7 +109,7 @@ beforeEach(() => {
|
|||
|
||||
describe("WorldCupSimulator", () => {
|
||||
it("throws when no participants are found", async () => {
|
||||
mockDb.query.participants.findMany.mockResolvedValue([]);
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue([]);
|
||||
mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
|
@ -120,7 +120,7 @@ describe("WorldCupSimulator", () => {
|
|||
|
||||
it("returns one result per participant", async () => {
|
||||
const participants = makeParticipants(48);
|
||||
mockDb.query.participants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
|
@ -136,7 +136,7 @@ describe("WorldCupSimulator", () => {
|
|||
|
||||
it("column sums for champion, runner-up, 3rd, 4th are each ≈1.0", async () => {
|
||||
const participants = makeParticipants(48);
|
||||
mockDb.query.participants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
|
@ -157,7 +157,7 @@ describe("WorldCupSimulator", () => {
|
|||
|
||||
it("probabilities are all non-negative", async () => {
|
||||
const participants = makeParticipants(48);
|
||||
mockDb.query.participants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
|
@ -178,7 +178,7 @@ describe("WorldCupSimulator", () => {
|
|||
it("SF losers land in 3rd or 4th, never 1st or 2nd", async () => {
|
||||
// Set up 8 participants (small bracket, 2 groups of 4)
|
||||
const participants = makeParticipants(8);
|
||||
mockDb.query.participants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
|
@ -200,7 +200,7 @@ describe("WorldCupSimulator", () => {
|
|||
|
||||
it("a team with pre-completed group stage result is fixed in simulation", async () => {
|
||||
const participants = makeParticipants(48);
|
||||
mockDb.query.participants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.scoringEvents.findFirst.mockResolvedValue({ id: "event-1" });
|
||||
|
||||
// One group fully complete: p0 wins everything, p3 loses everything
|
||||
|
|
@ -254,7 +254,7 @@ describe("WorldCupSimulator", () => {
|
|||
|
||||
it("probFifth through probEighth are equal for each participant (QF losers split evenly)", async () => {
|
||||
const participants = makeParticipants(48);
|
||||
mockDb.query.participants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue(participants);
|
||||
mockDb.query.scoringEvents.findFirst.mockResolvedValue(null);
|
||||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
|
|
|||
|
|
@ -198,16 +198,16 @@ export class AFLSimulator implements Simulator {
|
|||
// 1. Load participants, DB Elo, and standings in parallel.
|
||||
const [participantRows, evRows, standings] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId)),
|
||||
db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
getRegularSeasonStandings(sportsSeasonId),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ export class AutoRacingSimulator implements Simulator {
|
|||
const db = database();
|
||||
|
||||
// 1. Load all participants for this sports season
|
||||
const participants = await db.query.participants.findMany({
|
||||
where: eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
||||
const participants = await db.query.seasonParticipants.findMany({
|
||||
where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
|
||||
});
|
||||
|
||||
if (participants.length === 0) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
*/
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import { participantExpectedValues } from "~/database/schema";
|
||||
import { seasonParticipantExpectedValues } from "~/database/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { simulateBracket } from "~/services/bracket-simulator";
|
||||
import { convertFuturesToElo } from "~/services/probability-engine";
|
||||
|
|
@ -24,12 +24,12 @@ export class BracketSimulator implements Simulator {
|
|||
// Load all participants with their current EVs (which hold probability distributions)
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: participantExpectedValues.participantId,
|
||||
probFirst: participantExpectedValues.probFirst,
|
||||
sourceOdds: participantExpectedValues.sourceOdds,
|
||||
participantId: seasonParticipantExpectedValues.participantId,
|
||||
probFirst: seasonParticipantExpectedValues.probFirst,
|
||||
sourceOdds: seasonParticipantExpectedValues.sourceOdds,
|
||||
})
|
||||
.from(participantExpectedValues)
|
||||
.where(eq(participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(seasonParticipantExpectedValues)
|
||||
.where(eq(seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (evRows.length === 0) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -433,9 +433,9 @@ export class CSMajorSimulator implements Simulator {
|
|||
|
||||
// 1. Load all participants for this sports season.
|
||||
const allParticipants = await db
|
||||
.select({ id: schema.participants.id })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.select({ id: schema.seasonParticipants.id })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
const participantIds = allParticipants.map((p) => p.id);
|
||||
|
||||
|
|
@ -446,12 +446,12 @@ export class CSMajorSimulator implements Simulator {
|
|||
// 2. Load Elo ratings and world rankings from participantExpectedValues.
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
worldRanking: schema.participantExpectedValues.worldRanking,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
worldRanking: schema.seasonParticipantExpectedValues.worldRanking,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
const eloMap = new Map<string, { elo: number; rank: number }>();
|
||||
for (const row of evRows) {
|
||||
|
|
@ -508,7 +508,7 @@ export class CSMajorSimulator implements Simulator {
|
|||
if (completedEventIds.length > 0) {
|
||||
const actualResults = await db
|
||||
.select({
|
||||
participantId: schema.eventResults.participantId,
|
||||
participantId: schema.eventResults.seasonParticipantId,
|
||||
qualifyingPointsAwarded: schema.eventResults.qualifyingPointsAwarded,
|
||||
})
|
||||
.from(schema.eventResults)
|
||||
|
|
|
|||
|
|
@ -220,12 +220,12 @@ export class DartsSimulator implements Simulator {
|
|||
// 3. Load Elo ratings and world rankings.
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
worldRanking: schema.participantExpectedValues.worldRanking,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
worldRanking: schema.seasonParticipantExpectedValues.worldRanking,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
const eloMap = new Map<string, number>();
|
||||
const rankingMap = new Map<string, number>();
|
||||
|
|
@ -452,9 +452,9 @@ export class DartsSimulator implements Simulator {
|
|||
db: ReturnType<typeof database>
|
||||
): Promise<SimulationResult[]> {
|
||||
const allParticipants = await db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (allParticipants.length < 2) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -185,9 +185,9 @@ export class GolfSimulator implements Simulator {
|
|||
// Load participants, skills, QP config, and scoring events in parallel.
|
||||
const [allParticipants, skillsMap, qpConfigRows, events] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.participants.id })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
|
||||
.select({ id: schema.seasonParticipants.id })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId)),
|
||||
getGolfSkillsMap(sportsSeasonId),
|
||||
getQPConfig(sportsSeasonId),
|
||||
db.query.scoringEvents.findMany({
|
||||
|
|
@ -226,7 +226,7 @@ export class GolfSimulator implements Simulator {
|
|||
if (completedEventIds.length > 0) {
|
||||
const actualResults = await db
|
||||
.select({
|
||||
participantId: schema.eventResults.participantId,
|
||||
participantId: schema.eventResults.seasonParticipantId,
|
||||
qualifyingPointsAwarded: schema.eventResults.qualifyingPointsAwarded,
|
||||
})
|
||||
.from(schema.eventResults)
|
||||
|
|
|
|||
|
|
@ -264,9 +264,9 @@ export class LLWSSimulator implements Simulator {
|
|||
|
||||
// 1. Load all participants.
|
||||
const participants = await db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name, externalId: schema.participants.externalId })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name, externalId: schema.seasonParticipants.externalId })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (participants.length !== US_TEAM_COUNT + INTL_TEAM_COUNT) {
|
||||
throw new Error(
|
||||
|
|
@ -278,11 +278,11 @@ export class LLWSSimulator implements Simulator {
|
|||
// 2. Load championship futures odds.
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
const rawOddsMap = new Map<string, number>();
|
||||
for (const row of evRows) {
|
||||
|
|
|
|||
|
|
@ -409,9 +409,9 @@ export class MLBSimulator implements Simulator {
|
|||
|
||||
// 1. Load all participants for this sports season.
|
||||
const participantRows = await db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (participantRows.length === 0) {
|
||||
throw new Error(
|
||||
|
|
@ -452,12 +452,12 @@ export class MLBSimulator implements Simulator {
|
|||
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
const participantIdSet = new Set(participantIds);
|
||||
const oddsRows = evRows.filter(
|
||||
|
|
|
|||
|
|
@ -312,16 +312,16 @@ export class NBASimulator implements Simulator {
|
|||
// Load participant names and sourceElo values in parallel.
|
||||
const [participantRows, evRows] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId)),
|
||||
db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
]);
|
||||
|
||||
const nameById = new Map(participantRows.map((r) => [r.id, r.name]));
|
||||
|
|
@ -472,17 +472,17 @@ export class NBASimulator implements Simulator {
|
|||
|
||||
const [participantRows, standings, evRows] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId)),
|
||||
getRegularSeasonStandings(sportsSeasonId),
|
||||
db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
]);
|
||||
|
||||
if (participantRows.length === 0) {
|
||||
|
|
|
|||
|
|
@ -214,9 +214,9 @@ export class NCAAFootballSimulator implements Simulator {
|
|||
|
||||
// 1. Load all participants for this sports season.
|
||||
const participants = await db
|
||||
.select({ id: schema.participants.id })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.select({ id: schema.seasonParticipants.id })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (participants.length < BRACKET_SIZE) {
|
||||
throw new Error(
|
||||
|
|
@ -228,12 +228,12 @@ export class NCAAFootballSimulator implements Simulator {
|
|||
// 2. Load Elo/FPI ratings and optional futures odds in a single query.
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
// Build Elo and raw odds maps in a single pass.
|
||||
const eloFromDb = new Map<string, number>();
|
||||
|
|
|
|||
|
|
@ -494,9 +494,9 @@ export class NCAAMSimulator implements Simulator {
|
|||
|
||||
// 6. Load participant names from DB; build net rating lookup by ID.
|
||||
const participantRows = await db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(inArray(schema.participants.id, participantIds));
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(inArray(schema.seasonParticipants.id, participantIds));
|
||||
|
||||
if (participantRows.length < participantIds.length) {
|
||||
const foundIds = new Set(participantRows.map((r) => r.id));
|
||||
|
|
|
|||
|
|
@ -457,9 +457,9 @@ export class NCAAWSimulator implements Simulator {
|
|||
|
||||
// 6. Load participant names from DB; build Barthag lookup by ID.
|
||||
const participantRows = await db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(inArray(schema.participants.id, participantIds));
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(inArray(schema.seasonParticipants.id, participantIds));
|
||||
|
||||
if (participantRows.length < participantIds.length) {
|
||||
const foundIds = new Set(participantRows.map((r) => r.id));
|
||||
|
|
|
|||
|
|
@ -296,8 +296,8 @@ export class NFLSimulator implements Simulator {
|
|||
const db = database();
|
||||
|
||||
// Load participants
|
||||
const participants = await db.query.participants.findMany({
|
||||
where: eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
||||
const participants = await db.query.seasonParticipants.findMany({
|
||||
where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
|
||||
});
|
||||
|
||||
if (participants.length === 0) {
|
||||
|
|
@ -307,12 +307,12 @@ export class NFLSimulator implements Simulator {
|
|||
// Load EV rows (sourceElo + sourceOdds)
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
// Build Elo map: prefer sourceElo, fall back to converting sourceOdds
|
||||
const eloMap = new Map<string, number>();
|
||||
|
|
|
|||
|
|
@ -278,18 +278,18 @@ export class NHLSimulator implements Simulator {
|
|||
// 1. Load participants, standings, and sourceElo values in parallel.
|
||||
const [participantRows, standings, evRows] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId)),
|
||||
getRegularSeasonStandings(sportsSeasonId),
|
||||
db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
]);
|
||||
|
||||
if (participantRows.length === 0) {
|
||||
|
|
@ -620,17 +620,17 @@ export class NHLSimulator implements Simulator {
|
|||
// Load all participants and EV data in parallel.
|
||||
const [participantRows, evRows] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId)),
|
||||
db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
]);
|
||||
|
||||
const allParticipantIds = participantRows.map((r) => r.id);
|
||||
|
|
|
|||
|
|
@ -251,11 +251,11 @@ export class SnookerSimulator implements Simulator {
|
|||
// 3. Load Elo ratings from participantExpectedValues.
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
// Build Elo map; fall back to 1500 for any participant with no stored rating.
|
||||
const eloMap = new Map<string, number>();
|
||||
|
|
@ -453,9 +453,9 @@ export class SnookerSimulator implements Simulator {
|
|||
db: ReturnType<typeof database>
|
||||
): Promise<SimulationResult[]> {
|
||||
const allParticipants = await db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (allParticipants.length < 17) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -262,9 +262,9 @@ export class TennisSimulator implements Simulator {
|
|||
|
||||
// 1. Load all participants for this sports season.
|
||||
const allParticipants = await db
|
||||
.select({ id: schema.participants.id })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
||||
.select({ id: schema.seasonParticipants.id })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
if (allParticipants.length < 128) {
|
||||
throw new Error(
|
||||
|
|
@ -306,7 +306,7 @@ export class TennisSimulator implements Simulator {
|
|||
if (completedEventIds.length > 0) {
|
||||
const actualResults = await db
|
||||
.select({
|
||||
participantId: schema.eventResults.participantId,
|
||||
participantId: schema.eventResults.seasonParticipantId,
|
||||
qualifyingPointsAwarded: schema.eventResults.qualifyingPointsAwarded,
|
||||
})
|
||||
.from(schema.eventResults)
|
||||
|
|
|
|||
|
|
@ -147,11 +147,11 @@ export class UCLSimulator implements Simulator {
|
|||
// 5. Load futures odds from participantExpectedValues.
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
const evMap = new Map(evRows.map((r) => [r.participantId, r]));
|
||||
|
||||
|
|
|
|||
|
|
@ -159,18 +159,18 @@ export class WNBASimulator implements Simulator {
|
|||
// 1. Load participants, standings, and futures odds in parallel.
|
||||
const [participantRows, standings, evRows] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.participants.id, name: schema.participants.name })
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId)),
|
||||
.select({ id: schema.seasonParticipants.id, name: schema.seasonParticipants.name })
|
||||
.from(schema.seasonParticipants)
|
||||
.where(eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId)),
|
||||
getRegularSeasonStandings(sportsSeasonId),
|
||||
db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)),
|
||||
]);
|
||||
|
||||
if (participantRows.length === 0) {
|
||||
|
|
|
|||
|
|
@ -313,8 +313,8 @@ export class WorldCupSimulator implements Simulator {
|
|||
const db = database();
|
||||
|
||||
// 1. Load all participants for this season
|
||||
const participantRows = await db.query.participants.findMany({
|
||||
where: eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
||||
const participantRows = await db.query.seasonParticipants.findMany({
|
||||
where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
|
||||
});
|
||||
|
||||
if (participantRows.length === 0) {
|
||||
|
|
@ -327,12 +327,12 @@ export class WorldCupSimulator implements Simulator {
|
|||
// 2. Load stored ratings (sourceElo from Elo page, sourceOdds from futures page)
|
||||
const evRows = await db
|
||||
.select({
|
||||
participantId: schema.participantExpectedValues.participantId,
|
||||
sourceOdds: schema.participantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.participantExpectedValues.sourceElo,
|
||||
participantId: schema.seasonParticipantExpectedValues.participantId,
|
||||
sourceOdds: schema.seasonParticipantExpectedValues.sourceOdds,
|
||||
sourceElo: schema.seasonParticipantExpectedValues.sourceElo,
|
||||
})
|
||||
.from(schema.participantExpectedValues)
|
||||
.where(eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
.from(schema.seasonParticipantExpectedValues)
|
||||
.where(eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId));
|
||||
|
||||
const evMap = new Map(evRows.map((r) => [r.participantId, r]));
|
||||
const participantSet = new Set(participantIds);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { database } from "~/database/context";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as schema from "~/database/schema";
|
||||
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/participant";
|
||||
import { findParticipantsBySportsSeasonId, updateParticipant } from "~/models/season-participant";
|
||||
import { upsertRegularSeasonStandings } from "~/models/regular-season-standings";
|
||||
import { upsertPendingStandingsMappings } from "~/models/pending-standings-mappings";
|
||||
import { findMatchingTeamName } from "~/lib/normalize-team-name";
|
||||
|
|
|
|||
|
|
@ -115,16 +115,16 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
|
|||
with: { sport: true },
|
||||
orderBy: (s, { desc, asc }) => [desc(s.year), asc(s.name)],
|
||||
}),
|
||||
db.query.participants.findMany({
|
||||
db.query.seasonParticipants.findMany({
|
||||
with: { sportsSeason: { with: { sport: true } } },
|
||||
orderBy: (p, { asc }) => [asc(p.name)],
|
||||
}),
|
||||
db.query.participantExpectedValues.findMany({
|
||||
db.query.seasonParticipantExpectedValues.findMany({
|
||||
with: {
|
||||
participant: { with: { sportsSeason: { with: { sport: true } } } },
|
||||
},
|
||||
}),
|
||||
db.query.participantResults.findMany({
|
||||
db.query.seasonParticipantResults.findMany({
|
||||
with: {
|
||||
participant: { with: { sportsSeason: { with: { sport: true } } } },
|
||||
},
|
||||
|
|
@ -238,7 +238,7 @@ export async function importSportsDataFromJSON(
|
|||
// automatically removed when participants is deleted.
|
||||
await tx.delete(schema.seasonTemplateSports);
|
||||
await tx.delete(schema.seasonTemplates);
|
||||
await tx.delete(schema.participants); // cascades to EVs + results
|
||||
await tx.delete(schema.seasonParticipants); // cascades to EVs + results
|
||||
await tx.delete(schema.seasonSports);
|
||||
await tx.delete(schema.sportsSeasons);
|
||||
await tx.delete(schema.sports);
|
||||
|
|
@ -358,28 +358,28 @@ export async function importSportsDataFromJSON(
|
|||
continue;
|
||||
}
|
||||
|
||||
const existing = await tx.query.participants.findFirst({
|
||||
const existing = await tx.query.seasonParticipants.findFirst({
|
||||
where: and(
|
||||
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
||||
eq(schema.participants.name, participant.name)
|
||||
eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
|
||||
eq(schema.seasonParticipants.name, participant.name)
|
||||
),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update all mutable fields (fixes: merge mode was silently skipping)
|
||||
await tx
|
||||
.update(schema.participants)
|
||||
.update(schema.seasonParticipants)
|
||||
.set({
|
||||
shortName: participant.shortName,
|
||||
externalId: participant.externalId,
|
||||
expectedValue: String(participant.expectedValue ?? 0),
|
||||
})
|
||||
.where(eq(schema.participants.id, existing.id));
|
||||
.where(eq(schema.seasonParticipants.id, existing.id));
|
||||
participantIdMap.set(`${sportsSeasonId}:${participant.name}`, existing.id);
|
||||
updated++;
|
||||
} else {
|
||||
const [created_] = await tx
|
||||
.insert(schema.participants)
|
||||
.insert(schema.seasonParticipants)
|
||||
.values({
|
||||
sportsSeasonId,
|
||||
name: participant.name,
|
||||
|
|
@ -413,10 +413,10 @@ export async function importSportsDataFromJSON(
|
|||
continue;
|
||||
}
|
||||
|
||||
const existingEV = await tx.query.participantExpectedValues.findFirst({
|
||||
const existingEV = await tx.query.seasonParticipantExpectedValues.findFirst({
|
||||
where: and(
|
||||
eq(schema.participantExpectedValues.participantId, participantId),
|
||||
eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
eq(schema.seasonParticipantExpectedValues.participantId, participantId),
|
||||
eq(schema.seasonParticipantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -446,25 +446,25 @@ export async function importSportsDataFromJSON(
|
|||
if (existingEV) {
|
||||
if (mode === "merge") {
|
||||
await tx
|
||||
.update(schema.participantExpectedValues)
|
||||
.update(schema.seasonParticipantExpectedValues)
|
||||
.set({ ...evValues, updatedAt: new Date() })
|
||||
.where(eq(schema.participantExpectedValues.id, existingEV.id));
|
||||
.where(eq(schema.seasonParticipantExpectedValues.id, existingEV.id));
|
||||
await tx
|
||||
.update(schema.participants)
|
||||
.update(schema.seasonParticipants)
|
||||
.set({ expectedValue: ev.expectedValue })
|
||||
.where(eq(schema.participants.id, participantId));
|
||||
.where(eq(schema.seasonParticipants.id, participantId));
|
||||
updated++;
|
||||
}
|
||||
} else {
|
||||
await tx.insert(schema.participantExpectedValues).values({
|
||||
await tx.insert(schema.seasonParticipantExpectedValues).values({
|
||||
participantId,
|
||||
sportsSeasonId,
|
||||
...evValues,
|
||||
});
|
||||
await tx
|
||||
.update(schema.participants)
|
||||
.update(schema.seasonParticipants)
|
||||
.set({ expectedValue: ev.expectedValue })
|
||||
.where(eq(schema.participants.id, participantId));
|
||||
.where(eq(schema.seasonParticipants.id, participantId));
|
||||
created++;
|
||||
}
|
||||
}
|
||||
|
|
@ -489,10 +489,10 @@ export async function importSportsDataFromJSON(
|
|||
continue;
|
||||
}
|
||||
|
||||
const existingResult = await tx.query.participantResults.findFirst({
|
||||
const existingResult = await tx.query.seasonParticipantResults.findFirst({
|
||||
where: and(
|
||||
eq(schema.participantResults.participantId, participantId),
|
||||
eq(schema.participantResults.sportsSeasonId, sportsSeasonId)
|
||||
eq(schema.seasonParticipantResults.participantId, participantId),
|
||||
eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId)
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -505,13 +505,13 @@ export async function importSportsDataFromJSON(
|
|||
if (existingResult) {
|
||||
if (mode === "merge") {
|
||||
await tx
|
||||
.update(schema.participantResults)
|
||||
.update(schema.seasonParticipantResults)
|
||||
.set({ ...resultValues, updatedAt: new Date() })
|
||||
.where(eq(schema.participantResults.id, existingResult.id));
|
||||
.where(eq(schema.seasonParticipantResults.id, existingResult.id));
|
||||
updated++;
|
||||
}
|
||||
} else {
|
||||
await tx.insert(schema.participantResults).values({
|
||||
await tx.insert(schema.seasonParticipantResults).values({
|
||||
participantId,
|
||||
sportsSeasonId,
|
||||
...resultValues,
|
||||
|
|
|
|||
|
|
@ -150,6 +150,12 @@ export const simulatorTypeEnum = pgEnum("simulator_type", [
|
|||
"llws_bracket",
|
||||
]);
|
||||
|
||||
export const tournamentStatusEnum = pgEnum("tournament_status", [
|
||||
"scheduled",
|
||||
"in_progress",
|
||||
"completed",
|
||||
]);
|
||||
|
||||
export const playoffMatchGameStatusEnum = pgEnum("playoff_match_game_status", [
|
||||
"scheduled",
|
||||
"complete",
|
||||
|
|
@ -249,7 +255,7 @@ export const draftPicks = pgTable("draft_picks", {
|
|||
.references(() => teams.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
pickNumber: integer("pick_number").notNull(),
|
||||
round: integer("round").notNull(),
|
||||
pickInRound: integer("pick_in_round").notNull(),
|
||||
|
|
@ -272,7 +278,7 @@ export const draftQueue = pgTable("draft_queue", {
|
|||
.references(() => teams.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
queuePosition: integer("queue_position").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
|
|
@ -288,7 +294,7 @@ export const watchlist = pgTable("watchlist", {
|
|||
.references(() => teams.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
}, (t) => ({
|
||||
uniqueWatch: uniqueIndex("watchlist_season_team_participant_unique").on(t.seasonId, t.teamId, t.participantId),
|
||||
|
|
@ -373,11 +379,14 @@ export const sportsSeasons = pgTable("sports_seasons", {
|
|||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const participants = pgTable("participants", {
|
||||
export const seasonParticipants = pgTable("season_participants", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id").references(() => participants.id, {
|
||||
onDelete: "restrict",
|
||||
}),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
shortName: varchar("short_name", { length: 100 }),
|
||||
externalId: varchar("external_id", { length: 255 }),
|
||||
|
|
@ -389,6 +398,96 @@ export const participants = pgTable("participants", {
|
|||
uniqueIndex("participants_sports_season_name_unique").on(table.sportsSeasonId, table.name),
|
||||
]);
|
||||
|
||||
export const participants = pgTable(
|
||||
"participants",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
sportId: uuid("sport_id")
|
||||
.notNull()
|
||||
.references(() => sports.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
externalKey: varchar("external_key", { length: 255 }),
|
||||
metadata: jsonb("metadata"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
uniqueSportName: uniqueIndex("participants_sport_name_unique").on(
|
||||
t.sportId,
|
||||
t.name,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const tournaments = pgTable(
|
||||
"tournaments",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
sportId: uuid("sport_id")
|
||||
.notNull()
|
||||
.references(() => sports.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
year: integer("year").notNull(),
|
||||
startsAt: timestamp("starts_at"),
|
||||
endsAt: timestamp("ends_at"),
|
||||
surface: varchar("surface", { length: 50 }), // tennis only: hard | clay | grass
|
||||
location: varchar("location", { length: 255 }),
|
||||
status: tournamentStatusEnum("status").notNull().default("scheduled"),
|
||||
externalKey: varchar("external_key", { length: 255 }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
uniqueSportNameYear: uniqueIndex("tournaments_sport_name_year_unique").on(
|
||||
t.sportId,
|
||||
t.name,
|
||||
t.year,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const tournamentResults = pgTable(
|
||||
"tournament_results",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tournamentId: uuid("tournament_id")
|
||||
.notNull()
|
||||
.references(() => tournaments.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
placement: integer("placement"),
|
||||
rawScore: decimal("raw_score", { precision: 10, scale: 2 }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
uniqueTournamentParticipant: uniqueIndex(
|
||||
"tournament_results_tournament_participant_unique",
|
||||
).on(t.tournamentId, t.participantId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const participantSurfaceElos = pgTable(
|
||||
"participant_surface_elos",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
worldRanking: integer("world_ranking"),
|
||||
eloHard: integer("elo_hard"),
|
||||
eloClay: integer("elo_clay"),
|
||||
eloGrass: integer("elo_grass"),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
uniqueParticipant: uniqueIndex("participant_surface_elos_participant_unique").on(
|
||||
t.participantId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const seasonTemplateSports = pgTable("season_template_sports", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
templateId: uuid("template_id")
|
||||
|
|
@ -411,11 +510,11 @@ export const seasonSports = pgTable("season_sports", {
|
|||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const participantResults = pgTable("participant_results", {
|
||||
export const seasonParticipantResults = pgTable("season_participant_results", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -435,6 +534,9 @@ export const scoringEvents = pgTable("scoring_events", {
|
|||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
tournamentId: uuid("tournament_id").references(() => tournaments.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
name: varchar("name", { length: 255 }).notNull(), // "2024 Masters", "Super Bowl LIX", etc.
|
||||
eventDate: date("event_date"),
|
||||
eventStartsAt: timestamp("event_starts_at", { withTimezone: true }),
|
||||
|
|
@ -460,9 +562,9 @@ export const eventResults = pgTable("event_results", {
|
|||
scoringEventId: uuid("scoring_event_id")
|
||||
.notNull()
|
||||
.references(() => scoringEvents.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
seasonParticipantId: uuid("season_participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
// Placement in this specific event
|
||||
placement: integer("placement"),
|
||||
// For qualifying events: QP awarded in this event
|
||||
|
|
@ -483,10 +585,10 @@ export const playoffMatches = pgTable("playoff_matches", {
|
|||
.references(() => scoringEvents.id, { onDelete: "cascade" }),
|
||||
round: varchar("round", { length: 50 }).notNull(), // "Quarterfinals", "Semifinals", "Finals"
|
||||
matchNumber: integer("match_number").notNull(), // 1, 2, 3, 4 (for QF); 1, 2 (for SF); 1 (for F)
|
||||
participant1Id: uuid("participant1_id").references(() => participants.id, { onDelete: "set null" }),
|
||||
participant2Id: uuid("participant2_id").references(() => participants.id, { onDelete: "set null" }),
|
||||
winnerId: uuid("winner_id").references(() => participants.id, { onDelete: "set null" }),
|
||||
loserId: uuid("loser_id").references(() => participants.id, { onDelete: "set null" }),
|
||||
participant1Id: uuid("participant1_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||||
participant2Id: uuid("participant2_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||||
winnerId: uuid("winner_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||||
loserId: uuid("loser_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||||
isComplete: boolean("is_complete").notNull().default(false),
|
||||
// Optional detailed data
|
||||
participant1Score: decimal("participant1_score", { precision: 10, scale: 2 }),
|
||||
|
|
@ -510,7 +612,7 @@ export const playoffMatchGames = pgTable("playoff_match_games", {
|
|||
status: playoffMatchGameStatusEnum("status").notNull().default("scheduled"),
|
||||
participant1Score: decimal("participant1_score", { precision: 10, scale: 2 }),
|
||||
participant2Score: decimal("participant2_score", { precision: 10, scale: 2 }),
|
||||
winnerId: uuid("winner_id").references(() => participants.id, { onDelete: "set null" }),
|
||||
winnerId: uuid("winner_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||||
notes: text("notes"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
|
|
@ -524,7 +626,7 @@ export const playoffMatchOdds = pgTable("playoff_match_odds", {
|
|||
.references(() => playoffMatches.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
moneylineOdds: integer("moneyline_odds"), // American format e.g. -110, +150
|
||||
impliedProbability: decimal("implied_probability", { precision: 6, scale: 4 }),
|
||||
oddsSource: varchar("odds_source", { length: 100 }),
|
||||
|
|
@ -534,11 +636,11 @@ export const playoffMatchOdds = pgTable("playoff_match_odds", {
|
|||
});
|
||||
|
||||
// Aggregated participant qualifying points (for qualifying_points sports)
|
||||
export const participantQualifyingTotals = pgTable("participant_qualifying_totals", {
|
||||
export const seasonParticipantQualifyingTotals = pgTable("season_participant_qualifying_totals", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -639,11 +741,11 @@ export const teamStandingsSnapshots = pgTable("team_standings_snapshots", {
|
|||
}));
|
||||
|
||||
// Expected value tracking (sports-season-specific)
|
||||
export const participantExpectedValues = pgTable("participant_expected_values", {
|
||||
export const seasonParticipantExpectedValues = pgTable("season_participant_expected_values", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -688,7 +790,7 @@ export const tournamentGroupMembers = pgTable("tournament_group_members", {
|
|||
.references(() => tournamentGroups.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
eliminated: boolean("eliminated").notNull().default(false),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
|
|
@ -701,10 +803,10 @@ export const groupStageMatches = pgTable("group_stage_matches", {
|
|||
.references(() => tournamentGroups.id, { onDelete: "cascade" }),
|
||||
participant1Id: uuid("participant1_id")
|
||||
.notNull()
|
||||
.references(() => participants.id),
|
||||
.references(() => seasonParticipants.id),
|
||||
participant2Id: uuid("participant2_id")
|
||||
.notNull()
|
||||
.references(() => participants.id),
|
||||
.references(() => seasonParticipants.id),
|
||||
participant1Score: integer("participant1_score"),
|
||||
participant2Score: integer("participant2_score"),
|
||||
isComplete: boolean("is_complete").notNull().default(false),
|
||||
|
|
@ -723,7 +825,7 @@ export const participantSeasonResults = pgTable("participant_season_results", {
|
|||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -737,7 +839,7 @@ export const participantEvSnapshots = pgTable("participant_ev_snapshots", {
|
|||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -783,6 +885,8 @@ export const teamEvSnapshots = pgTable("team_ev_snapshots", {
|
|||
// Relations
|
||||
export const sportsRelations = relations(sports, ({ many }) => ({
|
||||
sportsSeasons: many(sportsSeasons),
|
||||
tournaments: many(tournaments),
|
||||
participants: many(participants),
|
||||
}));
|
||||
|
||||
export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({
|
||||
|
|
@ -790,23 +894,70 @@ export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) =
|
|||
fields: [sportsSeasons.sportId],
|
||||
references: [sports.id],
|
||||
}),
|
||||
participants: many(participants),
|
||||
participants: many(seasonParticipants),
|
||||
seasonTemplateSports: many(seasonTemplateSports),
|
||||
seasonSports: many(seasonSports),
|
||||
participantResults: many(participantResults),
|
||||
participantResults: many(seasonParticipantResults),
|
||||
regularSeasonStandings: many(regularSeasonStandings),
|
||||
pendingStandingsMappings: many(pendingStandingsMappings),
|
||||
}));
|
||||
|
||||
export const participantsRelations = relations(participants, ({ one, many }) => ({
|
||||
export const seasonParticipantsRelations = relations(seasonParticipants, ({ one, many }) => ({
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participants.sportsSeasonId],
|
||||
fields: [seasonParticipants.sportsSeasonId],
|
||||
references: [sportsSeasons.id],
|
||||
}),
|
||||
results: many(participantResults),
|
||||
canonicalParticipant: one(participants, {
|
||||
fields: [seasonParticipants.participantId],
|
||||
references: [participants.id],
|
||||
}),
|
||||
results: many(seasonParticipantResults),
|
||||
regularSeasonStandings: many(regularSeasonStandings),
|
||||
}));
|
||||
|
||||
export const participantsRelations = relations(participants, ({ one, many }) => ({
|
||||
sport: one(sports, {
|
||||
fields: [participants.sportId],
|
||||
references: [sports.id],
|
||||
}),
|
||||
seasonParticipants: many(seasonParticipants),
|
||||
tournamentResults: many(tournamentResults),
|
||||
surfaceElo: one(participantSurfaceElos, {
|
||||
fields: [participants.id],
|
||||
references: [participantSurfaceElos.participantId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const tournamentsRelations = relations(tournaments, ({ one, many }) => ({
|
||||
sport: one(sports, {
|
||||
fields: [tournaments.sportId],
|
||||
references: [sports.id],
|
||||
}),
|
||||
scoringEvents: many(scoringEvents),
|
||||
tournamentResults: many(tournamentResults),
|
||||
}));
|
||||
|
||||
export const tournamentResultsRelations = relations(tournamentResults, ({ one }) => ({
|
||||
tournament: one(tournaments, {
|
||||
fields: [tournamentResults.tournamentId],
|
||||
references: [tournaments.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
fields: [tournamentResults.participantId],
|
||||
references: [participants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const participantSurfaceElosRelations = relations(
|
||||
participantSurfaceElos,
|
||||
({ one }) => ({
|
||||
participant: one(participants, {
|
||||
fields: [participantSurfaceElos.participantId],
|
||||
references: [participants.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const seasonTemplatesRelations = relations(seasonTemplates, ({ many }) => ({
|
||||
seasonTemplateSports: many(seasonTemplateSports),
|
||||
seasons: many(seasons),
|
||||
|
|
@ -847,13 +998,13 @@ export const seasonSportsRelations = relations(seasonSports, ({ one }) => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
export const participantResultsRelations = relations(participantResults, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
fields: [participantResults.participantId],
|
||||
references: [participants.id],
|
||||
export const seasonParticipantResultsRelations = relations(seasonParticipantResults, ({ one }) => ({
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [seasonParticipantResults.participantId],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participantResults.sportsSeasonId],
|
||||
fields: [seasonParticipantResults.sportsSeasonId],
|
||||
references: [sportsSeasons.id],
|
||||
}),
|
||||
}));
|
||||
|
|
@ -889,9 +1040,9 @@ export const draftPicksRelations = relations(draftPicks, ({ one }) => ({
|
|||
fields: [draftPicks.teamId],
|
||||
references: [teams.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [draftPicks.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -904,9 +1055,9 @@ export const draftQueueRelations = relations(draftQueue, ({ one }) => ({
|
|||
fields: [draftQueue.teamId],
|
||||
references: [teams.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [draftQueue.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -919,9 +1070,9 @@ export const watchlistRelations = relations(watchlist, ({ one }) => ({
|
|||
fields: [watchlist.teamId],
|
||||
references: [teams.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [watchlist.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -943,6 +1094,10 @@ export const scoringEventsRelations = relations(scoringEvents, ({ one, many }) =
|
|||
fields: [scoringEvents.sportsSeasonId],
|
||||
references: [sportsSeasons.id],
|
||||
}),
|
||||
tournament: one(tournaments, {
|
||||
fields: [scoringEvents.tournamentId],
|
||||
references: [tournaments.id],
|
||||
}),
|
||||
eventResults: many(eventResults),
|
||||
playoffMatches: many(playoffMatches),
|
||||
tournamentGroups: many(tournamentGroups),
|
||||
|
|
@ -954,9 +1109,9 @@ export const eventResultsRelations = relations(eventResults, ({ one }) => ({
|
|||
fields: [eventResults.scoringEventId],
|
||||
references: [scoringEvents.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
fields: [eventResults.participantId],
|
||||
references: [participants.id],
|
||||
seasonParticipant: one(seasonParticipants, {
|
||||
fields: [eventResults.seasonParticipantId],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -965,21 +1120,21 @@ export const playoffMatchesRelations = relations(playoffMatches, ({ one, many })
|
|||
fields: [playoffMatches.scoringEventId],
|
||||
references: [scoringEvents.id],
|
||||
}),
|
||||
participant1: one(participants, {
|
||||
participant1: one(seasonParticipants, {
|
||||
fields: [playoffMatches.participant1Id],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
participant2: one(participants, {
|
||||
participant2: one(seasonParticipants, {
|
||||
fields: [playoffMatches.participant2Id],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
winner: one(participants, {
|
||||
winner: one(seasonParticipants, {
|
||||
fields: [playoffMatches.winnerId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
loser: one(participants, {
|
||||
loser: one(seasonParticipants, {
|
||||
fields: [playoffMatches.loserId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
games: many(playoffMatchGames),
|
||||
odds: many(playoffMatchOdds),
|
||||
|
|
@ -990,9 +1145,9 @@ export const playoffMatchGamesRelations = relations(playoffMatchGames, ({ one })
|
|||
fields: [playoffMatchGames.playoffMatchId],
|
||||
references: [playoffMatches.id],
|
||||
}),
|
||||
winner: one(participants, {
|
||||
winner: one(seasonParticipants, {
|
||||
fields: [playoffMatchGames.winnerId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -1001,19 +1156,19 @@ export const playoffMatchOddsRelations = relations(playoffMatchOdds, ({ one }) =
|
|||
fields: [playoffMatchOdds.playoffMatchId],
|
||||
references: [playoffMatches.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [playoffMatchOdds.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const participantQualifyingTotalsRelations = relations(participantQualifyingTotals, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
fields: [participantQualifyingTotals.participantId],
|
||||
references: [participants.id],
|
||||
export const seasonParticipantQualifyingTotalsRelations = relations(seasonParticipantQualifyingTotals, ({ one }) => ({
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [seasonParticipantQualifyingTotals.participantId],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participantQualifyingTotals.sportsSeasonId],
|
||||
fields: [seasonParticipantQualifyingTotals.sportsSeasonId],
|
||||
references: [sportsSeasons.id],
|
||||
}),
|
||||
}));
|
||||
|
|
@ -1058,21 +1213,21 @@ export const teamStandingsSnapshotsRelations = relations(teamStandingsSnapshots,
|
|||
}),
|
||||
}));
|
||||
|
||||
export const participantExpectedValuesRelations = relations(participantExpectedValues, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
fields: [participantExpectedValues.participantId],
|
||||
references: [participants.id],
|
||||
export const seasonParticipantExpectedValuesRelations = relations(seasonParticipantExpectedValues, ({ one }) => ({
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [seasonParticipantExpectedValues.participantId],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participantExpectedValues.sportsSeasonId],
|
||||
fields: [seasonParticipantExpectedValues.sportsSeasonId],
|
||||
references: [sportsSeasons.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const participantSeasonResultsRelations = relations(participantSeasonResults, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [participantSeasonResults.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participantSeasonResults.sportsSeasonId],
|
||||
|
|
@ -1095,9 +1250,9 @@ export const tournamentGroupMembersRelations = relations(tournamentGroupMembers,
|
|||
fields: [tournamentGroupMembers.tournamentGroupId],
|
||||
references: [tournamentGroups.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [tournamentGroupMembers.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
@ -1106,22 +1261,22 @@ export const groupStageMatchesRelations = relations(groupStageMatches, ({ one })
|
|||
fields: [groupStageMatches.tournamentGroupId],
|
||||
references: [tournamentGroups.id],
|
||||
}),
|
||||
participant1: one(participants, {
|
||||
participant1: one(seasonParticipants, {
|
||||
fields: [groupStageMatches.participant1Id],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
relationName: "groupMatchParticipant1",
|
||||
}),
|
||||
participant2: one(participants, {
|
||||
participant2: one(seasonParticipants, {
|
||||
fields: [groupStageMatches.participant2Id],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
relationName: "groupMatchParticipant2",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const participantEvSnapshotsRelations = relations(participantEvSnapshots, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [participantEvSnapshots.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participantEvSnapshots.sportsSeasonId],
|
||||
|
|
@ -1146,7 +1301,7 @@ export const regularSeasonStandings = pgTable("regular_season_standings", {
|
|||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -1177,9 +1332,9 @@ export const regularSeasonStandings = pgTable("regular_season_standings", {
|
|||
}));
|
||||
|
||||
export const regularSeasonStandingsRelations = relations(regularSeasonStandings, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [regularSeasonStandings.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [regularSeasonStandings.sportsSeasonId],
|
||||
|
|
@ -1216,11 +1371,11 @@ export const pendingStandingsMappingsRelations = relations(pendingStandingsMappi
|
|||
// Stores surface-specific Elo ratings for tennis (and future surface-based sports).
|
||||
// One row per (participantId, sportsSeasonId); nullable per surface.
|
||||
|
||||
export const participantSurfaceElos = pgTable("participant_surface_elos", {
|
||||
export const seasonParticipantSurfaceElos = pgTable("season_participant_surface_elos", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -1234,13 +1389,13 @@ export const participantSurfaceElos = pgTable("participant_surface_elos", {
|
|||
.on(t.participantId, t.sportsSeasonId),
|
||||
}));
|
||||
|
||||
export const participantSurfaceElosRelations = relations(participantSurfaceElos, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
fields: [participantSurfaceElos.participantId],
|
||||
references: [participants.id],
|
||||
export const seasonParticipantSurfaceElosRelations = relations(seasonParticipantSurfaceElos, ({ one }) => ({
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [seasonParticipantSurfaceElos.participantId],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participantSurfaceElos.sportsSeasonId],
|
||||
fields: [seasonParticipantSurfaceElos.sportsSeasonId],
|
||||
references: [sportsSeasons.id],
|
||||
}),
|
||||
}));
|
||||
|
|
@ -1255,7 +1410,7 @@ export const participantGolfSkills = pgTable("participant_golf_skills", {
|
|||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
sportsSeasonId: uuid("sports_season_id")
|
||||
.notNull()
|
||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||
|
|
@ -1275,9 +1430,9 @@ export const participantGolfSkills = pgTable("participant_golf_skills", {
|
|||
}));
|
||||
|
||||
export const participantGolfSkillsRelations = relations(participantGolfSkills, ({ one }) => ({
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [participantGolfSkills.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
sportsSeason: one(sportsSeasons, {
|
||||
fields: [participantGolfSkills.sportsSeasonId],
|
||||
|
|
@ -1302,7 +1457,7 @@ export const cs2MajorStageResults = pgTable("cs2_major_stage_results", {
|
|||
.references(() => scoringEvents.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||||
stageEntry: integer("stage_entry").notNull(),
|
||||
stageEliminated: integer("stage_eliminated"),
|
||||
stageEliminatedWins: integer("stage_eliminated_wins"),
|
||||
|
|
@ -1319,9 +1474,9 @@ export const cs2MajorStageResultsRelations = relations(cs2MajorStageResults, ({
|
|||
fields: [cs2MajorStageResults.scoringEventId],
|
||||
references: [scoringEvents.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
participant: one(seasonParticipants, {
|
||||
fields: [cs2MajorStageResults.participantId],
|
||||
references: [participants.id],
|
||||
references: [seasonParticipants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
|
|
|||
221
drizzle/0087_small_susan_delgado.sql
Normal file
221
drizzle/0087_small_susan_delgado.sql
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
ALTER TABLE "participant_expected_values" RENAME TO "season_participant_expected_values";--> statement-breakpoint
|
||||
ALTER TABLE "participant_qualifying_totals" RENAME TO "season_participant_qualifying_totals";--> statement-breakpoint
|
||||
ALTER TABLE "participant_results" RENAME TO "season_participant_results";--> statement-breakpoint
|
||||
ALTER TABLE "participant_surface_elos" RENAME TO "season_participant_surface_elos";--> statement-breakpoint
|
||||
ALTER TABLE "participants" RENAME TO "season_participants";--> statement-breakpoint
|
||||
ALTER TABLE "event_results" RENAME COLUMN "participant_id" TO "season_participant_id";--> statement-breakpoint
|
||||
ALTER TABLE "cs2_major_stage_results" DROP CONSTRAINT IF EXISTS "cs2_major_stage_results_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "draft_picks" DROP CONSTRAINT IF EXISTS "draft_picks_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "draft_queue" DROP CONSTRAINT IF EXISTS "draft_queue_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "event_results" DROP CONSTRAINT IF EXISTS "event_results_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "group_stage_matches" DROP CONSTRAINT IF EXISTS "group_stage_matches_participant1_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "group_stage_matches" DROP CONSTRAINT IF EXISTS "group_stage_matches_participant2_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "participant_ev_snapshots" DROP CONSTRAINT IF EXISTS "participant_ev_snapshots_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_expected_values" DROP CONSTRAINT IF EXISTS "participant_expected_values_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_expected_values" DROP CONSTRAINT IF EXISTS "participant_expected_values_sports_season_id_sports_seasons_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "participant_golf_skills" DROP CONSTRAINT IF EXISTS "participant_golf_skills_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_qualifying_totals" DROP CONSTRAINT IF EXISTS "participant_qualifying_totals_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_qualifying_totals" DROP CONSTRAINT IF EXISTS "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_results" DROP CONSTRAINT IF EXISTS "participant_results_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_results" DROP CONSTRAINT IF EXISTS "participant_results_sports_season_id_sports_seasons_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "participant_season_results" DROP CONSTRAINT IF EXISTS "participant_season_results_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_surface_elos" DROP CONSTRAINT IF EXISTS "participant_surface_elos_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participant_surface_elos" DROP CONSTRAINT IF EXISTS "participant_surface_elos_sports_season_id_sports_seasons_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "season_participants" DROP CONSTRAINT IF EXISTS "participants_sports_season_id_sports_seasons_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "playoff_match_games" DROP CONSTRAINT IF EXISTS "playoff_match_games_winner_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "playoff_match_odds" DROP CONSTRAINT IF EXISTS "playoff_match_odds_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "playoff_matches" DROP CONSTRAINT IF EXISTS "playoff_matches_participant1_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "playoff_matches" DROP CONSTRAINT IF EXISTS "playoff_matches_participant2_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "playoff_matches" DROP CONSTRAINT IF EXISTS "playoff_matches_winner_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "playoff_matches" DROP CONSTRAINT IF EXISTS "playoff_matches_loser_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "regular_season_standings" DROP CONSTRAINT IF EXISTS "regular_season_standings_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "tournament_group_members" DROP CONSTRAINT IF EXISTS "tournament_group_members_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "watchlist" DROP CONSTRAINT IF EXISTS "watchlist_participant_id_participants_id_fk";
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "cs2_major_stage_results" ADD CONSTRAINT "cs2_major_stage_results_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "draft_picks" ADD CONSTRAINT "draft_picks_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "draft_queue" ADD CONSTRAINT "draft_queue_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "event_results" ADD CONSTRAINT "event_results_season_participant_id_season_participants_id_fk" FOREIGN KEY ("season_participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "group_stage_matches" ADD CONSTRAINT "group_stage_matches_participant1_id_season_participants_id_fk" FOREIGN KEY ("participant1_id") REFERENCES "public"."season_participants"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "group_stage_matches" ADD CONSTRAINT "group_stage_matches_participant2_id_season_participants_id_fk" FOREIGN KEY ("participant2_id") REFERENCES "public"."season_participants"("id") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "participant_ev_snapshots" ADD CONSTRAINT "participant_ev_snapshots_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_expected_values" ADD CONSTRAINT "season_participant_expected_values_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_expected_values" ADD CONSTRAINT "season_participant_expected_values_sports_season_id_sports_seasons_id_fk" FOREIGN KEY ("sports_season_id") REFERENCES "public"."sports_seasons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "participant_golf_skills" ADD CONSTRAINT "participant_golf_skills_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_qualifying_totals" ADD CONSTRAINT "season_participant_qualifying_totals_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_qualifying_totals" ADD CONSTRAINT "season_participant_qualifying_totals_sports_season_id_sports_seasons_id_fk" FOREIGN KEY ("sports_season_id") REFERENCES "public"."sports_seasons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_results" ADD CONSTRAINT "season_participant_results_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_results" ADD CONSTRAINT "season_participant_results_sports_season_id_sports_seasons_id_fk" FOREIGN KEY ("sports_season_id") REFERENCES "public"."sports_seasons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "participant_season_results" ADD CONSTRAINT "participant_season_results_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_surface_elos" ADD CONSTRAINT "season_participant_surface_elos_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participant_surface_elos" ADD CONSTRAINT "season_participant_surface_elos_sports_season_id_sports_seasons_id_fk" FOREIGN KEY ("sports_season_id") REFERENCES "public"."sports_seasons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participants" ADD CONSTRAINT "season_participants_sports_season_id_sports_seasons_id_fk" FOREIGN KEY ("sports_season_id") REFERENCES "public"."sports_seasons"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "playoff_match_games" ADD CONSTRAINT "playoff_match_games_winner_id_season_participants_id_fk" FOREIGN KEY ("winner_id") REFERENCES "public"."season_participants"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "playoff_match_odds" ADD CONSTRAINT "playoff_match_odds_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "playoff_matches" ADD CONSTRAINT "playoff_matches_participant1_id_season_participants_id_fk" FOREIGN KEY ("participant1_id") REFERENCES "public"."season_participants"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "playoff_matches" ADD CONSTRAINT "playoff_matches_participant2_id_season_participants_id_fk" FOREIGN KEY ("participant2_id") REFERENCES "public"."season_participants"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "playoff_matches" ADD CONSTRAINT "playoff_matches_winner_id_season_participants_id_fk" FOREIGN KEY ("winner_id") REFERENCES "public"."season_participants"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "playoff_matches" ADD CONSTRAINT "playoff_matches_loser_id_season_participants_id_fk" FOREIGN KEY ("loser_id") REFERENCES "public"."season_participants"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "regular_season_standings" ADD CONSTRAINT "regular_season_standings_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "tournament_group_members" ADD CONSTRAINT "tournament_group_members_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "watchlist" ADD CONSTRAINT "watchlist_participant_id_season_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."season_participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
93
drizzle/0088_cheerful_norrin_radd.sql
Normal file
93
drizzle/0088_cheerful_norrin_radd.sql
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
CREATE TYPE "public"."tournament_status" AS ENUM('scheduled', 'in_progress', 'completed');--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "participant_surface_elos" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"participant_id" uuid NOT NULL,
|
||||
"world_ranking" integer,
|
||||
"elo_hard" integer,
|
||||
"elo_clay" integer,
|
||||
"elo_grass" integer,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "participants" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"sport_id" uuid NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"external_key" varchar(255),
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "tournament_results" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"tournament_id" uuid NOT NULL,
|
||||
"participant_id" uuid NOT NULL,
|
||||
"placement" integer,
|
||||
"raw_score" numeric(10, 2),
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "tournaments" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"sport_id" uuid NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"year" integer NOT NULL,
|
||||
"starts_at" timestamp,
|
||||
"ends_at" timestamp,
|
||||
"surface" varchar(50),
|
||||
"location" varchar(255),
|
||||
"status" "tournament_status" DEFAULT 'scheduled' NOT NULL,
|
||||
"external_key" varchar(255),
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "scoring_events" ADD COLUMN "tournament_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "season_participants" ADD COLUMN "participant_id" uuid;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "participant_surface_elos" ADD CONSTRAINT "participant_surface_elos_participant_id_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "participants" ADD CONSTRAINT "participants_sport_id_sports_id_fk" FOREIGN KEY ("sport_id") REFERENCES "public"."sports"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "tournament_results" ADD CONSTRAINT "tournament_results_tournament_id_tournaments_id_fk" FOREIGN KEY ("tournament_id") REFERENCES "public"."tournaments"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "tournament_results" ADD CONSTRAINT "tournament_results_participant_id_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "tournaments" ADD CONSTRAINT "tournaments_sport_id_sports_id_fk" FOREIGN KEY ("sport_id") REFERENCES "public"."sports"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "participant_surface_elos_participant_unique" ON "participant_surface_elos" USING btree ("participant_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "participants_sport_name_unique" ON "participants" USING btree ("sport_id","name");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "tournament_results_tournament_participant_unique" ON "tournament_results" USING btree ("tournament_id","participant_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "tournaments_sport_name_year_unique" ON "tournaments" USING btree ("sport_id","name","year");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "scoring_events" ADD CONSTRAINT "scoring_events_tournament_id_tournaments_id_fk" FOREIGN KEY ("tournament_id") REFERENCES "public"."tournaments"("id") ON DELETE set null ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "season_participants" ADD CONSTRAINT "season_participants_participant_id_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."participants"("id") ON DELETE restrict ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
5221
drizzle/meta/0087_snapshot.json
Normal file
5221
drizzle/meta/0087_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
5688
drizzle/meta/0088_snapshot.json
Normal file
5688
drizzle/meta/0088_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -610,6 +610,20 @@
|
|||
"when": 1777505810235,
|
||||
"tag": "0086_marvelous_infant_terrible",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 87,
|
||||
"version": "7",
|
||||
"when": 1777661299612,
|
||||
"tag": "0087_small_susan_delgado",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 88,
|
||||
"version": "7",
|
||||
"when": 1777666830394,
|
||||
"tag": "0088_cheerful_norrin_radd",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"db:generate": "dotenv -- drizzle-kit generate",
|
||||
"db:migrate": "dotenv -- drizzle-kit migrate",
|
||||
"db:sync-prod": "bash scripts/sync-prod-db.sh",
|
||||
"backfill:canonical": "dotenv -- tsx scripts/backfill-cli.ts",
|
||||
"dev": "NODE_OPTIONS='--import ./instrument.server.mjs' dotenv -- tsx watch server.ts",
|
||||
"start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' node dist/server.js",
|
||||
"start:production": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' node dist/server.js",
|
||||
|
|
|
|||
450
scripts/__tests__/backfill-canonical-layer.test.ts
Normal file
450
scripts/__tests__/backfill-canonical-layer.test.ts
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import { runBackfill } from "../backfill-canonical-layer";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
/**
|
||||
* Fixture rows used across multiple tests.
|
||||
*/
|
||||
const SPORT_ID = "sport-golf";
|
||||
const SEASON_ID = "season-golf-2026";
|
||||
|
||||
const GOLF_SEASON = {
|
||||
id: SEASON_ID,
|
||||
sportId: SPORT_ID,
|
||||
scoringPattern: "qualifying_points",
|
||||
} as const;
|
||||
|
||||
function makeEvent(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
return {
|
||||
id: "event-1",
|
||||
sportsSeasonId: SEASON_ID,
|
||||
tournamentId: null,
|
||||
name: "Masters Tournament",
|
||||
eventDate: "2026-04-09",
|
||||
eventType: "tournament",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeSeasonParticipant(
|
||||
overrides: Partial<Record<string, unknown>> = {},
|
||||
) {
|
||||
return {
|
||||
id: "sp-1",
|
||||
sportsSeasonId: SEASON_ID,
|
||||
participantId: null,
|
||||
name: "Scottie Scheffler",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a fake drizzle db that routes select/insert/update calls based
|
||||
* on the target table. The caller supplies per-table select-result
|
||||
* arrays (one per call to that table's select()). Inserts return the
|
||||
* value they were given, extended with a stub id. Updates are recorded
|
||||
* but otherwise no-op.
|
||||
*/
|
||||
interface TableState {
|
||||
/** Queue of results to return for successive select() calls on this table. */
|
||||
selects?: unknown[][];
|
||||
/** Rows that insert().values().returning() should yield. */
|
||||
insertReturns?: unknown[];
|
||||
/** Incremented on each update() call. */
|
||||
updates?: { count: number };
|
||||
}
|
||||
|
||||
interface FakeDbOptions {
|
||||
tables: Map<unknown, TableState>;
|
||||
/** Records every insert call: tableRef → rows seen. */
|
||||
inserts?: Map<unknown, unknown[]>;
|
||||
/** Records every update call: tableRef → count. */
|
||||
updateCounts?: Map<unknown, number>;
|
||||
}
|
||||
|
||||
function makeFakeDb(opts: FakeDbOptions) {
|
||||
const { tables, inserts, updateCounts } = opts;
|
||||
|
||||
// Each select() starts a new chain that ultimately resolves to the
|
||||
// next queued selects[] entry for the passed table. The chain is a
|
||||
// thenable via .limit/.where resolution.
|
||||
const db = {
|
||||
select: vi.fn().mockImplementation(() => {
|
||||
let boundTable: unknown;
|
||||
const chain: Record<string, unknown> = {
|
||||
from: vi.fn().mockImplementation((table: unknown) => {
|
||||
boundTable = table;
|
||||
return chain;
|
||||
}),
|
||||
where: vi.fn().mockImplementation(() => chain),
|
||||
limit: vi.fn().mockImplementation(() => chain),
|
||||
// Terminal: make chain thenable so `await chain` yields the queued result.
|
||||
then: (resolve: (v: unknown) => unknown) => {
|
||||
const state = tables.get(boundTable);
|
||||
const queue = state?.selects ?? [];
|
||||
const next = queue.shift() ?? [];
|
||||
return Promise.resolve(next).then(resolve);
|
||||
},
|
||||
};
|
||||
return chain;
|
||||
}),
|
||||
|
||||
insert: vi.fn().mockImplementation((table: unknown) => {
|
||||
return {
|
||||
values: vi.fn().mockImplementation((row: unknown) => {
|
||||
if (inserts) {
|
||||
const seen = inserts.get(table) ?? [];
|
||||
seen.push(row);
|
||||
inserts.set(table, seen);
|
||||
}
|
||||
const state = tables.get(table);
|
||||
const returnRows = state?.insertReturns ?? [
|
||||
{ ...(row as object), id: `generated-${Math.random()}` },
|
||||
];
|
||||
const afterValues = {
|
||||
returning: vi.fn().mockResolvedValue(returnRows),
|
||||
// If the caller doesn't chain .returning(), make it awaitable anyway.
|
||||
then: (resolve: (v: unknown) => unknown) =>
|
||||
Promise.resolve(returnRows).then(resolve),
|
||||
};
|
||||
return afterValues;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
update: vi.fn().mockImplementation((table: unknown) => {
|
||||
if (updateCounts) {
|
||||
updateCounts.set(table, (updateCounts.get(table) ?? 0) + 1);
|
||||
}
|
||||
return {
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// ─── 1. empty DB no-op ─────────────────────────────────────────────────────
|
||||
|
||||
describe("runBackfill - empty DB", () => {
|
||||
it("returns zero counts when no qualifying-points seasons exist", async () => {
|
||||
const tables = new Map<unknown, TableState>([
|
||||
[schema.sportsSeasons, { selects: [[]] }],
|
||||
]);
|
||||
vi.mocked(database).mockReturnValue(makeFakeDb({ tables }) as never);
|
||||
|
||||
const report = await runBackfill({ dryRun: false });
|
||||
|
||||
expect(report).toMatchObject({
|
||||
tournamentsCreated: 0,
|
||||
tournamentsLinked: 0,
|
||||
participantsCreated: 0,
|
||||
participantsLinked: 0,
|
||||
tournamentResultsCreated: 0,
|
||||
surfaceElosCreated: 0,
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 2. golf window with 4 events creates 4 tournaments ───────────────────
|
||||
|
||||
describe("runBackfill - golf tournaments", () => {
|
||||
it("creates 4 canonical tournaments for a golf season with 4 events", async () => {
|
||||
const events = [
|
||||
makeEvent({ id: "ev-1", name: "Masters Tournament", eventDate: "2026-04-09" }),
|
||||
makeEvent({ id: "ev-2", name: "PGA Championship", eventDate: "2026-05-14" }),
|
||||
makeEvent({ id: "ev-3", name: "U.S. Open", eventDate: "2026-06-18" }),
|
||||
makeEvent({ id: "ev-4", name: "The Open Championship", eventDate: "2026-07-16" }),
|
||||
];
|
||||
|
||||
const inserts = new Map<unknown, unknown[]>();
|
||||
|
||||
// Build per-event insertReturns so each new tournament gets its id.
|
||||
const tables = new Map<unknown, TableState>([
|
||||
[schema.sportsSeasons, { selects: [[GOLF_SEASON]] }],
|
||||
[
|
||||
schema.scoringEvents,
|
||||
{
|
||||
// 1st select: unlinked events; 2nd: refetch for results loop.
|
||||
selects: [events, events.map((e, i) => ({ ...e, tournamentId: `t-${i + 1}` }))],
|
||||
},
|
||||
],
|
||||
[
|
||||
schema.tournaments,
|
||||
{
|
||||
// One select-miss per event, all return [] (no existing row).
|
||||
selects: [[], [], [], []],
|
||||
// One insert per event; return sequential ids.
|
||||
insertReturns: [{ id: "t-1" }],
|
||||
},
|
||||
],
|
||||
[schema.seasonParticipants, { selects: [[]] }],
|
||||
[schema.eventResults, { selects: [[], [], [], []] }],
|
||||
[schema.seasonParticipantSurfaceElos, { selects: [[]] }],
|
||||
]);
|
||||
|
||||
// Because our insertReturns is consumed once per test, re-queue per event
|
||||
// by overriding insert behaviour via the fake db options.
|
||||
const tournamentIds = ["t-1", "t-2", "t-3", "t-4"];
|
||||
let insertIdx = 0;
|
||||
const db: ReturnType<typeof makeFakeDb> = makeFakeDb({ tables, inserts });
|
||||
// Override insert for tournaments specifically to return sequential ids.
|
||||
db.insert.mockImplementation((table: unknown) => ({
|
||||
values: vi.fn().mockImplementation((row: unknown) => {
|
||||
const seen = inserts.get(table) ?? [];
|
||||
seen.push(row);
|
||||
inserts.set(table, seen);
|
||||
let returnRows: unknown[];
|
||||
if (table === schema.tournaments) {
|
||||
returnRows = [{ ...(row as object), id: tournamentIds[insertIdx++] }];
|
||||
} else {
|
||||
returnRows = [{ ...(row as object), id: "x" }];
|
||||
}
|
||||
return {
|
||||
returning: vi.fn().mockResolvedValue(returnRows),
|
||||
then: (resolve: (v: unknown) => unknown) =>
|
||||
Promise.resolve(returnRows).then(resolve),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mocked(database).mockReturnValue(db as never);
|
||||
|
||||
const report = await runBackfill({ dryRun: false });
|
||||
|
||||
expect(report.tournamentsCreated).toBe(4);
|
||||
expect(report.tournamentsLinked).toBe(4);
|
||||
expect(inserts.get(schema.tournaments)).toHaveLength(4);
|
||||
expect(report.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 3. re-running doesn't duplicate ──────────────────────────────────────
|
||||
|
||||
describe("runBackfill - idempotence", () => {
|
||||
it("does not create new tournaments when they already exist", async () => {
|
||||
const events = [
|
||||
makeEvent({ id: "ev-1", name: "Masters Tournament", eventDate: "2026-04-09" }),
|
||||
];
|
||||
const existingTournament = {
|
||||
id: "t-existing",
|
||||
sportId: SPORT_ID,
|
||||
name: "Masters Tournament",
|
||||
year: 2026,
|
||||
};
|
||||
|
||||
const inserts = new Map<unknown, unknown[]>();
|
||||
const updateCounts = new Map<unknown, number>();
|
||||
const tables = new Map<unknown, TableState>([
|
||||
[schema.sportsSeasons, { selects: [[GOLF_SEASON]] }],
|
||||
[
|
||||
schema.scoringEvents,
|
||||
{
|
||||
selects: [events, [{ ...events[0], tournamentId: "t-existing" }]],
|
||||
},
|
||||
],
|
||||
[schema.tournaments, { selects: [[existingTournament]] }],
|
||||
[schema.seasonParticipants, { selects: [[]] }],
|
||||
[schema.eventResults, { selects: [[]] }],
|
||||
[schema.seasonParticipantSurfaceElos, { selects: [[]] }],
|
||||
]);
|
||||
|
||||
vi.mocked(database).mockReturnValue(
|
||||
makeFakeDb({ tables, inserts, updateCounts }) as never,
|
||||
);
|
||||
|
||||
const report = await runBackfill({ dryRun: false });
|
||||
|
||||
expect(report.tournamentsCreated).toBe(0);
|
||||
expect(report.tournamentsLinked).toBe(1);
|
||||
expect(inserts.get(schema.tournaments)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 4. participant backfill ───────────────────────────────────────────────
|
||||
|
||||
describe("runBackfill - participants", () => {
|
||||
it("creates canonical participants and links season_participants", async () => {
|
||||
const sps = [
|
||||
makeSeasonParticipant({ id: "sp-1", name: "Scottie Scheffler" }),
|
||||
makeSeasonParticipant({ id: "sp-2", name: "Rory McIlroy" }),
|
||||
];
|
||||
|
||||
const inserts = new Map<unknown, unknown[]>();
|
||||
const updateCounts = new Map<unknown, number>();
|
||||
const pIds = ["p-1", "p-2"];
|
||||
let pIdx = 0;
|
||||
|
||||
const tables = new Map<unknown, TableState>([
|
||||
[schema.sportsSeasons, { selects: [[GOLF_SEASON]] }],
|
||||
[schema.scoringEvents, { selects: [[], []] }],
|
||||
[schema.seasonParticipants, { selects: [sps] }],
|
||||
[schema.participants, { selects: [[], []] }],
|
||||
[schema.eventResults, { selects: [] }],
|
||||
[schema.seasonParticipantSurfaceElos, { selects: [[]] }],
|
||||
]);
|
||||
|
||||
const db = makeFakeDb({ tables, inserts, updateCounts });
|
||||
db.insert.mockImplementation((table: unknown) => ({
|
||||
values: vi.fn().mockImplementation((row: unknown) => {
|
||||
const seen = inserts.get(table) ?? [];
|
||||
seen.push(row);
|
||||
inserts.set(table, seen);
|
||||
let returnRows: unknown[];
|
||||
if (table === schema.participants) {
|
||||
returnRows = [{ ...(row as object), id: pIds[pIdx++] }];
|
||||
} else {
|
||||
returnRows = [{ ...(row as object), id: "x" }];
|
||||
}
|
||||
return {
|
||||
returning: vi.fn().mockResolvedValue(returnRows),
|
||||
then: (resolve: (v: unknown) => unknown) =>
|
||||
Promise.resolve(returnRows).then(resolve),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mocked(database).mockReturnValue(db as never);
|
||||
|
||||
const report = await runBackfill({ dryRun: false });
|
||||
|
||||
expect(report.participantsCreated).toBe(2);
|
||||
expect(report.participantsLinked).toBe(2);
|
||||
expect(inserts.get(schema.participants)).toHaveLength(2);
|
||||
expect(updateCounts.get(schema.seasonParticipants)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 5. Masters 2026 round-trip ───────────────────────────────────────────
|
||||
|
||||
describe("runBackfill - tournament_results from event_results", () => {
|
||||
it("copies placement and rawScore but NOT qualifying_points_awarded", async () => {
|
||||
const events = [
|
||||
makeEvent({
|
||||
id: "ev-masters",
|
||||
tournamentId: "t-masters",
|
||||
name: "Masters Tournament",
|
||||
eventDate: "2026-04-09",
|
||||
}),
|
||||
];
|
||||
const seasonParticipant = {
|
||||
id: "sp-1",
|
||||
sportsSeasonId: SEASON_ID,
|
||||
participantId: "p-scottie",
|
||||
name: "Scottie Scheffler",
|
||||
};
|
||||
const eventResult = {
|
||||
id: "er-1",
|
||||
scoringEventId: "ev-masters",
|
||||
seasonParticipantId: "sp-1",
|
||||
placement: 1,
|
||||
rawScore: "-12.00",
|
||||
qualifyingPointsAwarded: "100.00",
|
||||
};
|
||||
|
||||
const inserts = new Map<unknown, unknown[]>();
|
||||
|
||||
const tables = new Map<unknown, TableState>([
|
||||
[schema.sportsSeasons, { selects: [[GOLF_SEASON]] }],
|
||||
[
|
||||
schema.scoringEvents,
|
||||
{
|
||||
// 1st: no unlinked events (tournamentId already set)
|
||||
// 2nd: refetch — events linked to tournament
|
||||
selects: [[], events],
|
||||
},
|
||||
],
|
||||
[schema.tournaments, { selects: [] }],
|
||||
[schema.seasonParticipants, { selects: [[], [seasonParticipant]] }],
|
||||
[schema.participants, { selects: [] }],
|
||||
[schema.eventResults, { selects: [[eventResult]] }],
|
||||
[schema.tournamentResults, { selects: [[]] }],
|
||||
[schema.seasonParticipantSurfaceElos, { selects: [[]] }],
|
||||
]);
|
||||
|
||||
vi.mocked(database).mockReturnValue(
|
||||
makeFakeDb({ tables, inserts }) as never,
|
||||
);
|
||||
|
||||
const report = await runBackfill({ dryRun: false });
|
||||
|
||||
expect(report.tournamentResultsCreated).toBe(1);
|
||||
const trInserts = inserts.get(schema.tournamentResults) ?? [];
|
||||
expect(trInserts).toHaveLength(1);
|
||||
const inserted = trInserts[0] as Record<string, unknown>;
|
||||
expect(inserted.tournamentId).toBe("t-masters");
|
||||
expect(inserted.participantId).toBe("p-scottie");
|
||||
expect(inserted.placement).toBe(1);
|
||||
expect(inserted.rawScore).toBe("-12.00");
|
||||
// CRITICAL: qualifying_points_awarded must NOT be copied.
|
||||
expect(inserted).not.toHaveProperty("qualifyingPointsAwarded");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 6. Surface elo conflict detection ────────────────────────────────────
|
||||
|
||||
describe("runBackfill - surface elo conflicts", () => {
|
||||
it("records a conflict error when two windows disagree on eloHard", async () => {
|
||||
const sp = {
|
||||
id: "sp-1",
|
||||
sportsSeasonId: SEASON_ID,
|
||||
participantId: "p-tennis-1",
|
||||
name: "Carlos Alcaraz",
|
||||
};
|
||||
const windowElo = {
|
||||
id: "se-1",
|
||||
participantId: "sp-1",
|
||||
sportsSeasonId: SEASON_ID,
|
||||
eloHard: 2050,
|
||||
eloClay: 2100,
|
||||
eloGrass: 2000,
|
||||
worldRanking: 2,
|
||||
};
|
||||
const existingCanonicalElo = {
|
||||
id: "cpe-1",
|
||||
participantId: "p-tennis-1",
|
||||
eloHard: 2000, // differs!
|
||||
eloClay: 2100,
|
||||
eloGrass: 2000,
|
||||
worldRanking: 2,
|
||||
};
|
||||
|
||||
const inserts = new Map<unknown, unknown[]>();
|
||||
const tables = new Map<unknown, TableState>([
|
||||
[schema.sportsSeasons, { selects: [[GOLF_SEASON]] }],
|
||||
[schema.scoringEvents, { selects: [[], []] }],
|
||||
[schema.seasonParticipants, { selects: [[], [sp]] }],
|
||||
[schema.participants, { selects: [] }],
|
||||
[schema.eventResults, { selects: [] }],
|
||||
[schema.seasonParticipantSurfaceElos, { selects: [[windowElo]] }],
|
||||
[schema.participantSurfaceElos, { selects: [[existingCanonicalElo]] }],
|
||||
]);
|
||||
|
||||
vi.mocked(database).mockReturnValue(
|
||||
makeFakeDb({ tables, inserts }) as never,
|
||||
);
|
||||
|
||||
const report = await runBackfill({ dryRun: false });
|
||||
|
||||
expect(report.surfaceElosCreated).toBe(0);
|
||||
expect(report.errors).toHaveLength(1);
|
||||
expect(report.errors[0]).toMatch(/conflict for participant p-tennis-1/);
|
||||
expect(report.errors[0]).toMatch(/eloHard/);
|
||||
// Must not have inserted a conflicting row.
|
||||
expect(inserts.get(schema.participantSurfaceElos)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
358
scripts/backfill-canonical-layer.ts
Normal file
358
scripts/backfill-canonical-layer.ts
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
/**
|
||||
* Phase 2 one-off backfill: populate canonical tables
|
||||
* (`tournaments`, `participants`, `tournament_results`,
|
||||
* `participant_surface_elos`) from existing per-window data.
|
||||
*
|
||||
* See CLAUDE.md and the Phase 2 plan docs. Rules that this script
|
||||
* MUST obey:
|
||||
* - Never copy `qualifying_points_awarded` from `event_results` to
|
||||
* `tournament_results`. QP stays per-window.
|
||||
* - Never touch `season_participant_qualifying_totals`.
|
||||
* - Abort loud (collect into `report.errors`) if two windows disagree
|
||||
* on a canonical participant's surface-Elo values.
|
||||
*
|
||||
* `dryRun: true` means: run every read, compute every count, but never
|
||||
* issue an `insert()` or `update()`.
|
||||
*/
|
||||
|
||||
import { eq, and, isNull } from "drizzle-orm";
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
import { extractTournamentIdentity } from "./backfill/match-tournament";
|
||||
|
||||
export interface BackfillOptions {
|
||||
dryRun: boolean;
|
||||
sportId?: string;
|
||||
}
|
||||
|
||||
export interface BackfillReport {
|
||||
tournamentsCreated: number;
|
||||
/** Count of scoring_events whose tournament_id was (or would be) set. */
|
||||
tournamentsLinked: number;
|
||||
participantsCreated: number;
|
||||
/** Count of season_participants whose participant_id was (or would be) set. */
|
||||
participantsLinked: number;
|
||||
tournamentResultsCreated: number;
|
||||
surfaceElosCreated: number;
|
||||
warnings: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
type Db = ReturnType<typeof database>;
|
||||
type SportsSeasonRow = typeof schema.sportsSeasons.$inferSelect;
|
||||
type ScoringEventRow = typeof schema.scoringEvents.$inferSelect;
|
||||
type SeasonParticipantRow = typeof schema.seasonParticipants.$inferSelect;
|
||||
type EventResultRow = typeof schema.eventResults.$inferSelect;
|
||||
type SeasonParticipantSurfaceEloRow =
|
||||
typeof schema.seasonParticipantSurfaceElos.$inferSelect;
|
||||
type TournamentRow = typeof schema.tournaments.$inferSelect;
|
||||
type ParticipantRow = typeof schema.participants.$inferSelect;
|
||||
type TournamentResultRow = typeof schema.tournamentResults.$inferSelect;
|
||||
type ParticipantSurfaceEloRow =
|
||||
typeof schema.participantSurfaceElos.$inferSelect;
|
||||
|
||||
export async function runBackfill(
|
||||
opts: BackfillOptions,
|
||||
): Promise<BackfillReport> {
|
||||
const report: BackfillReport = {
|
||||
tournamentsCreated: 0,
|
||||
tournamentsLinked: 0,
|
||||
participantsCreated: 0,
|
||||
participantsLinked: 0,
|
||||
tournamentResultsCreated: 0,
|
||||
surfaceElosCreated: 0,
|
||||
warnings: [],
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const db = database();
|
||||
|
||||
// 1. Load qualifying-points seasons (optionally filtered by sport).
|
||||
const whereClauses = [
|
||||
eq(schema.sportsSeasons.scoringPattern, "qualifying_points"),
|
||||
];
|
||||
if (opts.sportId) {
|
||||
whereClauses.push(eq(schema.sportsSeasons.sportId, opts.sportId));
|
||||
}
|
||||
|
||||
const seasons = (await db
|
||||
.select()
|
||||
.from(schema.sportsSeasons)
|
||||
.where(and(...whereClauses))) as SportsSeasonRow[];
|
||||
|
||||
for (const season of seasons) {
|
||||
await backfillSeason(db, season, opts, report);
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
async function backfillSeason(
|
||||
db: Db,
|
||||
season: SportsSeasonRow,
|
||||
opts: BackfillOptions,
|
||||
report: BackfillReport,
|
||||
): Promise<void> {
|
||||
// ─── a. Tournament linking ────────────────────────────────────────────────
|
||||
const unlinkedEvents = (await db
|
||||
.select()
|
||||
.from(schema.scoringEvents)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.scoringEvents.sportsSeasonId, season.id),
|
||||
isNull(schema.scoringEvents.tournamentId),
|
||||
),
|
||||
)) as ScoringEventRow[];
|
||||
|
||||
for (const ev of unlinkedEvents) {
|
||||
let identity;
|
||||
try {
|
||||
identity = extractTournamentIdentity({
|
||||
name: ev.name,
|
||||
eventDate: ev.eventDate,
|
||||
eventType: ev.eventType,
|
||||
});
|
||||
} catch (e) {
|
||||
report.warnings.push(
|
||||
`skip scoring_event ${ev.id} (${ev.name}): ${(e as Error).message}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look up existing canonical tournament.
|
||||
const [existing] = (await db
|
||||
.select()
|
||||
.from(schema.tournaments)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.tournaments.sportId, season.sportId),
|
||||
eq(schema.tournaments.name, identity.name),
|
||||
eq(schema.tournaments.year, identity.year),
|
||||
),
|
||||
)
|
||||
.limit(1)) as TournamentRow[];
|
||||
|
||||
let tournamentId: string | undefined = existing?.id;
|
||||
|
||||
if (!existing) {
|
||||
const startsAt = ev.eventDate ? new Date(ev.eventDate) : null;
|
||||
const status =
|
||||
startsAt && startsAt.getTime() < Date.now() ? "completed" : "scheduled";
|
||||
|
||||
if (!opts.dryRun) {
|
||||
const [inserted] = (await db
|
||||
.insert(schema.tournaments)
|
||||
.values({
|
||||
sportId: season.sportId,
|
||||
name: identity.name,
|
||||
year: identity.year,
|
||||
startsAt,
|
||||
status,
|
||||
})
|
||||
.returning()) as TournamentRow[];
|
||||
tournamentId = inserted.id;
|
||||
}
|
||||
report.tournamentsCreated += 1;
|
||||
}
|
||||
|
||||
if (!opts.dryRun && tournamentId) {
|
||||
await db
|
||||
.update(schema.scoringEvents)
|
||||
.set({ tournamentId, updatedAt: new Date() })
|
||||
.where(eq(schema.scoringEvents.id, ev.id));
|
||||
}
|
||||
report.tournamentsLinked += 1;
|
||||
}
|
||||
|
||||
// ─── b. Participant linking ───────────────────────────────────────────────
|
||||
const unlinkedParticipants = (await db
|
||||
.select()
|
||||
.from(schema.seasonParticipants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.seasonParticipants.sportsSeasonId, season.id),
|
||||
isNull(schema.seasonParticipants.participantId),
|
||||
),
|
||||
)) as SeasonParticipantRow[];
|
||||
|
||||
for (const sp of unlinkedParticipants) {
|
||||
const [existing] = (await db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportId, season.sportId),
|
||||
eq(schema.participants.name, sp.name),
|
||||
),
|
||||
)
|
||||
.limit(1)) as ParticipantRow[];
|
||||
|
||||
let participantId: string | undefined = existing?.id;
|
||||
|
||||
if (!existing) {
|
||||
if (!opts.dryRun) {
|
||||
const [inserted] = (await db
|
||||
.insert(schema.participants)
|
||||
.values({
|
||||
sportId: season.sportId,
|
||||
name: sp.name,
|
||||
})
|
||||
.returning()) as ParticipantRow[];
|
||||
participantId = inserted.id;
|
||||
}
|
||||
report.participantsCreated += 1;
|
||||
}
|
||||
|
||||
if (!opts.dryRun && participantId) {
|
||||
await db
|
||||
.update(schema.seasonParticipants)
|
||||
.set({ participantId, updatedAt: new Date() })
|
||||
.where(eq(schema.seasonParticipants.id, sp.id));
|
||||
}
|
||||
report.participantsLinked += 1;
|
||||
}
|
||||
|
||||
// ─── c. Tournament results (copy completed event_results) ────────────────
|
||||
// Refetch events — they may now have tournamentId set (if !dryRun).
|
||||
const allEvents = (await db
|
||||
.select()
|
||||
.from(schema.scoringEvents)
|
||||
.where(
|
||||
eq(schema.scoringEvents.sportsSeasonId, season.id),
|
||||
)) as ScoringEventRow[];
|
||||
|
||||
for (const ev of allEvents) {
|
||||
if (!ev.tournamentId) {
|
||||
// In dry-run we may not have a tournamentId yet; skip result copy.
|
||||
continue;
|
||||
}
|
||||
|
||||
const results = (await db
|
||||
.select()
|
||||
.from(schema.eventResults)
|
||||
.where(
|
||||
eq(schema.eventResults.scoringEventId, ev.id),
|
||||
)) as EventResultRow[];
|
||||
|
||||
for (const r of results) {
|
||||
// Only copy rows with real placement/rawScore data.
|
||||
if (r.placement == null && r.rawScore == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look up the season_participants row to get canonical participantId.
|
||||
const [sp] = (await db
|
||||
.select()
|
||||
.from(schema.seasonParticipants)
|
||||
.where(
|
||||
eq(schema.seasonParticipants.id, r.seasonParticipantId),
|
||||
)
|
||||
.limit(1)) as SeasonParticipantRow[];
|
||||
|
||||
if (!sp || !sp.participantId) {
|
||||
// Link step should have handled this; skip defensively.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if a tournament_results row already exists.
|
||||
const [existingResult] = (await db
|
||||
.select()
|
||||
.from(schema.tournamentResults)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.tournamentResults.tournamentId, ev.tournamentId),
|
||||
eq(schema.tournamentResults.participantId, sp.participantId),
|
||||
),
|
||||
)
|
||||
.limit(1)) as TournamentResultRow[];
|
||||
|
||||
if (existingResult) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!opts.dryRun) {
|
||||
// NOTE: intentionally do NOT copy qualifyingPointsAwarded.
|
||||
await db.insert(schema.tournamentResults).values({
|
||||
tournamentId: ev.tournamentId,
|
||||
participantId: sp.participantId,
|
||||
placement: r.placement,
|
||||
rawScore: r.rawScore,
|
||||
});
|
||||
}
|
||||
report.tournamentResultsCreated += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── d. Surface Elo (per-window → canonical) ─────────────────────────────
|
||||
const elos = (await db
|
||||
.select()
|
||||
.from(schema.seasonParticipantSurfaceElos)
|
||||
.where(
|
||||
eq(schema.seasonParticipantSurfaceElos.sportsSeasonId, season.id),
|
||||
)) as SeasonParticipantSurfaceEloRow[];
|
||||
|
||||
for (const elo of elos) {
|
||||
const [sp] = (await db
|
||||
.select()
|
||||
.from(schema.seasonParticipants)
|
||||
.where(
|
||||
eq(schema.seasonParticipants.id, elo.participantId),
|
||||
)
|
||||
.limit(1)) as SeasonParticipantRow[];
|
||||
|
||||
if (!sp || !sp.participantId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const canonicalParticipantId = sp.participantId;
|
||||
|
||||
const [existingElo] = (await db
|
||||
.select()
|
||||
.from(schema.participantSurfaceElos)
|
||||
.where(
|
||||
eq(
|
||||
schema.participantSurfaceElos.participantId,
|
||||
canonicalParticipantId,
|
||||
),
|
||||
)
|
||||
.limit(1)) as ParticipantSurfaceEloRow[];
|
||||
|
||||
if (existingElo) {
|
||||
// Conflict detection: compare eloHard/eloClay/eloGrass/worldRanking.
|
||||
const fields: Array<keyof ParticipantSurfaceEloRow> = [
|
||||
"eloHard",
|
||||
"eloClay",
|
||||
"eloGrass",
|
||||
"worldRanking",
|
||||
];
|
||||
const mismatches: string[] = [];
|
||||
for (const f of fields) {
|
||||
if (existingElo[f] !== elo[f as keyof SeasonParticipantSurfaceEloRow]) {
|
||||
mismatches.push(
|
||||
`${f}: existing=${String(existingElo[f])} vs incoming=${String(elo[f as keyof SeasonParticipantSurfaceEloRow])}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (mismatches.length > 0) {
|
||||
report.errors.push(
|
||||
`conflict for participant ${canonicalParticipantId} (season ${season.id}): ${mismatches.join(", ")}`,
|
||||
);
|
||||
}
|
||||
// Do not overwrite.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!opts.dryRun) {
|
||||
await db.insert(schema.participantSurfaceElos).values({
|
||||
participantId: canonicalParticipantId,
|
||||
eloHard: elo.eloHard,
|
||||
eloClay: elo.eloClay,
|
||||
eloGrass: elo.eloGrass,
|
||||
worldRanking: elo.worldRanking,
|
||||
});
|
||||
}
|
||||
report.surfaceElosCreated += 1;
|
||||
}
|
||||
}
|
||||
82
scripts/backfill-cli.ts
Normal file
82
scripts/backfill-cli.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* CLI entry point for the Phase 2 canonical backfill.
|
||||
*
|
||||
* Usage:
|
||||
* npm run backfill:canonical -- [--dry-run | --apply] [--sport=<uuid>]
|
||||
*
|
||||
* Defaults to --dry-run for safety. No writes will be issued unless
|
||||
* --apply is passed explicitly.
|
||||
*/
|
||||
|
||||
import { DatabaseContext } from "~/database/context";
|
||||
import { db } from "../server/db";
|
||||
import { runBackfill } from "./backfill-canonical-layer";
|
||||
import type { BackfillOptions } from "./backfill-canonical-layer";
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(
|
||||
[
|
||||
"Usage: backfill-cli [options]",
|
||||
"",
|
||||
"Options:",
|
||||
" --dry-run Run without writing (default).",
|
||||
" --apply Actually write to the database.",
|
||||
" --sport=<uuid> Only backfill for the given sport id.",
|
||||
" --help Show this message.",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
function parseArgs(): BackfillOptions {
|
||||
const args = process.argv.slice(2);
|
||||
const opts: BackfillOptions = { dryRun: true }; // dry-run by default for safety
|
||||
for (const a of args) {
|
||||
if (a === "--apply") {
|
||||
opts.dryRun = false;
|
||||
} else if (a === "--dry-run") {
|
||||
opts.dryRun = true;
|
||||
} else if (a.startsWith("--sport=")) {
|
||||
opts.sportId = a.slice("--sport=".length);
|
||||
} else if (a === "--help" || a === "-h") {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(`unknown arg: ${a}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs();
|
||||
console.log(
|
||||
`Running backfill (dryRun=${opts.dryRun}, sportId=${opts.sportId ?? "all"})`,
|
||||
);
|
||||
const report = await DatabaseContext.run(db, () => runBackfill(opts));
|
||||
|
||||
console.log("---");
|
||||
console.log(`tournamentsCreated: ${report.tournamentsCreated}`);
|
||||
console.log(`tournamentsLinked: ${report.tournamentsLinked}`);
|
||||
console.log(`participantsCreated: ${report.participantsCreated}`);
|
||||
console.log(`participantsLinked: ${report.participantsLinked}`);
|
||||
console.log(`tournamentResultsCreated: ${report.tournamentResultsCreated}`);
|
||||
console.log(`surfaceElosCreated: ${report.surfaceElosCreated}`);
|
||||
|
||||
if (report.warnings.length) {
|
||||
console.log("\nWARNINGS:");
|
||||
for (const w of report.warnings) console.log(` ${w}`);
|
||||
}
|
||||
if (report.errors.length) {
|
||||
console.log("\nERRORS:");
|
||||
for (const e of report.errors) console.log(` ${e}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
44
scripts/backfill/__tests__/match-tournament.test.ts
Normal file
44
scripts/backfill/__tests__/match-tournament.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { extractTournamentIdentity } from "../match-tournament";
|
||||
|
||||
describe("extractTournamentIdentity", () => {
|
||||
it("uses eventDate year when the name has no trailing year", () => {
|
||||
const identity = extractTournamentIdentity({
|
||||
name: "Masters Tournament",
|
||||
eventDate: "2026-04-09",
|
||||
eventType: "tournament",
|
||||
});
|
||||
|
||||
expect(identity).toEqual({ name: "Masters Tournament", year: 2026 });
|
||||
});
|
||||
|
||||
it("derives the year from eventDate for a simple tournament name", () => {
|
||||
const identity = extractTournamentIdentity({
|
||||
name: "Wimbledon",
|
||||
eventDate: "2026-07-01",
|
||||
eventType: "tournament",
|
||||
});
|
||||
|
||||
expect(identity).toEqual({ name: "Wimbledon", year: 2026 });
|
||||
});
|
||||
|
||||
it("strips a trailing year from the name and uses it as the canonical year", () => {
|
||||
const identity = extractTournamentIdentity({
|
||||
name: "Wimbledon 2026",
|
||||
eventDate: "2026-07-01",
|
||||
eventType: "tournament",
|
||||
});
|
||||
|
||||
expect(identity).toEqual({ name: "Wimbledon", year: 2026 });
|
||||
});
|
||||
|
||||
it("throws when neither the name nor eventDate supply a year", () => {
|
||||
expect(() =>
|
||||
extractTournamentIdentity({
|
||||
name: "Wimbledon",
|
||||
eventDate: null,
|
||||
eventType: "tournament",
|
||||
}),
|
||||
).toThrow(/cannot determine year/);
|
||||
});
|
||||
});
|
||||
57
scripts/backfill/match-tournament.ts
Normal file
57
scripts/backfill/match-tournament.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Pure function(s) for extracting canonical tournament identity
|
||||
* (name + year) from a `scoring_events` row.
|
||||
*
|
||||
* Used by the Phase 2 backfill to group per-window events into
|
||||
* canonical `tournaments` rows.
|
||||
*/
|
||||
|
||||
export interface ScoringEventInput {
|
||||
name: string;
|
||||
eventDate: string | null;
|
||||
eventType: string;
|
||||
}
|
||||
|
||||
export interface TournamentIdentity {
|
||||
/** Tournament name with any trailing 4-digit year stripped. */
|
||||
name: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
const TRAILING_YEAR_RE = / (\d{4})$/;
|
||||
|
||||
/**
|
||||
* Extracts the canonical `(name, year)` identity for a tournament
|
||||
* from a scoring event.
|
||||
*
|
||||
* Resolution order for year:
|
||||
* 1. A trailing 4-digit year on the event name (e.g. "Wimbledon 2026").
|
||||
* The year is stripped from the returned name.
|
||||
* 2. The first 4 characters of `eventDate` (format `YYYY-MM-DD`).
|
||||
*
|
||||
* Throws if neither source supplies a year.
|
||||
*/
|
||||
export function extractTournamentIdentity(
|
||||
ev: ScoringEventInput,
|
||||
): TournamentIdentity {
|
||||
const trimmedName = ev.name.trim();
|
||||
const match = trimmedName.match(TRAILING_YEAR_RE);
|
||||
|
||||
if (match) {
|
||||
const yearFromName = Number(match[1]);
|
||||
const nameWithoutYear = trimmedName.slice(0, match.index).trim();
|
||||
return { name: nameWithoutYear, year: yearFromName };
|
||||
}
|
||||
|
||||
if (ev.eventDate) {
|
||||
const yearStr = ev.eventDate.slice(0, 4);
|
||||
const yearFromDate = Number(yearStr);
|
||||
if (Number.isFinite(yearFromDate) && yearStr.length === 4) {
|
||||
return { name: trimmedName, year: yearFromDate };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`cannot determine year for scoring event "${ev.name}" (eventDate=${ev.eventDate ?? "null"})`,
|
||||
);
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ beforeEach(() => {
|
|||
autodraftSettings: { findFirst: vi.fn() },
|
||||
draftPicks: { findMany: vi.fn(), findFirst: vi.fn() },
|
||||
draftQueue: { findMany: vi.fn() },
|
||||
participants: { findMany: vi.fn() },
|
||||
seasonParticipants: { findMany: vi.fn() },
|
||||
seasonTemplateSports: { findMany: vi.fn() },
|
||||
},
|
||||
update: vi.fn().mockReturnThis(),
|
||||
|
|
@ -146,7 +146,7 @@ describe('Timer Autodraft Integration', () => {
|
|||
|
||||
it('should fall back to highest EV when queue is empty and queueOnly is OFF', async () => {
|
||||
mockDb.query.draftQueue.findMany.mockResolvedValue([]);
|
||||
mockDb.query.participants.findMany.mockResolvedValue([
|
||||
mockDb.query.seasonParticipants.findMany.mockResolvedValue([
|
||||
{ id: 'participant-high-ev', name: 'High EV Participant', expectedValue: 1000 },
|
||||
]);
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ describe('Timer Autodraft Integration', () => {
|
|||
expect(queueItems.length).toBe(0);
|
||||
|
||||
const queueOnly = false;
|
||||
const availableParticipants = await mockDb.query.participants.findMany();
|
||||
const availableParticipants = await mockDb.query.seasonParticipants.findMany();
|
||||
const selectedParticipant = queueOnly ? null : availableParticipants[0];
|
||||
expect(selectedParticipant).not.toBeNull();
|
||||
expect(selectedParticipant?.expectedValue).toBe(1000);
|
||||
|
|
|
|||
|
|
@ -232,18 +232,18 @@ export function initializeSocketIO(httpServer: HTTPServer): SocketIOServer {
|
|||
pickInRound: schema.draftPicks.pickInRound,
|
||||
timeUsed: schema.draftPicks.timeUsed,
|
||||
team: schema.teams,
|
||||
participant: schema.participants,
|
||||
participant: schema.seasonParticipants,
|
||||
sport: schema.sports,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
|
||||
.innerJoin(
|
||||
schema.participants,
|
||||
eq(schema.draftPicks.participantId, schema.participants.id)
|
||||
schema.seasonParticipants,
|
||||
eq(schema.draftPicks.participantId, schema.seasonParticipants.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sportsSeasons,
|
||||
eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
eq(schema.seasonParticipants.sportsSeasonId, schema.sportsSeasons.id)
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sports,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue