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