-- Migration 159: Repair SAG email threading schema (idempotent) -- Purpose: -- 1) Ensure sag_emails link table exists and is constrained correctly. -- 2) Ensure email_messages has threading columns used by SAG email tab. -- 3) Backfill thread_key where missing. -- Ensure link table exists CREATE TABLE IF NOT EXISTS sag_emails ( sag_id INTEGER REFERENCES sag_sager(id) ON DELETE CASCADE, email_id INTEGER REFERENCES email_messages(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW() ); -- Ensure required columns on email_messages exist ALTER TABLE email_messages ADD COLUMN IF NOT EXISTS in_reply_to VARCHAR(500), ADD COLUMN IF NOT EXISTS email_references TEXT, ADD COLUMN IF NOT EXISTS thread_key VARCHAR(500); -- Cleanup duplicates before adding unique constraint/PK WITH ranked AS ( SELECT ctid, ROW_NUMBER() OVER (PARTITION BY sag_id, email_id ORDER BY created_at NULLS LAST, ctid) AS rn FROM sag_emails ) DELETE FROM sag_emails se USING ranked r WHERE se.ctid = r.ctid AND r.rn > 1; -- Ensure PK exists DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'sag_emails_pkey' AND conrelid = 'sag_emails'::regclass ) THEN ALTER TABLE sag_emails ADD CONSTRAINT sag_emails_pkey PRIMARY KEY (sag_id, email_id); END IF; END $$; -- Ensure FKs exist DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'sag_emails_sag_id_fkey' AND conrelid = 'sag_emails'::regclass ) THEN ALTER TABLE sag_emails ADD CONSTRAINT sag_emails_sag_id_fkey FOREIGN KEY (sag_id) REFERENCES sag_sager(id) ON DELETE CASCADE; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'sag_emails_email_id_fkey' AND conrelid = 'sag_emails'::regclass ) THEN ALTER TABLE sag_emails ADD CONSTRAINT sag_emails_email_id_fkey FOREIGN KEY (email_id) REFERENCES email_messages(id) ON DELETE CASCADE; END IF; END $$; -- Helpful indexes for case email tab CREATE INDEX IF NOT EXISTS idx_sag_emails_sag_id ON sag_emails(sag_id); CREATE INDEX IF NOT EXISTS idx_sag_emails_email_id ON sag_emails(email_id); CREATE INDEX IF NOT EXISTS idx_email_messages_thread_key ON email_messages(thread_key) WHERE thread_key IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_email_messages_in_reply_to ON email_messages(in_reply_to) WHERE in_reply_to IS NOT NULL; -- Backfill thread_key for rows where it is missing UPDATE email_messages SET thread_key = LOWER( REGEXP_REPLACE( COALESCE( NULLIF( SPLIT_PART( REGEXP_REPLACE(COALESCE(email_references, ''), '^[\s<>,]+', ''), ' ', 1 ), '' ), NULLIF(in_reply_to, ''), NULLIF(message_id, '') ), '[<>\s]', '', 'g' ) ) WHERE (thread_key IS NULL OR TRIM(thread_key) = '') AND COALESCE(NULLIF(email_references, ''), NULLIF(in_reply_to, ''), NULLIF(message_id, '')) IS NOT NULL; COMMENT ON TABLE sag_emails IS 'Emails linked to the Case (SAG).'; COMMENT ON COLUMN email_messages.in_reply_to IS 'Raw In-Reply-To header used for SAG threading lookup'; COMMENT ON COLUMN email_messages.email_references IS 'Raw References header used for SAG threading lookup'; COMMENT ON COLUMN email_messages.thread_key IS 'Stable normalized thread key for grouping email conversations';