SQL: Fix precision of TIMESTAMP types. (#5464)

Druid stores timestamps down to the millisecond, so we should use
precision = 3. Setting this wrong sometimes caused milliseconds
to be ignored in timestamp literals.

Fixes #5337.
This commit is contained in:
Gian Merlino 2018-03-05 21:56:52 -05:00 committed by Fangjin Yang
parent ff0de21fc5
commit 0f03ab0c74
7 changed files with 86 additions and 28 deletions

View File

@ -20,6 +20,7 @@
package io.druid.sql.calcite.expression;
import com.google.common.base.Preconditions;
import io.druid.sql.calcite.planner.Calcites;
import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.rex.RexCall;
@ -131,18 +132,16 @@ public class OperatorConversions
public OperatorBuilder returnType(final SqlTypeName typeName)
{
this.returnTypeInference = ReturnTypes.explicit(typeName);
this.returnTypeInference = ReturnTypes.explicit(
factory -> Calcites.createSqlType(factory, typeName)
);
return this;
}
public OperatorBuilder nullableReturnType(final SqlTypeName typeName)
{
this.returnTypeInference = ReturnTypes.explicit(
factory ->
factory.createTypeWithNullability(
factory.createSqlType(typeName),
true
)
factory -> Calcites.createSqlTypeWithNullability(factory, typeName, true)
);
return this;
}

View File

@ -32,10 +32,13 @@ import io.druid.server.security.AuthorizerMapper;
import io.druid.sql.calcite.schema.DruidSchema;
import io.druid.sql.calcite.schema.InformationSchema;
import org.apache.calcite.jdbc.CalciteSchema;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.ConversionUtil;
@ -163,6 +166,46 @@ public class Calcites
}
}
/**
* Like RelDataTypeFactory.createSqlType, but creates types that align best with how Druid represents them.
*/
public static RelDataType createSqlType(final RelDataTypeFactory typeFactory, final SqlTypeName typeName)
{
return createSqlTypeWithNullability(typeFactory, typeName, false);
}
/**
* Like RelDataTypeFactory.createSqlTypeWithNullability, but creates types that align best with how Druid
* represents them.
*/
public static RelDataType createSqlTypeWithNullability(
final RelDataTypeFactory typeFactory,
final SqlTypeName typeName,
final boolean nullable
)
{
final RelDataType dataType;
switch (typeName) {
case TIMESTAMP:
// Our timestamps are down to the millisecond (precision = 3).
dataType = typeFactory.createSqlType(typeName, 3);
break;
case CHAR:
case VARCHAR:
dataType = typeFactory.createTypeWithCharsetAndCollation(
typeFactory.createSqlType(typeName),
Calcites.defaultCharset(),
SqlCollation.IMPLICIT
);
break;
default:
dataType = typeFactory.createSqlType(typeName);
}
return typeFactory.createTypeWithNullability(dataType, nullable);
}
/**
* Calcite expects "TIMESTAMP" types to be an instant that has the expected local time fields if printed as UTC.
*

View File

@ -321,7 +321,7 @@ public class DruidPlanner implements Closeable
return new PlannerResult(
resultsSupplier,
typeFactory.createStructType(
ImmutableList.of(typeFactory.createSqlType(SqlTypeName.VARCHAR)),
ImmutableList.of(Calcites.createSqlType(typeFactory, SqlTypeName.VARCHAR)),
ImmutableList.of("PLAN")
)
);

View File

@ -93,9 +93,9 @@ public class DruidTypeSystem implements RelDataTypeSystem
// Widen all sums to 64-bits regardless of the size of the inputs.
if (SqlTypeName.INT_TYPES.contains(argumentType.getSqlTypeName())) {
return typeFactory.createSqlType(SqlTypeName.BIGINT);
return Calcites.createSqlType(typeFactory, SqlTypeName.BIGINT);
} else {
return typeFactory.createSqlType(SqlTypeName.DOUBLE);
return Calcites.createSqlType(typeFactory, SqlTypeName.DOUBLE);
}
}

View File

@ -111,7 +111,7 @@ public class CaseFilteredAggregatorRule extends RelOptRule
// Operand 1: Filter
final RexNode filter;
final RelDataType booleanType = typeFactory.createSqlType(SqlTypeName.BOOLEAN);
final RelDataType booleanType = Calcites.createSqlType(typeFactory, SqlTypeName.BOOLEAN);
final RexNode filterFromCase = rexBuilder.makeCall(
booleanType,
flip ? SqlStdOperatorTable.IS_FALSE : SqlStdOperatorTable.IS_TRUE,
@ -177,7 +177,7 @@ public class CaseFilteredAggregatorRule extends RelOptRule
false,
ImmutableList.of(),
newProjects.size() - 1,
typeFactory.createSqlType(SqlTypeName.BIGINT),
Calcites.createSqlType(typeFactory, SqlTypeName.BIGINT),
aggregateCall.getName()
);
} else if (RexLiteral.isNullLiteral(arg2) /* Case A1 */

View File

@ -36,7 +36,6 @@ import io.druid.sql.calcite.planner.Calcites;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.type.SqlTypeName;
import javax.annotation.Nonnull;
@ -152,35 +151,25 @@ public class RowSignature
final RelDataType type;
if (Column.TIME_COLUMN_NAME.equals(columnName)) {
type = typeFactory.createSqlType(SqlTypeName.TIMESTAMP);
type = Calcites.createSqlType(typeFactory, SqlTypeName.TIMESTAMP);
} else {
switch (columnType) {
case STRING:
// Note that there is no attempt here to handle multi-value in any special way. Maybe one day...
type = typeFactory.createTypeWithNullability(
typeFactory.createTypeWithCharsetAndCollation(
typeFactory.createSqlType(SqlTypeName.VARCHAR),
Calcites.defaultCharset(),
SqlCollation.IMPLICIT
),
true
);
type = Calcites.createSqlTypeWithNullability(typeFactory, SqlTypeName.VARCHAR, true);
break;
case LONG:
type = typeFactory.createSqlType(SqlTypeName.BIGINT);
type = Calcites.createSqlType(typeFactory, SqlTypeName.BIGINT);
break;
case FLOAT:
type = typeFactory.createSqlType(SqlTypeName.FLOAT);
type = Calcites.createSqlType(typeFactory, SqlTypeName.FLOAT);
break;
case DOUBLE:
type = typeFactory.createSqlType(SqlTypeName.DOUBLE);
type = Calcites.createSqlType(typeFactory, SqlTypeName.DOUBLE);
break;
case COMPLEX:
// Loses information about exactly what kind of complex column this is.
type = typeFactory.createTypeWithNullability(
typeFactory.createSqlType(SqlTypeName.OTHER),
true
);
type = Calcites.createSqlTypeWithNullability(typeFactory, SqlTypeName.OTHER, true);
break;
default:
throw new ISE("WTF?! valueType[%s] not translatable?", columnType);

View File

@ -2966,6 +2966,33 @@ public class CalciteQueryTest extends CalciteTestBase
);
}
@Test
public void testCountStarWithTimeMillisecondFilters() throws Exception
{
testQuery(
"SELECT COUNT(*) FROM druid.foo\n"
+ "WHERE __time = TIMESTAMP '2000-01-01 00:00:00.111'\n"
+ "OR (__time >= TIMESTAMP '2000-01-01 00:00:00.888' AND __time < TIMESTAMP '2000-01-02 00:00:00.222')",
ImmutableList.of(
Druids.newTimeseriesQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(
QSS(
Intervals.of("2000-01-01T00:00:00.111/2000-01-01T00:00:00.112"),
Intervals.of("2000-01-01T00:00:00.888/2000-01-02T00:00:00.222")
)
)
.granularity(Granularities.ALL)
.aggregators(AGGS(new CountAggregatorFactory("a0")))
.context(TIMESERIES_CONTEXT_DEFAULT)
.build()
),
ImmutableList.of(
new Object[]{1L}
)
);
}
@Test
public void testCountStarWithTimeFilterUsingStringLiterals() throws Exception
{