#!/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 { PrismaPg } = require('@prisma/adapter-pg'); // Load environment variables from .env file require('dotenv').config(); // Create PrismaClient with PostgreSQL adapter (matching src/lib/prisma.ts) const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); // 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 };