From 71fc04393fa40065d8058c67df4dc7dfcfa7ac7b Mon Sep 17 00:00:00 2001 From: Evgenij Ryazanov Date: Sun, 19 Mar 2023 18:03:24 +0800 Subject: [PATCH 1/5] Add static cache of ValueTime instances for each hour --- h2/src/main/org/h2/value/ValueTime.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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)); } From 8b0960c3bc08a3477392a2e616b3050a4d75f72a Mon Sep 17 00:00:00 2001 From: Evgenij Ryazanov Date: Sun, 19 Mar 2023 18:18:13 +0800 Subject: [PATCH 2/5] Add static cache of all TINYINT values --- h2/src/main/org/h2/value/ValueTinyint.java | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 From 66d57bdc9100d72019ec765c1e7d625ee0abb26d Mon Sep 17 00:00:00 2001 From: Evgenij Ryazanov Date: Sun, 19 Mar 2023 19:52:57 +0800 Subject: [PATCH 3/5] Round down fractional seconds in SYSDATE --- h2/src/docsrc/html/changelog.html | 2 ++ .../mode/CompatibilityDateTimeValueFunction.java | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/h2/src/docsrc/html/changelog.html b/h2/src/docsrc/html/changelog.html index 8b07340f6a..02dd773b08 100644 --- a/h2/src/docsrc/html/changelog.html +++ b/h2/src/docsrc/html/changelog.html @@ -21,6 +21,8 @@

Change Log

Next Version (unreleased)

    +
  • 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/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 From 3a0bff4034ec70750d96156be46f1349edb1e66e Mon Sep 17 00:00:00 2001 From: Evgenij Ryazanov Date: Sun, 19 Mar 2023 20:59:55 +0800 Subject: [PATCH 4/5] Add LAST_DAY function --- h2/src/main/org/h2/command/Parser.java | 2 + .../expression/function/DateTimeFunction.java | 74 +++++++++++++++---- h2/src/main/org/h2/res/help.csv | 11 ++- .../test/org/h2/test/scripts/TestScript.java | 2 +- .../functions/timeanddate/last_day.sql | 17 +++++ 5 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 h2/src/test/org/h2/test/scripts/functions/timeanddate/last_day.sql 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/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/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 From 646fa79f585283de7133406d2c49f76f4a80806f Mon Sep 17 00:00:00 2001 From: Evgenij Ryazanov Date: Sun, 19 Mar 2023 21:53:35 +0800 Subject: [PATCH 5/5] Adjust TestFunctions.testCompatibilityDateTime() and remove duplicate test --- h2/src/docsrc/html/changelog.html | 2 ++ h2/src/test/org/h2/test/db/TestFunctions.java | 33 +------------------ 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/h2/src/docsrc/html/changelog.html b/h2/src/docsrc/html/changelog.html index 02dd773b08..abfc6232c1 100644 --- a/h2/src/docsrc/html/changelog.html +++ b/h2/src/docsrc/html/changelog.html @@ -21,6 +21,8 @@

    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 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");