diff --git a/exist-core/pom.xml b/exist-core/pom.xml
index e50c2c58a72..f4c5ad0627d 100644
--- a/exist-core/pom.xml
+++ b/exist-core/pom.xml
@@ -680,6 +680,7 @@
src/test/java/org/exist/xquery/functions/xmldb/XMLDBAuthenticateTest.java
src/main/java/org/exist/xquery/functions/util/Eval.java
src/main/java/org/exist/xquery/value/ArrayListValueSequence.java
+ src/test/java/org/exist/xquery/value/BifurcanMapTest.java
src/main/java/org/exist/xquery/value/AtomicValueComparator.java
src/main/java/org/exist/xquery/value/ItemComparator.java
src/main/java/org/exist/xquery/value/SequenceComparator.java
@@ -820,6 +821,7 @@ The original license statement is also included below.]]>
src/test/java/org/exist/xquery/functions/xmldb/XMLDBAuthenticateTest.java
src/main/java/org/exist/xquery/functions/util/Eval.java
src/main/java/org/exist/xquery/value/ArrayListValueSequence.java
+ src/test/java/org/exist/xquery/value/BifurcanMapTest.java
src/main/java/org/exist/xquery/value/AtomicValueComparator.java
src/main/java/org/exist/xquery/value/ItemComparator.java
src/main/java/org/exist/xquery/value/SequenceComparator.java
diff --git a/exist-core/src/test/java/org/exist/xquery/value/BifurcanMapTest.java b/exist-core/src/test/java/org/exist/xquery/value/BifurcanMapTest.java
new file mode 100644
index 00000000000..c73391d0baa
--- /dev/null
+++ b/exist-core/src/test/java/org/exist/xquery/value/BifurcanMapTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2014, Evolved Binary Ltd
+ *
+ * This file was originally ported from FusionDB to eXist-db by
+ * Evolved Binary, for the benefit of the eXist-db Open Source community.
+ * Only the ported code as it appears in this file, at the time that
+ * it was contributed to eXist-db, was re-licensed under The GNU
+ * Lesser General Public License v2.1 only for use in eXist-db.
+ *
+ * This license grant applies only to a snapshot of the code as it
+ * appeared when ported, it does not offer or infer any rights to either
+ * updates of this source code or access to the original source code.
+ *
+ * The GNU Lesser General Public License v2.1 only license follows.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * Copyright (C) 2014, Evolved Binary Ltd
+ *
+ * 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; version 2.1.
+ *
+ * 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.value;
+
+import io.lacuna.bifurcan.IMap;
+import io.lacuna.bifurcan.LinearMap;
+import org.junit.Test;
+
+import javax.annotation.Nullable;
+
+import static org.exist.xquery.functions.map.MapType.newLinearMap;
+import static org.junit.Assert.*;
+
+/**
+ * Tests to demonstrate Bifurcan Map behaviour
+ *
+ * @author Adam Retter
+ */
+public class BifurcanMapTest {
+
+ /**
+ * Reproduces the XQSuite Test `mt:immutable-remove-then-remove()` from `maps.xql`:
+ *
+ *
+ * let $removed := map:remove(map { 1: true(), 2: true() }, 2)
+ * let $expected := $removed(1)
+ * let $result := map:remove($removed, 1)
+ * return
+ * (
+ * $expected eq $removed(1),
+ * $expected ne $result(1)
+ * )
+ *
+ */
+ @Test
+ public void immutableRemoveThenRemove() {
+
+ /*
+ 1. Create the initial map: `map { 1: true(), 2: true() }`
+ */
+ final IMap map = createMap();
+ checkMapIsForked(map);
+
+ /*
+ 2. Remove the entry in the initial map with key `2`: `let $removed := map:remove(..., 2)`
+ */
+ final IMap removed = removeFromMap(map, new IntegerValue(2));
+ // taken from MapType.java constructor
+ checkMapIsForked(removed);
+
+ /*
+ 3. Get the entry in the removed map with key `1`: `$expected := $removed(1)`
+ */
+ final Sequence expected = getFromMap(removed, new IntegerValue(1));
+
+ /*
+ 4. Remove the entry in the removed map with key `1`: `let $result := map:remove($removed, 1)`
+ */
+ final IMap result = removeFromMap(removed, new IntegerValue(1));
+ // taken from MapType.java constructor
+ checkMapIsForked(result);
+
+ /*
+ `$expected eq $removed(1)`
+ */
+ assertEquals(expected, getFromMap(removed, new IntegerValue(1)));
+
+ /*
+ `$expected ne $result(1)`
+ */
+ assertNotEquals(expected, getFromMap(result, new IntegerValue(1)));
+ }
+
+ /*
+ Taken from MapExpr.java
+ */
+ private static IMap createMap() {
+ final IMap map = newLinearMap(null);
+ map.put(new IntegerValue(1), BooleanValue.TRUE);
+ map.put(new IntegerValue(2), BooleanValue.TRUE);
+
+ // return an immutable map
+ return map.forked();
+ }
+
+ /*
+ Taken from MapFunction.java MapFunction#remove(Sequence[])
+ */
+ private static IMap removeFromMap(final IMap map, final AtomicValue... keys) {
+ // create a transient map
+ IMap newMap = map.linear();
+
+ for (final AtomicValue key: keys) {
+ newMap = newMap.remove(key);
+ }
+
+ // return an immutable map
+ return newMap.forked();
+ }
+
+ /*
+ Taken from MapType.java MapType#get(AtomicValue)
+ */
+ private static @Nullable Sequence getFromMap(final IMap map, final AtomicValue key) {
+ return map.get(key, null);
+ }
+
+ @Test
+ public void bifurcanImmutableRemoveThenRemove() {
+
+ /*
+ 1. Create the initial map: `map { 1: true(), 2: true() }`
+ */
+ IMap map = new LinearMap<>();
+ map.put(1, true);
+ map.put(2, true);
+ map = map.forked(); // make the map immutable
+ checkMapIsForked(map);
+
+ /*
+ 2. Remove the entry in the initial map with key `2`: `let $removed := map:remove(..., 2)`
+ */
+ IMap removed = map.linear(); // create a transient map for modifications
+ assertFalse(removed == map);
+ removed = removed.remove(2);
+ removed = removed.forked(); // make the map immutable
+ checkMapIsForked(removed);
+
+ /*
+ 3. Get the entry in the removed map with key `1`: `$expected := $removed(1)`
+ */
+ final Boolean expected = removed.get(1, null);
+
+ /*
+ 4. Remove the entry in the removed map with key `1`: `let $result := map:remove($removed, 1)`
+ */
+ IMap result = removed.linear(); // create a transient map for modifications
+ assertFalse(result == removed);
+ result = result.remove(1);
+ result = result.forked(); // make the map immutable
+ checkMapIsForked(result);
+
+ /*
+ `$expected eq $removed(1)`
+ */
+ assertEquals(expected, removed.get(1, null));
+
+ /*
+ `$expected ne $result(1)`
+ */
+ assertNotEquals(expected, result.get(1, null));
+ }
+
+ /*
+ Taken from MapType.java constructor
+ */
+ private static void checkMapIsForked(final IMap map) throws IllegalArgumentException {
+ if (map.isLinear()) {
+ throw new IllegalArgumentException("Map must be immutable, but linear Map was provided");
+ }
+ }
+}
diff --git a/exist-core/src/test/xquery/maps/MapsLookup.xml b/exist-core/src/test/xquery/maps/MapsLookup.xml
index 6c95723befd..8e73ce122b2 100644
--- a/exist-core/src/test/xquery/maps/MapsLookup.xml
+++ b/exist-core/src/test/xquery/maps/MapsLookup.xml
@@ -35,13 +35,19 @@
-
+
maps-lookup-001
diff --git a/exist-core/src/test/xquery/maps/maps.xql b/exist-core/src/test/xquery/maps/maps.xql
index 64beb7bb359..8c6ab37a78f 100644
--- a/exist-core/src/test/xquery/maps/maps.xql
+++ b/exist-core/src/test/xquery/maps/maps.xql
@@ -196,14 +196,28 @@ function mt:createWithSingleKey() {
$map("Su")
};
+(:~
+ : TODO(AR) implicit behaviour of map:merge according to XQ3.1 specification should be use-first not use-last
+:)
declare
%test:assertEquals("Saturday", "Caturday")
-function mt:overwriteKeyInNewMap() {
+function mt:merge-duplicate-keys-use-last-implicit-1() {
let $specialWeek := map:merge(($mt:integerKeys, map { 7 : "Caturday" }))
return
($mt:integerKeys(7), $specialWeek(7))
};
+(:~
+ : TODO(AR) implicit behaviour of map:merge according to XQ3.1 specification should be use-first not use-last
+:)
+declare
+ %test:assertEquals("Saturday", "Saturday")
+function mt:merge-duplicate-keys-use-last-implicit-2() {
+ let $specialWeek := map:merge((map { 7 : "Caturday" }, $mt:integerKeys))
+ return
+ ($mt:integerKeys(7), $specialWeek(7))
+};
+
declare
%test:assertEmpty
function mt:mapEmptyValue() {
@@ -627,3 +641,234 @@ function mt:multi-merge() {
map { "wolfgang": "de" }
))?*
};
+
+(:
+ immutability tests for https://github.com/eXist-db/exist/issues/3724
+:)
+declare variable $mt:test-key-one := 1;
+declare variable $mt:test-key-two := 2;
+declare variable $mt:test-key-three := 3;
+declare function mt:create-test-map() {
+ map {
+ $mt:test-key-one : true(),
+ $mt:test-key-two : true()
+ }
+};
+
+declare function mt:create-test-map2() {
+ map {
+ $mt:test-key-three : true()
+ }
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-put-then-put() {
+ let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
+ let $expected := $extended($mt:test-key-one)
+ let $result := map:put($extended, $mt:test-key-one, false())
+ return
+ (
+ $expected eq $extended($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-put-then-remove() {
+ let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
+ let $expected := $extended($mt:test-key-one)
+ let $result := map:remove($extended, $mt:test-key-one)
+ return
+ (
+ $expected eq $extended($mt:test-key-one),
+ empty($result($mt:test-key-one))
+ )
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-put-then-merge() {
+ let $extended := map:put(mt:create-test-map(), $mt:test-key-two, false())
+ let $expected := $extended($mt:test-key-one)
+ let $result := map:merge(($extended, map { $mt:test-key-one : false() }))
+ return
+ (
+ $expected eq $extended($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-remove-then-put() {
+ let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
+ let $expected := $removed($mt:test-key-one)
+ let $result := map:put($removed, $mt:test-key-one, false())
+ return
+ (
+ $expected eq $removed($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals("true", 1, "true", "true", 0)
+function mt:immutable-remove-then-remove() {
+ let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
+ let $expected := $removed($mt:test-key-one)
+ let $result := map:remove($removed, $mt:test-key-one)
+ return
+ (
+ fn:empty($removed($mt:test-key-two)),
+ map:size($removed),
+ $expected eq $removed($mt:test-key-one),
+ fn:empty($result($mt:test-key-one)),
+ map:size($result)
+ )
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-remove-then-merge() {
+ let $removed := map:remove(mt:create-test-map(), $mt:test-key-two)
+ let $expected := $removed($mt:test-key-one)
+ let $result := map:merge(($removed, map { $mt:test-key-one : false() }))
+ return
+ (
+ $expected eq $removed($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-merge-then-put() {
+ let $merged := map:merge(mt:create-test-map())
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:put($merged, $mt:test-key-one, false())
+ return
+ (
+ $expected eq $merged($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals(2, "true", "true", 1)
+function mt:immutable-merge-then-remove() {
+ let $merged := map:merge(mt:create-test-map())
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:remove($merged, $mt:test-key-one)
+ return
+ (
+ map:size($merged),
+ $expected eq $merged($mt:test-key-one),
+ fn:empty($result($mt:test-key-one)),
+ map:size($result)
+ )
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-merge-then-merge() {
+ let $merged := map:merge(mt:create-test-map())
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:merge(($merged, map { $mt:test-key-one : false() }))
+ return
+ (
+ $expected eq $merged($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-merge2-then-put() {
+ let $merged := map:merge((mt:create-test-map(), mt:create-test-map2()))
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:put($merged, $mt:test-key-one, false())
+ return
+ (
+ $expected eq $merged($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals(3, "true", "true", 2)
+function mt:immutable-merge2-then-remove() {
+ let $merged := map:merge((mt:create-test-map(), mt:create-test-map2()))
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:remove($merged, $mt:test-key-one)
+ return
+ (
+ map:size($merged),
+ $expected eq $merged($mt:test-key-one),
+ fn:empty($result($mt:test-key-one)),
+ $expected ne $result($mt:test-key-one),
+ map:size($result)
+ )
+};
+
+(:~
+ : TODO(AR) implicit behaviour of map:merge according to XQ3.1 specification should be use-first not use-last,
+ : therefore the result should be ("true", "true") instead
+:)
+declare
+ %test:assertEquals("true", "false")
+function mt:immutable-merge2-then-merge() {
+ let $merged := map:merge((mt:create-test-map(), mt:create-test-map2()))
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:merge((map { $mt:test-key-one : false() }, $merged))
+ return
+ (
+ $expected eq $merged($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+declare
+ %test:assertEquals("true", "true")
+function mt:immutable-merge-duplicates-then-put() {
+ let $merged := map:merge((mt:create-test-map(), mt:create-test-map()))
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:put($merged, $mt:test-key-one, false())
+ return
+ (
+ $expected eq $merged($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};
+
+declare
+ %test:assertEquals(2, "true", "true", 1)
+function mt:immutable-merge-duplicates-then-remove() {
+ let $merged := map:merge((mt:create-test-map(), mt:create-test-map()))
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:remove($merged, $mt:test-key-one)
+ return
+ (
+ map:size($merged),
+ $expected eq $merged($mt:test-key-one),
+ fn:empty($result($mt:test-key-one)),
+ map:size($result)
+ )
+};
+
+(:~
+ : TODO(AR) implicit behaviour of map:merge according to XQ3.1 specification should be use-first not use-last,
+ : therefore the result should be ("true", "true") instead
+:)
+declare
+ %test:assertEquals("true", "false")
+function mt:immutable-merge-duplicates-then-merge() {
+ let $merged := map:merge((mt:create-test-map(), mt:create-test-map()))
+ let $expected := $merged($mt:test-key-one)
+ let $result := map:merge((map { $mt:test-key-one : false() }, $merged))
+ return
+ (
+ $expected eq $merged($mt:test-key-one),
+ $expected ne $result($mt:test-key-one)
+ )
+};