Skip to content

Commit

Permalink
Throw exception for cast of nan and infinity to int types
Browse files Browse the repository at this point in the history
Fix cast of nan and infinity from DOUBLE/REAL to
BIGINT/INT/SMALLINT/TINYTINT.  Previously all except double -> bigint
would return zero.  Now they will all throw an INVALID_CAST_ARGUMENT
exception.

Fix silent overflow for casting from REAL to BIGINT type. We now throw
an INVALID_CAST_ARGUMENT if the value is out of the BIGINT range.

Change error code from NUMERIC_VALUE_OUT_OF_RANGE to
INVALID_CAST_ARGUMENT for out of range values in casts from floating
point to integer types.  With the library we now use for the
implementaiton for these casts, we can't always tell by the exception
type that the value is out of range, so we return the more generic, but
still relevant INVALID_CAST_ARGUMENT instead.
  • Loading branch information
rschlussel committed Jun 5, 2024
1 parent 7dfd981 commit b5b0dd8
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import com.facebook.presto.common.block.Block;
import com.facebook.presto.common.type.AbstractLongType;
import com.facebook.presto.common.type.StandardTypes;
import com.facebook.presto.operator.scalar.MathFunctions;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.function.BlockIndex;
import com.facebook.presto.spi.function.BlockPosition;
Expand Down Expand Up @@ -53,12 +52,10 @@
import static com.facebook.presto.common.type.DoubleType.DOUBLE;
import static com.facebook.presto.spi.StandardErrorCode.DIVISION_BY_ZERO;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT;
import static com.facebook.presto.spi.StandardErrorCode.NUMERIC_VALUE_OUT_OF_RANGE;
import static com.google.common.base.Preconditions.checkState;
import static io.airlift.slice.Slices.utf8Slice;
import static java.lang.Double.doubleToLongBits;
import static java.lang.Float.floatToRawIntBits;
import static java.lang.Math.toIntExact;
import static java.lang.String.format;
import static java.math.RoundingMode.FLOOR;
import static java.math.RoundingMode.HALF_UP;
Expand Down Expand Up @@ -195,10 +192,10 @@ public static boolean castToBoolean(@SqlType(StandardTypes.DOUBLE) double value)
public static long castToInteger(@SqlType(StandardTypes.DOUBLE) double value)
{
try {
return toIntExact((long) MathFunctions.round(value));
return DoubleMath.roundToInt(value, HALF_UP);
}
catch (ArithmeticException e) {
throw new PrestoException(NUMERIC_VALUE_OUT_OF_RANGE, "Out of range for integer: " + value, e);
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Unable to cast %s to integer", value), e);
}
}

Expand All @@ -207,10 +204,10 @@ public static long castToInteger(@SqlType(StandardTypes.DOUBLE) double value)
public static long castToSmallint(@SqlType(StandardTypes.DOUBLE) double value)
{
try {
return Shorts.checkedCast((long) MathFunctions.round(value));
return Shorts.checkedCast(DoubleMath.roundToInt(value, HALF_UP));
}
catch (IllegalArgumentException e) {
throw new PrestoException(NUMERIC_VALUE_OUT_OF_RANGE, "Out of range for smallint: " + value, e);
catch (ArithmeticException | IllegalArgumentException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Unable to cast %s to smallint", value), e);
}
}

Expand All @@ -219,10 +216,10 @@ public static long castToSmallint(@SqlType(StandardTypes.DOUBLE) double value)
public static long castToTinyint(@SqlType(StandardTypes.DOUBLE) double value)
{
try {
return SignedBytes.checkedCast((long) MathFunctions.round(value));
return SignedBytes.checkedCast(DoubleMath.roundToInt(value, HALF_UP));
}
catch (IllegalArgumentException e) {
throw new PrestoException(NUMERIC_VALUE_OUT_OF_RANGE, "Out of range for tinyint: " + value, e);
catch (ArithmeticException | IllegalArgumentException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Unable to cast %s to tinyint", value), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import com.facebook.presto.common.block.Block;
import com.facebook.presto.common.type.AbstractIntType;
import com.facebook.presto.common.type.StandardTypes;
import com.facebook.presto.operator.scalar.MathFunctions;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.function.BlockIndex;
import com.facebook.presto.spi.function.BlockPosition;
Expand Down Expand Up @@ -51,13 +50,14 @@
import static com.facebook.presto.common.function.OperatorType.SUBTRACT;
import static com.facebook.presto.common.function.OperatorType.XX_HASH_64;
import static com.facebook.presto.common.type.RealType.REAL;
import static com.facebook.presto.spi.StandardErrorCode.NUMERIC_VALUE_OUT_OF_RANGE;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT;
import static io.airlift.slice.Slices.utf8Slice;
import static java.lang.Float.floatToIntBits;
import static java.lang.Float.floatToRawIntBits;
import static java.lang.Float.intBitsToFloat;
import static java.lang.Math.toIntExact;
import static java.lang.String.format;
import static java.math.RoundingMode.FLOOR;
import static java.math.RoundingMode.HALF_UP;

public final class RealOperators
{
Expand Down Expand Up @@ -194,18 +194,23 @@ public static Slice castToVarchar(@SqlType(StandardTypes.REAL) long value)
@SqlType(StandardTypes.BIGINT)
public static long castToLong(@SqlType(StandardTypes.REAL) long value)
{
return (long) MathFunctions.round((double) intBitsToFloat((int) value));
try {
return DoubleMath.roundToLong(intBitsToFloat((int) value), HALF_UP);
}
catch (ArithmeticException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Unable to cast %s to bigint", value), e);
}
}

@ScalarOperator(CAST)
@SqlType(StandardTypes.INTEGER)
public static long castToInteger(@SqlType(StandardTypes.REAL) long value)
{
try {
return toIntExact((long) MathFunctions.round((double) intBitsToFloat((int) value)));
return DoubleMath.roundToInt(intBitsToFloat((int) value), HALF_UP);
}
catch (ArithmeticException e) {
throw new PrestoException(NUMERIC_VALUE_OUT_OF_RANGE, "Out of range for integer: " + value, e);
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Unable to cast %s to integer", value), e);
}
}

Expand All @@ -214,10 +219,10 @@ public static long castToInteger(@SqlType(StandardTypes.REAL) long value)
public static long castToSmallint(@SqlType(StandardTypes.REAL) long value)
{
try {
return Shorts.checkedCast((long) MathFunctions.round((double) intBitsToFloat((int) value)));
return Shorts.checkedCast(DoubleMath.roundToInt(intBitsToFloat((int) value), HALF_UP));
}
catch (IllegalArgumentException e) {
throw new PrestoException(NUMERIC_VALUE_OUT_OF_RANGE, "Out of range for smallint: " + value, e);
catch (ArithmeticException | IllegalArgumentException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Unable to cast %s to smallint", value), e);
}
}

Expand All @@ -226,10 +231,10 @@ public static long castToSmallint(@SqlType(StandardTypes.REAL) long value)
public static long castToTinyint(@SqlType(StandardTypes.REAL) long value)
{
try {
return SignedBytes.checkedCast((long) MathFunctions.round((double) intBitsToFloat((int) value)));
return SignedBytes.checkedCast(DoubleMath.roundToInt(intBitsToFloat((int) value), HALF_UP));
}
catch (IllegalArgumentException e) {
throw new PrestoException(NUMERIC_VALUE_OUT_OF_RANGE, "Out of range for tinyint: " + value, e);
catch (ArithmeticException | IllegalArgumentException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Unable to cast %s to tinyint", value), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ public void testJsonToArray()
assertInvalidCast("CAST(unchecked_to_json('[1, 2, 3') AS ARRAY<BIGINT>)", "Cannot cast to array(bigint).\n[1, 2, 3");

assertInvalidCast("CAST(JSON '[\"a\", \"b\"]' AS ARRAY<BIGINT>)", "Cannot cast to array(bigint). Cannot cast 'a' to BIGINT\n[\"a\",\"b\"]");
assertInvalidCast("CAST(JSON '[1234567890123.456]' AS ARRAY<INTEGER>)", "Cannot cast to array(integer). Out of range for integer: 1.234567890123456E12\n[1.234567890123456E12]");
assertInvalidCast("CAST(JSON '[1234567890123.456]' AS ARRAY<INTEGER>)", "Cannot cast to array(integer). Unable to cast 1.234567890123456E12 to integer\n[1.234567890123456E12]");

assertFunction("CAST(JSON '[1, 2.0, 3]' AS ARRAY(DECIMAL(10,5)))", new ArrayType(createDecimalType(10, 5)), ImmutableList.of(decimal("1.00000"), decimal("2.00000"), decimal("3.00000")));
assertFunction("CAST(CAST(ARRAY [1, 2.0, 3] as JSON) AS ARRAY(DECIMAL(10,5)))", new ArrayType(createDecimalType(10, 5)), ImmutableList.of(decimal("1.00000"), decimal("2.00000"), decimal("3.00000")));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
import static com.facebook.presto.common.type.BigintType.BIGINT;
import static com.facebook.presto.common.type.BooleanType.BOOLEAN;
import static com.facebook.presto.common.type.DoubleType.DOUBLE;
import static com.facebook.presto.common.type.IntegerType.INTEGER;
import static com.facebook.presto.common.type.RealType.REAL;
import static com.facebook.presto.common.type.SmallintType.SMALLINT;
import static com.facebook.presto.common.type.TinyintType.TINYINT;
import static com.facebook.presto.common.type.VarcharType.VARCHAR;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT;
import static java.lang.Double.doubleToLongBits;
Expand Down Expand Up @@ -209,6 +212,59 @@ public void testCastToBigint()
assertInvalidFunction("cast(nan() as bigint)", INVALID_CAST_ARGUMENT);
}

@Test
public void testCastToInteger()
{
assertFunction("cast(37.7E0 as integer)", INTEGER, 38);
assertFunction("cast(-37.7E0 as integer)", INTEGER, -38);
assertFunction("cast(17.1E0 as integer)", INTEGER, 17);
assertFunction("cast(-17.1E0 as integer)", INTEGER, -17);
assertFunction("cast(9.2E8 as integer)", INTEGER, 920000000);
assertFunction("cast(-9.2E8 as integer)", INTEGER, -920000000);
assertFunction("cast(2.21E8 as integer)", INTEGER, 221000000);
assertFunction("cast(-2.21E8 as integer)", INTEGER, -221000000);
assertFunction("cast(17.5E0 as integer)", INTEGER, 18);
assertFunction("cast(-17.5E0 as integer)", INTEGER, -18);

assertFunction("cast(" + Math.nextDown(0x1.0p31f) + " as integer)", INTEGER, (int) Math.nextDown(0x1.0p31f));
assertInvalidFunction("cast(" + 0x1.0p31 + " as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(" + Math.nextUp(0x1.0p31f) + " as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(" + Math.nextDown(-0x1.0p31f) + " as integer)", INVALID_CAST_ARGUMENT);
assertFunction("cast(" + -0x1.0p31 + " as integer)", INTEGER, (int) -0x1.0p31);
assertFunction("cast(" + Math.nextUp(-0x1.0p31f) + " as integer)", INTEGER, (int) Math.nextUp(-0x1.0p31f));

assertInvalidFunction("cast(9.3E9 as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(-9.3E9 as integer)", INVALID_CAST_ARGUMENT);

assertInvalidFunction("cast(infinity() as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(-infinity() as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(nan() as integer)", INVALID_CAST_ARGUMENT);
}

@Test
public void testCastToSmallInt()
{
assertFunction("cast(" + (0x1.0p15 - 0.6) + " as smallint)", SMALLINT, Short.MAX_VALUE);
assertInvalidFunction("cast(DOUBLE '" + 0x1.0p15 + "' as smallint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(9.2E9 as smallint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(-9.2E9 as smallint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(infinity() as smallint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(-infinity() as smallint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(nan() as smallint)", INVALID_CAST_ARGUMENT);
}

@Test
public void testCastToTinyInt()
{
assertFunction("cast(" + (0x1.0p7 - 0.6) + " as tinyint)", TINYINT, Byte.MAX_VALUE);
assertInvalidFunction("cast(DOUBLE '" + 0x1.0p7 + "' as tinyint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(9.2E9 as tinyint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(-9.2E9 as tinyint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(infinity() as tinyint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(-infinity() as tinyint)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(nan() as tinyint)", INVALID_CAST_ARGUMENT);
}

@Test
public void testCastToBoolean()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ public void testJsonToMap()
assertInvalidCast("CAST(unchecked_to_json('{\"a\": 1') AS MAP<VARCHAR, BIGINT>)", "Cannot cast to map(varchar,bigint).\n{\"a\": 1");

assertInvalidCast("CAST(JSON '{\"a\": \"b\"}' AS MAP<VARCHAR, BIGINT>)", "Cannot cast to map(varchar,bigint). Cannot cast 'b' to BIGINT\n{\"a\":\"b\"}");
assertInvalidCast("CAST(JSON '{\"a\": 1234567890123.456}' AS MAP<VARCHAR, INTEGER>)", "Cannot cast to map(varchar,integer). Out of range for integer: 1.234567890123456E12\n{\"a\":1.234567890123456E12}");
assertInvalidCast("CAST(JSON '{\"a\": 1234567890123.456}' AS MAP<VARCHAR, INTEGER>)", "Cannot cast to map(varchar,integer). Unable to cast 1.234567890123456E12 to integer\n{\"a\":1.234567890123456E12}");

assertInvalidCast("CAST(JSON '{\"1\":1, \"01\": 2}' AS MAP<BIGINT, BIGINT>)", "Cannot cast to map(bigint,bigint). Duplicate keys are not allowed\n{\"01\":2,\"1\":1}");
assertInvalidCast("CAST(JSON '[{\"1\":1, \"01\": 2}]' AS ARRAY<MAP<BIGINT, BIGINT>>)", "Cannot cast to array(map(bigint,bigint)). Duplicate keys are not allowed\n[{\"01\":2,\"1\":1}]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import static com.facebook.presto.common.type.SmallintType.SMALLINT;
import static com.facebook.presto.common.type.TinyintType.TINYINT;
import static com.facebook.presto.common.type.VarcharType.VARCHAR;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT;
import static java.lang.Float.floatToIntBits;
import static java.lang.Float.intBitsToFloat;
import static java.lang.Float.isNaN;
Expand Down Expand Up @@ -183,6 +184,11 @@ public void testCastToBigInt()
assertFunction("CAST(REAL'-754.2008' as BIGINT)", BIGINT, -754L);
assertFunction("CAST(REAL'1.98' as BIGINT)", BIGINT, 2L);
assertFunction("CAST(REAL'-0.0' as BIGINT)", BIGINT, 0L);

assertInvalidFunction("CAST(cast(nan() AS REAL) as BIGINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(infinity() AS REAL) as BIGINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(-infinity() AS REAL) as BIGINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(REAL '" + Float.MAX_VALUE + "' as BIGINT)", INVALID_CAST_ARGUMENT);
}

@Test
Expand All @@ -192,6 +198,20 @@ public void testCastToInteger()
assertFunction("CAST(REAL'-754.1985' AS INTEGER)", INTEGER, -754);
assertFunction("CAST(REAL'9.99' AS INTEGER)", INTEGER, 10);
assertFunction("CAST(REAL'-0.0' AS INTEGER)", INTEGER, 0);

assertFunction("cast(REAL '" + Math.nextDown(0x1.0p31f) + "' as integer)", INTEGER, (int) Math.nextDown(0x1.0p31f));
assertInvalidFunction("cast(REAL '" + 0x1.0p31 + "' as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(REAL '" + Math.nextUp(0x1.0p31f) + "' as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(REAL '" + Math.nextDown(-0x1.0p31f) + "' as integer)", INVALID_CAST_ARGUMENT);
assertFunction("cast(REAL '" + -0x1.0p31 + "' as integer)", INTEGER, (int) -0x1.0p31);
assertFunction("cast(REAL '" + Math.nextUp(-0x1.0p31f) + "' as integer)", INTEGER, (int) Math.nextUp(-0x1.0p31f));

assertInvalidFunction("cast(9.3E9 as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("cast(-9.3E9 as integer)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(nan() AS REAL) as INTEGER)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(infinity() AS REAL) as INTEGER)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(-infinity() AS REAL) as INTEGER)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(REAL '" + (Integer.MAX_VALUE + 0.6) + "' as INTEGER)", INVALID_CAST_ARGUMENT);
}

@Test
Expand All @@ -201,6 +221,10 @@ public void testCastToSmallint()
assertFunction("CAST(REAL'-754.1985' AS SMALLINT)", SMALLINT, (short) -754);
assertFunction("CAST(REAL'9.99' AS SMALLINT)", SMALLINT, (short) 10);
assertFunction("CAST(REAL'-0.0' AS SMALLINT)", SMALLINT, (short) 0);
assertInvalidFunction("CAST(cast(nan() AS REAL) as SMALLINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(infinity() AS REAL) as SMALLINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(-infinity() AS REAL) as SMALLINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(REAL '" + (Short.MAX_VALUE + 0.6) + "' as SMALLINT)", INVALID_CAST_ARGUMENT);
}

@Test
Expand All @@ -210,6 +234,10 @@ public void testCastToTinyint()
assertFunction("CAST(REAL'-128.234' AS TINYINT)", TINYINT, (byte) -128);
assertFunction("CAST(REAL'9.99' AS TINYINT)", TINYINT, (byte) 10);
assertFunction("CAST(REAL'-0.0' AS TINYINT)", TINYINT, (byte) 0);
assertInvalidFunction("CAST(cast(nan() AS REAL) as TINYINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(infinity() AS REAL) as TINYINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(cast(-infinity() AS REAL) as TINYINT)", INVALID_CAST_ARGUMENT);
assertInvalidFunction("CAST(REAL '" + (Byte.MAX_VALUE + 0.6) + "' as TINYINT)", INVALID_CAST_ARGUMENT);
}

@Test
Expand Down

0 comments on commit b5b0dd8

Please sign in to comment.