Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -267,14 +267,16 @@ throws PermissionDeniedException, EXistException, XPathException
v:VERSION_DECL
{
final String version = v.getText();
if (version.equals("3.1")) {
if (version.equals("4.0")) {
context.setXQueryVersion(40);
} else 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");
throw new XPathException(v, ErrorCodes.XQST0031, "Wrong XQuery version: require 1.0, 3.0, 3.1, or 4.0");
}
Comment thread
reinhapa marked this conversation as resolved.
Outdated
}
( enc:STRING_LITERAL )?
Expand Down
101 changes: 101 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 @@ -139,6 +139,9 @@
} 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, ErrorCodes.XPTY0004,
Expand All @@ -155,6 +158,104 @@
{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) {

Check warning on line 207 in exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java#L207

The method 'isXQ4ImplicitCast(int, int)' has an NPath complexity of 729, current threshold is 200
// xs:string → xs:anyURI
if (Type.subTypeOf(sourceType, Type.STRING) && requiredType == Type.ANY_URI) {
return true;
}
// xs:hexBinary ↔ xs:base64Binary
if (Type.subTypeOf(sourceType, Type.HEX_BINARY) && requiredType == Type.BASE64_BINARY) {
return true;
}
if (Type.subTypeOf(sourceType, Type.BASE64_BINARY) && requiredType == Type.HEX_BINARY) {
return true;
}
// Bidirectional numeric: xs:double → xs:decimal, xs:float → xs:decimal
// (Note: decimal→float, decimal→double, float→double already handled by XQ 3.1 rules)
if (Type.subTypeOf(sourceType, Type.DOUBLE) && requiredType == Type.DECIMAL) {
return true;
}
if (Type.subTypeOf(sourceType, Type.FLOAT) && requiredType == Type.DECIMAL) {
return true;
}
// XQ4 also allows any numeric → any other numeric
// "any numeric type to be implicitly converted to any other"
if (Type.subTypeOfUnion(sourceType, Type.NUMERIC) && Type.subTypeOfUnion(requiredType, Type.NUMERIC)) {

Check notice on line 229 in exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java#L229

Avoid unnecessary if..then..else statements when returning booleans
return true;
}
return false;
}

/**
* 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)
if (requiredPrimitive == requiredType) {
return false;
}
// J must be an instance of the same primitive type P
return 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
4 changes: 3 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 @@ -305,7 +305,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
(context.getXQueryVersion() >= 40 && (DynamicTypeCheck.isXQ4ImplicitCast(returnType, argType.getPrimaryType()) || DynamicTypeCheck.isXQ4Relabeling(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 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.
// 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
};