diff --git a/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java b/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java
index de724d4792b..6020046e9d4 100644
--- a/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java
+++ b/exist-core/src/main/java/org/exist/xquery/functions/map/MapType.java
@@ -66,6 +66,14 @@ public class MapType extends AbstractMapType {
* then this is set to {@link #MIXED_KEY_TYPES}.
*
* Uses integer values from {@link org.exist.xquery.value.Type}.
+ *
+ * This is reported via {@link #getKeyType()} but does NOT take part in key
+ * comparison. Lookups ({@link #get(AtomicValue)} / {@link #contains(AtomicValue)})
+ * delegate directly to the underlying map, whose comparator implements
+ * {@code op:same-key} (see {@link AbstractMapType#sameKey}). Keys are compared
+ * by their op:same-key family, never coerced to {@code keyType} - a key from a
+ * different family that shares a lexical value (e.g. the string {@code "12"}
+ * and the integer {@code 12}) is correctly treated as distinct.
*/
private int keyType = UNKNOWN_KEY_TYPE;
@@ -251,12 +259,7 @@ public void add(final AtomicValue key, final Sequence value) {
}
@Override
- public Sequence get(AtomicValue key) {
- key = convert(key);
- if (key == null) {
- return Sequence.EMPTY_SEQUENCE;
- }
-
+ public Sequence get(final AtomicValue key) {
return map.get(key, Sequence.EMPTY_SEQUENCE);
}
@@ -267,12 +270,7 @@ public AbstractMapType put(final AtomicValue key, final Sequence value) {
}
@Override
- public boolean contains(AtomicValue key) {
- key = convert(key);
- if (key == null) {
- return false;
- }
-
+ public boolean contains(final AtomicValue key) {
return map.contains(key);
}
@@ -375,17 +373,6 @@ private void setKeyType(final IMap newMap) {
}
}
- private AtomicValue convert(final AtomicValue key) {
- if (keyType != UNKNOWN_KEY_TYPE && keyType != MIXED_KEY_TYPES) {
- try {
- return key.convertTo(keyType);
- } catch (final XPathException e) {
- return null;
- }
- }
- return key;
- }
-
@Override
public int getKeyType() {
return keyType;
diff --git a/exist-core/src/test/xquery/maps/mapKeySameKey.xqm b/exist-core/src/test/xquery/maps/mapKeySameKey.xqm
new file mode 100644
index 00000000000..d3a74a65f45
--- /dev/null
+++ b/exist-core/src/test/xquery/maps/mapKeySameKey.xqm
@@ -0,0 +1,179 @@
+(:
+ : 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 map key comparison via op:same-key (XQuery 3.1 section 17.1).
+ :
+ : Map keys are compared with op:same-key, which is a TYPE-GROUP comparison:
+ : - the numeric family (xs:decimal/xs:integer/xs:float/xs:double) interchanges,
+ : - the string family (xs:string/xs:anyURI/xs:untypedAtomic) interchanges,
+ : - every other type matches only itself.
+ : There is NO key atomization or casting across groups.
+ :
+ : These tests deliberately use the map{...} CONSTRUCTOR form (which builds a
+ : homogeneously-typed map). The qt3/qt4 suites place their cross-family
+ : distinctness tests on map:entry() (a mixed-key map), so the constructor path
+ : was historically untested - and eXist used to conflate cross-family keys that
+ : shared a lexical value (e.g. map{"12":"x"}(12) wrongly returned "x").
+ :)
+module namespace skt="http://exist-db.org/xquery/test/maps/samekey";
+
+import module namespace test="http://exist-db.org/xquery/xqsuite" at "resource:org/exist/xquery/lib/xqsuite/xqsuite.xql";
+
+(: ----------------------------------------------------------------------- :)
+(: Cross-family distinctness: keys of different op:same-key families that :)
+(: share a lexical value must NOT match. :)
+(: ----------------------------------------------------------------------- :)
+
+declare
+ %test:name("string key vs integer lookup are distinct (contains)")
+ %test:assertFalse
+function skt:string-key-vs-integer-contains() {
+ map:contains(map { "12": "x" }, 12)
+};
+
+declare
+ %test:name("string key vs integer lookup are distinct (get)")
+ %test:assertEmpty
+function skt:string-key-vs-integer-get() {
+ map:get(map { "12": "x" }, 12)
+};
+
+declare
+ %test:name("string key vs double lookup are distinct (contains)")
+ %test:assertFalse
+function skt:string-key-vs-double-contains() {
+ map:contains(map { "5": 1 }, 5.0e0)
+};
+
+declare
+ %test:name("string key vs xs:date lookup are distinct (contains)")
+ %test:assertFalse
+function skt:string-key-vs-date-contains() {
+ map:contains(map { "2020-01-01": 1 }, xs:date("2020-01-01"))
+};
+
+declare
+ %test:name("xs:date key vs string lookup are distinct (contains)")
+ %test:assertFalse
+function skt:date-key-vs-string-contains() {
+ map:contains(map { xs:date("2020-01-01"): 1 }, "2020-01-01")
+};
+
+declare
+ %test:name("boolean key vs string lookup are distinct (contains)")
+ %test:assertFalse
+function skt:boolean-key-vs-string-contains() {
+ map:contains(map { true(): "x" }, "true")
+};
+
+declare
+ %test:name("integer key vs string lookup are distinct (contains)")
+ %test:assertFalse
+function skt:integer-key-vs-string-contains() {
+ map:contains(map { 12: "x" }, "12")
+};
+
+(: ----------------------------------------------------------------------- :)
+(: Numeric family is by VALUE, not by lossy cast: a non-integral lookup :)
+(: must not match an integer key (no truncation). :)
+(: ----------------------------------------------------------------------- :)
+
+declare
+ %test:name("integer key does not match a non-integral double lookup (contains)")
+ %test:assertFalse
+function skt:integer-key-vs-fractional-double-contains() {
+ map:contains(map { 1: "x" }, 1.5e0)
+};
+
+declare
+ %test:name("integer key does not match a non-integral double lookup (get)")
+ %test:assertEmpty
+function skt:integer-key-vs-fractional-double-get() {
+ map:get(map { 1: "x" }, 1.5e0)
+};
+
+(: ----------------------------------------------------------------------- :)
+(: Within-family positives: members of the SAME op:same-key family that :)
+(: are value-equal MUST interchange (these must not regress). :)
+(: ----------------------------------------------------------------------- :)
+
+declare
+ %test:name("numeric family: integer key matches equal double lookup (contains)")
+ %test:assertTrue
+function skt:numeric-integer-key-vs-double-contains() {
+ map:contains(map { 5: 1 }, 5.0e0)
+};
+
+declare
+ %test:name("numeric family: integer key matches equal double lookup (get)")
+ %test:assertEquals(1)
+function skt:numeric-integer-key-vs-double-get() {
+ map:get(map { 5: 1 }, 5.0e0)
+};
+
+declare
+ %test:name("numeric family: decimal key matches equal integer lookup (contains)")
+ %test:assertTrue
+function skt:numeric-decimal-key-vs-integer-contains() {
+ map:contains(map { 5.0: 1 }, 5)
+};
+
+declare
+ %test:name("string family: string key matches equal xs:anyURI lookup (contains)")
+ %test:assertTrue
+function skt:string-key-vs-anyuri-contains() {
+ map:contains(map { "urn:x": 1 }, xs:anyURI("urn:x"))
+};
+
+declare
+ %test:name("string family: string key matches equal xs:untypedAtomic lookup (get)")
+ %test:assertEquals("x")
+function skt:string-key-vs-untypedatomic-get() {
+ map:get(map { "12": "x" }, xs:untypedAtomic("12"))
+};
+
+declare
+ %test:name("string family: anyURI key matches equal string lookup (contains)")
+ %test:assertTrue
+function skt:anyuri-key-vs-string-contains() {
+ map:contains(map { xs:anyURI("urn:x"): 1 }, "urn:x")
+};
+
+(: ----------------------------------------------------------------------- :)
+(: Same-type self-matches still work after the fix. :)
+(: ----------------------------------------------------------------------- :)
+
+declare
+ %test:name("string key matches equal string lookup (get)")
+ %test:assertEquals("x")
+function skt:string-key-self-get() {
+ map:get(map { "12": "x" }, "12")
+};
+
+declare
+ %test:name("xs:date key matches equal xs:date lookup (contains)")
+ %test:assertTrue
+function skt:date-key-self-contains() {
+ map:contains(map { xs:date("2020-01-01"): 1 }, xs:date("2020-01-01"))
+};
diff --git a/exist-core/src/test/xquery/xquery3/fnSerializeNewline.xqm b/exist-core/src/test/xquery/xquery3/fnSerializeNewline.xqm
index 74b22dc20b1..a9c34e557aa 100644
--- a/exist-core/src/test/xquery/xquery3/fnSerializeNewline.xqm
+++ b/exist-core/src/test/xquery/xquery3/fnSerializeNewline.xqm
@@ -49,20 +49,25 @@ function ser:exist-insert-final-newline-true() {
declare
%test:assertTrue
-function ser:exist-insert-final-newline-false-json() {
+function ser:exist-insert-final-newline-true-json() {
let $doc := map { "a": 1 }
let $serialized := fn:serialize($doc,
map {
"method": "json",
- "exist:insert-final-newline": false()
+ xs:QName("exist:insert-final-newline"): true()
}
)
- return fn:ends-with($serialized, "}")
+ return fn:ends-with($serialized, "
")
};
+(: An eXist serialization parameter keyed by the prefixed string "exist:..." is
+ : NON-conformant and is now ignored - only the xs:QName key form is honored,
+ : because op:same-key treats a string key and a QName key as distinct. Passing
+ : true() via the string key therefore has no effect, so the default
+ : (insert-final-newline=false) applies and no trailing newline is added. :)
declare
%test:assertTrue
-function ser:exist-insert-final-newline-true-json() {
+function ser:exist-insert-final-newline-json-prefixed-string-ignored() {
let $doc := map { "a": 1 }
let $serialized := fn:serialize($doc,
map {
@@ -70,5 +75,5 @@ function ser:exist-insert-final-newline-true-json() {
"exist:insert-final-newline": true()
}
)
- return fn:ends-with($serialized, "
")
+ return fn:ends-with($serialized, "}")
};
diff --git a/exist-core/src/test/xquery/xquery3/serialize.xql b/exist-core/src/test/xquery/xquery3/serialize.xql
index 4ac541f0f16..159657f2384 100644
--- a/exist-core/src/test/xquery/xquery3/serialize.xql
+++ b/exist-core/src/test/xquery/xquery3/serialize.xql
@@ -681,14 +681,16 @@ function ser:exist-output-doctype-QName($value as xs:boolean) {
map { xs:QName("exist:output-doctype") : $value })
};
+(: An eXist serialization parameter keyed by the prefixed string "exist:..." is
+ : NON-conformant and is now ignored - only the xs:QName key form is honored,
+ : because op:same-key treats a string key and a QName key as distinct. Passing
+ : true() via the string key therefore has no effect, so the default (no doctype)
+ : applies. The xs:QName form is exercised by ser:exist-output-doctype-QName above. :)
declare
- %test:args("true")
- %test:assertXPath("contains($result, '-//OASIS//DTD DITA BookMap//EN') and contains($result, 'bookmap.dtd')")
- %test:args("false")
%test:assertXPath("not(contains($result, '-//OASIS//DTD DITA BookMap//EN')) and not(contains($result, 'bookmap.dtd'))")
-function ser:exist-output-doctype-string($value as xs:boolean) {
+function ser:exist-output-doctype-prefixed-string-ignored() {
serialize(doc($ser:collection || "/test-with-doctype.xml"),
- map { "exist:output-doctype" : $value })
+ map { "exist:output-doctype" : true() })
};
declare
@@ -701,14 +703,14 @@ function ser:exist-expand-xinclude-QName($value as xs:boolean) {
map { xs:QName("exist:expand-xincludes"): $value })
};
+(: Prefixed-string key is ignored (see note above); passing false() has no
+ : effect, so the default (expand-xincludes=true) applies and the include is
+ : expanded to its 'comment' content. :)
declare
- %test:args("true")
%test:assertXPath("contains($result, 'comment')")
- %test:args("false")
- %test:assertXPath("contains($result, 'include')")
-function ser:exist-expand-xinclude-string($value as xs:boolean) {
+function ser:exist-expand-xinclude-prefixed-string-ignored() {
serialize($ser:xi-doc,
- map { "exist:expand-xincludes": $value })
+ map { "exist:expand-xincludes": false() })
};
declare
@@ -723,16 +725,13 @@ function ser:exist-add-exist-id-QName($value as xs:string) {
map { xs:QName("exist:add-exist-id"): $value })
};
+(: Prefixed-string key is ignored (see note above); passing "all" has no effect,
+ : so the default (add-exist-id=none) applies and no exist:id attributes appear. :)
declare
- %test:args("all")
- %test:assertEquals('123')
- %test:args("element")
- %test:assertEquals('123')
- %test:args("none")
%test:assertXPath("not(contains($result, 'exist:id'))")
-function ser:exist-add-exist-id-string($value as xs:string) {
+function ser:exist-add-exist-id-prefixed-string-ignored() {
serialize(doc($ser:collection || "/test.xml"),
- map { "exist:add-exist-id": $value })
+ map { "exist:add-exist-id": "all" })
};
declare
@@ -750,17 +749,16 @@ function ser:exist-jsonp-QName($value as xs:string) {
)
};
+(: Prefixed-string key is ignored (see note above); passing "functionName" has
+ : no effect, so the default (no JSONP callback) applies and plain JSON results. :)
declare
- %test:args("functionName")
- %test:assertEquals('functionName({"author":["John Doe","Robert Smith"]})')
- %test:args("anotherName")
- %test:assertEquals('anotherName({"author":["John Doe","Robert Smith"]})')
-function ser:exist-jsonp-string($value as xs:string) {
+ %test:assertEquals('{"author":["John Doe","Robert Smith"]}')
+function ser:exist-jsonp-prefixed-string-ignored() {
serialize($ser:in-memory-book,
map {
"method": "json",
"media-type": "application/json",
- "exist:jsonp": $value
+ "exist:jsonp": "functionName"
}
)
};
@@ -775,14 +773,13 @@ function ser:exist-process-xsl-pi-QName($value as xs:boolean) {
map { xs:QName("exist:process-xsl-pi"): $value })
};
+(: Prefixed-string key is ignored (see note above); passing false() has no
+ : effect, so the default (process-xsl-pi=true) applies and the PI is processed. :)
declare
- %test:args("true")
%test:assertEquals('processed')
- %test:args("false")
- %test:assertXPath("contains($result, 'stylesheet')")
-function ser:exist-process-xsl-pi-string($value as xs:boolean) {
+function ser:exist-process-xsl-pi-prefixed-string-ignored() {
serialize(doc($ser:collection || "/test-xsl.xml"),
- map { "exist:process-xsl-pi": $value })
+ map { "exist:process-xsl-pi": false() })
};
declare
diff --git a/extensions/modules/file/src/test/xquery/modules/file/sync-serialize.xqm b/extensions/modules/file/src/test/xquery/modules/file/sync-serialize.xqm
index c26c201a77a..cca0300211e 100644
--- a/extensions/modules/file/src/test/xquery/modules/file/sync-serialize.xqm
+++ b/extensions/modules/file/src/test/xquery/modules/file/sync-serialize.xqm
@@ -207,7 +207,7 @@ function syse:insert-final-newline-yes() {
let $sync := file:sync(
$fixtures:collection,
$directory,
- map{ "exist:insert-final-newline": true() }
+ map{ xs:QName("exist:insert-final-newline"): true() }
)
return (
@@ -231,7 +231,7 @@ function syse:insert-final-newline-no() {
let $sync := file:sync(
$fixtures:collection,
$directory,
- map{ "exist:insert-final-newline": false() }
+ map{ xs:QName("exist:insert-final-newline"): false() }
)
return (