Repository navigation
buzz-search: read profile names without the profile_name_fields function - #9397
Conversation
Profile name search now extracts `name` and `display_name` from kind:0
content with an inline SQL expression instead of calling the plpgsql
function `profile_name_fields(text)`. Content PostgreSQL cannot read as
jsonb still has no names rather than failing the query: `pg_input_is_valid`
screens out what the function's exception handler caught, except nesting
deep enough to overflow the parser's stack, which it cannot catch. Content
with more than 4096 `[` and `{` is therefore never parsed. Ingest already
rejects kind:0 nested deeper than 128 levels, so only content stored some
other way is affected.
The function stays in the schema; a later migration drops it once this
release has shipped.
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Luke Tornquist <[email protected]>
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 No blocking findings. Two independent reviews of this commit (a source trace and a run against real Postgres) agree.
- Same results as
profile_name_fields. I ran the new SQL on a throwaway Postgres 16 container against a valid object; a top-level array, string, number andnull; non-string names;\u0000; a lone surrogate;1e1000000; truncated JSON; empty and NULL content; nesting 4095 deep (just under the bound) and 20k deep (over it); and duplicate keys. Only the objects returned names, and nothing raised. The function's object check carries over because->returns NULL on a non-object. - Search query unchanged otherwise. The community, kind, deletion and channel-less filters, the matching, the per-person ranking, the paging and the verified-profile branch all behave as before.
- PostgreSQL 16 floor.
IS JSON OBJECTinbuzz-dbalready needs 16, sopg_input_is_validdoesn't raise the minimum. - Ingest claim holds. kind:0 content goes through
serde_json::from_strat ingest, which stops at 128 levels of nesting by default. - Retirement order. Keeping the function until a later release follows the deploy-before-contract rule in
docs/database.md. - CI. The PostgreSQL 16 lane passed, including all four profile-name tests. The red Security check is the
hickory-netadvisory, and this PR doesn't change the lockfile.
One nonblocking note is inline.
The 4096-bracket bound in PROFILE_NAME_FIELDS keeps the jsonb parse inside PostgreSQL's default 2 MB max_stack_depth. Say so in the doc comment: at a much lower setting, deeply nested content stored outside ingest can still fail the search. Comment only; no behavior change. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
kazemek21
left a comment
There was a problem hiding this comment.
Kyle reviewed this PR and the Nexus review-tracker notes and approved with no comments.
Posted by Codex (gpt-6.1-sol) on behalf of kylez.
Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz> * origin/main: fix(mobile): require Builderlab consent for existing identities (#8823) feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897) fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425) fix(mobile): show shared chats under Solo (#9348) chore(release): release Buzz Desktop version 0.5.29 (#9456) feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945) fix(acp): bound observer retention by bytes (#7987) (#9436) Tune release codegen for faster/smaller binaries (#9434) feat(desktop): add dismissible banner pointing to the new buzz app (#9379) fix(mobile): keep publishing presence while the app is open (#9441) feat(mobile): share into Buzz from the iOS share sheet (#9433) fix(mobile): qualify every namesake mention and keep authored mention casing (#9325) fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437) fix(mobile): play short-delay animation frames at browser speed (#9432) buzz-search: read profile names without the profile_name_fields function (#9397) buzz-db: build the community deletion lock key in Rust (#9396) fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417) test(mobile): run push tests without a gateway dart-define (#9423) fix(mobile): never pop Home when archiving from channel settings (#9387) perf(acp): reduce observer CPU and lock overhead (#9381) Signed-off-by: Matthew Boston <[email protected]>
…count-backfill Co-authored-by: mboston.Engineer <932d3b6ebea6cc83e091551ae46d1c7e7f8f7d0fa8e9cef5e6facc8544af413b@buzz.block.builderlab.xyz> * bostonaholic--agent-reply-count: fix(mobile): require Builderlab consent for existing identities (#8823) feat(mobile): use HTTP/2 on iOS through app-wide URLSessions (#8897) fix(relay): publish a git push only when refs changed; check the protocol by refinement (#9425) fix(mobile): show shared chats under Solo (#9348) chore(release): release Buzz Desktop version 0.5.29 (#9456) feat(mobile): render messages with gpt_markdown 1.3.4 plusparse (#8945) fix(acp): bound observer retention by bytes (#7987) (#9436) Tune release codegen for faster/smaller binaries (#9434) feat(desktop): add dismissible banner pointing to the new buzz app (#9379) fix(mobile): keep publishing presence while the app is open (#9441) feat(mobile): share into Buzz from the iOS share sheet (#9433) fix(mobile): qualify every namesake mention and keep authored mention casing (#9325) fix(mobile): match browser GIF timing in the iOS reaction sheet (#9437) fix(mobile): play short-delay animation frames at browser speed (#9432) buzz-search: read profile names without the profile_name_fields function (#9397) buzz-db: build the community deletion lock key in Rust (#9396) fix(cli): pair for the active community, skip stopped desktops, accept a bare relay host (#9417) test(mobile): run push tests without a gateway dart-define (#9423) fix(mobile): never pop Home when archiving from channel settings (#9387) perf(acp): reduce observer CPU and lock overhead (#9381) Signed-off-by: Matthew Boston <[email protected]>
What
This is phase 1 of 2 for retiring the plpgsql function
profile_name_fields(text). Its only caller, profile name search (search_profile_names), now getsnameanddisplay_namefrom an inline SQL expression (PROFILE_NAME_FIELDSincrates/buzz-search/src/query.rs) instead.The function's exception handler made unreadable kind:0 content have no names rather than fail the search. The inline version keeps that guarantee in two steps:
pg_input_is_valid(content, 'jsonb')rejects the soft parse errors: a\^@escape, a lone surrogate, a huge number, and malformed JSON.max_stack_depth, it raisesstack depth limit exceeded, andpg_input_is_validdoes not catch that. I confirmed this on PG 17: depth 10k is fine and 20k raises. So content with more than 4096[and{is never parsed and has no names. The count bounds the depth. Ingest already rejects kind:0 content nested deeper than 128 levels (serde_json), so this only matters for content stored another way.OFFSET 0keeps the parse from being copied into each reference todoc, so each candidate row is parsed once, plus one validity pass. The plpgsql function also parsed each row once.Behavior change (edge only): a kind:0 with more than 4096 brackets, even inside strings, is no longer findable by name. Real profiles don't come close, and a
picturedata URL is base64 with no brackets.pg_input_is_validneeds PostgreSQL 16 or later. CI runs 16, and the dev compose runs 17.Phase 2 (not here): a migration and desired-state change drops the function, its
schema/functions/file and itsschema.sqlentry once this release has shipped.Tests
profile_names_survive_unparsable_contentalready stores a\^@profile, a lone-surrogate profile, a1e1000000profile and a 60k-deep profile next to a plain one, and requires the search to return only the plain one. It now also covers the bound itself: a profile with exactly 4096 brackets, all but one inside a string, is still found.What got simpler
Profile search no longer depends on a database function. Name extraction lives next to the rest of the search SQL it feeds.
Verification
Blox, at
be0db303ea90033cc2c37ac8f8f07c27f4b08e31(clean tree):cargo fmt --all --checkcargo clippy -p buzz-search --all-targets -- -D warningscargo nextest run -p buzz-search --libscripts/postgres-test-run.sh -p buzz-search --tests -E 'test(/profile_names/)'Probe on PostgreSQL 17.11:
pg_input_is_validreturns false for\^@, a lone surrogate,1e1000000and truncated JSON; nesting depth 10k is valid; depth 20k raisesstack depth limit exceeded. CI runs the full Postgres lanes.🤖 Generated with Claude Code
buzz-review-completed