diff --git a/exist-core/src/main/java/org/exist/xquery/Atomize.java b/exist-core/src/main/java/org/exist/xquery/Atomize.java index 19b4c9670ca..d9e553c810b 100644 --- a/exist-core/src/main/java/org/exist/xquery/Atomize.java +++ b/exist-core/src/main/java/org/exist/xquery/Atomize.java @@ -23,6 +23,7 @@ import org.exist.dom.persistent.DocumentSet; import org.exist.xquery.functions.array.ArrayType; +import org.exist.xquery.functions.map.AbstractMapType; import org.exist.xquery.util.ExpressionDumper; import org.exist.xquery.value.Item; import org.exist.xquery.value.Sequence; @@ -75,15 +76,25 @@ public static Sequence atomize(Sequence input) throws XPathException { if (input.isEmpty()) {return Sequence.EMPTY_SEQUENCE;} input = ArrayType.flatten(input); - if (input.hasOne()) {return - input.itemAt(0).atomize(); + if (input.hasOne()) { + final Item single = input.itemAt(0); + // XQ4: maps are atomizable — expand to their values before atomizing + if (single instanceof AbstractMapType mapType && mapType.isXq4Atomizable()) { + return mapType.atomizeValues(); + } + return single.atomize(); } Item next; final ValueSequence result = new ValueSequence(); for(final SequenceIterator i = input.iterate(); i.hasNext(); ) { next = i.nextItem(); - result.add(next.atomize()); + // XQ4: maps are atomizable — expand to their values before atomizing + if (next instanceof AbstractMapType mapType && mapType.isXq4Atomizable()) { + result.addAll(mapType.atomizeValues()); + } else { + result.add(next.atomize()); + } } return result; } diff --git a/exist-core/src/main/java/org/exist/xquery/ConcatExpr.java b/exist-core/src/main/java/org/exist/xquery/ConcatExpr.java index fd19c50e2ef..f8f9b9d3a3e 100644 --- a/exist-core/src/main/java/org/exist/xquery/ConcatExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ConcatExpr.java @@ -21,6 +21,7 @@ */ package org.exist.xquery; +import org.exist.xquery.functions.map.AbstractMapType; import org.exist.xquery.util.Error; import org.exist.xquery.value.*; @@ -65,8 +66,17 @@ public Sequence eval(Sequence contextSequence, Item contextItem) final Sequence seq = step.eval(contextSequence, contextItem); for (final SequenceIterator i = seq.iterate(); i.hasNext(); ) { final Item item = i.nextItem(); - if (Type.subTypeOf(item.getType(), Type.FUNCTION)) - {throw new XPathException(this, ErrorCodes.FOTY0013, "Got a function item as operand in string concatenation");} + if (Type.subTypeOf(item.getType(), Type.FUNCTION)) { + // XQ4: maps are no longer function items for atomization purposes + if (item instanceof AbstractMapType mapType && mapType.isXq4Atomizable()) { + final Sequence atomized = mapType.atomizeValues(); + for (final SequenceIterator ai = atomized.iterate(); ai.hasNext(); ) { + concat.append(ai.nextItem().getStringValue()); + } + continue; + } + throw new XPathException(this, ErrorCodes.FOTY0013, "Got a function item as operand in string concatenation"); + } concat.append(item.getStringValue()); } } diff --git a/exist-core/src/main/java/org/exist/xquery/functions/map/AbstractMapType.java b/exist-core/src/main/java/org/exist/xquery/functions/map/AbstractMapType.java index 0ea9d160dbf..b571b60444a 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/map/AbstractMapType.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/map/AbstractMapType.java @@ -223,6 +223,61 @@ protected static boolean sameKey(@Nullable final Collator collator, final Atomic return false; } + /** + * Returns {@code true} if this map can be atomized under the current XQuery version. + * In XQuery 4.0, maps are no longer function items and can be atomized + * (returning the atomized values of their entries). + * + * @return true if the map is atomizable (XQuery 4.0+) + */ + public boolean isXq4Atomizable() { + return context != null && context.getXQueryVersion() >= 40; + } + + /** + * Atomize all values in this map, returning them as a flat sequence of atomic values. + * This implements the XQuery 4.0 semantics where atomizing a map returns the + * concatenation of the atomized values of its entries. + * + * @return a sequence of atomic values from all map entry values + * @throws XPathException if any value cannot be atomized + */ + public Sequence atomizeValues() throws XPathException { + if (size() == 0) { + return Sequence.EMPTY_SEQUENCE; + } + final ValueSequence result = new ValueSequence(); + for (final IEntry entry : this) { + final Sequence value = entry.value(); + for (final SequenceIterator vi = value.iterate(); vi.hasNext(); ) { + result.add(vi.nextItem().atomize()); + } + } + return result; + } + + @Override + public AtomicValue atomize() throws XPathException { + if (isXq4Atomizable()) { + if (size() == 0) { + // follows the ArrayType pattern for empty containers + return null; + } + final Sequence atomized = atomizeValues(); + if (atomized.isEmpty()) { + return null; + } + if (atomized.getItemCount() > 1) { + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, + "Atomization of a map with multiple values requires a sequence context"); + } + return (AtomicValue) atomized.itemAt(0); + } + // XQuery 3.1 and earlier: maps are function items and cannot be atomized + throw new XPathException(getExpression(), ErrorCodes.FOTY0013, + "A function item other than an array cannot be atomized"); + } + @Override public String toString() { final StringBuilder buf = new StringBuilder("map {");