Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
-- GitHub paths are case-insensitive, but repos.url is UNIQUE case-sensitively.
-- Writers that predate URL lowercasing (cargo initial sync on 2026-06-19, maven
-- enrichment before CM-1305) inserted mixed-case variants of rows that already
-- existed lowercase: ~40k duplicate groups, ~10k of them serving duplicate
-- security contacts from both variants. The CM-1305 backfills re-pointed
-- package links to the lowercase rows but left the stale variants behind.
--
-- Keeper per group: the all-lowercase row when present (links were already
-- re-pointed to it), else the lowest id. Only package_repos is primary data
-- and gets re-pointed (repo_docker comes along since it is a free UPDATE);
-- contacts, snapshots, and scorecard rows on losers are derived and re-fill
-- via the regular sweeps, so they are dropped with the loser rows.

CREATE TEMP TABLE repo_merge_members AS
SELECT id AS repo_id, keeper_id, id = keeper_id AS is_keeper
Comment thread
mbani01 marked this conversation as resolved.
FROM (
SELECT
id,
FIRST_VALUE(id) OVER (
PARTITION BY LOWER(url)
ORDER BY (url = LOWER(url)) DESC, id
) AS keeper_id,
COUNT(*) OVER (PARTITION BY LOWER(url)) AS group_size
FROM repos
WHERE host = 'github'
) grouped
WHERE group_size > 1;
Comment thread
mbani01 marked this conversation as resolved.

CREATE INDEX ON repo_merge_members (repo_id);
ANALYZE repo_merge_members;

-- Keep one link per (package, group), ranked the way consumers pick links
-- (sqlFragments bestRepoLink: confidence DESC, declared-on-ties) so the merge
-- preserves the strongest provenance; keeper status only breaks full ties.
-- ROW_NUMBER instead of an EXISTS check against the keeper because 3-variant
-- groups exist: two losers linking the same package would collide on
-- UNIQUE (package_id, repo_id) after the re-point below.
DELETE FROM package_repos
WHERE id IN (
SELECT id
FROM (
SELECT pr.id,
ROW_NUMBER() OVER (
PARTITION BY m.keeper_id, pr.package_id
ORDER BY pr.confidence DESC, (pr.source = 'declared') DESC,
m.is_keeper DESC, pr.verified_at DESC, pr.id
) AS rn
FROM package_repos pr
JOIN repo_merge_members m ON m.repo_id = pr.repo_id
) ranked
WHERE rn > 1
);

UPDATE package_repos pr
SET repo_id = m.keeper_id
FROM repo_merge_members m
WHERE pr.repo_id = m.repo_id
AND NOT m.is_keeper;
Comment thread
mbani01 marked this conversation as resolved.
Outdated

UPDATE repo_docker d
SET repo_id = m.keeper_id
FROM repo_merge_members m
WHERE d.repo_id = m.repo_id
AND NOT m.is_keeper;

DELETE FROM repo_scorecard_checks c
USING repo_merge_members m
WHERE c.repo_id = m.repo_id
AND NOT m.is_keeper;

-- packages.repository_url is denormalized and must keep matching the
-- canonical repos.url (the maven backfill updates the two atomically for the
-- same reason). Joined on repos directly, not the merge map, so it also
-- covers packages pointing at mixed-case singletons normalized below; runs
-- while loser urls still exist to match against. last_synced_at is the
-- packages watermark, bumping it ships the correction to Tinybird.
UPDATE packages p
SET repository_url = LOWER(p.repository_url), last_synced_at = NOW()
FROM repos r
WHERE r.host = 'github'
AND p.repository_url = r.url
AND p.repository_url <> LOWER(p.repository_url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case-sensitive package URL join

Medium Severity

The migration lowercases packages.repository_url only when it equals a GitHub repos.url byte-for-byte. GitHub URLs are case-insensitive, so a package can refer to the same repo as a row in repos while using a casing variant that was never inserted. Those rows skip the update and keep a mixed-case repository_url after repos is normalized, breaking the intended alignment with canonical lowercase URLs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eff5795. Configure here.


-- Cascades security_contacts and repo_activity_snapshot on losers.
DELETE FROM repos r
USING repo_merge_members m
WHERE r.id = m.repo_id
AND NOT m.is_keeper;

-- Lowercase every surviving mixed-case github url: keepers whose group had
-- no lowercase variant, plus mixed-case singletons that never had a
-- duplicate. Singletons must be normalized before the guard index exists —
-- writers upsert with ON CONFLICT (url), which does not arbitrate on the
-- LOWER(url) index, so a later insert of the same repo's canonical lowercase
-- url (all writers lowercase now) would raise unique_violation instead of
-- upserting. Safe against UNIQUE (url): any two rows sharing LOWER(url) were
-- a group above, and their losers are gone by now.
UPDATE repos r
SET url = LOWER(r.url), updated_at = NOW()
WHERE r.host = 'github'
AND r.url <> LOWER(r.url);
Comment on lines +109 to +112

-- The recurrence-guard unique index lives in the next migration: it must be
-- built CONCURRENTLY (repos takes continuous worker writes), which cannot run
-- inside this transaction.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Recurrence guard for case-duplicate GitHub repos, split from the merge in
-- V1784718693: CONCURRENTLY cannot run inside a transaction, and under
-- flyway's mixed=true a shared file would demote the merge itself to
-- autocommit, losing its atomicity. The concurrent build keeps repos writable
-- for the workers during the scan, and doubles as verification of the merge —
-- a surviving duplicate fails the build.
--
-- GitHub-only: the only host with both confirmed duplicates and guaranteed
-- lowercase-on-write today (CASE_INSENSITIVE_HOSTS in canonicalizeRepoUrl
-- also covers gitlab.com, whose merge is a follow-up). On other hosts case
-- can be significant and writers do not normalize, so a wider index could
-- reject legitimately distinct repos.
--
-- A failed concurrent build leaves an INVALID index behind; the DROP lets a
-- re-run replace it, where IF NOT EXISTS would silently keep the broken one.
-- Also CONCURRENTLY: a plain drop takes an ACCESS EXCLUSIVE lock on repos,
-- stalling the workers on the retry path this exists for.
DROP INDEX CONCURRENTLY IF EXISTS repos_github_lower_url_uq;

CREATE UNIQUE INDEX CONCURRENTLY repos_github_lower_url_uq
ON repos (LOWER(url))
WHERE host = 'github';
Loading