mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-07-12 18:43:45 +00:00
feat(#56): Address PR comments
This commit is contained in:
@@ -9,6 +9,118 @@
|
||||
ALTER TABLE public.catch_records
|
||||
ADD COLUMN IF NOT EXISTS "pokedexId" UUID;
|
||||
|
||||
-- 1.5) Guards: fail early with diagnostics if the backfill would be unsafe.
|
||||
--
|
||||
-- Why:
|
||||
-- - The backfill below only updates rows for users that have at least one pokedex.
|
||||
-- If any user has catch_records but no pokedexes, those rows would remain NULL
|
||||
-- and the NOT NULL enforcement would fail.
|
||||
-- - The backfill assigns all NULL pokedexId rows to each user's oldest pokedex.
|
||||
-- That can create duplicate (userId, pokedexId, pokedexEntryId) combinations.
|
||||
--
|
||||
-- If either condition is detected, this migration aborts and prints sample IDs.
|
||||
DO $$
|
||||
DECLARE
|
||||
orphan_user_count bigint;
|
||||
orphan_row_count bigint;
|
||||
orphan_user_samples text;
|
||||
|
||||
dup_group_count bigint;
|
||||
dup_group_samples text;
|
||||
BEGIN
|
||||
-- Guard A: Any catch_records with NULL pokedexId for users who have no pokedexes?
|
||||
WITH orphan_users AS (
|
||||
SELECT cr."userId" AS user_id,
|
||||
COUNT(*)::bigint AS null_row_count
|
||||
FROM public.catch_records cr
|
||||
WHERE cr."pokedexId" IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.pokedexes p
|
||||
WHERE p."userId" = cr."userId"
|
||||
)
|
||||
GROUP BY cr."userId"
|
||||
)
|
||||
SELECT
|
||||
COUNT(*)::bigint,
|
||||
COALESCE(SUM(null_row_count), 0)::bigint,
|
||||
(
|
||||
SELECT string_agg(user_id::text, ', ' ORDER BY user_id::text)
|
||||
FROM (
|
||||
SELECT user_id, null_row_count
|
||||
FROM orphan_users
|
||||
ORDER BY null_row_count DESC, user_id
|
||||
LIMIT 10
|
||||
) s
|
||||
)
|
||||
INTO orphan_user_count, orphan_row_count, orphan_user_samples
|
||||
FROM orphan_users;
|
||||
|
||||
IF orphan_user_count > 0 THEN
|
||||
RAISE EXCEPTION
|
||||
'USAFE MIGRATION: found % user(s) with % catch_records row(s) where "pokedexId" is NULL but the user has no pokedexes. Sample userId(s): %',
|
||||
orphan_user_count,
|
||||
orphan_row_count,
|
||||
COALESCE(orphan_user_samples, '(none)');
|
||||
END IF;
|
||||
|
||||
-- Guard B: Would assigning NULL pokedexId rows to each user''s oldest pokedex create duplicates?
|
||||
WITH primary_pokedex AS (
|
||||
SELECT DISTINCT ON ("userId") id AS pokedex_id, "userId"
|
||||
FROM public.pokedexes
|
||||
ORDER BY "userId", "createdAt" ASC
|
||||
),
|
||||
projected AS (
|
||||
SELECT
|
||||
cr."userId",
|
||||
COALESCE(cr."pokedexId", pp.pokedex_id) AS projected_pokedex_id,
|
||||
cr."pokedexEntryId"
|
||||
FROM public.catch_records cr
|
||||
LEFT JOIN primary_pokedex pp
|
||||
ON pp."userId" = cr."userId"
|
||||
),
|
||||
dup_groups AS (
|
||||
SELECT
|
||||
"userId",
|
||||
projected_pokedex_id,
|
||||
"pokedexEntryId",
|
||||
COUNT(*)::bigint AS row_count
|
||||
FROM projected
|
||||
WHERE projected_pokedex_id IS NOT NULL
|
||||
GROUP BY "userId", projected_pokedex_id, "pokedexEntryId"
|
||||
HAVING COUNT(*) > 1
|
||||
)
|
||||
SELECT
|
||||
COUNT(*)::bigint,
|
||||
(
|
||||
SELECT string_agg(
|
||||
format(
|
||||
'(userId=%s, pokedexId=%s, pokedexEntryId=%s, rows=%s)',
|
||||
"userId"::text,
|
||||
projected_pokedex_id::text,
|
||||
"pokedexEntryId"::text,
|
||||
row_count::text
|
||||
),
|
||||
E'\n'
|
||||
ORDER BY row_count DESC, "userId"::text
|
||||
)
|
||||
FROM (
|
||||
SELECT * FROM dup_groups
|
||||
ORDER BY row_count DESC, "userId"::text
|
||||
LIMIT 10
|
||||
) s
|
||||
)
|
||||
INTO dup_group_count, dup_group_samples
|
||||
FROM dup_groups;
|
||||
|
||||
IF dup_group_count > 0 THEN
|
||||
RAISE EXCEPTION
|
||||
'USAFE MIGRATION: backfill would create % duplicate (userId, pokedexId, pokedexEntryId) group(s) after projecting NULL "pokedexId" to each user''s oldest pokedex. Sample group(s):\n%',
|
||||
dup_group_count,
|
||||
COALESCE(dup_group_samples, '(none)');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2) Best-effort backfill for any NULL pokedexId rows.
|
||||
-- Strategy: assign to the user's oldest pokedex.
|
||||
WITH primary_pokedex AS (
|
||||
@@ -95,4 +207,3 @@ CREATE POLICY "Users can delete own catch records" ON public.catch_records
|
||||
AND public.pokedexes."userId" = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user