diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index d852d700444..f6d1cb7e780 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -1271,14 +1271,17 @@ forwardAxis : forwardAxisSpecifier COLON! COLON! ; forwardAxisSpecifier : "child" | "self" | "attribute" | "descendant" | "descendant-or-self" - | "following-sibling" | "following" + | "following-sibling-or-self" | "following-sibling" + | "following-or-self" | "following" ; reverseAxis : reverseAxisSpecifier COLON! COLON! ; reverseAxisSpecifier : - "parent" | "ancestor" | "ancestor-or-self" | "preceding-sibling" | "preceding" + "parent" | "ancestor" | "ancestor-or-self" + | "preceding-sibling-or-self" | "preceding-sibling" + | "preceding-or-self" | "preceding" ; nodeTest throws XPathException @@ -2117,12 +2120,20 @@ reservedKeywords returns [String name] | "ancestor-or-self" { name= "ancestor-or-self"; } | + "preceding-sibling-or-self" { name= "preceding-sibling-or-self"; } + | "preceding-sibling" { name= "preceding-sibling"; } | + "following-sibling-or-self" { name= "following-sibling-or-self"; } + | "following-sibling" { name= "following-sibling"; } | + "following-or-self" { name = "following-or-self"; } + | "following" { name = "following"; } | + "preceding-or-self" { name = "preceding-or-self"; } + | "preceding" { name = "preceding"; } | "item" { name= "item"; } diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 20308296806..6d39c9f7e95 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -3310,12 +3310,20 @@ throws PermissionDeniedException, EXistException | "descendant-or-self" { axis= Constants.DESCENDANT_SELF_AXIS; } | + "following-sibling-or-self" { axis= Constants.FOLLOWING_SIBLING_OR_SELF_AXIS; } + | "following-sibling" { axis= Constants.FOLLOWING_SIBLING_AXIS; } | + "following-or-self" { axis= Constants.FOLLOWING_OR_SELF_AXIS; } + | "following" { axis= Constants.FOLLOWING_AXIS; } | + "preceding-sibling-or-self" { axis= Constants.PRECEDING_SIBLING_OR_SELF_AXIS; } + | "preceding-sibling" { axis= Constants.PRECEDING_SIBLING_AXIS; } | + "preceding-or-self" { axis= Constants.PRECEDING_OR_SELF_AXIS; } + | "preceding" { axis= Constants.PRECEDING_AXIS; } | "ancestor" { axis= Constants.ANCESTOR_AXIS; } diff --git a/exist-core/src/main/java/org/exist/dom/persistent/NewArrayNodeSet.java b/exist-core/src/main/java/org/exist/dom/persistent/NewArrayNodeSet.java index ed5f630028d..a765adc29d8 100644 --- a/exist-core/src/main/java/org/exist/dom/persistent/NewArrayNodeSet.java +++ b/exist-core/src/main/java/org/exist/dom/persistent/NewArrayNodeSet.java @@ -792,6 +792,7 @@ public NodeSet selectFollowing(final NodeSet pl, final int position, final int c if(!reference.getNodeId().isDescendantOf(nodes[j].getNodeId())) { if(position < 0 || ++n == position) { if (contextId != Expression.IGNORE_CONTEXT + && contextId != Expression.NO_CONTEXT_ID && nodes[j].getContext() != null && reference.getContext() != null && nodes[j].getContext().getContextId() == reference.getContext().getContextId()) { @@ -846,6 +847,7 @@ public NodeSet selectPreceding(final NodeSet pl, final int position, if(!reference.getNodeId().isDescendantOf(nodes[j].getNodeId())) { if(position < 0 || ++n == position) { if (contextId != Expression.IGNORE_CONTEXT + && contextId != Expression.NO_CONTEXT_ID && nodes[j].getContext() != null && reference.getContext() != null && nodes[j].getContext().getContextId() == reference.getContext().getContextId()) { diff --git a/exist-core/src/main/java/org/exist/xquery/Constants.java b/exist-core/src/main/java/org/exist/xquery/Constants.java index 7a5069d7416..d6387defe26 100644 --- a/exist-core/src/main/java/org/exist/xquery/Constants.java +++ b/exist-core/src/main/java/org/exist/xquery/Constants.java @@ -73,6 +73,14 @@ public interface Constants { //combines /descendant-or-self::node()/attribute:* int DESCENDANT_ATTRIBUTE_AXIS = 13; + // --- XQuery 4.0 axes --- + int FOLLOWING_OR_SELF_AXIS = 14; + int FOLLOWING_SIBLING_OR_SELF_AXIS = 15; + /** Reverse axis */ + int PRECEDING_OR_SELF_AXIS = 16; + /** Reverse axis */ + int PRECEDING_SIBLING_OR_SELF_AXIS = 17; + /** * Node types */ diff --git a/exist-core/src/main/java/org/exist/xquery/LocationStep.java b/exist-core/src/main/java/org/exist/xquery/LocationStep.java index 624795add20..6e47f6594a4 100644 --- a/exist-core/src/main/java/org/exist/xquery/LocationStep.java +++ b/exist-core/src/main/java/org/exist/xquery/LocationStep.java @@ -443,6 +443,21 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) result = getSiblings(context, contextSequence); break; + // --- XQuery 4.0 combined axes --- + case Constants.FOLLOWING_OR_SELF_AXIS: + case Constants.PRECEDING_OR_SELF_AXIS: + result = getOrSelfAxis(context, contextSequence, + axis == Constants.FOLLOWING_OR_SELF_AXIS + ? Constants.FOLLOWING_AXIS : Constants.PRECEDING_AXIS); + break; + + case Constants.FOLLOWING_SIBLING_OR_SELF_AXIS: + case Constants.PRECEDING_SIBLING_OR_SELF_AXIS: + result = getOrSelfAxis(context, contextSequence, + axis == Constants.FOLLOWING_SIBLING_OR_SELF_AXIS + ? Constants.FOLLOWING_SIBLING_AXIS : Constants.PRECEDING_SIBLING_AXIS); + break; + default: throw new IllegalArgumentException("Unsupported axis specified"); } @@ -912,6 +927,63 @@ protected Sequence getSiblings(final XQueryContext context, final Sequence conte * * @throws XPathException if an error occurs */ + /** + * Evaluates an XQuery 4.0 combined axis (e.g., following-or-self, preceding-sibling-or-self). + * Returns the union of the self axis result and the base axis result, preserving document order. + * + * @param context the XQuery context + * @param contextSequence the context sequence + * @param baseAxis the base axis constant (e.g., Constants.FOLLOWING_AXIS) + * @return the combined result in document order + */ + private Sequence getOrSelfAxis(final XQueryContext context, final Sequence contextSequence, + final int baseAxis) throws XPathException { + // Save and temporarily switch axis to get results + final int savedAxis = this.axis; + try { + final Sequence selfOrRelatedResult; + final Sequence baseResult; + + if (baseAxis == Constants.FOLLOWING_AXIS) { + // following-or-self = descendant-or-self | following + // (all nodes at or after context node in document order) + this.axis = Constants.DESCENDANT_SELF_AXIS; + selfOrRelatedResult = getDescendants(context, contextSequence); + this.axis = Constants.FOLLOWING_AXIS; + baseResult = getPrecedingOrFollowing(context, contextSequence); + } else if (baseAxis == Constants.PRECEDING_AXIS) { + // preceding-or-self = ancestor-or-self | preceding + // (all nodes at or before context node in document order) + this.axis = Constants.ANCESTOR_SELF_AXIS; + selfOrRelatedResult = getAncestors(context, contextSequence); + this.axis = Constants.PRECEDING_AXIS; + baseResult = getPrecedingOrFollowing(context, contextSequence); + } else { + // following-sibling-or-self / preceding-sibling-or-self = self | sibling + this.axis = Constants.SELF_AXIS; + selfOrRelatedResult = getSelf(context, contextSequence); + this.axis = baseAxis; + baseResult = getSiblings(context, contextSequence); + } + + // Union preserving document order + if (selfOrRelatedResult.isEmpty()) { + return baseResult; + } + if (baseResult.isEmpty()) { + return selfOrRelatedResult; + } + final ValueSequence combined = new ValueSequence(); + combined.addAll(selfOrRelatedResult); + combined.addAll(baseResult); + combined.removeDuplicates(); + combined.sortInDocumentOrder(); + return combined; + } finally { + this.axis = savedAxis; + } + } + private Sequence getPrecedingOrFollowing(final XQueryContext context, final Sequence contextSequence) throws XPathException { final int position = computeLimit(); diff --git a/exist-core/src/main/java/org/exist/xquery/Predicate.java b/exist-core/src/main/java/org/exist/xquery/Predicate.java index 986de11bb8a..bd231a2b308 100644 --- a/exist-core/src/main/java/org/exist/xquery/Predicate.java +++ b/exist-core/src/main/java/org/exist/xquery/Predicate.java @@ -514,13 +514,21 @@ private Sequence selectByPosition(final Sequence outerSequence, temp = contextSet.selectPrecedingSiblings(p, Expression.IGNORE_CONTEXT); break; case Constants.FOLLOWING_SIBLING_AXIS: + case Constants.FOLLOWING_SIBLING_OR_SELF_AXIS: temp = contextSet.selectFollowingSiblings(p, Expression.IGNORE_CONTEXT); reverseAxis = false; break; case Constants.FOLLOWING_AXIS: + case Constants.FOLLOWING_OR_SELF_AXIS: temp = contextSet.selectFollowing(p, Expression.IGNORE_CONTEXT); reverseAxis = false; break; + case Constants.PRECEDING_OR_SELF_AXIS: + temp = contextSet.selectPreceding(p, Expression.IGNORE_CONTEXT); + break; + case Constants.PRECEDING_SIBLING_OR_SELF_AXIS: + temp = contextSet.selectPrecedingSiblings(p, Expression.IGNORE_CONTEXT); + break; case Constants.SELF_AXIS: temp = p; reverseAxis = false; diff --git a/exist-core/src/test/java/org/exist/xquery/XQ4AxesTest.java b/exist-core/src/test/java/org/exist/xquery/XQ4AxesTest.java new file mode 100644 index 00000000000..0e039548bf4 --- /dev/null +++ b/exist-core/src/test/java/org/exist/xquery/XQ4AxesTest.java @@ -0,0 +1,115 @@ +/* + * 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 + */ +package org.exist.xquery; + +import org.exist.test.ExistXmldbEmbeddedServer; +import org.junit.ClassRule; +import org.junit.Test; +import org.xmldb.api.base.ResourceSet; +import org.xmldb.api.base.XMLDBException; +import org.xmldb.api.modules.XQueryService; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for XQuery 4.0 combined axes: + * following-or-self, following-sibling-or-self, + * preceding-or-self, preceding-sibling-or-self. + */ +public class XQ4AxesTest { + + @ClassRule + public static final ExistXmldbEmbeddedServer server = + new ExistXmldbEmbeddedServer(false, true, true); + + private static final String DATA = + "" + + " " + + " " + + " " + + " " + + " " + + " " + + ""; + + private String query(final String xquery) throws XMLDBException { + final XQueryService qs = server.getRoot().getService(XQueryService.class); + final String fullQuery = "let $data := " + DATA + + " return string-join(" + xquery + ", ',')"; + final ResourceSet result = qs.query(fullQuery); + return result.getResource(0).getContent().toString(); + } + + // --- following-or-self --- + + @Test + public void followingOrSelf() throws XMLDBException { + // following-or-self = context node and all nodes after it in document order + assertEquals("3,4,5,6", query("$data//c/following-or-self::*/@id/string()")); + } + + @Test + public void followingOrSelfFromFirst() throws XMLDBException { + // following-or-self from a = a plus all nodes after a in document order (descendants + following) + assertEquals("1,2,3,4,5,6", query("$data/a/following-or-self::*/@id/string()")); + } + + // --- following-sibling-or-self --- + + @Test + public void followingSiblingOrSelf() throws XMLDBException { + assertEquals("3,5", query("$data/a/c/following-sibling-or-self::*/@id/string()")); + } + + @Test + public void followingSiblingOrSelfFromFirst() throws XMLDBException { + assertEquals("2,3,5", query("$data/a/b/following-sibling-or-self::*/@id/string()")); + } + + // --- preceding-or-self --- + + @Test + public void precedingOrSelf() throws XMLDBException { + // preceding-or-self = context node and all nodes before it in document order (ancestors + preceding) + assertEquals("1,2,3", query("$data//c/preceding-or-self::*/@id/string()")); + } + + // --- preceding-sibling-or-self --- + + @Test + public void precedingSiblingOrSelf() throws XMLDBException { + assertEquals("2,3", query("$data/a/c/preceding-sibling-or-self::*/@id/string()")); + } + + @Test + public void precedingSiblingOrSelfNameTest() throws XMLDBException { + assertEquals("3", query("$data/a/c/preceding-sibling-or-self::c/@id/string()")); + } + + // --- self included in name-specific test --- + + @Test + public void followingSiblingOrSelfNameMatch() throws XMLDBException { + // c has no following sibling named 'c', but self is 'c' + assertEquals("3", query("$data/a/c/following-sibling-or-self::c/@id/string()")); + } +} diff --git a/exist-core/src/test/java/xquery/xquery4/XQuery4Tests.java b/exist-core/src/test/java/xquery/xquery4/XQuery4Tests.java new file mode 100644 index 00000000000..df6e65ce8d5 --- /dev/null +++ b/exist-core/src/test/java/xquery/xquery4/XQuery4Tests.java @@ -0,0 +1,32 @@ +/* + * 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 + */ +package xquery.xquery4; + +import org.exist.test.runner.XSuite; +import org.junit.runner.RunWith; + +@RunWith(XSuite.class) +@XSuite.XSuiteFiles({ + "src/test/xquery/xquery4", +}) +public class XQuery4Tests { +} diff --git a/exist-core/src/test/xquery/axes-persistent-nodes.xqm b/exist-core/src/test/xquery/axes-persistent-nodes.xqm index d9984336a2c..ece5539b2be 100644 --- a/exist-core/src/test/xquery/axes-persistent-nodes.xqm +++ b/exist-core/src/test/xquery/axes-persistent-nodes.xqm @@ -102,6 +102,25 @@ function axpn:preceding-with-predicate-db-map() { ! (./@id || ":" || (./preceding::pb[1]/@id, "PRECEDING_PB_NOT_FOUND")[1]) }; +declare + %test:assertEquals("w1:pb1", "w2:pb1", "w3:pb1", "w4:pb2", "w5:pb2") +function axpn:preceding-with-context-predicate-db-flwor() { + for $w in doc("/db/test/test.xml")//w[exists(.)] + let $preceding-page := $w/preceding::pb[1] + return + if ($preceding-page) then + $w/@id || ":" || $preceding-page/@id + else + $w/@id || ":PRECEDING_PB_NOT_FOUND" +}; + +declare + %test:assertEquals("w1:pb1", "w2:pb1", "w3:pb1", "w4:pb2", "w5:pb2") +function axpn:preceding-with-context-predicate-db-map() { + doc("/db/test/test.xml")//w[exists(.)] + ! (./@id || ":" || (./preceding::pb[1]/@id, "PRECEDING_PB_NOT_FOUND")[1]) +}; + declare %test:assertEquals("w1:pb1", "w2:pb1", "w3:pb1", "w4:pb2", "w5:pb2") function axpn:preceding-without-predicate-flwor() { @@ -161,6 +180,25 @@ function axpn:following-with-predicate-db-map() { ! (./@id || ":" || (./following::pb[1]/@id, "FOLLOWING_PB_NOT_FOUND")[1]) }; +declare + %test:assertEquals("w1:pb2", "w2:pb2", "w3:pb2", "w4:pb3", "w5:pb3") +function axpn:following-with-context-predicate-db-flwor() { + for $w in doc("/db/test/test.xml")//w[exists(.)] + let $following-page := $w/following::pb[1] + return + if ($following-page) then + $w/@id || ":" || $following-page/@id + else + $w/@id || ":FOLLOWING_PB_NOT_FOUND" +}; + +declare + %test:assertEquals("w1:pb2", "w2:pb2", "w3:pb2", "w4:pb3", "w5:pb3") +function axpn:following-with-context-predicate-db-map() { + doc("/db/test/test.xml")//w[exists(.)] + ! (./@id || ":" || (./following::pb[1]/@id, "FOLLOWING_PB_NOT_FOUND")[1]) +}; + declare %test:assertEquals("w1:pb2", "w2:pb2", "w3:pb2", "w4:pb3", "w5:pb3") function axpn:following-without-predicate-flwor() { diff --git a/exist-core/src/test/xquery/xquery4/xq4-axes.xql b/exist-core/src/test/xquery/xquery4/xq4-axes.xql new file mode 100644 index 00000000000..fc58918fcde --- /dev/null +++ b/exist-core/src/test/xquery/xquery4/xq4-axes.xql @@ -0,0 +1,104 @@ +(: + : 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"; + +(:~ Tests for XQuery 4.0 combined axes :) +module namespace axes="http://exist-db.org/xquery/test/xq4-axes"; + +declare namespace test="http://exist-db.org/xquery/xqsuite"; + +declare variable $axes:DATA := + + + + + + + + + + ; + +(: === following-or-self axis === :) + +declare + %test:assertEquals("3", "4", "5", "6") +function axes:following-or-self-from-c() { + $axes:DATA//c/following-or-self::*/@id/string() +}; + +declare + %test:assertEquals("1", "2", "3", "4", "5", "6") +function axes:following-or-self-from-root-child() { + $axes:DATA/a/following-or-self::*/@id/string() +}; + +(: === following-sibling-or-self axis === :) + +declare + %test:assertEquals("3", "5") +function axes:following-sibling-or-self-from-c() { + $axes:DATA/a/c/following-sibling-or-self::*/@id/string() +}; + +declare + %test:assertEquals("2", "3", "5") +function axes:following-sibling-or-self-from-b() { + $axes:DATA/a/b/following-sibling-or-self::*/@id/string() +}; + +(: === preceding-or-self axis === :) + +declare + %test:assertEquals("1", "2", "3") +function axes:preceding-or-self-from-c() { + $axes:DATA//c/preceding-or-self::*/@id/string() +}; + +(: === preceding-sibling-or-self axis === :) + +declare + %test:assertEquals("2", "3") +function axes:preceding-sibling-or-self-from-c() { + $axes:DATA/a/c/preceding-sibling-or-self::*/@id/string() +}; + +declare + %test:assertEquals("5") +function axes:preceding-sibling-or-self-from-e-only() { + (: e has no preceding siblings that are elements named 'e' :) + $axes:DATA/a/e/preceding-sibling-or-self::e/@id/string() +}; + +(: === node test with new axes === :) + +declare + %test:assertExists +function axes:following-or-self-node-test() { + $axes:DATA//c/following-or-self::node() +}; + +declare + %test:assertEquals("3") +function axes:following-sibling-or-self-name-test() { + $axes:DATA/a/c/following-sibling-or-self::c/@id/string() +};