You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by fj...@apache.org on 2019/07/04 22:40:11 UTC

[incubator-druid] branch master updated: SQL: Add TIME_CEIL function. (#8027)

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

fjy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 613f09b  SQL: Add TIME_CEIL function. (#8027)
613f09b is described below

commit 613f09b45a287c169f288aa3f53eb13d21e79249
Author: Gian Merlino <gi...@gmail.com>
AuthorDate: Thu Jul 4 15:40:03 2019 -0700

    SQL: Add TIME_CEIL function. (#8027)
    
    Also simplify conversions for CEIL, FLOOR, and TIME_FLOOR by allowing them to
    share more code.
---
 docs/content/querying/sql.md                       |   1 +
 .../druid/sql/calcite/expression/TimeUnits.java    |  14 +--
 .../expression/builtin/CeilOperatorConversion.java |  54 ++--------
 .../builtin/FloorOperatorConversion.java           |  46 +++------
 ...ersion.java => TimeCeilOperatorConversion.java} |  59 +++++------
 .../builtin/TimeFloorOperatorConversion.java       | 115 +++++++++++++++------
 .../sql/calcite/planner/DruidOperatorTable.java    |   2 +
 .../sql/calcite/expression/ExpressionsTest.java    |  33 +++++-
 8 files changed, 168 insertions(+), 156 deletions(-)

diff --git a/docs/content/querying/sql.md b/docs/content/querying/sql.md
index 92b9e41..76c13d8 100644
--- a/docs/content/querying/sql.md
+++ b/docs/content/querying/sql.md
@@ -282,6 +282,7 @@ simplest way to write literal timestamps in other time zones is to use TIME_PARS
 |`CURRENT_TIMESTAMP`|Current timestamp in the connection's time zone.|
 |`CURRENT_DATE`|Current date in the connection's time zone.|
 |`DATE_TRUNC(<unit>, <timestamp_expr>)`|Rounds down a timestamp, returning it as a new timestamp. Unit can be 'milliseconds', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year', 'decade', 'century', or 'millenium'.|
+|`TIME_CEIL(<timestamp_expr>, <period>, [<origin>, [<timezone>]])`|Rounds up a timestamp, returning it as a new timestamp. Period can be any ISO8601 period, like P3M (quarters) or PT12H (half-days). The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00". This function is similar to `CEIL` but is more flexible.|
 |`TIME_FLOOR(<timestamp_expr>, <period>, [<origin>, [<timezone>]])`|Rounds down a timestamp, returning it as a new timestamp. Period can be any ISO8601 period, like P3M (quarters) or PT12H (half-days). The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00". This function is similar to `FLOOR` but is more flexible.|
 |`TIME_SHIFT(<timestamp_expr>, <period>, <step>, [<timezone>])`|Shifts a timestamp by a period (step times), returning it as a new timestamp. Period can be any ISO8601 period. Step may be negative. The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00".|
 |`TIME_EXTRACT(<timestamp_expr>, [<unit>, [<timezone>]])`|Extracts a time part from expr, returning it as a number. Unit can be EPOCH, SECOND, MINUTE, HOUR, DAY (day of month), DOW (day of week), DOY (day of year), WEEK (week of [week year](https://en.wikipedia.org/wiki/ISO_week_date)), MONTH (1 through 12), QUARTER (1 through 4), or YEAR. The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00". This function is similar to `EXTRACT` but i [...]
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/TimeUnits.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/TimeUnits.java
index 4b369c5..97a9ad4 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/expression/TimeUnits.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/TimeUnits.java
@@ -21,10 +21,9 @@ package org.apache.druid.sql.calcite.expression;
 
 import com.google.common.collect.ImmutableMap;
 import org.apache.calcite.avatica.util.TimeUnitRange;
-import org.apache.druid.java.util.common.granularity.PeriodGranularity;
-import org.joda.time.DateTimeZone;
 import org.joda.time.Period;
 
+import javax.annotation.Nullable;
 import java.util.Map;
 
 public class TimeUnits
@@ -44,17 +43,12 @@ public class TimeUnits
    * Returns the Druid QueryGranularity corresponding to a Calcite TimeUnitRange, or null if there is none.
    *
    * @param timeUnitRange time unit
-   * @param timeZone      session time zone
    *
    * @return queryGranularity, or null
    */
-  public static PeriodGranularity toQueryGranularity(final TimeUnitRange timeUnitRange, final DateTimeZone timeZone)
+  @Nullable
+  public static Period toPeriod(final TimeUnitRange timeUnitRange)
   {
-    final Period period = PERIOD_MAP.get(timeUnitRange);
-    if (period == null) {
-      return null;
-    }
-
-    return new PeriodGranularity(period, null, timeZone);
+    return PERIOD_MAP.get(timeUnitRange);
   }
 }
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CeilOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CeilOperatorConversion.java
index 7f0ab00..d11f84cbf 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CeilOperatorConversion.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/CeilOperatorConversion.java
@@ -19,23 +19,17 @@
 
 package org.apache.druid.sql.calcite.expression.builtin;
 
-import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.druid.java.util.common.StringUtils;
-import org.apache.druid.java.util.common.granularity.PeriodGranularity;
 import org.apache.druid.sql.calcite.expression.DruidExpression;
-import org.apache.druid.sql.calcite.expression.Expressions;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
 import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
-import org.apache.druid.sql.calcite.expression.TimeUnits;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.apache.druid.sql.calcite.table.RowSignature;
 
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import javax.annotation.Nullable;
 
 public class CeilOperatorConversion implements SqlOperatorConversion
 {
@@ -46,6 +40,7 @@ public class CeilOperatorConversion implements SqlOperatorConversion
   }
 
   @Override
+  @Nullable
   public DruidExpression toDruidExpression(
       final PlannerContext plannerContext,
       final RowSignature rowSignature,
@@ -53,47 +48,18 @@ public class CeilOperatorConversion implements SqlOperatorConversion
   )
   {
     final RexCall call = (RexCall) rexNode;
-    final RexNode arg = call.getOperands().get(0);
-    final DruidExpression druidExpression = Expressions.toDruidExpression(
-        plannerContext,
-        rowSignature,
-        arg
-    );
-    if (druidExpression == null) {
-      return null;
-    } else if (call.getOperands().size() == 1) {
-      // CEIL(expr)
-      return druidExpression.map(
-          simpleExtraction -> null,
-          expression -> StringUtils.format("ceil(%s)", expression)
-      );
-    } else if (call.getOperands().size() == 2) {
-      // CEIL(expr TO timeUnit)
-      final RexLiteral flag = (RexLiteral) call.getOperands().get(1);
-      final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
-      final PeriodGranularity granularity = TimeUnits.toQueryGranularity(timeUnit, plannerContext.getTimeZone());
-      if (granularity == null) {
-        return null;
-      }
 
-      // Unlike FLOOR(expr TO timeUnit) there is no built-in extractionFn that can behave like timestamp_ceil.
-      // So there is no simple extraction for this operator.
+    if (call.getOperands().size() == 1) {
+      // CEIL(expr) -- numeric CEIL
+      return OperatorConversions.convertCall(plannerContext, rowSignature, call, "ceil");
+    } else if (call.getOperands().size() == 2) {
+      // CEIL(expr TO timeUnit) -- time CEIL
       return DruidExpression.fromFunctionCall(
           "timestamp_ceil",
-          Stream
-              .of(
-                  druidExpression.getExpression(),
-                  DruidExpression.stringLiteral(granularity.getPeriod().toString()),
-                  DruidExpression.numberLiteral(
-                      granularity.getOrigin() == null ? null : granularity.getOrigin().getMillis()
-                  ),
-                  DruidExpression.stringLiteral(granularity.getTimeZone().toString())
-              )
-              .map(DruidExpression::fromExpression)
-              .collect(Collectors.toList())
+          TimeFloorOperatorConversion.toTimestampFloorOrCeilArgs(plannerContext, rowSignature, call.getOperands())
       );
     } else {
-      // WTF? CEIL with 3 arguments?
+      // WTF? CEIL with the wrong number of arguments?
       return null;
     }
   }
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/FloorOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/FloorOperatorConversion.java
index fb5daa1..b890343 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/FloorOperatorConversion.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/FloorOperatorConversion.java
@@ -19,21 +19,18 @@
 
 package org.apache.druid.sql.calcite.expression.builtin;
 
-import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.druid.java.util.common.StringUtils;
-import org.apache.druid.java.util.common.granularity.PeriodGranularity;
 import org.apache.druid.sql.calcite.expression.DruidExpression;
-import org.apache.druid.sql.calcite.expression.Expressions;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
 import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
-import org.apache.druid.sql.calcite.expression.TimeUnits;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.apache.druid.sql.calcite.table.RowSignature;
 
+import javax.annotation.Nullable;
+
 public class FloorOperatorConversion implements SqlOperatorConversion
 {
   @Override
@@ -43,6 +40,7 @@ public class FloorOperatorConversion implements SqlOperatorConversion
   }
 
   @Override
+  @Nullable
   public DruidExpression toDruidExpression(
       final PlannerContext plannerContext,
       final RowSignature rowSignature,
@@ -50,36 +48,18 @@ public class FloorOperatorConversion implements SqlOperatorConversion
   )
   {
     final RexCall call = (RexCall) rexNode;
-    final RexNode arg = call.getOperands().get(0);
-    final DruidExpression druidExpression = Expressions.toDruidExpression(
-        plannerContext,
-        rowSignature,
-        arg
-    );
-    if (druidExpression == null) {
-      return null;
-    } else if (call.getOperands().size() == 1) {
-      // FLOOR(expr)
-      return druidExpression.map(
-          simpleExtraction -> null, // BucketExtractionFn could do this, but it's lame since it returns strings.
-          expression -> StringUtils.format("floor(%s)", expression)
-      );
-    } else if (call.getOperands().size() == 2) {
-      // FLOOR(expr TO timeUnit)
-      final RexLiteral flag = (RexLiteral) call.getOperands().get(1);
-      final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
-      final PeriodGranularity granularity = TimeUnits.toQueryGranularity(timeUnit, plannerContext.getTimeZone());
-      if (granularity == null) {
-        return null;
-      }
 
-      return TimeFloorOperatorConversion.applyTimestampFloor(
-          druidExpression,
-          granularity,
-          plannerContext.getExprMacroTable()
+    if (call.getOperands().size() == 1) {
+      // FLOOR(expr) -- numeric FLOOR
+      return OperatorConversions.convertCall(plannerContext, rowSignature, call, "floor");
+    } else if (call.getOperands().size() == 2) {
+      // FLOOR(expr TO timeUnit) -- time FLOOR
+      return DruidExpression.fromFunctionCall(
+          "timestamp_floor",
+          TimeFloorOperatorConversion.toTimestampFloorOrCeilArgs(plannerContext, rowSignature, call.getOperands())
       );
     } else {
-      // WTF? FLOOR with 3 arguments?
+      // WTF? FLOOR with the wrong number of arguments?
       return null;
     }
   }
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/FloorOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeCeilOperatorConversion.java
similarity index 50%
copy from sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/FloorOperatorConversion.java
copy to sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeCeilOperatorConversion.java
index fb5daa1..4bfa3f7 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/FloorOperatorConversion.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeCeilOperatorConversion.java
@@ -19,30 +19,40 @@
 
 package org.apache.druid.sql.calcite.expression.builtin;
 
-import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlOperator;
-import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.druid.java.util.common.StringUtils;
-import org.apache.druid.java.util.common.granularity.PeriodGranularity;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.druid.sql.calcite.expression.DruidExpression;
-import org.apache.druid.sql.calcite.expression.Expressions;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
 import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
-import org.apache.druid.sql.calcite.expression.TimeUnits;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.apache.druid.sql.calcite.table.RowSignature;
 
-public class FloorOperatorConversion implements SqlOperatorConversion
+import javax.annotation.Nullable;
+import java.util.List;
+
+public class TimeCeilOperatorConversion implements SqlOperatorConversion
 {
+  private static final SqlFunction SQL_FUNCTION = OperatorConversions
+      .operatorBuilder("TIME_CEIL")
+      .operandTypes(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER, SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER)
+      .requiredOperands(2)
+      .returnType(SqlTypeName.TIMESTAMP)
+      .functionCategory(SqlFunctionCategory.TIMEDATE)
+      .build();
+
   @Override
   public SqlOperator calciteOperator()
   {
-    return SqlStdOperatorTable.FLOOR;
+    return SQL_FUNCTION;
   }
 
   @Override
+  @Nullable
   public DruidExpression toDruidExpression(
       final PlannerContext plannerContext,
       final RowSignature rowSignature,
@@ -50,37 +60,16 @@ public class FloorOperatorConversion implements SqlOperatorConversion
   )
   {
     final RexCall call = (RexCall) rexNode;
-    final RexNode arg = call.getOperands().get(0);
-    final DruidExpression druidExpression = Expressions.toDruidExpression(
+    final List<DruidExpression> functionArgs = TimeFloorOperatorConversion.toTimestampFloorOrCeilArgs(
         plannerContext,
         rowSignature,
-        arg
+        call.getOperands()
     );
-    if (druidExpression == null) {
-      return null;
-    } else if (call.getOperands().size() == 1) {
-      // FLOOR(expr)
-      return druidExpression.map(
-          simpleExtraction -> null, // BucketExtractionFn could do this, but it's lame since it returns strings.
-          expression -> StringUtils.format("floor(%s)", expression)
-      );
-    } else if (call.getOperands().size() == 2) {
-      // FLOOR(expr TO timeUnit)
-      final RexLiteral flag = (RexLiteral) call.getOperands().get(1);
-      final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
-      final PeriodGranularity granularity = TimeUnits.toQueryGranularity(timeUnit, plannerContext.getTimeZone());
-      if (granularity == null) {
-        return null;
-      }
 
-      return TimeFloorOperatorConversion.applyTimestampFloor(
-          druidExpression,
-          granularity,
-          plannerContext.getExprMacroTable()
-      );
-    } else {
-      // WTF? FLOOR with 3 arguments?
+    if (functionArgs == null) {
       return null;
     }
+
+    return DruidExpression.fromFunctionCall("timestamp_ceil", functionArgs);
   }
 }
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFloorOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFloorOperatorConversion.java
index 24dbd95..b903c3b 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFloorOperatorConversion.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/TimeFloorOperatorConversion.java
@@ -21,6 +21,7 @@ package org.apache.druid.sql.calcite.expression.builtin;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
@@ -30,7 +31,6 @@ import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
-import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.java.util.common.granularity.PeriodGranularity;
 import org.apache.druid.math.expr.ExprMacroTable;
 import org.apache.druid.query.expression.TimestampFloorExprMacro;
@@ -38,13 +38,14 @@ import org.apache.druid.sql.calcite.expression.DruidExpression;
 import org.apache.druid.sql.calcite.expression.Expressions;
 import org.apache.druid.sql.calcite.expression.OperatorConversions;
 import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.expression.TimeUnits;
 import org.apache.druid.sql.calcite.planner.Calcites;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 import org.apache.druid.sql.calcite.table.RowSignature;
-import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
 import org.joda.time.Period;
 
+import javax.annotation.Nullable;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
@@ -59,6 +60,11 @@ public class TimeFloorOperatorConversion implements SqlOperatorConversion
       .functionCategory(SqlFunctionCategory.TIMEDATE)
       .build();
 
+  /**
+   * Function that floors a DruidExpression to a particular granularity. Not actually used by the
+   * TimeFloorOperatorConversion, but I'm not sure where else to put this. It makes some sense in this file, since
+   * it's responsible for generating "timestamp_floor" calls.
+   */
   public static DruidExpression applyTimestampFloor(
       final DruidExpression input,
       final PeriodGranularity granularity,
@@ -98,6 +104,74 @@ public class TimeFloorOperatorConversion implements SqlOperatorConversion
     );
   }
 
+  /**
+   * Function that converts SQL TIME_FLOOR or TIME_CEIL args to Druid expression "timestamp_floor" or "timestamp_ceil"
+   * args. The main reason this function is necessary is because the handling of origin and timezone must take into
+   * account the SQL context timezone. It also helps with handling SQL FLOOR and CEIL, by offering handling of
+   * TimeUnitRange args.
+   */
+  @Nullable
+  public static List<DruidExpression> toTimestampFloorOrCeilArgs(
+      final PlannerContext plannerContext,
+      final RowSignature rowSignature,
+      final List<RexNode> operands
+  )
+  {
+    final List<DruidExpression> functionArgs = new ArrayList<>();
+
+    // Timestamp
+    functionArgs.add(Expressions.toDruidExpression(plannerContext, rowSignature, operands.get(0)));
+
+    // Period
+    final RexNode periodOperand = operands.get(1);
+    if (periodOperand.isA(SqlKind.LITERAL) && RexLiteral.value(periodOperand) instanceof TimeUnitRange) {
+      // TimeUnitRange literals are used by FLOOR(t TO unit) and CEIL(t TO unit)
+      final Period period = TimeUnits.toPeriod((TimeUnitRange) RexLiteral.value(periodOperand));
+
+      if (period == null) {
+        // Unrecognized time unit, bail out.
+        return null;
+      }
+
+      functionArgs.add(DruidExpression.fromExpression(DruidExpression.stringLiteral(period.toString())));
+    } else {
+      // Other literal types are used by TIME_FLOOR and TIME_CEIL
+      functionArgs.add(Expressions.toDruidExpression(plannerContext, rowSignature, periodOperand));
+    }
+
+    // Origin
+    functionArgs.add(
+        OperatorConversions.getOperandWithDefault(
+            operands,
+            2,
+            operand -> {
+              if (operand.isA(SqlKind.LITERAL)) {
+                return DruidExpression.fromExpression(
+                    DruidExpression.numberLiteral(
+                        Calcites.calciteDateTimeLiteralToJoda(operand, plannerContext.getTimeZone()).getMillis()
+                    )
+                );
+              } else {
+                return Expressions.toDruidExpression(plannerContext, rowSignature, operand);
+              }
+            },
+            DruidExpression.fromExpression(DruidExpression.nullLiteral())
+        )
+    );
+
+    // Time zone
+    functionArgs.add(
+        OperatorConversions.getOperandWithDefault(
+            operands,
+            3,
+            operand -> Expressions.toDruidExpression(plannerContext, rowSignature, operand),
+            DruidExpression.fromExpression(DruidExpression.stringLiteral(plannerContext.getTimeZone().getID()))
+        )
+    );
+
+    return functionArgs.stream().noneMatch(Objects::isNull) ? functionArgs : null;
+  }
+
   private static boolean periodIsDayMultiple(final Period period)
   {
     return period.getMillis() == 0
@@ -114,6 +188,7 @@ public class TimeFloorOperatorConversion implements SqlOperatorConversion
   }
 
   @Override
+  @Nullable
   public DruidExpression toDruidExpression(
       final PlannerContext plannerContext,
       final RowSignature rowSignature,
@@ -121,40 +196,16 @@ public class TimeFloorOperatorConversion implements SqlOperatorConversion
   )
   {
     final RexCall call = (RexCall) rexNode;
-    final List<RexNode> operands = call.getOperands();
-    final List<DruidExpression> druidExpressions = Expressions.toDruidExpressions(
+    final List<DruidExpression> functionArgs = toTimestampFloorOrCeilArgs(
         plannerContext,
         rowSignature,
-        operands
+        call.getOperands()
     );
 
-    if (druidExpressions == null) {
+    if (functionArgs == null) {
       return null;
-    } else if (operands.get(1).isA(SqlKind.LITERAL)
-               && (operands.size() <= 2 || operands.get(2).isA(SqlKind.LITERAL))
-               && (operands.size() <= 3 || operands.get(3).isA(SqlKind.LITERAL))) {
-      // Granularity is a literal. Special case since we can use an extractionFn here.
-      final Period period = new Period(RexLiteral.stringValue(operands.get(1)));
-
-      final DateTime origin = OperatorConversions.getOperandWithDefault(
-          call.getOperands(),
-          2,
-          operand -> Calcites.calciteDateTimeLiteralToJoda(operands.get(2), plannerContext.getTimeZone()),
-          null
-      );
-
-      final DateTimeZone timeZone = OperatorConversions.getOperandWithDefault(
-          call.getOperands(),
-          3,
-          operand -> DateTimes.inferTzFromString(RexLiteral.stringValue(operand)),
-          plannerContext.getTimeZone()
-      );
-
-      final PeriodGranularity granularity = new PeriodGranularity(period, origin, timeZone);
-      return applyTimestampFloor(druidExpressions.get(0), granularity, plannerContext.getExprMacroTable());
-    } else {
-      // Granularity is dynamic
-      return DruidExpression.fromFunctionCall("timestamp_floor", druidExpressions);
     }
+
+    return DruidExpression.fromFunctionCall("timestamp_floor", functionArgs);
   }
 }
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
index e0cbf73..4a3ac99 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -87,6 +87,7 @@ import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.SubstringOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TextcatOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeArithmeticOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.TimeCeilOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeExtractOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeFloorOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeFormatOperatorConversion;
@@ -186,6 +187,7 @@ public class DruidOperatorTable implements SqlOperatorTable
           .add(new TimeArithmeticOperatorConversion.TimeMinusIntervalOperatorConversion())
           .add(new TimeArithmeticOperatorConversion.TimePlusIntervalOperatorConversion())
           .add(new TimeExtractOperatorConversion())
+          .add(new TimeCeilOperatorConversion())
           .add(new TimeFloorOperatorConversion())
           .add(new TimeFormatOperatorConversion())
           .add(new TimeParseOperatorConversion())
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
index fc46a0a..3a5ba95 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
@@ -53,6 +53,7 @@ import org.apache.druid.sql.calcite.expression.builtin.RightOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.RoundOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StringFormatOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.TimeCeilOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeExtractOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeFloorOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.TimeFormatOperatorConversion;
@@ -520,7 +521,8 @@ public class ExpressionsTest extends CalciteTestBase
     final SqlFunction roundFunction = new RoundOperatorConversion().calciteOperator();
 
     expectedException.expect(IAE.class);
-    expectedException.expectMessage("The first argument to the function[round] should be integer or double type but get the STRING type");
+    expectedException.expectMessage(
+        "The first argument to the function[round] should be integer or double type but get the STRING type");
     testExpression(
         rexBuilder.makeCall(roundFunction, inputRef("s")),
         DruidExpression.fromExpression("round(\"s\")"),
@@ -534,7 +536,8 @@ public class ExpressionsTest extends CalciteTestBase
     final SqlFunction roundFunction = new RoundOperatorConversion().calciteOperator();
 
     expectedException.expect(IAE.class);
-    expectedException.expectMessage("The second argument to the function[round] should be integer type but get the STRING type");
+    expectedException.expectMessage(
+        "The second argument to the function[round] should be integer type but get the STRING type");
     testExpression(
         rexBuilder.makeCall(roundFunction, inputRef("x"), rexBuilder.makeLiteral("foo")),
         DruidExpression.fromExpression("round(\"x\",'foo')"),
@@ -673,6 +676,32 @@ public class ExpressionsTest extends CalciteTestBase
   }
 
   @Test
+  public void testTimeCeil()
+  {
+    testExpression(
+        rexBuilder.makeCall(
+            new TimeCeilOperatorConversion().calciteOperator(),
+            timestampLiteral(DateTimes.of("2000-02-03T04:05:06Z")),
+            rexBuilder.makeLiteral("PT1H")
+        ),
+        DruidExpression.fromExpression("timestamp_ceil(949550706000,'PT1H',null,'UTC')"),
+        DateTimes.of("2000-02-03T05:00:00").getMillis()
+    );
+
+    testExpression(
+        rexBuilder.makeCall(
+            new TimeCeilOperatorConversion().calciteOperator(),
+            inputRef("t"),
+            rexBuilder.makeLiteral("P1D"),
+            rexBuilder.makeNullLiteral(typeFactory.createSqlType(SqlTypeName.TIMESTAMP)),
+            rexBuilder.makeLiteral("America/Los_Angeles")
+        ),
+        DruidExpression.fromExpression("timestamp_ceil(\"t\",'P1D',null,'America/Los_Angeles')"),
+        DateTimes.of("2000-02-03T08:00:00").getMillis()
+    );
+  }
+
+  @Test
   public void testOtherTimeCeil()
   {
     // CEIL(__time TO unit)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org