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..171dc09dcb1 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 @@ -161,6 +161,7 @@ imaginaryTokenDefinitions MAP MAP_TEST LOOKUP + FILTER_AM ARRAY ARRAY_TEST PROLOG @@ -1326,10 +1327,24 @@ postfixExpr throws XPathException | (LPAREN) => dynamicFunCall | + // XQuery 4.0: FilterExprAM - must check before lookup + (QUESTION LPPAREN) => filterExprAM + | (QUESTION) => lookup )* ; +// XQuery 4.0: Array/Map Filter Expression +filterExprAM throws XPathException +{ } +: + q:QUESTION! LPPAREN! expr:exprSingle RPPAREN! + { + #filterExprAM = #(#[FILTER_AM, "?["], #expr); + #filterExprAM.copyLexInfo(#q); + } + ; + arrowExpr throws XPathException : unaryExpr ( ARROW_OP^ arrowFunctionSpecifier argumentList )* 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..30841644503 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 @@ -3137,6 +3137,8 @@ throws PermissionDeniedException, EXistException, XPathException ( step = lookup [step] | + step = filterExprAM [step] + | #( PREDICATE { @@ -3198,6 +3200,24 @@ throws PermissionDeniedException, EXistException, XPathException ) ; +// === XQuery 4.0: Array/Map Filter Expression (?[expr]) === +filterExprAM [Expression leftExpr] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +: + #( + filterAM:FILTER_AM + { + PathExpr predExpr = new PathExpr(context); + } + ( expr [predExpr] )+ + { + step = new FilterExprAM(context, leftExpr, predExpr); + step.setASTNode(filterAM); + } + ) + ; + lookup [Expression leftExpr] returns [Expression step] throws PermissionDeniedException, EXistException, XPathException diff --git a/exist-core/src/main/java/org/exist/xquery/FilterExprAM.java b/exist-core/src/main/java/org/exist/xquery/FilterExprAM.java new file mode 100644 index 00000000000..c59fea9d9c9 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/FilterExprAM.java @@ -0,0 +1,152 @@ +/* + * 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 io.lacuna.bifurcan.IEntry; +import org.exist.xquery.functions.array.ArrayType; +import org.exist.xquery.functions.map.AbstractMapType; +import org.exist.xquery.functions.map.MapType; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.*; + +/** + * XQuery 4.0 FilterExprAM — the array/map filter expression {@code ?[expr]}. + * + *

Filters array members or map entries by evaluating a predicate expression + * with each member/value as the context item. Only items where the predicate's + * effective boolean value is true are kept in the result.

+ * + *
+ * [1, 2, 3, 4, 5]?[. > 3]         → [4, 5]
+ * map{"a":1, "b":2, "c":3}?[. > 1] → map{"b":2, "c":3}
+ * 
+ * + * @see + * QT4 spec: FilterExprAM + */ +public class FilterExprAM extends AbstractExpression { + + private final Expression target; + private final Expression predicate; + + public FilterExprAM(final XQueryContext context, final Expression target, final Expression predicate) { + super(context); + this.target = target; + this.predicate = predicate; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + target.analyze(contextInfo); + + // The predicate runs with each member/value as context item + final AnalyzeContextInfo predInfo = new AnalyzeContextInfo(contextInfo); + predInfo.setStaticType(Type.ITEM); + predicate.analyze(predInfo); + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { + // Evaluate the target expression + final Sequence targetSeq = target.eval(contextSequence, contextItem); + + if (targetSeq.isEmpty()) { + return Sequence.EMPTY_SEQUENCE; + } + + // Process each item in the target sequence + final ValueSequence result = new ValueSequence(); + for (final SequenceIterator iter = targetSeq.iterate(); iter.hasNext(); ) { + final Item item = iter.nextItem(); + + if (item.getType() == Type.ARRAY_ITEM) { + result.add(filterArray((ArrayType) item)); + } else if (Type.subTypeOf(item.getType(), Type.MAP_ITEM)) { + result.add(filterMap((AbstractMapType) item)); + } else { + throw new XPathException(this, ErrorCodes.XPTY0004, + "FilterExprAM (?[]) requires an array or map, got " + + Type.getTypeName(item.getType())); + } + } + + return result; + } + + private ArrayType filterArray(final ArrayType array) throws XPathException { + final ArrayType filtered = new ArrayType(context, Sequence.EMPTY_SEQUENCE); + for (int i = 0; i < array.getSize(); i++) { + final Sequence member = array.get(i); + + // Evaluate predicate with member as context item + final Sequence predResult = predicate.eval(member, null); + if (predResult.effectiveBooleanValue()) { + filtered.add(member); + } + } + return filtered; + } + + private AbstractMapType filterMap(final AbstractMapType map) throws XPathException { + final MapType filtered = new MapType(this, context); + for (final IEntry entry : map) { + final Sequence value = entry.value(); + + // Evaluate predicate with value as context item + final Sequence predResult = predicate.eval(value, null); + if (predResult.effectiveBooleanValue()) { + filtered.add(entry.key(), value); + } + } + return filtered; + } + + @Override + public int returnsType() { + return target.returnsType(); + } + + @Override + public Cardinality getCardinality() { + return target.getCardinality(); + } + + @Override + public void dump(final ExpressionDumper dumper) { + target.dump(dumper); + dumper.display("?["); + predicate.dump(dumper); + dumper.display("]"); + } + + @Override + public String toString() { + return target.toString() + "?[" + predicate.toString() + "]"; + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + target.resetState(postOptimization); + predicate.resetState(postOptimization); + } +} diff --git a/exist-core/src/test/xquery/xquery3/filterExprAM.xql b/exist-core/src/test/xquery/xquery3/filterExprAM.xql new file mode 100644 index 00000000000..6f6b5325a6d --- /dev/null +++ b/exist-core/src/test/xquery/xquery3/filterExprAM.xql @@ -0,0 +1,133 @@ +(: + : 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 FilterExprAM (?[expr]) — array/map filter expression. + :) +module namespace fam = "http://exist-db.org/xquery/test/filter-expr-am"; + +declare namespace test = "http://exist-db.org/xquery/xqsuite"; + +(: === Array filtering === :) + +declare + %test:assertEquals(2) +function fam:array-filter-size() { + let $result := [1, 2, 3, 4, 5]?[. > 3] + return array:size($result) +}; + +declare + %test:assertEquals(4, 5) +function fam:array-filter-values() { + let $result := [1, 2, 3, 4, 5]?[. > 3] + return ($result(1), $result(2)) +}; + +declare + %test:assertEquals(0) +function fam:array-filter-none-match() { + let $result := [1, 2, 3]?[. > 10] + return array:size($result) +}; + +declare + %test:assertEquals(3) +function fam:array-filter-all-match() { + let $result := [1, 2, 3]?[. > 0] + return array:size($result) +}; + +declare + %test:assertTrue +function fam:array-filter-returns-array() { + [1, 2, 3]?[. > 1] instance of array(*) +}; + +declare + %test:assertEquals(0) +function fam:array-filter-empty() { + let $result := []?[. > 0] + return array:size($result) +}; + +declare + %test:assertEquals(3) +function fam:array-filter-even() { + array:size([1, 2, 3, 4, 5, 6]?[. mod 2 = 0]) +}; + +declare + %test:assertEquals(3) +function fam:array-filter-strings() { + array:size(["apple", "banana", "avocado", "cherry", "apricot"]?[starts-with(., "a")]) +}; + +(: === Map filtering === :) + +declare + %test:assertTrue +function fam:map-filter-returns-map() { + map { "a": 1, "b": 2, "c": 3 }?[. > 1] instance of map(*) +}; + +declare + %test:assertEquals(2) +function fam:map-filter-size() { + map:size(map { "a": 1, "b": 2, "c": 3 }?[. > 1]) +}; + +declare + %test:assertEquals(0) +function fam:map-filter-empty-result() { + map:size(map { "a": 1, "b": 2 }?[. > 10]) +}; + +declare + %test:assertEquals(2) +function fam:map-filter-all-entries() { + map:size(map { "a": 1, "b": 2 }?[. > 0]) +}; + +(: === Chaining === :) + +declare + %test:assertEquals(3) +function fam:chain-filter-then-size() { + let $data := [10, 20, 30, 40, 50] + return array:size($data?[. >= 30]) +}; + +(: === Type error === :) + +declare + %test:assertError("XPTY0004") +function fam:type-error-on-string() { + "hello"?[. > 0] +}; + +declare + %test:assertError("XPTY0004") +function fam:type-error-on-integer() { + 42?[. > 0] +};