Skip to content

Commit 0165cc7

Browse files
fix(migrations): resolve the #548/#549 collision that made the digest timezone fix inert (#563)
#548 (keyset pagination for get_daily_digest_candidates) and #549 (the tenant-local streak-nudge comparison) were developed in parallel and landed within hours of each other. Each merged cleanly on its own; together they left master in a state neither PR was reviewed in. Two problems. 1. Duplicate migration version. Both `daily_digest_candidates_pagination` and `league_rollover_catchup_and_bands` were stamped 20260726140000, so a fresh `db reset` would try to record two rows under one primary key. The league migration is independent of everything around it, so it moves to 20260726140500 rather than reordering anything. 2. The #549 digest fix was inert. #548 replaced the zero-argument function with a three-argument keyset-paginated one, and explicitly DROPped the zero-arg version because an overload makes a zero-arg call ambiguous (PostgREST 300). #549's migration then re-created that zero-arg function. Because it sorts after #548's, a fresh reset ended up with both: the cron passes three named arguments so it kept resolving to the paginated definition, which still carried the `= CURRENT_DATE - 1` UTC comparison #549 exists to remove — while #549's own function sat unused. The bug was live and the fix looked applied. The #549 migration now replaces the paginated signature, carrying #548's cursor and ORDER BY over verbatim, so one function has both fixes. Verified by applying both migrations in file order inside a rolled-back transaction: exactly one `get_daily_digest_candidates(uuid,uuid,integer)` survives, it contains `>= CURRENT_DATE - 2`, and it retains `_after_tenant_id`. 502/502 unit tests and typecheck green. Cloud was already patched directly against the live paginated definition, so it has been correct throughout; this brings the repo in line with it. Claude-Session: https://claude.ai/code/session_01RGCEptT7pgWwRiMdB3TUCM Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9fd240f commit 0165cc7

2 files changed

Lines changed: 30 additions & 8 deletions

File tree

supabase/migrations/20260726140100_digest_candidates_local_day.sql

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-- =============================================================================
22
-- Daily digest candidates: stop pre-filtering streak risk on the UTC day
3-
-- Issue #549 §3 (epic #540). Fixes 20260714150000_daily_digest.
3+
-- Issue #549 §3 (epic #540).
44
--
55
-- The streak-at-risk branch tested `gp.last_activity_date = CURRENT_DATE - 1`,
66
-- i.e. UTC yesterday, while the send decision and the idempotency key in
@@ -20,11 +20,24 @@
2020
-- isStreakAtRisk(), which does know the timezone. Rows that turn out not to be
2121
-- at risk are dropped there before anything is sent.
2222
--
23+
-- IMPORTANT — this replaces the KEYSET-PAGINATED signature introduced by
24+
-- 20260726140000_daily_digest_candidates_pagination.sql (issue #548), not the
25+
-- original zero-argument one. #548 and #549 were developed in parallel and this
26+
-- file originally re-created the zero-arg function, which would have (a) added
27+
-- back the overload #548 deliberately dropped — a zero-arg call against both
28+
-- candidates is ambiguous and PostgREST answers 300 — and (b) left the
29+
-- three-argument definition the cron actually calls still carrying the UTC bug,
30+
-- making this fix inert. The pagination logic below is carried over verbatim.
31+
--
2332
-- award_xp()'s own day boundary is deliberately NOT changed here: it would
2433
-- shift every existing streak. See the note on isStreakAtRisk().
2534
-- =============================================================================
2635

27-
CREATE OR REPLACE FUNCTION public.get_daily_digest_candidates()
36+
CREATE OR REPLACE FUNCTION public.get_daily_digest_candidates(
37+
_after_tenant_id uuid DEFAULT NULL,
38+
_after_user_id uuid DEFAULT NULL,
39+
_limit integer DEFAULT 500
40+
)
2841
RETURNS TABLE (
2942
tenant_id uuid,
3043
user_id uuid,
@@ -76,14 +89,23 @@ AS $$
7689
COALESCE(rc.due_count, 0) > 0
7790
OR COALESCE(sg.pending_count, 0) > 0
7891
-- Superset of "streak at risk" across all tenant timezones; the exact
79-
-- local-day comparison happens in isStreakAtRisk().
92+
-- local-day comparison happens in isStreakAtRisk(). Was
93+
-- `= CURRENT_DATE - 1` (issue #549 §3).
8094
OR (COALESCE(gp.current_streak, 0) >= 3
8195
AND gp.last_activity_date >= CURRENT_DATE - 2)
82-
);
96+
)
97+
-- Keyset cursor from issue #548, carried over unchanged.
98+
AND (
99+
_after_tenant_id IS NULL
100+
OR (tu.tenant_id, tu.user_id)
101+
> (_after_tenant_id, COALESCE(_after_user_id, '00000000-0000-0000-0000-000000000000'::uuid))
102+
)
103+
ORDER BY tu.tenant_id, tu.user_id
104+
LIMIT LEAST(GREATEST(COALESCE(_limit, 500), 1), 1000);
83105
$$;
84106

85107
-- Service-role only: the function reads auth.users and crosses tenants.
86-
REVOKE ALL ON FUNCTION public.get_daily_digest_candidates() FROM PUBLIC;
87-
REVOKE ALL ON FUNCTION public.get_daily_digest_candidates() FROM anon;
88-
REVOKE ALL ON FUNCTION public.get_daily_digest_candidates() FROM authenticated;
89-
GRANT EXECUTE ON FUNCTION public.get_daily_digest_candidates() TO service_role;
108+
REVOKE ALL ON FUNCTION public.get_daily_digest_candidates(uuid, uuid, integer) FROM PUBLIC;
109+
REVOKE ALL ON FUNCTION public.get_daily_digest_candidates(uuid, uuid, integer) FROM anon;
110+
REVOKE ALL ON FUNCTION public.get_daily_digest_candidates(uuid, uuid, integer) FROM authenticated;
111+
GRANT EXECUTE ON FUNCTION public.get_daily_digest_candidates(uuid, uuid, integer) TO service_role;

supabase/migrations/20260726140000_league_rollover_catchup_and_bands.sql renamed to supabase/migrations/20260726140500_league_rollover_catchup_and_bands.sql

File renamed without changes.

0 commit comments

Comments
 (0)