Skip to content

Merge duplicate players by name, and make the maintenance pass actually heal duplicates (#266) - #270

Merged
jozef2svrcek merged 2 commits into
mainfrom
player-dedup-by-name
Aug 15, 2026
Merged

Merge duplicate players by name, and make the maintenance pass actually heal duplicates (#266)#270
jozef2svrcek merged 2 commits into
mainfrom
player-dedup-by-name

Conversation

@jozef2svrcek

Copy link
Copy Markdown
Contributor

Fixes #266 — the same game listed twice in a player's game list, with no maintenance step able to clear it.

The chain

Player identity is keyed on the normalised name (lowercase, comma→space, whitespace collapsed) — get_or_create_player looks a name up in a cache of every existing name_normalized before it ever considers a FIDE ID. Re-importing the same PGN, with or without FIDE headers, is therefore already a no-op; that part was never broken.

What broke was a rename. players normalise / players import rewrite a record to its FIDE-canonical spelling and recompute name_normalized, and the new value can be one another record already holds. Two records, one person, indistinguishable on screen. From there:

  • automatic player merging only grouped by fide_id, so a pair where one side had no FIDE ID stayed split forever;
  • dedup_games pairs on white_id AND black_id, so every copy hanging off the second record was invisible to game deduplication too.

That is the reported symptom: duplicate games that no button could remove.

What this does

Merge by normalised name as well as by FIDE ID. Union-find over both keys, so the relation is transitive (A —fide— B —name— C collapses to one survivor). This is the key import already identifies people by, so it restores an invariant rather than guessing.

The one refusal: a name held by two different FIDE IDs is left entirely alone, including any FIDE-less row under it. Those are namesakes FIDE distinguishes, and a merge cannot be undone.

A merge re-opens the kept player's games for deduplication. Every dedup verdict reached while the records were split is stale, and the pipeline's dedup_games is incremental — without this the copies a merge had just exposed were never re-examined. This is the fix that actually makes the pass self-healing; the merge alone only looked like it worked, because I first verified with a full CLI dedup rather than the incremental one the pipeline runs. The manual merge endpoint gets it too.

normalise now runs before dedup_players. A rename can itself create a duplicate, so merging has to come after the renames — otherwise every pass ends by creating duplicates only the next one cleans up.

Survivors keep the FIDE ID (the merge only rewrites games, so a FIDE-less survivor would drop it off the database), and game counts are refreshed, which dedup_players never did on its own.

Preview

chess-db players dedup --dry-run, and a Preview button beside the real one on the Maintenance card — merges cannot be undone, so the safe action is offered alongside rather than hidden behind a checkbox. It lists every planned merge and counts the ones linked by name alone, the only ones that could be genuine namesakes:

  keep "Abadjian, Vahram" [3] (FIDE 12345) ← "Abadjian, Vahram" [1]
Dry run: 1 duplicate player record(s) across 1 player(s) would be merged.

In practice those should be near-zero: the only thing that creates a same-name split is a rename, and renames act on records that carry a FIDE ID.

Maintenance page

Same pass, since the names were actively misleading: "Deduplication" was games deduplication, "Merge duplicate players" was the automatic one, and "Merge players" — on a different tab — was the manual one.

Before After
"Deduplication" (Databases) Remove duplicate games (Database)
"Merge duplicate players" (Databases) Merge duplicate players — automatic (Players)
"Merge players" (Players) Merge two players — manual (Players)

Tabs now hold what their name says, in pipeline order, and each opens with one line on the order its steps belong in — including that duplicate games are matched through their player records, which is the dependency the old grouping hid.

Verification

  • 103 tests pass. New coverage: the name key, the namesake refusal, FIDE-ID preservation, cross-key chaining, empty names not being an identity, count refresh, dry-run inertness, and one that fails on the old behaviour — merge followed by an incremental dedup.
  • End to end through the job API, on a database in the reported state (two records, two copies of one game, both already vetted). One Prepare database run:
normalise      Normalised 1 player name(s)
dedup_players  Removed 1 duplicate player record(s) across 1 player(s)
dedup_games    Done: 1 duplicate game(s) deleted
→ games: 1, players: 2

Still not covered

Duplicates spelled differently with no FIDE ID on either side (Dr. Vahram,Abadjian vs Abadjian, Vahram). Those normalise differently and still need the manual merge; fuzzy matching would be a much larger, much riskier change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hx8hR7hu2hrMGhZr3HxRko

jozef2svrcek and others added 2 commits August 15, 2026 15:27
Automatic player merging only grouped rows sharing a fide_id, so two
records for one person with no FIDE ID between them stayed split
forever. That also hid duplicate games: dedup_games pairs on white_id
AND black_id, so every copy hanging off the second record survived
deduplication — the reported symptom, the same game listed twice with
no maintenance step able to clear it.

dedup_players now clusters on two keys, union-find over both so the
relation is transitive (A—fide—B—name—C collapses to one survivor):

  - same fide_id, as before
  - same name_normalized, which is the key get_or_create_player already
    identifies people by. One normalised name has always meant one
    person at import; two rows holding one are an artefact — usually a
    rename, since players normalise / players import rewrite a row to
    its FIDE-canonical spelling, which can land on a name another row
    already holds. So this restores an invariant rather than guessing.

The one refusal: a name held by two different FIDE IDs is left entirely
alone, including any FIDE-less row under it. Those are namesakes FIDE
itself distinguishes, and a merge cannot be undone.

Survivor selection now prefers a row carrying the cluster's FIDE ID —
the merge only rewrites games, so a FIDE-less survivor would drop the ID
off the database — then name_score, then the lowest (oldest) id, which
is what actually decides inside a name group where spellings score
alike. Game counts are refreshed at the end: survivors own their losers'
games, and a merge run on its own from the Maintenance page has to leave
the numbers right without waiting for a later dedup_games.

The Maintenance page's tabs and button names are reorganised in the same
pass: "Deduplication" was games deduplication and "Merge duplicate
players" was the automatic one while "Merge players" on another tab was
the manual one. They are now "Remove duplicate games", "Merge duplicate
players — automatic" and "Merge two players — manual", the tabs hold
what their name says, and each opens with the order its steps belong in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hx8hR7hu2hrMGhZr3HxRko
…#266)

Three follow-ups to the name-based player merge, two of which are what
actually makes the pipeline self-healing.

players dedup --dry-run lists every planned merge — which record is
kept, which are folded in, and whether a FIDE ID or the name alone
linked them — and writes nothing. A merge cannot be undone, so the
Maintenance card offers Preview beside the real run rather than hiding
it behind a checkbox, and the report calls out the name-only links: the
only ones that could be genuine namesakes.

A merge now clears `deduped` on the kept player's games. dedup_games
pairs on white_id AND black_id, so every verdict reached while the
records were split is stale — and the pipeline's dedup_games is
incremental, so without this the duplicate copies a merge had just
exposed were never re-examined and survived forever. This was the real
reason the previous commit only appeared to fix the reported symptom:
verified with a full CLI dedup, which is not what the pipeline runs. The
manual merge endpoint gets the same treatment, since merging by hand is
exactly how a user exposes copies dedup could not previously see.

The maintenance pass now runs normalise BEFORE dedup_players. Renaming a
row to its FIDE-canonical spelling is itself a way to create a duplicate
— the new name can be one another row already holds — so merging has to
come after the renames, or every pass ends by creating duplicates that
only the next one cleans up. Verified end to end through the job API: a
split pair with both copies already vetted collapses to one player and
one game in a single pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hx8hR7hu2hrMGhZr3HxRko
@jozef2svrcek
jozef2svrcek merged commit 7ff9fbc into main Aug 15, 2026
6 checks passed
@jozef2svrcek
jozef2svrcek deleted the player-dedup-by-name branch August 15, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

doubles not merged/deleted under win 0.15.5

1 participant