diff --git a/app/models/surface-elo.ts b/app/models/surface-elo.ts index 6267fcf..83258c2 100644 --- a/app/models/surface-elo.ts +++ b/app/models/surface-elo.ts @@ -8,7 +8,7 @@ import { database } from "~/database/context"; import { participantSurfaceElos, seasonParticipantSurfaceElos, seasonParticipants } from "~/database/schema"; -import { eq, sql } from "drizzle-orm"; +import { eq, inArray, sql } from "drizzle-orm"; export type CourtSurface = "hard" | "clay" | "grass"; @@ -99,6 +99,45 @@ export async function batchUpsertSurfaceElos( updatedAt: sql`excluded.updated_at`, }, }); + + // Mirror writes to the canonical participant_surface_elos table. + // The simulator reads from canonical, so per-window-only edits would + // not affect simulations. Remove the per-window path entirely in Phase 4. + const sps = await db + .select({ id: seasonParticipants.id, canonicalId: seasonParticipants.participantId }) + .from(seasonParticipants) + .where(inArray(seasonParticipants.id, inputs.map((i) => i.participantId))); + const canonicalByInputId = new Map(sps.map((sp) => [sp.id, sp.canonicalId])); + + const canonicalRows = inputs + .map((i) => ({ + canonicalId: canonicalByInputId.get(i.participantId), + worldRanking: i.worldRanking ?? null, + eloHard: i.eloHard ?? null, + eloClay: i.eloClay ?? null, + eloGrass: i.eloGrass ?? null, + })) + .filter((r): r is typeof r & { canonicalId: string } => typeof r.canonicalId === "string"); + + if (canonicalRows.length === 0) return; + + await db + .insert(participantSurfaceElos) + .values(canonicalRows.map(({ canonicalId, ...rest }) => ({ + participantId: canonicalId, + ...rest, + updatedAt: now, + }))) + .onConflictDoUpdate({ + target: [participantSurfaceElos.participantId], + set: { + worldRanking: sql`excluded.world_ranking`, + eloHard: sql`excluded.elo_hard`, + eloClay: sql`excluded.elo_clay`, + eloGrass: sql`excluded.elo_grass`, + updatedAt: sql`excluded.updated_at`, + }, + }); } /**