Skip to content

Commit

Permalink
Merge pull request #3791 from katzyn/datetime
Browse files Browse the repository at this point in the history
Formatted cast of datetimes to/from character strings
  • Loading branch information
katzyn committed May 1, 2023
2 parents b89eb3a + 7a0a97f commit b55aa66
Show file tree
Hide file tree
Showing 26 changed files with 1,596 additions and 82 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 #3791: Formatted cast of datetimes to/from character strings
</li>
<li>Issue #3785: CSVRead: Fails to translate empty Numbers, when cells are quoted
</li>
<li>Issue #3762: Comparison predicates with row values don't create index conditions
</li>
<li>PR #3761: LAST_DAY function and other changes
Expand Down
2 changes: 1 addition & 1 deletion h2/src/main/org/h2/api/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,7 @@ public class ErrorCode {

/**
* The error with code <code>90056</code> is thrown when trying to format a
* timestamp using TO_DATE and TO_TIMESTAMP with an invalid format.
* timestamp using TO_DATE and TO_TIMESTAMP with an invalid format.
*/
public static final int INVALID_TO_DATE_FORMAT = 90056;

Expand Down
9 changes: 5 additions & 4 deletions h2/src/main/org/h2/command/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -5164,8 +5164,9 @@ private Expression readTermWithoutIdentifier() {
Expression arg = readExpression();
read(AS);
Column column = parseColumnWithType(null);
Expression template = readIf("FORMAT") ? readExpression() : null;
read(CLOSE_PAREN);
r = new CastSpecification(arg, column);
r = new CastSpecification(arg, column, template);
break;
}
case CURRENT_CATALOG:
Expand Down Expand Up @@ -5415,13 +5416,13 @@ && equalsToken("E", name)) {
}
String time = token.value(session).getString();
read();
return ValueExpression.get(ValueTimeTimeZone.parse(time));
return ValueExpression.get(ValueTimeTimeZone.parse(time, session));
} else {
boolean without = readIf("WITHOUT", "TIME", "ZONE");
if (currentTokenType == LITERAL && token.value(session).getValueType() == Value.VARCHAR) {
String time = token.value(session).getString();
read();
return ValueExpression.get(ValueTime.parse(time));
return ValueExpression.get(ValueTime.parse(time, session));
} else if (without) {
throw getSyntaxError();
}
Expand All @@ -5448,7 +5449,7 @@ && equalsToken("E", name)) {
if (equalsToken("T", name)) {
String time = token.value(session).getString();
read();
return ValueExpression.get(ValueTime.parse(time));
return ValueExpression.get(ValueTime.parse(time, session));
} else if (equalsToken("TS", name)) {
String timestamp = token.value(session).getString();
read();
Expand Down
2 changes: 1 addition & 1 deletion h2/src/main/org/h2/command/dml/Set.java
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ private static TimeZoneProvider parseTimeZone(Value v) {
TimeZoneProvider timeZone;
try {
timeZone = TimeZoneProvider.ofId(v.getString());
} catch (IllegalArgumentException ex) {
} catch (RuntimeException ex) {
throw DbException.getInvalidValueException("TIME ZONE", v.getTraceSQL());
}
return timeZone;
Expand Down
68 changes: 55 additions & 13 deletions h2/src/main/org/h2/expression/function/CastSpecification.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,81 @@
import org.h2.expression.Expression;
import org.h2.expression.TypedValueExpression;
import org.h2.expression.ValueExpression;
import org.h2.message.DbException;
import org.h2.schema.Domain;
import org.h2.table.Column;
import org.h2.util.DateTimeTemplate;
import org.h2.util.HasSQL;
import org.h2.value.DataType;
import org.h2.value.TypeInfo;
import org.h2.value.Value;
import org.h2.value.ValueNull;
import org.h2.value.ValueVarchar;

/**
* A cast specification.
*/
public final class CastSpecification extends Function1 {
public final class CastSpecification extends Function1_2 {

private Domain domain;

public CastSpecification(Expression arg, Column column, Expression template) {
super(arg, template);
type = column.getType();
domain = column.getDomain();
}

public CastSpecification(Expression arg, Column column) {
super(arg);
super(arg, null);
type = column.getType();
domain = column.getDomain();
}

public CastSpecification(Expression arg, TypeInfo type) {
super(arg);
super(arg, null);
this.type = type;
}

@Override
public Value getValue(SessionLocal session) {
Value v = arg.getValue(session).castTo(type, session);
protected Value getValue(SessionLocal session, Value v1, Value v2) {
if (v2 != null) {
v1 = getValueWithTemplate(v1, v2, session);
}
v1 = v1.castTo(type, session);
if (domain != null) {
domain.checkConstraints(session, v);
domain.checkConstraints(session, v1);
}
return v;
return v1;
}

private Value getValueWithTemplate(Value v, Value template, SessionLocal session) {
if (v == ValueNull.INSTANCE) {
return ValueNull.INSTANCE;
}
int valueType = v.getValueType();
if (DataType.isDateTimeType(valueType)) {
if (DataType.isCharacterStringType(type.getValueType())) {
return ValueVarchar.get(DateTimeTemplate.of(template.getString()).format(v), session);
}
} else if (DataType.isCharacterStringType(valueType)) {
if (DataType.isDateTimeType(type.getValueType())) {
return DateTimeTemplate.of(template.getString()).parse(v.getString(), type, session);
}
}
throw DbException.getUnsupportedException(
type.getSQL(v.getType().getSQL(new StringBuilder("CAST with template from "), HasSQL.TRACE_SQL_FLAGS)
.append(" to "), HasSQL.DEFAULT_SQL_FLAGS).toString());
}

@Override
public Expression optimize(SessionLocal session) {
arg = arg.optimize(session);
if (arg.isConstant()) {
left = left.optimize(session);
if (right != null) {
right = right.optimize(session);
}
if (left.isConstant() && (right == null || right.isConstant())) {
Value v = getValue(session);
if (v == ValueNull.INSTANCE || canOptimizeCast(arg.getType().getValueType(), type.getValueType())) {
if (v == ValueNull.INSTANCE || canOptimizeCast(left.getType().getValueType(), type.getValueType())) {
return TypedValueExpression.get(v, type);
}
}
Expand All @@ -56,7 +92,8 @@ public Expression optimize(SessionLocal session) {

@Override
public boolean isConstant() {
return arg instanceof ValueExpression && canOptimizeCast(arg.getType().getValueType(), type.getValueType());
return left instanceof ValueExpression && (right == null || right.isConstant())
&& canOptimizeCast(left.getType().getValueType(), type.getValueType());
}

private static boolean canOptimizeCast(int src, int dst) {
Expand Down Expand Up @@ -103,8 +140,13 @@ private static boolean canOptimizeCast(int src, int dst) {
@Override
public StringBuilder getUnenclosedSQL(StringBuilder builder, int sqlFlags) {
builder.append("CAST(");
arg.getUnenclosedSQL(builder, arg instanceof ValueExpression ? sqlFlags | NO_CASTS : sqlFlags).append(" AS ");
return (domain != null ? domain : type).getSQL(builder, sqlFlags).append(')');
left.getUnenclosedSQL(builder, left instanceof ValueExpression ? sqlFlags | NO_CASTS : sqlFlags) //
.append(" AS ");
(domain != null ? domain : type).getSQL(builder, sqlFlags);
if (right != null) {
right.getSQL(builder.append(" FORMAT "), sqlFlags);
}
return builder.append(')');
}

@Override
Expand Down
24 changes: 7 additions & 17 deletions h2/src/main/org/h2/expression/function/DateTimeFormatFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQueries;
import java.time.zone.ZoneRules;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Objects;

Expand All @@ -29,6 +29,7 @@
import org.h2.expression.TypedValueExpression;
import org.h2.message.DbException;
import org.h2.util.JSR310Utils;
import org.h2.util.SmallLRUCache;
import org.h2.value.TypeInfo;
import org.h2.value.Value;
import org.h2.value.ValueTime;
Expand Down Expand Up @@ -105,16 +106,7 @@ private static final class CacheValue {
"FORMATDATETIME", "PARSEDATETIME" //
};

private static final LinkedHashMap<CacheKey, CacheValue> CACHE = new LinkedHashMap<CacheKey, CacheValue>() {

private static final long serialVersionUID = 1L;

@Override
protected boolean removeEldestEntry(java.util.Map.Entry<CacheKey, CacheValue> eldest) {
return size() > 100;
}

};
private static final SmallLRUCache<CacheKey, CacheValue> CACHE = SmallLRUCache.newInstance(100);

private final int function;

Expand Down Expand Up @@ -288,12 +280,10 @@ private static CacheValue getDateFormat(String format, String locale, String tim
synchronized (CACHE) {
value = CACHE.get(key);
if (value == null) {
DateTimeFormatter df;
if (locale == null) {
df = DateTimeFormatter.ofPattern(format);
} else {
df = DateTimeFormatter.ofPattern(format, new Locale(locale));
}
DateTimeFormatter df = new DateTimeFormatterBuilder().parseCaseInsensitive()
.appendPattern(format)
.toFormatter(locale == null ? Locale.getDefault(Locale.Category.FORMAT)
: new Locale(locale));
ZoneId zoneId;
if (timeZone != null) {
zoneId = getZoneId(timeZone);
Expand Down
3 changes: 2 additions & 1 deletion h2/src/main/org/h2/mode/FunctionsMSSQLServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public final class FunctionsMSSQLServer extends ModeFunction {
FUNCTIONS.put("GETDATE", new FunctionInfo("GETDATE", GETDATE, 0, Value.TIMESTAMP, false, true));
FUNCTIONS.put("LEN", new FunctionInfo("LEN", LEN, 1, Value.INTEGER, true, true));
FUNCTIONS.put("NEWID", new FunctionInfo("NEWID", NEWID, 0, Value.UUID, true, false));
FUNCTIONS.put("NEWSEQUENTIALID", new FunctionInfo("NEWSEQUENTIALID", NEWSEQUENTIALID, 0, Value.UUID, true, false));
FUNCTIONS.put("NEWSEQUENTIALID",
new FunctionInfo("NEWSEQUENTIALID", NEWSEQUENTIALID, 0, Value.UUID, true, false));
FUNCTIONS.put("ISNULL", new FunctionInfo("ISNULL", ISNULL, 2, Value.NULL, false, true));
FUNCTIONS.put("SCOPE_IDENTITY",
new FunctionInfo("SCOPE_IDENTITY", SCOPE_IDENTITY, 0, Value.NUMERIC, true, false));
Expand Down
50 changes: 48 additions & 2 deletions h2/src/main/org/h2/res/help.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2864,17 +2864,59 @@ CASE WHEN A IS NULL THEN 'Null' ELSE 'Not null' END
"

"Other Grammar","Cast specification","
CAST(value AS dataTypeOrDomain)
CAST(value AS dataTypeOrDomain [ FORMAT templateString ])
","
Converts a value to another data type. The following conversion rules are used:
When converting a number to a boolean, 0 is false and every other value is true.
When converting a boolean to a number, false is 0 and true is 1.
When converting a number to a number of another type, the value is checked for overflow.
When converting a string to binary, UTF-8 encoding is used.
Note that some data types may need explicitly specified precision to avoid overflow or rounding.

Template may only be specified for casts from datetime data types to character string data types
and for casts from character string data types to datetime data types.

'-', '.', '/', ',', '''', ';', ':' and ' ' (space) characters can be used as delimiters.

Y, YY, YYY, YYYY represent last 1, 2, 3, or 4 digits of year.
YYYY, if delimited, can also be used to parse any year, including negative years.
When a year is parsed with Y, YY, or YYY pattern missing leading digits are filled using digits from the current year.

RR and RRRR have the same meaning as YY and YYYY for formatting.
When a year is parsed with RR, the resulting year is within current year - 49 years and current year + 50 years in H2,
other database systems may use different range of years.

MM represent a month.

DD represent a day of month.

DDD represent a day of year, if this pattern in specified, MM and DD may not be specified.

HH24 represent an hour (from 0 to 23).

HH and HH12 represent an hour (from 1 to 12), this pattern may only be used together with A.M. or P.M. pattern.
These patterns may not be used together with HH24.

MI represent minutes.

SS represent seconds of minute.

SSSSS represent seconds of day, this pattern may not be used together with HH24, HH (HH12), A.M. (P.M.), MI or SS pattern.

FF1, FF2, ..., FF9 represent fractional seconds.

TZH, TZM and TZH represent hours, minutes and seconds of time zone offset.

Multiple patterns for the same datetime field may not be specified.

If year is not specified, current year is used. If month is not specified, current month is used. If day is not specified, 1 is used.

If some fields of time or time zone are not specified, 0 is used.

","
CAST(NAME AS INT);
CAST(TIMESTAMP '2010-01-01 10:40:00.123456' AS TIME(6))
CAST(TIMESTAMP '2010-01-01 10:40:00.123456' AS TIME(6));
CAST('12:00:00 P.M.' AS TIME FORMAT 'HH:MI:SS A.M.');
"

"Other Grammar","Cipher","
Expand Down Expand Up @@ -5958,6 +6000,8 @@ If TIMESTAMP WITH TIME ZONE is passed and timeZoneString is specified,
the timestamp is converted to the specified time zone and its UTC value is preserved.

This method returns a string.

See also [cast specification](https://h2database.com/html/grammar.html#cast_specification).
","
CALL FORMATDATETIME(TIMESTAMP '2001-02-03 04:05:06',
'EEE, d MMM yyyy HH:mm:ss z', 'en', 'GMT')
Expand Down Expand Up @@ -6011,6 +6055,8 @@ y year, M month, d day, H hour, m minute, s second.
For details of the format, see ""java.time.format.DateTimeFormatter"".

If timeZoneString is specified, it is used as default.

See also [cast specification](https://h2database.com/html/grammar.html#cast_specification).
","
CALL PARSEDATETIME('Sat, 3 Feb 2001 03:05:06 GMT',
'EEE, d MMM yyyy HH:mm:ss z', 'en', 'GMT')
Expand Down
8 changes: 4 additions & 4 deletions h2/src/main/org/h2/tools/Csv.java
Original file line number Diff line number Diff line change
Expand Up @@ -555,11 +555,11 @@ public Object[] readRow() throws SQLException {
}
}
if (i < row.length) {
// Empty Strings should be NULL
// in order to prevent conversion of zero-length String
// Empty Strings should be NULL
// in order to prevent conversion of zero-length String
// to Number
row[i++] = v!=null && v.length() > 0
? v
row[i++] = v!=null && v.length() > 0
? v
: null;
}
if (endOfLine) {
Expand Down

0 comments on commit b55aa66

Please sign in to comment.