Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LAST_DAY function and other changes #3761

Merged
merged 5 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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