From da883a3ea8dedb4d384b668aa6066de17fc0ae44 Mon Sep 17 00:00:00 2001 From: Gunther Rademacher Date: Tue, 25 Nov 2025 14:29:47 +0100 Subject: [PATCH 1/3] add support for type xs:dateTimeStamp --- .../java/org/basex/core/jobs/QueryJob.java | 3 +- .../java/org/basex/query/QueryDateTime.java | 3 +- .../main/java/org/basex/query/QueryError.java | 2 + .../main/java/org/basex/query/expr/Calc.java | 15 +++---- .../main/java/org/basex/query/expr/CmpG.java | 1 + .../java/org/basex/query/func/Function.java | 8 ++-- .../query/func/archive/ArchiveCreate.java | 3 +- .../java/org/basex/query/func/fn/FnMin.java | 7 +++- .../basex/query/func/fn/FnParseIetfDate.java | 3 +- .../basex/query/util/format/Formatter.java | 6 ++- .../org/basex/query/value/item/ADate.java | 9 +++-- .../java/org/basex/query/value/item/Dtm.java | 26 +++++++++---- .../org/basex/query/value/type/AtomType.java | 39 +++++++++++++------ .../org/basex/query/value/type/Types.java | 2 + .../java/org/basex/query/SeqTypeTest.java | 19 +++++++++ .../java/org/basex/query/simple/TypeTest.java | 2 + 16 files changed, 106 insertions(+), 42 deletions(-) diff --git a/basex-core/src/main/java/org/basex/core/jobs/QueryJob.java b/basex-core/src/main/java/org/basex/core/jobs/QueryJob.java index 0c57867558..6a1af547a6 100644 --- a/basex-core/src/main/java/org/basex/core/jobs/QueryJob.java +++ b/basex-core/src/main/java/org/basex/core/jobs/QueryJob.java @@ -11,6 +11,7 @@ import org.basex.query.*; import org.basex.query.value.*; import org.basex.query.value.item.*; +import org.basex.query.value.type.*; import org.basex.util.*; import org.basex.util.log.*; @@ -124,7 +125,7 @@ public static Item toTime(final String string, final InputInfo info) throws Quer // time if(ADate.TIME.matcher(string).matches()) return new Tim(token(string), info); // dateTime - return new Dtm(token(string), info); + return new Dtm(token(string), AtomType.DATE_TIME, info); } /** diff --git a/basex-core/src/main/java/org/basex/query/QueryDateTime.java b/basex-core/src/main/java/org/basex/query/QueryDateTime.java index 5c2d0cd4e4..1610882d44 100644 --- a/basex-core/src/main/java/org/basex/query/QueryDateTime.java +++ b/basex-core/src/main/java/org/basex/query/QueryDateTime.java @@ -5,6 +5,7 @@ import java.util.*; import org.basex.query.value.item.*; +import org.basex.query.value.type.*; import org.basex.util.*; /** @@ -37,7 +38,7 @@ public QueryDateTime() throws QueryException { final String znm = zon.substring(0, 3), zns = zon.substring(3); time = new Tim(token(hms + znm + ':' + zns), null); date = new Dat(token(ymd + znm + ':' + zns), null); - datm = new Dtm(token(ymd + 'T' + hms + znm + ':' + zns), null); + datm = new Dtm(token(ymd + 'T' + hms + znm + ':' + zns), AtomType.DATE_TIME_STAMP, null); zone = new DTDur(Strings.toInt(znm), Strings.toInt(zns)); nano = System.nanoTime(); } diff --git a/basex-core/src/main/java/org/basex/query/QueryError.java b/basex-core/src/main/java/org/basex/query/QueryError.java index ab290bcf7e..bc52c97553 100644 --- a/basex-core/src/main/java/org/basex/query/QueryError.java +++ b/basex-core/src/main/java/org/basex/query/QueryError.java @@ -702,6 +702,8 @@ public enum QueryError { /** Error code. */ INVALIDZONE_X(FORG, 1, "Invalid timezone: %."), /** Error code. */ + MISSINGZONE_X(FORG, 1, "xs:dateTimeStamp value must have a timezone: %."), + /** Error code. */ FUNCCAST_X_X(FORG, 1, "Cannot convert to %: %."), /** Error code. */ FUNCCAST_X_X_X(FORG, 1, "Cannot convert % to %: %."), diff --git a/basex-core/src/main/java/org/basex/query/expr/Calc.java b/basex-core/src/main/java/org/basex/query/expr/Calc.java index 5b00849e45..0eaee9ac58 100644 --- a/basex-core/src/main/java/org/basex/query/expr/Calc.java +++ b/basex-core/src/main/java/org/basex/query/expr/Calc.java @@ -44,8 +44,8 @@ public Item eval(final Item item1, final Item item2, final InputInfo info) if(type1 == YEAR_MONTH_DURATION) return new YMDur((YMDur) item1, (YMDur) item2, true, info); if(type1 == DAY_TIME_DURATION) return new DTDur((DTDur) item1, (DTDur) item2, true, info); } - if(type1 == DATE_TIME) return new Dtm((Dtm) item1, dur(info, item2), true, info); - if(type2 == DATE_TIME) return new Dtm((Dtm) item2, dur(info, item1), true, info); + if(type1.instanceOf(DATE_TIME)) return new Dtm((Dtm) item1, dur(info, item2), true, info); + if(type2.instanceOf(DATE_TIME)) return new Dtm((Dtm) item2, dur(info, item1), true, info); if(type1 == DATE) return new Dat((Dat) item1, dur(info, item2), true, info); if(type2 == DATE) return new Dat((Dat) item2, dur(info, item1), true, info); if(type1 == TIME && type2 == DAY_TIME_DURATION) @@ -83,7 +83,8 @@ public Expr optimize(final Expr expr1, final Expr expr2, final InputInfo info, public Type type(final Type type1, final Type type2) { if(type1 == YEAR_MONTH_DURATION && type2 == YEAR_MONTH_DURATION) return YEAR_MONTH_DURATION; if(type1 == DAY_TIME_DURATION && type2 == DAY_TIME_DURATION) return DAY_TIME_DURATION; - if(type1 == DATE_TIME || type2 == DATE_TIME) return DATE_TIME; + if(type1.instanceOf(DATE_TIME)) return type1; + if(type2.instanceOf(DATE_TIME)) return type2; if(type1 == DATE || type2 == DATE) return DATE; if(type1 == TIME && type2 == DAY_TIME_DURATION || type1 == DAY_TIME_DURATION && type2 == TIME) return TIME; @@ -117,7 +118,7 @@ public Item eval(final Item item1, final Item item2, final InputInfo info) // dates or durations if(type1 == type2) { - if(type1 == DATE_TIME || type1 == DATE || type1 == TIME) + if(type1.oneOf(DATE_TIME_STAMP, DATE_TIME, DATE, TIME)) return new DTDur((ADate) item1, (ADate) item2, info); if(type1 == YEAR_MONTH_DURATION) return new YMDur((YMDur) item1, (YMDur) item2, false, info); @@ -125,7 +126,7 @@ public Item eval(final Item item1, final Item item2, final InputInfo info) return new DTDur((DTDur) item1, (DTDur) item2, false, info); throw numberError(item1, info); } - if(type1 == DATE_TIME) return new Dtm((Dtm) item1, dur(info, item2), false, info); + if(type1.instanceOf(DATE_TIME)) return new Dtm((Dtm) item1, dur(info, item2), false, info); if(type1 == DATE) return new Dat((Dat) item1, dur(info, item2), false, info); if(type1 == TIME && type2 == DAY_TIME_DURATION) return new Tim((Tim) item1, (DTDur) item2, false); @@ -160,10 +161,10 @@ public Expr optimize(final Expr expr1, final Expr expr2, final InputInfo info, @Override public Type type(final Type type1, final Type type2) { - if(type1 == DATE_TIME && type2 == DATE_TIME || + if(type1.instanceOf(DATE_TIME) && type2.instanceOf(DATE_TIME) || type1 == DATE && type2 == DATE) return DAY_TIME_DURATION; if(type1 == TIME && type2 == TIME) return DAY_TIME_DURATION; - if(type1 == DATE_TIME) return DATE_TIME; + if(type1.instanceOf(DATE_TIME)) return type1; if(type1 == DATE) return DATE; if(type1 == TIME && type2 == DAY_TIME_DURATION) return TIME; return numType(type1, type2); diff --git a/basex-core/src/main/java/org/basex/query/expr/CmpG.java b/basex-core/src/main/java/org/basex/query/expr/CmpG.java index 2ade500d6a..3e181ebb2b 100644 --- a/basex-core/src/main/java/org/basex/query/expr/CmpG.java +++ b/basex-core/src/main/java/org/basex/query/expr/CmpG.java @@ -381,6 +381,7 @@ public static boolean comparable(final Type type1, final Type type2, final boole type1.isNumber() && type2.isNumber() || type1.isStringOrUntyped() && type2.isStringOrUntyped() || untyped && (type1.isUntyped() || type2.isUntyped()) || + type1.instanceOf(AtomType.DATE_TIME) && type2.instanceOf(AtomType.DATE_TIME) || type1.instanceOf(AtomType.DURATION) && type2.instanceOf(AtomType.DURATION) || type1.instanceOf(AtomType.BINARY) && type2.instanceOf(AtomType.BINARY); } diff --git a/basex-core/src/main/java/org/basex/query/func/Function.java b/basex-core/src/main/java/org/basex/query/func/Function.java index ee2d67a1df..5d982e158f 100644 --- a/basex-core/src/main/java/org/basex/query/func/Function.java +++ b/basex-core/src/main/java/org/basex/query/func/Function.java @@ -174,7 +174,7 @@ public enum Function implements AFunction { params(), DATE_O, flag(NDT)), /** XQuery function. */ CURRENT_DATETIME(FnCurrentDateTime::new, "current-dateTime()", - params(), DATE_TIME_O, flag(NDT)), + params(), DATE_TIME_STAMP_O, flag(NDT)), /** XQuery function. */ CURRENT_TIME(FnCurrentTime::new, "current-time()", params(), TIME_O, flag(NDT)), @@ -721,7 +721,7 @@ ITEM_ZM, flag(HOF)), params(ITEM_ZM), STRING_O), /** XQuery function. */ UNIX_DATETIME(FnUnixDateTime::new, "unix-dateTime([value])", - params(INTEGER_ZO), DATE_TIME_O), + params(INTEGER_ZO), DATE_TIME_STAMP_O), /** XQuery function. */ UNORDERED(FnUnordered::new, "unordered(input)", params(ITEM_ZM), ITEM_ZM), @@ -1196,7 +1196,7 @@ ANY_URI_O, flag(NDT), CLIENT_URI, Perm.CREATE), params(INTEGER_O, INTEGER_O), STRING_O, CONVERT_URI), /** XQuery function. */ _CONVERT_INTEGER_TO_DATETIME(ConvertIntegerToDateTime::new, "integer-to-dateTime(value)", - params(INTEGER_O), DATE_TIME_O, CONVERT_URI), + params(INTEGER_O), DATE_TIME_STAMP_O, CONVERT_URI), /** XQuery function. */ _CONVERT_INTEGER_TO_DAYTIME(ConvertIntegerToDayTime::new, "integer-to-dayTime(value)", params(INTEGER_O), DAY_TIME_DURATION_O, CONVERT_URI), @@ -1456,7 +1456,7 @@ ANY_URI_O, flag(NDT), CLIENT_URI, Perm.CREATE), params(STRING_O), BOOLEAN_O, flag(NDT), FILE_URI, Perm.ADMIN), /** XQuery function. */ _FILE_LAST_MODIFIED(FileLastModified::new, "last-modified(path)", - params(STRING_O), DATE_TIME_O, flag(NDT), FILE_URI, Perm.ADMIN), + params(STRING_O), DATE_TIME_STAMP_O, flag(NDT), FILE_URI, Perm.ADMIN), /** XQuery function. */ _FILE_LINE_SEPARATOR(FileLineSeparator::new, "line-separator()", params(), STRING_O, FILE_URI), diff --git a/basex-core/src/main/java/org/basex/query/func/archive/ArchiveCreate.java b/basex-core/src/main/java/org/basex/query/func/archive/ArchiveCreate.java index 949fa37404..e80425a3b6 100644 --- a/basex-core/src/main/java/org/basex/query/func/archive/ArchiveCreate.java +++ b/basex-core/src/main/java/org/basex/query/func/archive/ArchiveCreate.java @@ -17,6 +17,7 @@ import org.basex.query.value.array.*; import org.basex.query.value.item.*; import org.basex.query.value.node.*; +import org.basex.query.value.type.*; import org.basex.util.*; /** @@ -194,7 +195,7 @@ final long timestamp(final Item header, final QueryContext qc) throws QueryExcep if(header instanceof final ANode node) { final byte[] value = node.attribute(Q_LAST_MODIFIED); try { - if(value != null) return toMs(new Dtm(value, info), qc); + if(value != null) return toMs(new Dtm(value, AtomType.DATE_TIME, info), qc); } catch(final QueryException ex) { Util.debug(ex); throw ARCHIVE_TIMESTAMP_X.get(info, value); diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnMin.java b/basex-core/src/main/java/org/basex/query/func/fn/FnMin.java index 8fc9c35103..0b395c0d47 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnMin.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnMin.java @@ -56,8 +56,10 @@ final Item minmax(final boolean min, final QueryContext qc) throws QueryExceptio final Type type = item.type; if(!type.isSortable()) throw COMPARE_X_X.get(info, type, item); - final boolean string = item instanceof AStr, numeric = !string && !( + final boolean string = item instanceof AStr; + final boolean numeric = !string && !( type == BOOLEAN || item instanceof ADate || item instanceof Dur || item instanceof Bin); + final boolean dateTime = type.instanceOf(DATE_TIME); if(numeric) { if(type.isUntyped()) item = DOUBLE.cast(item, qc, info); if(item == Dbl.NAN || item == Flt.NAN) return item; @@ -68,7 +70,8 @@ final Item minmax(final boolean min, final QueryContext qc) throws QueryExceptio if(type2.isUntyped()) it = DOUBLE.cast(it, qc, info); if(it == Dbl.NAN || it == Flt.NAN) return it; } - if(!(numeric ? it instanceof ANum : string ? it instanceof AStr : type == type2)) { + if(!(numeric ? it instanceof ANum : string ? it instanceof AStr : dateTime ? + type2.instanceOf(DATE_TIME) : type == type2)) { throw ARGTYPE_X_X_X.get(info, type, type2, it); } if(min ^ item.compare(it, collation, true, info) < 0) item = it; diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnParseIetfDate.java b/basex-core/src/main/java/org/basex/query/func/fn/FnParseIetfDate.java index f354dcb793..43210dc229 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnParseIetfDate.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnParseIetfDate.java @@ -9,6 +9,7 @@ import org.basex.query.func.*; import org.basex.query.value.item.*; import org.basex.query.value.seq.*; +import org.basex.query.value.type.*; import org.basex.util.*; /** @@ -107,7 +108,7 @@ private Dtm parse() throws QueryException { addNumber(tb, Math.abs(z % 60), 2); } try { - return new Dtm(tb.finish(), info); + return new Dtm(tb.finish(), AtomType.DATE_TIME, info); } catch(final QueryException ex) { Util.debug(ex); throw QueryError.IETF_INV_X.get(info, original); diff --git a/basex-core/src/main/java/org/basex/query/util/format/Formatter.java b/basex-core/src/main/java/org/basex/query/util/format/Formatter.java index c2f7b71925..78429b792a 100644 --- a/basex-core/src/main/java/org/basex/query/util/format/Formatter.java +++ b/basex-core/src/main/java/org/basex/query/util/format/Formatter.java @@ -227,8 +227,10 @@ public final byte[] formatDate(final ADate dt, final byte[] languageTag, final b case 'w': num = date.toJava().toGregorianCalendar().get(Calendar.WEEK_OF_MONTH); // first week of month: fix value, according to ISO 8601 - if(num == 0) num = new Dtm(new Dtm(date), new DTDur(date.day() * 24, 0), - false, info).toJava().toGregorianCalendar().get(Calendar.WEEK_OF_MONTH); + if(num == 0) { + num = new Dtm(new Dtm(date, AtomType.DATE_TIME, info), new DTDur(date.day() * 24, 0), + false, info).toJava().toGregorianCalendar().get(Calendar.WEEK_OF_MONTH); + } err = tim; break; case 'H': diff --git a/basex-core/src/main/java/org/basex/query/value/item/ADate.java b/basex-core/src/main/java/org/basex/query/value/item/ADate.java index 600fcb92c3..fd5e95c197 100644 --- a/basex-core/src/main/java/org/basex/query/value/item/ADate.java +++ b/basex-core/src/main/java/org/basex/query/value/item/ADate.java @@ -395,14 +395,15 @@ public final boolean equal(final Item item, final Collation coll, final InputInf @Override public final boolean deepEqual(final Item item, final DeepEqual deep) throws QueryException { - return type == item.type && compare(item, deep.info) == 0 && - (!deep.options.get(DeepEqualOptions.TIMEZONES) || tz == ((ADate) item).tz); + return (type.instanceOf(item.type) || item.type.instanceOf(type)) + && compare(item, deep.info) == 0 + && (!deep.options.get(DeepEqualOptions.TIMEZONES) || tz == ((ADate) item).tz); } @Override public final boolean atomicEqual(final Item item) throws QueryException { - return this == item || type == item.type && compare(item, null) == 0 && - hasTz() == ((ADate) item).hasTz(); + return this == item || (type.instanceOf(item.type) || item.type.instanceOf(type)) + && compare(item, null) == 0 && hasTz() == ((ADate) item).hasTz(); } @Override diff --git a/basex-core/src/main/java/org/basex/query/value/item/Dtm.java b/basex-core/src/main/java/org/basex/query/value/item/Dtm.java index 088fee444d..70e4e23d8f 100644 --- a/basex-core/src/main/java/org/basex/query/value/item/Dtm.java +++ b/basex-core/src/main/java/org/basex/query/value/item/Dtm.java @@ -11,7 +11,7 @@ import org.basex.util.*; /** - * DateTime item ({@code xs:dateTime}). + * DateTime item ({@code xs:dateTime} and {@code xs:dateTimeStamp}). * * @author BaseX Team, BSD License * @author Christian Gruen @@ -23,9 +23,13 @@ public final class Dtm extends ADate { /** * Constructor. * @param date date + * @param type item type + * @param info input info (can be {@code null}) + * @throws QueryException query exception */ - public Dtm(final ADate date) { - super(AtomType.DATE_TIME, date); + public Dtm(final ADate date, final Type type, final InputInfo info) throws QueryException { + super(type, date); + if(type == AtomType.DATE_TIME_STAMP && !hasTz()) throw MISSINGZONE_X.get(info, date); if(hour == -1) { hour = 0; minute = 0; @@ -56,11 +60,12 @@ public Dtm(final Dat date, final Tim time, final InputInfo info) throws QueryExc /** * Constructor. * @param dateTime date time + * @param type item type * @param info input info (can be {@code null}) * @throws QueryException query exception */ - public Dtm(final byte[] dateTime, final InputInfo info) throws QueryException { - super(AtomType.DATE_TIME); + public Dtm(final byte[] dateTime, final Type type, final InputInfo info) throws QueryException { + super(type); final int i = Token.indexOf(dateTime, 'T'); if(i == -1) throw dateError(dateTime, XDTM, info); date(Token.substring(dateTime, 0, i), XDTM, info); @@ -78,7 +83,7 @@ public Dtm(final byte[] dateTime, final InputInfo info) throws QueryException { public Dtm(final Dtm dateTime, final Dur dur, final boolean plus, final InputInfo info) throws QueryException { - this(dateTime); + this(dateTime, dateTime.type, info); if(dur instanceof final DTDur dtd) { calc(dtd, plus); if(year <= MIN_YEAR || year > MAX_YEAR) throw YEARRANGE_X.get(info, year); @@ -90,7 +95,7 @@ public Dtm(final Dtm dateTime, final Dur dur, final boolean plus, final InputInf @Override public Dtm timeZone(final DTDur dur, final boolean undefined, final InputInfo info) throws QueryException { - final Dtm dtm = new Dtm(this); + final Dtm dtm = new Dtm(this, AtomType.DATE_TIME, info); dtm.tz(dur, undefined, info); return dtm; } @@ -102,9 +107,14 @@ public Dtm timeZone(final DTDur dur, final boolean undefined, final InputInfo in */ public static Dtm get(final long ms) { try { - return new Dtm(Token.token(DateTime.format(new Date(ms))), null); + return new Dtm(Token.token(DateTime.format(new Date(ms))), AtomType.DATE_TIME_STAMP, null); } catch(final QueryException ex) { throw Util.notExpected(ex); } } + + @Override + public boolean comparable(final Item item) { + return item instanceof Dtm; + } } diff --git a/basex-core/src/main/java/org/basex/query/value/type/AtomType.java b/basex-core/src/main/java/org/basex/query/value/type/AtomType.java index 22d7bd114b..01fb09799d 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/AtomType.java +++ b/basex-core/src/main/java/org/basex/query/value/type/AtomType.java @@ -634,8 +634,8 @@ public DTDur read(final DataInput in, final QueryContext qc) @Override public Dtm cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type == DATE) return new Dtm((ADate) item); - if(isString(item)) return new Dtm(item.string(info), info); + if(item.type.oneOf(DATE, DATE_TIME_STAMP)) return new Dtm((ADate) item, DATE_TIME, info); + if(isString(item)) return new Dtm(item.string(info), DATE_TIME, info); throw typeError(item, this, info); } @Override @@ -645,19 +645,36 @@ public Dtm cast(final Object value, final QueryContext qc, final InputInfo info) } @Override public Dtm read(final DataInput in, final QueryContext qc) throws IOException, QueryException { - return new Dtm(in.readToken(), null); + return new Dtm(in.readToken(), DATE_TIME, null); } }, /** DateTimeStamp type. */ - DATE_TIME_STAMP("dateTimeStamp", null, XS_URI, false, false, false, true, Type.ID.DTS), + DATE_TIME_STAMP("dateTimeStamp", DATE_TIME, XS_URI, false, false, false, true, Type.ID.DTS) { + @Override + public Dtm cast(final Item item, final QueryContext qc, final InputInfo info) + throws QueryException { + if(item.type.oneOf(DATE, DATE_TIME)) return new Dtm((ADate) item, DATE_TIME_STAMP, info); + if(isString(item)) return new Dtm(item.string(info), DATE_TIME_STAMP, info); + throw typeError(item, this, info); + } + @Override + public Dtm cast(final Object value, final QueryContext qc, final InputInfo info) + throws QueryException { + return cast(Str.get(value, qc, info), qc, info); + } + @Override + public Dtm read(final DataInput in, final QueryContext qc) throws IOException, QueryException { + return new Dtm(in.readToken(), DATE_TIME_STAMP, null); + } + }, /** Date type. */ DATE("date", ANY_ATOMIC_TYPE, XS_URI, false, false, false, true, Type.ID.DAT) { @Override public Dat cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type == DATE_TIME) return new Dat((ADate) item); + if(item.type.instanceOf(DATE_TIME)) return new Dat((ADate) item); if(isString(item)) return new Dat(item.string(info), info); throw typeError(item, this, info); } @@ -677,7 +694,7 @@ public Dat read(final DataInput in, final QueryContext qc) throws IOException, Q @Override public Tim cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type == DATE_TIME) return new Tim((ADate) item); + if(item.type.instanceOf(DATE_TIME)) return new Tim((ADate) item); if(isString(item)) return new Tim(item.string(info), info); throw typeError(item, this, info); } @@ -697,7 +714,7 @@ public Tim read(final DataInput in, final QueryContext qc) throws IOException, Q @Override public GDt cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type.oneOf(DATE_TIME, DATE)) return new GDt((ADate) item, this); + if(item.type.oneOf(DATE_TIME_STAMP, DATE_TIME, DATE)) return new GDt((ADate) item, this); if(isString(item)) return new GDt(item.string(info), this, info); throw typeError(item, this, info); } @@ -717,7 +734,7 @@ public GDt read(final DataInput in, final QueryContext qc) throws IOException, Q @Override public GDt cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type.oneOf(DATE_TIME, DATE)) return new GDt((ADate) item, this); + if(item.type.oneOf(DATE_TIME_STAMP, DATE_TIME, DATE)) return new GDt((ADate) item, this); if(isString(item)) return new GDt(item.string(info), this, info); throw typeError(item, this, info); } @@ -737,7 +754,7 @@ public GDt read(final DataInput in, final QueryContext qc) throws IOException, Q @Override public GDt cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type.oneOf(DATE_TIME, DATE)) return new GDt((ADate) item, this); + if(item.type.oneOf(DATE_TIME_STAMP, DATE_TIME, DATE)) return new GDt((ADate) item, this); if(isString(item)) return new GDt(item.string(info), this, info); throw typeError(item, this, info); } @@ -757,7 +774,7 @@ public GDt read(final DataInput in, final QueryContext qc) throws IOException, Q @Override public GDt cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type.oneOf(DATE_TIME, DATE)) return new GDt((ADate) item, this); + if(item.type.oneOf(DATE_TIME_STAMP, DATE_TIME, DATE)) return new GDt((ADate) item, this); if(isString(item)) return new GDt(item.string(info), this, info); throw typeError(item, this, info); } @@ -777,7 +794,7 @@ public GDt read(final DataInput in, final QueryContext qc) throws IOException, Q @Override public GDt cast(final Item item, final QueryContext qc, final InputInfo info) throws QueryException { - if(item.type.oneOf(DATE_TIME, DATE)) return new GDt((ADate) item, this); + if(item.type.oneOf(DATE_TIME_STAMP, DATE_TIME, DATE)) return new GDt((ADate) item, this); if(isString(item)) return new GDt(item.string(info), this, info); throw typeError(item, this, info); } diff --git a/basex-core/src/main/java/org/basex/query/value/type/Types.java b/basex-core/src/main/java/org/basex/query/value/type/Types.java index b0e1417aed..b51d23dcf0 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/Types.java +++ b/basex-core/src/main/java/org/basex/query/value/type/Types.java @@ -112,6 +112,8 @@ public final class Types { public static final SeqType DATE_TIME_O = DATE_TIME.seqType(); /** Zero or one date-time. */ public static final SeqType DATE_TIME_ZO = DATE_TIME.seqType(ZERO_OR_ONE); + /** One date-time-stamp. */ + public static final SeqType DATE_TIME_STAMP_O = DATE_TIME_STAMP.seqType(); /** One time. */ public static final SeqType TIME_O = TIME.seqType(); /** Zero or one time. */ diff --git a/basex-core/src/test/java/org/basex/query/SeqTypeTest.java b/basex-core/src/test/java/org/basex/query/SeqTypeTest.java index cf5e67ee01..327a2168fe 100644 --- a/basex-core/src/test/java/org/basex/query/SeqTypeTest.java +++ b/basex-core/src/test/java/org/basex/query/SeqTypeTest.java @@ -138,6 +138,8 @@ private static void compute(final Occ[][] table, final BiFunction assertTrue(r1.eq(r2)); assertTrue(r2.eq(r1)); + assertFalse(DATE_TIME_O.eq(DATE_TIME_STAMP_O)); + assertFalse(DATE_TIME_STAMP_O.eq(DATE_TIME_O)); } /** @@ -150,6 +152,8 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(ANY_ATOMIC_TYPE_ZM.instanceOf(BOOLEAN_O)); assertTrue(DOUBLE_O.instanceOf(DOUBLE_ZM)); assertFalse(DOUBLE_ZM.instanceOf(DOUBLE_O)); + assertFalse(DATE_TIME_O.instanceOf(DATE_TIME_STAMP_O)); + assertTrue(DATE_TIME_STAMP_O.instanceOf(DATE_TIME_O)); // functions final SeqType f = FuncType.get(DECIMAL_ZO, BOOLEAN_O).seqType(); @@ -337,12 +341,16 @@ private static void compute(final Occ[][] table, final BiFunction combine(ATTRIBUTE_O, op); combine(ELEMENT_O, op); combine(NODE_O, op); + combine(DATE_TIME_O, op); + combine(DATE_TIME_STAMP_O, op); combine(STRING_O, INTEGER_O, ANY_ATOMIC_TYPE_O, op); combine(STRING_O, STRING_O, STRING_O, op); combine(STRING_O, ATTRIBUTE_O, ITEM_O, op); combine(NORMALIZED_STRING.seqType(), STRING_O, STRING_O, op); combine(STRING_O, NORMALIZED_STRING.seqType(), STRING_O, op); + combine(DATE_TIME_STAMP_O, DATE_TIME_O, DATE_TIME_O, op); + combine(DATE_TIME_O, DATE_TIME_STAMP_O, DATE_TIME_O, op); combine(ATTRIBUTE_O, ELEMENT_O, NODE_O, op); combine(NODE_O, ELEMENT_O, NODE_O, op); @@ -578,6 +586,11 @@ private static void compute(final Occ[][] table, final BiFunction combine(ATTRIBUTE_O, op); combine(ELEMENT_O, op); combine(NODE_O, op); + combine(DATE_TIME_O, op); + combine(DATE_TIME_STAMP_O, op); + + combine(DATE_TIME_O, DATE_TIME_STAMP_O, DATE_TIME_STAMP_O, op); + combine(DATE_TIME_STAMP_O, DATE_TIME_O, DATE_TIME_STAMP_O, op); combine(EMPTY_SEQUENCE_Z, ITEM_O, null, op); combine(ATTRIBUTE_O, ATTRIBUTE_O, ATTRIBUTE_O, op); @@ -829,6 +842,8 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(NODE_O.mayBeNumber()); assertFalse(ELEMENT_O.mayBeNumber()); assertFalse(NMTOKENS_O.mayBeNumber()); + assertFalse(DATE_TIME_O.mayBeNumber()); + assertFalse(DATE_TIME_STAMP_O.mayBeNumber()); assertTrue(ITEM_O.mayBeArray()); assertTrue(FUNCTION_O.mayBeArray()); @@ -843,6 +858,8 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(NODE_O.mayBeArray()); assertFalse(ELEMENT_O.mayBeArray()); assertFalse(NMTOKENS_O.mayBeArray()); + assertFalse(DATE_TIME_O.mayBeArray()); + assertFalse(DATE_TIME_STAMP_O.mayBeArray()); assertTrue(ITEM_O.mayBeFunction()); assertTrue(FUNCTION_O.mayBeFunction()); @@ -857,6 +874,8 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(NODE_O.mayBeFunction()); assertFalse(ELEMENT_O.mayBeFunction()); assertFalse(NMTOKENS_O.mayBeFunction()); + assertFalse(DATE_TIME_O.mayBeFunction()); + assertFalse(DATE_TIME_STAMP_O.mayBeFunction()); } /** diff --git a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java index 30fdf9871c..847c8e0b5e 100644 --- a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java +++ b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java @@ -46,6 +46,8 @@ public final class TypeTest extends QueryTest { + "fn() as record(a? as xs:integer)"}, { "Type 13", booleans(false), "fn() as record(a? as xs:integer) { {} } instance of " + "fn() as record(a as xs:integer)"}, + { "Type 14", strings("xs:dateTimeStamp"), "type-of(current-dateTime())" }, + { "Type 15", strings("xs:dateTime"), "type-of(current-dateTime() cast as xs:dateTime)" }, { "TypeErr 1", "1 instance of xs:abcde" }, { "TypeErr 2", "1 instance of xs:string()" }, From 0d3d704421f6c8c56e24eefd9b6b981a453c1fbc Mon Sep 17 00:00:00 2001 From: Gunther Rademacher Date: Tue, 25 Nov 2025 18:17:36 +0100 Subject: [PATCH 2/3] add support for type xs:error --- .../org/basex/query/value/type/AtomType.java | 7 +++++++ .../java/org/basex/query/value/type/Type.java | 1 + .../java/org/basex/query/SeqTypeTest.java | 9 +++++++++ .../java/org/basex/query/ast/TCOTest.java | 19 +++++++++++++++++++ .../java/org/basex/query/simple/TypeTest.java | 8 ++++++++ 5 files changed, 44 insertions(+) diff --git a/basex-core/src/main/java/org/basex/query/value/type/AtomType.java b/basex-core/src/main/java/org/basex/query/value/type/AtomType.java index 01fb09799d..2ab5814293 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/AtomType.java +++ b/basex-core/src/main/java/org/basex/query/value/type/AtomType.java @@ -42,6 +42,9 @@ public enum AtomType implements Type { /** Any atomic type. */ ANY_ATOMIC_TYPE("anyAtomicType", ITEM, XS_URI, false, false, false, false, Type.ID.AAT), + /** Error type. */ + ERROR("error", ANY_ATOMIC_TYPE, XS_URI, false, false, false, false, Type.ID.ERR), + /** Untyped Atomic type. */ UNTYPED_ATOMIC("untypedAtomic", ANY_ATOMIC_TYPE, XS_URI, false, true, false, true, Type.ID.ATM) { @Override @@ -1051,6 +1054,8 @@ public final boolean instanceOf(final Type type) { @Override public final Type union(final Type type) { + if(this == ERROR) return type; + if(type == ERROR) return this; if(type instanceof ChoiceItemType || type instanceof EnumType) return type.union(this); if(instanceOf(type)) return type; if(type.instanceOf(this)) return this; @@ -1066,6 +1071,8 @@ public final Type union(final Type type) { @Override public final Type intersect(final Type type) { + if(this == ERROR) return type; + if(type == ERROR) return this; return type instanceof ChoiceItemType ? type.intersect(this) : instanceOf(type) ? this : type.instanceOf(this) ? type : null; } diff --git a/basex-core/src/main/java/org/basex/query/value/type/Type.java b/basex-core/src/main/java/org/basex/query/value/type/Type.java index 4082f99b87..5d5e034a8f 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/Type.java +++ b/basex-core/src/main/java/org/basex/query/value/type/Type.java @@ -45,6 +45,7 @@ enum ID { /** xs:untyped. */ UTY(33), /** xs:anyType. */ ATY(34), /** xs:anySimpleType. */ AST(35), + /** xs:error. */ ERR(0), /** xs:anyAtomicType. */ AAT(36), /** xs:untypedAtomic. */ ATM(37), /** xs:string. */ STR(38), diff --git a/basex-core/src/test/java/org/basex/query/SeqTypeTest.java b/basex-core/src/test/java/org/basex/query/SeqTypeTest.java index 327a2168fe..a84d6b4406 100644 --- a/basex-core/src/test/java/org/basex/query/SeqTypeTest.java +++ b/basex-core/src/test/java/org/basex/query/SeqTypeTest.java @@ -24,6 +24,8 @@ public final class SeqTypeTest { /** Occurrences. */ private static final Occ[] OCCS = { ZERO, ZERO_OR_ONE, EXACTLY_ONE, ZERO_OR_MORE, ONE_OR_MORE }; + /** Error type. */ + private static final SeqType ERROR_O = ERROR.seqType(); /** Tests for {@link Occ#intersect(Occ)}. */ @Test public void occIntersect() { @@ -154,6 +156,7 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(DOUBLE_ZM.instanceOf(DOUBLE_O)); assertFalse(DATE_TIME_O.instanceOf(DATE_TIME_STAMP_O)); assertTrue(DATE_TIME_STAMP_O.instanceOf(DATE_TIME_O)); + assertTrue(ERROR_O.instanceOf(ANY_ATOMIC_TYPE_O)); // functions final SeqType f = FuncType.get(DECIMAL_ZO, BOOLEAN_O).seqType(); @@ -343,6 +346,7 @@ private static void compute(final Occ[][] table, final BiFunction combine(NODE_O, op); combine(DATE_TIME_O, op); combine(DATE_TIME_STAMP_O, op); + combine(ERROR_O, op); combine(STRING_O, INTEGER_O, ANY_ATOMIC_TYPE_O, op); combine(STRING_O, STRING_O, STRING_O, op); @@ -351,6 +355,7 @@ private static void compute(final Occ[][] table, final BiFunction combine(STRING_O, NORMALIZED_STRING.seqType(), STRING_O, op); combine(DATE_TIME_STAMP_O, DATE_TIME_O, DATE_TIME_O, op); combine(DATE_TIME_O, DATE_TIME_STAMP_O, DATE_TIME_O, op); + combine(ERROR_O, POSITIVE_INTEGER_O, POSITIVE_INTEGER_O, op); combine(ATTRIBUTE_O, ELEMENT_O, NODE_O, op); combine(NODE_O, ELEMENT_O, NODE_O, op); @@ -588,6 +593,7 @@ private static void compute(final Occ[][] table, final BiFunction combine(NODE_O, op); combine(DATE_TIME_O, op); combine(DATE_TIME_STAMP_O, op); + combine(ERROR_O, ITEM_O, ITEM_O, op); combine(DATE_TIME_O, DATE_TIME_STAMP_O, DATE_TIME_STAMP_O, op); combine(DATE_TIME_STAMP_O, DATE_TIME_O, DATE_TIME_STAMP_O, op); @@ -844,6 +850,7 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(NMTOKENS_O.mayBeNumber()); assertFalse(DATE_TIME_O.mayBeNumber()); assertFalse(DATE_TIME_STAMP_O.mayBeNumber()); + assertFalse(ERROR_O.mayBeNumber()); assertTrue(ITEM_O.mayBeArray()); assertTrue(FUNCTION_O.mayBeArray()); @@ -860,6 +867,7 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(NMTOKENS_O.mayBeArray()); assertFalse(DATE_TIME_O.mayBeArray()); assertFalse(DATE_TIME_STAMP_O.mayBeArray()); + assertFalse(ERROR_O.mayBeArray()); assertTrue(ITEM_O.mayBeFunction()); assertTrue(FUNCTION_O.mayBeFunction()); @@ -876,6 +884,7 @@ private static void compute(final Occ[][] table, final BiFunction assertFalse(NMTOKENS_O.mayBeFunction()); assertFalse(DATE_TIME_O.mayBeFunction()); assertFalse(DATE_TIME_STAMP_O.mayBeFunction()); + assertFalse(ERROR_O.mayBeFunction()); } /** diff --git a/basex-core/src/test/java/org/basex/query/ast/TCOTest.java b/basex-core/src/test/java/org/basex/query/ast/TCOTest.java index eed8a7aa9b..4f9f72be94 100644 --- a/basex-core/src/test/java/org/basex/query/ast/TCOTest.java +++ b/basex-core/src/test/java/org/basex/query/ast/TCOTest.java @@ -143,4 +143,23 @@ public final class TCOTest extends SandboxTest { 42); } + + /** Checks if static type xs:error affects tail call optimization. */ + @Test public void typeCheckOnFnError() { + check("declare function local:sum($result, $values) as xs:integer\n" + + "{\n" + + " if (empty($values)) then\n" + + " $result\n" + + " else\n" + + " let $head := head($values)\n" + + " return\n" + + " if ($head instance of xs:integer) then\n" + + " local:sum($result + $head, tail($values))\n" + + " else\n" + + " fn:error() cast as xs:error\n" + + "};\n" + + "local:sum(0, 1 to 100000)", 5000050000L, + exists(Util.className(StaticFuncCall.class) + "[@tailCall eq 'true']") + ); + } } diff --git a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java index 847c8e0b5e..a01b3046fd 100644 --- a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java +++ b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java @@ -17,6 +17,10 @@ public final class TypeTest extends QueryTest { { "Simple 4", strings("42"), "(42 cast as enum('42')) cast as enum('42', '43')" }, { "Simple 5", booleans(true), "(42 cast as enum('42')) castable as enum('42', '43')" }, { "Simple 6", booleans(false), "(42 cast as enum('42')) castable as enum('43')" }, + { "Simple 7", booleans(true), "() castable as xs:error?" }, + { "Simple 8", booleans(false), "42 castable as xs:error?" }, + { "Simple 9", emptySequence(), "() cast as xs:error?" }, + { "Simple 10", emptySequence(), "xs:error(())" }, { "SimpleErr 1", "1 castable as xs:integer+" }, { "SimpleErr 2", "1 castable as xs:integer()" }, @@ -25,6 +29,8 @@ public final class TypeTest extends QueryTest { { "SimpleErr 5", "1 castable as xs:NOTATION" }, { "SimpleErr 6", "1 castable as xs:anyAtomicType" }, { "SimpleErr 7", "(42 cast as enum('42')) cast as enum('x')" }, + { "SimpleErr 8", "42 cast as xs:error?" }, + { "SimpleErr 9", "xs:error(42)" }, { "Type 1", booleans(true), "1 instance of item()" }, { "Type 2", booleans(true), "1 instance of xs:anyAtomicType" }, @@ -48,6 +54,8 @@ public final class TypeTest extends QueryTest { + "fn() as record(a as xs:integer)"}, { "Type 14", strings("xs:dateTimeStamp"), "type-of(current-dateTime())" }, { "Type 15", strings("xs:dateTime"), "type-of(current-dateTime() cast as xs:dateTime)" }, + { "Type 16", booleans(false), "42 instance of xs:error" }, + { "Type 16", booleans(true), "() instance of xs:error?" }, { "TypeErr 1", "1 instance of xs:abcde" }, { "TypeErr 2", "1 instance of xs:string()" }, From c96260886cddb74d0ba259a7081f65e49d7bd478 Mon Sep 17 00:00:00 2001 From: Gunther Rademacher Date: Wed, 26 Nov 2025 12:45:20 +0100 Subject: [PATCH 3/3] remove xs:error from ChoiceItemType members --- .../basex/query/value/type/ChoiceItemType.java | 18 ++++++++++++------ .../java/org/basex/query/value/type/Types.java | 3 +++ .../java/org/basex/query/simple/TypeTest.java | 14 +++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/basex-core/src/main/java/org/basex/query/value/type/ChoiceItemType.java b/basex-core/src/main/java/org/basex/query/value/type/ChoiceItemType.java index 4e48cbbc38..34b4adfaff 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/ChoiceItemType.java +++ b/basex-core/src/main/java/org/basex/query/value/type/ChoiceItemType.java @@ -31,9 +31,15 @@ public final class ChoiceItemType implements Type { * @param types alternative item types */ public ChoiceItemType(final List types) { - this.types = types; + if(types.contains(Types.ERROR_O)) { + final ArrayList list = new ArrayList<>(types); + do { } while(list.remove(Types.ERROR_O)); + this.types = list.isEmpty() ? types : list; + } else { + this.types = types; + } Type tp = null; - for(final SeqType st : types) { + for(final SeqType st : this.types) { tp = tp == null ? st.type : tp.union(st.type); } union = tp; @@ -116,15 +122,15 @@ public Type union(final Type type) { @Override public Type intersect(final Type type) { - final ArrayList list = new ArrayList<>(); + final ArrayList list = new ArrayList<>(); for(final SeqType st : types) { final Type tp = type.intersect(st.type); - if(tp != null) list.add(tp); + if(tp != null) list.add(tp.seqType()); } return switch(list.size()) { case 0 -> null; - case 1 -> list.get(0); - default -> new ChoiceItemType(types); + case 1 -> list.get(0).type; + default -> new ChoiceItemType(list); }; } diff --git a/basex-core/src/main/java/org/basex/query/value/type/Types.java b/basex-core/src/main/java/org/basex/query/value/type/Types.java index b51d23dcf0..7516270572 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/Types.java +++ b/basex-core/src/main/java/org/basex/query/value/type/Types.java @@ -35,6 +35,9 @@ public final class Types { /** Zero or more xs:anyAtomicType. */ public static final SeqType ANY_ATOMIC_TYPE_ZM = ANY_ATOMIC_TYPE.seqType(ZERO_OR_MORE); + /** Error. */ + public static final SeqType ERROR_O = ERROR.seqType(); + /** Numeric. */ public static final SeqType NUMERIC_O = NUMERIC.seqType(); /** Zero or one numeric. */ diff --git a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java index a01b3046fd..86e2950843 100644 --- a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java +++ b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java @@ -64,13 +64,21 @@ public final class TypeTest extends QueryTest { { "Subtyping 1", booleans(true), "declare namespace p1='p1'; declare namespace p2='p2'; " + "declare variable $x external := ''; function() as element(p1:a)? { $x } " + "instance of function() as element(p1:*)?" }, - { "Subtyping 1", booleans(false), "declare namespace p1='p1'; declare namespace p2='p2'; " + { "Subtyping 2", booleans(false), "declare namespace p1='p1'; declare namespace p2='p2'; " + "declare variable $x external := ''; function() as element(p1:*|p2:*)? { $x } " + "instance of function() as element(p1:*)?" }, - { "Subtyping 1", booleans(true), "declare variable $x external := ''; function() as " + { "Subtyping 3", booleans(true), "declare variable $x external := ''; function() as " + "element(a|b)? { $x } instance of function() as element(*:a|*:b)?" }, - { "Subtyping 1", booleans(false), "declare variable $x external := ''; function() as " + { "Subtyping 4", booleans(false), "declare variable $x external := ''; function() as " + "element(a|b|c)? { $x } instance of function() as (element(a)|element(b))?" }, + { "Subtyping 5", booleans(true), + "fn($x as xs:int) as xs:int {'x'} instance of fn((xs:error | xs:int)) as xs:int"}, + { "Subtyping 6", booleans(true), + "fn($x as (xs:error | xs:int)) as xs:int {'x'} instance of fn(xs:int) as xs:int"}, + { "Subtyping 7", booleans(true), + "fn($x as xs:int) as xs:int {'x'} instance of fn(xs:int) as (xs:error | xs:int)"}, + { "Subtyping 8", booleans(true), + "fn($x as xs:int) as (xs:error | xs:int) {'x'} instance of fn(xs:int) as xs:int"}, }; } }