blob: 211a296a53aa99e557311ab9b4e3cc8e3a2caddb [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.impala.analysis;
import java.math.BigDecimal;
import org.apache.impala.catalog.ScalarType;
import org.apache.impala.catalog.Type;
import org.apache.impala.common.AnalysisException;
import com.google.common.base.Preconditions;
// Utility class for handling types.
public class TypesUtil {
// The sql standard specifies that the scale after division is incremented
// by a system wide constant. Hive picked 4 so we will as well.
// TODO: how did they pick this?
static final int DECIMAL_DIVISION_SCALE_INCREMENT = 4;
/**
* [1-9] precision -> 4 bytes
* [10-18] precision -> 8 bytes
* [19-38] precision -> 16 bytes
* TODO: Support 12 byte decimal?
* For precision [20-28], we could support a 12 byte decimal but currently a 12
* byte decimal in the BE is not implemented.
*/
public static int getDecimalSlotSize(ScalarType type) {
Preconditions.checkState(type.isDecimal() && !type.isWildcardDecimal());
if (type.decimalPrecision() <= 9) return 4;
if (type.decimalPrecision() <= 18) return 8;
return 16;
}
/**
* Returns the decimal type that can hold t1 and t2 without loss of precision.
* decimal(10, 2) && decimal(12, 2) -> decimal(12, 2)
* decimal (10, 5) && decimal(12, 3) -> decimal(14, 5)
* Either t1 or t2 can be a wildcard decimal (but not both).
*
* If strictDecimal is true, returns a type that results in no loss of information. If
* this is not possible, returns INVLAID. For example,
* decimal(38,0) && decimal(38,38) -> INVALID.
*/
public static ScalarType getDecimalAssignmentCompatibleType(
ScalarType t1, ScalarType t2, boolean strictDecimal) {
Preconditions.checkState(t1.isDecimal());
Preconditions.checkState(t2.isDecimal());
Preconditions.checkState(!(t1.isWildcardDecimal() && t2.isWildcardDecimal()));
if (t1.isWildcardDecimal()) return t2;
if (t2.isWildcardDecimal()) return t1;
Preconditions.checkState(t1.isFullySpecifiedDecimal());
Preconditions.checkState(t2.isFullySpecifiedDecimal());
if (t1.equals(t2)) return t1;
int s1 = t1.decimalScale();
int s2 = t2.decimalScale();
int p1 = t1.decimalPrecision();
int p2 = t2.decimalPrecision();
int digitsBefore = Math.max(p1 - s1, p2 - s2);
int digitsAfter = Math.max(s1, s2);
if (strictDecimal) {
if (digitsBefore + digitsAfter > 38) return Type.INVALID;
return ScalarType.createDecimalType(digitsBefore + digitsAfter, digitsAfter);
}
return ScalarType.createClippedDecimalType(digitsBefore + digitsAfter, digitsAfter);
}
/**
* Returns the necessary result type for t1 op t2. Throws an analysis exception
* if the operation does not make sense for the types.
*/
public static Type getArithmeticResultType(Type t1, Type t2,
ArithmeticExpr.Operator op, boolean decimalV2) throws AnalysisException {
Preconditions.checkState(t1.isNumericType() || t1.isNull());
Preconditions.checkState(t2.isNumericType() || t2.isNull());
if (t1.isNull() && t2.isNull()) return Type.NULL;
if (t1.isDecimal() || t2.isDecimal()) {
if (t1.isNull()) return t2;
if (t2.isNull()) return t1;
// For multiplications involving at least one floating point type we cast decimal to
// double in order to prevent decimals from overflowing.
if (op == ArithmeticExpr.Operator.MULTIPLY &&
(t1.isFloatingPointType() || t2.isFloatingPointType())) {
return Type.DOUBLE;
}
t1 = ((ScalarType) t1).getMinResolutionDecimal();
t2 = ((ScalarType) t2).getMinResolutionDecimal();
Preconditions.checkState(t1.isDecimal());
Preconditions.checkState(t2.isDecimal());
return getDecimalArithmeticResultType(t1, t2, op, decimalV2);
}
Type type = null;
switch (op) {
case MULTIPLY:
case ADD:
case SUBTRACT:
// If one of the types is null, use the compatible type without promotion.
// Otherwise, promote the compatible type to the next higher resolution type,
// to ensure that that a <op> b won't overflow/underflow.
Type compatibleType =
ScalarType.getAssignmentCompatibleType(t1, t2, false, false);
Preconditions.checkState(compatibleType.isScalarType());
type = ((ScalarType) compatibleType).getNextResolutionType();
break;
case MOD:
type = ScalarType.getAssignmentCompatibleType(t1, t2, false, false);
break;
case DIVIDE:
type = Type.DOUBLE;
break;
default:
throw new AnalysisException("Invalid op: " + op);
}
Preconditions.checkState(type.isValid());
return type;
}
/**
* Returns the result type for (t1 op t2) where t1 and t2 are both DECIMAL, depending
* on whether DECIMAL version 1 or DECIMAL version 2 is enabled.
*
* TODO: IMPALA-4924: remove DECIMAL V1 code.
*/
private static ScalarType getDecimalArithmeticResultType(Type t1, Type t2,
ArithmeticExpr.Operator op, boolean decimalV2) throws AnalysisException {
if (decimalV2) return getDecimalArithmeticResultTypeV2(t1, t2, op);
return getDecimalArithmeticResultTypeV1(t1, t2, op);
}
/**
* Returns the result type for (t1 op t2) where t1 and t2 are both DECIMAL, used when
* DECIMAL version 1 is enabled.
*
* These rules were mostly taken from the (pre Dec 2016) Hive / sql server rules with
* some changes.
* http://blogs.msdn.com/b/sqlprogrammability/archive/2006/03/29/564110.aspx
* https://msdn.microsoft.com/en-us/library/ms190476.aspx
*
* Changes:
* - Multiply does not need +1 for the result precision.
* - Divide scale truncation is different. When the maximum precision is exceeded,
* unlike SQL server, we do not try to preserve at least a scale of 6.
* - For other operators, when maximum precision is exceeded, we clip the precision
* and scale at maximum precision rather tha reducing scale. Therefore, we
* sacrifice digits to the left of the decimal point before sacrificing digits to
* the right.
*/
private static ScalarType getDecimalArithmeticResultTypeV1(Type t1, Type t2,
ArithmeticExpr.Operator op) throws AnalysisException {
Preconditions.checkState(t1.isFullySpecifiedDecimal());
Preconditions.checkState(t2.isFullySpecifiedDecimal());
ScalarType st1 = (ScalarType) t1;
ScalarType st2 = (ScalarType) t2;
int s1 = st1.decimalScale();
int s2 = st2.decimalScale();
int p1 = st1.decimalPrecision();
int p2 = st2.decimalPrecision();
int sMax = Math.max(s1, s2);
switch (op) {
case ADD:
case SUBTRACT:
return ScalarType.createClippedDecimalType(
sMax + Math.max(p1 - s1, p2 - s2) + 1, sMax);
case MULTIPLY:
return ScalarType.createClippedDecimalType(p1 + p2, s1 + s2);
case DIVIDE:
int resultScale = Math.max(DECIMAL_DIVISION_SCALE_INCREMENT, s1 + p2 + 1);
int resultPrecision = p1 - s1 + s2 + resultScale;
if (resultPrecision > ScalarType.MAX_PRECISION) {
// In this case, the desired resulting precision exceeds the maximum and
// we need to truncate some way. We can either remove digits before or
// after the decimal and there is no right answer. This is an implementation
// detail and different databases will handle this differently.
// For simplicity, we will set the resulting scale to be the max of the input
// scales and use the maximum precision.
resultScale = Math.max(s1, s2);
resultPrecision = ScalarType.MAX_PRECISION;
}
return ScalarType.createClippedDecimalType(resultPrecision, resultScale);
case MOD:
return ScalarType.createClippedDecimalType(
Math.min(p1 - s1, p2 - s2) + sMax, sMax);
default:
throw new AnalysisException(
"Operation '" + op + "' is not allowed for decimal types.");
}
}
/**
* Returns the result type for (t1 op t2) where t1 and t2 are both DECIMAL, used when
* DECIMAL version 2 is enabled.
*
* These rules are similar to (post Dec 2016) Hive / sql server rules.
* http://blogs.msdn.com/b/sqlprogrammability/archive/2006/03/29/564110.aspx
* https://msdn.microsoft.com/en-us/library/ms190476.aspx
*
* Changes:
* - There are slight difference with how precision/scale reduction occurs compared
* to SQL server when the desired precision is more than the maximum supported
* precision. But an algorithm of reducing scale to a minimum of 6 is used.
*/
private static ScalarType getDecimalArithmeticResultTypeV2(Type t1, Type t2,
ArithmeticExpr.Operator op) throws AnalysisException {
Preconditions.checkState(t1.isFullySpecifiedDecimal());
Preconditions.checkState(t2.isFullySpecifiedDecimal());
ScalarType st1 = (ScalarType) t1;
ScalarType st2 = (ScalarType) t2;
int s1 = st1.decimalScale();
int s2 = st2.decimalScale();
int p1 = st1.decimalPrecision();
int p2 = st2.decimalPrecision();
int resultScale;
int resultPrecision;
switch (op) {
case DIVIDE:
// Divide result always gets at least MIN_ADJUSTED_SCALE decimal places.
resultScale = Math.max(ScalarType.MIN_ADJUSTED_SCALE, s1 + p2 + 1);
resultPrecision = p1 - s1 + s2 + resultScale;
break;
case MOD:
resultScale = Math.max(s1, s2);
resultPrecision = Math.min(p1 - s1, p2 - s2) + resultScale;
break;
case MULTIPLY:
resultScale = s1 + s2;
resultPrecision = p1 + p2 + 1;
break;
case ADD:
case SUBTRACT:
resultScale = Math.max(s1, s2);
resultPrecision = Math.max(p1 - s1, p2 - s2) + resultScale + 1;
break;
default:
Preconditions.checkState(false);
return null;
}
// Use the scale reduction technique when resultPrecision is too large.
return ScalarType.createAdjustedDecimalType(resultPrecision, resultScale);
}
/**
* Computes the ColumnType that can represent 'v' with no loss of resolution.
* The scale/precision in BigDecimal is not compatible with SQL decimal semantics
* (much more like significant figures and exponent).
* Returns null if the value cannot be represented.
*/
public static Type computeDecimalType(BigDecimal v) {
// PlainString returns the string with no exponent. We walk it to compute
// the digits before and after.
// TODO: better way?
String str = v.toPlainString();
int digitsBefore = 0;
int digitsAfter = 0;
boolean decimalFound = false;
boolean leadingZeros = true;
for (int i = 0; i < str.length(); ++i) {
char c = str.charAt(i);
if (c == '-') continue;
if (c == '.') {
decimalFound = true;
continue;
}
if (decimalFound) {
++digitsAfter;
} else {
// Strip out leading 0 before the decimal point. We want "0.1" to
// be parsed as ".1" (1 digit instead of 2).
if (c == '0' && leadingZeros) continue;
leadingZeros = false;
++digitsBefore;
}
}
if (digitsAfter > ScalarType.MAX_SCALE) return null;
if (digitsBefore + digitsAfter > ScalarType.MAX_PRECISION) return null;
if (digitsBefore == 0 && digitsAfter == 0) digitsBefore = 1;
return ScalarType.createDecimalType(digitsBefore + digitsAfter, digitsAfter);
}
}