"""repair thread counters left stale by server-side flag changes and expunges `threads.unread_count`, `pinned` and `message_count` are denormalized from the thread's messages. Two sync paths updated messages without recomputing them: a flag change reported by the server (mail read on a phone, or filed by a server-side rule) and a QRESYNC expunge. The row then disagreed with its own messages — the message list showing a message as unread that the reading pane showed as read, and only a manual "mark as read" reconciling it. The code paths are fixed; this repairs rows that already drifted. Recomputes from the messages themselves, so it is safe to run on a healthy database: threads that already agree are left untouched. Revision ID: 0018 Revises: 0017 Create Date: 2026-09-18 """ from __future__ import annotations from collections.abc import Sequence from alembic import op revision: str = "0018" down_revision: str | None = "0017" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.execute( r""" UPDATE threads t SET unread_count = s.unread, pinned = s.pinned, message_count = s.total FROM ( SELECT thread_id, count(*) AS total, count(*) FILTER (WHERE NOT ('\Seen' = ANY(flags))) AS unread, bool_or('\Flagged' = ANY(flags)) AS pinned FROM messages WHERE thread_id IS NOT NULL GROUP BY thread_id ) AS s WHERE t.id = s.thread_id AND (t.unread_count, t.pinned, t.message_count) IS DISTINCT FROM (s.unread, s.pinned, s.total) """ ) def downgrade() -> None: """Nothing to undo: this only makes rows agree with the messages they describe."""