Skip to content

Commit

Permalink
[CALCITE-6585] In the Postgres TO_CHAR function, improve caching
Browse files Browse the repository at this point in the history
* Refactored code to separate parsing a date/time format and applying it
* A parsed date/time format can now be reused
* Fixed up some minor issues with when to space pad day or month names
* Removed usage of ZoneOffset.get()
* Reworked to no longer use static methods for TO_CHAR and TO_TIMESTAMP
* ToCharPgImplementor and ToTimestampPgImplementor now use a a new instance of DateFormatFunctionPg
* Moved the PostgreSQL date/time formatting and parsing methods to their own class
* Changed how the ZoneId of UTC is obtained
* Added some tests with format reuse

Close #3978
  • Loading branch information
normanj-bitquill authored and julianhyde committed Sep 30, 2024
1 parent a4a27e3 commit 60e0a3f
Show file tree
Hide file tree
Showing 42 changed files with 2,610 additions and 1,822 deletions.
12 changes: 6 additions & 6 deletions babel/src/test/resources/sql/postgresql.iq
Original file line number Diff line number Diff line change
Expand Up @@ -263,17 +263,17 @@ EXPR$0
a.d.
!ok

select to_char(timestamp '2022-06-03 12:15:48.678', 'MONTH');
select to_char(timestamp '2022-06-03 12:15:48.678', 'FMMONTH');
EXPR$0
JUNE
!ok

select to_char(timestamp '2022-06-03 12:15:48.678', 'Month');
select to_char(timestamp '2022-06-03 12:15:48.678', 'FMMonth');
EXPR$0
June
!ok

select to_char(timestamp '2022-06-03 12:15:48.678', 'month');
select to_char(timestamp '2022-06-03 12:15:48.678', 'FMmonth');
EXPR$0
june
!ok
Expand All @@ -293,17 +293,17 @@ EXPR$0
jun
!ok

select to_char(timestamp '2022-06-03 12:15:48.678', 'DAY');
select to_char(timestamp '2022-06-03 12:15:48.678', 'FMDAY');
EXPR$0
FRIDAY
!ok

select to_char(timestamp '2022-06-03 12:15:48.678', 'Day');
select to_char(timestamp '2022-06-03 12:15:48.678', 'FMDay');
EXPR$0
Friday
!ok

select to_char(timestamp '2022-06-03 12:15:48.678', 'day');
select to_char(timestamp '2022-06-03 12:15:48.678', 'FMday');
EXPR$0
friday
!ok
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4849,10 +4849,12 @@ private static class ToCharPgImplementor extends AbstractRexCallImplementor {

@Override Expression implementSafe(RexToLixTranslator translator, RexCall call,
List<Expression> argValueList) {
final Expression target =
Expressions.new_(BuiltInMethod.TO_CHAR_PG.method.getDeclaringClass(),
new ParameterExpression(0, DataContext.class, "root"));
final Expression operand0 = argValueList.get(0);
final Expression operand1 = argValueList.get(1);
return Expressions.call(BuiltInMethod.TO_CHAR_PG.method, translator.getRoot(),
operand0, operand1);
return Expressions.call(target, BuiltInMethod.TO_CHAR_PG.method, operand0, operand1);
}
}

Expand All @@ -4867,9 +4869,12 @@ private static class ToTimestampPgImplementor extends AbstractRexCallImplementor

@Override Expression implementSafe(RexToLixTranslator translator, RexCall call,
List<Expression> argValueList) {
final Expression target =
Expressions.new_(method.getDeclaringClass(),
new ParameterExpression(0, DataContext.class, "root"));
final Expression operand0 = argValueList.get(0);
final Expression operand1 = argValueList.get(1);
return Expressions.call(method, translator.getRoot(), operand0, operand1);
return Expressions.call(target, method, operand0, operand1);
}
}
}
117 changes: 62 additions & 55 deletions core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.apache.calcite.util.format.FormatElement;
import org.apache.calcite.util.format.FormatModel;
import org.apache.calcite.util.format.FormatModels;
import org.apache.calcite.util.format.postgresql.CompiledDateTimeFormat;
import org.apache.calcite.util.format.postgresql.PostgresqlDateTimeFormatter;

import org.apache.commons.codec.DecoderException;
Expand Down Expand Up @@ -4343,20 +4344,6 @@ public static long unixDateExtract(String rangeString, long date) {
* {@code FORMAT_DATETIME}, {@code FORMAT_TIME}, {@code TO_CHAR} functions. */
@Deterministic
public static class DateFormatFunction {
// Timezone to use for PostgreSQL parsing of timestamps
private static final ZoneId LOCAL_ZONE;
static {
ZoneId zoneId;
try {
// Currently the parsed timestamps are expected to be the number of
// milliseconds since the epoch in UTC, with no timezone information
zoneId = ZoneId.of("UTC");
} catch (Exception e) {
zoneId = ZoneId.systemDefault();
}
LOCAL_ZONE = zoneId;
}

/** Work space for various functions. Clear it before you use it. */
final StringBuilder sb = new StringBuilder();

Expand Down Expand Up @@ -4402,57 +4389,16 @@ public String toChar(long timestamp, String pattern) {
return sb.toString().trim();
}

public static String toCharPg(DataContext root, long timestamp, String pattern) {
final ZoneId zoneId = DataContext.Variable.TIME_ZONE.<TimeZone>get(root).toZoneId();
final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(root));
final Timestamp sqlTimestamp = internalToTimestamp(timestamp);
final ZonedDateTime zonedDateTime =
ZonedDateTime.of(sqlTimestamp.toLocalDateTime(), zoneId);
return PostgresqlDateTimeFormatter.toChar(pattern, zonedDateTime, locale).trim();
}

public int toDate(String dateString, String fmtString) {
return toInt(
new java.sql.Date(internalToDateTime(dateString, fmtString)));
}

public static int toDatePg(DataContext root, String dateString, String fmtString) {
try {
final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(root));
return (int) PostgresqlDateTimeFormatter.toTimestamp(dateString, fmtString, LOCAL_ZONE,
locale)
.getLong(ChronoField.EPOCH_DAY);
} catch (Exception e) {
SQLException sqlEx =
new SQLException(
String.format(Locale.ROOT,
"Invalid format: '%s' for datetime string: '%s'.", fmtString,
dateString));
throw Util.toUnchecked(sqlEx);
}
}

public long toTimestamp(String timestampString, String fmtString) {
return toLong(
new java.sql.Timestamp(internalToDateTime(timestampString, fmtString)));
}

public static long toTimestampPg(DataContext root, String timestampString, String fmtString) {
try {
final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(root));
return PostgresqlDateTimeFormatter.toTimestamp(timestampString, fmtString, LOCAL_ZONE,
locale)
.toInstant().toEpochMilli();
} catch (Exception e) {
SQLException sqlEx =
new SQLException(
String.format(Locale.ROOT,
"Invalid format: '%s' for timestamp string: '%s'.", fmtString,
timestampString));
throw Util.toUnchecked(sqlEx);
}
}

private long internalToDateTime(String dateString, String fmtString) {
final ParsePosition pos = new ParsePosition(0);

Expand Down Expand Up @@ -4486,6 +4432,67 @@ public String formatTime(String fmtString, int time) {
}
}

/** State for {@code FORMAT_DATE}, {@code FORMAT_TIMESTAMP},
* {@code FORMAT_DATETIME}, {@code FORMAT_TIME}, {@code TO_CHAR} functions. */
@Deterministic
public static class DateFormatFunctionPg {
// Timezone to use for PostgreSQL parsing of timestamps
private static final ZoneId LOCAL_ZONE = ZoneId.ofOffset("", ZoneOffset.UTC);

private final DataContext dataContext;
private final LoadingCache<String, CompiledDateTimeFormat> formatCachePg =
CacheBuilder.newBuilder()
.maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
.build(CacheLoader.from(PostgresqlDateTimeFormatter::compilePattern));

public DateFormatFunctionPg(DataContext dataContext) {
this.dataContext = dataContext;
}

public String toChar(long timestamp, String pattern) {
final ZoneId zoneId = DataContext.Variable.TIME_ZONE.<TimeZone>get(dataContext).toZoneId();
final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(dataContext));
final CompiledDateTimeFormat dateTimeFormat = formatCachePg.getUnchecked(pattern);
final Timestamp sqlTimestamp = internalToTimestamp(timestamp);
final ZonedDateTime zonedDateTime =
ZonedDateTime.of(sqlTimestamp.toLocalDateTime(), zoneId);
return dateTimeFormat.formatDateTime(zonedDateTime, locale);
}

public int toDate(String dateString, String fmtString) {
try {
final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(dataContext));
final CompiledDateTimeFormat dateTimeFormat = formatCachePg.getUnchecked(fmtString);
return (int) dateTimeFormat.parseDateTime(dateString, LOCAL_ZONE, locale).getLong(
ChronoField.EPOCH_DAY);
} catch (Exception e) {
SQLException sqlEx =
new SQLException(
String.format(Locale.ROOT,
"Invalid format: '%s' for datetime string: '%s'.", fmtString,
dateString));
throw Util.toUnchecked(sqlEx);
}
}

public long toTimestamp(String timestampString, String fmtString) {
try {
final Locale locale = requireNonNull(DataContext.Variable.LOCALE.get(dataContext));
final CompiledDateTimeFormat dateTimeFormat = formatCachePg.getUnchecked(fmtString);
return dateTimeFormat.parseDateTime(timestampString, LOCAL_ZONE, locale)
.toInstant()
.toEpochMilli();
} catch (Exception e) {
SQLException sqlEx =
new SQLException(
String.format(Locale.ROOT,
"Invalid format: '%s' for timestamp string: '%s'.", fmtString,
timestampString));
throw Util.toUnchecked(sqlEx);
}
}
}

/** State for {@code PARSE_DATE}, {@code PARSE_TIMESTAMP},
* {@code PARSE_DATETIME}, {@code PARSE_TIME} functions. */
@Deterministic
Expand Down
12 changes: 6 additions & 6 deletions core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -709,16 +709,16 @@ public enum BuiltInMethod {
String.class, long.class),
TO_CHAR(SqlFunctions.DateFormatFunction.class, "toChar", long.class,
String.class),
TO_CHAR_PG(SqlFunctions.DateFormatFunction.class, "toCharPg",
DataContext.class, long.class, String.class),
TO_CHAR_PG(SqlFunctions.DateFormatFunctionPg.class, "toChar", long.class,
String.class),
TO_DATE(SqlFunctions.DateFormatFunction.class, "toDate", String.class,
String.class),
TO_DATE_PG(SqlFunctions.DateFormatFunction.class, "toDatePg",
DataContext.class, String.class, String.class),
TO_DATE_PG(SqlFunctions.DateFormatFunctionPg.class, "toDate", String.class,
String.class),
TO_TIMESTAMP(SqlFunctions.DateFormatFunction.class, "toTimestamp", String.class,
String.class),
TO_TIMESTAMP_PG(SqlFunctions.DateFormatFunction.class, "toTimestampPg",
DataContext.class, String.class, String.class),
TO_TIMESTAMP_PG(SqlFunctions.DateFormatFunctionPg.class, "toTimestamp",
String.class, String.class),
FORMAT_DATE(SqlFunctions.DateFormatFunction.class, "formatDate",
String.class, int.class),
FORMAT_TIME(SqlFunctions.DateFormatFunction.class, "formatTime",
Expand Down
Loading

0 comments on commit 60e0a3f

Please sign in to comment.