blob: 20377a03aacbd6d1c57744931879b5492700277f [file] [log] [blame]
/*
* 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);
}
}