Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g
Original file line number Diff line number Diff line change
Expand Up @@ -451,15 +451,13 @@ throws PermissionDeniedException, EXistException, XPathException
v:VERSION_DECL
{
final String version = v.getText();
if (version.equals("3.1")) {
context.setXQueryVersion(31);
} else if (version.equals("3.0")) {
context.setXQueryVersion(30);
} else if (version.equals("1.0")) {
context.setXQueryVersion(10);
} else {
throw new XPathException(v, ErrorCodes.XQST0031, "Wrong XQuery version: require 1.0, 3.0 or 3.1");
}
context.setXQueryVersion(switch (version) {
case "4.0" -> 40;
case "3.1" -> 31;
case "3.0" -> 30;
case "1.0" -> 10;
default -> throw new XPathException(v, ErrorCodes.XQST0031, "Wrong XQuery version: require 1.0, 3.0, 3.1, or 4.0");
});
}
( enc:STRING_LITERAL )?
{
Expand Down
83 changes: 83 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ private void check(Sequence result, Item item) throws XPathException {
} else if (type == Type.ANY_URI && requiredType == Type.STRING) {
item = item.convertTo(Type.STRING);
type = Type.STRING;
// XQuery 4.0 implicit casting (spec §3.4.1 item 4)
} else if (context.getXQueryVersion() >= 40) {
item = xq4ImplicitCast(item, type, requiredType);
} else {
if (!(Type.subTypeOf(type, requiredType))) {
throw new XPathException(expression, typeMismatchError,
Expand All @@ -165,6 +168,86 @@ private void check(Sequence result, Item item) throws XPathException {
{result.add(item);}
}

/**
* XQuery 4.0 coercion rules per spec §3.4.1.
* Handles implicit casting (item 4) and relabeling (item 6).
*/
private Item xq4ImplicitCast(Item item, final int type, final int requiredType) throws XPathException {
// Item 4: Implicit Casting table
if (isXQ4ImplicitCast(type, requiredType)) {
try {
return item.convertTo(requiredType);
} catch (final XPathException e) {
throw new XPathException(expression, ErrorCodes.XPTY0004,
"Cannot coerce " + Type.getTypeName(type) + "(" +
item.getStringValue() + ") to " + Type.getTypeName(requiredType));
}
}
// Item 6: Relabeling — if R is derived from primitive P, and J is an instance of P,
// relabel J as R if J's datum is within the value space of R.
if (isXQ4Relabeling(type, requiredType)) {
try {
return item.convertTo(requiredType);
} catch (final XPathException e) {
throw new XPathException(expression, ErrorCodes.XPTY0004,
"Cannot relabel " + Type.getTypeName(type) + "(" +
item.getStringValue() + ") as " + Type.getTypeName(requiredType) +
": value is not in the value space of the target type");
}
}
// Fall through to the standard type error
if (!(Type.subTypeOf(type, requiredType))) {
throw new XPathException(expression, ErrorCodes.XPTY0004,
Type.getTypeName(item.getType()) + "(" + item.getStringValue() +
") is not a sub-type of " + Type.getTypeName(requiredType));
} else {
throw new XPathException(expression, ErrorCodes.FOCH0002, "Required type is " +
Type.getTypeName(requiredType) + " but got '" + Type.getTypeName(item.getType()) + "(" +
item.getStringValue() + ")'");
}
}

/**
* Check if an implicit cast is allowed from a source type to a target type
* under XQuery 4.0 coercion rules (spec §3.4.1 item 4, Implicit Casting table).
*
* The "from" column matches if J is an instance of "from" (including subtypes).
* The "to" column must match R exactly (the required type must be the primitive type).
*/
static boolean isXQ4ImplicitCast(final int sourceType, final int requiredType) {
return
// xs:string → xs:anyURI
(Type.subTypeOf(sourceType, Type.STRING) && requiredType == Type.ANY_URI)
// xs:hexBinary ↔ xs:base64Binary
|| (Type.subTypeOf(sourceType, Type.HEX_BINARY) && requiredType == Type.BASE64_BINARY)
|| (Type.subTypeOf(sourceType, Type.BASE64_BINARY) && requiredType == Type.HEX_BINARY)
// any numeric type → any other numeric type. This subsumes the specific cases such as
// xs:double → xs:decimal and xs:float → xs:decimal; decimal→float, decimal→double and
// float→double are already permitted by the XQuery 3.1 numeric type promotion rules.
|| (Type.subTypeOfUnion(sourceType, Type.NUMERIC) && Type.subTypeOfUnion(requiredType, Type.NUMERIC));
}

/**
* Check if relabeling is allowed under XQuery 4.0 coercion rules (spec §3.4.1 item 6).
* Relabeling applies when R is derived from a primitive type P, and J is an instance of P
* (but not already an instance of R). The actual value check (whether the datum is in
* the value space of R) is deferred to convertTo().
*/
static boolean isXQ4Relabeling(final int sourceType, final int requiredType) {
// Only applies to atomic types
if (!Type.subTypeOf(sourceType, Type.ANY_ATOMIC_TYPE) || !Type.subTypeOf(requiredType, Type.ANY_ATOMIC_TYPE)) {
return false;
}
try {
final int requiredPrimitive = Type.primitiveTypeOf(requiredType);
// Relabeling only applies when R is a derived type (not a primitive itself),
// and J must be an instance of the same primitive type P.
return requiredPrimitive != requiredType && Type.subTypeOf(sourceType, requiredPrimitive);
} catch (final IllegalArgumentException e) {
return false;
}
}

/* (non-Javadoc)
* @see org.exist.xquery.Expression#dump(org.exist.xquery.util.ExpressionDumper)
*/
Expand Down
15 changes: 14 additions & 1 deletion exist-core/src/main/java/org/exist/xquery/Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ private Expression checkArgumentType(
if (returnType != Type.ITEM && !Type.subTypeOf(returnType, argType.getPrimaryType())) {
if (!(Type.subTypeOf(argType.getPrimaryType(), returnType) ||
//Because () is seen as a node
(argType.getCardinality().isSuperCardinalityOrEqualOf(Cardinality.EMPTY_SEQUENCE) && returnType == Type.NODE))) {
(argType.getCardinality().isSuperCardinalityOrEqualOf(Cardinality.EMPTY_SEQUENCE) && returnType == Type.NODE) ||
// XQuery 4.0: allow implicit casts and relabeling
isXQ4CoercionAllowed(returnType, argType.getPrimaryType()))) {
LOG.debug(ExpressionDumper.dump(argument));
throw new XPathException(this, ErrorCodes.XPTY0004, Messages.getMessage(Error.FUNC_PARAM_TYPE_STATIC,
String.valueOf(argPosition), mySignature, argType.toString(), Type.getTypeName(returnType)));
Expand All @@ -328,6 +330,17 @@ private Expression checkArgumentType(
return new DynamicTypeCheck(context, argType.getPrimaryType(), argument);
}

/**
* Whether a value of {@code sourceType} may be implicitly coerced to {@code requiredType}
* under the XQuery 4.0 function coercion rules (spec §3.4.1): implicit casting or relabeling.
* Always false below XQuery 4.0.
*/
private boolean isXQ4CoercionAllowed(final int sourceType, final int requiredType) {
return context.getXQueryVersion() >= 40
&& (DynamicTypeCheck.isXQ4ImplicitCast(sourceType, requiredType)
|| DynamicTypeCheck.isXQ4Relabeling(sourceType, requiredType));
}

protected boolean checkArgumentTypeCardinality(
final Expression argument,
@Nullable final SequenceType argType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ private Item convert(Item item) throws XPathException {
if (Type.subTypeOf(item.getType(), requiredType)) {
return item;
}
if (item.getType() == Type.INTEGER && requiredType == Type.POSITIVE_INTEGER) {
// In XQuery 3.1, reject integer→positiveInteger conversion.
Comment thread
dizzzz marked this conversation as resolved.
// In XQuery 4.0, relabeling allows this if the value is positive (§3.4.1 item 6).
if (item.getType() == Type.INTEGER && requiredType == Type.POSITIVE_INTEGER
&& context.getXQueryVersion() < 40) {
throw new XPathException(this, ErrorCodes.FORG0001,
"cannot convert '"
+ Type.getTypeName(item.getType())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
(:
: 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";

module namespace tpg="http://exist-db.org/xquery/test/type-promotion-gating";

declare namespace test="http://exist-db.org/xquery/xqsuite";

(: Helper function that expects xs:anyURI :)
declare function tpg:expect-uri($uri as xs:anyURI) as xs:string {
string($uri)
};

(: In XQuery 3.1, string -> anyURI coercion should NOT work :)
declare
%test:assertError("XPTY0004")
function tpg:string-to-anyuri-rejected-in-31() {
tpg:expect-uri("http://example.com")
};
111 changes: 111 additions & 0 deletions exist-core/src/test/xquery/xquery3/xq4-type-promotion.xql
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
(:
: 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 "4.0";

module namespace tp="http://exist-db.org/xquery/test/type-promotion";

declare namespace test="http://exist-db.org/xquery/xqsuite";

(: Helper function that expects xs:anyURI :)
declare function tp:expect-uri($uri as xs:anyURI) as xs:string {
string($uri)
};

(: Helper function that expects xs:base64Binary :)
declare function tp:expect-base64($val as xs:base64Binary) as xs:string {
string($val)
};

(: Helper function that expects xs:hexBinary :)
declare function tp:expect-hex($val as xs:hexBinary) as xs:string {
string($val)
};

(: Helper function that expects xs:decimal :)
declare function tp:expect-decimal($val as xs:decimal) as xs:decimal {
$val
};

(: === xs:string -> xs:anyURI implicit casting === :)

declare
%test:assertEquals("http://example.com")
function tp:string-to-anyuri-coercion() {
tp:expect-uri("http://example.com")
};

declare
%test:assertEquals("")
function tp:empty-string-to-anyuri() {
tp:expect-uri("")
};

(: === xs:hexBinary <-> xs:base64Binary implicit casting === :)

declare
%test:assertEquals("AQID")
function tp:hex-to-base64-coercion() {
tp:expect-base64(xs:hexBinary("010203"))
};

declare
%test:assertEquals("010203")
function tp:base64-to-hex-coercion() {
tp:expect-hex(xs:base64Binary("AQID"))
};

(: === Bidirectional numeric implicit casting === :)

declare
%test:assertEquals(3.14)
function tp:double-to-decimal-coercion() {
tp:expect-decimal(3.14e0)
};

(: === Relabeling: derived atomic types (§3.4.1 item 6) === :)

(: Helper function that expects xs:positiveInteger :)
declare function tp:expect-positive-integer($val as xs:positiveInteger) as xs:integer {
$val
};

declare
%test:assertEquals(42)
function tp:integer-to-positive-integer-relabeling() {
tp:expect-positive-integer(42)
};

declare
%test:assertError("XPTY0004")
function tp:negative-integer-to-positive-integer-fails() {
tp:expect-positive-integer(-5)
};

(: === Version gating: ensure coercion only works in XQ4 === :)
(: These tests run in this 4.0 module, so they should pass :)

declare
%test:assertTrue
function tp:string-to-anyuri-instance-check() {
let $result := tp:expect-uri("test")
return $result instance of xs:string
};
Loading