diff --git a/src-tauri/migrations/0008_calendar_dedup_cleanup.sql b/src-tauri/migrations/0008_calendar_dedup_cleanup.sql new file mode 100644 index 0000000..695e335 --- /dev/null +++ b/src-tauri/migrations/0008_calendar_dedup_cleanup.sql @@ -0,0 +1,68 @@ +-- Bug fix: prior imports could accumulate many duplicate rows for the same +-- real calendar event on every re-import -- either because the (source, +-- raw_uid) unique index (added in 0005) never retroactively deduped rows +-- that existed before it, or because an event with no UID in its source data +-- got `raw_uid = NULL`, which that index's `WHERE raw_uid IS NOT NULL` clause +-- explicitly exempts from uniqueness, so it duplicated on every single +-- re-import forever. This collapses whatever's already in the table, then +-- backfills a stable content-based key for anything still missing a UID so +-- future re-imports resolve to the same row instead of minting a new one +-- (see calendar::content_uid in src/calendar/mod.rs, which produces the +-- identical 'content:subject|organizer|starts_at|ends_at' format used here). +PRAGMA foreign_keys = ON; + +-- Drop the index first: the backfill below can momentarily produce rows that +-- share a (source, raw_uid) pair before they're deduped a few statements +-- later, which the index would reject mid-UPDATE. +DROP INDEX IF EXISTS idx_calendar_events_source_uid; + +UPDATE calendar_events +SET raw_uid = 'content:' || COALESCE(subject, '') || '|' || COALESCE(organizer, '') + || '|' || COALESCE(starts_at, 0) || '|' || COALESCE(ends_at, 0) +WHERE raw_uid IS NULL; + +-- One survivor per (source, raw_uid) group: whichever row a meeting is +-- already attached to (so `attach_meeting_to_event` links don't break), else +-- the lexicographically-first id (arbitrary but deterministic). +CREATE TEMP TABLE calendar_event_survivors AS +SELECT source, raw_uid, MIN(id) AS keep_id +FROM calendar_events +GROUP BY source, raw_uid; + +UPDATE calendar_event_survivors +SET keep_id = ( + SELECT m.calendar_event_id FROM meetings m + JOIN calendar_events ce ON ce.id = m.calendar_event_id + WHERE ce.source = calendar_event_survivors.source + AND ce.raw_uid = calendar_event_survivors.raw_uid + LIMIT 1 +) +WHERE EXISTS ( + SELECT 1 FROM meetings m + JOIN calendar_events ce ON ce.id = m.calendar_event_id + WHERE ce.source = calendar_event_survivors.source + AND ce.raw_uid = calendar_event_survivors.raw_uid +); + +-- Repoint any meeting attached to a duplicate that's about to be deleted +-- onto the group's survivor instead. +UPDATE meetings +SET calendar_event_id = ( + SELECT s.keep_id FROM calendar_event_survivors s + JOIN calendar_events ce ON ce.source = s.source AND ce.raw_uid = s.raw_uid + WHERE ce.id = meetings.calendar_event_id +) +WHERE calendar_event_id IN ( + SELECT ce.id FROM calendar_events ce + JOIN calendar_event_survivors s ON ce.source = s.source AND ce.raw_uid = s.raw_uid + WHERE ce.id != s.keep_id +); + +-- Drop the duplicates (cascades to calendar_event_participants). +DELETE FROM calendar_events +WHERE id NOT IN (SELECT keep_id FROM calendar_event_survivors); + +DROP TABLE calendar_event_survivors; + +CREATE UNIQUE INDEX idx_calendar_events_source_uid ON calendar_events(source, raw_uid) + WHERE raw_uid IS NOT NULL;