wacrm/supabase/migrations/004_contact_delete_set_null.sql
Arnas Donauskas 5f488e129a
fix(contacts): allow deletion — SET NULL on broadcast_recipients, deals
Root cause (error 23503):
  broadcast_recipients.contact_id and deals.contact_id were declared
  NOT NULL REFERENCES contacts(id) with no ON DELETE clause, so
  Postgres defaulted to NO ACTION. Any contact who'd ever received a
  broadcast or been attached to a deal became un-deletable with
  "violates foreign key constraint broadcast_recipients_contact_id_fkey".

Fix (migration 004):
  Relax both FKs to ON DELETE SET NULL + drop NOT NULL on both
  columns. History rows survive with contact_id = NULL. The UI is
  already null-safe:
    - broadcast detail:  contact?.name ?? 'Unknown'
    - deal card:         contact?.name || contact?.phone || 'No contact'
    - CSV export:        contact?.name ?? ''

Why not CASCADE:
  CASCADE would wipe broadcast recipient rows and trigger the
  aggregate recount on broadcasts (migration 003), retroactively
  changing sent_count / delivered_count / read_count. It would also
  delete deal history. SET NULL preserves both counts and history.

Type/code:
  - Deal.contact_id + BroadcastRecipient.contact_id widen to
    string | null
  - DealForm: setContactId(deal.contact_id ?? "") — the contact
    picker uses "" to mean unselected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 16:56:45 +03:00

66 lines
2.3 KiB
SQL

-- ============================================================
-- Allow contact deletion without wiping history.
--
-- broadcast_recipients.contact_id and deals.contact_id were declared
-- NOT NULL REFERENCES contacts(id) with no ON DELETE action, so
-- Postgres defaults to NO ACTION. The first time a user tried to
-- delete a contact that had ever received a broadcast or been
-- attached to a deal, the delete failed with:
--
-- ERROR 23503: update or delete on table "contacts" violates
-- foreign key constraint ... on table <other>
--
-- CASCADE is the wrong fix — it would silently wipe historical
-- broadcast recipient rows (breaking audit + retroactively moving
-- broadcasts.sent_count / delivered_count / read_count etc. via the
-- aggregate trigger) and deal rows.
--
-- SET NULL is the right fix: history rows survive with a NULL
-- contact_id. The UI is already null-safe (contact?.name ?? 'Unknown',
-- contact?.phone, etc.).
--
-- Idempotent — safe to run multiple times.
-- ============================================================
-- ── broadcast_recipients.contact_id ────────────────────────────
ALTER TABLE broadcast_recipients
ALTER COLUMN contact_id DROP NOT NULL;
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'broadcast_recipients_contact_id_fkey'
AND conrelid = 'broadcast_recipients'::regclass
) THEN
ALTER TABLE broadcast_recipients
DROP CONSTRAINT broadcast_recipients_contact_id_fkey;
END IF;
END $$;
ALTER TABLE broadcast_recipients
ADD CONSTRAINT broadcast_recipients_contact_id_fkey
FOREIGN KEY (contact_id) REFERENCES contacts(id)
ON DELETE SET NULL;
-- ── deals.contact_id ───────────────────────────────────────────
ALTER TABLE deals
ALTER COLUMN contact_id DROP NOT NULL;
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'deals_contact_id_fkey'
AND conrelid = 'deals'::regclass
) THEN
ALTER TABLE deals
DROP CONSTRAINT deals_contact_id_fkey;
END IF;
END $$;
ALTER TABLE deals
ADD CONSTRAINT deals_contact_id_fkey
FOREIGN KEY (contact_id) REFERENCES contacts(id)
ON DELETE SET NULL;