This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit 0ad974c308d74d2de4bb32f3cff66dc08a3bed59
Author: Greg Hart <[email protected]>
AuthorDate: Tue Nov 15 10:04:06 2022 -0800

    [CALCITE-5414] Use DateTimeUtils to correctly convert between java.sql 
types and Unix timestamps
    
    Close apache/calcite#2995
---
 .../adapter/jdbc/JdbcToEnumerableConverter.java    |   8 +-
 .../org/apache/calcite/runtime/SqlFunctions.java   | 270 +++++++++++++---
 .../org/apache/calcite/util/BuiltInMethod.java     |  12 +-
 .../java/org/apache/calcite/test/JdbcTest.java     |  84 +++++
 .../org/apache/calcite/test/SqlFunctionsTest.java  | 341 +++++++++++++++++++++
 .../java/org/apache/calcite/test/QuidemTest.java   |   1 +
 6 files changed, 668 insertions(+), 48 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
 
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
index 8f02d040ef..442bc564b5 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverter.java
@@ -308,8 +308,12 @@ public class JdbcToEnumerableConverter
     switch (sqlTypeName) {
     case DATE:
       return (nullable
-          ? BuiltInMethod.DATE_TO_INT_OPTIONAL
-          : BuiltInMethod.DATE_TO_INT).method;
+          ? (offset
+          ? BuiltInMethod.DATE_TO_INT_OPTIONAL_OFFSET
+          : BuiltInMethod.DATE_TO_INT_OPTIONAL)
+          : (offset
+              ? BuiltInMethod.DATE_TO_INT_OFFSET
+              : BuiltInMethod.DATE_TO_INT)).method;
     case TIME:
       return (nullable
           ? BuiltInMethod.TIME_TO_INT_OPTIONAL
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index 61d04f0051..9a7fa9b9b1 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -60,6 +60,8 @@ import java.sql.SQLException;
 import java.sql.Timestamp;
 import java.text.DecimalFormat;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -67,7 +69,6 @@ import java.util.Base64;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -93,11 +94,11 @@ import static java.util.Objects.requireNonNull;
  *
  * <p>Not present: and, or, not (builtin operators are better, because they
  * use lazy evaluation. Implementations do not check for null values; the
- * calling code must do that.</p>
+ * calling code must do that.
  *
  * <p>Many of the functions do not check for null values. This is intentional.
  * If null arguments are possible, the code-generation framework checks for
- * nulls before calling the functions.</p>
+ * nulls before calling the functions.
  */
 @SuppressWarnings("UnnecessaryUnboxing")
 @Deterministic
@@ -2086,41 +2087,96 @@ public class SqlFunctions {
         : (Short) cannotConvert(o, short.class);
   }
 
-  /** Converts the Java type used for UDF parameters of SQL DATE type
-   * ({@link java.sql.Date}) to internal representation (int).
+  /**
+   * Converts a SQL DATE value from the Java type
+   * ({@link java.sql.Date}) to the internal representation type
+   * (number of days since January 1st, 1970 as {@code int})
+   * in the local time zone.
+   *
+   * <p>Since a time zone is not available, the date is converted to represent
+   * the same date as a Unix date in UTC as the {@link java.sql.Date} value in
+   * the local time zone.
    *
-   * <p>Converse of {@link #internalToDate(int)}. */
-  public static int toInt(java.util.Date v) {
+   * @see #toInt(java.sql.Date, TimeZone)
+   * @see #internalToDate(int) converse method
+   */
+  public static int toInt(java.sql.Date v) {
     return toInt(v, LOCAL_TZ);
   }
 
-  public static int toInt(java.util.Date v, TimeZone timeZone) {
-    return (int) (toLong(v, timeZone)  / DateTimeUtils.MILLIS_PER_DAY);
+  /**
+   * Converts a SQL DATE value from the Java type
+   * ({@link java.sql.Date}) to the internal representation type
+   * (number of days since January 1st, 1970 as {@code int}).
+   *
+   * <p>The {@link java.sql.Date} class uses the standard Gregorian calendar
+   * which switches from the Julian calendar to the Gregorian calendar in
+   * October 1582. For compatibility with ISO-8601, the internal representation
+   * is converted to use the proleptic Gregorian calendar.
+   *
+   * <p>If the date contains a partial day, it will be rounded to a full day
+   * depending on the milliseconds value. If the milliseconds value is 
positive,
+   * it will be rounded down to the closest full day. If the milliseconds value
+   * is negative, it will be rounded up to the closest full day.
+   */
+  public static int toInt(java.sql.Date v, TimeZone timeZone) {
+    return DateTimeUtils.sqlDateToUnixDate(v, timeZone);
   }
 
-  public static @PolyNull Integer toIntOptional(java.util.@PolyNull Date v) {
-    return v == null ? castNonNull(null) : toInt(v);
+  /**
+   * Converts a nullable SQL DATE value from the Java type
+   * ({@link java.sql.Date}) to the internal representation type
+   * (number of days since January 1st, 1970 as {@link Integer})
+   * in the local time zone.
+   *
+   * <p>Since a time zone is not available, the date is converted to represent
+   * the same date as a Unix date in UTC as the {@link java.sql.Date} value in
+   * the local time zone.
+   *
+   * @see #toInt(java.sql.Date, TimeZone)
+   * @see #internalToDate(Integer) converse method
+   */
+  public static @PolyNull Integer toIntOptional(java.sql.@PolyNull Date v) {
+    return v == null
+        ? castNonNull(null)
+        : toInt(v);
   }
 
-  public static @PolyNull Integer toIntOptional(java.util.@PolyNull Date v,
+  /**
+   * Converts a nullable SQL DATE value from the Java type
+   * ({@link java.sql.Date}) to the internal representation type
+   * (number of days since January 1st, 1970 as {@link Integer}).
+   *
+   * @see #toInt(java.sql.Date, TimeZone)
+   */
+  public static @PolyNull Integer toIntOptional(java.sql.@PolyNull Date v,
       TimeZone timeZone) {
     return v == null
         ? castNonNull(null)
         : toInt(v, timeZone);
   }
 
-  public static long toLong(Date v) {
-    return toLong(v, LOCAL_TZ);
-  }
-
-  /** Converts the Java type used for UDF parameters of SQL TIME type
-   * ({@link java.sql.Time}) to internal representation (int).
+  /**
+   * Converts a SQL TIME value from the Java type
+   * ({@link java.sql.Time}) to the internal representation type
+   * (number of milliseconds since January 1st, 1970 as {@code int})
+   * in the local time zone.
    *
-   * <p>Converse of {@link #internalToTime(int)}. */
+   * @see #toIntOptional(java.sql.Time)
+   * @see #internalToTime(int) converse method
+   */
   public static int toInt(java.sql.Time v) {
-    return (int) (toLong(v) % DateTimeUtils.MILLIS_PER_DAY);
+    return DateTimeUtils.sqlTimeToUnixTime(v, LOCAL_TZ);
   }
 
+  /**
+   * Converts a nullable SQL TIME value from the Java type
+   * ({@link java.sql.Time}) to the internal representation type
+   * (number of milliseconds since January 1st, 1970 as {@link Integer}).
+   *
+   * @see #toInt(java.sql.Time)
+   * @see #internalToTime(Integer) converse method
+   */
   public static @PolyNull Integer toIntOptional(java.sql.@PolyNull Time v) {
     return v == null ? castNonNull(null) : toInt(v);
   }
@@ -2137,7 +2193,8 @@ public class SqlFunctions {
     return o instanceof Integer ? (Integer) o
         : o instanceof Number ? toInt((Number) o)
         : o instanceof String ? toInt((String) o)
-        : o instanceof java.util.Date ? toInt((java.util.Date) o)
+        : o instanceof java.sql.Date ? toInt((java.sql.Date) o)
+        : o instanceof java.sql.Time ? toInt((java.sql.Time) o)
         : (Integer) cannotConvert(o, int.class);
   }
 
@@ -2145,27 +2202,80 @@ public class SqlFunctions {
     return o == null ? castNonNull(null) : toInt(o);
   }
 
-  /** Converts the Java type used for UDF parameters of SQL TIMESTAMP type
-   * ({@link java.sql.Timestamp}) to internal representation (long).
+  /**
+   * Converts a SQL TIMESTAMP value from the Java type
+   * ({@link java.util.Date}) to the internal representation type
+   * (number of milliseconds since January 1st, 1970 as {@code long}).
    *
-   * <p>Converse of {@link #internalToTimestamp(long)}. */
+   * <p>Since a time zone is not available, converts the timestamp to represent
+   * the same date and time as a Unix timestamp in UTC as the
+   * {@link java.util.Date} value in the local time zone.
+   *
+   * <p>The {@link java.util.Date} class uses the standard Gregorian calendar
+   * which switches from the Julian calendar to the Gregorian calendar in
+   * October 1582. For compatibility with ISO-8601, converts the internal
+   * representation to use the proleptic Gregorian calendar.
+   */
+  public static long toLong(java.util.Date v) {
+    return DateTimeUtils.utilDateToUnixTimestamp(v, LOCAL_TZ);
+  }
+
+  /**
+   * Converts a SQL TIMESTAMP value from the Java type
+   * ({@link Timestamp}) to the internal representation type
+   * (number of milliseconds since January 1st, 1970 as {@code long}).
+   *
+   * <p>Since a time zone is not available, converts the timestamp to represent
+   * the same date and time as a Unix timestamp in UTC as the
+   * {@link Timestamp} value in the local time zone.
+   *
+   * @see #toLong(Timestamp, TimeZone)
+   * @see #internalToTimestamp(Long) converse method
+   */
   public static long toLong(Timestamp v) {
     return toLong(v, LOCAL_TZ);
   }
 
-  // mainly intended for java.sql.Timestamp but works for other dates also
-  @SuppressWarnings("JavaUtilDate")
-  public static long toLong(java.util.Date v, TimeZone timeZone) {
-    final long time = v.getTime();
-    return time + timeZone.getOffset(time);
+  /**
+   * Converts a SQL TIMESTAMP value from the Java type
+   * ({@link Timestamp}) to the internal representation type
+   * (number of milliseconds since January 1st, 1970 as {@code long}).
+   *
+   * <p>For backwards compatibility, time zone offsets are calculated in
+   * relation to the local time zone instead of UTC. Providing the default time
+   * zone or {@code null} will return the timestamp unmodified.
+   *
+   * <p>The {@link Timestamp} class uses the standard Gregorian calendar which
+   * switches from the Julian calendar to the Gregorian calendar in
+   * October 1582. For compatibility with ISO-8601, the internal representation
+   * is converted to use the proleptic Gregorian calendar.
+   */
+  public static long toLong(Timestamp v, TimeZone timeZone) {
+    return DateTimeUtils.sqlTimestampToUnixTimestamp(v, timeZone);
   }
 
-  // mainly intended for java.sql.Timestamp but works for other dates also
-  public static @PolyNull Long toLongOptional(java.util.@PolyNull Date v) {
+  /**
+   * Converts a nullable SQL TIMESTAMP value from the Java type
+   * ({@link Timestamp}) to the internal representation type
+   * (number of milliseconds since January 1st, 1970 as {@link Long})
+   * in the local time zone.
+   *
+   * @see #toLong(Timestamp, TimeZone)
+   * @see #internalToTimestamp(Long) converse method
+   */
+  public static @PolyNull Long toLongOptional(@PolyNull Timestamp v) {
     return v == null ? castNonNull(null) : toLong(v, LOCAL_TZ);
   }
 
-  public static @PolyNull Long toLongOptional(@PolyNull Timestamp v, TimeZone 
timeZone) {
+  /**
+   * Converts a nullable SQL TIMESTAMP value from the Java type
+   * ({@link Timestamp}) to the internal representation type
+   * (number of milliseconds since January 1st, 1970 as {@link Long}).
+   *
+   * @see #toLong(Timestamp, TimeZone)
+   */
+  public static @PolyNull Long toLongOptional(@PolyNull Timestamp v,
+      TimeZone timeZone) {
     if (v == null) {
       return castNonNull(null);
     }
@@ -2187,6 +2297,9 @@ public class SqlFunctions {
     return o instanceof Long ? (Long) o
         : o instanceof Number ? toLong((Number) o)
         : o instanceof String ? toLong((String) o)
+        : o instanceof java.sql.Date ? toInt((java.sql.Date) o)
+        : o instanceof java.sql.Time ? toInt((java.sql.Time) o)
+        : o instanceof java.sql.Timestamp ? toLong((java.sql.Timestamp) o)
         : o instanceof java.util.Date ? toLong((java.util.Date) o)
         : (Long) cannotConvert(o, long.class);
   }
@@ -2243,24 +2356,61 @@ public class SqlFunctions {
         : toBigDecimal(o.toString());
   }
 
-  /** Converts the internal representation of a SQL DATE (int) to the Java
-   * type used for UDF parameters ({@link java.sql.Date}). */
+  /**
+   * Converts a SQL DATE value from the internal representation type
+   * (number of days since January 1st, 1970) to the Java type
+   * ({@link java.sql.Date}).
+   *
+   * <p>Since a time zone is not available, converts the date to represent the
+   * same date as a {@link java.sql.Date} in the local time zone as the Unix
+   * date in UTC.
+   *
+   * <p>The Unix date should be the number of days since January 1st, 1970
+   * using the proleptic Gregorian calendar as defined by ISO-8601. The
+   * returned {@link java.sql.Date} object will use the standard Gregorian
+   * calendar which switches from the Julian calendar to the Gregorian calendar
+   * in October 1582.
+   *
+   * @see #internalToDate(Integer)
+   * @see #toInt(java.sql.Date) converse method
+   */
   public static java.sql.Date internalToDate(int v) {
-    final long t = v * DateTimeUtils.MILLIS_PER_DAY;
-    return new java.sql.Date(t - LOCAL_TZ.getOffset(t));
+    final LocalDate date = LocalDate.ofEpochDay(v);
+    return java.sql.Date.valueOf(date);
   }
 
-  /** As {@link #internalToDate(int)} but allows nulls. */
+  /**
+   * Converts a nullable SQL DATE value from the internal representation type
+   * (number of days since January 1st, 1970) to the Java type
+   * ({@link java.sql.Date}).
+   *
+   * @see #internalToDate(int)
+   * @see #toIntOptional(java.sql.Date) converse method
+   */
   public static java.sql.@PolyNull Date internalToDate(@PolyNull Integer v) {
     return v == null ? castNonNull(null) : internalToDate(v.intValue());
   }
 
-  /** Converts the internal representation of a SQL TIME (int) to the Java
-   * type used for UDF parameters ({@link java.sql.Time}). */
+  /**
+   * Converts a SQL TIME value from the internal representation type
+   * (number of milliseconds since January 1st, 1970) to the Java type
+   * ({@link java.sql.Time}).
+   *
+   * @see #internalToTime(Integer)
+   * @see #toInt(java.sql.Time) converse method
+   */
   public static java.sql.Time internalToTime(int v) {
     return new java.sql.Time(v - LOCAL_TZ.getOffset(v));
   }
 
+  /**
+   * Converts a nullable SQL TIME value from the internal representation type
+   * (number of milliseconds since January 1st, 1970) to the Java type
+   * ({@link java.sql.Time}).
+   *
+   * @see #internalToTime(Integer)
+   * @see #toIntOptional(java.sql.Time) converse method
+   */
   public static java.sql.@PolyNull Time internalToTime(@PolyNull Integer v) {
     return v == null ? castNonNull(null) : internalToTime(v.intValue());
   }
@@ -2316,12 +2466,48 @@ public class SqlFunctions {
         .toString();
   }
 
-  /** Converts the internal representation of a SQL TIMESTAMP (long) to the 
Java
-   * type used for UDF parameters ({@link java.sql.Timestamp}). */
+  /**
+   * Converts a SQL TIMESTAMP value from the internal representation type
+   * (number of milliseconds since January 1st, 1970) to the Java Type
+   * ({@link Timestamp})
+   * in the local time zone.
+   *
+   * <p>Since a time zone is not available, the timestamp is converted to
+   * represent the same timestamp as a {@link Timestamp} in the local time zone
+   * as the Unix timestamp in UTC.
+   *
+   * <p>The Unix timestamp should be the number of milliseconds since
+   * January 1st, 1970 using the proleptic Gregorian calendar as defined by
+   * ISO-8601. The returned {@link Timestamp} object will use the standard
+   * Gregorian calendar which switches from the Julian calendar to the
+   * Gregorian calendar in October 1582.
+   *
+   * @see #internalToTimestamp(Long)
+   * @see #toLong(Timestamp, TimeZone)
+   * @see #toLongOptional(Timestamp)
+   * @see #toLongOptional(Timestamp, TimeZone)
+   * @see #toLong(Timestamp) converse method
+   */
   public static java.sql.Timestamp internalToTimestamp(long v) {
-    return new java.sql.Timestamp(v - LOCAL_TZ.getOffset(v));
+    final LocalDateTime dateTime = LocalDateTime.ofEpochSecond(
+        Math.floorDiv(v, DateTimeUtils.MILLIS_PER_SECOND),
+        (int) (Math.floorMod(v, DateTimeUtils.MILLIS_PER_SECOND) * 
DateTimeUtils.NANOS_PER_MILLI),
+        ZoneOffset.UTC);
+    return java.sql.Timestamp.valueOf(dateTime);
   }
 
+  /**
+   * Converts a nullable SQL TIMESTAMP value from the internal representation
+   * type (number of milliseconds since January 1st, 1970) to the Java Type
+   * ({@link Timestamp})
+   * in the local time zone.
+   *
+   * @see #internalToTimestamp(long)
+   * @see #toLong(Timestamp)
+   * @see #toLong(Timestamp, TimeZone)
+   * @see #toLongOptional(Timestamp, TimeZone)
+   * @see #toLongOptional(Timestamp) converse method
+   */
   public static java.sql.@PolyNull Timestamp internalToTimestamp(@PolyNull 
Long v) {
     return v == null ? castNonNull(null) : internalToTimestamp(v.longValue());
   }
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index da839ea09d..ff9c5fcb30 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -563,13 +563,17 @@ public enum BuiltInMethod {
       Comparable.class, Comparator.class),
   ROUND_LONG(SqlFunctions.class, "round", long.class, long.class),
   ROUND_INT(SqlFunctions.class, "round", int.class, int.class),
-  DATE_TO_INT(SqlFunctions.class, "toInt", java.util.Date.class),
+  DATE_TO_INT(SqlFunctions.class, "toInt", java.sql.Date.class),
+  DATE_TO_INT_OFFSET(SqlFunctions.class, "toInt", java.sql.Date.class,
+      TimeZone.class),
   DATE_TO_INT_OPTIONAL(SqlFunctions.class, "toIntOptional",
-      java.util.Date.class),
+      java.sql.Date.class),
+  DATE_TO_INT_OPTIONAL_OFFSET(SqlFunctions.class, "toIntOptional",
+      java.sql.Date.class, TimeZone.class),
   TIME_TO_INT(SqlFunctions.class, "toInt", Time.class),
   TIME_TO_INT_OPTIONAL(SqlFunctions.class, "toIntOptional", Time.class),
-  TIMESTAMP_TO_LONG(SqlFunctions.class, "toLong", java.util.Date.class),
-  TIMESTAMP_TO_LONG_OFFSET(SqlFunctions.class, "toLong", java.util.Date.class,
+  TIMESTAMP_TO_LONG(SqlFunctions.class, "toLong", Timestamp.class),
+  TIMESTAMP_TO_LONG_OFFSET(SqlFunctions.class, "toLong", Timestamp.class,
       TimeZone.class),
   TIMESTAMP_TO_LONG_OPTIONAL(SqlFunctions.class, "toLongOptional",
       Timestamp.class),
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 61f97f90fa..d54a9224a0 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -8030,6 +8030,90 @@ public class JdbcTest {
         .returns("EXPR$0=[0E+1, 1.1]\n");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5414";>[CALCITE-5414]</a>
+   * Convert between standard Gregorian and proleptic Gregorian calendars for
+   * literal dates in local time zone. */
+  @Test void testLiteralDateToSqlTimestamp() {
+    CalciteAssert.that()
+        .with(CalciteConnectionProperty.TIME_ZONE, 
TimeZone.getDefault().getID())
+        .query("select cast('1500-04-30' as date)")
+        .returns(resultSet -> {
+          try {
+            assertTrue(resultSet.next());
+            assertEquals("1500-04-30", resultSet.getString(1));
+            assertEquals(Date.valueOf("1500-04-30"), resultSet.getDate(1));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw new RuntimeException(e);
+          }
+        });
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5414";>[CALCITE-5414]</a>
+   * Convert between standard Gregorian and proleptic Gregorian calendars for
+   * literal timestamps in local time zone. */
+  @Test void testLiteralTimestampToSqlTimestamp() {
+    CalciteAssert.that()
+        .with(CalciteConnectionProperty.TIME_ZONE, 
TimeZone.getDefault().getID())
+        .query("select cast('1500-04-30 12:00:00' as timestamp)")
+        .returns(resultSet -> {
+          try {
+            assertTrue(resultSet.next());
+            assertEquals("1500-04-30 12:00:00", resultSet.getString(1));
+            assertEquals(Timestamp.valueOf("1500-04-30 12:00:00"), 
resultSet.getTimestamp(1));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw new RuntimeException(e);
+          }
+        });
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5414";>[CALCITE-5414]</a>
+   * Convert between standard Gregorian and proleptic Gregorian calendars for
+   * dynamic dates in local time zone. */
+  @Test void testDynamicDateToSqlTimestamp() {
+    final Date date = Date.valueOf("1500-04-30");
+    CalciteAssert.that()
+        .with(CalciteConnectionProperty.TIME_ZONE, 
TimeZone.getDefault().getID())
+        .query("select cast(? as date)")
+        .consumesPreparedStatement(statement -> statement.setDate(1, date))
+        .returns(resultSet -> {
+          try {
+            assertTrue(resultSet.next());
+            assertEquals("1500-04-30", resultSet.getString(1));
+            assertEquals(date, resultSet.getDate(1));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw new RuntimeException(e);
+          }
+        });
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-5414";>[CALCITE-5414]</a>
+   * Convert between standard Gregorian and proleptic Gregorian calendars for
+   * dynamic timestamps in local time zone. */
+  @Test void testDynamicTimestampToSqlTimestamp() {
+    final Timestamp timestamp = Timestamp.valueOf("1500-04-30 12:00:00");
+    CalciteAssert.that()
+        .with(CalciteConnectionProperty.TIME_ZONE, 
TimeZone.getDefault().getID())
+        .query("select cast(? as timestamp)")
+        .consumesPreparedStatement(statement -> statement.setTimestamp(1, 
timestamp))
+        .returns(resultSet -> {
+          try {
+            assertTrue(resultSet.next());
+            assertEquals("1500-04-30 12:00:00", resultSet.getString(1));
+            assertEquals(timestamp, resultSet.getTimestamp(1));
+            assertFalse(resultSet.next());
+          } catch (SQLException e) {
+            throw new RuntimeException(e);
+          }
+        });
+  }
+
   private static String sums(int n, boolean c) {
     final StringBuilder b = new StringBuilder();
     for (int i = 0; i < n; i++) {
diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
index 683333b596..ea1fdc2d6f 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -25,11 +25,20 @@ import org.apache.calcite.runtime.Utilities;
 import org.junit.jupiter.api.Test;
 
 import java.math.BigDecimal;
+import java.sql.Time;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Calendar;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
 
+import static org.apache.calcite.avatica.util.DateTimeUtils.MILLIS_PER_DAY;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.dateStringToUnixDate;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.timeStringToUnixDate;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.timestampStringToUnixDate;
 import static org.apache.calcite.avatica.util.DateTimeUtils.ymdToUnixDate;
 import static org.apache.calcite.runtime.SqlFunctions.addMonths;
 import static org.apache.calcite.runtime.SqlFunctions.charLength;
@@ -37,6 +46,9 @@ import static org.apache.calcite.runtime.SqlFunctions.concat;
 import static org.apache.calcite.runtime.SqlFunctions.fromBase64;
 import static org.apache.calcite.runtime.SqlFunctions.greater;
 import static org.apache.calcite.runtime.SqlFunctions.initcap;
+import static org.apache.calcite.runtime.SqlFunctions.internalToDate;
+import static org.apache.calcite.runtime.SqlFunctions.internalToTime;
+import static org.apache.calcite.runtime.SqlFunctions.internalToTimestamp;
 import static org.apache.calcite.runtime.SqlFunctions.lesser;
 import static org.apache.calcite.runtime.SqlFunctions.lower;
 import static org.apache.calcite.runtime.SqlFunctions.ltrim;
@@ -47,6 +59,10 @@ import static org.apache.calcite.runtime.SqlFunctions.rtrim;
 import static org.apache.calcite.runtime.SqlFunctions.sha1;
 import static org.apache.calcite.runtime.SqlFunctions.subtractMonths;
 import static org.apache.calcite.runtime.SqlFunctions.toBase64;
+import static org.apache.calcite.runtime.SqlFunctions.toInt;
+import static org.apache.calcite.runtime.SqlFunctions.toIntOptional;
+import static org.apache.calcite.runtime.SqlFunctions.toLong;
+import static org.apache.calcite.runtime.SqlFunctions.toLongOptional;
 import static org.apache.calcite.runtime.SqlFunctions.trim;
 import static org.apache.calcite.runtime.SqlFunctions.upper;
 import static org.apache.calcite.test.Matchers.within;
@@ -974,4 +990,329 @@ class SqlFunctionsTest {
       // ok
     }
   }
+
+  /**
+   * Tests that a date in the local time zone converts to a Unix timestamp in
+   * UTC.
+   */
+  @Test void testToIntWithSqlDate() {
+    assertThat(toInt(new java.sql.Date(0L)), is(0));  // rounded to closest day
+    assertThat(sqlDate("1970-01-01"), is(0));
+    assertThat(sqlDate("1500-04-30"), is(dateStringToUnixDate("1500-04-30")));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by
+   * {@code java.sql} and the proleptic Gregorian calendar used by Unix
+   * timestamps.
+   */
+  @Test void testToIntWithSqlDateInGregorianShift() {
+    assertThat(sqlDate("1582-10-04"), is(dateStringToUnixDate("1582-10-04")));
+    assertThat(sqlDate("1582-10-05"), is(dateStringToUnixDate("1582-10-15")));
+    assertThat(sqlDate("1582-10-15"), is(dateStringToUnixDate("1582-10-15")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent 0001-01-01 depending on the default
+   * time zone. If the date would fall outside of Anno Domini (AD) when
+   * converted to the default time zone, that date should not be tested.
+   *
+   * <p>Not every time zone has a January 1st 12:00am, so this test skips those
+   * dates.
+   */
+  @Test void testToIntWithSqlDateInAnsiDateRange() {
+    for (int i = 2; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01", i);
+      final java.sql.Date date = java.sql.Date.valueOf(str);
+      final Timestamp timestamp = new Timestamp(date.getTime());
+      if (timestamp.toString().endsWith("00:00:00.0")) {
+        // Test equality if the time is valid in Java
+        assertThat("Converts '" + str + "' from SQL to Unix date",
+            toInt(date),
+            is(dateStringToUnixDate(str)));
+      } else {
+        // Test result matches legacy behavior if the time cannot be
+        // represented in Java. This probably results in a different date but
+        // is pretty rare.
+        final long expected =
+            (date.getTime() + 
DateTimeUtils.DEFAULT_ZONE.getOffset(date.getTime()))
+                / DateTimeUtils.MILLIS_PER_DAY;
+        assertThat("Converts '" + str
+                + "' from SQL to Unix date using legacy behavior",
+            toInt(date),
+            is((int) expected));
+      }
+    }
+  }
+
+  /**
+   * Test using a custom {@link TimeZone} to calculate the Unix timestamp.
+   * Dates created by a {@link java.sql.Date} method should be converted
+   * relative to the local time and not UTC.
+   */
+  @Test public void testToIntWithTimeZone() {
+    // Dates created by a Calendar should be converted to a Unix date in that
+    // time zone
+    final Calendar utcCal =
+        Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(
+        toInt(new java.sql.Date(utcCal.getTimeInMillis()),
+            utcCal.getTimeZone()),
+        is(0));
+
+    // Dates should be relative to the local time and not UTC
+    final java.sql.Date epoch = java.sql.Date.valueOf("1970-01-01");
+
+    final TimeZone minusDayZone = TimeZone.getDefault();
+    minusDayZone.setRawOffset((int) (minusDayZone.getOffset(0L) - 
MILLIS_PER_DAY));
+    assertThat(toInt(epoch, minusDayZone), is(-1));
+
+    final TimeZone plusDayZone = TimeZone.getDefault();
+    plusDayZone.setRawOffset((int) (plusDayZone.getOffset(0L) + 
MILLIS_PER_DAY));
+    assertThat(toInt(epoch, plusDayZone), is(1));
+  }
+
+  /**
+   * Tests that a nullable date in the local time zone converts to a Unix
+   * timestamp in UTC.
+   */
+  @Test void testToIntOptionalWithLocalTimeZone() {
+    assertThat(toIntOptional(java.sql.Date.valueOf("1970-01-01")), is(0));
+    assertThat(toIntOptional((java.sql.Date) null), is(nullValue()));
+  }
+
+  /**
+   * Tests that a nullable date in the given time zone converts to a Unix
+   * timestamp in UTC.
+   */
+  @Test void testToIntOptionalWithCustomTimeZone() {
+    final TimeZone utc = TimeZone.getTimeZone("UTC");
+    assertThat(toIntOptional(new java.sql.Date(0L), utc), is(0));
+    assertThat(toIntOptional(null, utc), is(nullValue()));
+  }
+
+  /**
+   * Tests that a time in the local time zone converts to a Unix time in UTC.
+   */
+  @Test void testToIntWithSqlTime() {
+    assertThat(sqlTime("00:00:00"), is(timeStringToUnixDate("00:00:00")));
+    assertThat(sqlTime("23:59:59"), is(timeStringToUnixDate("23:59:59")));
+  }
+
+  /**
+   * Tests that a nullable time in the local time zone converts to a Unix time
+   * in UTC.
+   */
+  @Test void testToIntOptionalWithSqlTime() {
+    assertThat(toIntOptional(Time.valueOf("00:00:00")), is(0));
+    assertThat(toIntOptional((Time) null), is(nullValue()));
+  }
+
+  /**
+   * Tests that a timestamp in the local time zone converts to a Unix timestamp
+   * in UTC.
+   */
+  @Test void testToLongWithSqlTimestamp() {
+    assertThat(sqlTimestamp("1970-01-01 00:00:00"), is(0L));
+    assertThat(sqlTimestamp("2014-09-30 15:28:27.356"),
+        is(timestampStringToUnixDate("2014-09-30 15:28:27.356")));
+    assertThat(sqlTimestamp("1500-04-30 12:00:00.123"),
+        is(timestampStringToUnixDate("1500-04-30 12:00:00.123")));
+  }
+
+  /**
+   * Test using a custom {@link TimeZone} to calculate the Unix timestamp.
+   * Timestamps created by a {@link Calendar} should be converted to a Unix
+   * timestamp in the given time zone. Timestamps created by a {@link 
Timestamp}
+   * method should be converted relative to the local time and not UTC.
+   */
+  @Test void testToLongWithSqlTimestampAndCustomTimeZone() {
+    final Timestamp epoch = java.sql.Timestamp.valueOf("1970-01-01 00:00:00");
+
+    final Calendar utcCal =
+        Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ROOT);
+    utcCal.set(1970, Calendar.JANUARY, 1, 0, 0, 0);
+    utcCal.set(Calendar.MILLISECOND, 0);
+    assertThat(toLong(new Timestamp(utcCal.getTimeInMillis()), 
utcCal.getTimeZone()),
+        is(0L));
+
+    final TimeZone est = TimeZone.getTimeZone("GMT-5:00");
+    assertThat(toLong(epoch, est),
+        is(epoch.getTime() + est.getOffset(epoch.getTime())));
+
+    final TimeZone ist = TimeZone.getTimeZone("GMT+5:00");
+    assertThat(toLong(epoch, ist),
+        is(epoch.getTime() + ist.getOffset(epoch.getTime())));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by
+   * {@code java.sql} and the proleptic Gregorian calendar used by Unix
+   * timestamps.
+   */
+  @Test void testToLongWithSqlTimestampInGregorianShift() {
+    assertThat(sqlTimestamp("1582-10-04 00:00:00"),
+        is(timestampStringToUnixDate("1582-10-04 00:00:00")));
+    assertThat(sqlTimestamp("1582-10-05 00:00:00"),
+        is(timestampStringToUnixDate("1582-10-15 00:00:00")));
+    assertThat(sqlTimestamp("1582-10-15 00:00:00"),
+        is(timestampStringToUnixDate("1582-10-15 00:00:00")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent 0001-01-01 depending on the default
+   * time zone. If the date would fall outside of Anno Domini (AD) when
+   * converted to the default time zone, that date should not be tested.
+   *
+   * <p>Not every time zone has a January 1st 12:00am, so this test skips those
+   * dates.
+   */
+  @Test void testToLongWithSqlTimestampInAnsiDateRange() {
+    for (int i = 2; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01 00:00:00", i);
+      final Timestamp timestamp = Timestamp.valueOf(str);
+      if (timestamp.toString().endsWith("00:00:00.0")) {
+        // Test equality if the time is valid in Java
+        assertThat("Converts '" + str + "' from SQL to Unix timestamp",
+            toLong(timestamp),
+            is(timestampStringToUnixDate(str)));
+      } else {
+        // Test result matches legacy behavior if the time cannot be 
represented in Java
+        // This probably results in a different date but is pretty rare
+        final long expected = timestamp.getTime()
+            + DateTimeUtils.DEFAULT_ZONE.getOffset(timestamp.getTime());
+        assertThat("Converts '" + str
+                + "' from SQL to Unix timestamp using legacy behavior",
+            toLong(timestamp),
+            is(expected));
+      }
+    }
+  }
+
+  /**
+   * Tests that a nullable timestamp in the local time zone converts to a Unix
+   * timestamp in UTC.
+   */
+  @Test void testToLongOptionalWithLocalTimeZone() {
+    assertThat(toLongOptional(Timestamp.valueOf("1970-01-01 00:00:00")), 
is(0L));
+    assertThat(toLongOptional(null), is(nullValue()));
+  }
+
+  /**
+   * Tests that a nullable timestamp in the given time zone converts to a Unix
+   * timestamp in UTC.
+   */
+  @Test void testToLongOptionalWithCustomTimeZone() {
+    final TimeZone utc = TimeZone.getTimeZone("UTC");
+    assertThat(toLongOptional(new Timestamp(0L), utc), is(0L));
+    assertThat(toLongOptional(null, utc), is(nullValue()));
+  }
+
+  /**
+   * Tests that a Unix timestamp converts to a date in the local time zone.
+   */
+  @Test void testInternalToDate() {
+    assertThat(internalToDate(0), is(java.sql.Date.valueOf("1970-01-01")));
+    assertThat(internalToDate(dateStringToUnixDate("1500-04-30")),
+        is(java.sql.Date.valueOf("1500-04-30")));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by
+   * {@code java.sql} and the proleptic Gregorian calendar used by Unix
+   * timestamps.
+   */
+  @Test void testInternalToDateWithGregorianShift() {
+    // Gregorian shift
+    assertThat(internalToDate(dateStringToUnixDate("1582-10-04")),
+        is(java.sql.Date.valueOf("1582-10-04")));
+    assertThat(internalToDate(dateStringToUnixDate("1582-10-05")),
+        is(java.sql.Date.valueOf("1582-10-15")));
+    assertThat(internalToDate(dateStringToUnixDate("1582-10-15")),
+        is(java.sql.Date.valueOf("1582-10-15")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent all dates depending on the default 
time zone, but both
+   * the expected and actual assertion values handles that in the same way.
+   */
+  @Test void testInternalToDateWithAnsiDateRange() {
+    for (int i = 2; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01", i);
+      assertThat(internalToDate(dateStringToUnixDate(str)),
+          is(java.sql.Date.valueOf(str)));
+    }
+  }
+
+  /**
+   * Tests that a Unix time converts to a SQL time in the local time zone.
+   */
+  @Test void testInternalToTime() {
+    assertThat(internalToTime(0), is(Time.valueOf("00:00:00")));
+    assertThat(internalToTime(86399000), is(Time.valueOf("23:59:59")));
+  }
+
+  /**
+   * Tests that a Unix timestamp converts to a SQL timestamp in the local time
+   * zone.
+   */
+  @Test void testInternalToTimestamp() {
+    assertThat(internalToTimestamp(0),
+        is(Timestamp.valueOf("1970-01-01 00:00:00.0")));
+    assertThat(internalToTimestamp(timestampStringToUnixDate("2014-09-30 
15:28:27.356")),
+        is(Timestamp.valueOf("2014-09-30 15:28:27.356")));
+    assertThat(internalToTimestamp(timestampStringToUnixDate("1500-04-30 
12:00:00.123")),
+        is(Timestamp.valueOf("1500-04-30 12:00:00.123")));
+  }
+
+  /**
+   * Test calendar conversion from the standard Gregorian calendar used by
+   * {@code java.sql} and the proleptic Gregorian calendar used by Unix 
timestamps.
+   */
+  @Test void testInternalToTimestampWithGregorianShift() {
+    assertThat(
+        internalToTimestamp(timestampStringToUnixDate("1582-10-04 00:00:00")),
+        is(Timestamp.valueOf("1582-10-04 00:00:00.0")));
+    assertThat(
+        internalToTimestamp(timestampStringToUnixDate("1582-10-05 00:00:00")),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+    assertThat(
+        internalToTimestamp(timestampStringToUnixDate("1582-10-15 00:00:00")),
+        is(Timestamp.valueOf("1582-10-15 00:00:00.0")));
+  }
+
+  /**
+   * Test date range 0001-01-01 to 9999-12-31 required by ANSI SQL.
+   *
+   * <p>Java may not be able to represent all dates depending on the default
+   * time zone, but both the expected and actual assertion values handles that
+   * in the same way.
+   */
+  @Test void testInternalToTimestampWithAnsiDateRange() {
+    for (int i = 2; i <= 9999; ++i) {
+      final String str = String.format(Locale.ROOT, "%04d-01-01 00:00:00", i);
+      assertThat(internalToTimestamp(timestampStringToUnixDate(str)),
+          is(Timestamp.valueOf(str)));
+    }
+  }
+
+  private int sqlDate(String str) {
+    return toInt(java.sql.Date.valueOf(str));
+  }
+
+  private int sqlTime(String str) {
+    return toInt(java.sql.Time.valueOf(str));
+  }
+
+  private long sqlTimestamp(String str) {
+    return toLong(java.sql.Timestamp.valueOf(str));
+  }
 }
diff --git a/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java 
b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
index debb0b4ff7..555c01f95e 100644
--- a/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
@@ -293,6 +293,7 @@ public abstract class QuidemTest {
             .connect();
       case "catchall":
         return CalciteAssert.that()
+            .with(CalciteConnectionProperty.TIME_ZONE, "UTC")
             .withSchema("s",
                 new ReflectiveSchema(
                     new CatchallSchema()))


Reply via email to