Review caught a boot-crash risk: 0038's repoint-UPDATE could set two non-canonical variant links of one manga to the same canonical id in a single statement (NOT EXISTS sees the pre-statement snapshot) -> manga_genres PK violation -> migration rollback -> startup failure. Replace with INSERT ... SELECT DISTINCT ... ON CONFLICT DO NOTHING (collision-proof) then delete variants. Amended in place (0038 is unpushed). Tested with the exact collision scenario. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
45 lines
2.3 KiB
SQL
45 lines
2.3 KiB
SQL
-- Enforce case-insensitive genre uniqueness, matching `authors` and `tags`
|
|
-- (both got `UNIQUE (lower(name))` in 0009). `genres` only had a case-SENSITIVE
|
|
-- `name UNIQUE`, but the crawler's `sync_genres` treats genres as
|
|
-- case-insensitive (`WHERE lower(name) = lower($1)`) and can INSERT a new row
|
|
-- from a source string. Under a race (or across ticks) two different-cased
|
|
-- strings — "Isekai" vs "isekai" — could both miss the pre-check and both
|
|
-- insert, since the exact-name unique doesn't collide. That leaves duplicate
|
|
-- genre rows for one logical genre.
|
|
|
|
-- Heal any pre-existing case-variant duplicates before adding the index, so
|
|
-- `sqlx::migrate!` can't crash on a dirty table (mirrors 0031's pre-dedup).
|
|
-- Canonical = lowest id per lower(name).
|
|
|
|
-- 1. Ensure every manga linked to any case-variant also has the canonical link.
|
|
-- INSERT ... ON CONFLICT DO NOTHING is collision-proof: SELECT DISTINCT
|
|
-- collapses multiple variants of one manga to a single canonical row, and the
|
|
-- ON CONFLICT absorbs a canonical link that already exists. (A prior
|
|
-- UPDATE-repoint could set two non-canonical rows of the SAME manga to the
|
|
-- same canonical id in a single statement and violate the manga_genres PK,
|
|
-- rolling the migration back and wedging startup.)
|
|
INSERT INTO manga_genres (manga_id, genre_id)
|
|
SELECT DISTINCT mg.manga_id, k.keep_id
|
|
FROM manga_genres mg
|
|
JOIN genres g ON mg.genre_id = g.id
|
|
JOIN (SELECT lower(name) AS lname, (array_agg(id ORDER BY id))[1] AS keep_id
|
|
FROM genres GROUP BY lower(name)) k
|
|
ON lower(g.name) = k.lname
|
|
WHERE g.id <> k.keep_id
|
|
ON CONFLICT (manga_id, genre_id) DO NOTHING;
|
|
|
|
-- 2. Every non-canonical link now has a canonical sibling — drop the variants.
|
|
DELETE FROM manga_genres mg
|
|
USING genres g,
|
|
(SELECT lower(name) AS lname, (array_agg(id ORDER BY id))[1] AS keep_id
|
|
FROM genres GROUP BY lower(name)) k
|
|
WHERE mg.genre_id = g.id AND lower(g.name) = k.lname AND g.id <> k.keep_id;
|
|
|
|
-- Remove the now-orphaned duplicate genre rows.
|
|
DELETE FROM genres g
|
|
USING (SELECT lower(name) AS lname, (array_agg(id ORDER BY id))[1] AS keep_id
|
|
FROM genres GROUP BY lower(name)) k
|
|
WHERE lower(g.name) = k.lname AND g.id <> k.keep_id;
|
|
|
|
CREATE UNIQUE INDEX genres_name_lower_uniq ON genres (lower(name));
|