Files
webmail-client/backend/migrations/versions/0001_initial.py
T

201 lines
6.8 KiB
Python
Raw Normal View History

2026-07-17 23:20:34 +02:00
"""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)