From 7855ad70ff00233ffa8efef8643993b389d1eacd Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 8 Apr 2026 07:43:07 -0400 Subject: [PATCH 1/3] [bugfix] fix declare copy-namespaces no-inherit breaking element constructors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `` inside `` could not see the inherited default namespace, and a prefixed name like `` whose prefix was declared on an enclosing element threw XPTY0004. 2. `in-scope-prefixes()` returning incomplete results for nested direct constructors: `{{}}` — `` 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/exist#2182 where `declare copy-namespaces preserve, no-inherit` caused XPath steps over constructed elements to return empty sequences. Companion to PR #6219 (serializer xmlns="" fix). Co-Authored-By: Claude Sonnet 4.6 --- .../org/exist/xquery/ElementConstructor.java | 46 ++++-- .../java/org/exist/xquery/EnclosedExpr.java | 155 +++++++++++++++++- .../java/org/exist/xquery/ModuleContext.java | 10 ++ .../java/org/exist/xquery/XQueryContext.java | 12 ++ .../functions/fn/FunInScopePrefixes.java | 48 +++--- 5 files changed, 225 insertions(+), 46 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java b/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java index 20b94537797..746736f3313 100644 --- a/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java +++ b/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java @@ -275,24 +275,38 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr if (qnitem instanceof QNameValue) { qn = ((QNameValue) qnitem).getQName(); } else { - //Do we have the same result than Atomize there ? -pb - try { - qn = QName.parse(context, qnitem.getStringValue()); - } catch (final QName.IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPTY0004, "'" + qnitem.getStringValue() + "' is not a valid element name"); - } catch (final XPathException e) { - e.setLocation(getLine(), getColumn(), getSource()); - throw e; + // Element constructors must resolve namespace prefixes using the full + // inherited namespace context, regardless of declare copy-namespaces no-inherit. + // The no-inherit option governs how namespaces propagate from copied source + // nodes, not how constructor names are resolved (XQuery 3.1 §3.9.3.4). + final boolean savedInherit = context.inheritNamespaces(); + if (!savedInherit) { + context.setInheritNamespaces(true); } + try { + //Do we have the same result than Atomize there ? -pb + try { + qn = QName.parse(context, qnitem.getStringValue()); + } catch (final QName.IllegalQNameException e) { + throw new XPathException(this, ErrorCodes.XPTY0004, "'" + qnitem.getStringValue() + "' is not a valid element name"); + } catch (final XPathException e) { + e.setLocation(getLine(), getColumn(), getSource()); + throw e; + } - //Use the default namespace if specified - /* - if (qn.getPrefix() == null && context.inScopeNamespaces.get("xmlns") != null) { - qn.setNamespaceURI((String)context.inScopeNamespaces.get("xmlns")); - } - */ - if (qn.getPrefix() == null && context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX) != null) { - qn = new QName(qn.getLocalPart(), context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX), qn.getPrefix()); + //Use the default namespace if specified + /* + if (qn.getPrefix() == null && context.inScopeNamespaces.get("xmlns") != null) { + qn.setNamespaceURI((String)context.inScopeNamespaces.get("xmlns")); + } + */ + if (qn.getPrefix() == null && context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX) != null) { + qn = new QName(qn.getLocalPart(), context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX), qn.getPrefix()); + } + } finally { + if (!savedInherit) { + context.setInheritNamespaces(false); + } } } diff --git a/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java b/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java index b11a10e06f4..dbec14fcb92 100644 --- a/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java @@ -21,20 +21,30 @@ */ package org.exist.xquery; +import org.exist.dom.QName; import org.exist.dom.memtree.DocumentBuilderReceiver; +import org.exist.dom.memtree.DocumentImpl; import org.exist.dom.memtree.MemTreeBuilder; +import org.exist.dom.memtree.NodeImpl; import org.exist.dom.memtree.TextImpl; +import org.exist.util.serializer.AttrList; import org.exist.xquery.functions.array.ArrayType; import org.exist.xquery.util.ExpressionDumper; import org.exist.xquery.value.*; import org.w3c.dom.DOMException; import org.xml.sax.SAXException; +import javax.xml.XMLConstants; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + /** * Represents an enclosed expression {expr} inside element * content. Enclosed expressions within attribute values are processed by * {@link org.exist.xquery.AttributeConstructor}. - * + * * @author Wolfgang Meier */ public class EnclosedExpr extends PathExpr { @@ -42,7 +52,7 @@ public class EnclosedExpr extends PathExpr { public EnclosedExpr(XQueryContext context) { super(context); } - + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); newContextInfo.removeFlag(IN_NODE_CONSTRUCTOR); @@ -76,12 +86,33 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc try { context.pushDocumentContext(); + MemTreeBuilder innerBuilder = null; try { result = super.eval(contextSequence, null); + // Capture the builder that may have been created by direct constructors inside + // this enclosed expression. If null, no direct constructors ran — the result + // items are all pre-existing nodes (variable references / copies). + innerBuilder = context.peekDocumentBuilder(); } finally { context.popDocumentContext(); } + // Compute ancestor namespace context for no-inherit copy handling. + // This is the union of inherited namespaces (from outer constructors) and + // in-scope namespaces (from the immediately enclosing constructor), i.e. all + // namespace bindings that an ancestor traversal from this element would find. + final boolean noInherit = !context.inheritNamespaces(); + Map ancestorNS = null; + if (noInherit) { + final Map inherited = context.getAllInheritedNamespaces(); + final Map inScope = context.getInScopeNamespaces(); + if ((inherited != null && !inherited.isEmpty()) || (inScope != null && !inScope.isEmpty())) { + ancestorNS = new HashMap<>(); + if (inherited != null) { ancestorNS.putAll(inherited); } + if (inScope != null) { ancestorNS.putAll(inScope); } + } + } + // create the output final MemTreeBuilder builder = context.getDocumentBuilder(); final DocumentBuilderReceiver receiver = new DocumentBuilderReceiver(this, builder); @@ -130,7 +161,25 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc } try { receiver.setCheckNS(false); - next.copyTo(context.getBroker(), receiver); + // When copy-namespaces no-inherit is active, pre-existing element nodes + // (i.e. nodes from variable references, not direct constructors) must have + // namespace undeclarations injected so that ancestor-traversal in + // in-scope-prefixes() is neutralized for inherited namespace bindings. + if (noInherit && ancestorNS != null + && next.getType() == Type.ELEMENT + && next instanceof NodeImpl) { + final NodeImpl nodeImpl = (NodeImpl) next; + final boolean isPreExisting = (innerBuilder == null) + || (nodeImpl.getOwnerDocument() != innerBuilder.getDocument()); + if (isPreExisting) { + next.copyTo(context.getBroker(), + new NoInheritCopyReceiver(this, builder, ancestorNS)); + } else { + next.copyTo(context.getBroker(), receiver); + } + } else { + next.copyTo(context.getBroker(), receiver); + } receiver.setCheckNS(true); } catch (DOMException e) { if (e.code == DOMException.NAMESPACE_ERR) { @@ -194,4 +243,104 @@ public Expression simplify() { public boolean evalNextExpressionOnEmptyContextSequence() { return true; } + + /** + * A {@link DocumentBuilderReceiver} that injects namespace undeclarations onto the + * root element of a copied pre-existing node when {@code declare copy-namespaces no-inherit} + * is active. The undeclarations neutralize ancestor namespace bindings so that + * {@code fn:in-scope-prefixes()} traversing the ancestor chain returns the correct result. + * + *

Only prefixes present in {@code ancestorNS} but absent from the root element's own + * namespace declarations are undeclared (i.e. recorded as {@code xmlns:prefix=""}).

+ */ + private static final class NoInheritCopyReceiver extends DocumentBuilderReceiver { + + private final Map ancestorNS; + /** True once the root element's startElement event has been seen. */ + private boolean rootSeen = false; + /** True once undeclarations have been flushed (happens before first non-namespace event). */ + private boolean undeclsFlushed = false; + /** Prefixes that the root element itself declares (element prefix + xmlns:* nodes). */ + private final Set rootOwnPrefixes = new HashSet<>(); + + NoInheritCopyReceiver(final Expression expr, final MemTreeBuilder builder, + final Map ancestorNS) { + super(expr, builder); + this.ancestorNS = ancestorNS; + } + + @Override + public void startElement(final QName qname, final AttrList attribs) { + if (!rootSeen) { + rootSeen = true; + // Track the element's own namespace prefix so we don't undeclare it. + final String prefix = qname.getPrefix(); + if (prefix != null && !prefix.isEmpty()) { + rootOwnPrefixes.add(prefix); + } + // Note: namespace declaration nodes for this element come via addNamespaceNode + // after startElement; they are collected in rootOwnPrefixes there. + } else { + maybeFlushUndeclarations(); + } + super.startElement(qname, attribs); + } + + @Override + public void addNamespaceNode(final QName qname) throws SAXException { + if (rootSeen && !undeclsFlushed) { + // Collect the prefix that this namespace node declares on the root element. + rootOwnPrefixes.add(qname.getLocalPart()); + } + super.addNamespaceNode(qname); + } + + @Override + public void characters(final CharSequence seq) throws SAXException { + maybeFlushUndeclarations(); + super.characters(seq); + } + + @Override + public void characters(final char[] ch, final int start, final int len) throws SAXException { + maybeFlushUndeclarations(); + super.characters(ch, start, len); + } + + @Override + public void endElement(final String ns, final String local, final String qname) throws SAXException { + maybeFlushUndeclarations(); + super.endElement(ns, local, qname); + } + + @Override + public void endElement(final QName qname) throws SAXException { + maybeFlushUndeclarations(); + super.endElement(qname); + } + + /** + * Emit namespace undeclarations for every ancestor namespace binding whose prefix is + * not already declared by the root element itself. Called before the first non-namespace + * event on the root element (child, text, endElement) to ensure the nodes attach to the + * root element node number in the MemTree. + */ + private void maybeFlushUndeclarations() { + if (rootSeen && !undeclsFlushed) { + undeclsFlushed = true; + for (final Map.Entry entry : ancestorNS.entrySet()) { + final String prefix = entry.getKey(); + if (!rootOwnPrefixes.contains(prefix)) { + try { + super.addNamespaceNode( + new QName(prefix, XMLConstants.NULL_NS_URI, XMLConstants.XMLNS_ATTRIBUTE)); + } catch (final SAXException e) { + // Silently skip — undeclaration is best-effort; worst case + // in-scope-prefixes() returns a superset which is handled by cleanup. + } + } + } + } + } + } } diff --git a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java index fd63f4f0b6c..2ebea09c167 100644 --- a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java +++ b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java @@ -357,6 +357,11 @@ public MemTreeBuilder getDocumentBuilder(final boolean explicitCreation) { return parentContext.getDocumentBuilder(explicitCreation); } + @Override + public MemTreeBuilder peekDocumentBuilder() { + return parentContext.peekDocumentBuilder(); + } + @Override public void pushDocumentContext() { parentContext.pushDocumentContext(); @@ -523,6 +528,11 @@ public String getInheritedNamespace(final String prefix) { return parentContext.getInheritedNamespace(prefix); } + @Override + public Map getAllInheritedNamespaces() { + return parentContext.getAllInheritedNamespaces(); + } + @Override public String getInheritedPrefix(final String uri) { return parentContext.getInheritedPrefix(uri); diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index b3721c34179..22765517328 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -969,6 +969,18 @@ public String getInheritedNamespace(final String prefix) { return inheritedInScopeNamespaces == null ? null : inheritedInScopeNamespaces.get(prefix); } + public Map getAllInheritedNamespaces() { + return inheritedInScopeNamespaces; + } + + public Map getInScopeNamespaces() { + return inScopeNamespaces; + } + + public MemTreeBuilder peekDocumentBuilder() { + return documentBuilder; + } + @Override public String getInheritedPrefix(final String uri) { return inheritedInScopePrefixes == null ? null : inheritedInScopePrefixes.get(uri); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java index a0a248dface..b8d46a82ff8 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInScopePrefixes.java @@ -154,25 +154,20 @@ public static Map collectPrefixes(XQueryContext context, NodeVal Node node = nodeValue.getNode(); if (context.preserveNamespaces()) { //Horrible hacks to work-around bad in-scope NS : we reconstruct a NS context ! - if (context.inheritNamespaces()) { - //Grab ancestors' NS - final Deque stack = new ArrayDeque<>(); - do { - if (node.getNodeType() == Node.ELEMENT_NODE) { - stack.add((Element) node); - } - node = node.getParentNode(); - } while (node != null && node.getNodeType() == Node.ELEMENT_NODE); - - while (!stack.isEmpty()) { - collectNamespacePrefixes(stack.pop(), prefixes); - } - - } else { - //Grab self's NS + // Always traverse ancestors for in-memory nodes. + // When copy-namespaces no-inherit is active, pre-existing nodes (variable copies) + // carry explicit namespace undeclarations (xmlns:prefix="") so that ancestor + // namespace entries are neutralized by the cleanup pass below. + final Deque stack = new ArrayDeque<>(); + do { if (node.getNodeType() == Node.ELEMENT_NODE) { - collectNamespacePrefixes((Element) node, prefixes); + stack.add((Element) node); } + node = node.getParentNode(); + } while (node != null && node.getNodeType() == Node.ELEMENT_NODE); + + while (!stack.isEmpty()) { + collectNamespacePrefixes(stack.pop(), prefixes); } } else { if (context.inheritNamespaces()) { @@ -192,17 +187,16 @@ public static Map collectPrefixes(XQueryContext context, NodeVal } } - //clean up - String key = null; - String value = null; - for (final Entry entry : prefixes.entrySet()) { - key = entry.getKey(); - value = entry.getValue(); - - if ((key == null || key.isEmpty()) && (value == null || value.isEmpty())) { - prefixes.remove(key); + // clean up: remove namespace undeclarations (entries with empty URI). + // With copy-namespaces no-inherit, pre-existing nodes carry explicit undeclarations + // (xmlns:prefix="") that neutralize ancestor namespace bindings; those must not appear + // in the in-scope-prefixes() result. + final Iterator> cleanupIt = prefixes.entrySet().iterator(); + while (cleanupIt.hasNext()) { + final Entry entry = cleanupIt.next(); + if (entry.getValue() == null || entry.getValue().isEmpty()) { + cleanupIt.remove(); } - } return prefixes; From 2d9e248c0aac4590bbfe9e5f6126132d9b6a32c2 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 15 Apr 2026 00:17:25 -0400 Subject: [PATCH 2/3] [refactor] Address reviewer feedback on copy-namespaces no-inherit implementation - 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 --- .../org/exist/xquery/ElementConstructor.java | 6 ------ .../main/java/org/exist/xquery/EnclosedExpr.java | 16 +++++++++++----- .../java/org/exist/xquery/ModuleContext.java | 4 ++-- .../java/org/exist/xquery/XQueryContext.java | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java b/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java index 746736f3313..eea4cdb5032 100644 --- a/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java +++ b/exist-core/src/main/java/org/exist/xquery/ElementConstructor.java @@ -294,12 +294,6 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr throw e; } - //Use the default namespace if specified - /* - if (qn.getPrefix() == null && context.inScopeNamespaces.get("xmlns") != null) { - qn.setNamespaceURI((String)context.inScopeNamespaces.get("xmlns")); - } - */ if (qn.getPrefix() == null && context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX) != null) { qn = new QName(qn.getLocalPart(), context.getInScopeNamespace(XMLConstants.DEFAULT_NS_PREFIX), qn.getPrefix()); } diff --git a/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java b/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java index dbec14fcb92..d818271e6ff 100644 --- a/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/EnclosedExpr.java @@ -84,15 +84,22 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc Sequence result; context.enterEnclosedExpr(); try { + // Check copy-namespaces mode before evaluation so we can lazily capture + // innerBuilder only when no-inherit is active. In the default (inherit) case + // this avoids the peekDocumentBuilder() call entirely. + final boolean noInherit = !context.inheritNamespaces(); + context.pushDocumentContext(); MemTreeBuilder innerBuilder = null; try { result = super.eval(contextSequence, null); - // Capture the builder that may have been created by direct constructors inside - // this enclosed expression. If null, no direct constructors ran — the result - // items are all pre-existing nodes (variable references / copies). - innerBuilder = context.peekDocumentBuilder(); + // Only capture the inner builder when no-inherit is active — it is used + // solely to distinguish pre-existing nodes (variable references / copies) + // from nodes constructed inside this enclosed expression. + if (noInherit) { + innerBuilder = context.getCurrentDocumentBuilder(); + } } finally { context.popDocumentContext(); } @@ -101,7 +108,6 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc // This is the union of inherited namespaces (from outer constructors) and // in-scope namespaces (from the immediately enclosing constructor), i.e. all // namespace bindings that an ancestor traversal from this element would find. - final boolean noInherit = !context.inheritNamespaces(); Map ancestorNS = null; if (noInherit) { final Map inherited = context.getAllInheritedNamespaces(); diff --git a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java index 2ebea09c167..da028882b1e 100644 --- a/exist-core/src/main/java/org/exist/xquery/ModuleContext.java +++ b/exist-core/src/main/java/org/exist/xquery/ModuleContext.java @@ -358,8 +358,8 @@ public MemTreeBuilder getDocumentBuilder(final boolean explicitCreation) { } @Override - public MemTreeBuilder peekDocumentBuilder() { - return parentContext.peekDocumentBuilder(); + public MemTreeBuilder getCurrentDocumentBuilder() { + return parentContext.getCurrentDocumentBuilder(); } @Override diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index 22765517328..1b15f7702a9 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -977,7 +977,7 @@ public Map getInScopeNamespaces() { return inScopeNamespaces; } - public MemTreeBuilder peekDocumentBuilder() { + public MemTreeBuilder getCurrentDocumentBuilder() { return documentBuilder; } From 17a680e2707f4dc3617d473e209848e48379cc16 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Mon, 27 Apr 2026 08:16:51 -0400 Subject: [PATCH 3/3] [test] Add XQSuite tests for copy-namespaces inherit/no-inherit (issue #2182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per duncdrum's review on PR #6222: query-based tests demonstrating the copy-namespaces inherit and no-inherit modes from issue #2182. Two modules — one per prolog declaration — cover: - default-namespace inheritance into nested element constructors, - prefixed-name resolution into nested element constructors, - fn:in-scope-prefixes ancestor traversal on directly constructed elements (regression coverage for K2-CopyNamespacesProlog-4/5/9 and copynamespace-2), - the issue #2182 reproducer (xsi:type prefix preserved on a fragment selected with copy-namespaces inherit). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../xquery3/copy-namespaces-no-inherit.xqm | 70 ++++++++++++++++ .../test/xquery/xquery3/copy-namespaces.xqm | 81 +++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 exist-core/src/test/xquery/xquery3/copy-namespaces-no-inherit.xqm create mode 100644 exist-core/src/test/xquery/xquery3/copy-namespaces.xqm diff --git a/exist-core/src/test/xquery/xquery3/copy-namespaces-no-inherit.xqm b/exist-core/src/test/xquery/xquery3/copy-namespaces-no-inherit.xqm new file mode 100644 index 00000000000..af467a6410a --- /dev/null +++ b/exist-core/src/test/xquery/xquery3/copy-namespaces-no-inherit.xqm @@ -0,0 +1,70 @@ +(: + : eXist-db Open Source Native XML Database + : Copyright (C) 2001 The eXist-db Authors + : + : info@exist-db.org + : http://www.exist-db.org + : + : This library is free software; you can redistribute it and/or + : modify it under the terms of the GNU Lesser General Public + : License as published by the Free Software Foundation; either + : version 2.1 of the License, or (at your option) any later version. + : + : This library is distributed in the hope that it will be useful, + : but WITHOUT ANY WARRANTY; without even the implied warranty of + : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + : Lesser General Public License for more details. + : + : You should have received a copy of the GNU Lesser General Public + : License along with this library; if not, write to the Free Software + : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + :) +xquery version "3.1"; + +(:~ + : XQSuite tests for `declare copy-namespaces preserve, no-inherit`. + : + : Per XQuery 3.1 §3.9.3.4, no-inherit governs how namespaces propagate + : from copied source nodes (variable references) into a constructor. + : It must NOT prevent name resolution for direct constructors, nor + : prevent fn:in-scope-prefixes from walking the ancestor chain on + : directly constructed nested elements. + : + : Regression tests for the bugs fixed alongside issue #2182: + : - Name resolution failure for default and prefixed names in nested + : direct constructors (XPTY0004 / empty paths) + : - in-scope-prefixes returning incomplete results for nested direct + : constructors (XQTS K2-CopyNamespacesProlog-4/5/9, copynamespace-2) + :) +module namespace cnn = "http://exist-db.org/xquery/test/copy-namespaces-no-inherit"; + +declare namespace test = "http://exist-db.org/xquery/xqsuite"; +declare namespace ns = "http://example.com/ns"; + +declare copy-namespaces preserve, no-inherit; + +declare + %test:assertEquals("http://example.com/") +function cnn:default-ns-still-resolves-in-direct-constructor() { + let $e := { } + return namespace-uri($e/*[1]) +}; + +declare + %test:assertEquals("http://example.com/ns") +function cnn:prefix-still-resolves-in-direct-constructor() { + let $e := + return namespace-uri($e/ns:b) +}; + +declare + %test:assertEquals(3) +function cnn:in-scope-prefixes-include-ancestor-bindings-on-direct-construction() { + let $e := + + + + + return count(in-scope-prefixes($e/e2/e1)[. = ("n1", "n2", "n3")]) +}; + diff --git a/exist-core/src/test/xquery/xquery3/copy-namespaces.xqm b/exist-core/src/test/xquery/xquery3/copy-namespaces.xqm new file mode 100644 index 00000000000..3b50c58c254 --- /dev/null +++ b/exist-core/src/test/xquery/xquery3/copy-namespaces.xqm @@ -0,0 +1,81 @@ +(: + : eXist-db Open Source Native XML Database + : Copyright (C) 2001 The eXist-db Authors + : + : info@exist-db.org + : http://www.exist-db.org + : + : This library is free software; you can redistribute it and/or + : modify it under the terms of the GNU Lesser General Public + : License as published by the Free Software Foundation; either + : version 2.1 of the License, or (at your option) any later version. + : + : This library is distributed in the hope that it will be useful, + : but WITHOUT ANY WARRANTY; without even the implied warranty of + : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + : Lesser General Public License for more details. + : + : You should have received a copy of the GNU Lesser General Public + : License along with this library; if not, write to the Free Software + : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + :) +xquery version "3.1"; + +(:~ + : XQSuite tests for `declare copy-namespaces preserve, inherit` (the + : default mode). Covers element-constructor name resolution and the + : behaviour exercised by the issue #2182 reproducer. + : + : See XQuery 3.1 §3.9.3.4 for spec semantics. + : See also copy-namespaces-no-inherit.xqm for the no-inherit mode. + :) +module namespace cn = "http://exist-db.org/xquery/test/copy-namespaces"; + +declare namespace test = "http://exist-db.org/xquery/xqsuite"; +declare namespace ns = "http://example.com/ns"; + +declare copy-namespaces preserve, inherit; + +declare + %test:assertEquals("http://example.com/") +function cn:default-ns-inherited-by-child-constructor() { + let $e := { } + return namespace-uri($e/*[1]) +}; + +declare + %test:assertEquals("http://example.com/ns") +function cn:prefix-visible-to-child-constructor() { + let $e := + return namespace-uri($e/ns:b) +}; + +declare + %test:assertEquals(3) +function cn:in-scope-prefixes-include-ancestor-bindings() { + let $e := + + + + + return count(in-scope-prefixes($e/e2/e1)[. = ("n1", "n2", "n3")]) +}; + +(:~ + : Issue #2182 reproducer: an xsi:type value references a prefix + : declared only on an ancestor. With copy-namespaces inherit, selecting + : the inner element keeps the ancestor binding so the qualified type + : name remains resolvable. + :) +declare + %test:assertTrue +function cn:issue-2182-xsi-type-prefix-preserved() { + let $xml := + + + + let $fragment := $xml/*[1] + return "hl7nl" = in-scope-prefixes($fragment) +};