Skip to content

Commit

Permalink
Merge pull request #3761 from katzyn/datetime
Browse files Browse the repository at this point in the history
LAST_DAY function and other changes
  • Loading branch information
katzyn committed Mar 20, 2023
2 parents 9ef778d + 646fa79 commit d8af652
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 59 deletions.
4 changes: 4 additions & 0 deletions h2/src/docsrc/html/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ <h1>Change Log</h1>

<h2>Next Version (unreleased)</h2>
<ul>
<li>PR #3761: LAST_DAY function and other changes
</li>
<li>Issue #3705: Oracle DATE type: milliseconds (second fractions) rounded in H2 but truncated in Oracle (fixed in SYSDATE only)
</li>
<li>Issue #3642: AssertionError in mvstore.FileStore.serializeAndStore
</li>
<li>Issue #3675: H2 2.x cannot read PostgreSQL-style sequence generator start with option without WITH keyword
Expand Down
2 changes: 2 additions & 0 deletions h2/src/main/org/h2/command/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
74 changes: 58 additions & 16 deletions h2/src/main/org/h2/expression/function/DateTimeFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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(')');
}
Expand Down
16 changes: 9 additions & 7 deletions h2/src/main/org/h2/mode/CompatibilityDateTimeValueFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion h2/src/main/org/h2/res/help.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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)
","
Expand Down
15 changes: 15 additions & 0 deletions h2/src/main/org/h2/value/ValueTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
*/
Expand All @@ -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));
}

Expand Down
20 changes: 18 additions & 2 deletions h2/src/main/org/h2/value/ValueTinyint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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
Expand Down
33 changes: 1 addition & 32 deletions h2/src/test/org/h2/test/db/TestFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ public void test() throws Exception {
testCompatibilityDateTime();
testAnnotationProcessorsOutput();
testSignal();
testLegacyDateTime();

deleteDb("functions");
}
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion h2/src/test/org/h2/test/scripts/TestScript.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
17 changes: 17 additions & 0 deletions h2/src/test/org/h2/test/scripts/functions/timeanddate/last_day.sql
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit d8af652

Please sign in to comment.