| /* |
| * 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())); |
| } |
| } |
| |
| } |