Conversation
An axis step with a name test returned the correct result on its first evaluation and an empty sequence on every evaluation after that, within a single query, whenever the context node carried a predicate context item. Silent -- no error, no warning, and data loss proportional to loop length. LocationStep caches its structural-index lookup in currentSet for the whole query execution; resetState() clears it only between executions, so a loop inside one query reuses it every time. NewArrayNodeSet's sibling and preceding/following select methods then stamp those cached NodeProxy objects in place with the matching reference node's context, and a duplicate-suppression guard reads those stamps back to decide whether it has already handled a node. On the second evaluation it is reading stamps left by the first, so it suppresses every node. Clearing predicate contexts on currentSet when the cached set is reused, rather than freshly built, leaves the guard seeing only stamps from the current evaluation. A freshly built set carries no contexts, so only the reuse path needs it. Applied at the two call sites that feed the guarded select methods, getSiblings and getPrecedingOrFollowing. The guard itself is still needed: with it disabled the new regression test passes, but ten existing tests fail across axes.xql, npt.xqm and positional-nested.xql. It suppresses genuine duplicates between different reference nodes within one evaluation. The bug is the stale state it reads, not the guard. Known limitation: the previous evaluation's result set holds the same NodeProxy instances as the cached set, so clearing at the start of evaluation n retroactively mutates the result of evaluation n-1. No case where that is observable could be constructed -- it is harmless under count() and in every test here -- but it is a real property of this approach, and the reason an alternative candidate exists. The range index named in the report is not required to trigger this; any predicate produces the context item that does. Nor is it a regression: both the caching and the guard are identical in the eXist-6.4.1 tag and current develop. SiblingAxisRepeatedEvaluationRegressionTest evaluates each affected axis four times over the same node and asserts stability, pinning the wildcard form, a node reached without a predicate, an unaffected axis, and a non-matching name alongside. Two of its eight cases fail on unfixed develop; the preceding:: and following:: cases pass either way and are pins rather than demonstrations. Addresses eXist-db#6690 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📊 XQTS result comparisonComparison of this run against Warning 70 test cases were recorded in only one of the two runs (0 only in the previous run, 70 only in the current run). The runner's JUnit output is not fully deterministic (see eXist-db/exist-xqts-runner#74), so totals and per-category deltas include recording noise; the newly passing/failing lists count only tests recorded in both runs.
Relative to 🔴 Newly failing tests (1)
⚪ Recorded only in this run (70)
Runtime: 369.7s (-39.28s vs |
|
[This response was prompted by Joe, drafted by Claude Code, and reviewed by Joe.] Converting this to a draft. @duncdrum has said on #6700 that candidate B (#6702) looks like the better direction, and I agree — so unless the discussion there turns, this PR is likely to be closed rather than merged. Leaving it open as a draft for now for two reasons: #6700 is still the live decision and it is easier to compare two things that both exist, and the trade-off recorded in this PR's description and commit message is the clearest statement of what candidate B is buying us. If B lands, I will close this. Note it now conflicts with |
[This PR was prompted by Joe, drafted by Claude Code, and reviewed by Joe.]
This is one of two candidate fixes for #6690. See #6700 for the design question, the trade-offs, and a link to the alternative candidate. Please do not merge this without reading that issue — the two PRs are deliberately competing, not stacked.
Addresses #6690
Summary
An axis step with a name test returned the correct result on its first evaluation and an empty sequence on every evaluation after that, within a single query, whenever the context node carried a predicate context item. Silent — no error, no warning, data loss proportional to loop length.
What Changed
exist-core/src/main/java/org/exist/xquery/LocationStep.javaLocationStepcaches its structural-index lookup incurrentSetfor the whole query execution, andNewArrayNodeSet's sibling and preceding/following select methods stamp those cachedNodeProxyobjects in place with the matching reference node's context. A duplicate-suppression guard then reads those stamps back — and on the second evaluation it is reading stamps left by the first, so it suppresses every node.This change clears predicate contexts on
currentSetwhen the cached set is reused rather than freshly built, so the guard only ever sees stamps from the current evaluation. A freshly built set carries no contexts, so only the reuse path needs it.Applied at the two call sites that feed the guarded select methods:
getSiblingsandgetPrecedingOrFollowing.Why not simply remove the guard
The guard is still needed. With it disabled the new regression test passes, but ten existing tests fail across
axes.xql,npt.xqmandpositional-nested.xql— including the file the guard's original 2019 commit added its tests to. It suppresses genuine duplicates between different reference nodes within one evaluation. The bug is the stale state it reads, not the guard.Known limitation
The previous evaluation's result set holds the same
NodeProxyinstances as the cached set, so clearing at the start of evaluation n retroactively mutates the result of evaluation n-1. I could not construct a case where that is observable — it is harmless undercount()and in every test we have — but it is a real property of this approach and the main reason candidate B exists. This is discussed in #6700.Test Plan
SiblingAxisRepeatedEvaluationRegressionTestfails on unfixeddevelop, passes with the fixxquery.CoreTests— the wholesrc/test/xquerycorpus, including the ten tests naive guard removal breaks — greenmvn testonexist-coregreenlicense:checkcleanScope
Two call sites in one class. No change to the node-set algorithms or to any hot path.
🤖 Generated with Claude Code