/** * Local mirror schema. * * Two conventions run through this file and both matter: * * 1. **UIDVALIDITY is the cache epoch.** IMAP guarantees a UID is stable only * for as long as a mailbox's UIDVALIDITY is unchanged. It is denormalized * onto every message row so that invalidating a generation is a single * DELETE rather than a join. * * 2. **64-bit IMAP counters are stored as TEXT.** `uidValidity` and * `highestModseq` are `bigint` in ImapFlow. SQLite INTEGER would hold them, * but `JSON.stringify` throws on BigInt and `node:sqlite` returns INTEGER as * a JS number by default — which silently loses precision past 2^53. TEXT * round-trips exactly, and these values are only ever compared for equality * or passed back to the server verbatim. */ /** Bumped when the statements below change shape. */ export declare const SCHEMA_VERSION = 4; export declare const SCHEMA_SQL = "\n-- Per-mailbox sync watermarks.\nCREATE TABLE IF NOT EXISTS mailbox_state (\n account TEXT NOT NULL,\n mailbox TEXT NOT NULL,\n uid_validity TEXT NOT NULL,\n uid_next INTEGER,\n highest_modseq TEXT,\n sync_tier TEXT NOT NULL,\n total_messages INTEGER,\n unseen_messages INTEGER,\n last_synced_at INTEGER,\n body_backfill_uid INTEGER,\n PRIMARY KEY (account, mailbox)\n);\n\n-- Mirrored messages. Envelope, body structure, headers and body are immutable\n-- for a given (uid, uid_validity); flags are not, and are revalidated rather\n-- than trusted. seen/flagged/answered are derived from flags_json at write\n-- time so filtered listings run as indexed SQL instead of JSON scans.\nCREATE TABLE IF NOT EXISTS message (\n account TEXT NOT NULL,\n mailbox TEXT NOT NULL,\n uid INTEGER NOT NULL,\n uid_validity TEXT NOT NULL,\n modseq TEXT,\n email_id TEXT,\n envelope_json TEXT NOT NULL,\n body_structure_json TEXT,\n flags_json TEXT NOT NULL,\n seen INTEGER NOT NULL DEFAULT 0,\n flagged INTEGER NOT NULL DEFAULT 0,\n answered INTEGER NOT NULL DEFAULT 0,\n message_id TEXT,\n in_reply_to TEXT,\n references_json TEXT,\n internal_date INTEGER NOT NULL,\n subject TEXT,\n from_text TEXT,\n preview TEXT,\n body_text TEXT,\n body_subtype TEXT,\n headers_json TEXT,\n body_fetched_at INTEGER,\n has_attachments INTEGER NOT NULL DEFAULT 0,\n attachment_names TEXT,\n cached_at INTEGER NOT NULL,\n last_accessed_at INTEGER,\n PRIMARY KEY (account, mailbox, uid, uid_validity)\n);\n\n-- Serves date-ordered pagination without the client-side slicing the live\n-- path uses: page 5 costs the same as page 1.\nCREATE INDEX IF NOT EXISTS idx_message_date\n ON message (account, mailbox, internal_date DESC);\n\n-- Serves get_thread and find_email_folder, which otherwise issue one IMAP\n-- SEARCH per mailbox or per Message-ID.\nCREATE INDEX IF NOT EXISTS idx_message_msgid\n ON message (account, message_id);\n\n-- Serves the hottest filtered listing: \"unseen, newest first\".\nCREATE INDEX IF NOT EXISTS idx_message_flags\n ON message (account, mailbox, uid_validity, seen, internal_date DESC);\n\n-- Attachment content lives in the content-addressed blob store on disk; this\n-- table is the index that maps a MIME part to its blob and carries the\n-- access times the eviction loop ranks by. Reference counting is a COUNT(*)\n-- over sha256 \u2014 no counter column to drift.\nCREATE TABLE IF NOT EXISTS attachment (\n account TEXT NOT NULL,\n mailbox TEXT NOT NULL,\n uid INTEGER NOT NULL,\n uid_validity TEXT NOT NULL,\n part_id TEXT NOT NULL,\n filename TEXT,\n mime_type TEXT,\n size INTEGER NOT NULL,\n sha256 TEXT NOT NULL,\n cached_at INTEGER NOT NULL,\n last_accessed_at INTEGER,\n PRIMARY KEY (account, mailbox, uid, uid_validity, part_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_attachment_sha\n ON attachment (sha256);\n\n-- Offline full-text search. External-content table: FTS5 stores only the\n-- index and reads column values back from the message table via rowid, so\n-- bodies are not duplicated on disk.\nCREATE VIRTUAL TABLE IF NOT EXISTS message_fts USING fts5(\n subject,\n from_text,\n body_text,\n attachment_names,\n content='message',\n content_rowid='rowid',\n tokenize='porter unicode61'\n);\n\n-- External-content FTS5 does not self-sync; these triggers keep the index and\n-- the table from drifting. The 'delete' command must be issued with the OLD\n-- values, which is why update is expressed as delete-then-insert. Every row\n-- mutation must go through INSERT/UPDATE/DELETE on message \u2014 never write to\n-- message_fts directly.\nCREATE TRIGGER IF NOT EXISTS message_fts_insert AFTER INSERT ON message BEGIN\n INSERT INTO message_fts (rowid, subject, from_text, body_text, attachment_names)\n VALUES (new.rowid, new.subject, new.from_text, new.body_text, new.attachment_names);\nEND;\n\nCREATE TRIGGER IF NOT EXISTS message_fts_delete AFTER DELETE ON message BEGIN\n INSERT INTO message_fts (message_fts, rowid, subject, from_text, body_text, attachment_names)\n VALUES ('delete', old.rowid, old.subject, old.from_text, old.body_text, old.attachment_names);\nEND;\n\n-- Guarded on the indexed columns only: touchAccess writes last_accessed_at on\n-- every cached read, and an unguarded trigger would delete-and-reinsert the\n-- whole body (up to 256 KB) each time \u2014 inflating the page count that eviction\n-- measures and undoing the point of a cache hit.\nCREATE TRIGGER IF NOT EXISTS message_fts_update AFTER UPDATE ON message\nWHEN new.subject IS NOT old.subject\n OR new.from_text IS NOT old.from_text\n OR new.body_text IS NOT old.body_text\n OR new.attachment_names IS NOT old.attachment_names\nBEGIN\n INSERT INTO message_fts (message_fts, rowid, subject, from_text, body_text, attachment_names)\n VALUES ('delete', old.rowid, old.subject, old.from_text, old.body_text, old.attachment_names);\n INSERT INTO message_fts (rowid, subject, from_text, body_text, attachment_names)\n VALUES (new.rowid, new.subject, new.from_text, new.body_text, new.attachment_names);\nEND;\n"; //# sourceMappingURL=schema.d.ts.map