blob: dcfb7d98db771e50faf16b2e2cfa19ef33c9853b [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.calcite.adapter.druid;
import org.apache.calcite.avatica.util.DateTimeUtils;
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.SqlKind;
import org.apache.calcite.sql.type.SqlTypeName;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.util.Locale;
import java.util.TimeZone;
import javax.annotation.Nullable;
import static org.apache.calcite.adapter.druid.DruidQuery.writeFieldIf;
import static org.apache.calcite.util.DateTimeStringUtils.ISO_DATETIME_FRACTIONAL_SECOND_FORMAT;
/**
* Implementation of Druid time format extraction function.
*
* <p>These functions return the dimension value formatted according to the given format string,
* time zone, and locale.
*
* <p>For __time dimension values, this formats the time value bucketed by the aggregation
* granularity.
*/
public class TimeExtractionFunction implements ExtractionFunction {
private static final ImmutableSet<TimeUnitRange> VALID_TIME_EXTRACT = Sets.immutableEnumSet(
TimeUnitRange.YEAR,
TimeUnitRange.MONTH,
TimeUnitRange.DAY,
TimeUnitRange.WEEK,
TimeUnitRange.HOUR,
TimeUnitRange.MINUTE,
TimeUnitRange.SECOND);
private static final ImmutableSet<TimeUnitRange> VALID_TIME_FLOOR = Sets.immutableEnumSet(
TimeUnitRange.YEAR,
TimeUnitRange.QUARTER,
TimeUnitRange.MONTH,
TimeUnitRange.DAY,
TimeUnitRange.WEEK,
TimeUnitRange.HOUR,
TimeUnitRange.MINUTE,
TimeUnitRange.SECOND);
private final String format;
private final Granularity granularity;
private final String timeZone;
private final String local;
public TimeExtractionFunction(String format, Granularity granularity, String timeZone,
String local) {
this.format = format;
this.granularity = granularity;
this.timeZone = timeZone;
this.local = local;
}
@Override public void write(JsonGenerator generator) throws IOException {
generator.writeStartObject();
generator.writeStringField("type", "timeFormat");
writeFieldIf(generator, "format", format);
writeFieldIf(generator, "granularity", granularity);
writeFieldIf(generator, "timeZone", timeZone);
writeFieldIf(generator, "locale", local);
generator.writeEndObject();
}
public String getFormat() {
return format;
}
public Granularity getGranularity() {
return granularity;
}
/**
* Creates the default time format extraction function.
*
* @return the time extraction function
*/
public static TimeExtractionFunction createDefault(String timeZone) {
return new TimeExtractionFunction(ISO_DATETIME_FRACTIONAL_SECOND_FORMAT,
null, timeZone, null);
}
/**
* Creates the time format extraction function for the given granularity.
*
* @param granularity granularity to apply to the column
* @return the time extraction function corresponding to the granularity input unit
* {@link TimeExtractionFunction#VALID_TIME_EXTRACT} for supported granularity
*/
public static TimeExtractionFunction createExtractFromGranularity(
Granularity granularity, String timeZone) {
final String local = Locale.US.toLanguageTag();
switch (granularity.getType()) {
case DAY:
return new TimeExtractionFunction("d", null, timeZone, local);
case MONTH:
return new TimeExtractionFunction("M", null, timeZone, local);
case YEAR:
return new TimeExtractionFunction("yyyy", null, timeZone, local);
case WEEK:
return new TimeExtractionFunction("w", null, timeZone, local);
case HOUR:
return new TimeExtractionFunction("H", null, timeZone, local);
case MINUTE:
return new TimeExtractionFunction("m", null, timeZone, local);
case SECOND:
return new TimeExtractionFunction("s", null, timeZone, local);
default:
throw new IllegalArgumentException("Granularity [" + granularity + "] is not supported");
}
}
/**
* Creates time format floor time extraction function using a given granularity.
*
* @param granularity granularity to apply to the column
* @return the time extraction function or null if granularity is not supported
*/
public static TimeExtractionFunction createFloorFromGranularity(
Granularity granularity, String timeZone) {
return new TimeExtractionFunction(ISO_DATETIME_FRACTIONAL_SECOND_FORMAT, granularity, timeZone,
Locale.ROOT.toLanguageTag());
}
/**
* Returns whether the RexCall contains a valid extract unit that we can
* serialize to Druid.
*
* @param rexNode Extract expression
*
* @return true if the extract unit is valid
*/
public static boolean isValidTimeExtract(RexNode rexNode) {
final RexCall call = (RexCall) rexNode;
if (call.getKind() != SqlKind.EXTRACT || call.getOperands().size() != 2) {
return false;
}
final RexLiteral flag = (RexLiteral) call.operands.get(0);
final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
return timeUnit != null && VALID_TIME_EXTRACT.contains(timeUnit);
}
/**
* Returns whether the RexCall contains a valid FLOOR unit that we can
* serialize to Druid.
*
* @param rexNode Extract expression
*
* @return true if the extract unit is valid
*/
public static boolean isValidTimeFloor(RexNode rexNode) {
if (rexNode.getKind() != SqlKind.FLOOR) {
return false;
}
final RexCall call = (RexCall) rexNode;
if (call.operands.size() != 2) {
return false;
}
final RexLiteral flag = (RexLiteral) call.operands.get(1);
final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
return timeUnit != null && VALID_TIME_FLOOR.contains(timeUnit);
}
/** Translates a CAST expression to a Druid Time extraction function, or null
* when can not translate the cast.
*
* @param rexNode CAST RexNode
* @param timeZone Timezone
*/
@Nullable
public static TimeExtractionFunction translateCastToTimeExtract(RexNode rexNode,
TimeZone timeZone) {
assert rexNode.getKind() == SqlKind.CAST;
final RexCall rexCall = (RexCall) rexNode;
final String castFormat = DruidSqlCastConverter
.dateTimeFormatString(rexCall.getType().getSqlTypeName());
final String timeZoneId = timeZone == null ? null : timeZone.getID();
if (castFormat == null) {
// unknown format
return null;
}
SqlTypeName fromType = rexCall.getOperands().get(0).getType().getSqlTypeName();
SqlTypeName toType = rexCall.getType().getSqlTypeName();
String granularityTZId;
switch (fromType) {
case DATE:
case TIMESTAMP:
granularityTZId = DateTimeUtils.UTC_ZONE.getID();
break;
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
granularityTZId = timeZoneId;
break;
default:
return null;
}
switch (toType) {
case DATE:
return new TimeExtractionFunction(castFormat,
Granularities.createGranularity(TimeUnitRange.DAY, granularityTZId),
DateTimeUtils.UTC_ZONE.getID(), Locale.ENGLISH.toString());
case TIMESTAMP:
// date -> timestamp: UTC
// timestamp -> timestamp: UTC
// timestamp with local time zone -> granularityTZId
return new TimeExtractionFunction(
castFormat, null, granularityTZId, Locale.ENGLISH.toString());
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
return new TimeExtractionFunction(
castFormat, null, DateTimeUtils.UTC_ZONE.getID(), Locale.ENGLISH.toString());
default:
return null;
}
}
}