blob: e02cd645894506f2ca146568fc8389e64d841beb [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.table;
import com.google.common.base.Preconditions;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeComparability;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperandCountRange;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.type.AbstractSqlType;
import org.apache.calcite.sql.type.SqlOperandCountRanges;
import org.apache.calcite.sql.type.SqlSingleOperandTypeChecker;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.druid.common.config.NullHandling;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.query.ordering.StringComparator;
import org.apache.druid.query.ordering.StringComparators;
import org.apache.druid.segment.column.ColumnHolder;
import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.column.RowSignature;
import org.apache.druid.segment.column.ValueType;
import org.apache.druid.segment.serde.ComplexMetricSerde;
import org.apache.druid.segment.serde.ComplexMetrics;
import org.apache.druid.sql.calcite.expression.SimpleExtraction;
import org.apache.druid.sql.calcite.planner.Calcites;
import javax.annotation.Nonnull;
import java.util.List;
/**
* Utility functions for working with {@link RowSignature}.
*/
public class RowSignatures
{
private RowSignatures()
{
// No instantiation.
}
public static RowSignature fromRelDataType(final List<String> rowOrder, final RelDataType rowType)
{
if (rowOrder.size() != rowType.getFieldCount()) {
throw new IAE("Field count %d != %d", rowOrder.size(), rowType.getFieldCount());
}
final RowSignature.Builder rowSignatureBuilder = RowSignature.builder();
for (int i = 0; i < rowOrder.size(); i++) {
final RelDataType dataType = rowType.getFieldList().get(i).getType();
final ColumnType valueType = Calcites.getColumnTypeForRelDataType(dataType);
rowSignatureBuilder.add(rowOrder.get(i), valueType);
}
return rowSignatureBuilder.build();
}
/**
* Return the "natural" {@link StringComparator} for an extraction from a row signature. This will be a
* lexicographic comparator for String types and a numeric comparator for Number types.
*/
@Nonnull
public static StringComparator getNaturalStringComparator(
final RowSignature rowSignature,
final SimpleExtraction simpleExtraction
)
{
Preconditions.checkNotNull(simpleExtraction, "simpleExtraction");
if (simpleExtraction.getExtractionFn() != null
|| rowSignature.getColumnType(simpleExtraction.getColumn())
.map(type -> type.is(ValueType.STRING))
.orElse(false)) {
return StringComparators.LEXICOGRAPHIC;
} else {
return StringComparators.NUMERIC;
}
}
/**
* Returns a Calcite {@link RelDataType} corresponding to a {@link RowSignature}. It will typecast __time column to
* TIMESTAMP irrespective of the type present in the row signature
*/
public static RelDataType toRelDataType(final RowSignature rowSignature, final RelDataTypeFactory typeFactory)
{
return toRelDataType(rowSignature, typeFactory, true);
}
/**
* Returns a Calcite {@link RelDataType} corresponding to a {@link RowSignature}. For columns that are named
* "__time", it automatically casts it to TIMESTAMP if typecastTimeColumn is set to true
*/
public static RelDataType toRelDataType(
final RowSignature rowSignature,
final RelDataTypeFactory typeFactory,
boolean typecastTimeColumn
)
{
final RelDataTypeFactory.Builder builder = typeFactory.builder();
final boolean nullNumeric = !NullHandling.replaceWithDefault();
for (final String columnName : rowSignature.getColumnNames()) {
final RelDataType type;
if (typecastTimeColumn && ColumnHolder.TIME_COLUMN_NAME.equals(columnName)) {
type = Calcites.createSqlType(typeFactory, SqlTypeName.TIMESTAMP);
} else {
final ColumnType columnType =
rowSignature.getColumnType(columnName)
.orElseThrow(() -> new ISE("Encountered null type for column[%s]", columnName));
type = columnTypeToRelDataType(typeFactory, columnType, nullNumeric);
}
builder.add(columnName, type);
}
return builder.build();
}
/**
* Returns a Calcite {@link RelDataType} corresponding to a {@link ColumnType}
*/
public static RelDataType columnTypeToRelDataType(
RelDataTypeFactory typeFactory,
ColumnType columnType,
boolean nullNumeric
)
{
final RelDataType type;
switch (columnType.getType()) {
case STRING:
// Note that there is no attempt here to handle multi-value in any special way. Maybe one day...
type = Calcites.createSqlTypeWithNullability(typeFactory, SqlTypeName.VARCHAR, true);
break;
case LONG:
type = Calcites.createSqlTypeWithNullability(typeFactory, SqlTypeName.BIGINT, nullNumeric);
break;
case FLOAT:
type = Calcites.createSqlTypeWithNullability(typeFactory, SqlTypeName.FLOAT, nullNumeric);
break;
case DOUBLE:
type = Calcites.createSqlTypeWithNullability(typeFactory, SqlTypeName.DOUBLE, nullNumeric);
break;
case ARRAY:
final RelDataType elementType = columnTypeToRelDataType(
typeFactory,
(ColumnType) columnType.getElementType(),
nullNumeric
);
type = typeFactory.createTypeWithNullability(
typeFactory.createArrayType(elementType, -1),
true
);
break;
case COMPLEX:
type = makeComplexType(typeFactory, columnType, true);
break;
default:
throw new ISE("valueType[%s] not translatable", columnType);
} return type;
}
/**
* Creates a {@link ComplexSqlType} using the supplied {@link RelDataTypeFactory} to ensure that the
* {@link ComplexSqlType} is interned. This is important because Calcite checks that the references are equal
* instead of the objects being equivalent.
* <p>
* This method uses {@link RelDataTypeFactory#createTypeWithNullability(RelDataType, boolean) ensures that if the
* type factory is a {@link org.apache.calcite.rel.type.RelDataTypeFactoryImpl} that the type is passed through
* {@link org.apache.calcite.rel.type.RelDataTypeFactoryImpl#canonize(RelDataType)} which interns the type.
*/
public static RelDataType makeComplexType(RelDataTypeFactory typeFactory, ColumnType columnType, boolean isNullable)
{
return typeFactory.createTypeWithNullability(
new ComplexSqlType(SqlTypeName.OTHER, columnType, isNullable),
isNullable
);
}
/**
* Calcite {@link RelDataType} for Druid complex columns, to preserve complex type information.
* <p>
* If using with other operations of a {@link RelDataTypeFactory}, consider wrapping the creation of this type in
* {@link RelDataTypeFactory#createTypeWithNullability(RelDataType, boolean) to ensure that if the type factory is a
* {@link org.apache.calcite.rel.type.RelDataTypeFactoryImpl} that the type is passed through
* {@link org.apache.calcite.rel.type.RelDataTypeFactoryImpl#canonize(RelDataType)} which interns the type.
* <p>
* If {@link SqlTypeName} is going to be {@link SqlTypeName#OTHER} and a {@link RelDataTypeFactory} is available,
* consider using {@link #makeComplexType(RelDataTypeFactory, ColumnType, boolean)}.
* <p>
* This type does not work well with {@link org.apache.calcite.sql.type.ReturnTypes#explicit(RelDataType)}, which
* will create new {@link RelDataType} using {@link SqlTypeName} during return type inference, so implementors of
* {@link org.apache.druid.sql.calcite.expression.SqlOperatorConversion} should implement the
* {@link org.apache.calcite.sql.type.SqlReturnTypeInference} directly for best results.
*/
public static final class ComplexSqlType extends AbstractSqlType
{
private final ColumnType columnType;
public ComplexSqlType(
SqlTypeName typeName,
ColumnType columnType,
boolean isNullable
)
{
super(typeName, isNullable, null);
// homogenize complex type names to common name
final ComplexMetricSerde serde = columnType.getComplexTypeName() != null
?
ComplexMetrics.getSerdeForType(columnType.getComplexTypeName())
: null;
if (serde != null) {
this.columnType = ColumnType.ofComplex(serde.getTypeName());
} else {
this.columnType = columnType;
}
this.computeDigest();
}
@Override
public RelDataTypeComparability getComparability()
{
return RelDataTypeComparability.UNORDERED;
}
@Override
protected void generateTypeString(StringBuilder sb, boolean withDetail)
{
sb.append(columnType.asTypeString());
}
public ColumnType getColumnType()
{
return columnType;
}
public String getComplexTypeName()
{
return columnType.getComplexTypeName();
}
public String asTypeString()
{
return columnType.asTypeString();
}
}
public static ComplexSqlSingleOperandTypeChecker complexTypeChecker(ColumnType complexType)
{
return new ComplexSqlSingleOperandTypeChecker(
new ComplexSqlType(SqlTypeName.OTHER, complexType, true)
);
}
public static final class ComplexSqlSingleOperandTypeChecker implements SqlSingleOperandTypeChecker
{
private final ComplexSqlType type;
public ComplexSqlSingleOperandTypeChecker(
ComplexSqlType type
)
{
this.type = type;
}
@Override
public boolean checkSingleOperandType(
SqlCallBinding callBinding,
SqlNode operand,
int iFormalOperand,
boolean throwOnFailure
)
{
return type.equals(callBinding.getValidator().deriveType(callBinding.getScope(), operand));
}
@Override
public boolean checkOperandTypes(SqlCallBinding callBinding, boolean throwOnFailure)
{
if (callBinding.getOperandCount() != 1) {
return false;
}
return checkSingleOperandType(callBinding, callBinding.operand(0), 0, throwOnFailure);
}
@Override
public SqlOperandCountRange getOperandCountRange()
{
return SqlOperandCountRanges.of(1);
}
@Override
public String getAllowedSignatures(SqlOperator op, String opName)
{
return StringUtils.format("'%s'(%s)", opName, type);
}
@Override
public Consistency getConsistency()
{
return Consistency.NONE;
}
@Override
public boolean isOptional(int i)
{
return false;
}
}
}