Conversation
… raise the GFQL error Review follow-up on #2086. Two contract divergences, both found by auditing the admission boundaries and both reproduced with the served leg proven to have taken the route. 1. `rows(attach_prop_columns=...)` and `rows(attach_prop_aliases=...)` were silently ignored. The suffix check rejected `source`, `alias_endpoints`, `alias_prefilters` and a non-node `table`, but not those two, so this route answered a query the canonical polars route refuses outright with NotImplementedError. The answer looked right, which is what makes it worse: parity-or-error was broken quietly. Admission is now an allow-list, so a rows() parameter added later declines until someone teaches this route what it means. Rejecting known-bad params fails open; admitting known-good fails safe. 2. A repeated `select` output name surfaced polars' own DuplicateError instead of GFQLTypeError [invalid-node-reference]. The projection now declines on a repeated name and the canonical route raises its own error. Both pinned by tests that were verified against the reinstated bugs. The suite's own vacuity is fixed with them. `_both` turns point-rows off -- it was admitting several of these shapes first, so both legs ran a different route and the comparison said nothing about this code -- and asserts the array route actually served. That assertion immediately failed six existing cases. The four decline cases are now explicit negative tests, and the index-policy test states which policy serves rather than assuming all three do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
|
You were right to push on this, and it found real defects. I audited every admission/decline boundary in the diff against the tests, and the honest answer was that the suite looked broader than it was. Two contract divergences, both fixed (
|
| query | gate calls / query | decline ms / query | share of query |
|---|---|---|---|
| seed-lookup | 0.0 | 0 | 0% |
| message-content | 0.0 | 0 | 0% |
| message-creator | 0.0 | 0 | 0% |
| new-topics | 1.0 | 0.00513 | 0.008% |
Three of the four non-qualifying queries never reach the gate at all; the fourth pays 5 microseconds.
A query-level route-on/route-off comparison over 6 alternating rounds reads seed-lookup +1.55%, message-content −1.42%, message-creator +7.88%, new-topics +2.36% against −63.9% and −68.3% for the two that serve. The +7.88% is noise, and the gate timing proves it rather than arguing it: that query calls the gate zero times, so this route cannot be responsible for any part of its number.
Local suite at this head: 13250 passed, 24 failed — the 24 are a polars-conformance set that reproduces identically on the untouched parent commit.
|
Three more commits on this branch since the comment above, head is now
Still unpinned and worth naming rather than hiding: undirected and variable-length hops all decline today, but nothing distinguishes why, so a regression that flipped one sub-condition would be caught by no test. |
… raise the GFQL error Review follow-up on #2086. Two contract divergences, both found by auditing the admission boundaries and both reproduced with the served leg proven to have taken the route. 1. `rows(attach_prop_columns=...)` and `rows(attach_prop_aliases=...)` were silently ignored. The suffix check rejected `source`, `alias_endpoints`, `alias_prefilters` and a non-node `table`, but not those two, so this route answered a query the canonical polars route refuses outright with NotImplementedError. The answer looked right, which is what makes it worse: parity-or-error was broken quietly. Admission is now an allow-list, so a rows() parameter added later declines until someone teaches this route what it means. Rejecting known-bad params fails open; admitting known-good fails safe. 2. A repeated `select` output name surfaced polars' own DuplicateError instead of GFQLTypeError [invalid-node-reference]. The projection now declines on a repeated name and the canonical route raises its own error. Both pinned by tests that were verified against the reinstated bugs. The suite's own vacuity is fixed with them. `_both` turns point-rows off -- it was admitting several of these shapes first, so both legs ran a different route and the comparison said nothing about this code -- and asserts the array route actually served. That assertion immediately failed six existing cases. The four decline cases are now explicit negative tests, and the index-policy test states which policy serves rather than assuming all three do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
e875f31 to
b4a82f3
Compare
…rrays A seeded pattern feeding a bounded projection no longer materializes a frame per hop and a full bindings table. The traversal keeps row positions per alias in index arrays, and only the projected columns are ever built. Filters still run through the unchanged filter on a narrow gathered frame, the seed still goes through the property-index seam, and the same served trace step is recorded, so index-engagement receipts are unchanged. Polars and directed fixed hops only; everything else declines to the canonical route, which is the oracle for the new differential suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
CI's routes-off lanes caught it: a new hot path the route registry does not know stays on when the indexed kernel is switched off. It registers as polars-bindings-select, and switching indexed-kernel off switches it off too, since it is a second implementation of that kernel. The chain imports the symbol at module scope so the switch can patch what the chain actually calls, and the differential tests now disable through that same switch rather than the defining module, which the hoisted import would have left bound. Adds the polars per-file coverage floor the lane requires for the new file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
… raise the GFQL error Review follow-up on #2086. Two contract divergences, both found by auditing the admission boundaries and both reproduced with the served leg proven to have taken the route. 1. `rows(attach_prop_columns=...)` and `rows(attach_prop_aliases=...)` were silently ignored. The suffix check rejected `source`, `alias_endpoints`, `alias_prefilters` and a non-node `table`, but not those two, so this route answered a query the canonical polars route refuses outright with NotImplementedError. The answer looked right, which is what makes it worse: parity-or-error was broken quietly. Admission is now an allow-list, so a rows() parameter added later declines until someone teaches this route what it means. Rejecting known-bad params fails open; admitting known-good fails safe. 2. A repeated `select` output name surfaced polars' own DuplicateError instead of GFQLTypeError [invalid-node-reference]. The projection now declines on a repeated name and the canonical route raises its own error. Both pinned by tests that were verified against the reinstated bugs. The suite's own vacuity is fixed with them. `_both` turns point-rows off -- it was admitting several of these shapes first, so both legs ran a different route and the comparison said nothing about this code -- and asserts the array route actually served. That assertion immediately failed six existing cases. The four decline cases are now explicit negative tests, and the index-policy test states which policy serves rather than assuming all three do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
numpy promotes (int64, uint64) to float64, which cannot tell 2^53+1 from 2^53+2, so an id->row probe that promoted would land on the wrong node. The probe only promotes when the two sides differ, and the schema-equality guard already declines that case -- but nothing said so. Two tests now do: uniform UInt64 ids above 2^53 serve and match the canonical route exactly, and UInt64 node ids against Int64 endpoints decline instead of promoting. That is what makes the seam unreachable from the public surface rather than merely unlikely to be reached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
Asserting that the route actually served turned every case in this file into an engagement pin, and the routes-off lanes switch that route off for the whole session -- so `gfql-routes-off (indexed-kernel)` reported 52 divergences that were all the same assertion firing where the route is deliberately disabled. The repo already has the contract for this: a test that asserts a route SERVES carries `@pytest.mark.route_engaged(...)` and is skipped when one of its routes is off, so the replay reports result divergences only. Applied at module level, since every case here asserts it. The file still runs in the other eight lanes, including polars-point-rows, which is the one where it gains cases rather than losing them. Verified across all ten modes: 63 passed in eight, skipped in indexed-kernel and polars-bindings-select. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
`_seed_rows` has two branches: seed on the graph's own id column, and seed on some other column through the node property index. Every case in this file seeded on the id column, so the second branch was never executed by any test -- and it is also the only branch that reports public_seed_scan in the trace. Two tests now cover it: a multi-seed shape compared against the canonical route with a non-empty expected row set, and a direct pin that the property-index seam is the one that ran, so the first cannot start passing through the id branch unnoticed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
…dary The admission is an allow-list precisely so that a parameter nobody thought of declines instead of being answered against its own meaning -- but only two of the seven were tested, which is the same shape of gap the allow-list exists to close. A sweep now decides all of them plus the two allowed forms: bare rows() and rows(table="nodes") and an explicit matching binding_ops SERVE; table="edges", source, alias_endpoints, alias_prefilters, attach_prop_columns and attach_prop_aliases DECLINE. Each case asserts which side it is on AND that the served and canonical legs agree, including agreeing on the exception type when the canonical route raises. Verified against the real surface: all eight behave as the sweep asserts, and the duplicate-projection-name case raises the same GFQLTypeError code on both legs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
The parent PR's end-to-end case now asserts that the indexed kernel actually served, and this branch adds a SECOND implementation of that kernel which answers first -- so the assertion fired, correctly, on five parameters. The array route has its own differential suite; this case is about the frame kernel, so it switches that route off alongside the other siblings. Without the assertion the case would have silently started comparing the array route against itself, which is the failure mode the assertion exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
b4a82f3 to
c8e835b
Compare
Problem
After #2084 the seeded fixed-hop bindings path still builds a frame at every hop and a full bindings table before projecting. On LDBC SNB IC8
recent-repliesthat is about eight Polars operations per hop over ~2k-row frames, each paying fixed plan cost, plus an attach-and-sort over 2114 rows whose six projected columns are all the query ever reads. Two of the four aliases in that query need no properties at all, yet their full 32-column node rows are gathered on every hop.A probe against the same indexes measured the ceiling: the identical result set, computed entirely in index arrays, takes 1.8 ms where the current route takes 9.9.
Change
A new specialization recognizes a seeded fixed-hop pattern whose
rows()is immediately followed by a boundedselect, and runs it without materializing anything but the projected columns.graphistry/compute/gfql/index/array_bindings.pycarries the traversal. A path bag is one row position per alias per matched path, held in index arrays. Hops walk the same CSR adjacency indexes, orientation and path ordering arelexsortplussearchsortedranges, and the endpoint join is the range expansion that perf(gfql): cut per-hop and rows-stage overhead for seeded polars chains #2084 already introduced for the frame path.chain_specializations/bindings_select.pygates and projects. It admits only Polars, directed fixed hops, an indexed seed, and a projection whose every item is a literal, a bare node alias, oralias.columnover a column that exists. Everything else declines to the canonical route.Three behaviors are preserved deliberately, because each is observable:
filter_by_dicton a narrow gathered frame, so three-valued and dtype semantics stay the engine's own._seed_rows_via_property_indexas a module attribute, because the benchmark harness counts that seam by patching it.Measured
LDBC SNB SF0.1, the H684 index lane recipe, local box.
Point queries are unchanged: seed-lookup 1.18, message-content 0.42, message-creator 0.61.
DGX A/B on the benchmark recipe
Measured at 4c1912a. The head since then differs only by route registration, the polars
coverage floor, the CI matrix and test wiring; the sole product-code change is hoisting one
import to module scope so the route switch can patch what the chain calls, which does not
touch the served path.
Three runs per arm against merged master 65c359b, same H684 recipe, canonical rows identical in every cell.
The pandas gain comes from #2084, which this branch includes; the specialization itself is Polars-only.
Every other cell is flat or inside that cell's own noise. Three deserve naming rather than burying:
Against the competitor arms at SF0.1, message-replies now beats Kuzu's 23.712 by about eight times. recent-replies is still a loss against Neo4j's 5.425 and Memgraph's 6.333, but by roughly two times rather than the five it was. The remaining budget is measured and tracked: about 2.3 ms in filters that still route through polars, 0.6 in projection, and 2.1 in dispatch.
Tests
graphistry/tests/compute/gfql/index/test_array_bindings_select.pyis a differential suite against the canonical route as oracle. Every case runs twice, once served and once with the specialization disabled, and the two frames must match in columns, dtypes, values and order. It covers seven pattern shapes crossed with four projection shapes, edge payload projection with ordering, all three index policies, declines for duplicate node ids, string ids, null endpoints and missing indexes, index-trace equality between the two routes, and a 24-seed fuzz over random graphs, hop counts, directions, filters and projections. A guard asserts the corpus actually serves, since a differential suite that silently stopped serving would pass while proving nothing.That suite caught a real defect during development: literal projection items were built from a Python list, giving Int64 where the canonical route lowers through
pl.litand gives Int32.Broad local suite over
tests/compute/gfql, chain specializations, chain and hops: 16126 passed, 3 failed, matching the #2084 baseline exactly (two lane-registration tests for the new file, since fixed, and one cuDF zero-hop test that fails identically on master with the local cuDF version). Type-hygiene and comment-encoding guards pass with both new files at baseline zero; mypy and ruff clean.Stacked on #2084; review that first. Base moves to master once #2084 lands.
🤖 Generated with Claude Code
https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp