Skip to content

Commit c48b87a

Browse files
claudejoewiz
authored andcommitted
[bugfix] Preceding/following axes miss nodes when predicates add context tracking
Closes #4109. When a context-dependent predicate like [exists(.)] is applied to the input expression of a FLWOR for clause or simple map operator, the self axis evaluation inside the predicate adds context tracking entries to each NodeProxy. These context entries then leak into the cached result nodes of subsequent selectPreceding/selectFollowing calls via copyContext(), causing a false-positive match in the context ID deduplication check. This makes the axis skip valid result nodes after the first match per group. The fix adds a NO_CONTEXT_ID guard to the context deduplication check in both selectFollowing and selectPreceding of NewArrayNodeSet. When contextId is NO_CONTEXT_ID, the step is not inside a predicate context tracking scope, so the deduplication check should not be applied. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent acde823 commit c48b87a

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

exist-core/src/main/java/org/exist/dom/persistent/NewArrayNodeSet.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ public NodeSet selectFollowing(final NodeSet pl, final int position, final int c
792792
if(!reference.getNodeId().isDescendantOf(nodes[j].getNodeId())) {
793793
if(position < 0 || ++n == position) {
794794
if (contextId != Expression.IGNORE_CONTEXT
795+
&& contextId != Expression.NO_CONTEXT_ID
795796
&& nodes[j].getContext() != null
796797
&& reference.getContext() != null
797798
&& nodes[j].getContext().getContextId() == reference.getContext().getContextId()) {
@@ -846,6 +847,7 @@ public NodeSet selectPreceding(final NodeSet pl, final int position,
846847
if(!reference.getNodeId().isDescendantOf(nodes[j].getNodeId())) {
847848
if(position < 0 || ++n == position) {
848849
if (contextId != Expression.IGNORE_CONTEXT
850+
&& contextId != Expression.NO_CONTEXT_ID
849851
&& nodes[j].getContext() != null
850852
&& reference.getContext() != null
851853
&& nodes[j].getContext().getContextId() == reference.getContext().getContextId()) {

exist-core/src/test/xquery/axes-persistent-nodes.xqm

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ function axpn:preceding-with-predicate-db-map() {
102102
! (./@id || ":" || (./preceding::pb[1]/@id, "PRECEDING_PB_NOT_FOUND")[1])
103103
};
104104

105+
declare
106+
%test:assertEquals("w1:pb1", "w2:pb1", "w3:pb1", "w4:pb2", "w5:pb2")
107+
function axpn:preceding-with-context-predicate-db-flwor() {
108+
for $w in doc("/db/test/test.xml")//w[exists(.)]
109+
let $preceding-page := $w/preceding::pb[1]
110+
return
111+
if ($preceding-page) then
112+
$w/@id || ":" || $preceding-page/@id
113+
else
114+
$w/@id || ":PRECEDING_PB_NOT_FOUND"
115+
};
116+
117+
declare
118+
%test:assertEquals("w1:pb1", "w2:pb1", "w3:pb1", "w4:pb2", "w5:pb2")
119+
function axpn:preceding-with-context-predicate-db-map() {
120+
doc("/db/test/test.xml")//w[exists(.)]
121+
! (./@id || ":" || (./preceding::pb[1]/@id, "PRECEDING_PB_NOT_FOUND")[1])
122+
};
123+
105124
declare
106125
%test:assertEquals("w1:pb1", "w2:pb1", "w3:pb1", "w4:pb2", "w5:pb2")
107126
function axpn:preceding-without-predicate-flwor() {
@@ -161,6 +180,25 @@ function axpn:following-with-predicate-db-map() {
161180
! (./@id || ":" || (./following::pb[1]/@id, "FOLLOWING_PB_NOT_FOUND")[1])
162181
};
163182

183+
declare
184+
%test:assertEquals("w1:pb2", "w2:pb2", "w3:pb2", "w4:pb3", "w5:pb3")
185+
function axpn:following-with-context-predicate-db-flwor() {
186+
for $w in doc("/db/test/test.xml")//w[exists(.)]
187+
let $following-page := $w/following::pb[1]
188+
return
189+
if ($following-page) then
190+
$w/@id || ":" || $following-page/@id
191+
else
192+
$w/@id || ":FOLLOWING_PB_NOT_FOUND"
193+
};
194+
195+
declare
196+
%test:assertEquals("w1:pb2", "w2:pb2", "w3:pb2", "w4:pb3", "w5:pb3")
197+
function axpn:following-with-context-predicate-db-map() {
198+
doc("/db/test/test.xml")//w[exists(.)]
199+
! (./@id || ":" || (./following::pb[1]/@id, "FOLLOWING_PB_NOT_FOUND")[1])
200+
};
201+
164202
declare
165203
%test:assertEquals("w1:pb2", "w2:pb2", "w3:pb2", "w4:pb3", "w5:pb3")
166204
function axpn:following-without-predicate-flwor() {

0 commit comments

Comments
 (0)