mirror of
https://github.com/block/buzz.git
synced 2026-11-12 20:31:12 +00:00
## Summary **Intent:** let clients find and count reactions to an **addressable event** at the SQL layer. Addressable events are NIP-01 kinds 30000–39999, which NIP-33 called "parameterized replaceable events". Clients reference one by its address, not its event id. An addressable event is identified by its address `<kind>:<pubkey>:<d>`. Each edit publishes a new event with a new id at the same address. A reaction (kind:7) that tags the address with `a` therefore still applies after the target is edited. A reaction that tags only the current revision's id with `e` is left behind on the old revision. Use cases: - **Agent personas (kind:30175).** A ⭐ on a shared persona is a kind:7 tagged `["a", "30175:<owner>:<slug>"]`. A client that lists shared personas by popularity needs `{kinds:[7], "#a":[...]}` and the matching COUNT to return every star across persona edits, not only stars on the current revision. - **Other addressable kinds**, such as teams and channel metadata, work the same way with no extra code. - **Replaceable events, such as user profiles.** Kind:0 profiles and kind:10100 agent profiles are *replaceable* (kinds 0, 3 and 10000–19999), not addressable. NIP-01 still gives them an `a` address with an empty `d`, `0:<pubkey>:`, so reactions that tag a profile that way are matched by the same filter. Before this change, the relay applied `#a` only after the SQL `LIMIT`. A selective read fetched the newest kind:7 rows across the whole community and then dropped the ones that didn't match. It could return fewer than `limit` matches, or none, while older matches existed. COUNT also could not take its exact fast path. What changed: - **`EventQuery.a_tags`** (buzz-db) works like `e_tags`. It is an exact positional tag match (`tag->>0 = 'a' AND tag->>1 = ANY(..)`) with JSONB `@>` as the prefilter on the existing GIN index `idx_events_tags_gin`. - **No schema change, migration or backfill.** - It gets the same MATERIALIZED CTE fence as `#e`, so the planner cannot walk `created_at` backward looking for a rare match. - It reads the table whole rather than recent partitions first. - It is counted in `count_events_sql`. - An empty set matches nothing, as the NIP-01 post-filter does. - **`filter_to_query_params`** fills `a_tags` for every filter, and **`filter_fully_pushable`** treats `#a` as pushed. REQ, COUNT and the HTTP bridge `/query` and `/count` all go through these two functions. - **`per_channel_scope`** now skips per-channel reads for every fenced query, through a shared `reference_tag_fence` helper. Before this, it excluded `#e` reads explicitly, and `#a` with a multi-value `#h` would have wrapped the fence around the per-channel lateral, which is invalid SQL. Not included: `#k` and other low-cardinality tags. Postgres's estimate for JSONB `@>` assumes any match is rare, so pushing a common value could switch to a slower plan. They remain post-filters. ### Related issue None found. ### Testing - `req.rs` unit test: `#a` maps to `a_tags` (including empty and absent) and is fully pushable; `#a` + `#k` is not. - buzz-db shape tests: an `#a` read is fenced, COUNT applies the match, an `#a` read is never split into recent and older passes, and `#a` makes a read ineligible for per-channel scope. An empty `#a` is answered without a database read. - buzz-db Postgres: an `#a` case added to `pushed_down_tag_filters_reject_positional_decoys`, covering query and COUNT. - New `handlers/tag_pushdown_postgres_tests.rs`: - REQ returns an older `#a` match ahead of a full page of newer reactions to another address. COUNT is exact, and an empty `#a` counts 0. This test fails when `a_tags` is removed from `filter_to_query_params`. - REQ and COUNT with `#a` + a multi-value `#h`. This test fails, with `CLOSED database error`, without the `per_channel_scope` fix. - Full buzz-db and buzz-relay suites, unit and Postgres lanes, run locally. - Live check against a local relay built from this branch, over WebSocket with NIP-42 auth. Two reactions to one address (each on a different head event, as after an edit) and six newer reactions to another address were published. `REQ {kinds:[7], #a:[needle], limit:2}` returned exactly the two needle reactions, newest first. COUNT returned 2 for the needle, 6 for the hay and 0 for an empty `#a`. `#a` + `#e` with `limit:1` returned the newest needle reaction. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Jemiah Westerman <jemiah@squareup.com> Co-authored-by: Son of Anton <8e0ef2ab02d69a7ac089ef5eca90d1a3fbbdc546f6322dc34d0c4f3deecea7f4@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>