[This issue was prompted by Joe, drafted by Claude Code, and reviewed by Joe.]
Summary
Used as an axis-step predicate, an unnamed kind test on the self axis — [self::comment()], [self::text()], [self::element()], [self::processing-instruction()] — matches every node on the axis when the nodes are persistent. Nothing is filtered, so the step silently returns more than it should.
The same tests are correct on in-memory nodes, and correct in step position (/self::comment()) on persistent nodes. Only the predicate form on persistent nodes is affected.
This is a silent wrong-result bug in the opposite direction from #6689: that one dropped data, this one adds it. Neither raises an error.
Reproducing
Store a document with three child nodes — two processing instructions and one element:
xmldb:store('/db', 'probe.xml', '<root><?target inner?><?other x?><a/></root>', 'application/xml')
Then count what each predicate selects:
let $r := doc('/db/probe.xml')/root
return (
count($r/node()[self::comment()]), (: expected 0 :)
count($r/node()[self::text()]), (: expected 0 :)
count($r/node()[self::element()]), (: expected 1 :)
count($r/node()/self::comment()) (: expected 0 — step position :)
)
Actual
| expression |
persistent |
in-memory |
expected |
$r/node()[self::comment()] |
3 |
0 |
0 |
$r/node()[self::text()] |
3 |
0 |
0 |
$r/node()[self::element()] |
3 |
1 |
1 |
$r/node()/self::comment() (step) |
0 |
0 |
0 |
$r/node()[self::element(a)] (named) |
1 |
1 |
1 |
Three is the total child count — every predicate returns the whole axis.
Cause
In LocationStep.getSelf, the wildcard branch splits on whether a context id is in play:
if (Expression.NO_CONTEXT_ID != contextId) {
// ... stamps contexts on matching nodes ...
return contextSet; // ← the whole context set, unfiltered
} else {
final NewArrayNodeSet results = new NewArrayNodeSet();
for (final NodeProxy p : contextSet) {
if (test.matches(p)) {
results.add(p); // ← correctly filtered
}
}
return results;
}
The NO_CONTEXT_ID path — step position — filters correctly. The predicate path returns contextSet whole, having only stamped context nodes onto the members that actually matched. Whatever downstream filtering that stamping was meant to drive does not happen, so the predicate is satisfied by every node.
Named tests are unaffected because they take the other branch entirely: a NameTest is not a wildcard test, so it goes to the structural-index lookup, which filters by construction.
Relationship to other issues
Notes
I have not attempted a fix. The predicate path looks like it is relying on the context-stamping mechanism to filter downstream, so the right change depends on whether that mechanism is supposed to filter here and is failing to, or whether this branch should simply filter like its sibling does. That is a question for someone who knows what the context-id path is meant to guarantee.
[This issue was prompted by Joe, drafted by Claude Code, and reviewed by Joe.]
Summary
Used as an axis-step predicate, an unnamed kind test on the self axis —
[self::comment()],[self::text()],[self::element()],[self::processing-instruction()]— matches every node on the axis when the nodes are persistent. Nothing is filtered, so the step silently returns more than it should.The same tests are correct on in-memory nodes, and correct in step position (
/self::comment()) on persistent nodes. Only the predicate form on persistent nodes is affected.This is a silent wrong-result bug in the opposite direction from #6689: that one dropped data, this one adds it. Neither raises an error.
Reproducing
Store a document with three child nodes — two processing instructions and one element:
Then count what each predicate selects:
Actual
$r/node()[self::comment()]$r/node()[self::text()]$r/node()[self::element()]$r/node()/self::comment()(step)$r/node()[self::element(a)](named)Three is the total child count — every predicate returns the whole axis.
Cause
In
LocationStep.getSelf, the wildcard branch splits on whether a context id is in play:The
NO_CONTEXT_IDpath — step position — filters correctly. The predicate path returnscontextSetwhole, having only stamped context nodes onto the members that actually matched. Whatever downstream filtering that stamping was meant to drive does not happen, so the predicate is satisfied by every node.Named tests are unaffected because they take the other branch entirely: a
NameTestis not a wildcard test, so it goes to the structural-index lookup, which filters by construction.Relationship to other issues
self::attribute(NAME)is always false on attributes of persistent nodes #6689 is the same method and the same family — a named attribute kind test resolved against the wrong half of the structural index and so was always false. Its fix ([bugfix] Look up a named attribute kind test in the attribute index on the self axis #6703) does not touch this branch. The processing-instruction part of that PR does incidentally correct the[self::processing-instruction()]row above, because PI tests no longer reach this branch at all;comment(),text()andelement()remain affected.self::attribute(NAME)is always false on attributes of persistent nodes #6689 described this symptom in a follow-up there —self::NAMEas an axis-step predicate being always true, observed from module scope. I initially could not reproduce it and attributed it to [BUG] Axis + name test returns wrong result from the 2nd evaluation onwards, when the node was reached through a range-indexed lookup #6690; that was wrong. His observation was right, though the trigger is the unnamed kind test rather than the name test in his example.Notes
I have not attempted a fix. The predicate path looks like it is relying on the context-stamping mechanism to filter downstream, so the right change depends on whether that mechanism is supposed to filter here and is failing to, or whether this branch should simply filter like its sibling does. That is a question for someone who knows what the context-id path is meant to guarantee.