Skip to content

Commit c918213

Browse files
committed
fix: merge case-duplicate github repos and enforce lowercase url uniqueness
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent 1987d18 commit c918213

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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

Comments
 (0)