/* * 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.logical; import static org.apache.drill.exec.planner.logical.DrillOptiq.isLiteralNull; import java.io.IOException; import java.math.BigDecimal; import java.util.GregorianCalendar; import java.util.List; import com.google.common.collect.ImmutableList; import org.apache.calcite.rel.AbstractRelNode; import org.apache.calcite.rel.RelWriter; import org.apache.calcite.rel.metadata.RelMetadataQuery; import org.apache.calcite.rel.type.RelDataTypeField; import org.apache.calcite.sql.SqlExplainLevel; import org.apache.calcite.sql.type.SqlTypeUtil; import org.apache.calcite.util.NlsString; import org.apache.calcite.util.Pair; import org.apache.drill.common.JSONOptions; import org.apache.drill.common.exceptions.DrillRuntimeException; import org.apache.drill.common.logical.data.LogicalOperator; import org.apache.calcite.rel.RelNode; import org.apache.calcite.plan.RelOptCluster; import org.apache.calcite.plan.RelOptCost; import org.apache.calcite.plan.RelOptPlanner; import org.apache.calcite.plan.RelTraitSet; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rex.RexLiteral; import com.fasterxml.jackson.core.JsonLocation; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.util.TokenBuffer; import org.apache.drill.common.logical.data.Values; import org.apache.drill.exec.vector.complex.fn.ExtendedJsonOutput; import org.apache.drill.exec.vector.complex.fn.JsonOutput; import org.joda.time.DateTime; import org.joda.time.Period; /** * Values implemented in Drill. */ public class DrillValuesRel extends AbstractRelNode implements DrillRel { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillValuesRel.class); private static final ObjectMapper MAPPER = new ObjectMapper(); private static final long MILLIS_IN_DAY = 1000*60*60*24; private final JSONOptions options; private final double rowCount; protected DrillValuesRel(RelOptCluster cluster, RelDataType rowType, ImmutableList<ImmutableList<RexLiteral>> tuples, RelTraitSet traits) { super(cluster, traits); assert getConvention() == DRILL_LOGICAL; verifyRowType(tuples, rowType); this.rowType = rowType; this.rowCount = tuples.size(); try{ this.options = new JSONOptions(convertToJsonNode(rowType, tuples), JsonLocation.NA); }catch(IOException e){ throw new DrillRuntimeException("Failure while attempting to encode ValuesRel in JSON.", e); } } private DrillValuesRel(RelOptCluster cluster, RelDataType rowType, RelTraitSet traits, JSONOptions options, double rowCount){ super(cluster, traits); this.options = options; this.rowCount = rowCount; this.rowType = rowType; } private static void verifyRowType(final ImmutableList<ImmutableList<RexLiteral>> tuples, RelDataType rowType){ for (List<RexLiteral> tuple : tuples) { assert (tuple.size() == rowType.getFieldCount()); for (Pair<RexLiteral, RelDataTypeField> pair : Pair.zip(tuple, rowType.getFieldList())) { RexLiteral literal = pair.left; RelDataType fieldType = pair.right.getType(); if ((!(RexLiteral.isNullLiteral(literal))) && (!(SqlTypeUtil.canAssignFrom(fieldType, literal.getType())))) { throw new AssertionError("to " + fieldType + " from " + literal); } } } } public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq) { return planner.getCostFactory().makeCost(this.rowCount, 1.0d, 0.0d); } @Override public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs) { assert inputs.isEmpty(); return new DrillValuesRel(getCluster(), rowType, traitSet, options, rowCount); } @Override public LogicalOperator implement(DrillImplementor implementor) { return Values.builder() .content(options.asNode()) .build(); } public JSONOptions getTuplesAsJsonOptions() throws IOException { return options; } public double estimateRowCount(RelMetadataQuery mq) { return rowCount; } public RelWriter explainTerms(RelWriter pw) { return super.explainTerms(pw) .itemIf("type", this.rowType, pw.getDetailLevel() == SqlExplainLevel.DIGEST_ATTRIBUTES) .itemIf("type", this.rowType.getFieldList(), pw.nest()) .itemIf("tuplesCount", rowCount, pw.getDetailLevel() != SqlExplainLevel.ALL_ATTRIBUTES) .itemIf("tuples", options.asNode(), pw.getDetailLevel() == SqlExplainLevel.ALL_ATTRIBUTES); } private static JsonNode convertToJsonNode(RelDataType rowType, ImmutableList<ImmutableList<RexLiteral>> tuples) throws IOException{ TokenBuffer out = new TokenBuffer(MAPPER.getFactory().getCodec(), false); JsonOutput json = new ExtendedJsonOutput(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, BigDecimal.ROUND_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, BigDecimal.ROUND_HALF_UP)).intValue()); } return; case DECIMAL: if (isLiteralNull(literal)) { out.writeDoubleNull(); }else{ out.writeDouble(((BigDecimal) literal.getValue()).doubleValue()); } logger.warn("Converting exact decimal into approximate decimal. Should be fixed once decimal is implemented."); 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(new DateTime((GregorianCalendar)literal.getValue())); } return; case TIME: if (isLiteralNull(literal)) { out.writeTimeNull(); }else{ out.writeTime(new DateTime((GregorianCalendar)literal.getValue())); } return; case TIMESTAMP: if (isLiteralNull(literal)) { out.writeTimestampNull(); }else{ out.writeTimestamp(new DateTime((GregorianCalendar)literal.getValue())); } return; case INTERVAL_YEAR_MONTH: if (isLiteralNull(literal)) { out.writeIntervalNull(); }else{ int months = ((BigDecimal) (literal.getValue())).intValue(); out.writeInterval(new Period().plusMonths(months)); } return; case INTERVAL_DAY_TIME: if (isLiteralNull(literal)) { out.writeIntervalNull(); }else{ long millis = ((BigDecimal) (literal.getValue())).longValue(); int days = (int) (millis/MILLIS_IN_DAY); millis = millis - (days * MILLIS_IN_DAY); out.writeInterval(new Period().plusDays(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())); } } }