Files
euchre_camp/scripts/merge-duplicate-players.js
T

315 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* Script to merge duplicate players with timestamp suffixes
* This script identifies players with the same base name (e.g., "Emily")
* and merges them into a single player with combined stats.
*/
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Pattern to extract base name from "Name-Timestamp-RandomString" format
const NAME_PATTERN = /^(.+?)-(\d{13})-([a-z0-9]{6})$/;
function extractBaseName(name) {
const match = name.match(NAME_PATTERN);
if (match) {
return match[1].trim(); // Return the base name (everything before the timestamp)
}
return name.trim(); // Return as-is if no pattern matches
}
async function mergeDuplicatePlayers() {
console.log('🔍 Scanning for duplicate players...\n');
// Get all players
const allPlayers = await prisma.player.findMany({
orderBy: { id: 'asc' },
});
// Group players by base name
const playersByBaseName = new Map();
for (const player of allPlayers) {
const baseName = extractBaseName(player.name);
if (!playersByBaseName.has(baseName)) {
playersByBaseName.set(baseName, []);
}
playersByBaseName.get(baseName).push(player);
}
// Find groups with duplicates (more than 1 player per base name)
const duplicateGroups = Array.from(playersByBaseName.entries())
.filter(([_, players]) => players.length > 1);
console.log(`Found ${duplicateGroups.length} groups of duplicate players\n`);
if (duplicateGroups.length === 0) {
console.log('✅ No duplicate players found!');
return;
}
// Process each group
for (const [baseName, players] of duplicateGroups) {
console.log(`\n📊 Processing group: "${baseName}" (${players.length} players)`);
// Sort players by ID (earliest created is the canonical player)
const [canonicalPlayer, ...duplicatePlayers] = players.sort((a, b) => a.id - b.id);
console.log(` Canonical player: ${canonicalPlayer.name} (ID: ${canonicalPlayer.id})`);
// Merge stats from duplicates into canonical player
let totalGamesPlayed = canonicalPlayer.gamesPlayed;
let totalWins = canonicalPlayer.wins;
let totalLosses = canonicalPlayer.losses;
let totalEloChange = 0; // We'll calculate this from Elo snapshots
for (const dupPlayer of duplicatePlayers) {
console.log(` Merging duplicate: ${dupPlayer.name} (ID: ${dupPlayer.id})`);
totalGamesPlayed += dupPlayer.gamesPlayed;
totalWins += dupPlayer.wins;
totalLosses += dupPlayer.losses;
}
// Update canonical player with merged stats
await prisma.player.update({
where: { id: canonicalPlayer.id },
data: {
gamesPlayed: totalGamesPlayed,
wins: totalWins,
losses: totalLosses,
// Keep the original name (without suffix) for display
name: baseName,
normalizedName: baseName.toLowerCase(),
},
});
console.log(` Updated canonical player with merged stats:`);
console.log(` Games: ${totalGamesPlayed}, Wins: ${totalWins}, Losses: ${totalLosses}`);
// Update all foreign key references to point to canonical player
const updates = [];
// Update matches
updates.push(
prisma.match.updateMany({
where: { team1P1Id: { in: duplicatePlayers.map(p => p.id) } },
data: { team1P1Id: canonicalPlayer.id },
}),
prisma.match.updateMany({
where: { team1P2Id: { in: duplicatePlayers.map(p => p.id) } },
data: { team1P2Id: canonicalPlayer.id },
}),
prisma.match.updateMany({
where: { team2P1Id: { in: duplicatePlayers.map(p => p.id) } },
data: { team2P1Id: canonicalPlayer.id },
}),
prisma.match.updateMany({
where: { team2P2Id: { in: duplicatePlayers.map(p => p.id) } },
data: { team2P2Id: canonicalPlayer.id },
})
);
// Update event participants - need to handle unique constraint
// First, check for conflicts and delete duplicates
for (const dupPlayer of duplicatePlayers) {
const dupParticipants = await prisma.eventParticipant.findMany({
where: { playerId: dupPlayer.id },
});
for (const dupParticipant of dupParticipants) {
// Check if canonical player already participates in this event
const existingParticipant = await prisma.eventParticipant.findFirst({
where: {
eventId: dupParticipant.eventId,
playerId: canonicalPlayer.id,
},
});
if (existingParticipant) {
// Delete the duplicate participant
await prisma.eventParticipant.delete({
where: { id: dupParticipant.id },
});
} else {
// Update the participant to use canonical player
await prisma.eventParticipant.update({
where: { id: dupParticipant.id },
data: { playerId: canonicalPlayer.id },
});
}
}
}
// Update teams - need to handle unique constraint on (player1Id, player2Id)
for (const dupPlayer of duplicatePlayers) {
// Update teams where dupPlayer is player1
const teamsAsPlayer1 = await prisma.team.findMany({
where: { player1Id: dupPlayer.id },
});
for (const team of teamsAsPlayer1) {
// Check if a team already exists with canonical player as player1 and same player2
const existingTeam = await prisma.team.findFirst({
where: {
eventId: team.eventId,
player1Id: canonicalPlayer.id,
player2Id: team.player2Id,
},
});
if (existingTeam) {
// Delete the duplicate team
await prisma.team.delete({
where: { id: team.id },
});
} else {
// Update the team to use canonical player
await prisma.team.update({
where: { id: team.id },
data: { player1Id: canonicalPlayer.id },
});
}
}
// Update teams where dupPlayer is player2
const teamsAsPlayer2 = await prisma.team.findMany({
where: { player2Id: dupPlayer.id },
});
for (const team of teamsAsPlayer2) {
// Check if a team already exists with canonical player as player2 and same player1
const existingTeam = await prisma.team.findFirst({
where: {
eventId: team.eventId,
player1Id: team.player1Id,
player2Id: canonicalPlayer.id,
},
});
if (existingTeam) {
// Delete the duplicate team
await prisma.team.delete({
where: { id: team.id },
});
} else {
// Update the team to use canonical player
await prisma.team.update({
where: { id: team.id },
data: { player2Id: canonicalPlayer.id },
});
}
}
}
// Update partnership games
updates.push(
prisma.partnershipGame.updateMany({
where: { player1Id: { in: duplicatePlayers.map(p => p.id) } },
data: { player1Id: canonicalPlayer.id },
}),
prisma.partnershipGame.updateMany({
where: { player2Id: { in: duplicatePlayers.map(p => p.id) } },
data: { player2Id: canonicalPlayer.id },
})
);
// Update partnership stats - this is more complex as we need to merge them
for (const dupPlayer of duplicatePlayers) {
// Find partnerships involving this duplicate player
const partnerships = await prisma.partnershipStat.findMany({
where: {
OR: [
{ player1Id: dupPlayer.id },
{ player2Id: dupPlayer.id },
],
},
});
for (const partnership of partnerships) {
const otherPlayerId = partnership.player1Id === dupPlayer.id
? partnership.player2Id
: partnership.player1Id;
// Check if a partnership already exists between canonical and other player
const existingPartnership = await prisma.partnershipStat.findFirst({
where: {
OR: [
{
player1Id: canonicalPlayer.id,
player2Id: otherPlayerId
},
{
player1Id: otherPlayerId,
player2Id: canonicalPlayer.id
},
],
},
});
if (existingPartnership) {
// Merge stats into existing partnership and delete the old one
await prisma.partnershipStat.update({
where: { id: existingPartnership.id },
data: {
gamesPlayed: { increment: partnership.gamesPlayed },
wins: { increment: partnership.wins },
losses: { increment: partnership.losses },
totalEloChange: { increment: partnership.totalEloChange },
lastPlayed: partnership.lastPlayed > existingPartnership.lastPlayed
? partnership.lastPlayed
: existingPartnership.lastPlayed,
},
});
// Delete the duplicate partnership
await prisma.partnershipStat.delete({
where: { id: partnership.id },
});
} else {
// Update partnership to use canonical player
const isPlayer1 = partnership.player1Id === dupPlayer.id;
await prisma.partnershipStat.update({
where: { id: partnership.id },
data: isPlayer1
? { player1Id: canonicalPlayer.id }
: { player2Id: canonicalPlayer.id },
});
}
}
}
// Execute all updates
await Promise.all(updates);
console.log(` Updated foreign key references`);
// Delete duplicate players
for (const dupPlayer of duplicatePlayers) {
await prisma.player.delete({
where: { id: dupPlayer.id },
});
}
console.log(` Deleted ${duplicatePlayers.length} duplicate players`);
}
console.log('\n✅ Merge complete!');
}
async function main() {
try {
await mergeDuplicatePlayers();
} catch (error) {
console.error('❌ Error merging players:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// Run if called directly
if (require.main === module) {
main();
}
module.exports = { mergeDuplicatePlayers };