"""threads.senders — who wrote, as distinct from who was involved The message list showed a thread's `participants`, which the threader builds from from + to + cc because the subject-fallback join matches on recipients. In a list that meant every ordinary email read "Sender, You" — and the "+N" badge counted the user themselves. `senders` holds only the from addresses, newest writer first. Backfilled here so existing threads do not have to wait to be touched again. Revision ID: 0019 Revises: 0018 Create Date: 2026-09-18 """ from __future__ import annotations from collections.abc import Sequence from alembic import op revision: str = "0019" down_revision: str | None = "0018" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.execute("ALTER TABLE threads ADD COLUMN senders jsonb NOT NULL DEFAULT '[]'") # Newest writer first, de-duplicated by lower-cased address, matching what # denormalize_thread computes from now on. op.execute( """ WITH ranked AS ( SELECT m.thread_id, lower(a->>'email') AS email_key, jsonb_build_object('name', coalesce(a->>'name', ''), 'email', coalesce(a->>'email', '')) AS addr, row_number() OVER (PARTITION BY m.thread_id, lower(a->>'email') ORDER BY m.internal_date DESC) AS dup_rank, max(m.internal_date) OVER (PARTITION BY m.thread_id, lower(a->>'email')) AS recency FROM messages m CROSS JOIN LATERAL jsonb_array_elements(m.from_addr) AS a WHERE m.thread_id IS NOT NULL AND coalesce(a->>'email', '') <> '' ), collected AS ( SELECT thread_id, jsonb_agg(addr ORDER BY recency DESC) AS senders FROM ranked WHERE dup_rank = 1 GROUP BY thread_id ) UPDATE threads t SET senders = c.senders FROM collected c WHERE c.thread_id = t.id """ ) def downgrade() -> None: op.execute("ALTER TABLE threads DROP COLUMN IF EXISTS senders")