diff --git a/h2/src/docsrc/html/changelog.html b/h2/src/docsrc/html/changelog.html
index 8b07340f6a..abfc6232c1 100644
--- a/h2/src/docsrc/html/changelog.html
+++ b/h2/src/docsrc/html/changelog.html
@@ -21,6 +21,10 @@
Change Log
Next Version (unreleased)
+- PR #3761: LAST_DAY function and other changes
+
+- Issue #3705: Oracle DATE type: milliseconds (second fractions) rounded in H2 but truncated in Oracle (fixed in SYSDATE only)
+
- Issue #3642: AssertionError in mvstore.FileStore.serializeAndStore
- Issue #3675: H2 2.x cannot read PostgreSQL-style sequence generator start with option without WITH keyword
diff --git a/h2/src/main/org/h2/command/Parser.java b/h2/src/main/org/h2/command/Parser.java
index 0a8a0de595..5e926b78fb 100644
--- a/h2/src/main/org/h2/command/Parser.java
+++ b/h2/src/main/org/h2/command/Parser.java
@@ -4221,6 +4221,8 @@ private Expression readBuiltinFunctionIf(String upperName) {
case "TIMESTAMPDIFF":
return new DateTimeFunction(DateTimeFunction.DATEDIFF, readDateTimeField(), readNextArgument(),
readLastArgument());
+ case "LAST_DAY":
+ return new DateTimeFunction(DateTimeFunction.LAST_DAY, -1, readSingleArgument(), null);
case "FORMATDATETIME":
return readDateTimeFormatFunction(DateTimeFormatFunction.FORMATDATETIME);
case "PARSEDATETIME":
diff --git a/h2/src/main/org/h2/expression/function/DateTimeFunction.java b/h2/src/main/org/h2/expression/function/DateTimeFunction.java
index 34912d8498..e013aeec2e 100644
--- a/h2/src/main/org/h2/expression/function/DateTimeFunction.java
+++ b/h2/src/main/org/h2/expression/function/DateTimeFunction.java
@@ -65,8 +65,13 @@ public final class DateTimeFunction extends Function1_2 {
*/
public static final int DATEDIFF = DATEADD + 1;
+ /**
+ * LAST_DAY() (non-standard);
+ */
+ public static final int LAST_DAY = DATEDIFF + 1;
+
private static final String[] NAMES = { //
- "EXTRACT", "DATE_TRUNC", "DATEADD", "DATEDIFF" //
+ "EXTRACT", "DATE_TRUNC", "DATEADD", "DATEDIFF", "LAST_DAY" //
};
// Standard fields
@@ -359,6 +364,9 @@ public Value getValue(SessionLocal session, Value v1, Value v2) {
case DATEDIFF:
v1 = ValueBigint.get(datediff(session, field, v1, v2));
break;
+ case LAST_DAY:
+ v1 = lastDay(session, v1);
+ break;
default:
throw DbException.getInternalError("function=" + function);
}
@@ -957,6 +965,32 @@ private static ValueNumeric extractEpoch(SessionLocal session, Value value) {
return result;
}
+ private static Value lastDay(SessionLocal session, Value v) {
+ long dateValue;
+ int valueType = v.getValueType();
+ switch (valueType) {
+ case Value.DATE:
+ dateValue = ((ValueDate) v).getDateValue();
+ break;
+ case Value.TIMESTAMP:
+ dateValue = ((ValueTimestamp) v).getDateValue();
+ break;
+ case Value.TIMESTAMP_TZ:
+ dateValue = ((ValueTimestampTimeZone) v).getDateValue();
+ break;
+ default:
+ dateValue = ((ValueTimestampTimeZone) DateTimeUtils.parseTimestamp(v.getString(), session, true))
+ .getDateValue();
+ }
+ int year = DateTimeUtils.yearFromDateValue(dateValue), month = DateTimeUtils.monthFromDateValue(dateValue);
+ int day = DateTimeUtils.getDaysInMonth(year, month);
+ long lastDay = DateTimeUtils.dateValue(year, month, day);
+ if (lastDay == dateValue && valueType == Value.DATE) {
+ return v;
+ }
+ return ValueDate.fromDateValue(lastDay);
+ }
+
@Override
public Expression optimize(SessionLocal session) {
left = left.optimize(session);
@@ -1000,6 +1034,9 @@ public Expression optimize(SessionLocal session) {
case DATEDIFF:
type = TypeInfo.TYPE_BIGINT;
break;
+ case LAST_DAY:
+ type = TypeInfo.TYPE_DATE;
+ break;
default:
throw DbException.getInternalError("function=" + function);
}
@@ -1011,21 +1048,26 @@ public Expression optimize(SessionLocal session) {
@Override
public StringBuilder getUnenclosedSQL(StringBuilder builder, int sqlFlags) {
- builder.append(getName()).append('(').append(getFieldName(field));
- switch (function) {
- case EXTRACT:
- left.getUnenclosedSQL(builder.append(" FROM "), sqlFlags);
- break;
- case DATE_TRUNC:
- left.getUnenclosedSQL(builder.append(", "), sqlFlags);
- break;
- case DATEADD:
- case DATEDIFF:
- left.getUnenclosedSQL(builder.append(", "), sqlFlags).append(", ");
- right.getUnenclosedSQL(builder, sqlFlags);
- break;
- default:
- throw DbException.getInternalError("function=" + function);
+ builder.append(getName()).append('(');
+ if (function == LAST_DAY) {
+ left.getUnenclosedSQL(builder, sqlFlags);
+ } else {
+ builder.append(getFieldName(field));
+ switch (function) {
+ case EXTRACT:
+ left.getUnenclosedSQL(builder.append(" FROM "), sqlFlags);
+ break;
+ case DATE_TRUNC:
+ left.getUnenclosedSQL(builder.append(", "), sqlFlags);
+ break;
+ case DATEADD:
+ case DATEDIFF:
+ left.getUnenclosedSQL(builder.append(", "), sqlFlags).append(", ");
+ right.getUnenclosedSQL(builder, sqlFlags);
+ break;
+ default:
+ throw DbException.getInternalError("function=" + function);
+ }
}
return builder.append(')');
}
diff --git a/h2/src/main/org/h2/mode/CompatibilityDateTimeValueFunction.java b/h2/src/main/org/h2/mode/CompatibilityDateTimeValueFunction.java
index ee5ba29c73..f6e9fc7c29 100644
--- a/h2/src/main/org/h2/mode/CompatibilityDateTimeValueFunction.java
+++ b/h2/src/main/org/h2/mode/CompatibilityDateTimeValueFunction.java
@@ -31,8 +31,6 @@ final class CompatibilityDateTimeValueFunction extends Operation0 implements Nam
*/
static final int SYSTIMESTAMP = 1;
- private static final int[] TYPES = { Value.TIMESTAMP, Value.TIMESTAMP_TZ };
-
private static final String[] NAMES = { "SYSDATE", "SYSTIMESTAMP" };
private final int function, scale;
@@ -42,10 +40,11 @@ final class CompatibilityDateTimeValueFunction extends Operation0 implements Nam
CompatibilityDateTimeValueFunction(int function, int scale) {
this.function = function;
this.scale = scale;
- if (scale < 0) {
- scale = function == SYSDATE ? 0 : ValueTimestamp.DEFAULT_SCALE;
+ if (function == SYSDATE) {
+ type = TypeInfo.getTypeInfo(Value.TIMESTAMP, 0L, 0, null);
+ } else {
+ type = TypeInfo.getTypeInfo(Value.TIMESTAMP_TZ, 0L, scale, null);
}
- type = TypeInfo.getTypeInfo(TYPES[function], 0L, scale, null);
}
@Override
@@ -59,8 +58,11 @@ public Value getValue(SessionLocal session) {
if (offsetSeconds != newOffset) {
v = DateTimeUtils.timestampTimeZoneAtOffset(dateValue, timeNanos, offsetSeconds, newOffset);
}
- return (function == SYSDATE ? ValueTimestamp.fromDateValueAndNanos(v.getDateValue(), v.getTimeNanos()) : v)
- .castTo(type, session);
+ if (function == SYSDATE) {
+ return ValueTimestamp.fromDateValueAndNanos(v.getDateValue(),
+ v.getTimeNanos() / DateTimeUtils.NANOS_PER_SECOND * DateTimeUtils.NANOS_PER_SECOND);
+ }
+ return v.castTo(type, session);
}
@Override
diff --git a/h2/src/main/org/h2/res/help.csv b/h2/src/main/org/h2/res/help.csv
index 263f762dca..096fca74df 100644
--- a/h2/src/main/org/h2/res/help.csv
+++ b/h2/src/main/org/h2/res/help.csv
@@ -5865,13 +5865,22 @@ DATEDIFF(YEAR, T1.CREATED, T2.CREATED)
"
"Functions (Time and Date)","DATE_TRUNC","
-@h2@ DATE_TRUNC (datetimeField, dateAndTime)
+@h2@ DATE_TRUNC(datetimeField, dateAndTime)
","
Truncates the specified date-time value to the specified field.
","
DATE_TRUNC(DAY, TIMESTAMP '2010-01-03 10:40:00');
"
+"Functions (Time and Date)","LAST_DAY","
+@h2@ LAST_DAY(date | timestamp | timestampWithTimeZone | string)
+","
+Returns the last day of the month that contains the specified date-time value.
+This function returns a date.
+","
+LAST_DAY(DAY, DATE '2020-02-05');
+"
+
"Functions (Time and Date)","DAYNAME","
@h2@ DAYNAME(dateAndTime)
","
diff --git a/h2/src/main/org/h2/value/ValueTime.java b/h2/src/main/org/h2/value/ValueTime.java
index cf29e9b60e..12296a88c1 100644
--- a/h2/src/main/org/h2/value/ValueTime.java
+++ b/h2/src/main/org/h2/value/ValueTime.java
@@ -10,6 +10,8 @@
import org.h2.message.DbException;
import org.h2.util.DateTimeUtils;
+import static org.h2.util.DateTimeUtils.NANOS_PER_HOUR;
+
/**
* Implementation of the TIME data type.
*/
@@ -37,11 +39,21 @@ public final class ValueTime extends Value {
*/
public static final int MAXIMUM_SCALE = 9;
+ private static final ValueTime[] STATIC_CACHE;
+
/**
* Nanoseconds since midnight
*/
private final long nanos;
+ static {
+ ValueTime[] cache = new ValueTime[24];
+ for (int hour = 0; hour < 24; hour++) {
+ cache[hour] = new ValueTime(hour * NANOS_PER_HOUR);
+ }
+ STATIC_CACHE = cache;
+ }
+
/**
* @param nanos nanoseconds since midnight
*/
@@ -60,6 +72,9 @@ public static ValueTime fromNanos(long nanos) {
throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2, "TIME",
DateTimeUtils.appendTime(new StringBuilder(), nanos).toString());
}
+ if (nanos % NANOS_PER_HOUR == 0L) {
+ return STATIC_CACHE[(int) (nanos / NANOS_PER_HOUR)];
+ }
return (ValueTime) Value.cache(new ValueTime(nanos));
}
diff --git a/h2/src/main/org/h2/value/ValueTinyint.java b/h2/src/main/org/h2/value/ValueTinyint.java
index 21dff3718f..0bd0786b7c 100644
--- a/h2/src/main/org/h2/value/ValueTinyint.java
+++ b/h2/src/main/org/h2/value/ValueTinyint.java
@@ -32,8 +32,18 @@ public final class ValueTinyint extends Value {
*/
static final int DISPLAY_SIZE = 4;
+ private static final ValueTinyint[] STATIC_CACHE;
+
private final byte value;
+ static {
+ ValueTinyint[] cache = new ValueTinyint[256];
+ for (int i = 0; i < 256; i++) {
+ cache[i] = new ValueTinyint((byte) (i - 128));
+ }
+ STATIC_CACHE = cache;
+ }
+
private ValueTinyint(byte value) {
this.value = value;
}
@@ -110,6 +120,12 @@ public int getValueType() {
return TINYINT;
}
+ @Override
+ public int getMemory() {
+ // All possible values are statically initialized
+ return 0;
+ }
+
@Override
public byte[] getBytes() {
return new byte[] { value };
@@ -166,13 +182,13 @@ public int hashCode() {
}
/**
- * Get or create a TINYINT value for the given byte.
+ * Get a TINYINT value for the given byte.
*
* @param i the byte
* @return the value
*/
public static ValueTinyint get(byte i) {
- return (ValueTinyint) Value.cache(new ValueTinyint(i));
+ return STATIC_CACHE[i + 128];
}
@Override
diff --git a/h2/src/test/org/h2/test/db/TestFunctions.java b/h2/src/test/org/h2/test/db/TestFunctions.java
index c263bf5daa..31a1298f5e 100644
--- a/h2/src/test/org/h2/test/db/TestFunctions.java
+++ b/h2/src/test/org/h2/test/db/TestFunctions.java
@@ -142,7 +142,6 @@ public void test() throws Exception {
testCompatibilityDateTime();
testAnnotationProcessorsOutput();
testSignal();
- testLegacyDateTime();
deleteDb("functions");
}
@@ -1927,36 +1926,6 @@ private void testSignal() throws SQLException {
conn.close();
}
- private void testLegacyDateTime() throws SQLException {
- deleteDb("functions");
- TimeZone tz = TimeZone.getDefault();
- try {
- TimeZone.setDefault(TimeZone.getTimeZone("GMT+1"));
- Connection conn = getConnection("functions;MODE=LEGACY");
- conn.setAutoCommit(false);
- Statement stat = conn.createStatement();
- ResultSet rs = stat.executeQuery("SELECT SYSDATE, SYSTIMESTAMP, SYSTIMESTAMP(0), SYSTIMESTAMP(9)");
- rs.next();
- LocalDateTime ldt = rs.getObject(1, LocalDateTime.class);
- OffsetDateTime odt = rs.getObject(2, OffsetDateTime.class);
- OffsetDateTime odt0 = rs.getObject(3, OffsetDateTime.class);
- OffsetDateTime odt9 = rs.getObject(4, OffsetDateTime.class);
- assertEquals(3_600, odt.getOffset().getTotalSeconds());
- assertEquals(3_600, odt9.getOffset().getTotalSeconds());
- assertEquals(ldt, odt0.toLocalDateTime());
- stat.execute("SET TIME ZONE '2:00'");
- rs = stat.executeQuery("SELECT SYSDATE, SYSTIMESTAMP, SYSTIMESTAMP(0), SYSTIMESTAMP(9)");
- rs.next();
- assertEquals(ldt, rs.getObject(1, LocalDateTime.class));
- assertEquals(odt, rs.getObject(2, OffsetDateTime.class));
- assertEquals(odt0, rs.getObject(3, OffsetDateTime.class));
- assertEquals(odt9, rs.getObject(4, OffsetDateTime.class));
- conn.close();
- } finally {
- TimeZone.setDefault(tz);
- }
- }
-
private void testThatCurrentTimestampIsSane() throws SQLException,
ParseException {
deleteDb("functions");
@@ -2057,7 +2026,7 @@ private void testCompatibilityDateTime() throws SQLException {
OffsetDateTime odt9 = rs.getObject(4, OffsetDateTime.class);
assertEquals(3_600, odt.getOffset().getTotalSeconds());
assertEquals(3_600, odt9.getOffset().getTotalSeconds());
- assertEquals(ldt, odt0.toLocalDateTime());
+ assertEquals(ldt, odt9.toLocalDateTime().withNano(0));
if (mode.equals("LEGACY")) {
stat.execute("SET TIME ZONE '3:00'");
rs = stat.executeQuery("SELECT SYSDATE, SYSTIMESTAMP, SYSTIMESTAMP(0), SYSTIMESTAMP(9) FROM DUAL");
diff --git a/h2/src/test/org/h2/test/scripts/TestScript.java b/h2/src/test/org/h2/test/scripts/TestScript.java
index d13749fca5..29974b17ca 100644
--- a/h2/src/test/org/h2/test/scripts/TestScript.java
+++ b/h2/src/test/org/h2/test/scripts/TestScript.java
@@ -213,7 +213,7 @@ public void test() throws Exception {
for (String s : new String[] { "current_date", "current_timestamp",
"current-time", "dateadd", "datediff", "dayname",
"day-of-month", "day-of-week", "day-of-year", "extract",
- "formatdatetime", "hour", "minute", "month", "monthname",
+ "formatdatetime", "hour", "last_day", "minute", "month", "monthname",
"parsedatetime", "quarter", "second", "truncate", "week", "year", "date_trunc" }) {
testScript("functions/timeanddate/" + s + ".sql");
}
diff --git a/h2/src/test/org/h2/test/scripts/functions/timeanddate/last_day.sql b/h2/src/test/org/h2/test/scripts/functions/timeanddate/last_day.sql
new file mode 100644
index 0000000000..81680a2208
--- /dev/null
+++ b/h2/src/test/org/h2/test/scripts/functions/timeanddate/last_day.sql
@@ -0,0 +1,17 @@
+-- Copyright 2004-2023 H2 Group. Multiple-Licensed under the MPL 2.0,
+-- and the EPL 1.0 (https://h2database.com/html/license.html).
+-- Initial Developer: H2 Group
+--
+
+SELECT N, LAST_DAY(A), LAST_DAY(B), LAST_DAY(C), LAST_DAY(D)
+FROM (VALUES
+(1, DATE '2023-02-04', TIMESTAMP '2020-12-01 15:00:00', TIMESTAMP WITH TIME ZONE '1999-05-18 03:00:00+10', '2010-05-07'),
+(2, DATE '2020-02-29', TIMESTAMP '2020-02-28 23:00:00', TIMESTAMP WITH TIME ZONE '2000-02-01 05:00:00-12', '2015-04-01 12:00:00'),
+(3, DATE '2000-02-01', TIMESTAMP '2000-11-28 15:00:00', TIMESTAMP WITH TIME ZONE '2000-03-01 05:00:00+12', '2015-06-09 11:30:56+01')
+) T(N, A, B, C, D);
+> N LAST_DAY(A) LAST_DAY(B) LAST_DAY(C) LAST_DAY(D)
+> - ----------- ----------- ----------- -----------
+> 1 2023-02-28 2020-12-31 1999-05-31 2010-05-31
+> 2 2020-02-29 2020-02-29 2000-02-29 2015-04-30
+> 3 2000-02-29 2000-11-30 2000-03-31 2015-06-30
+> rows: 3