Files
buzz/scripts/maintenance/nip_rs_search_allowlist.sql
Matt Toohey 60256e4323 fix(search): parse kind-0 content once and name the PostgreSQL 16 floor
Addresses the three findings from the review of ca974eb29; no behaviour
changes beyond what it asks.

The kind-0 `name_match` select column parsed `content` four times per
candidate row (one `pg_input_is_valid` plus three `::jsonb` casts), and it
ran for every row the WHERE admitted because the inner DISTINCT ON subquery
has no LIMIT. On a brownfield database the candidate set is exactly the
noisy one this branch fixes, so a short typeahead could spend hundreds of
milliseconds per keystroke in `jsonb_in`. Parse once instead: a
`CROSS JOIN LATERAL (SELECT CASE WHEN pg_input_is_valid(content, 'jsonb')
THEN content::jsonb END AS j OFFSET 0) AS profile` joins only for
`kinds == [0]`, and `name_match` reads `profile.j ->> 'name'`,
`'display_name'`, and `'displayName'`. `OFFSET 0` stops the planner pulling
the subquery up and re-expanding the cast at each reference. A NULL `j`
(invalid JSON) or a non-object document yields NULL from `->>`, an empty
vector, and `false`, so `name_match` is still never NULL and the ORDER BY is
unchanged. Every changed `push` sits inside the `profile_query` branch and
the split literal concatenates to the previous bytes, so the SQL emitted for
non-kind-0 searches is byte-identical.

Verified on PostgreSQL 17.9: with a notice-raising IMMUTABLE stand-in for
the cast, the lateral shape evaluates it 7 times over 7 rows and the
pulled-up shape without `OFFSET 0` evaluates it 21 times; `name_match` is
`false`, not NULL, for `not json`, `[1,2]`, `"wes"`, and `null` content.

Migration 0056 is the first to require PostgreSQL 16, and
`check_function_bodies` made it fail on an older server with a bare
"function pg_input_is_valid(text, unknown) does not exist". A preflight
`DO` block at the top now raises "Buzz requires PostgreSQL 16 or later
(migration 0056 uses pg_input_is_valid); this server is <version>" before
CREATE FUNCTION runs. The embedded-migrator content test asserts the
preflight precedes CREATE FUNCTION. README.md and the CONTRIBUTING.md
prerequisites table record the floor, noting that both Compose bundles ship
17 so it only matters for a database you run yourself. 0056 is unmerged, so
only development databases that already applied this branch's earlier
version see its checksum change.

`scripts/maintenance/nip_rs_search_allowlist.sql` now runs the same
`to_regprocedure('profile_search_tsv(text)')` precheck as
`profile_search_text_fields.sql`, after BEGIN and lock_timeout but before
ALTER TABLE, so a database that has not started on 0056 fails with the
readable instruction before taking ACCESS EXCLUSIVE. Exercised against an
empty scratch database: BEGIN, SET, the instruction, exit 3, no lock.

Verified with the lld linker workaround for sqlx proc-macros on this
machine: `scripts/postgres-test-run.sh -p buzz-search --tests` (23 passed,
including the four kind-0 tests and the two pre-existing ones);
`scripts/postgres-test-run.sh -p buzz-db --lib -E 'test(migration::postgres_tests)'`
(12 passed); `cargo test -p buzz-db --lib runtime::migration` (14 passed);
`cargo fmt --all --check`; and `cargo clippy -p buzz-search -p buzz-db
--all-targets -- -D warnings`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-10-02 14:29:16 +10:00

36 lines
1.6 KiB
PL/PgSQL

-- OUT-OF-BAND MAINTENANCE: do not run from relay startup migrations.
--
-- This rewrites every events partition and rebuilds the partitioned GIN index.
-- Run only in a maintenance window after confirming enough free space for the
-- replacement heap/TOAST/index files plus WAL. ALTER TABLE takes ACCESS
-- EXCLUSIVE, so event reads and writes block until this transaction commits.
-- Consider combining this with the planned partition repack/reclaim operation.
--
-- The result is the expression migration 0056 gives empty installations:
-- kind 0 routes through profile_search_tsv(content) (created by 0056 at relay
-- startup, so run this after the relay has started on 0056; the guard below
-- refuses otherwise). That keeps this rewrite from undoing
-- scripts/maintenance/profile_search_text_fields.sql.
BEGIN;
SET LOCAL lock_timeout = '5s';
-- Fail with an instruction before ALTER TABLE takes ACCESS EXCLUSIVE, rather
-- than inside the rewrite with a bare "function does not exist".
DO $$
BEGIN
IF to_regprocedure('profile_search_tsv(text)') IS NULL THEN
RAISE EXCEPTION 'profile_search_tsv(text) is missing; start the relay once so migration 0056 creates it, then rerun';
END IF;
END $$;
ALTER TABLE events DROP COLUMN search_tsv;
ALTER TABLE events ADD COLUMN search_tsv TSVECTOR GENERATED ALWAYS AS (
CASE WHEN kind = 0 THEN profile_search_tsv(content)
WHEN kind IN (9, 40002, 45001, 45003) THEN to_tsvector('simple', content)
ELSE NULL::tsvector
END
) STORED;
CREATE INDEX idx_events_search_tsv ON events USING GIN (search_tsv);
COMMIT;