[bugfix] declare copy-namespaces no-inherit breaking element constructors#6222
[bugfix] declare copy-namespaces no-inherit breaking element constructors#6222joewiz wants to merge 2 commits intoeXist-db:developfrom
Conversation
3f2b3bc to
45a3e4d
Compare
…tructors With `declare copy-namespaces preserve, no-inherit`, eXist was incorrectly applying the no-inherit flag during element constructor evaluation, causing two distinct bugs: 1. Name resolution failure: `getURIForPrefix()` skips `inheritedInScopeNamespaces` when `inheritNamespaces=false`, so a child constructor `<b/>` inside `<e xmlns="http://example.com/">` could not see the inherited default namespace, and a prefixed name like `<ns:c/>` whose prefix was declared on an enclosing element threw XPTY0004. 2. `in-scope-prefixes()` returning incomplete results for nested direct constructors: `<e3>{<e2>{<e1/>}</e2>}</e3>` — `<e1>` only returned its own prefix, missing `namespace2` and `namespace3` from the enclosing constructor elements. Per XQuery 3.1 §3.9.3.4, `no-inherit` governs how namespaces propagate from *copied source nodes* (existing XDM nodes placed into constructors via `{$var}`) into the result — it must not affect namespace resolution for element constructors themselves, nor prevent ancestor traversal when collecting in-scope prefixes of directly constructed elements. Fix A (ElementConstructor — name resolution): temporarily restore `inheritNamespaces=true` while resolving the element's QName so that `QName.parse()` can look up prefix-to-URI mappings in the inherited context. Restored to `false` in a `finally` block. Fix B (FunInScopePrefixes): always traverse the in-memory node's ancestor chain when collecting namespace prefixes. The previous coarse `inheritNamespaces()` switch prevented ancestor traversal for all no-inherit queries, including direct constructors where traversal is correct. Updated the cleanup pass to remove all entries with an empty URI (namespace undeclarations), not just empty-key+empty-value pairs. Fix C (EnclosedExpr + NoInheritCopyReceiver): when `no-inherit` is active and an enclosed expression places a pre-existing element node (a variable reference, not a direct constructor) into the outer element, inject namespace undeclarations (xmlns:prefix="") onto the root of the copy for every ancestor namespace binding not already declared by that root. This neutralizes ancestor traversal for those nodes so that `in-scope-prefixes()` returns only the node's own namespace context. Pre-existing nodes are distinguished from direct constructors by capturing the MemTreeBuilder allocated during the enclosed expression's evaluation (via new `peekDocumentBuilder()`) and checking whether each result node belongs to that builder's document. Fixes the following pre-existing XQTS failures in prod-CopyNamespacesDecl: K2-CopyNamespacesProlog-4, K2-CopyNamespacesProlog-5, K2-CopyNamespacesProlog-9 (second half: direct constructors), copynamespace-2 Also fixes the real-world regression reported in eXist-db#2182 where `declare copy-namespaces preserve, no-inherit` caused XPath steps over constructed elements to return empty sequences. Companion to PR eXist-db#6219 (serializer xmlns="" fix). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
45a3e4d to
7855ad7
Compare
duncdrum
left a comment
There was a problem hiding this comment.
more namespace fun, one comment in file.
| /* | ||
| if (qn.getPrefix() == null && context.inScopeNamespaces.get("xmlns") != null) { | ||
| qn.setNamespaceURI((String)context.inScopeNamespaces.get("xmlns")); | ||
| } | ||
| */ |
There was a problem hiding this comment.
Looks like it, I would be in favour of removing this.
| return inScopeNamespaces; | ||
| } | ||
|
|
||
| public MemTreeBuilder peekDocumentBuilder() { |
There was a problem hiding this comment.
Why is this method named _peek_DocumentBuilder?
This looks like a standard getter to me
line-o
left a comment
There was a problem hiding this comment.
Do these extra checks have a significant impact on element construction performance even if copy-namespaces no-inherit is not set?
| && next.getType() == Type.ELEMENT | ||
| && next instanceof NodeImpl) { | ||
| final NodeImpl nodeImpl = (NodeImpl) next; | ||
| final boolean isPreExisting = (innerBuilder == null) |
There was a problem hiding this comment.
I would prefer to populate innerBuilder lazily on first access instead of always regardless if it will ever be used.
Then it also becomes clear that only its document is needed to check for preExisting nodes.
…plementation - Remove stale commented-out code in ElementConstructor (dead since the in-scope namespace API was updated) - Rename XQueryContext.peekDocumentBuilder() to getCurrentDocumentBuilder() for consistency with standard Java getter naming - Lazily capture innerBuilder in EnclosedExpr only when copy-namespaces no-inherit is active, avoiding the method call entirely in the common (inherit) case Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
[This response was co-authored with Claude Code. -Joe] Good catches, addressed in 2d9e248:
On the performance question: with the lazy fix, the overhead in the default (inherit) case is exactly one |
Summary
declare copy-namespaces preserve, no-inheritwas incorrectly applying theno-inheritflag during element constructor evaluation, causing two distinct bugs:Name resolution failure:
getURIForPrefix()skipsinheritedInScopeNamespaceswheninheritNamespaces=false, so a child constructor<b/>inside<e xmlns="http://example.com/">could not see the inherited default namespace, and a prefixed name like<ns:c/>whose prefix was declared on an enclosing element threwXPTY0004.in-scope-prefixes()returning incomplete results: Nested direct constructors like<e3>{<e2>{<e1/>}</e2>}</e3>— callingin-scope-prefixes(<e1>)only returned<e1>'s own prefix, missingnamespace2andnamespace3declared by the enclosing constructors.Per XQuery 3.1 §3.9.3.4,
no-inheritgoverns how namespaces propagate from copied source nodes (existing XDM nodes placed into constructors via{$var}) — it must not affect namespace resolution for element constructors themselves.Also fixes the real-world regression in #2182 where
declare copy-namespaces preserve, no-inheritcaused XPath path steps over constructed elements to silently return empty sequences.Companion to PR #6219 (serializer
xmlns=""fix).What Changed
ElementConstructor.java— Fix A (name resolution)Temporarily restores
inheritNamespaces=truewhile resolving the element's QName viaQName.parse(), so that prefix-to-URI lookups consult the inherited namespace context. Restored in afinallyblock.FunInScopePrefixes.java— Fix B (ancestor traversal)Always traverse the in-memory node's ancestor chain when collecting namespace prefixes, removing the coarse
inheritNamespaces()switch that previously blocked traversal for allno-inheritqueries. Updated cleanup to remove all entries with an empty URI (namespace undeclarations).EnclosedExpr.java— Fix C (no-inherit copy semantics)When
no-inheritis active and an enclosed expression{...}places a pre-existing element node (a variable reference) into the outer element, a newNoInheritCopyReceiverintercepts the copy event stream and injects namespace undeclarations (xmlns:prefix="") onto the root of the copy for every ancestor namespace binding not already declared by that root. This neutralizes ancestor traversal for those nodes soin-scope-prefixes()returns only the node's own context.Pre-existing nodes are distinguished from direct constructors by capturing the
MemTreeBuilderallocated during the enclosed expression's evaluation (via newpeekDocumentBuilder()) and checking whether each result node belongs to that builder's document.XQueryContext.java/ModuleContext.java: addedpeekDocumentBuilder()to inspect the current builder without disturbing the context stack.Spec References
XQTS Before/After (prod-CopyNamespacesDecl, XQ 3.1)
35/35 passing in the prod-CopyNamespacesDecl test set.
CI Changes (second commit)
This PR also cherry-picks CI improvements from PR #6186 and extends them:
exist-parent/pom.xml: AddedforkedProcessTimeoutInSeconds=600to surefire and failsafe plugins, preventing hung test JVMs from blocking CI indefinitely (e.g.DeadlockIT,MoveResourceTest)..github/workflows/ci-test.yml: Added--offlineto thelicense:checkstep (eliminates 7+ min timeouts from unreachable repos; the develop cache has all needed artifacts). Added wagon HTTP timeout flags via step-levelMAVEN_OPTSto prevent Maven Build hangs caused by stalled connections torepo.exist-db.organdrepo.evolvedbinary.com.Test Plan
prod-CopyNamespacesDecl: 35/35 pass (4 were failing before, none regressed)declare copy-namespaces preserve, no-inherit; <e xmlns="http://example.com/">{ <b/> }</e>→<b>inhttp://example.com/(implied by K2-CopyNamespacesProlog-4 ✓)$items/t:item/cc:collectionreturns result instead of empty sequence (implied by copynamespace-2 + Fix A ✓)🤖 Generated with Claude Code