Conversation
EnRaiha
force-pushed
the
fix/issue296-graph-cursor
branch
from
September 17, 2026 02:16
befb0de to
94214ab
Compare
A mistyped clause keyword (`DEPTS` for `DEPTH`) defaulted the clause and left its value unread, so the statement answered a different question than it asked, with no error. The clause readers were independent forward scans that ignored every token between a keyword and its value. A cursor now claims the tokens each clause consumes, and the dispatcher refuses the first token no clause claimed, naming it. All statement variants are covered; `MATCH` statements are untouched.
Fusion parameters were read from a raw token slice and the variant then claimed every remaining token, so a mistyped option keyword ran the statement with a default and no error. The shared extractor now reads through the cursor: each keyword and value is claimed, the ARRAY[...] payload is claimed element by element, and a token no clause owns is refused by name. A present-but-unreadable value is an error, never a default. - drop the raw-text readers; the ARRAY payload and the RRF_K count come from the same token stream - the BM25 field's ON is read after the BM25 anchor: ON also introduces the collection, and the first match is not the field's - the wrapped SEARCH ... USING FUSION(...) surface validates with the same extractor and refuses unclaimed tokens
EnRaiha
force-pushed
the
fix/issue296-graph-cursor
branch
from
September 17, 2026 04:38
94214ab to
ddf5394
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The graph DSL clause readers were independent forward scans over a flat token list. A mistyped clause keyword (
DEPTSforDEPTH) left its value unread, the clause kept its default, and the statement answered a different question than it asked — with no error anywhere. An audit of the parser showed the same silent-default class across every multi-word clause, and forGRAPH RAG FUSIONthe exemption was explicit: the variant calledconsume_rest(), claiming every remaining token so the dispatcher's unclaimed-token check could never fire.This PR makes token consumption explicit. Every clause reads through a claim-tracking cursor; a token no clause claims is refused by name; a clause that is present but unreadable is an error, never a default.
Behaviour changes
GRAPH TRAVERSE … DEPTS 3(mistyped clause keyword)DEPTS 3went unread42601, the error namesDEPTSGRAPH RAG FUSION … VECTOR_TOPK 542601, namesVECTOR_TOPK… VECTOR_TOP_K five42601, namesVECTOR_TOP_KRRF_K (60.0)orRRF_K (1,2,3,4)42601; two or three numbers requiredBM25 'text' ON 'field'in the DSLfieldSEARCH c USING FUSION(… VECTOR_TOPK 5)42601, names the tokenCloses #296.
Root cause
&[Tok](quoted_after,word_after,usize_after,float_pair_after,float_triple_after,array_floats_after). Each searched forward for its keyword and ignored everything between keywords.Cursor::finish), butGRAPH RAG FUSIONbypassed it withconsume_rest()because its parameter extractor read theARRAY[…]payload from the raw statement text and therefore claimed no tokens.FusionParams::extractvalidated nothing: an absent or misspelled keyword producedNonefor that parameter and the executor applied its default. The extractor's own doc comment claimed it refused malformed options; it did not.floats_after(originally added for the fusionRRF_Kpair) had no library caller at all — tests only.What changed
Commit
62cf9dd34— the cursor and the per-variant rewiring:graph_parse/cursor.rs(new): one claim-tracking reader per token shape —claim_text,find,word_after,quoted_after,quoted_after_from,object_after,usize_after,usize_after_checked,float_after,direction_after, andfinish, which refuses the first token no clause owns, naming it.parse_traverse,parse_path,parse_algo,parse_rag_fusion,parse_insert_vertex/parse_insert_edge, …) reads through the cursor.MATCHstatements are untouched.entry.rsruns the dispatcher andfinishwith the statement label, so a refusal names the statement and the token.Commit
94214abad— the fusion rework:FusionParams::extracttakes&mut Cursorand reads every option through it: keywords and values are claimed, so a mistyped option keyword is refused by name instead of defaulting.usize_after_checkedrefusesVECTOR_TOP_K five;floats_after_maxrefusesRRF_K fastand accepts exactly two or three numbers (RRF_K (60.0),RRF_K (1,2,3,4)refused).QUERY ARRAY[…]payload is claimed element by element from the same token stream (Cursor::floats_array_afterclaims the anchor, theARRAYword, and the numeric run). No raw-text reader remains;helpers.rskeeps onlymissing_clause.consume_rest()is deleted. The only exemption in the parser is gone.ONis read withquoted_after_fromafter theBM25anchor:ONalso introduces the collection, and the first match was the collection name, not the field's. This was a latent mis-parse the new wire test surfaced.floats_afterdead-code item is resolved by deletion, replaced byfloats_after_max(its test rewritten): the accessor now has a real library caller.parse_search_using_fusionreturnsResult<Option<…>>and runsfinishon its own cursor, so the wrappedSEARCH … USING FUSION(…)surface validates with the same extractor. The one caller maps the error to42601.Regression proof
On base
main, without this change, four wire tests fail: a mistyped clause keyword, a stray literal, a mistyped fusion option keyword, and an unreadable option value. With this change all four pass, and the neighbouring DSL and fusion modules pass unchanged.Tested
cargo nextest run -p nodedb-sql --lib— 899 passed (includes the new cursor unit tests, the fusion-parameter unit tests, and the DSL dispatcher tests inentry.rs)cargo nextest run -p nodedb --test wire -E 'test(cases::graph_rag_fusion) | test(cases::sql_three_source_rrf) | test(cases::graph_dsl_handlers)'— 36 passedcargo fmt --all -- --check— cleancargo clippy -p nodedb-sql -p nodedb --lib -- -D warnings— cleanReview
A separate, read-only parity audit ran over this branch before submission, covering test-inventory parity, caller closure, producer/consumer enumeration, invariant chokepoints, module contracts, and repository norms. All items passed with no blockers. The items it could not execute read-only are covered here: the fail-before evidence is in Regression proof, the full test count is measured by CI, and cross-crate consumers are covered by the workspace build.
Exclusions
MATCHstatements are deliberately untouched (their parser was not part of the defect).floats_afteris removed because it had no caller.Commits
62cf9dd34—fix(sql): track token consumption in the graph DSL94214abad—fix(sql): read fusion options through a claim-tracking cursor