You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by st...@apache.org on 2022/11/29 13:46:50 UTC

[phoenix] branch master updated: PHOENIX-6823 calling Joda-based round() function on temporal PK field causes division by zero error

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

stoty pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/master by this push:
     new 66fad95a15 PHOENIX-6823 calling Joda-based round() function on temporal PK field causes division by zero error
66fad95a15 is described below

commit 66fad95a15f55620e9d11b5f4566e9543da30d44
Author: Istvan Toth <st...@apache.org>
AuthorDate: Tue Nov 22 07:22:23 2022 +0100

    PHOENIX-6823 calling Joda-based round() function on temporal PK field causes division by zero error
    
    also fix other bugs in the key range pushdown logic for temporals
---
 .../phoenix/end2end/RoundFloorCeilFuncIT.java      |  437 +++++++
 .../expression/function/CeilMonthExpression.java   |   13 +
 .../expression/function/CeilWeekExpression.java    |   13 +
 .../expression/function/CeilYearExpression.java    |   13 +
 .../expression/function/FloorMonthExpression.java  |   13 +
 .../expression/function/FloorWeekExpression.java   |   13 +
 .../expression/function/FloorYearExpression.java   |   13 +
 .../expression/function/RoundDateExpression.java   |   82 +-
 .../function/RoundJodaDateExpression.java          |   10 +-
 .../expression/function/RoundMonthExpression.java  |   21 +
 .../expression/function/RoundWeekExpression.java   |   22 +
 .../expression/function/RoundYearExpression.java   |   21 +
 .../java/org/apache/phoenix/util/DateUtil.java     |   55 +-
 .../apache/phoenix/compile/WhereOptimizerTest.java |  251 +++-
 .../expression/RoundFloorCeilExpressionsTest.java  | 1235 ++++++++++++++++++++
 15 files changed, 2165 insertions(+), 47 deletions(-)

diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/RoundFloorCeilFuncIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/RoundFloorCeilFuncIT.java
index 2eba6e08b6..f7facaf253 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/RoundFloorCeilFuncIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/RoundFloorCeilFuncIT.java
@@ -29,8 +29,11 @@ import java.sql.DriverManager;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.Statement;
 import java.sql.Time;
 import java.sql.Timestamp;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.util.Properties;
 
 import org.apache.phoenix.expression.function.CeilFunction;
@@ -669,4 +672,438 @@ public class RoundFloorCeilFuncIT extends ParallelStatsDisabledIT {
     assertEquals(1380603, rs.getLong(1));
   }
 
+  @Test
+  public void testRoundFunctionsRowKey() throws SQLException {
+      // Exercise the newKeyPart push down logic in RoundDateExpression
+
+      java.time.LocalDateTime LD_PAST = java.time.LocalDateTime.of(2000, 6, 6, 12, 0, 30, 0);
+      java.time.Instant INSTANT_LD_PAST = LD_PAST.toInstant(ZoneOffset.UTC);
+
+      java.time.LocalDateTime LD_FUTURE = java.time.LocalDateTime.of(2040, 6, 6, 12, 0, 30, 0);
+      java.time.Instant INSTANT_LD_FUTURE = LD_FUTURE.toInstant(ZoneOffset.UTC);
+
+      java.time.LocalDateTime LD_MID_MIN = java.time.LocalDateTime.of(2022, 6, 6, 12, 0, 30, 0);
+      java.time.Instant INSTANT_MID_MIN = LD_MID_MIN.toInstant(ZoneOffset.UTC);
+      Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+      try (Connection conn = DriverManager.getConnection(getUrl(), props);
+              Statement stmt = conn.createStatement();) {
+
+          String tableName = generateUniqueName();
+          stmt.executeUpdate(
+              "CREATE TABLE " + tableName + " ( " + " df DATE primary key, dfv DATE) ");
+
+          PreparedStatement ps =
+                  conn.prepareStatement("UPSERT INTO " + tableName + " VALUES (?, ?)");
+
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_LD_PAST));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_LD_PAST));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN.minusMillis(1)));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN.minusMillis(1)));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN.plusMillis(1)));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN.plusMillis(1)));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_LD_FUTURE));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_LD_FUTURE));
+          ps.executeUpdate();
+          conn.commit();
+
+          ResultSet rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(df, 'MINUTE', 1) = DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(df, 'MINUTE', 1) = DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN -1
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(df, 'MINUTE', 1) > DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // FUTURE
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(df, 'MINUTE', 1) >= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1, FUTURE
+          assertEquals(3, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(df, 'MINUTE', 1) < DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(df, 'MINUTE', 1) <= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN, MID_MIN +1
+          assertEquals(4, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(df, 'MINUTE', 1) <= DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+
+          //
+          // The same, but without range scan:
+          //
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(dfv, 'MINUTE', 1) = DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(dfv, 'MINUTE', 1) = DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN -1
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(dfv, 'MINUTE', 1) > DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // FUTURE
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(dfv, 'MINUTE', 1) >= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1, FUTURE
+          assertEquals(3, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(dfv, 'MINUTE', 1) < DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(dfv, 'MINUTE', 1) <= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN, MID_MIN +1
+          assertEquals(4, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where round(dfv, 'MINUTE', 1) <= DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+      }
+  }
+
+  @Test
+  public void testFloorFunctionsRowKey() throws SQLException {
+      // Exercise the newKeyPart push down logic in RoundDateExpression
+
+      java.time.LocalDateTime LD_PAST = java.time.LocalDateTime.of(2000, 6, 6, 12, 1, 0, 0);
+      java.time.Instant INSTANT_LD_PAST = LD_PAST.toInstant(ZoneOffset.UTC);
+
+      java.time.LocalDateTime LD_FUTURE = java.time.LocalDateTime.of(2040, 6, 6, 12, 1, 0, 0);
+      java.time.Instant INSTANT_LD_FUTURE = LD_FUTURE.toInstant(ZoneOffset.UTC);
+
+      java.time.LocalDateTime LD_MID_MIN = java.time.LocalDateTime.of(2022, 6, 6, 12, 1, 0, 0);
+      java.time.Instant INSTANT_MID_MIN = LD_MID_MIN.toInstant(ZoneOffset.UTC);
+
+      Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+      try (Connection conn = DriverManager.getConnection(getUrl(), props);
+              Statement stmt = conn.createStatement();) {
+
+          String tableName = generateUniqueName();
+          stmt.executeUpdate(
+              "CREATE TABLE " + tableName + " ( " + " df DATE primary key, dfv DATE) ");
+
+          PreparedStatement ps =
+                  conn.prepareStatement("UPSERT INTO " + tableName + " VALUES (?, ?)");
+
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_LD_PAST));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_LD_PAST));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN.minusMillis(1)));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN.minusMillis(1)));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN.plusMillis(1)));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN.plusMillis(1)));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_LD_FUTURE));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_LD_FUTURE));
+          ps.executeUpdate();
+          conn.commit();
+
+          ResultSet rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(df, 'MINUTE', 1) = DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(df, 'MINUTE', 1) = DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN -1
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(df, 'MINUTE', 1) > DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // FUTURE
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(df, 'MINUTE', 1) >= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1, FUTURE
+          assertEquals(3, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(df, 'MINUTE', 1) < DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(df, 'MINUTE', 1) <= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN, MID_MIN +1
+          assertEquals(4, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(df, 'MINUTE', 1) <= DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+
+          //
+          // The same, but without range scan:
+          //
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(dfv, 'MINUTE', 1) = DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(dfv, 'MINUTE', 1) = DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN -1
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(dfv, 'MINUTE', 1) > DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // FUTURE
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(dfv, 'MINUTE', 1) >= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // MID_MIN, MID_MIN +1, FUTURE
+          assertEquals(3, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(dfv, 'MINUTE', 1) < DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(dfv, 'MINUTE', 1) <= DATE '2022-06-06 12:01:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN, MID_MIN +1
+          assertEquals(4, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where floor(dfv, 'MINUTE', 1) <= DATE '2022-06-06 12:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1
+          assertEquals(2, rs.getInt(1));
+      }
+  }
+
+  @Test
+  public void testCeilFunctionsRowKey() throws SQLException {
+      // Exercise the newKeyPart push down logic in RoundDateExpression and children
+
+      java.time.LocalDateTime LD_PAST = java.time.LocalDateTime.of(2000, 6, 6, 12, 1, 0, 0);
+      java.time.Instant INSTANT_LD_PAST = LD_PAST.toInstant(ZoneOffset.UTC);
+
+      java.time.LocalDateTime LD_FUTURE = java.time.LocalDateTime.of(2040, 6, 6, 12, 0, 0, 0);
+      java.time.Instant INSTANT_LD_FUTURE = LD_FUTURE.toInstant(ZoneOffset.UTC);
+
+      java.time.LocalDateTime LD_MID_MIN = java.time.LocalDateTime.of(2022, 6, 1, 0, 0, 0, 0);
+      java.time.Instant INSTANT_MID_MIN = LD_MID_MIN.toInstant(ZoneOffset.UTC);
+
+      Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+      try (Connection conn = DriverManager.getConnection(getUrl(), props);
+              Statement stmt = conn.createStatement();) {
+
+          String tableName = generateUniqueName();
+          stmt.executeUpdate(
+              "CREATE TABLE " + tableName + " ( " + " df DATE primary key, dfv DATE) ");
+
+          PreparedStatement ps =
+                  conn.prepareStatement("UPSERT INTO " + tableName + " VALUES (?, ?)");
+
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_LD_PAST));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_LD_PAST));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN.minusMillis(1)));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN.minusMillis(1)));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_MID_MIN.plusMillis(1)));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_MID_MIN.plusMillis(1)));
+          ps.executeUpdate();
+          ps.setTimestamp(1, java.sql.Timestamp.from(INSTANT_LD_FUTURE));
+          ps.setTimestamp(2, java.sql.Timestamp.from(INSTANT_LD_FUTURE));
+          ps.executeUpdate();
+          conn.commit();
+
+          ResultSet rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(df, 'MONTH', 1) = DATE '2022-06-01 00:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN-1, MID_MIN
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(df, 'MONTH', 1) = DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN +1
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(df, 'MONTH', 1) > DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // FUTURE
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(df, 'MONTH', 1) >= DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN +1, FUTURE
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(df, 'MONTH', 1) < DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN
+          assertEquals(3, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(df, 'MONTH', 1) <= DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN, MID_MIN +1
+          assertEquals(4, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(df, 'MONTH', 1) <= DATE '2022-06-01 00:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN
+          assertEquals(3, rs.getInt(1));
+
+          //
+          // The same, but without range scan:
+          //
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(dfv, 'MONTH', 1) = DATE '2022-06-01 00:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN-1, MID_MIN
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(dfv, 'MONTH', 1) = DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN +1
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(dfv, 'MONTH', 1) > DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // FUTURE
+          assertEquals(1, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(dfv, 'MONTH', 1) >= DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // MID_MIN +1, FUTURE
+          assertEquals(2, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(dfv, 'MONTH', 1) < DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN
+          assertEquals(3, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(dfv, 'MONTH', 1) <= DATE '2022-07-01 00:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN, MID_MIN +1
+          assertEquals(4, rs.getInt(1));
+
+          rs =
+                  stmt.executeQuery("select count(*) " + " from " + tableName
+                          + " where ceil(dfv, 'MONTH', 1) <= DATE '2022-06-01 00:00:00'");
+          assertTrue(rs.next());
+          // PAST, MID_MIN-1, MID_MIN
+          assertEquals(3, rs.getInt(1));
+      }
+  }
+
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilMonthExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilMonthExpression.java
index 22daeb551b..e35a0f348b 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilMonthExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilMonthExpression.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
 import org.joda.time.DateTime;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -41,4 +42,16 @@ public class CeilMonthExpression extends RoundJodaDateExpression {
         return dateTime.monthOfYear().roundCeilingCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long time) {
+        // floor(time - 1) + 1
+        return (new DateTime(time - 1, GJChronology.getInstanceUTC())).monthOfYear()
+                .roundFloorCopy().getMillis() + 1;
+    }
+
+    @Override
+    public long rangeUpper(long time) {
+        // ceil
+        return roundDateTime(new DateTime(time, GJChronology.getInstanceUTC()));
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilWeekExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilWeekExpression.java
index 28da94f4f1..658eb402bb 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilWeekExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilWeekExpression.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
 import org.joda.time.DateTime;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -41,4 +42,16 @@ public class CeilWeekExpression extends RoundJodaDateExpression {
         return dateTime.weekOfWeekyear().roundCeilingCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long time) {
+        // floor(time - 1) + 1
+        return (new DateTime(time - 1, GJChronology.getInstanceUTC())).weekOfWeekyear()
+                .roundFloorCopy().getMillis() + 1;
+    }
+
+    @Override
+    public long rangeUpper(long time) {
+        // ceil
+        return roundDateTime(new DateTime(time, GJChronology.getInstanceUTC()));
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilYearExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilYearExpression.java
index b6e6b63a31..c09e56a31b 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilYearExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/CeilYearExpression.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
 import org.joda.time.DateTime;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -41,4 +42,16 @@ public class CeilYearExpression extends RoundJodaDateExpression {
        return dateTime.year().roundCeilingCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long time) {
+        // floor(time - 1) + 1
+        return (new DateTime(time - 1, GJChronology.getInstanceUTC())).year().roundFloorCopy()
+                .getMillis() + 1;
+    }
+
+    @Override
+    public long rangeUpper(long time) {
+        // ceil
+        return roundDateTime(new DateTime(time, GJChronology.getInstanceUTC()));
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorMonthExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorMonthExpression.java
index cf802055ba..1d5733d876 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorMonthExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorMonthExpression.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
 import org.joda.time.DateTime;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -41,4 +42,16 @@ public class FloorMonthExpression extends RoundJodaDateExpression {
         return datetime.monthOfYear().roundFloorCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long time) {
+        // floor
+        return roundDateTime(new DateTime(time, GJChronology.getInstanceUTC()));
+    }
+
+    @Override
+    public long rangeUpper(long time) {
+        // ceil(time + 1) -1
+        return (new DateTime(time + 1, GJChronology.getInstanceUTC())).monthOfYear()
+                .roundCeilingCopy().getMillis() - 1;
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorWeekExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorWeekExpression.java
index b6596c9f20..73cd048ae7 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorWeekExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorWeekExpression.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
 import org.joda.time.DateTime;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * Floor function that rounds up the {@link DateTime} to start of week. 
@@ -40,4 +41,16 @@ public class FloorWeekExpression extends RoundJodaDateExpression {
         return datetime.weekOfWeekyear().roundFloorCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long time) {
+        // floor
+        return roundDateTime(new DateTime(time, GJChronology.getInstanceUTC()));
+    }
+
+    @Override
+    public long rangeUpper(long time) {
+        // ceil(time + 1) -1
+        return (new DateTime(time + 1, GJChronology.getInstanceUTC())).weekOfWeekyear()
+                .roundCeilingCopy().getMillis() - 1;
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorYearExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorYearExpression.java
index 4ac8792b5f..77c926dfdc 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorYearExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/FloorYearExpression.java
@@ -21,6 +21,7 @@ import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
 import org.joda.time.DateTime;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -41,4 +42,16 @@ public class FloorYearExpression extends RoundJodaDateExpression {
         return datetime.year().roundFloorCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long time) {
+        // floor
+        return roundDateTime(new DateTime(time, GJChronology.getInstanceUTC()));
+    }
+
+    @Override
+    public long rangeUpper(long time) {
+        // ceil(time + 1) -1
+        return (new DateTime(time + 1, GJChronology.getInstanceUTC())).year().roundCeilingCopy()
+                .getMillis() - 1;
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundDateExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundDateExpression.java
index fb2c624211..3d628e08c1 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundDateExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundDateExpression.java
@@ -45,9 +45,9 @@ import org.apache.phoenix.schema.types.PDataType.PDataCodec;
 import org.apache.phoenix.schema.types.PDate;
 import org.apache.phoenix.schema.types.PInteger;
 import org.apache.phoenix.schema.types.PVarchar;
-import org.apache.phoenix.util.ByteUtil;
-
+import org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
 import org.apache.phoenix.thirdparty.com.google.common.collect.Lists;
+import org.apache.phoenix.util.ByteUtil;
 
 /**
  * Function used to bucketize date/time values by rounding them to
@@ -105,7 +105,7 @@ public class RoundDateExpression extends ScalarFunction {
     
     public static Expression create(List<Expression> children) throws SQLException {
         int numChildren = children.size();
-        if(numChildren < 2 || numChildren > 3) {
+        if (numChildren < 2 || numChildren > 3) {
             throw new IllegalArgumentException("Wrong number of arguments : " + numChildren);
         }
         Object timeUnitValue = ((LiteralExpression)children.get(1)).getValue();
@@ -138,7 +138,7 @@ public class RoundDateExpression extends ScalarFunction {
         Object multiplierValue = numChildren > 2 ? ((LiteralExpression)children.get(2)).getValue() : null;
         int multiplier = multiplierValue == null ? 1 :((Number)multiplierValue).intValue();
         TimeUnit timeUnit = TimeUnit.getTimeUnit(timeUnitValue != null ? timeUnitValue.toString() : null);
-        if(timeUnit.ordinal() < TIME_UNIT_MS.length) {
+        if (timeUnit.ordinal() < TIME_UNIT_MS.length) {
             divBy = multiplier * TIME_UNIT_MS[timeUnit.ordinal()];
         }
     }
@@ -148,8 +148,8 @@ public class RoundDateExpression extends ScalarFunction {
         return divBy/2;
     }
     
-    
-    protected long roundTime(long time) {
+    @VisibleForTesting
+    public long roundTime(long time) {
         long value;
         long roundUpAmount = getRoundUpAmount();
         if (time <= Long.MAX_VALUE - roundUpAmount) { // If no overflow, add
@@ -159,7 +159,21 @@ public class RoundDateExpression extends ScalarFunction {
         }
         return value * divBy;
     }
-    
+
+    @VisibleForTesting
+    public long rangeLower(long time) {
+        // This is for the ms based intervals. This needs to be separately implemented for the
+        // joda based intervals
+        return roundTime(time) - getRoundUpAmount();
+    }
+
+    @VisibleForTesting
+    public long rangeUpper(long time) {
+        // This is for the ms based intervals. This needs to be separately implemented for the
+        // joda based intervals
+        return roundTime(time) + (divBy - getRoundUpAmount()) - 1;
+    }
+
     @Override
     public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) {
         if (children.get(0).evaluate(tuple, ptr)) {
@@ -260,7 +274,8 @@ public class RoundDateExpression extends ScalarFunction {
                 PDataCodec codec = getKeyRangeCodec(type);
                 int offset = ByteUtil.isInclusive(op) ? 1 : 0;
                 long value = codec.decodeLong(key, 0, SortOrder.getDefault());
-                byte[] nextKey = new byte[type.getByteSize()];
+                byte[] lowerKey = new byte[type.getByteSize()];
+                byte[] upperKey = new byte[type.getByteSize()];
                 KeyRange range;
                 switch (op) {
                 case EQUAL:
@@ -269,21 +284,58 @@ public class RoundDateExpression extends ScalarFunction {
                     // had ROUND(dateCol,'DAY') = TO_DATE('2013-01-01 23:00:00')
                     // it could never be equal, since date constant isn't at a day
                     // boundary.
-                    if (value % divBy != 0) {
+                    if (value != roundTime(value)) {
                         return KeyRange.EMPTY_RANGE;
                     }
-                    codec.encodeLong(value + divBy, nextKey, 0);
-                    range = type.getKeyRange(key, true, nextKey, false);
+                    codec.encodeLong(rangeLower(value), lowerKey, 0);
+                    codec.encodeLong(rangeUpper(value), upperKey, 0);
+                    range = type.getKeyRange(lowerKey, true, upperKey, true);
                     break;
+                    // a simple number example (with half up rounding):
+                    // round(x) = 10 ==> [9.5, 10.5)
+                    // round(x) <= 10 ==> [-inf, 10.5)
+                    // round(x) <= 10.1 === round(x) <= 10 => [-inf, 10.5)
+                    // round(x) <= 9.9 === round(x) <= 9 => [-inf, 9.5)
+                    // round(x) < 10 ==> round(x) <= 9 ==> [-inf, 9.5)
                 case GREATER:
+                    if (value == roundTime(value)) {
+                        codec.encodeLong(rangeUpper(value), lowerKey, 0);
+                        range = type.getKeyRange(lowerKey, false, KeyRange.UNBOUND, false);
+                        break;
+                    }
+                    //fallthrough intended
                 case GREATER_OR_EQUAL:
-                    codec.encodeLong((value + divBy - offset)/divBy*divBy, nextKey, 0);
-                    range = type.getKeyRange(nextKey, true, KeyRange.UNBOUND, false);
+                    codec.encodeLong(rangeLower(value), lowerKey, 0);
+                    range = type.getKeyRange(lowerKey, true, KeyRange.UNBOUND, false);
+                    if (value <= roundTime(value)) {
+                        //always true for ceil
+                        codec.encodeLong(rangeLower(value), lowerKey, 0);
+                        range = type.getKeyRange(lowerKey, true, KeyRange.UNBOUND, false);
+                    } else {
+                        //always true for floor, except when exact
+                        codec.encodeLong(rangeUpper(value), lowerKey, 0);
+                        range = type.getKeyRange(lowerKey, false, KeyRange.UNBOUND, false);
+                    }
                     break;
                 case LESS:
+                    if (value == roundTime(value)) {
+                        codec.encodeLong(rangeLower(value), upperKey, 0);
+                        range = type.getKeyRange(KeyRange.UNBOUND, false, upperKey, false);
+                        break;
+                    }
+                    //fallthrough intended
                 case LESS_OR_EQUAL:
-                    codec.encodeLong((value + divBy - (1 -offset))/divBy*divBy, nextKey, 0);
-                    range = type.getKeyRange(KeyRange.UNBOUND, false, nextKey, false);
+                    codec.encodeLong(rangeUpper(value), upperKey, 0);
+                    range = type.getKeyRange(KeyRange.UNBOUND, false, upperKey, true);
+                    if (value >= roundTime(value)) {
+                        //always true for floor
+                        codec.encodeLong(rangeUpper(value), upperKey, 0);
+                        range = type.getKeyRange(KeyRange.UNBOUND, false, upperKey, true);
+                    } else {
+                        //always true for ceil, except when exact
+                        codec.encodeLong(rangeLower(value), upperKey, 0);
+                        range = type.getKeyRange(KeyRange.UNBOUND, false, upperKey, false);
+                    }
                     break;
                 default:
                     return childPart.getKeyRange(op, rhs);
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundJodaDateExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundJodaDateExpression.java
index aa9ef070a8..0a8ee678cf 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundJodaDateExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundJodaDateExpression.java
@@ -25,7 +25,7 @@ import org.apache.phoenix.expression.Expression;
 import org.apache.phoenix.schema.tuple.Tuple;
 import org.apache.phoenix.schema.types.PDataType;
 import org.joda.time.DateTime;
-import org.joda.time.chrono.ISOChronology;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -48,7 +48,7 @@ public abstract class RoundJodaDateExpression extends RoundDateExpression{
             }
             PDataType dataType = getDataType();
             long time = dataType.getCodec().decodeLong(ptr, children.get(0).getSortOrder());
-            DateTime dt = new DateTime(time, ISOChronology.getInstanceUTC());
+            DateTime dt = new DateTime(time, GJChronology.getInstanceUTC());
             long value = roundDateTime(dt);
             Date d = new Date(value);
             byte[] byteValue = dataType.toBytes(d);
@@ -63,4 +63,10 @@ public abstract class RoundJodaDateExpression extends RoundDateExpression{
      * @return Time in millis.
      */
     public abstract long roundDateTime(DateTime dateTime);
+
+    @Override
+    // We need a working roundTime() for the RowKey pushdown logic.
+    public long roundTime(long time) {
+        return roundDateTime(new DateTime(time, GJChronology.getInstanceUTC()));
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundMonthExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundMonthExpression.java
index 5ca21da5de..e333dcdd9e 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundMonthExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundMonthExpression.java
@@ -20,7 +20,10 @@ package org.apache.phoenix.expression.function;
 import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.util.DateUtil;
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeFieldType;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -39,4 +42,22 @@ public class RoundMonthExpression extends RoundJodaDateExpression {
        return dateTime.monthOfYear().roundHalfEvenCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long epochMs) {
+        // We're doing unnecessary conversions here, but this is not perf sensitive
+        DateTime rounded =
+                new DateTime(roundDateTime(new DateTime(epochMs, GJChronology.getInstanceUTC())),
+                        GJChronology.getInstanceUTC());
+        DateTime prev = rounded.minusMonths(1);
+        return DateUtil.rangeJodaHalfEven(rounded, prev, DateTimeFieldType.monthOfYear());
+    }
+
+    @Override
+    public long rangeUpper(long epochMs) {
+        DateTime rounded =
+                new DateTime(roundDateTime(new DateTime(epochMs, GJChronology.getInstanceUTC())),
+                        GJChronology.getInstanceUTC());
+        DateTime next = rounded.plusMonths(1);
+        return DateUtil.rangeJodaHalfEven(rounded, next, DateTimeFieldType.monthOfYear());
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundWeekExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundWeekExpression.java
index 0572e19f7e..e66f580cf0 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundWeekExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundWeekExpression.java
@@ -20,7 +20,10 @@ package org.apache.phoenix.expression.function;
 import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.util.DateUtil;
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeFieldType;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -38,4 +41,23 @@ public class RoundWeekExpression extends RoundJodaDateExpression {
     public long roundDateTime(DateTime dateTime) {
        return dateTime.weekOfWeekyear().roundHalfEvenCopy().getMillis();
     }
+
+    @Override
+    public long rangeLower(long epochMs) {
+        // We're doing unnecessary conversions here, but this is NOT perf sensitive
+        DateTime rounded =
+                new DateTime(roundDateTime(new DateTime(epochMs, GJChronology.getInstanceUTC())),
+                        GJChronology.getInstanceUTC());
+        DateTime prev = rounded.minusWeeks(1);
+        return DateUtil.rangeJodaHalfEven(rounded, prev, DateTimeFieldType.weekOfWeekyear());
+    }
+
+    @Override
+    public long rangeUpper(long epochMs) {
+        DateTime rounded =
+                new DateTime(roundDateTime(new DateTime(epochMs, GJChronology.getInstanceUTC())),
+                        GJChronology.getInstanceUTC());
+        DateTime next = rounded.plusWeeks(1);
+        return DateUtil.rangeJodaHalfEven(rounded, next, DateTimeFieldType.weekOfWeekyear());
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundYearExpression.java b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundYearExpression.java
index b73d05ac76..8a99d7b20d 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundYearExpression.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/expression/function/RoundYearExpression.java
@@ -20,7 +20,10 @@ package org.apache.phoenix.expression.function;
 import java.util.List;
 
 import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.util.DateUtil;
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeFieldType;
+import org.joda.time.chrono.GJChronology;
 
 /**
  * 
@@ -39,4 +42,22 @@ public class RoundYearExpression extends RoundJodaDateExpression {
         return dateTime.year().roundHalfEvenCopy().getMillis();
     }
 
+    @Override
+    public long rangeLower(long epochMs) {
+        // We're doing unnecessary conversions here, but this is NOT perf sensitive
+        DateTime rounded =
+                new DateTime(roundDateTime(new DateTime(epochMs, GJChronology.getInstanceUTC())),
+                        GJChronology.getInstanceUTC());
+        DateTime prev = rounded.minusYears(1);
+        return DateUtil.rangeJodaHalfEven(rounded, prev, DateTimeFieldType.year());
+    }
+
+    @Override
+    public long rangeUpper(long epochMs) {
+        DateTime rounded =
+                new DateTime(roundDateTime(new DateTime(epochMs, GJChronology.getInstanceUTC())),
+                        GJChronology.getInstanceUTC());
+        DateTime next = rounded.plusYears(1);
+        return DateUtil.rangeJodaHalfEven(rounded, next, DateTimeFieldType.year());
+    }
 }
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/util/DateUtil.java b/phoenix-core/src/main/java/org/apache/phoenix/util/DateUtil.java
index cc16eb6c30..9122f6195b 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/util/DateUtil.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/util/DateUtil.java
@@ -40,16 +40,15 @@ import org.apache.phoenix.schema.types.PDate;
 import org.apache.phoenix.schema.types.PTimestamp;
 import org.apache.phoenix.schema.types.PUnsignedDate;
 import org.apache.phoenix.schema.types.PUnsignedTimestamp;
+import org.apache.phoenix.thirdparty.com.google.common.collect.Lists;
 import org.joda.time.DateTime;
+import org.joda.time.DateTimeFieldType;
 import org.joda.time.DateTimeZone;
-import org.joda.time.chrono.ISOChronology;
 import org.joda.time.chrono.GJChronology;
 import org.joda.time.format.DateTimeFormatter;
 import org.joda.time.format.DateTimeFormatterBuilder;
 import org.joda.time.format.ISODateTimeFormat;
 
-import org.apache.phoenix.thirdparty.com.google.common.collect.Lists;
-
 import edu.umd.cs.findbugs.annotations.NonNull;
 
 
@@ -155,7 +154,7 @@ public class DateUtil {
         if (pattern == null || pattern.length() == 0) {
             pattern = defaultPattern;
         }
-        if(defaultPattern.equals(pattern)) {
+        if (defaultPattern.equals(pattern)) {
             return JulianDateFormatParserFactory.getParser(timeZone);
         } else {
             return new SimpleDateFormatParser(pattern, timeZone);
@@ -207,7 +206,7 @@ public class DateUtil {
             String nanosStr = timestampValue.substring(period + 1);
             if (nanosStr.length() > 9)
                 throw new IllegalDataException("nanos > 999999999 or < 0");
-            if(nanosStr.length() > 3 ) {
+            if (nanosStr.length() > 3) {
                 int nanos = Integer.parseInt(nanosStr);
                 for (int i = 0; i < 9 - nanosStr.length(); i++) {
                     nanos *= 10;
@@ -347,4 +346,50 @@ public class DateUtil {
             return formatter.getZone().toTimeZone();
         }
     }
+
+    public static long rangeJodaHalfEven(DateTime roundedDT, DateTime otherDT,
+            DateTimeFieldType type) {
+        // It's OK if this is slow, as it's only called O(1) times per query
+        //
+        // We need to reverse engineer what roundHalfEvenCopy() does
+        // and return the lower/upper (inclusive) range here
+        // Joda simply works on milliseconds between the floor and ceil values.
+        // We could avoid the period call for units less than a day, but this is not a perf
+        // critical function.
+        long roundedMs = roundedDT.getMillis();
+        long otherMs = otherDT.getMillis();
+        long midMs = (roundedMs + otherMs) / 2;
+        long remainder = (roundedMs + otherMs) % 2;
+        if (remainder == 0) {
+            int roundedUnits = roundedDT.get(type);
+            if (otherMs > roundedMs) {
+                // Upper range, other is bigger.
+                if ((roundedUnits & 1) == 0) {
+                    // This unit is even, the next second is odd, so we get the mid point
+                    return midMs;
+                } else {
+                    // This unit is odd, the next second is even and takes the midpoint.
+                    return midMs - 1;
+                }
+            } else {
+                // Lower range, other is smaller.
+                if ((roundedUnits & 1) == 0) {
+                    // This unit is even, the next second is odd, so we get the mid point
+                    return midMs;
+                } else {
+                    // This unit is odd, the next second is even and takes the midpoint.
+                    return midMs + 1;
+                }
+            }
+        } else {
+            // probably never happens
+            if (otherMs > roundedMs) {
+                // Upper range, return the rounded down value
+                return midMs;
+            } else {
+                // Lower range, the mid value belongs to the previous unit.
+                return midMs + 1;
+            }
+        }
+    }
 }
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/compile/WhereOptimizerTest.java b/phoenix-core/src/test/java/org/apache/phoenix/compile/WhereOptimizerTest.java
index 0b9c1ee76b..e9aca1a3f5 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/compile/WhereOptimizerTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/compile/WhereOptimizerTest.java
@@ -409,10 +409,11 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
     public void testEqualRound() throws Exception {
         String inst = "a";
         String host = "b";
-        Date startDate = DateUtil.parseDate("2012-01-01 00:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00");
+        Date roundDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date startDate = DateUtil.parseDate("2011-12-31 12:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-01 12:00:00"); //Hbase normalizes scans to left closed
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')=?";
-        List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
         Scan scan = compileStatement(query, binds).getScan();
         assertNull(scan.getFilter());
 
@@ -420,10 +421,12 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PDate.INSTANCE.toBytes(startDate));
         assertArrayEquals(startRow, scan.getStartRow());
+        assertTrue(scan.includeStartRow());
         byte[] stopRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PDate.INSTANCE.toBytes(endDate));
         assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
     }
 
     @Test
@@ -441,17 +444,18 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
     public void testBoundaryGreaterThanRound() throws Exception {
         String inst = "a";
         String host = "b";
-        Date startDate = DateUtil.parseDate("2012-01-01 00:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00");
+        Date roundDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date startDate = DateUtil.parseDate("2012-01-01 12:00:00");
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')>?";
-        List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
         Scan scan = compileStatement(query, binds).getScan();
 
         assertNull(scan.getFilter());
         byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
-                PDate.INSTANCE.toBytes(endDate));
+                PDate.INSTANCE.toBytes(startDate));
         assertArrayEquals(startRow, scan.getStartRow());
+        assertTrue(scan.includeStartRow());
         byte[] stopRow = ByteUtil.nextKey(ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY));
         assertArrayEquals(stopRow, scan.getStopRow());
@@ -462,7 +466,7 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
         String inst = "a";
         String host = "b";
         Date startDate = DateUtil.parseDate("2012-01-01 00:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date startDateHalfRange = DateUtil.parseDate("2011-12-31 12:00:00.000");
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')>=?";
         List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
         Scan scan = compileStatement(query, binds).getScan();
@@ -470,7 +474,7 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
 
         byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
-                PDate.INSTANCE.toBytes(endDate));
+                PDate.INSTANCE.toBytes(startDateHalfRange));
         assertArrayEquals(startRow, scan.getStartRow());
         byte[] stopRow = ByteUtil.nextKey(ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY));
@@ -481,17 +485,19 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
     public void testGreaterThanRound() throws Exception {
         String inst = "a";
         String host = "b";
-        Date startDate = DateUtil.parseDate("2012-01-01 01:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00");
+        Date roundDate = DateUtil.parseDate("2012-01-01 01:00:00");
+        Date startDate = DateUtil.parseDate("2012-01-01 12:00:00"); //Hbase normalizes scans to left closed
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')>?";
-        List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
+
         Scan scan = compileStatement(query, binds).getScan();
         assertNull(scan.getFilter());
 
         byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
-                PDate.INSTANCE.toBytes(endDate));
+                PDate.INSTANCE.toBytes(startDate));
         assertArrayEquals(startRow, scan.getStartRow());
+        assertTrue(scan.includeStartRow());
         byte[] stopRow = ByteUtil.nextKey(ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY));
         assertArrayEquals(stopRow, scan.getStopRow());
@@ -501,10 +507,10 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
     public void testLessThanRound() throws Exception {
         String inst = "a";
         String host = "b";
-        Date startDate = DateUtil.parseDate("2012-01-01 01:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00");
+        Date roundDate = DateUtil.parseDate("2012-01-01 01:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-01 12:00:00"); //Hbase normalizes scans to left closed
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')<?";
-        List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
         Scan scan = compileStatement(query, binds).getScan();
 
         assertNull(scan.getFilter());
@@ -515,16 +521,18 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PDate.INSTANCE.toBytes(endDate));
         assertArrayEquals(stopRow, scan.getStopRow());
+        assertTrue(scan.includeStartRow());
     }
 
     @Test
     public void testBoundaryLessThanRound() throws Exception {
         String inst = "a";
         String host = "b";
-        Date startDate = DateUtil.parseDate("2012-01-01 00:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date roundDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date endDate = DateUtil.parseDate("2011-12-31 12:00:00"); //Hbase normalizes scans to left closed
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')<?";
-        List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
+
         Scan scan = compileStatement(query, binds).getScan();
         assertNull(scan.getFilter());
 
@@ -535,16 +543,17 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PDate.INSTANCE.toBytes(endDate));
         assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
     }
 
     @Test
     public void testLessThanOrEqualRound() throws Exception {
         String inst = "a";
         String host = "b";
-        Date startDate = DateUtil.parseDate("2012-01-01 01:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00");
+        Date roundDate = DateUtil.parseDate("2012-01-01 01:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-01 12:00:00"); //Hbase normalizes scans to left closed
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')<=?";
-        List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
         Scan scan = compileStatement(query, binds).getScan();
         assertNull(scan.getFilter());
 
@@ -555,19 +564,147 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PDate.INSTANCE.toBytes(endDate));
         assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testLessThanOrEqualRound2() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date roundDate = DateUtil.parseDate("2011-12-31 23:00:00");
+        Date endDate = DateUtil.parseDate("2011-12-31 12:00:00"); //Hbase normalizes scans to left closed
+        String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')<=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
+        Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host)/*,QueryConstants.SEPARATOR_BYTE_ARRAY*/);
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PDate.INSTANCE.toBytes(endDate));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
     }
 
     @Test
     public void testBoundaryLessThanOrEqualRound() throws Exception {
         String inst = "a";
         String host = "b";
-        Date startDate = DateUtil.parseDate("2012-01-01 00:00:00");
-        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00");
+        Date roundDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-01 12:00:00"); //Hbase normalizes scans to left closed
         String query = "select * from ptsdb where inst=? and host=? and round(date,'DAY')<=?";
-        List<Object> binds = Arrays.<Object>asList(inst,host,startDate);
+        List<Object> binds = Arrays.<Object>asList(inst,host,roundDate);
+        Scan scan = compileStatement(query, binds).getScan();
+
+        assertNull(scan.getFilter());
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host)/*,QueryConstants.SEPARATOR_BYTE_ARRAY*/);
+        assertArrayEquals(startRow, scan.getStartRow());
+        assertTrue(scan.includeStartRow());
+        byte[] stopRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PDate.INSTANCE.toBytes(endDate));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testLessThanOrEqualFloor() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date floorDate = DateUtil.parseDate("2012-01-01 01:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00"); //Hbase normalizes scans to left closed
+        String query = "select * from ptsdb where inst=? and host=? and floor(date,'DAY')<=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,floorDate);
+        Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host)/*,QueryConstants.SEPARATOR_BYTE_ARRAY*/);
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PDate.INSTANCE.toBytes(endDate));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testLessThanOrEqualFloorBoundary() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date floorDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-02 00:00:00"); //Hbase normalizes scans to left closed
+        String query = "select * from ptsdb where inst=? and host=? and floor(date,'DAY')<=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,floorDate);
+        Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host)/*,QueryConstants.SEPARATOR_BYTE_ARRAY*/);
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PDate.INSTANCE.toBytes(endDate));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testGreaterThanOrEqualFloor() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date floorDate = DateUtil.parseDate("2012-01-01 01:00:00");
+        Date startDate = DateUtil.parseDate("2012-01-02 00:00:00");
+        String query = "select * from ptsdb where inst=? and host=? and floor(date,'DAY')>=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,floorDate);
         Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PDate.INSTANCE.toBytes(startDate));
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.nextKey(ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
 
+    @Test
+    public void testGreaterThanOrEqualFloorBoundary() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date floorDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date startDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        String query = "select * from ptsdb where inst=? and host=? and floor(date,'DAY')>=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,floorDate);
+        Scan scan = compileStatement(query, binds).getScan();
         assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+            PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+            PDate.INSTANCE.toBytes(startDate));
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.nextKey(ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testLessThanOrEqualCeil() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date ceilDate = DateUtil.parseDate("2012-01-01 01:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-01 00:00:00.001"); //Hbase normalizes scans to left closed
+        String query = "select * from ptsdb where inst=? and host=? and ceil(date,'DAY')<=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,ceilDate);
+        Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
         byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PVarchar.INSTANCE.toBytes(host)/*,QueryConstants.SEPARATOR_BYTE_ARRAY*/);
         assertArrayEquals(startRow, scan.getStartRow());
@@ -575,6 +712,70 @@ public class WhereOptimizerTest extends BaseConnectionlessQueryTest {
                 PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
                 PDate.INSTANCE.toBytes(endDate));
         assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testLessThanOrEqualCeilBoundary() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date ceilDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date endDate = DateUtil.parseDate("2012-01-01 00:00:00.001"); //Hbase normalizes scans to left closed
+        String query = "select * from ptsdb where inst=? and host=? and ceil(date,'DAY')<=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,ceilDate);
+        Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host)/*,QueryConstants.SEPARATOR_BYTE_ARRAY*/);
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PDate.INSTANCE.toBytes(endDate));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testGreaterThanOrEqualCeil() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date ceilDate = DateUtil.parseDate("2012-01-01 01:00:00");
+        Date startDate = DateUtil.parseDate("2012-01-01 00:00:00.001");
+        String query = "select * from ptsdb where inst=? and host=? and ceil(date,'DAY')>=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,ceilDate);
+        Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PDate.INSTANCE.toBytes(startDate));
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.nextKey(ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
+    }
+
+    @Test
+    public void testGreaterThanOrEqualCeilBoundary() throws Exception {
+        String inst = "a";
+        String host = "b";
+        Date ceilDate = DateUtil.parseDate("2012-01-01 00:00:00");
+        Date startDate = DateUtil.parseDate("2011-12-31 00:00:00.001");
+        String query = "select * from ptsdb where inst=? and host=? and ceil(date,'DAY')>=?";
+        List<Object> binds = Arrays.<Object>asList(inst,host,ceilDate);
+        Scan scan = compileStatement(query, binds).getScan();
+        assertNull(scan.getFilter());
+
+        byte[] startRow = ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+            PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY,
+            PDate.INSTANCE.toBytes(startDate));
+        assertArrayEquals(startRow, scan.getStartRow());
+        byte[] stopRow = ByteUtil.nextKey(ByteUtil.concat(PVarchar.INSTANCE.toBytes(inst),QueryConstants.SEPARATOR_BYTE_ARRAY,
+                PVarchar.INSTANCE.toBytes(host),QueryConstants.SEPARATOR_BYTE_ARRAY));
+        assertArrayEquals(stopRow, scan.getStopRow());
+        assertFalse(scan.includeStopRow());
     }
 
     @Test
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/expression/RoundFloorCeilExpressionsTest.java b/phoenix-core/src/test/java/org/apache/phoenix/expression/RoundFloorCeilExpressionsTest.java
index e652c69c25..4c81ab3fd9 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/expression/RoundFloorCeilExpressionsTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/expression/RoundFloorCeilExpressionsTest.java
@@ -20,6 +20,7 @@ package org.apache.phoenix.expression;
 import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -37,10 +38,19 @@ import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
 import org.apache.phoenix.compile.KeyPart;
 import org.apache.phoenix.expression.function.CeilDateExpression;
 import org.apache.phoenix.expression.function.CeilDecimalExpression;
+import org.apache.phoenix.expression.function.CeilMonthExpression;
+import org.apache.phoenix.expression.function.CeilWeekExpression;
+import org.apache.phoenix.expression.function.CeilYearExpression;
 import org.apache.phoenix.expression.function.FloorDateExpression;
 import org.apache.phoenix.expression.function.FloorDecimalExpression;
+import org.apache.phoenix.expression.function.FloorMonthExpression;
+import org.apache.phoenix.expression.function.FloorWeekExpression;
+import org.apache.phoenix.expression.function.FloorYearExpression;
 import org.apache.phoenix.expression.function.RoundDateExpression;
 import org.apache.phoenix.expression.function.RoundDecimalExpression;
+import org.apache.phoenix.expression.function.RoundMonthExpression;
+import org.apache.phoenix.expression.function.RoundWeekExpression;
+import org.apache.phoenix.expression.function.RoundYearExpression;
 import org.apache.phoenix.expression.function.ScalarFunction;
 import org.apache.phoenix.expression.function.TimeUnit;
 import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
@@ -58,6 +68,8 @@ import org.apache.phoenix.schema.types.PInteger;
 import org.apache.phoenix.schema.types.PVarchar;
 import org.apache.phoenix.util.DateUtil;
 import org.apache.phoenix.util.PropertiesUtil;
+import org.joda.time.Chronology;
+import org.joda.time.chrono.GJChronology;
 import org.junit.Test;
 
 /**
@@ -70,6 +82,26 @@ import org.junit.Test;
  */
 public class RoundFloorCeilExpressionsTest extends BaseConnectionlessQueryTest {
 
+
+    private static long HALF_SEC = 500;
+    private static long SEC = 2 * HALF_SEC;
+
+    private static long HALF_MIN = 30 * 1000;
+    private static long MIN = 2 * HALF_MIN;
+
+    private static long HALF_HOUR = 30 * 60 * 1000;
+    private static long HOUR = 2 * HALF_HOUR;
+
+    private static long HALF_DAY = 12 * 60 * 60 * 1000;
+    private static long DAY = 2 * HALF_DAY;
+
+    private static long HALF_WEEK = 7 * 12 * 60 * 60 * 1000;
+    private static long WEEK = 2 * HALF_WEEK;
+
+    // Note that without the "l" the integer arithmetic below would overflow
+    private static long HALF_YEAR = 365l * 12 * 60 * 60 * 1000;
+    private static long YEAR = 2l * HALF_YEAR;
+
     // Decimal Expression Tests
 
     @Test
@@ -688,4 +720,1207 @@ public class RoundFloorCeilExpressionsTest extends BaseConnectionlessQueryTest {
         Date resultDate = (Date)result;
         assertEquals(DateUtil.parseDate("2016-01-04 00:00:00"), resultDate);
     }
+
+    private RoundDateExpression getRoundMsExpression(String s, TimeUnit u, int m) throws SQLException {
+        return (RoundDateExpression)RoundDateExpression.create(LiteralExpression.newConstant(s), u, m );
+    }
+
+    // The three tests below are backported from PHOENIX-5066.
+    // When PHOENIX-5066 lands, these can be removed as redundant.
+
+    @Test
+    public void testRoundingGMT() throws SQLException {
+        // We operate on Instants for time units up to Days, simply counting millis
+
+        RoundDateExpression oddWholeSecondExp =
+                getRoundMsExpression("2022-11-11 11:11:11", TimeUnit.SECOND, 1);
+        java.sql.Timestamp oddWholeSecond =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:11").getTime());
+        long lowerBoundaryOddWholeSecond = oddWholeSecond.getTime() - HALF_SEC;
+        long upperBoundaryOddWholeSecond = oddWholeSecond.getTime() + HALF_SEC - 1;
+        assertEquals(lowerBoundaryOddWholeSecond,
+            oddWholeSecondExp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecond,
+            oddWholeSecondExp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(lowerBoundaryOddWholeSecond)));
+        assertNotEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(lowerBoundaryOddWholeSecond - 1)));
+        assertEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(upperBoundaryOddWholeSecond)));
+        assertNotEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(upperBoundaryOddWholeSecond + 1)));
+
+        // 10 sec range
+        RoundDateExpression oddWholeSecondRound10Exp =
+                getRoundMsExpression("2022-11-11 11:11:10", TimeUnit.SECOND, 10);
+        java.sql.Timestamp oddWholeSecondRound10 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:10").getTime());
+        long lowerBoundaryOddWholeSecondRound10 = oddWholeSecondRound10.getTime() - 5 * SEC;
+        long upperBoundaryOddWholeSecondRound10 = oddWholeSecondRound10.getTime() + 5 * SEC - 1;
+        assertEquals(lowerBoundaryOddWholeSecondRound10,
+            oddWholeSecondRound10Exp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecondRound10,
+            oddWholeSecondRound10Exp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecondRound10, new java.sql.Timestamp(
+                oddWholeSecondRound10Exp.roundTime(lowerBoundaryOddWholeSecondRound10)));
+        assertNotEquals(oddWholeSecondRound10, new java.sql.Timestamp(
+                oddWholeSecondRound10Exp.roundTime(lowerBoundaryOddWholeSecondRound10 - 1)));
+        assertEquals(oddWholeSecondRound10, new java.sql.Timestamp(
+                oddWholeSecondRound10Exp.roundTime(upperBoundaryOddWholeSecondRound10)));
+        assertNotEquals(oddWholeSecondRound10, new java.sql.Timestamp(
+                oddWholeSecondRound10Exp.roundTime(upperBoundaryOddWholeSecondRound10 + 1)));
+
+        // 15 sec range
+        RoundDateExpression oddWholeSecondRound15Exp =
+                getRoundMsExpression("2022-11-11 11:11:15", TimeUnit.SECOND, 15);
+        java.sql.Timestamp oddWholeSecondRound15 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:15").getTime());
+        long lowerBoundaryOddWholeSecondRound15 = oddWholeSecondRound15.getTime() - 15 * HALF_SEC;
+        long upperBoundaryOddWholeSecondRound15 =
+                oddWholeSecondRound15.getTime() + 15 * HALF_SEC - 1;
+        assertEquals(lowerBoundaryOddWholeSecondRound15,
+            oddWholeSecondRound15Exp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecondRound15,
+            oddWholeSecondRound15Exp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecondRound15, new java.sql.Timestamp(
+                oddWholeSecondRound15Exp.roundTime(lowerBoundaryOddWholeSecondRound15)));
+        assertNotEquals(oddWholeSecondRound15, new java.sql.Timestamp(
+                oddWholeSecondRound15Exp.roundTime(lowerBoundaryOddWholeSecondRound15 - 1)));
+        assertEquals(oddWholeSecondRound15, new java.sql.Timestamp(
+                oddWholeSecondRound15Exp.roundTime(upperBoundaryOddWholeSecondRound15)));
+        assertNotEquals(oddWholeSecondRound15, new java.sql.Timestamp(
+                oddWholeSecondRound15Exp.roundTime(upperBoundaryOddWholeSecondRound15 + 1)));
+
+        RoundDateExpression evenWholeSecondExp =
+                getRoundMsExpression("2022-11-11 11:11:12", TimeUnit.SECOND, 1);
+        java.sql.Timestamp evenWholeSecond =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:12").getTime());
+        long lowerBoundaryEvenWholeSecond = evenWholeSecond.getTime() - HALF_SEC;
+        long upperBoundaryEvenWholeSecond = evenWholeSecond.getTime() + HALF_SEC - 1;
+        assertEquals(lowerBoundaryEvenWholeSecond,
+            evenWholeSecondExp.rangeLower(evenWholeSecond.getTime()));
+        assertEquals(upperBoundaryEvenWholeSecond,
+            evenWholeSecondExp.rangeUpper(evenWholeSecond.getTime()));
+        assertEquals(evenWholeSecond,
+            new java.sql.Timestamp(evenWholeSecondExp.roundTime(lowerBoundaryEvenWholeSecond)));
+        assertNotEquals(evenWholeSecond,
+            new java.sql.Timestamp(evenWholeSecondExp.roundTime(lowerBoundaryEvenWholeSecond - 1)));
+        assertEquals(evenWholeSecond,
+            new java.sql.Timestamp(evenWholeSecondExp.roundTime(upperBoundaryEvenWholeSecond)));
+        assertNotEquals(evenWholeSecond,
+            new java.sql.Timestamp(evenWholeSecondExp.roundTime(upperBoundaryEvenWholeSecond + 1)));
+
+        RoundDateExpression oddWholeMinuteExp =
+                getRoundMsExpression("2022-11-11 11:11:0", TimeUnit.MINUTE, 1);
+        java.sql.Timestamp oddWholeMinute =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:0").getTime());
+        long lowerBoundaryOddWholeMinute = oddWholeMinute.getTime() - HALF_MIN;
+        long upperBoundaryOddWholeMinute = oddWholeMinute.getTime() + HALF_MIN - 1;
+        assertEquals(lowerBoundaryOddWholeMinute,
+            oddWholeMinuteExp.rangeLower(oddWholeMinute.getTime()));
+        assertEquals(upperBoundaryOddWholeMinute,
+            oddWholeMinuteExp.rangeUpper(oddWholeMinute.getTime()));
+        assertEquals(oddWholeMinute,
+            new java.sql.Timestamp(oddWholeMinuteExp.roundTime(lowerBoundaryOddWholeMinute)));
+        assertNotEquals(oddWholeMinute,
+            new java.sql.Timestamp(oddWholeMinuteExp.roundTime(lowerBoundaryOddWholeMinute - 1)));
+        assertEquals(oddWholeMinute,
+            new java.sql.Timestamp(oddWholeMinuteExp.roundTime(upperBoundaryOddWholeMinute)));
+        assertNotEquals(oddWholeMinute,
+            new java.sql.Timestamp(oddWholeMinuteExp.roundTime(upperBoundaryOddWholeMinute + 1)));
+
+        RoundDateExpression oddWholeMinuteRound20Exp =
+                getRoundMsExpression("2022-11-11 11:20:0", TimeUnit.MINUTE, 20);
+        java.sql.Timestamp oddWholeMinuteRound20 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:20:0").getTime());
+        long lowerBoundaryOddWholeMinute20 = oddWholeMinuteRound20.getTime() - 10 * MIN;
+        long upperBoundaryOddWholeMinute20 = oddWholeMinuteRound20.getTime() + 10 * MIN - 1;
+        assertEquals(lowerBoundaryOddWholeMinute20,
+            oddWholeMinuteRound20Exp.rangeLower(oddWholeMinute.getTime()));
+        assertEquals(upperBoundaryOddWholeMinute20,
+            oddWholeMinuteRound20Exp.rangeUpper(oddWholeMinute.getTime()));
+        assertEquals(oddWholeMinuteRound20, new java.sql.Timestamp(
+                oddWholeMinuteRound20Exp.roundTime(lowerBoundaryOddWholeMinute20)));
+        assertNotEquals(oddWholeMinuteRound20, new java.sql.Timestamp(
+                oddWholeMinuteRound20Exp.roundTime(lowerBoundaryOddWholeMinute20 - 1)));
+        assertEquals(oddWholeMinuteRound20, new java.sql.Timestamp(
+                oddWholeMinuteRound20Exp.roundTime(upperBoundaryOddWholeMinute20)));
+        assertNotEquals(oddWholeMinuteRound20, new java.sql.Timestamp(
+                oddWholeMinuteRound20Exp.roundTime(upperBoundaryOddWholeMinute20 + 1)));
+
+        // Minutes since epoch, don't expect the rounded value to be "round"
+
+        RoundDateExpression oddWholeMinuteRound17Exp =
+                getRoundMsExpression("2022-11-11 11:12:0", TimeUnit.MINUTE, 17);
+        java.sql.Timestamp oddWholeMinuteRound17 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:12:00").getTime());
+        long lowerBoundaryOddWholeMinute17 = oddWholeMinuteRound17.getTime() - 17 * HALF_MIN;
+        long upperBoundaryOddWholeMinute17 = oddWholeMinuteRound17.getTime() + 17 * HALF_MIN - 1;
+        assertEquals(lowerBoundaryOddWholeMinute17,
+            oddWholeMinuteRound17Exp.rangeLower(oddWholeMinute.getTime()));
+        assertEquals(upperBoundaryOddWholeMinute17,
+            oddWholeMinuteRound17Exp.rangeUpper(oddWholeMinute.getTime()));
+        assertEquals(oddWholeMinuteRound17, new java.sql.Timestamp(
+                oddWholeMinuteRound17Exp.roundTime(lowerBoundaryOddWholeMinute17)));
+        assertNotEquals(oddWholeMinuteRound17, new java.sql.Timestamp(
+                oddWholeMinuteRound17Exp.roundTime(lowerBoundaryOddWholeMinute17 - 1)));
+        assertEquals(oddWholeMinuteRound17, new java.sql.Timestamp(
+                oddWholeMinuteRound17Exp.roundTime(upperBoundaryOddWholeMinute17)));
+        assertNotEquals(oddWholeMinuteRound17, new java.sql.Timestamp(
+                oddWholeMinuteRound17Exp.roundTime(upperBoundaryOddWholeMinute17 + 1)));
+
+        RoundDateExpression evenWholeMinuteExp =
+                getRoundMsExpression("2022-11-11 11:12:0", TimeUnit.MINUTE, 1);
+        java.sql.Timestamp evenWholeMinute =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:12:0").getTime());
+        long lowerBoundaryEvenWholeMinute = evenWholeMinute.getTime() - HALF_MIN;
+        long upperBoundaryEvenWholeMinute = evenWholeMinute.getTime() + HALF_MIN - 1;
+        assertEquals(lowerBoundaryEvenWholeMinute,
+            evenWholeMinuteExp.rangeLower(evenWholeMinute.getTime()));
+        assertEquals(upperBoundaryEvenWholeMinute,
+            evenWholeMinuteExp.rangeUpper(evenWholeMinute.getTime()));
+        assertEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(lowerBoundaryEvenWholeMinute)));
+        assertNotEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(lowerBoundaryEvenWholeMinute - 1)));
+        assertEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(upperBoundaryEvenWholeMinute)));
+        assertNotEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(upperBoundaryEvenWholeMinute + 1)));
+
+        RoundDateExpression oddWholeHourExp =
+                getRoundMsExpression("2022-11-11 11:0:0", TimeUnit.HOUR, 1);
+        java.sql.Timestamp oddWholeHour =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:0:0").getTime());
+        long lowerBoundaryOddWholeHour = oddWholeHour.getTime() - HALF_HOUR;
+        long upperBoundaryOddWholeHour = oddWholeHour.getTime() + HALF_HOUR - 1;
+        assertEquals(lowerBoundaryOddWholeHour, oddWholeHourExp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour, oddWholeHourExp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(lowerBoundaryOddWholeHour)));
+        assertNotEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(lowerBoundaryOddWholeHour - 1)));
+        assertEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(upperBoundaryOddWholeHour)));
+        assertNotEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(upperBoundaryOddWholeHour + 1)));
+
+        // Not rounding to hourOfDay
+        RoundDateExpression oddWholeHour10Exp =
+                getRoundMsExpression("2022-11-11 12:0:0", TimeUnit.HOUR, 10);
+        java.sql.Timestamp oddWholeHour10 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 12:0:0").getTime());
+        long lowerBoundaryOddWholeHour10 = oddWholeHour10.getTime() - HALF_HOUR * 10;
+        long upperBoundaryOddWholeHour10 = oddWholeHour10.getTime() + HALF_HOUR * 10 - 1;
+        assertEquals(lowerBoundaryOddWholeHour10,
+            oddWholeHour10Exp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour10,
+            oddWholeHour10Exp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(lowerBoundaryOddWholeHour10)));
+        assertNotEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(lowerBoundaryOddWholeHour10 - 1)));
+        assertEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(upperBoundaryOddWholeHour10)));
+        assertNotEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(upperBoundaryOddWholeHour10 + 1)));
+
+        // Not rounding to hourOfDay
+        RoundDateExpression oddWholeHour11Exp =
+                getRoundMsExpression("2022-11-11 07:0:0", TimeUnit.HOUR, 11);
+        java.sql.Timestamp oddWholeHour11 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 07:0:0").getTime());
+        long lowerBoundaryOddWholeHour11 = oddWholeHour11.getTime() - HALF_HOUR * 11;
+        long upperBoundaryOddWholeHour11 = oddWholeHour11.getTime() + HALF_HOUR * 11 - 1;
+        assertEquals(lowerBoundaryOddWholeHour11,
+            oddWholeHour11Exp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour11,
+            oddWholeHour11Exp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(lowerBoundaryOddWholeHour11)));
+        assertNotEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(lowerBoundaryOddWholeHour11 - 1)));
+        assertEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(upperBoundaryOddWholeHour11)));
+        assertNotEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(upperBoundaryOddWholeHour11 + 1)));
+
+        RoundDateExpression evenwholeHourExp =
+                getRoundMsExpression("2022-11-11 12:0:0", TimeUnit.HOUR, 1);
+        java.sql.Timestamp evenwholeHour =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 12:0:0").getTime());
+        long lowerBoundaryEvenWholeHour = evenwholeHour.getTime() - HALF_HOUR;
+        long upperBoundaryEvenWholeHour = evenwholeHour.getTime() + HALF_HOUR - 1;
+        assertEquals(lowerBoundaryEvenWholeHour,
+            evenwholeHourExp.rangeLower(evenwholeHour.getTime()));
+        assertEquals(upperBoundaryEvenWholeHour,
+            evenwholeHourExp.rangeUpper(evenwholeHour.getTime()));
+        assertEquals(evenwholeHour,
+            new java.sql.Timestamp(evenwholeHourExp.roundTime(lowerBoundaryEvenWholeHour)));
+        assertNotEquals(evenwholeHour,
+            new java.sql.Timestamp(evenwholeHourExp.roundTime(lowerBoundaryEvenWholeHour - 1)));
+        assertEquals(evenwholeHour,
+            new java.sql.Timestamp(evenwholeHourExp.roundTime(upperBoundaryEvenWholeHour)));
+        assertNotEquals(evenwholeHour,
+            new java.sql.Timestamp(evenwholeHourExp.roundTime(upperBoundaryEvenWholeHour + 1)));
+
+        // No DST switchover
+        RoundDateExpression oddWholeDayExp =
+                getRoundMsExpression("2022-11-11 0:0:0", TimeUnit.DAY, 1);
+        java.sql.Timestamp oddWholeDay =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 0:0:0").getTime());
+        long lowerBoundaryOddWholeDay = oddWholeDay.getTime() - HALF_DAY;
+        long upperBoundaryOddWholeDay = oddWholeDay.getTime() + HALF_DAY - 1;
+        assertEquals(lowerBoundaryOddWholeDay, oddWholeDayExp.rangeLower(oddWholeDay.getTime()));
+        assertEquals(upperBoundaryOddWholeDay, oddWholeDayExp.rangeUpper(oddWholeDay.getTime()));
+        assertEquals(oddWholeDay,
+            new java.sql.Timestamp(oddWholeDayExp.roundTime(lowerBoundaryOddWholeDay)));
+        assertNotEquals(oddWholeDay,
+            new java.sql.Timestamp(oddWholeDayExp.roundTime(lowerBoundaryOddWholeDay - 1)));
+        assertEquals(oddWholeDay,
+            new java.sql.Timestamp(oddWholeDayExp.roundTime(upperBoundaryOddWholeDay)));
+        assertNotEquals(oddWholeDay,
+            new java.sql.Timestamp(oddWholeDayExp.roundTime(upperBoundaryOddWholeDay + 1)));
+
+        RoundDateExpression oddWholeDay10Exp =
+                getRoundMsExpression("2022-11-14 0:0:0", TimeUnit.DAY, 10);
+        java.sql.Timestamp oddWholeDay10 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-14 0:0:0").getTime());
+        long lowerBoundaryOddWholeDay10 = oddWholeDay10.getTime() - 10 * HALF_DAY;
+        long upperBoundaryOddWholeDay10 = oddWholeDay10.getTime() + 10 * HALF_DAY - 1;
+        assertEquals(lowerBoundaryOddWholeDay10,
+            oddWholeDay10Exp.rangeLower(oddWholeDay.getTime()));
+        assertEquals(upperBoundaryOddWholeDay10,
+            oddWholeDay10Exp.rangeUpper(oddWholeDay.getTime()));
+        assertEquals(oddWholeDay10,
+            new java.sql.Timestamp(oddWholeDay10Exp.roundTime(lowerBoundaryOddWholeDay10)));
+        assertNotEquals(oddWholeDay10,
+            new java.sql.Timestamp(oddWholeDay10Exp.roundTime(lowerBoundaryOddWholeDay10 - 1)));
+        assertEquals(oddWholeDay10,
+            new java.sql.Timestamp(oddWholeDay10Exp.roundTime(upperBoundaryOddWholeDay10)));
+        assertNotEquals(oddWholeDay10,
+            new java.sql.Timestamp(oddWholeDay10Exp.roundTime(upperBoundaryOddWholeDay10 + 1)));
+
+        RoundDateExpression oddWholeDay3Exp =
+                getRoundMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 3);
+        java.sql.Timestamp oddWholeDay3 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryOddWholeDay3 = oddWholeDay3.getTime() - 3 * HALF_DAY;
+        long upperBoundaryOddWholeDay3 = oddWholeDay3.getTime() + 3 * HALF_DAY - 1;
+        assertEquals(lowerBoundaryOddWholeDay3, oddWholeDay3Exp.rangeLower(oddWholeDay.getTime()));
+        assertEquals(upperBoundaryOddWholeDay3, oddWholeDay3Exp.rangeUpper(oddWholeDay.getTime()));
+        assertEquals(oddWholeDay3,
+            new java.sql.Timestamp(oddWholeDay3Exp.roundTime(lowerBoundaryOddWholeDay3)));
+        assertNotEquals(oddWholeDay3,
+            new java.sql.Timestamp(oddWholeDay3Exp.roundTime(lowerBoundaryOddWholeDay3 - 1)));
+        assertEquals(oddWholeDay3,
+            new java.sql.Timestamp(oddWholeDay3Exp.roundTime(upperBoundaryOddWholeDay3)));
+        assertNotEquals(oddWholeDay3,
+            new java.sql.Timestamp(oddWholeDay3Exp.roundTime(upperBoundaryOddWholeDay3 + 1)));
+
+        RoundDateExpression evenWholeDayExp =
+                getRoundMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 1);
+        java.sql.Timestamp evenWholeDay =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryEvenWholeDay = evenWholeDay.getTime() - HALF_DAY;
+        long upperBoundaryEvenWholeDay = evenWholeDay.getTime() + HALF_DAY - 1;
+        assertEquals(lowerBoundaryEvenWholeDay, evenWholeDayExp.rangeLower(evenWholeDay.getTime()));
+        assertEquals(upperBoundaryEvenWholeDay, evenWholeDayExp.rangeUpper(evenWholeDay.getTime()));
+        assertEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(lowerBoundaryEvenWholeDay)));
+        assertNotEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(lowerBoundaryEvenWholeDay - 1)));
+        assertEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(upperBoundaryEvenWholeDay)));
+        assertNotEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(upperBoundaryEvenWholeDay + 1)));
+
+        // Stateless, we can reuse it for every week test
+        RoundWeekExpression roundWeekExpression = new RoundWeekExpression();
+        java.sql.Timestamp wholeWeekOdd =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-10-10 0:0:0").getTime());
+        long lowerBoundaryWholeWeekOdd = wholeWeekOdd.getTime() - (HALF_WEEK - 1);
+        long upperBoundaryWholeWeekOdd = wholeWeekOdd.getTime() + HALF_WEEK - 1;
+        assertEquals(lowerBoundaryWholeWeekOdd,
+            roundWeekExpression.rangeLower(wholeWeekOdd.getTime()));
+        assertEquals(upperBoundaryWholeWeekOdd,
+            roundWeekExpression.rangeUpper(wholeWeekOdd.getTime()));
+        assertEquals(wholeWeekOdd,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeWeekOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekOdd,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeWeekOdd - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeWeekOdd,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeWeekOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekOdd,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeWeekOdd + 1, GJChronology.getInstanceUTC()))));
+
+        java.sql.Timestamp wholeWeekEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-10-17 0:0:0").getTime());
+        long lowerBoundaryWholeWeekEven = wholeWeekEven.getTime() - HALF_WEEK;
+        long upperBoundaryWholeWeekEven = wholeWeekEven.getTime() + HALF_WEEK;
+        assertEquals(lowerBoundaryWholeWeekEven,
+            roundWeekExpression.rangeLower(wholeWeekEven.getTime()));
+        assertEquals(upperBoundaryWholeWeekEven,
+            roundWeekExpression.rangeUpper(wholeWeekEven.getTime()));
+        assertEquals(wholeWeekEven,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeWeekEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekEven,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeWeekEven - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeWeekEven,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeWeekEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekEven,
+            new java.sql.Timestamp(roundWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeWeekEven + 1, GJChronology.getInstanceUTC()))));
+
+        RoundMonthExpression roundMonthExpression = new RoundMonthExpression();
+        // We're still using roundHalfEven here for backwards compatibility
+        java.sql.Timestamp wholeMonthEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-06-1 0:0:0").getTime());
+        // May is 31 days
+        long lowerBoundaryWholeMonthEven = wholeMonthEven.getTime() - 31 * HALF_DAY;
+        // June is 30 days
+        long upperBoundaryWholeMonthEven = wholeMonthEven.getTime() + 30 * HALF_DAY;
+        assertEquals(lowerBoundaryWholeMonthEven,
+            roundMonthExpression.rangeLower(wholeMonthEven.getTime()));
+        assertEquals(upperBoundaryWholeMonthEven,
+            roundMonthExpression.rangeUpper(wholeMonthEven.getTime()));
+        assertEquals(wholeMonthEven,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthEven,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthEven - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeMonthEven,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthEven,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthEven + 1, GJChronology.getInstanceUTC()))));
+
+        // We're still using roundHalfEven here for backwards compatibility
+        java.sql.Timestamp wholeMonthOdd =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-07-1 0:0:0").getTime());
+        // June is 30 days
+        long lowerBoundaryWholeMonthOdd = wholeMonthOdd.getTime() - 30 * HALF_DAY + 1;
+        // July is 31 days
+        long upperBoundaryWholeMonthOdd = wholeMonthOdd.getTime() + 31 * HALF_DAY - 1;
+        assertEquals(lowerBoundaryWholeMonthOdd,
+            roundMonthExpression.rangeLower(wholeMonthOdd.getTime()));
+        assertEquals(upperBoundaryWholeMonthOdd,
+            roundMonthExpression.rangeUpper(wholeMonthOdd.getTime()));
+        assertEquals(wholeMonthOdd,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthOdd,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthOdd - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeMonthOdd,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthOdd,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthOdd + 1, GJChronology.getInstanceUTC()))));
+
+        // We're still using roundHalfEven here for backwards compatibility
+        java.sql.Timestamp wholeMonthLeap =
+                new java.sql.Timestamp(DateUtil.parseDate("2024-02-1 0:0:0").getTime());
+        // January is 31 days
+        long lowerBoundaryWholeMonthLeap = wholeMonthLeap.getTime() - 31 * HALF_DAY;
+        // February is 29 days
+        long upperBoundaryWholeMonthLeap = wholeMonthLeap.getTime() + 29 * HALF_DAY;
+        assertEquals(lowerBoundaryWholeMonthLeap,
+            roundMonthExpression.rangeLower(wholeMonthLeap.getTime()));
+        assertEquals(upperBoundaryWholeMonthLeap,
+            roundMonthExpression.rangeUpper(wholeMonthLeap.getTime()));
+        assertEquals(wholeMonthLeap,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthLeap, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthLeap,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthLeap - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeMonthLeap,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthLeap, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthLeap,
+            new java.sql.Timestamp(roundMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthLeap + 1, GJChronology.getInstanceUTC()))));
+
+        // We're still using roundHalfEven here for backwards compatibility
+        RoundYearExpression roundYearExpression = new RoundYearExpression();
+        java.sql.Timestamp wholeYearEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-1-1 0:0:0").getTime());
+        long lowerBoundaryWholeYearEven = wholeYearEven.getTime() - HALF_YEAR;
+        long upperBoundaryWholeYearEven = wholeYearEven.getTime() + HALF_YEAR;
+        assertEquals(lowerBoundaryWholeYearEven,
+            roundYearExpression.rangeLower(wholeYearEven.getTime()));
+        assertEquals(upperBoundaryWholeYearEven,
+            roundYearExpression.rangeUpper(wholeYearEven.getTime()));
+        assertEquals(wholeYearEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearEven - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeYearEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearEven + 1, GJChronology.getInstanceUTC()))));
+
+        // We're still using roundHalfEven here for backwards compatibility
+        java.sql.Timestamp wholeYearOdd =
+                new java.sql.Timestamp(DateUtil.parseDate("2023-1-1 0:0:0").getTime());
+        long lowerBoundaryWholeYearOdd = wholeYearOdd.getTime() - HALF_YEAR + 1;
+        long upperBoundaryWholeYearOdd = wholeYearOdd.getTime() + HALF_YEAR - 1;
+        assertEquals(lowerBoundaryWholeYearOdd,
+            roundYearExpression.rangeLower(wholeYearOdd.getTime()));
+        assertEquals(upperBoundaryWholeYearOdd,
+            roundYearExpression.rangeUpper(wholeYearOdd.getTime()));
+        assertEquals(wholeYearOdd,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearOdd,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearOdd - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeYearOdd,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearOdd,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearOdd + 1, GJChronology.getInstanceUTC()))));
+
+        // We're still using roundHalfEven here for backwards compatibility
+        java.sql.Timestamp wholeYearLeapEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2024-1-1 0:0:0").getTime());
+        long lowerBoundaryWholeYearLeapEven = wholeYearLeapEven.getTime() - HALF_YEAR;
+        long upperBoundaryWholeYearLeapEven = wholeYearLeapEven.getTime() + HALF_YEAR + HALF_DAY;
+        assertEquals(lowerBoundaryWholeYearLeapEven,
+            roundYearExpression.rangeLower(wholeYearLeapEven.getTime()));
+        assertEquals(upperBoundaryWholeYearLeapEven,
+            roundYearExpression.rangeUpper(wholeYearLeapEven.getTime()));
+        assertEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearLeapEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearLeapEven - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearLeapEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(roundYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearLeapEven + 1, GJChronology.getInstanceUTC()))));
+    }
+
+    private FloorDateExpression getFloorMsExpression(String s, TimeUnit u, int m)
+            throws SQLException {
+        return (FloorDateExpression) FloorDateExpression.create(LiteralExpression.newConstant(s), u,
+            m);
+    }
+
+    @Test
+    public void testFloorGMT() throws SQLException {
+
+        // No need to repeat odd / even cases
+        // The logic for upper and lower scan ranges is always
+        // [floor(ts), ceil(ts+1)-1]
+
+        RoundDateExpression oddWholeSecondExp =
+                getFloorMsExpression("2022-11-11 11:11:11", TimeUnit.SECOND, 1);
+        java.sql.Timestamp oddWholeSecond =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:11").getTime());
+        long lowerBoundaryOddWholeSecond = oddWholeSecond.getTime();
+        long upperBoundaryOddWholeSecond = oddWholeSecond.getTime() + SEC - 1;
+        assertEquals(lowerBoundaryOddWholeSecond,
+            oddWholeSecondExp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecond,
+            oddWholeSecondExp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(lowerBoundaryOddWholeSecond)));
+        assertNotEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(lowerBoundaryOddWholeSecond - 1)));
+        assertEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(upperBoundaryOddWholeSecond)));
+        assertNotEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(upperBoundaryOddWholeSecond + 1)));
+
+        // 10 sec range
+        RoundDateExpression oddWholeSecondFloor10Exp =
+                getFloorMsExpression("2022-11-11 11:11:10", TimeUnit.SECOND, 10);
+        java.sql.Timestamp oddWholeSecondFloor10 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:10").getTime());
+        long lowerBoundaryOddWholeSecondFloor10 = oddWholeSecondFloor10.getTime();
+        long upperBoundaryOddWholeSecondFloor10 = oddWholeSecondFloor10.getTime() + 10 * SEC - 1;
+        assertEquals(lowerBoundaryOddWholeSecondFloor10,
+            oddWholeSecondFloor10Exp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecondFloor10,
+            oddWholeSecondFloor10Exp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecondFloor10, new java.sql.Timestamp(
+                oddWholeSecondFloor10Exp.roundTime(lowerBoundaryOddWholeSecondFloor10)));
+        assertNotEquals(oddWholeSecondFloor10, new java.sql.Timestamp(
+                oddWholeSecondFloor10Exp.roundTime(lowerBoundaryOddWholeSecondFloor10 - 1)));
+        assertEquals(oddWholeSecondFloor10, new java.sql.Timestamp(
+                oddWholeSecondFloor10Exp.roundTime(upperBoundaryOddWholeSecondFloor10)));
+        assertNotEquals(oddWholeSecondFloor10, new java.sql.Timestamp(
+                oddWholeSecondFloor10Exp.roundTime(upperBoundaryOddWholeSecondFloor10 + 1)));
+
+        // 15 sec range
+        RoundDateExpression oddWholeSecondFloor15Exp =
+                getFloorMsExpression("2022-11-11 11:11:0", TimeUnit.SECOND, 15);
+        java.sql.Timestamp oddWholeSecondFloor15 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:0").getTime());
+        long lowerBoundaryOddWholeSecondFloor15 = oddWholeSecondFloor15.getTime();
+        long upperBoundaryOddWholeSecondFloor15 = oddWholeSecondFloor15.getTime() + 15 * SEC - 1;
+        assertEquals(lowerBoundaryOddWholeSecondFloor15,
+            oddWholeSecondFloor15Exp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecondFloor15,
+            oddWholeSecondFloor15Exp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecondFloor15, new java.sql.Timestamp(
+                oddWholeSecondFloor15Exp.roundTime(lowerBoundaryOddWholeSecondFloor15)));
+        assertNotEquals(oddWholeSecondFloor15, new java.sql.Timestamp(
+                oddWholeSecondFloor15Exp.roundTime(lowerBoundaryOddWholeSecondFloor15 - 1)));
+        assertEquals(oddWholeSecondFloor15, new java.sql.Timestamp(
+                oddWholeSecondFloor15Exp.roundTime(upperBoundaryOddWholeSecondFloor15)));
+        assertNotEquals(oddWholeSecondFloor15, new java.sql.Timestamp(
+                oddWholeSecondFloor15Exp.roundTime(upperBoundaryOddWholeSecondFloor15 + 1)));
+
+        RoundDateExpression evenWholeMinuteExp =
+                getFloorMsExpression("2022-11-11 11:12:0", TimeUnit.MINUTE, 1);
+        java.sql.Timestamp evenWholeMinute =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:12:0").getTime());
+        long lowerBoundaryEvenWholeMinute = evenWholeMinute.getTime();
+        long upperBoundaryEvenWholeMinute = evenWholeMinute.getTime() + MIN - 1;
+        assertEquals(lowerBoundaryEvenWholeMinute,
+            evenWholeMinuteExp.rangeLower(evenWholeMinute.getTime()));
+        assertEquals(upperBoundaryEvenWholeMinute,
+            evenWholeMinuteExp.rangeUpper(evenWholeMinute.getTime()));
+        assertEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(lowerBoundaryEvenWholeMinute)));
+        assertNotEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(lowerBoundaryEvenWholeMinute - 1)));
+        assertEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(upperBoundaryEvenWholeMinute)));
+        assertNotEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(upperBoundaryEvenWholeMinute + 1)));
+
+        RoundDateExpression evenWholeMinuteFloor20Exp =
+                getFloorMsExpression("2022-11-11 11:00:0", TimeUnit.MINUTE, 20);
+        java.sql.Timestamp evenWholeMinuteFloor20 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:00:0").getTime());
+        long lowerBoundaryEvenWholeMinuteFloor20 = evenWholeMinuteFloor20.getTime();
+        long upperBoundaryEvenWholeMinuteFloor20 = evenWholeMinuteFloor20.getTime() + 20 * MIN - 1;
+        assertEquals(lowerBoundaryEvenWholeMinuteFloor20,
+            evenWholeMinuteFloor20Exp.rangeLower(evenWholeMinute.getTime()));
+        assertEquals(upperBoundaryEvenWholeMinuteFloor20,
+            evenWholeMinuteFloor20Exp.rangeUpper(evenWholeMinute.getTime()));
+        assertEquals(evenWholeMinuteFloor20, new java.sql.Timestamp(
+                evenWholeMinuteFloor20Exp.roundTime(lowerBoundaryEvenWholeMinuteFloor20)));
+        assertNotEquals(evenWholeMinuteFloor20, new java.sql.Timestamp(
+                evenWholeMinuteFloor20Exp.roundTime(lowerBoundaryEvenWholeMinuteFloor20 - 1)));
+        assertEquals(evenWholeMinuteFloor20, new java.sql.Timestamp(
+                evenWholeMinuteFloor20Exp.roundTime(upperBoundaryEvenWholeMinuteFloor20)));
+        assertNotEquals(evenWholeMinuteFloor20, new java.sql.Timestamp(
+                evenWholeMinuteFloor20Exp.roundTime(upperBoundaryEvenWholeMinuteFloor20 + 1)));
+
+        // Minutes since epoch, don't expect the rounded value to be "round"
+        RoundDateExpression evenWholeMinuteFloor17Exp =
+                getFloorMsExpression("2022-11-11 11:12:00", TimeUnit.MINUTE, 17);
+        java.sql.Timestamp evenWholeMinuteFloor17 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:12:00").getTime());
+        long lowerBoundaryEvenWholeMinute17 = evenWholeMinuteFloor17.getTime();
+        long upperBoundaryEvenWholeMinute17 = evenWholeMinuteFloor17.getTime() + 17 * MIN - 1;
+        assertEquals(lowerBoundaryEvenWholeMinute17,
+            evenWholeMinuteFloor17Exp.rangeLower(evenWholeMinute.getTime()));
+        assertEquals(upperBoundaryEvenWholeMinute17,
+            evenWholeMinuteFloor17Exp.rangeUpper(evenWholeMinute.getTime()));
+        assertEquals(evenWholeMinuteFloor17, new java.sql.Timestamp(
+                evenWholeMinuteFloor17Exp.roundTime(lowerBoundaryEvenWholeMinute17)));
+        assertNotEquals(evenWholeMinuteFloor17, new java.sql.Timestamp(
+                evenWholeMinuteFloor17Exp.roundTime(lowerBoundaryEvenWholeMinute17 - 1)));
+        assertEquals(evenWholeMinuteFloor17, new java.sql.Timestamp(
+                evenWholeMinuteFloor17Exp.roundTime(upperBoundaryEvenWholeMinute17)));
+        assertNotEquals(evenWholeMinuteFloor17, new java.sql.Timestamp(
+                evenWholeMinuteFloor17Exp.roundTime(upperBoundaryEvenWholeMinute17 + 1)));
+
+        RoundDateExpression oddWholeHourExp =
+                getFloorMsExpression("2022-11-11 11:0:0", TimeUnit.HOUR, 1);
+        java.sql.Timestamp oddWholeHour =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:0:0").getTime());
+        long lowerBoundaryOddWholeHour = oddWholeHour.getTime();
+        long upperBoundaryOddWholeHour = oddWholeHour.getTime() + HOUR - 1;
+        assertEquals(lowerBoundaryOddWholeHour, oddWholeHourExp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour, oddWholeHourExp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(lowerBoundaryOddWholeHour)));
+        assertNotEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(lowerBoundaryOddWholeHour - 1)));
+        assertEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(upperBoundaryOddWholeHour)));
+        assertNotEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(upperBoundaryOddWholeHour + 1)));
+
+        // Not rounding to hourOfDay
+        RoundDateExpression oddWholeHour10Exp =
+                getFloorMsExpression("2022-11-11 02:0:0", TimeUnit.HOUR, 10);
+        java.sql.Timestamp oddWholeHour10 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 02:0:0").getTime());
+        long lowerBoundaryOddWholeHour10 = oddWholeHour10.getTime();
+        long upperBoundaryOddWholeHour10 = oddWholeHour10.getTime() + HOUR * 10 - 1;
+        assertEquals(lowerBoundaryOddWholeHour10,
+            oddWholeHour10Exp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour10,
+            oddWholeHour10Exp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(lowerBoundaryOddWholeHour10)));
+        assertNotEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(lowerBoundaryOddWholeHour10 - 1)));
+        assertEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(upperBoundaryOddWholeHour10)));
+        assertNotEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(upperBoundaryOddWholeHour10 + 1)));
+
+        // Not rounding to hourOfDay
+        RoundDateExpression oddWholeHour11Exp =
+                getFloorMsExpression("2022-11-11 07:0:0", TimeUnit.HOUR, 11);
+        java.sql.Timestamp oddWholeHour11 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 07:0:0").getTime());
+        long lowerBoundaryOddWholeHour11 = oddWholeHour11.getTime();
+        long upperBoundaryOddWholeHour11 = oddWholeHour11.getTime() + HOUR * 11 - 1;
+        assertEquals(lowerBoundaryOddWholeHour11,
+            oddWholeHour11Exp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour11,
+            oddWholeHour11Exp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(lowerBoundaryOddWholeHour11)));
+        assertNotEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(lowerBoundaryOddWholeHour11 - 1)));
+        assertEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(upperBoundaryOddWholeHour11)));
+        assertNotEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(upperBoundaryOddWholeHour11 + 1)));
+
+        // No DST switchover
+        RoundDateExpression evenWholeDayExp =
+                getFloorMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 1);
+        java.sql.Timestamp evenWholeDay =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryEvenWholeDay = evenWholeDay.getTime();
+        long upperBoundaryEvenWholeDay = evenWholeDay.getTime() + DAY - 1;
+        assertEquals(lowerBoundaryEvenWholeDay, evenWholeDayExp.rangeLower(evenWholeDay.getTime()));
+        assertEquals(upperBoundaryEvenWholeDay, evenWholeDayExp.rangeUpper(evenWholeDay.getTime()));
+        assertEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(lowerBoundaryEvenWholeDay)));
+        assertNotEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(lowerBoundaryEvenWholeDay - 1)));
+        assertEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(upperBoundaryEvenWholeDay)));
+        assertNotEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(upperBoundaryEvenWholeDay + 1)));
+
+        RoundDateExpression evenWholeDay2Exp =
+                getFloorMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 2);
+        java.sql.Timestamp evenWholeDay2 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryEvenWholeDay2 = evenWholeDay2.getTime();
+        long upperBoundaryEvenWholeDay2 = evenWholeDay2.getTime() + 2 * DAY - 1;
+        assertEquals(lowerBoundaryEvenWholeDay2,
+            evenWholeDay2Exp.rangeLower(evenWholeDay.getTime()));
+        assertEquals(upperBoundaryEvenWholeDay2,
+            evenWholeDay2Exp.rangeUpper(evenWholeDay.getTime()));
+        assertEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(lowerBoundaryEvenWholeDay2)));
+        assertNotEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(lowerBoundaryEvenWholeDay2 - 1)));
+        assertEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(upperBoundaryEvenWholeDay2)));
+        assertNotEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(upperBoundaryEvenWholeDay2 + 1)));
+
+        RoundDateExpression evenWholeDay3Exp =
+                getFloorMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 3);
+        java.sql.Timestamp evenWholeDay3 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryEvenWholeDay3 = evenWholeDay3.getTime();
+        long upperBoundaryEvenWholeDay3 = evenWholeDay3.getTime() + 3 * DAY - 1;
+        assertEquals(lowerBoundaryEvenWholeDay3,
+            evenWholeDay3Exp.rangeLower(evenWholeDay.getTime()));
+        assertEquals(upperBoundaryEvenWholeDay3,
+            evenWholeDay3Exp.rangeUpper(evenWholeDay.getTime()));
+        assertEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(lowerBoundaryEvenWholeDay3)));
+        assertNotEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(lowerBoundaryEvenWholeDay3 - 1)));
+        assertEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(upperBoundaryEvenWholeDay3)));
+        assertNotEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(upperBoundaryEvenWholeDay3 + 1)));
+
+        FloorWeekExpression floorWeekExpression = new FloorWeekExpression();
+        java.sql.Timestamp wholeWeekOdd =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-10-10 0:0:0").getTime());
+        long lowerBoundaryWholeWeekOdd = wholeWeekOdd.getTime();
+        long upperBoundaryWholeWeekOdd = wholeWeekOdd.getTime() + WEEK - 1;
+        assertEquals(lowerBoundaryWholeWeekOdd,
+            floorWeekExpression.rangeLower(wholeWeekOdd.getTime()));
+        assertEquals(upperBoundaryWholeWeekOdd,
+            floorWeekExpression.rangeUpper(wholeWeekOdd.getTime()));
+        assertEquals(wholeWeekOdd,
+            new java.sql.Timestamp(floorWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeWeekOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekOdd,
+            new java.sql.Timestamp(floorWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeWeekOdd - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeWeekOdd,
+            new java.sql.Timestamp(floorWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeWeekOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekOdd,
+            new java.sql.Timestamp(floorWeekExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeWeekOdd + 1, GJChronology.getInstanceUTC()))));
+
+        FloorMonthExpression floorMonthExpression = new FloorMonthExpression();
+        java.sql.Timestamp wholeMonthOdd =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-07-1 0:0:0").getTime());
+        long lowerBoundaryWholeMonthOdd = wholeMonthOdd.getTime();
+        // July is 31 days
+        long upperBoundaryWholeMonthOdd = wholeMonthOdd.getTime() + 31 * DAY - 1;
+        assertEquals(lowerBoundaryWholeMonthOdd,
+            floorMonthExpression.rangeLower(wholeMonthOdd.getTime()));
+        assertEquals(upperBoundaryWholeMonthOdd,
+            floorMonthExpression.rangeUpper(wholeMonthOdd.getTime()));
+        assertEquals(wholeMonthOdd,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthOdd,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthOdd - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeMonthOdd,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthOdd, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthOdd,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthOdd + 1, GJChronology.getInstanceUTC()))));
+
+        java.sql.Timestamp wholeMonthLeap =
+                new java.sql.Timestamp(DateUtil.parseDate("2024-02-1 0:0:0").getTime());
+        long lowerBoundaryWholeMonthLeap = wholeMonthLeap.getTime();
+        // February is 29 days
+        long upperBoundaryWholeMonthLeap = wholeMonthLeap.getTime() + 29 * DAY - 1;
+        assertEquals(lowerBoundaryWholeMonthLeap,
+            floorMonthExpression.rangeLower(wholeMonthLeap.getTime()));
+        assertEquals(upperBoundaryWholeMonthLeap,
+            floorMonthExpression.rangeUpper(wholeMonthLeap.getTime()));
+        assertEquals(wholeMonthLeap,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthLeap, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthLeap,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthLeap - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeMonthLeap,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthLeap, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthLeap,
+            new java.sql.Timestamp(floorMonthExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthLeap + 1, GJChronology.getInstanceUTC()))));
+
+        FloorYearExpression floorYearExpression = new FloorYearExpression();
+        java.sql.Timestamp wholeYearEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-1-1 0:0:0").getTime());
+        long lowerBoundaryWholeYearEven = wholeYearEven.getTime();
+        long upperBoundaryWholeYearEven = wholeYearEven.getTime() + YEAR - 1;
+        assertEquals(lowerBoundaryWholeYearEven,
+            floorYearExpression.rangeLower(wholeYearEven.getTime()));
+        assertEquals(upperBoundaryWholeYearEven,
+            floorYearExpression.rangeUpper(wholeYearEven.getTime()));
+        assertEquals(wholeYearEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearEven - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeYearEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearEven + 1, GJChronology.getInstanceUTC()))));
+
+        java.sql.Timestamp wholeYearLeapEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2024-1-1 0:0:0").getTime());
+        long lowerBoundaryWholeYearLeapEven = wholeYearLeapEven.getTime();
+        long upperBoundaryWholeYearLeapEven = wholeYearLeapEven.getTime() + YEAR + DAY - 1;
+        assertEquals(lowerBoundaryWholeYearLeapEven,
+            floorYearExpression.rangeLower(wholeYearLeapEven.getTime()));
+        assertEquals(upperBoundaryWholeYearLeapEven,
+            floorYearExpression.rangeUpper(wholeYearLeapEven.getTime()));
+        assertEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearLeapEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearLeapEven - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearLeapEven, GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(floorYearExpression.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearLeapEven + 1, GJChronology.getInstanceUTC()))));
+    }
+
+    private CeilDateExpression getCeilMsExpression(String s, TimeUnit u, int m)
+            throws SQLException {
+        return (CeilDateExpression) CeilDateExpression.create(LiteralExpression.newConstant(s), u,
+            m);
+    }
+
+    @Test
+    public void testCeilGMT() throws SQLException {
+
+        // No need to repeat odd / even cases
+        // The logic for upper and lower scan ranges is always
+        // [floor(ts-1)+1, ceil(ts)]
+
+        RoundDateExpression oddWholeSecondExp =
+                getCeilMsExpression("2022-11-11 11:11:11", TimeUnit.SECOND, 1);
+        java.sql.Timestamp oddWholeSecond =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:11").getTime());
+        long lowerBoundaryOddWholeSecond = oddWholeSecond.getTime() - SEC + 1;
+        long upperBoundaryOddWholeSecond = oddWholeSecond.getTime();
+        assertEquals(lowerBoundaryOddWholeSecond,
+            oddWholeSecondExp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecond,
+            oddWholeSecondExp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(lowerBoundaryOddWholeSecond)));
+        assertNotEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(lowerBoundaryOddWholeSecond - 1)));
+        assertEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(upperBoundaryOddWholeSecond)));
+        assertNotEquals(oddWholeSecond,
+            new java.sql.Timestamp(oddWholeSecondExp.roundTime(upperBoundaryOddWholeSecond + 1)));
+
+        // 10 sec range
+        RoundDateExpression oddWholeSecondCeil10Exp =
+                getCeilMsExpression("2022-11-11 11:11:20", TimeUnit.SECOND, 10);
+        java.sql.Timestamp oddWholeSecondCeil10 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:20").getTime());
+        long lowerBoundaryOddWholeSecondCeil10 = oddWholeSecondCeil10.getTime() - 10 * SEC + 1;
+        long upperBoundaryOddWholeSecondCeil10 = oddWholeSecondCeil10.getTime();
+        assertEquals(lowerBoundaryOddWholeSecondCeil10,
+            oddWholeSecondCeil10Exp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecondCeil10,
+            oddWholeSecondCeil10Exp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecondCeil10, new java.sql.Timestamp(
+                oddWholeSecondCeil10Exp.roundTime(lowerBoundaryOddWholeSecondCeil10)));
+        assertNotEquals(oddWholeSecondCeil10, new java.sql.Timestamp(
+                oddWholeSecondCeil10Exp.roundTime(lowerBoundaryOddWholeSecondCeil10 - 1)));
+        assertEquals(oddWholeSecondCeil10, new java.sql.Timestamp(
+                oddWholeSecondCeil10Exp.roundTime(upperBoundaryOddWholeSecondCeil10)));
+        assertNotEquals(oddWholeSecondCeil10, new java.sql.Timestamp(
+                oddWholeSecondCeil10Exp.roundTime(upperBoundaryOddWholeSecondCeil10 + 1)));
+
+        // 15 sec range
+        RoundDateExpression oddWholeSecondCeil15Exp =
+                getCeilMsExpression("2022-11-11 11:11:15", TimeUnit.SECOND, 15);
+        java.sql.Timestamp oddWholeSecondCeil15 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:11:15").getTime());
+        long lowerBoundaryOddWholeSecondFloor15 = oddWholeSecondCeil15.getTime() - 15 * SEC + 1;
+        long upperBoundaryOddWholeSecondFloor15 = oddWholeSecondCeil15.getTime();
+        assertEquals(lowerBoundaryOddWholeSecondFloor15,
+            oddWholeSecondCeil15Exp.rangeLower(oddWholeSecond.getTime()));
+        assertEquals(upperBoundaryOddWholeSecondFloor15,
+            oddWholeSecondCeil15Exp.rangeUpper(oddWholeSecond.getTime()));
+        assertEquals(oddWholeSecondCeil15, new java.sql.Timestamp(
+                oddWholeSecondCeil15Exp.roundTime(lowerBoundaryOddWholeSecondFloor15)));
+        assertNotEquals(oddWholeSecondCeil15, new java.sql.Timestamp(
+                oddWholeSecondCeil15Exp.roundTime(lowerBoundaryOddWholeSecondFloor15 - 1)));
+        assertEquals(oddWholeSecondCeil15, new java.sql.Timestamp(
+                oddWholeSecondCeil15Exp.roundTime(upperBoundaryOddWholeSecondFloor15)));
+        assertNotEquals(oddWholeSecondCeil15, new java.sql.Timestamp(
+                oddWholeSecondCeil15Exp.roundTime(upperBoundaryOddWholeSecondFloor15 + 1)));
+
+        RoundDateExpression evenWholeMinuteExp =
+                getCeilMsExpression("2022-11-11 11:12:0", TimeUnit.MINUTE, 1);
+        java.sql.Timestamp evenWholeMinute =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:12:0").getTime());
+        long lowerBoundaryEvenWholeMinute = evenWholeMinute.getTime() - MIN + 1;
+        long upperBoundaryEvenWholeMinute = evenWholeMinute.getTime();
+        assertEquals(lowerBoundaryEvenWholeMinute,
+            evenWholeMinuteExp.rangeLower(evenWholeMinute.getTime()));
+        assertEquals(upperBoundaryEvenWholeMinute,
+            evenWholeMinuteExp.rangeUpper(evenWholeMinute.getTime()));
+        assertEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(lowerBoundaryEvenWholeMinute)));
+        assertNotEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(lowerBoundaryEvenWholeMinute - 1)));
+        assertEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(upperBoundaryEvenWholeMinute)));
+        assertNotEquals(evenWholeMinute,
+            new java.sql.Timestamp(evenWholeMinuteExp.roundTime(upperBoundaryEvenWholeMinute + 1)));
+
+        RoundDateExpression evenWholeMinuteCeil20Exp =
+                getCeilMsExpression("2022-11-11 11:20:0", TimeUnit.MINUTE, 20);
+        java.sql.Timestamp evenWholeMinuteCeil20 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:20:0").getTime());
+        long lowerBoundaryEvenWholeMinuteCeil20 = evenWholeMinuteCeil20.getTime() - 20 * MIN + 1;
+        long upperBoundaryEvenWholeMinuteCeil20 = evenWholeMinuteCeil20.getTime();
+        assertEquals(lowerBoundaryEvenWholeMinuteCeil20,
+            evenWholeMinuteCeil20Exp.rangeLower(evenWholeMinute.getTime()));
+        assertEquals(upperBoundaryEvenWholeMinuteCeil20,
+            evenWholeMinuteCeil20Exp.rangeUpper(evenWholeMinute.getTime()));
+        assertEquals(evenWholeMinuteCeil20, new java.sql.Timestamp(
+                evenWholeMinuteCeil20Exp.roundTime(lowerBoundaryEvenWholeMinuteCeil20)));
+        assertNotEquals(evenWholeMinuteCeil20, new java.sql.Timestamp(
+                evenWholeMinuteCeil20Exp.roundTime(lowerBoundaryEvenWholeMinuteCeil20 - 1)));
+        assertEquals(evenWholeMinuteCeil20, new java.sql.Timestamp(
+                evenWholeMinuteCeil20Exp.roundTime(upperBoundaryEvenWholeMinuteCeil20)));
+        assertNotEquals(evenWholeMinuteCeil20, new java.sql.Timestamp(
+                evenWholeMinuteCeil20Exp.roundTime(upperBoundaryEvenWholeMinuteCeil20 + 1)));
+
+        // Minutes since epoch, don't expect the rounded value to be "round"
+        RoundDateExpression evenWholeMinuteCeil17Exp =
+                getCeilMsExpression("2022-11-11 11:12:00", TimeUnit.MINUTE, 17);
+        java.sql.Timestamp evenWholeMinuteCeil17 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:12:00").getTime());
+        long lowerBoundaryEvenWholeMinute17 = evenWholeMinuteCeil17.getTime() - 17 * MIN + 1;
+        long upperBoundaryEvenWholeMinute17 = evenWholeMinuteCeil17.getTime();
+        assertEquals(lowerBoundaryEvenWholeMinute17,
+            evenWholeMinuteCeil17Exp.rangeLower(evenWholeMinute.getTime()));
+        assertEquals(upperBoundaryEvenWholeMinute17,
+            evenWholeMinuteCeil17Exp.rangeUpper(evenWholeMinute.getTime()));
+        assertEquals(evenWholeMinuteCeil17, new java.sql.Timestamp(
+                evenWholeMinuteCeil17Exp.roundTime(lowerBoundaryEvenWholeMinute17)));
+        assertNotEquals(evenWholeMinuteCeil17, new java.sql.Timestamp(
+                evenWholeMinuteCeil17Exp.roundTime(lowerBoundaryEvenWholeMinute17 - 1)));
+        assertEquals(evenWholeMinuteCeil17, new java.sql.Timestamp(
+                evenWholeMinuteCeil17Exp.roundTime(upperBoundaryEvenWholeMinute17)));
+        assertNotEquals(evenWholeMinuteCeil17, new java.sql.Timestamp(
+                evenWholeMinuteCeil17Exp.roundTime(upperBoundaryEvenWholeMinute17 + 1)));
+
+        RoundDateExpression oddWholeHourExp =
+                getCeilMsExpression("2022-11-11 11:0:0", TimeUnit.HOUR, 1);
+        java.sql.Timestamp oddWholeHour =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 11:0:0").getTime());
+        long lowerBoundaryOddWholeHour = oddWholeHour.getTime() - HOUR + 1;
+        long upperBoundaryOddWholeHour = oddWholeHour.getTime();
+        assertEquals(lowerBoundaryOddWholeHour,
+            oddWholeHourExp.rangeLower(oddWholeHour.getTime() - 1));
+        assertEquals(upperBoundaryOddWholeHour, oddWholeHourExp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(lowerBoundaryOddWholeHour)));
+        assertNotEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(lowerBoundaryOddWholeHour - 1)));
+        assertEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(upperBoundaryOddWholeHour)));
+        assertNotEquals(oddWholeHour,
+            new java.sql.Timestamp(oddWholeHourExp.roundTime(upperBoundaryOddWholeHour + 1)));
+
+        // Not rounding to hourOfDay
+        RoundDateExpression oddWholeHour10Exp =
+                getCeilMsExpression("2022-11-11 12:0:0", TimeUnit.HOUR, 10);
+        java.sql.Timestamp oddWholeHour10 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 12:0:0").getTime());
+        long lowerBoundaryOddWholeHour10 = oddWholeHour10.getTime() - 10 * HOUR + 1;
+        long upperBoundaryOddWholeHour10 = oddWholeHour10.getTime();
+        assertEquals(lowerBoundaryOddWholeHour10,
+            oddWholeHour10Exp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour10,
+            oddWholeHour10Exp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(lowerBoundaryOddWholeHour10)));
+        assertNotEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(lowerBoundaryOddWholeHour10 - 1)));
+        assertEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(upperBoundaryOddWholeHour10)));
+        assertNotEquals(oddWholeHour10,
+            new java.sql.Timestamp(oddWholeHour10Exp.roundTime(upperBoundaryOddWholeHour10 + 1)));
+
+        // Not rounding to hourOfDay
+        RoundDateExpression oddWholeHour11Exp =
+                getCeilMsExpression("2022-11-11 12:0:0", TimeUnit.HOUR, 11);
+        java.sql.Timestamp oddWholeHour11 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-11 18:0:0").getTime());
+        long lowerBoundaryOddWholeHour11 = oddWholeHour11.getTime() - 11 * HOUR + 1;
+        long upperBoundaryOddWholeHour11 = oddWholeHour11.getTime();
+        assertEquals(lowerBoundaryOddWholeHour11,
+            oddWholeHour11Exp.rangeLower(oddWholeHour.getTime()));
+        assertEquals(upperBoundaryOddWholeHour11,
+            oddWholeHour11Exp.rangeUpper(oddWholeHour.getTime()));
+        assertEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(lowerBoundaryOddWholeHour11)));
+        assertNotEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(lowerBoundaryOddWholeHour11 - 1)));
+        assertEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(upperBoundaryOddWholeHour11)));
+        assertNotEquals(oddWholeHour11,
+            new java.sql.Timestamp(oddWholeHour11Exp.roundTime(upperBoundaryOddWholeHour11 + 1)));
+
+        RoundDateExpression evenWholeDayExp =
+                getCeilMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 1);
+        java.sql.Timestamp evenWholeDay =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryEvenWholeDay = evenWholeDay.getTime() - DAY + 1;
+        long upperBoundaryEvenWholeDay = evenWholeDay.getTime();
+        assertEquals(lowerBoundaryEvenWholeDay, evenWholeDayExp.rangeLower(evenWholeDay.getTime()));
+        assertEquals(upperBoundaryEvenWholeDay, evenWholeDayExp.rangeUpper(evenWholeDay.getTime()));
+        assertEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(lowerBoundaryEvenWholeDay)));
+        assertNotEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(lowerBoundaryEvenWholeDay - 1)));
+        assertEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(upperBoundaryEvenWholeDay)));
+        assertNotEquals(evenWholeDay,
+            new java.sql.Timestamp(evenWholeDayExp.roundTime(upperBoundaryEvenWholeDay + 1)));
+
+        RoundDateExpression evenWholeDay2Exp =
+                getCeilMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 2);
+        java.sql.Timestamp evenWholeDay2 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryEvenWholeDay2 = evenWholeDay2.getTime() - 2 * DAY + 1;
+        long upperBoundaryEvenWholeDay2 = evenWholeDay2.getTime();
+        assertEquals(lowerBoundaryEvenWholeDay2,
+            evenWholeDay2Exp.rangeLower(evenWholeDay.getTime()));
+        assertEquals(upperBoundaryEvenWholeDay2,
+            evenWholeDay2Exp.rangeUpper(evenWholeDay.getTime()));
+        assertEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(lowerBoundaryEvenWholeDay2)));
+        assertNotEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(lowerBoundaryEvenWholeDay2 - 1)));
+        assertEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(upperBoundaryEvenWholeDay2)));
+        assertNotEquals(evenWholeDay2,
+            new java.sql.Timestamp(evenWholeDay2Exp.roundTime(upperBoundaryEvenWholeDay2 + 1)));
+
+        RoundDateExpression evenWholeDay3Exp =
+                getCeilMsExpression("2022-11-12 0:0:0", TimeUnit.DAY, 3);
+        java.sql.Timestamp evenWholeDay3 =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-11-12 0:0:0").getTime());
+        long lowerBoundaryEvenWholeDay3 = evenWholeDay3.getTime() - 3 * DAY + 1;
+        long upperBoundaryEvenWholeDay3 = evenWholeDay3.getTime();
+        assertEquals(lowerBoundaryEvenWholeDay3,
+            evenWholeDay3Exp.rangeLower(evenWholeDay.getTime()));
+        assertEquals(upperBoundaryEvenWholeDay3,
+            evenWholeDay3Exp.rangeUpper(evenWholeDay.getTime()));
+        assertEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(lowerBoundaryEvenWholeDay3)));
+        assertNotEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(lowerBoundaryEvenWholeDay3 - 1)));
+        assertEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(upperBoundaryEvenWholeDay3)));
+        assertNotEquals(evenWholeDay3,
+            new java.sql.Timestamp(evenWholeDay3Exp.roundTime(upperBoundaryEvenWholeDay3 + 1)));
+
+        CeilWeekExpression ceilWeekExp = new CeilWeekExpression();
+        java.sql.Timestamp wholeWeekOdd =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-10-10 0:0:0").getTime());
+        long lowerBoundaryWholeWeekOdd = wholeWeekOdd.getTime() - WEEK + 1;
+        long upperBoundaryWholeWeekOdd = wholeWeekOdd.getTime();
+        assertEquals(lowerBoundaryWholeWeekOdd, ceilWeekExp.rangeLower(wholeWeekOdd.getTime()));
+        assertEquals(upperBoundaryWholeWeekOdd, ceilWeekExp.rangeUpper(wholeWeekOdd.getTime()));
+        assertEquals(wholeWeekOdd, new java.sql.Timestamp(
+                ceilWeekExp.roundDateTime(new org.joda.time.DateTime(lowerBoundaryWholeWeekOdd,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekOdd, new java.sql.Timestamp(
+                ceilWeekExp.roundDateTime(new org.joda.time.DateTime(lowerBoundaryWholeWeekOdd - 1,
+                        GJChronology.getInstanceUTC()))));
+        assertEquals(wholeWeekOdd, new java.sql.Timestamp(
+                ceilWeekExp.roundDateTime(new org.joda.time.DateTime(upperBoundaryWholeWeekOdd,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeWeekOdd, new java.sql.Timestamp(
+                ceilWeekExp.roundDateTime(new org.joda.time.DateTime(upperBoundaryWholeWeekOdd + 1,
+                        GJChronology.getInstanceUTC()))));
+
+        CeilMonthExpression ceilMonthExp = new CeilMonthExpression();
+        java.sql.Timestamp wholeMonthOdd =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-08-1 0:0:0").getTime());
+        // July is 31 days
+        long lowerBoundaryWholeMonthOdd = wholeMonthOdd.getTime() - 31 * DAY + 1;
+        long upperBoundaryWholeMonthOdd = wholeMonthOdd.getTime();
+        assertEquals(lowerBoundaryWholeMonthOdd, ceilMonthExp.rangeLower(wholeMonthOdd.getTime()));
+        assertEquals(upperBoundaryWholeMonthOdd, ceilMonthExp.rangeUpper(wholeMonthOdd.getTime()));
+        assertEquals(wholeMonthOdd, new java.sql.Timestamp(
+                ceilMonthExp.roundDateTime(new org.joda.time.DateTime(lowerBoundaryWholeMonthOdd,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthOdd,
+            new java.sql.Timestamp(ceilMonthExp.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthOdd - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeMonthOdd, new java.sql.Timestamp(
+                ceilMonthExp.roundDateTime(new org.joda.time.DateTime(upperBoundaryWholeMonthOdd,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthOdd,
+            new java.sql.Timestamp(ceilMonthExp.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthOdd + 1, GJChronology.getInstanceUTC()))));
+
+        java.sql.Timestamp wholeMonthLeap =
+                new java.sql.Timestamp(DateUtil.parseDate("2024-03-1 0:0:0").getTime());
+        // February is 29 days
+        long lowerBoundaryWholeMonthLeap = wholeMonthLeap.getTime() - 29 * DAY + 1;
+        long upperBoundaryWholeMonthLeap = wholeMonthLeap.getTime();
+        assertEquals(lowerBoundaryWholeMonthLeap,
+            ceilMonthExp.rangeLower(wholeMonthLeap.getTime()));
+        assertEquals(upperBoundaryWholeMonthLeap,
+            ceilMonthExp.rangeUpper(wholeMonthLeap.getTime()));
+        assertEquals(wholeMonthLeap, new java.sql.Timestamp(
+                ceilMonthExp.roundDateTime(new org.joda.time.DateTime(lowerBoundaryWholeMonthLeap,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthLeap,
+            new java.sql.Timestamp(ceilMonthExp.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeMonthLeap - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeMonthLeap, new java.sql.Timestamp(
+                ceilMonthExp.roundDateTime(new org.joda.time.DateTime(upperBoundaryWholeMonthLeap,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeMonthLeap,
+            new java.sql.Timestamp(ceilMonthExp.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeMonthLeap + 1, GJChronology.getInstanceUTC()))));
+
+        CeilYearExpression ceilYearExp = new CeilYearExpression();
+        java.sql.Timestamp wholeYearEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2022-1-1 0:0:0").getTime());
+        long lowerBoundaryWholeYearEven = wholeYearEven.getTime() - YEAR + 1;
+        long upperBoundaryWholeYearEven = wholeYearEven.getTime();
+        assertEquals(lowerBoundaryWholeYearEven, ceilYearExp.rangeLower(wholeYearEven.getTime()));
+        assertEquals(upperBoundaryWholeYearEven, ceilYearExp.rangeUpper(wholeYearEven.getTime()));
+        assertEquals(wholeYearEven, new java.sql.Timestamp(
+                ceilYearExp.roundDateTime(new org.joda.time.DateTime(lowerBoundaryWholeYearEven,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearEven, new java.sql.Timestamp(
+                ceilYearExp.roundDateTime(new org.joda.time.DateTime(lowerBoundaryWholeYearEven - 1,
+                        GJChronology.getInstanceUTC()))));
+        assertEquals(wholeYearEven, new java.sql.Timestamp(
+                ceilYearExp.roundDateTime(new org.joda.time.DateTime(upperBoundaryWholeYearEven,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearEven, new java.sql.Timestamp(
+                ceilYearExp.roundDateTime(new org.joda.time.DateTime(upperBoundaryWholeYearEven + 1,
+                        GJChronology.getInstanceUTC()))));
+
+        java.sql.Timestamp wholeYearLeapEven =
+                new java.sql.Timestamp(DateUtil.parseDate("2025-1-1 0:0:0").getTime());
+        long lowerBoundaryWholeYearLeapEven = wholeYearLeapEven.getTime() - (YEAR + DAY) + 1;
+        long upperBoundaryWholeYearLeapEven = wholeYearLeapEven.getTime();
+        assertEquals(lowerBoundaryWholeYearLeapEven,
+            ceilYearExp.rangeLower(wholeYearLeapEven.getTime()));
+        assertEquals(upperBoundaryWholeYearLeapEven,
+            ceilYearExp.rangeUpper(wholeYearLeapEven.getTime()));
+        assertEquals(wholeYearLeapEven, new java.sql.Timestamp(
+                ceilYearExp.roundDateTime(new org.joda.time.DateTime(lowerBoundaryWholeYearLeapEven,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(ceilYearExp.roundDateTime(new org.joda.time.DateTime(
+                    lowerBoundaryWholeYearLeapEven - 1, GJChronology.getInstanceUTC()))));
+        assertEquals(wholeYearLeapEven, new java.sql.Timestamp(
+                ceilYearExp.roundDateTime(new org.joda.time.DateTime(upperBoundaryWholeYearLeapEven,
+                        GJChronology.getInstanceUTC()))));
+        assertNotEquals(wholeYearLeapEven,
+            new java.sql.Timestamp(ceilYearExp.roundDateTime(new org.joda.time.DateTime(
+                    upperBoundaryWholeYearLeapEven + 1, GJChronology.getInstanceUTC()))));
+    }
+
 }