diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java index a35fba35a015..371685cd407e 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java @@ -417,11 +417,23 @@ static BigDecimal checkOverflow(BigDecimal value, int precision, int scale, if (scale < 0) { // The result maybe scientific notation string,e.g. 1.234E+6, // we need to convert it to 1234000 + if (!isBoundedDecimal(result)) { + throw new IllegalArgumentException( + "DECIMAL literal exceeds the configured plain-notation bound: " + result); + } return new BigDecimal(result.toPlainString()); } return result; } + private static boolean isBoundedDecimal(BigDecimal value) { + // Mirrors org.apache.calcite.sql.SqlUtil.isBoundedDecimal without + // introducing a core -> linq4j dependency (linq4j cannot depend on core). + // Bound is calcite.parser.maxDecimalLiteralPlainDigits (default 10000). + int limit = Integer.getInteger("calcite.parser.maxDecimalLiteralPlainDigits", 10_000); + return (long) value.precision() + Math.abs((long) value.scale()) <= limit; + } + /** Called from BuiltInMethod.CHAR_DECIMAL_CAST */ public static @Nullable Object charToDecimalCast( @Nullable String value, int precision, int scale) { diff --git a/linq4j/src/test/java/org/apache/calcite/linq4j/test/PrimitiveTest.java b/linq4j/src/test/java/org/apache/calcite/linq4j/test/PrimitiveTest.java index eedb153e0e0a..1e4898f02d09 100644 --- a/linq4j/src/test/java/org/apache/calcite/linq4j/test/PrimitiveTest.java +++ b/linq4j/src/test/java/org/apache/calcite/linq4j/test/PrimitiveTest.java @@ -20,15 +20,18 @@ import org.junit.jupiter.api.Test; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasToString; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -305,4 +308,26 @@ public void set(Object v) { assertThat(Primitive.BOOLEAN.arrayToString(booleans4), is("[true, false, false, false, true, true, false]")); } + + @Test void testCharToDecimalCastWithinBounds() { + assertThat(Primitive.charToDecimalCast("1.5", 5, 2), + is(new BigDecimal("1.50"))); + assertThat(Primitive.charToDecimalCast("0", 38, 0), + is(new BigDecimal("0"))); + // scale < 0 branch, well below the bound. + assertThat(Primitive.charToDecimalCast("1000", 4, -3), + is(new BigDecimal("1000"))); + } + + /** Test case for + * [CALCITE-7750] + * Bound plain-notation expansion of DECIMAL literals in + * Primitive.checkOverflow. */ + @Test void testCharToDecimalCastRejectsPathologicalScale() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> + Primitive.charToDecimalCast("1E10000", 1, -10_000)); + assertThat(e.getMessage(), + containsString("plain-notation bound")); + } }