blob: 233414c20b3c656d5de5845841ab29728dbba390 [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.drill.exec.planner.common;
import static org.apache.drill.exec.planner.logical.DrillOptiq.isLiteralNull;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.core.Values;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.util.NlsString;
import org.apache.drill.common.exceptions.DrillRuntimeException;
import org.apache.drill.common.util.GuavaUtils;
import org.apache.drill.common.util.JacksonUtils;
import org.apache.drill.exec.vector.complex.fn.BasicJsonOutput;
import org.apache.drill.exec.vector.complex.fn.JsonOutput;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.Period;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.TokenBuffer;
/**
* Base class for logical and physical Values implemented in Drill.
*/
public abstract class DrillValuesRelBase extends Values implements DrillRelNode {
private static final ObjectMapper MAPPER = JacksonUtils.createObjectMapper();
protected final String content;
public DrillValuesRelBase(RelOptCluster cluster, RelDataType rowType, List<? extends List<RexLiteral>> tuples, RelTraitSet traits) {
this(cluster, rowType, tuples, traits, convertToJsonOptions(rowType, tuples));
}
/**
* This constructor helps to avoid unnecessary tuples parsing into json options
* during copying or logical to physical values conversion.
*/
public DrillValuesRelBase(RelOptCluster cluster,
RelDataType rowType,
List<? extends List<RexLiteral>> tuples,
RelTraitSet traits,
String content) {
super(cluster, rowType, GuavaUtils.convertToNestedUnshadedImmutableList(tuples), traits);
this.content = content;
}
/**
* @return values content represented as json string
*/
public String getContent() {
return content;
}
/**
* Converts tuples into json representation taking into account row type.
* Example: [['A']] -> [{"EXPR$0":"A"}], [[1]] -> [{"EXPR$0":{"$numberLong":1}}]
*
* @param rowType row type
* @param tuples list of constant values in a row-expression
* @return json representation of tuples
*/
private static String convertToJsonOptions(RelDataType rowType, List<? extends List<RexLiteral>> tuples) {
try {
return MAPPER.writeValueAsString(convertToJsonNode(rowType, tuples));
} catch (IOException e) {
throw new DrillRuntimeException("Failure while attempting to encode Values in JSON.", e);
}
}
private static JsonNode convertToJsonNode(RelDataType rowType, List<? extends List<RexLiteral>> tuples) throws IOException {
TokenBuffer out = new TokenBuffer(MAPPER.getFactory().getCodec(), false);
JsonOutput json = new BasicJsonOutput(out);
json.writeStartArray();
String[] fields = rowType.getFieldNames().toArray(new String[rowType.getFieldCount()]);
for (List<RexLiteral> row : tuples) {
json.writeStartObject();
int i = 0;
for (RexLiteral field : row) {
json.writeFieldName(fields[i]);
writeLiteral(field, json);
i++;
}
json.writeEndObject();
}
json.writeEndArray();
json.flush();
return out.asParser().readValueAsTree();
}
private static void writeLiteral(RexLiteral literal, JsonOutput out) throws IOException {
switch (literal.getType().getSqlTypeName()) {
case BIGINT:
if (isLiteralNull(literal)) {
out.writeBigIntNull();
} else {
out.writeBigInt((((BigDecimal) literal.getValue()).setScale(0, RoundingMode.HALF_UP)).longValue());
}
return;
case BOOLEAN:
if (isLiteralNull(literal)) {
out.writeBooleanNull();
} else {
out.writeBoolean((Boolean) literal.getValue());
}
return;
case CHAR:
if (isLiteralNull(literal)) {
out.writeVarcharNull();
} else {
// Since Calcite treats string literals as fixed char and adds trailing spaces to the strings to make them the
// same length, here we do an rtrim() to get the string without the trailing spaces. If we don't rtrim, the comparison
// with Drill's varchar column values would not return a match.
// TODO: However, note that if the user had explicitly added spaces in the string literals then even those would get
// trimmed, so this exposes another issue that needs to be resolved.
out.writeVarChar(((NlsString) literal.getValue()).rtrim().getValue());
}
return;
case DOUBLE:
if (isLiteralNull(literal)) {
out.writeDoubleNull();
} else {
out.writeDouble(((BigDecimal) literal.getValue()).doubleValue());
}
return;
case FLOAT:
if (isLiteralNull(literal)) {
out.writeFloatNull();
} else {
out.writeFloat(((BigDecimal) literal.getValue()).floatValue());
}
return;
case INTEGER:
if (isLiteralNull(literal)) {
out.writeIntNull();
} else {
out.writeInt((((BigDecimal) literal.getValue()).setScale(0, RoundingMode.HALF_UP)).intValue());
}
return;
case DECIMAL:
// Converting exact decimal into double since values in the list may have different scales
// so the resulting scale wouldn't be calculated correctly
if (isLiteralNull(literal)) {
out.writeDoubleNull();
} else {
out.writeDouble(((BigDecimal) literal.getValue()).doubleValue());
}
return;
case VARCHAR:
if (isLiteralNull(literal)) {
out.writeVarcharNull();
} else {
out.writeVarChar(((NlsString) literal.getValue()).getValue());
}
return;
case SYMBOL:
if (isLiteralNull(literal)) {
out.writeVarcharNull();
} else {
out.writeVarChar(literal.getValue().toString());
}
return;
case DATE:
if (isLiteralNull(literal)) {
out.writeDateNull();
} else {
out.writeDate(LocalDateTime.ofInstant(Instant.ofEpochMilli(new DateTime(literal.getValue()).getMillis()), ZoneOffset.UTC).toLocalDate());
}
return;
case TIME:
if (isLiteralNull(literal)) {
out.writeTimeNull();
} else {
out.writeTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(new DateTime(literal.getValue()).getMillis()), ZoneOffset.UTC).toLocalTime());
}
return;
case TIMESTAMP:
if (isLiteralNull(literal)) {
out.writeTimestampNull();
} else {
out.writeTimestamp(LocalDateTime.ofInstant(Instant.ofEpochMilli(new DateTime(literal.getValue()).getMillis()), ZoneOffset.UTC));
}
return;
case INTERVAL_YEAR:
case INTERVAL_YEAR_MONTH:
case INTERVAL_MONTH:
if (isLiteralNull(literal)) {
out.writeIntervalNull();
} else {
int months = ((BigDecimal) (literal.getValue())).intValue();
out.writeInterval(new Period().plusMonths(months));
}
return;
case INTERVAL_DAY:
case INTERVAL_DAY_HOUR:
case INTERVAL_DAY_MINUTE:
case INTERVAL_DAY_SECOND:
case INTERVAL_HOUR:
case INTERVAL_HOUR_MINUTE:
case INTERVAL_HOUR_SECOND:
case INTERVAL_MINUTE:
case INTERVAL_MINUTE_SECOND:
case INTERVAL_SECOND:
if (isLiteralNull(literal)) {
out.writeIntervalNull();
} else {
long millis = ((BigDecimal) (literal.getValue())).longValue();
long days = millis / DateTimeConstants.MILLIS_PER_DAY;
millis = millis - (days * DateTimeConstants.MILLIS_PER_DAY);
out.writeInterval(new Period().plusDays((int) days).plusMillis((int) millis));
}
return;
case NULL:
out.writeUntypedNull();
return;
case ANY:
default:
throw new UnsupportedOperationException(
String.format("Unable to convert the value of %s and type %s to a Drill constant expression.",
literal, literal.getType().getSqlTypeName()));
}
}
}