← back to Stayclaim

db/migrations/017_seed_idempotency.sql

52 lines

-- Idempotency hardening for the two seed-targeted tables.
--
-- Both `restaurant_sighting` and `entity_place_association` use random UUIDs
-- as the only unique key, which means seed scripts using ON CONFLICT DO NOTHING
-- never actually conflict — re-running the seeds duplicates rows.
--
-- This migration:
--   1. Dedupes existing rows (keeps oldest by recorded_at/created_at)
--   2. Adds UNIQUE indexes on the natural keys so future ON CONFLICT works
--   3. Seed scripts must update their INSERTs to target these constraints

-- 1a. Dedupe restaurant_sighting. Natural key includes seen_date so that
-- two legitimate dated sightings of the same patron at the same restaurant
-- in the same era are NOT collapsed (replay-safe — migration 018 widens the
-- index but the dedupe partition was always intended to be this wide).
WITH dupes AS (
  SELECT id, ROW_NUMBER() OVER (
    PARTITION BY restaurant_id, person_name,
                 COALESCE(era, ''),
                 COALESCE(seen_date, DATE '1900-01-01')
    ORDER BY recorded_at ASC, id ASC
  ) AS rn
  FROM restaurant_sighting
)
DELETE FROM restaurant_sighting
WHERE id IN (SELECT id FROM dupes WHERE rn > 1);

-- Index matches the dedupe partition above (includes seen_date). Migration
-- 018 is now a no-op idempotent re-creation kept for the prod DB that already
-- ran 017's earlier (narrow) form.
CREATE UNIQUE INDEX IF NOT EXISTS uniq_sighting_natural
  ON restaurant_sighting (
    restaurant_id,
    person_name,
    COALESCE(era, ''),
    COALESCE(seen_date, DATE '1900-01-01')
  );

-- 1b. Dedupe entity_place_association on (entity_id, listing_id, relation)
WITH dupes AS (
  SELECT id, ROW_NUMBER() OVER (
    PARTITION BY entity_id, listing_id, relation
    ORDER BY created_at ASC, id ASC
  ) AS rn
  FROM entity_place_association
)
DELETE FROM entity_place_association
WHERE id IN (SELECT id FROM dupes WHERE rn > 1);

CREATE UNIQUE INDEX IF NOT EXISTS uniq_epa_natural
  ON entity_place_association (entity_id, listing_id, relation);