Skip to content

Commit

Permalink
[CALCITE-6449] Enable PostgreSQL implementations of to_date/to_timestamp
Browse files Browse the repository at this point in the history
* Supports all date/time format patterns of PostreSQL 14
* Oracle will use the existing implementations
* Added to_date/to_timestamp tests to PostgreSQL iq file
* PostgreSQL to_timestamp function returns TIMESTAMP_TZ that is nullable
  • Loading branch information
normanj-bitquill authored and mihaibudiu committed Jul 12, 2024
1 parent 8581dba commit 374091b
Show file tree
Hide file tree
Showing 22 changed files with 3,299 additions and 113 deletions.
710 changes: 710 additions & 0 deletions babel/src/test/resources/sql/postgresql.iq

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,10 @@
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_CHAR_PG;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_CODE_POINTS;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_DATE;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_DATE_PG;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_HEX;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_TIMESTAMP;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TO_TIMESTAMP_PG;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRANSLATE3;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRUNC_BIG_QUERY;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.TRY_CAST;
Expand Down Expand Up @@ -801,7 +803,9 @@ Builder populate2() {
defineReflective(TO_CHAR, BuiltInMethod.TO_CHAR.method);
defineReflective(TO_CHAR_PG, BuiltInMethod.TO_CHAR_PG.method);
defineReflective(TO_DATE, BuiltInMethod.TO_DATE.method);
defineReflective(TO_DATE_PG, BuiltInMethod.TO_DATE_PG.method);
defineReflective(TO_TIMESTAMP, BuiltInMethod.TO_TIMESTAMP.method);
defineReflective(TO_TIMESTAMP_PG, BuiltInMethod.TO_TIMESTAMP_PG.method);
final FormatDatetimeImplementor datetimeFormatImpl =
new FormatDatetimeImplementor();
map.put(FORMAT_DATE, datetimeFormatImpl);
Expand Down
43 changes: 43 additions & 0 deletions core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -4033,6 +4033,20 @@ 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 @@ -4090,11 +4104,40 @@ public int toDate(String dateString, String fmtString) {
new java.sql.Date(internalToDateTime(dateString, fmtString)));
}

public int toDatePg(String dateString, String fmtString) {
try {
return (int) PostgresqlDateTimeFormatter.toTimestamp(dateString, fmtString,
LOCAL_ZONE)
.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 long toTimestampPg(String timestampString, String fmtString) {
try {
return PostgresqlDateTimeFormatter.toTimestamp(timestampString, fmtString, LOCAL_ZONE)
.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
Original file line number Diff line number Diff line change
Expand Up @@ -1726,22 +1726,40 @@ private static RelDataType deriveTypeMapFromEntries(SqlOperatorBinding opBinding

/** The "TO_DATE(string1, string2)" function; casts string1
* to a DATE using the format specified in string2. */
@LibraryOperator(libraries = {ORACLE, POSTGRESQL})
@LibraryOperator(libraries = {ORACLE, REDSHIFT})
public static final SqlFunction TO_DATE =
SqlBasicFunction.create("TO_DATE",
ReturnTypes.DATE_NULLABLE,
OperandTypes.STRING_STRING,
SqlFunctionCategory.TIMEDATE);

/** The "TO_DATE(string1, string2)" function for PostgreSQL; casts string1
* to a DATE using the format specified in string2. */
@LibraryOperator(libraries = {POSTGRESQL}, exceptLibraries = {REDSHIFT})
public static final SqlFunction TO_DATE_PG =
new SqlBasicFunction("TO_DATE", SqlKind.OTHER_FUNCTION,
SqlSyntax.FUNCTION, true, ReturnTypes.DATE_NULLABLE, null,
OperandHandlers.DEFAULT, OperandTypes.STRING_STRING, 0,
SqlFunctionCategory.TIMEDATE, call -> SqlMonotonicity.NOT_MONOTONIC, false) { };

/** The "TO_TIMESTAMP(string1, string2)" function; casts string1
* to a TIMESTAMP using the format specified in string2. */
@LibraryOperator(libraries = {ORACLE, POSTGRESQL})
@LibraryOperator(libraries = {ORACLE, REDSHIFT})
public static final SqlFunction TO_TIMESTAMP =
SqlBasicFunction.create("TO_TIMESTAMP",
ReturnTypes.TIMESTAMP_NULLABLE,
OperandTypes.STRING_STRING,
SqlFunctionCategory.TIMEDATE);

/** The "TO_TIMESTAMP(string1, string2)" function for PostgreSQL; casts string1
* to a TIMESTAMP using the format specified in string2. */
@LibraryOperator(libraries = {POSTGRESQL}, exceptLibraries = {REDSHIFT})
public static final SqlFunction TO_TIMESTAMP_PG =
new SqlBasicFunction("TO_TIMESTAMP", SqlKind.OTHER_FUNCTION,
SqlSyntax.FUNCTION, true, ReturnTypes.TIMESTAMP_TZ_NULLABLE, null,
OperandHandlers.DEFAULT, OperandTypes.STRING_STRING, 0,
SqlFunctionCategory.TIMEDATE, call -> SqlMonotonicity.NOT_MONOTONIC, false) { };

/**
* The "PARSE_TIME(string, string)" function (BigQuery);
* converts a string representation of time to a TIME value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,13 @@ public static SqlCall stripSeparator(SqlCall call) {
public static final SqlReturnTypeInference TIMESTAMP_LTZ_NULLABLE =
TIMESTAMP_LTZ.andThen(SqlTypeTransforms.TO_NULLABLE);

/**
* Type-inference strategy whereby the result type of a call is nullable
* TIMESTAMP WITH TIME ZONE.
*/
public static final SqlReturnTypeInference TIMESTAMP_TZ_NULLABLE =
TIMESTAMP_TZ.andThen(SqlTypeTransforms.TO_NULLABLE);

/**
* Type-inference strategy whereby the result type of a call is Double.
*/
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,12 @@ public enum BuiltInMethod {
String.class),
TO_DATE(SqlFunctions.DateFormatFunction.class, "toDate", String.class,
String.class),
TO_DATE_PG(SqlFunctions.DateFormatFunction.class, "toDatePg", String.class,
String.class),
TO_TIMESTAMP(SqlFunctions.DateFormatFunction.class, "toTimestamp", String.class,
String.class),
TO_TIMESTAMP_PG(SqlFunctions.DateFormatFunction.class, "toTimestampPg", String.class,
String.class),
FORMAT_DATE(SqlFunctions.DateFormatFunction.class, "formatDate",
String.class, int.class),
FORMAT_TIME(SqlFunctions.DateFormatFunction.class, "formatTime",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.calcite.util.format.postgresql;

import com.google.common.collect.ImmutableSet;

import java.time.temporal.ChronoUnit;
import java.util.Set;

import static org.apache.calcite.util.format.postgresql.DateCalendarEnum.GREGORIAN;
import static org.apache.calcite.util.format.postgresql.DateCalendarEnum.ISO_8601;
import static org.apache.calcite.util.format.postgresql.DateCalendarEnum.JULIAN;
import static org.apache.calcite.util.format.postgresql.DateCalendarEnum.NONE;

/**
* A component of a datetime. May belong to a type of calendar. Also contains
* a list of parent values. For example months are in a year. A datetime can be
* reconstructed from one or more <code>ChronUnitEnum</code> items along with
* their associated values.
*
* <p>Some <code>ChronoUnitEnum</code> items conflict with others.
*/
public enum ChronoUnitEnum {
ERAS(ChronoUnit.ERAS, NONE),
CENTURIES(
ChronoUnit.CENTURIES,
ImmutableSet.of(ISO_8601, GREGORIAN),
ERAS),
YEARS_ISO_8601(
ChronoUnit.YEARS,
ISO_8601,
CENTURIES),
YEARS_IN_MILLENIA_ISO_8601(
ChronoUnit.YEARS,
ISO_8601,
CENTURIES),
YEARS_IN_CENTURY_ISO_8601(
ChronoUnit.YEARS,
ISO_8601,
CENTURIES),
DAYS_IN_YEAR_ISO_8601(
ChronoUnit.DAYS,
ISO_8601,
YEARS_ISO_8601),
WEEKS_IN_YEAR_ISO_8601(
ChronoUnit.WEEKS,
ISO_8601,
YEARS_ISO_8601),
DAYS_JULIAN(
ChronoUnit.DAYS,
JULIAN),
YEARS(
ChronoUnit.YEARS,
GREGORIAN,
CENTURIES),
YEARS_IN_MILLENIA(
ChronoUnit.YEARS,
GREGORIAN,
CENTURIES),
YEARS_IN_CENTURY(
ChronoUnit.YEARS,
GREGORIAN,
CENTURIES),
MONTHS_IN_YEAR(
ChronoUnit.MONTHS,
GREGORIAN,
YEARS, YEARS_IN_CENTURY),
DAYS_IN_YEAR(
ChronoUnit.DAYS,
GREGORIAN,
YEARS, YEARS_IN_CENTURY),
DAYS_IN_MONTH(
ChronoUnit.DAYS,
GREGORIAN,
MONTHS_IN_YEAR),
WEEKS_IN_YEAR(
ChronoUnit.WEEKS,
GREGORIAN,
YEARS, YEARS_IN_CENTURY),
WEEKS_IN_MONTH(
ChronoUnit.WEEKS,
GREGORIAN,
MONTHS_IN_YEAR),
DAYS_IN_WEEK(
ChronoUnit.DAYS,
NONE,
YEARS_ISO_8601, WEEKS_IN_MONTH, WEEKS_IN_YEAR),
HOURS_IN_DAY(
ChronoUnit.HOURS,
NONE,
DAYS_IN_YEAR, DAYS_IN_MONTH, DAYS_IN_WEEK),
HALF_DAYS(
ChronoUnit.HALF_DAYS,
NONE,
DAYS_IN_YEAR, DAYS_IN_MONTH, DAYS_IN_WEEK),
HOURS_IN_HALF_DAY(
ChronoUnit.HOURS,
NONE,
HALF_DAYS),
MINUTES_IN_HOUR(
ChronoUnit.MINUTES,
NONE,
HOURS_IN_DAY, HOURS_IN_HALF_DAY),
SECONDS_IN_DAY(
ChronoUnit.SECONDS,
NONE,
DAYS_IN_YEAR, DAYS_IN_MONTH, DAYS_IN_WEEK),
SECONDS_IN_MINUTE(
ChronoUnit.SECONDS,
NONE,
MINUTES_IN_HOUR),
MILLIS(
ChronoUnit.MILLIS,
NONE,
SECONDS_IN_DAY, SECONDS_IN_MINUTE),
MICROS(
ChronoUnit.MICROS,
NONE,
MILLIS),
TENTHS_OF_SECOND(
ChronoUnit.MILLIS,
NONE,
SECONDS_IN_DAY, SECONDS_IN_MINUTE),
HUNDREDTHS_OF_SECOND(
ChronoUnit.MILLIS,
NONE,
SECONDS_IN_DAY, SECONDS_IN_MINUTE),
THOUSANDTHS_OF_SECOND(
ChronoUnit.MILLIS,
NONE,
SECONDS_IN_DAY, SECONDS_IN_MINUTE),
TENTHS_OF_MS(
ChronoUnit.MICROS,
NONE,
SECONDS_IN_DAY, SECONDS_IN_MINUTE),
HUNDREDTHS_OF_MS(
ChronoUnit.MICROS,
NONE,
SECONDS_IN_DAY, SECONDS_IN_MINUTE),
THOUSANDTHS_OF_MS(
ChronoUnit.MICROS,
NONE,
SECONDS_IN_DAY, SECONDS_IN_MINUTE),
TIMEZONE_HOURS(
ChronoUnit.HOURS,
NONE),
TIMEZONE_MINUTES(
ChronoUnit.MINUTES,
NONE,
TIMEZONE_HOURS);

private final ChronoUnit chronoUnit;
private final ImmutableSet<ChronoUnitEnum> parentUnits;
private final ImmutableSet<DateCalendarEnum> calendars;

ChronoUnitEnum(ChronoUnit chronoUnit, DateCalendarEnum calendar,
ChronoUnitEnum... parentUnits) {
this.chronoUnit = chronoUnit;
this.parentUnits = ImmutableSet.copyOf(parentUnits);
this.calendars = ImmutableSet.of(calendar);
}

ChronoUnitEnum(ChronoUnit chronoUnit, Set<DateCalendarEnum> calendars,
ChronoUnitEnum... parentUnits) {
this.chronoUnit = chronoUnit;
this.parentUnits = ImmutableSet.copyOf(parentUnits);
this.calendars = ImmutableSet.<DateCalendarEnum>builder().addAll(calendars).build();
}

/**
* Get the ChronoUnit value that corresponds to this value.
*
* @return a ChronoUnit value
*/
public ChronoUnit getChronoUnit() {
return chronoUnit;
}

/**
* Get the set of calendars that this value can be in.
*
* @return set of calendars that this value can be in
*/
public Set<DateCalendarEnum> getCalendars() {
return calendars;
}

/**
* Checks if the current item can be added to <code>units</code> without causing
* any conflicts.
*
* @param units a <code>Set</code> of items to test against
* @return <code>true</code> if this item does not conflict with <code>units</code>
*/
public boolean isCompatible(Set<ChronoUnitEnum> units) {
if (!calendars.isEmpty()) {
for (ChronoUnitEnum unit : units) {
boolean haveCompatibleCalendar = false;

for (DateCalendarEnum unitCalendar : unit.getCalendars()) {
for (DateCalendarEnum calendar : calendars) {
if (unitCalendar == NONE || calendar == NONE
|| unitCalendar.isCalendarCompatible(calendar)) {
haveCompatibleCalendar = true;
break;
}
}

if (haveCompatibleCalendar) {
break;
}
}

if (!haveCompatibleCalendar) {
return false;
}
}
}

for (ChronoUnitEnum unit : units) {
if (parentUnits.equals(unit.parentUnits)) {
return false;
}
}

return true;
}
}
Loading

0 comments on commit 374091b

Please sign in to comment.