"""initial schema (SPEC §4) Revision ID: 0001 Revises: Create Date: 2026-07-16 The authoritative DDL lives in SPEC §4; this migration reproduces it verbatim. """ from __future__ import annotations from collections.abc import Sequence from alembic import op revision: str = "0001" down_revision: str | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None UPGRADE_STMTS: list[str] = [ "CREATE EXTENSION IF NOT EXISTS citext", """ CREATE TABLE users ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), handle citext UNIQUE NOT NULL, password_hash text NOT NULL, totp_secret bytea, recovery_email citext, settings jsonb NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now() ) """, """ CREATE TABLE accounts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, email citext NOT NULL, display_name text NOT NULL DEFAULT '', color text NOT NULL DEFAULT '#888888', imap_config jsonb NOT NULL, smtp_config jsonb NOT NULL, secret_ct bytea NOT NULL, discovery_src text NOT NULL, state text NOT NULL DEFAULT 'backfilling', state_detail text NOT NULL DEFAULT '', last_sync_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (user_id, email) ) """, "CREATE INDEX ix_accounts_user_id ON accounts (user_id)", """ CREATE TABLE mailboxes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, path text NOT NULL, special_use text, uidvalidity bigint NOT NULL DEFAULT 0, highestmodseq bigint, last_seen_uid bigint NOT NULL DEFAULT 0, UNIQUE (account_id, path) ) """, """ CREATE TABLE threads ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, subject_norm text NOT NULL, snippet text NOT NULL DEFAULT '', participants jsonb NOT NULL DEFAULT '[]', account_ids uuid[] NOT NULL DEFAULT '{}', message_count int NOT NULL DEFAULT 0, unread_count int NOT NULL DEFAULT 0, has_attachments boolean NOT NULL DEFAULT false, last_message_at timestamptz NOT NULL ) """, "CREATE INDEX ix_threads_user_last ON threads (user_id, last_message_at DESC)", """ CREATE TABLE messages ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, mailbox_id uuid NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE, uid bigint NOT NULL, thread_id uuid REFERENCES threads(id), message_id_hdr text, in_reply_to text, references_hdr text[] NOT NULL DEFAULT '{}', subject text NOT NULL DEFAULT '', from_addr jsonb NOT NULL DEFAULT '[]', to_addr jsonb NOT NULL DEFAULT '[]', cc_addr jsonb NOT NULL DEFAULT '[]', date_hdr timestamptz, internal_date timestamptz NOT NULL, flags text[] NOT NULL DEFAULT '{}', size_bytes int NOT NULL DEFAULT 0, has_attachments boolean NOT NULL DEFAULT false, snippet text NOT NULL DEFAULT '', blob_sha256 text NOT NULL, body_indexed tsvector, UNIQUE (mailbox_id, uid) ) """, "CREATE INDEX ix_messages_user_thread ON messages (user_id, thread_id)", "CREATE INDEX ix_messages_account_msgid ON messages (account_id, message_id_hdr)", "CREATE INDEX ix_messages_body_indexed ON messages USING gin (body_indexed)", """ CREATE TABLE body_parts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), message_id uuid NOT NULL REFERENCES messages(id) ON DELETE CASCADE, kind text NOT NULL, content text NOT NULL, sanitizer_v int NOT NULL DEFAULT 0 ) """, """ CREATE TABLE attachments ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), message_id uuid NOT NULL REFERENCES messages(id) ON DELETE CASCADE, user_id uuid NOT NULL, filename text NOT NULL, mime_type text NOT NULL, size_bytes int NOT NULL, content_id text, blob_sha256 text NOT NULL ) """, """ CREATE TABLE drafts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, account_id uuid NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, thread_id uuid REFERENCES threads(id), reply_to_message_id uuid REFERENCES messages(id), kind text NOT NULL DEFAULT 'reply', payload jsonb NOT NULL, updated_at timestamptz NOT NULL DEFAULT now() ) """, """ CREATE TABLE outbox ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL, draft_id uuid NOT NULL, state text NOT NULL DEFAULT 'undo_window', send_after timestamptz NOT NULL, error text ) """, """ CREATE TABLE saved_searches ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, name text NOT NULL, query text NOT NULL, pinned boolean NOT NULL DEFAULT true, UNIQUE (user_id, name) ) """, """ CREATE TABLE known_recipients ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, email citext NOT NULL, name text NOT NULL DEFAULT '', times_used int NOT NULL DEFAULT 0, last_used_at timestamptz NOT NULL, PRIMARY KEY (user_id, email) ) """, "CREATE INDEX ix_known_recipients_user_email " "ON known_recipients (user_id, email text_pattern_ops)", ] DOWNGRADE_STMTS: list[str] = [ "DROP TABLE IF EXISTS known_recipients", "DROP TABLE IF EXISTS saved_searches", "DROP TABLE IF EXISTS outbox", "DROP TABLE IF EXISTS drafts", "DROP TABLE IF EXISTS attachments", "DROP TABLE IF EXISTS body_parts", "DROP TABLE IF EXISTS messages", "DROP TABLE IF EXISTS threads", "DROP TABLE IF EXISTS mailboxes", "DROP TABLE IF EXISTS accounts", "DROP TABLE IF EXISTS users", ] def upgrade() -> None: for stmt in UPGRADE_STMTS: op.execute(stmt) def downgrade() -> None: for stmt in DOWNGRADE_STMTS: op.execute(stmt)