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()
+};