-- ── Events (partitioned by month on created_at) ────────────────────────────── -- Conformance: "Channel-less global events and DMs". `community_id` leads the -- PK and every hot-path index. Partition stays BY RANGE (created_at) — the -- monthly partition manager is unchanged (Max's call, plan §5/Lane0 contract). -- Cross-community dedup: same signed event may exist in two communities; -- (community_id, created_at, id) dedupes within one, allows across. CREATE TABLE events ( community_id UUID NOT NULL REFERENCES communities(id), id BYTEA NOT NULL, pubkey BYTEA NOT NULL, created_at TIMESTAMPTZ NOT NULL, kind INT NOT NULL, tags JSONB NOT NULL, content TEXT NOT NULL, -- Full-text search vector (Typesense → Postgres FTS). Generated/STORED so -- it is a single source of truth — no sidecar indexer to keep coherent -- (Quinn option A, Lane-0 call). 'simple' config = no stemming/stopwords, -- matching the existing substring-ish search semantics; the search lane can -- revisit the config behind evidence. Tenant scoping is by the -- community-leading btree filters BitmapAnd-ed with the GIN probe, so the -- GIN index itself stays the minimal `GIN (search_tsv)` (Max's caveat: -- avoid btree_gin unless EXPLAIN proves it buys something). -- Privacy: encrypted/private routing wrappers and p-gated membership notices -- must never be discoverable through NIP-50 full-text search. NULL tsvector -- never matches `@@`. -- Keep in sync with migrations (final state: 0001 + 0005 + 0014 + 0033). search_tsv TSVECTOR GENERATED ALWAYS AS ( CASE WHEN kind IN (1059, 30179, 30300, 30350, 30622, 44100, 44101, 44200) THEN NULL::tsvector ELSE to_tsvector('simple', content) END ) STORED, sig BYTEA NOT NULL, received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), channel_id UUID, deleted_at TIMESTAMPTZ, d_tag TEXT, not_before BIGINT, delivered_at BIGINT, PRIMARY KEY (community_id, created_at, id) ) PARTITION BY RANGE (created_at); -- Direct id lookup: the PK can't serve `WHERE id=$1` because created_at sits -- between community_id and id. This index makes the scoped form -- `WHERE community_id=$ AND id=$` index-served, not a partition scan. CREATE INDEX idx_events_community_id ON events (community_id, id, created_at DESC); -- Hot-path indexes, all community-leading. CREATE INDEX idx_events_community_channel_created ON events (community_id, channel_id, created_at DESC, id); CREATE INDEX idx_events_community_pubkey_kind_created ON events (community_id, pubkey, kind, created_at DESC, id); CREATE INDEX idx_events_community_kind_created ON events (community_id, kind, created_at DESC, id); CREATE INDEX idx_events_community_deleted ON events (community_id, deleted_at); -- Addressable (replaceable) and NIP-33 parameterized lookups. CREATE INDEX idx_events_addressable ON events (community_id, kind, pubkey, channel_id, deleted_at); CREATE INDEX idx_events_parameterized ON events (community_id, kind, pubkey, d_tag, created_at DESC, id) WHERE d_tag IS NOT NULL AND deleted_at IS NULL; CREATE INDEX idx_events_not_before ON events (community_id, not_before) WHERE not_before IS NOT NULL AND deleted_at IS NULL AND delivered_at IS NULL; -- Full-text search. Minimal GIN over the generated tsvector; community scoping -- is supplied by the community-leading btree filters above (BitmapAnd), so this -- stays a single-column GIN. The search lane confirms the final spelling with -- EXPLAIN before its work lands (Quinn option A; Max's index-spelling caveat). CREATE INDEX idx_events_search_tsv ON events USING GIN (search_tsv); -- e-tag containment (`tags @> '[["e",""]]'`) for the aux closure and #e -- reads. Mirrors migrations/0004; jsonb_path_ops supports exactly @>. CREATE INDEX idx_events_tags_gin ON events USING GIN (tags jsonb_path_ops);