| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.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; |
| import org.apache.calcite.sql.SqlFunction; |
| import org.apache.calcite.sql.SqlFunctionCategory; |
| 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.granularity.PeriodGranularity; |
| import org.apache.druid.math.expr.ExprMacroTable; |
| import org.apache.druid.query.expression.TimestampFloorExprMacro; |
| import org.apache.druid.segment.column.RowSignature; |
| 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.joda.time.Period; |
| |
| import javax.annotation.Nullable; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.stream.Collectors; |
| |
| public class TimeFloorOperatorConversion implements SqlOperatorConversion |
| { |
| private static final SqlFunction SQL_FUNCTION = OperatorConversions |
| .operatorBuilder("TIME_FLOOR") |
| .operandTypes(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER, SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER) |
| .requiredOperands(2) |
| .returnTypeCascadeNullable(SqlTypeName.TIMESTAMP) |
| .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, |
| final ExprMacroTable macroTable |
| ) |
| { |
| Preconditions.checkNotNull(input, "input"); |
| Preconditions.checkNotNull(granularity, "granularity"); |
| |
| // Collapse floor chains if possible. Useful for constructs like CAST(FLOOR(__time TO QUARTER) AS DATE). |
| if (granularity.getPeriod().equals(Period.days(1))) { |
| final TimestampFloorExprMacro.TimestampFloorExpr floorExpr = Expressions.asTimestampFloorExpr( |
| input, |
| macroTable |
| ); |
| |
| if (floorExpr != null) { |
| final PeriodGranularity inputGranularity = floorExpr.getGranularity(); |
| if (Objects.equals(inputGranularity.getTimeZone(), granularity.getTimeZone()) |
| && Objects.equals(inputGranularity.getOrigin(), granularity.getOrigin()) |
| && periodIsDayMultiple(inputGranularity.getPeriod())) { |
| return input; |
| } |
| } |
| } |
| |
| return DruidExpression.fromFunctionCall( |
| "timestamp_floor", |
| ImmutableList.of( |
| input.getExpression(), |
| DruidExpression.stringLiteral(granularity.getPeriod().toString()), |
| DruidExpression.numberLiteral( |
| granularity.getOrigin() == null ? null : granularity.getOrigin().getMillis() |
| ), |
| DruidExpression.stringLiteral(granularity.getTimeZone().toString()) |
| ).stream().map(DruidExpression::fromExpression).collect(Collectors.toList()) |
| ); |
| } |
| |
| /** |
| * 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 |
| && period.getSeconds() == 0 |
| && period.getMinutes() == 0 |
| && period.getHours() == 0 |
| && (period.getDays() > 0 || period.getWeeks() > 0 || period.getMonths() > 0 || period.getYears() > 0); |
| } |
| |
| @Override |
| public SqlOperator calciteOperator() |
| { |
| return SQL_FUNCTION; |
| } |
| |
| @Override |
| @Nullable |
| public DruidExpression toDruidExpression( |
| final PlannerContext plannerContext, |
| final RowSignature rowSignature, |
| final RexNode rexNode |
| ) |
| { |
| final RexCall call = (RexCall) rexNode; |
| final List<DruidExpression> functionArgs = toTimestampFloorOrCeilArgs( |
| plannerContext, |
| rowSignature, |
| call.getOperands() |
| ); |
| |
| if (functionArgs == null) { |
| return null; |
| } |
| |
| return DruidExpression.fromFunctionCall("timestamp_floor", functionArgs); |
| } |
| } |