|
| 1 | +-- GitHub paths are case-insensitive, but repos.url is UNIQUE case-sensitively. |
| 2 | +-- Writers that predate URL lowercasing (cargo initial sync on 2026-06-19, maven |
| 3 | +-- enrichment before CM-1305) inserted mixed-case variants of rows that already |
| 4 | +-- existed lowercase: ~40k duplicate groups, ~10k of them serving duplicate |
| 5 | +-- security contacts from both variants. The CM-1305 backfills re-pointed |
| 6 | +-- package links to the lowercase rows but left the stale variants behind. |
| 7 | +-- |
| 8 | +-- Keeper per group: the all-lowercase row when present (links were already |
| 9 | +-- re-pointed to it), else the lowest id. Only package_repos is primary data |
| 10 | +-- and gets re-pointed (repo_docker comes along since it is a free UPDATE); |
| 11 | +-- contacts, snapshots, and scorecard rows on losers are derived and re-fill |
| 12 | +-- via the regular sweeps, so they are dropped with the loser rows. |
| 13 | + |
| 14 | +CREATE TEMP TABLE repo_merge_members AS |
| 15 | +SELECT id AS repo_id, keeper_id, id = keeper_id AS is_keeper |
| 16 | +FROM ( |
| 17 | + SELECT |
| 18 | + id, |
| 19 | + FIRST_VALUE(id) OVER ( |
| 20 | + PARTITION BY LOWER(url) |
| 21 | + ORDER BY (url = LOWER(url)) DESC, id |
| 22 | + ) AS keeper_id, |
| 23 | + COUNT(*) OVER (PARTITION BY LOWER(url)) AS group_size |
| 24 | + FROM repos |
| 25 | + WHERE host = 'github' |
| 26 | +) grouped |
| 27 | +WHERE group_size > 1; |
| 28 | + |
| 29 | +CREATE INDEX ON repo_merge_members (repo_id); |
| 30 | +ANALYZE repo_merge_members; |
| 31 | + |
| 32 | +-- Keep one link per (package, group): the keeper's if it exists, else the most |
| 33 | +-- recently verified. ROW_NUMBER instead of an EXISTS check against the keeper |
| 34 | +-- because 3-variant groups exist: two losers linking the same package would |
| 35 | +-- collide on UNIQUE (package_id, repo_id) after the re-point below. |
| 36 | +DELETE FROM package_repos |
| 37 | +WHERE id IN ( |
| 38 | + SELECT id |
| 39 | + FROM ( |
| 40 | + SELECT pr.id, |
| 41 | + ROW_NUMBER() OVER ( |
| 42 | + PARTITION BY m.keeper_id, pr.package_id |
| 43 | + ORDER BY m.is_keeper DESC, pr.verified_at DESC, pr.id |
| 44 | + ) AS rn |
| 45 | + FROM package_repos pr |
| 46 | + JOIN repo_merge_members m ON m.repo_id = pr.repo_id |
| 47 | + ) ranked |
| 48 | + WHERE rn > 1 |
| 49 | +); |
| 50 | + |
| 51 | +UPDATE package_repos pr |
| 52 | +SET repo_id = m.keeper_id |
| 53 | +FROM repo_merge_members m |
| 54 | +WHERE pr.repo_id = m.repo_id |
| 55 | + AND NOT m.is_keeper; |
| 56 | + |
| 57 | +UPDATE repo_docker d |
| 58 | +SET repo_id = m.keeper_id |
| 59 | +FROM repo_merge_members m |
| 60 | +WHERE d.repo_id = m.repo_id |
| 61 | + AND NOT m.is_keeper; |
| 62 | + |
| 63 | +DELETE FROM repo_scorecard_checks c |
| 64 | +USING repo_merge_members m |
| 65 | +WHERE c.repo_id = m.repo_id |
| 66 | + AND NOT m.is_keeper; |
| 67 | + |
| 68 | +-- Cascades security_contacts and repo_activity_snapshot on losers. |
| 69 | +DELETE FROM repos r |
| 70 | +USING repo_merge_members m |
| 71 | +WHERE r.id = m.repo_id |
| 72 | + AND NOT m.is_keeper; |
| 73 | + |
| 74 | +-- Groups that had no lowercase variant: lowercase the surviving row. Safe |
| 75 | +-- against UNIQUE (url) — every row sharing LOWER(url) was in the same group, |
| 76 | +-- and its losers are gone by now. |
| 77 | +UPDATE repos r |
| 78 | +SET url = LOWER(r.url), updated_at = NOW() |
| 79 | +FROM repo_merge_members m |
| 80 | +WHERE r.id = m.repo_id |
| 81 | + AND m.is_keeper |
| 82 | + AND r.url <> LOWER(r.url); |
| 83 | + |
| 84 | +-- Recurrence guard; also fails the migration if any duplicate survived the |
| 85 | +-- merge. GitHub-only: it is the only host with both confirmed duplicates and |
| 86 | +-- guaranteed lowercase-on-write today (CASE_INSENSITIVE_HOSTS in |
| 87 | +-- canonicalizeRepoUrl also covers gitlab.com, whose merge is a follow-up). |
| 88 | +-- On other hosts case can be significant and writers do not normalize, so a |
| 89 | +-- wider index could reject legitimately distinct repos. |
| 90 | +CREATE UNIQUE INDEX IF NOT EXISTS repos_github_lower_url_uq |
| 91 | + ON repos (LOWER(url)) |
| 92 | + WHERE host = 'github'; |
0 commit comments