ESQL: Fix ROUND() with unsigned longs throwing in some edge cases (#119536)
There were different error cases with `ROUND(number, decimals)`: - Decimals accepted unsigned longs, but threw a 500 with a `can't process [unsigned_long -> long]` in the cast evaluator - Fixed by improving the `resolveType()` - If the number was a BigInteger unsigned long, there were 2 cases throwing an exception: 1. Negative decimals outside the range of integer: Error 2. Negative decimals insie the range of integer, but "big enough" for `BigInteger.TEN.pow(...)` to throw a `BigInteger would overflow supported range` 3. -19 decimals with big unsigned longs like `18446744073709551615` was throwing an `unsigned_long overflow` Also, when the number is a BigInteger and the decimals is a big negative (but not big enough to throw), it may be **very** slow. Taking _many_ seconds for a single computation (It tries to calculate a `10^(big number)`. I didn't do anything here, but I wonder if we should limit it. To solve most of the cases, a warnExceptions was added for the overflow case, and a guard clause to return 0 for <-19 decimals on unsigned longs. Another issue is that rounding to a number like 7 to -1 returns 0 instead of 10, which may be considered an error. But it's consistent, so I'm leaving it to another PR
This commit is contained in:
parent
b8cb080bc8
commit
acb46af612
|
@ -0,0 +1,5 @@
|
|||
pr: 119536
|
||||
summary: Fix ROUND() with unsigned longs throwing in some edge cases
|
||||
area: ES|QL
|
||||
type: bug
|
||||
issues: []
|
|
@ -34,6 +34,24 @@
|
|||
"variadic" : false,
|
||||
"returnType" : "double"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
"name" : "number",
|
||||
"type" : "double",
|
||||
"optional" : false,
|
||||
"description" : "The numeric value to round. If `null`, the function returns `null`."
|
||||
},
|
||||
{
|
||||
"name" : "decimals",
|
||||
"type" : "long",
|
||||
"optional" : true,
|
||||
"description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`."
|
||||
}
|
||||
],
|
||||
"variadic" : false,
|
||||
"returnType" : "double"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
|
@ -64,6 +82,24 @@
|
|||
"variadic" : false,
|
||||
"returnType" : "integer"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
"name" : "number",
|
||||
"type" : "integer",
|
||||
"optional" : false,
|
||||
"description" : "The numeric value to round. If `null`, the function returns `null`."
|
||||
},
|
||||
{
|
||||
"name" : "decimals",
|
||||
"type" : "long",
|
||||
"optional" : true,
|
||||
"description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`."
|
||||
}
|
||||
],
|
||||
"variadic" : false,
|
||||
"returnType" : "integer"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
|
@ -94,6 +130,24 @@
|
|||
"variadic" : false,
|
||||
"returnType" : "long"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
"name" : "number",
|
||||
"type" : "long",
|
||||
"optional" : false,
|
||||
"description" : "The numeric value to round. If `null`, the function returns `null`."
|
||||
},
|
||||
{
|
||||
"name" : "decimals",
|
||||
"type" : "long",
|
||||
"optional" : true,
|
||||
"description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`."
|
||||
}
|
||||
],
|
||||
"variadic" : false,
|
||||
"returnType" : "long"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
|
@ -105,6 +159,42 @@
|
|||
],
|
||||
"variadic" : false,
|
||||
"returnType" : "unsigned_long"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
"name" : "number",
|
||||
"type" : "unsigned_long",
|
||||
"optional" : false,
|
||||
"description" : "The numeric value to round. If `null`, the function returns `null`."
|
||||
},
|
||||
{
|
||||
"name" : "decimals",
|
||||
"type" : "integer",
|
||||
"optional" : true,
|
||||
"description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`."
|
||||
}
|
||||
],
|
||||
"variadic" : false,
|
||||
"returnType" : "unsigned_long"
|
||||
},
|
||||
{
|
||||
"params" : [
|
||||
{
|
||||
"name" : "number",
|
||||
"type" : "unsigned_long",
|
||||
"optional" : false,
|
||||
"description" : "The numeric value to round. If `null`, the function returns `null`."
|
||||
},
|
||||
{
|
||||
"name" : "decimals",
|
||||
"type" : "long",
|
||||
"optional" : true,
|
||||
"description" : "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`."
|
||||
}
|
||||
],
|
||||
"variadic" : false,
|
||||
"returnType" : "unsigned_long"
|
||||
}
|
||||
],
|
||||
"examples" : [
|
||||
|
|
|
@ -6,10 +6,15 @@
|
|||
|===
|
||||
number | decimals | result
|
||||
double | integer | double
|
||||
double | long | double
|
||||
double | | double
|
||||
integer | integer | integer
|
||||
integer | long | integer
|
||||
integer | | integer
|
||||
long | integer | long
|
||||
long | long | long
|
||||
long | | long
|
||||
unsigned_long | integer | unsigned_long
|
||||
unsigned_long | long | unsigned_long
|
||||
unsigned_long | | unsigned_long
|
||||
|===
|
||||
|
|
|
@ -771,6 +771,28 @@ ul:ul
|
|||
18446744073709551615
|
||||
;
|
||||
|
||||
roundMaxULWithBigNegativeDecimals
|
||||
required_capability: fn_round_ul_fixes
|
||||
row
|
||||
ul1 = round(18446744073709551615, -6144415263046370459::long),
|
||||
ul2 = round(18446744073709551615, -20::long),
|
||||
ul3 = round(12446744073709551615, -19::long);
|
||||
|
||||
ul1:ul | ul2:ul | ul3:ul
|
||||
0 | 0 | 10000000000000000000
|
||||
;
|
||||
|
||||
roundBigULWithRoundULOverflow
|
||||
required_capability: fn_round_ul_fixes
|
||||
row ul = round(18446744073709551615, -19::long);
|
||||
|
||||
warning:Line 1:10: evaluation of [round(18446744073709551615, -19::long)] failed, treating result as null. Only first 20 failures recorded.
|
||||
warning:Line 1:10: java.lang.ArithmeticException: unsigned_long overflow
|
||||
|
||||
ul:ul
|
||||
null
|
||||
;
|
||||
|
||||
mvAvg
|
||||
from employees | where emp_no > 10008 | eval salary_change = mv_avg(salary_change) | sort emp_no | keep emp_no, salary_change.int, salary_change | limit 7;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
// 2.0.
|
||||
package org.elasticsearch.xpack.esql.expression.function.scalar.math;
|
||||
|
||||
import java.lang.ArithmeticException;
|
||||
import java.lang.IllegalArgumentException;
|
||||
import java.lang.Override;
|
||||
import java.lang.String;
|
||||
|
@ -52,7 +53,7 @@ public final class RoundUnsignedLongEvaluator implements EvalOperator.Expression
|
|||
if (decimalsVector == null) {
|
||||
return eval(page.getPositionCount(), valBlock, decimalsBlock);
|
||||
}
|
||||
return eval(page.getPositionCount(), valVector, decimalsVector).asBlock();
|
||||
return eval(page.getPositionCount(), valVector, decimalsVector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,16 +83,26 @@ public final class RoundUnsignedLongEvaluator implements EvalOperator.Expression
|
|||
result.appendNull();
|
||||
continue position;
|
||||
}
|
||||
result.appendLong(Round.processUnsignedLong(valBlock.getLong(valBlock.getFirstValueIndex(p)), decimalsBlock.getLong(decimalsBlock.getFirstValueIndex(p))));
|
||||
try {
|
||||
result.appendLong(Round.processUnsignedLong(valBlock.getLong(valBlock.getFirstValueIndex(p)), decimalsBlock.getLong(decimalsBlock.getFirstValueIndex(p))));
|
||||
} catch (ArithmeticException e) {
|
||||
warnings().registerException(e);
|
||||
result.appendNull();
|
||||
}
|
||||
}
|
||||
return result.build();
|
||||
}
|
||||
}
|
||||
|
||||
public LongVector eval(int positionCount, LongVector valVector, LongVector decimalsVector) {
|
||||
try(LongVector.FixedBuilder result = driverContext.blockFactory().newLongVectorFixedBuilder(positionCount)) {
|
||||
public LongBlock eval(int positionCount, LongVector valVector, LongVector decimalsVector) {
|
||||
try(LongBlock.Builder result = driverContext.blockFactory().newLongBlockBuilder(positionCount)) {
|
||||
position: for (int p = 0; p < positionCount; p++) {
|
||||
result.appendLong(p, Round.processUnsignedLong(valVector.getLong(p), decimalsVector.getLong(p)));
|
||||
try {
|
||||
result.appendLong(Round.processUnsignedLong(valVector.getLong(p), decimalsVector.getLong(p)));
|
||||
} catch (ArithmeticException e) {
|
||||
warnings().registerException(e);
|
||||
result.appendNull();
|
||||
}
|
||||
}
|
||||
return result.build();
|
||||
}
|
||||
|
|
|
@ -192,6 +192,11 @@ public class EsqlCapabilities {
|
|||
*/
|
||||
FN_SUBSTRING_EMPTY_NULL,
|
||||
|
||||
/**
|
||||
* Fixes on function {@code ROUND} that avoid it throwing exceptions on runtime for unsigned long cases.
|
||||
*/
|
||||
FN_ROUND_UL_FIXES,
|
||||
|
||||
/**
|
||||
* All functions that take TEXT should never emit TEXT, only KEYWORD. #114334
|
||||
*/
|
||||
|
|
|
@ -35,7 +35,7 @@ import java.util.function.BiFunction;
|
|||
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
|
||||
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
|
||||
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric;
|
||||
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isWholeNumber;
|
||||
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
|
||||
import static org.elasticsearch.xpack.esql.core.util.NumericUtils.unsignedLongAsNumber;
|
||||
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.bigIntegerToUnsignedLong;
|
||||
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.longToUnsignedLong;
|
||||
|
@ -63,7 +63,7 @@ public class Round extends EsqlScalarFunction implements OptionalArgument {
|
|||
@Param(
|
||||
optional = true,
|
||||
name = "decimals",
|
||||
type = { "integer" }, // TODO long is supported here too
|
||||
type = { "integer", "long" },
|
||||
description = "The number of decimal places to round to. Defaults to 0. If `null`, the function returns `null`."
|
||||
) Expression decimals
|
||||
) {
|
||||
|
@ -103,7 +103,15 @@ public class Round extends EsqlScalarFunction implements OptionalArgument {
|
|||
return resolution;
|
||||
}
|
||||
|
||||
return decimals == null ? TypeResolution.TYPE_RESOLVED : isWholeNumber(decimals, sourceText(), SECOND);
|
||||
return decimals == null
|
||||
? TypeResolution.TYPE_RESOLVED
|
||||
: isType(
|
||||
decimals,
|
||||
dt -> dt.isWholeNumber() && dt != DataType.UNSIGNED_LONG,
|
||||
sourceText(),
|
||||
SECOND,
|
||||
"whole number except unsigned_long or counter types"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -123,11 +131,16 @@ public class Round extends EsqlScalarFunction implements OptionalArgument {
|
|||
|
||||
@Evaluator(extraName = "Long")
|
||||
static long process(long val, long decimals) {
|
||||
return Maths.round(val, decimals).longValue();
|
||||
return Maths.round(val, decimals);
|
||||
}
|
||||
|
||||
@Evaluator(extraName = "UnsignedLong")
|
||||
@Evaluator(extraName = "UnsignedLong", warnExceptions = ArithmeticException.class)
|
||||
static long processUnsignedLong(long val, long decimals) {
|
||||
if (decimals <= -20) {
|
||||
// Unsigned long max value is 2^64 - 1, which has 20 digits
|
||||
return longToUnsignedLong(0, false);
|
||||
}
|
||||
|
||||
Number ul = unsignedLongAsNumber(val);
|
||||
if (ul instanceof BigInteger bi) {
|
||||
BigInteger rounded = Maths.round(bi, decimals);
|
||||
|
|
|
@ -284,27 +284,26 @@ public class VerifierTests extends ESTestCase {
|
|||
error("row a = 1, b = \"c\" | eval x = round(b)")
|
||||
);
|
||||
assertEquals(
|
||||
"1:31: second argument of [round(a, b)] must be [integer], found value [b] type [keyword]",
|
||||
"1:31: second argument of [round(a, b)] must be [whole number except unsigned_long or counter types], "
|
||||
+ "found value [b] type [keyword]",
|
||||
error("row a = 1, b = \"c\" | eval x = round(a, b)")
|
||||
);
|
||||
assertEquals(
|
||||
"1:31: second argument of [round(a, 3.5)] must be [integer], found value [3.5] type [double]",
|
||||
"1:31: second argument of [round(a, 3.5)] must be [whole number except unsigned_long or counter types], "
|
||||
+ "found value [3.5] type [double]",
|
||||
error("row a = 1, b = \"c\" | eval x = round(a, 3.5)")
|
||||
);
|
||||
}
|
||||
|
||||
public void testImplicitCastingErrorMessages() {
|
||||
assertEquals(
|
||||
"1:23: Cannot convert string [c] to [INTEGER], error [Cannot parse number [c]]",
|
||||
error("row a = round(123.45, \"c\")")
|
||||
);
|
||||
assertEquals("1:23: Cannot convert string [c] to [LONG], error [Cannot parse number [c]]", error("row a = round(123.45, \"c\")"));
|
||||
assertEquals(
|
||||
"1:27: Cannot convert string [c] to [DOUBLE], error [Cannot parse number [c]]",
|
||||
error("row a = 1 | eval x = acos(\"c\")")
|
||||
);
|
||||
assertEquals(
|
||||
"1:33: Cannot convert string [c] to [DOUBLE], error [Cannot parse number [c]]\n"
|
||||
+ "line 1:38: Cannot convert string [a] to [INTEGER], error [Cannot parse number [a]]",
|
||||
+ "line 1:38: Cannot convert string [a] to [LONG], error [Cannot parse number [a]]",
|
||||
error("row a = 1 | eval x = round(acos(\"c\"),\"a\")")
|
||||
);
|
||||
assertEquals(
|
||||
|
|
|
@ -95,6 +95,24 @@ public abstract class AbstractScalarFunctionTestCase extends AbstractFunctionTes
|
|||
return parameterSuppliersFromTypedData(anyNullIsNull(entirelyNullPreservesType, randomizeBytesRefsOffset(suppliers)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of test cases into a list of parameter suppliers.
|
||||
* Also, adds a default set of extra test cases.
|
||||
* <p>
|
||||
* Use if possible, as this method may get updated with new checks in the future.
|
||||
* </p>
|
||||
*
|
||||
* @param nullsExpectedType See {@link #anyNullIsNull(List, ExpectedType, ExpectedEvaluatorToString)}
|
||||
* @param evaluatorToString See {@link #anyNullIsNull(List, ExpectedType, ExpectedEvaluatorToString)}
|
||||
*/
|
||||
protected static Iterable<Object[]> parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(
|
||||
ExpectedType nullsExpectedType,
|
||||
ExpectedEvaluatorToString evaluatorToString,
|
||||
List<TestCaseSupplier> suppliers
|
||||
) {
|
||||
return parameterSuppliersFromTypedData(anyNullIsNull(randomizeBytesRefsOffset(suppliers), nullsExpectedType, evaluatorToString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of test cases into a list of parameter suppliers.
|
||||
* Also, adds a default set of extra test cases.
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.xpack.esql.expression.function.scalar.math;
|
||||
|
||||
import org.elasticsearch.xpack.esql.core.expression.Expression;
|
||||
import org.elasticsearch.xpack.esql.core.tree.Source;
|
||||
import org.elasticsearch.xpack.esql.core.type.DataType;
|
||||
import org.elasticsearch.xpack.esql.expression.function.ErrorsForCasesWithoutExamplesTestCase;
|
||||
import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
public class RoundErrorTests extends ErrorsForCasesWithoutExamplesTestCase {
|
||||
@Override
|
||||
protected List<TestCaseSupplier> cases() {
|
||||
return paramsToSuppliers(RoundTests.parameters());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Expression build(Source source, List<Expression> args) {
|
||||
return new Round(source, args.get(0), args.size() == 1 ? null : args.get(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Matcher<String> expectedTypeErrorMatcher(List<Set<DataType>> validPerPosition, List<DataType> signature) {
|
||||
return equalTo(
|
||||
typeErrorMessage(
|
||||
true,
|
||||
validPerPosition,
|
||||
signature,
|
||||
(v, p) -> p == 0 ? "numeric" : "whole number except unsigned_long or counter types"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import org.elasticsearch.xpack.esql.core.util.NumericUtils;
|
|||
import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
|
||||
import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
|
@ -26,8 +27,6 @@ import java.util.function.Function;
|
|||
import java.util.function.Supplier;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
public class RoundTests extends AbstractScalarFunctionTestCase {
|
||||
public RoundTests(@Name("TestCase") Supplier<TestCaseSupplier.TestCase> testCaseSupplier) {
|
||||
|
@ -37,11 +36,13 @@ public class RoundTests extends AbstractScalarFunctionTestCase {
|
|||
@ParametersFactory
|
||||
public static Iterable<Object[]> parameters() {
|
||||
List<TestCaseSupplier> suppliers = new ArrayList<>();
|
||||
|
||||
// Double field
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<double>",
|
||||
DataType.DOUBLE,
|
||||
() -> 1 / randomDouble(),
|
||||
() -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true),
|
||||
"RoundDoubleNoDecimalsEvaluator[val=Attribute[channel=0]]",
|
||||
d -> Maths.round(d, 0)
|
||||
)
|
||||
|
@ -50,36 +51,252 @@ public class RoundTests extends AbstractScalarFunctionTestCase {
|
|||
supplier(
|
||||
"<double>, <integer>",
|
||||
DataType.DOUBLE,
|
||||
() -> 1 / randomDouble(),
|
||||
() -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true),
|
||||
DataType.INTEGER,
|
||||
() -> between(-30, 30),
|
||||
"RoundDoubleEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]",
|
||||
Maths::round
|
||||
)
|
||||
);
|
||||
// TODO randomized cases for more types
|
||||
// TODO errorsForCasesWithoutExamples
|
||||
suppliers = anyNullIsNull(
|
||||
suppliers,
|
||||
(nullPosition, nullValueDataType, original) -> nullPosition == 0 ? nullValueDataType : original.expectedType(),
|
||||
(nullPosition, nullData, original) -> original
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<double>, <long>",
|
||||
DataType.DOUBLE,
|
||||
() -> randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true),
|
||||
DataType.LONG,
|
||||
() -> randomLongBetween(-30, 30),
|
||||
"RoundDoubleEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
Maths::round
|
||||
)
|
||||
);
|
||||
|
||||
suppliers.add(new TestCaseSupplier("two doubles", List.of(DataType.DOUBLE, DataType.INTEGER), () -> {
|
||||
double number1 = 1 / randomDouble();
|
||||
double number2 = 1 / randomDouble();
|
||||
int precision = between(-30, 30);
|
||||
return new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(List.of(number1, number2), DataType.DOUBLE, "number"),
|
||||
new TestCaseSupplier.TypedData(precision, DataType.INTEGER, "decimals")
|
||||
),
|
||||
"RoundDoubleEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]",
|
||||
DataType.DOUBLE,
|
||||
is(nullValue())
|
||||
).withWarning("Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.")
|
||||
.withWarning("Line -1:-1: java.lang.IllegalArgumentException: single-value function encountered multi-value");
|
||||
}));
|
||||
// Long decimals
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<integer>, <long>",
|
||||
DataType.INTEGER,
|
||||
ESTestCase::randomInt,
|
||||
DataType.LONG,
|
||||
ESTestCase::randomLong,
|
||||
"RoundIntEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
(n, d) -> Maths.round((Number) n, d)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<long>, <long>",
|
||||
DataType.LONG,
|
||||
ESTestCase::randomLong,
|
||||
DataType.LONG,
|
||||
ESTestCase::randomLong,
|
||||
"RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
(n, d) -> Maths.round((Number) n, d)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<unsigned_long>, <long>",
|
||||
DataType.UNSIGNED_LONG,
|
||||
ESTestCase::randomLong,
|
||||
DataType.LONG,
|
||||
// Safe negative integer to not trigger an exception and not slow down the test
|
||||
() -> randomLongBetween(-10_000, Long.MAX_VALUE),
|
||||
"RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
(n, d) -> Maths.round(NumericUtils.unsignedLongAsBigInteger(n), d)
|
||||
)
|
||||
);
|
||||
|
||||
// Integer decimals
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<integer>, <integer>",
|
||||
DataType.INTEGER,
|
||||
ESTestCase::randomInt,
|
||||
DataType.INTEGER,
|
||||
ESTestCase::randomInt,
|
||||
"RoundIntEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]",
|
||||
(n, d) -> Maths.round((Number) n, d)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<long>, <integer>",
|
||||
DataType.LONG,
|
||||
ESTestCase::randomLong,
|
||||
DataType.INTEGER,
|
||||
ESTestCase::randomInt,
|
||||
"RoundLongEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]",
|
||||
(n, d) -> Maths.round((Number) n, d)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
supplier(
|
||||
"<unsigned_long>, <integer>",
|
||||
DataType.UNSIGNED_LONG,
|
||||
ESTestCase::randomLong,
|
||||
DataType.INTEGER,
|
||||
// Safe negative integer to not trigger an exception and not slow down the test
|
||||
() -> randomIntBetween(-10_000, Integer.MAX_VALUE),
|
||||
"RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=CastIntToLongEvaluator[v=Attribute[channel=1]]]",
|
||||
(n, d) -> Maths.round(NumericUtils.unsignedLongAsBigInteger(n), d)
|
||||
)
|
||||
);
|
||||
|
||||
// Unsigned long errors
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<big unsigned_long>, <negative long out of integer range>",
|
||||
List.of(DataType.UNSIGNED_LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-9223372036854775808L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.UNSIGNED_LONG,
|
||||
equalTo(BigInteger.ZERO)
|
||||
)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<max unsigned_long>, <negative long in integer range>",
|
||||
List.of(DataType.UNSIGNED_LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-2147483647L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.UNSIGNED_LONG,
|
||||
equalTo(BigInteger.ZERO)
|
||||
)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<max unsigned_long>, <-20>",
|
||||
List.of(DataType.UNSIGNED_LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-20L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.UNSIGNED_LONG,
|
||||
equalTo(BigInteger.ZERO)
|
||||
)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<max unsigned_long>, <-19>",
|
||||
List.of(DataType.UNSIGNED_LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(new BigInteger("18446744073709551615"), DataType.UNSIGNED_LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-19L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.UNSIGNED_LONG,
|
||||
equalTo(null)
|
||||
).withWarning("Line -1:-1: evaluation of [] failed, treating result as null. Only first 20 failures recorded.")
|
||||
.withWarning("Line -1:-1: java.lang.ArithmeticException: unsigned_long overflow")
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<big unsigned_long>, <-19>",
|
||||
List.of(DataType.UNSIGNED_LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(new BigInteger("14446744073709551615"), DataType.UNSIGNED_LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-19L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundUnsignedLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.UNSIGNED_LONG,
|
||||
equalTo(new BigInteger("10000000000000000000"))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Max longs and overflows
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<max long>, <-20>",
|
||||
List.of(DataType.LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(Long.MAX_VALUE, DataType.LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-20L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.LONG,
|
||||
equalTo(0L)
|
||||
)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<max long>, <-19>",
|
||||
List.of(DataType.LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(Long.MAX_VALUE, DataType.LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-19L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.LONG,
|
||||
equalTo(0L)
|
||||
)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<big long>, <-18>",
|
||||
List.of(DataType.LONG, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(Long.MAX_VALUE, DataType.LONG, "number"),
|
||||
new TestCaseSupplier.TypedData(-18L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundLongEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.LONG,
|
||||
equalTo(9000000000000000000L)
|
||||
)
|
||||
)
|
||||
);
|
||||
// Max integers and overflows
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<max integer>, <-10>",
|
||||
List.of(DataType.INTEGER, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(Integer.MAX_VALUE, DataType.INTEGER, "number"),
|
||||
new TestCaseSupplier.TypedData(-10L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundIntEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.INTEGER,
|
||||
equalTo(0)
|
||||
)
|
||||
)
|
||||
);
|
||||
suppliers.add(
|
||||
new TestCaseSupplier(
|
||||
"<max integer>, <-9>",
|
||||
List.of(DataType.INTEGER, DataType.LONG),
|
||||
() -> new TestCaseSupplier.TestCase(
|
||||
List.of(
|
||||
new TestCaseSupplier.TypedData(Integer.MAX_VALUE, DataType.INTEGER, "number"),
|
||||
new TestCaseSupplier.TypedData(-9L, DataType.LONG, "decimals")
|
||||
),
|
||||
"RoundIntEvaluator[val=Attribute[channel=0], decimals=Attribute[channel=1]]",
|
||||
DataType.INTEGER,
|
||||
equalTo(2000000000)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Integer or Long without a decimals parameter is a noop
|
||||
suppliers.add(supplier("<integer>", DataType.INTEGER, ESTestCase::randomInt, "Attribute[channel=0]", Function.identity()));
|
||||
|
@ -128,7 +345,12 @@ public class RoundTests extends AbstractScalarFunctionTestCase {
|
|||
suppliers.add(supplier(0, 0, 0));
|
||||
suppliers.add(supplier(123, 2, 123));
|
||||
suppliers.add(supplier(123, -1, 120));
|
||||
return parameterSuppliersFromTypedData(suppliers);
|
||||
|
||||
return parameterSuppliersFromTypedDataWithDefaultChecksNoErrors(
|
||||
(nullPosition, nullValueDataType, original) -> nullPosition == 0 ? nullValueDataType : original.expectedType(),
|
||||
(nullPosition, nullData, original) -> original,
|
||||
suppliers
|
||||
);
|
||||
}
|
||||
|
||||
private static TestCaseSupplier supplier(double v, double expected) {
|
||||
|
|
Loading…
Reference in New Issue