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:
Iván Cea Fontenla 2025-01-17 14:38:14 +01:00 committed by GitHub
parent b8cb080bc8
commit acb46af612
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 477 additions and 43 deletions

View File

@ -0,0 +1,5 @@
pr: 119536
summary: Fix ROUND() with unsigned longs throwing in some edge cases
area: ES|QL
type: bug
issues: []

View File

@ -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" : [

View File

@ -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
|===

View File

@ -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;

View File

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

View File

@ -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
*/

View File

@ -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);

View File

@ -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(

View File

@ -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.

View File

@ -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"
)
);
}
}

View File

@ -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) {