This is an automated email from the ASF dual-hosted git repository.
rubenada pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 5eb36fb126 [CALCITE-7731] Bound plain-notation expansion of DECIMAL
literals to prevent parse-time OutOfMemoryError
5eb36fb126 is described below
commit 5eb36fb1262582c44515bcb2a515469941cf879f
Author: Ruben Quesada Lopez <[email protected]>
AuthorDate: Fri Aug 21 17:12:48 2026 +0100
[CALCITE-7731] Bound plain-notation expansion of DECIMAL literals to
prevent parse-time OutOfMemoryError
---
.../apache/calcite/config/CalciteSystemProperty.java | 16 ++++++++++++++++
.../apache/calcite/rel/rel2sql/SqlImplementor.java | 8 ++++++--
.../main/java/org/apache/calcite/rex/RexBuilder.java | 4 ++++
.../org/apache/calcite/sql/SqlNumericLiteral.java | 4 ++++
.../src/main/java/org/apache/calcite/sql/SqlUtil.java | 14 ++++++++++++++
.../org/apache/calcite/sql/parser/SqlParserUtil.java | 11 +++++++----
.../org/apache/calcite/sql/parser/SqlParserTest.java | 19 +++++++++++++++++++
7 files changed, 70 insertions(+), 6 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
b/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
index 39ba5c2f32..0d14fed45b 100644
--- a/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
+++ b/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
@@ -492,6 +492,22 @@ public final class CalciteSystemProperty<T> {
public static final CalciteSystemProperty<String> MODEL_CLASSES_DENIED =
stringProperty("calcite.model.classes.denied", "");
+ /**
+ * Maximum number of decimal digits that the plain-notation expansion of a
{@code DECIMAL}
+ * literal may contain.
+ *
+ * <p>{@link java.math.BigDecimal} accepts any {@code int} exponent, so
without a bound
+ * a ~15-character literal such as {@code DECIMAL '1E2147483647'} would ask
+ * {@link java.math.BigDecimal#toPlainString()} to materialize one character
per digit,
+ * i.e. a multi-gigabyte allocation that would end in {@link
OutOfMemoryError} inside the parser.
+ *
+ * <p>Default {@code 10000} is well beyond any dialect's realistic maximum
{@code DECIMAL}
+ * precision while still bounding worst-case allocation to ~20 KB. Raise if
a dialect
+ * legitimately needs more.
+ */
+ public static final CalciteSystemProperty<Integer>
MAX_DECIMAL_LITERAL_PLAIN_DIGITS =
+ intProperty("calcite.parser.maxDecimalLiteralPlainDigits", 10_000, v ->
v > 0);
+
private static CalciteSystemProperty<Boolean> booleanProperty(String key,
boolean defaultValue) {
// Note that "" -> true (convenient for command-lines flags like '-Dflag')
diff --git
a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 60b068438a..a7a5257911 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -1758,8 +1758,12 @@ public static SqlNode toSql(RexLiteral literal) {
}
return SqlLiteral.createApproxNumeric(d.toString(), POS);
} else {
- return SqlLiteral.createExactNumeric(
- castNonNull(literal.getValueAs(BigDecimal.class)).toPlainString(),
POS);
+ final BigDecimal bd =
castNonNull(literal.getValueAs(BigDecimal.class));
+ if (!SqlUtil.isBoundedDecimal(bd)) {
+ throw new IllegalStateException(
+ "DECIMAL literal exceeds the configured plain-notation bound: "
+ bd);
+ }
+ return SqlLiteral.createExactNumeric(bd.toPlainString(), POS);
}
}
case APPROXIMATE_NUMERIC:
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index 667de88227..efcb80f203 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -1549,6 +1549,10 @@ protected RexLiteral makeLiteral(
} else if (type.getScale() != RelDataType.SCALE_NOT_SPECIFIED) {
o = ((BigDecimal) o).setScale(type.getScale(),
typeFactory.getTypeSystem().roundingMode());
if (type.getScale() < 0) {
+ if (!SqlUtil.isBoundedDecimal((BigDecimal) o)) {
+ throw new IllegalArgumentException("Cannot convert " + o + " to "
+ type
+ + ": plain-notation expansion exceeds the configured bound");
+ }
o = new BigDecimal(((BigDecimal) o).toPlainString());
}
}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlNumericLiteral.java
b/core/src/main/java/org/apache/calcite/sql/SqlNumericLiteral.java
index 29572c1360..575388ed56 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlNumericLiteral.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlNumericLiteral.java
@@ -90,6 +90,10 @@ public boolean isExact() {
@Override public String toValue() {
final BigDecimal bd = getValueNonNull();
if (exact) {
+ if (!SqlUtil.isBoundedDecimal(bd)) {
+ throw new IllegalArgumentException("DECIMAL literal '" + bd
+ + "' exceeds the configured plain-notation bound");
+ }
return bd.toPlainString();
}
return Util.toScientificNotation(bd);
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
index 1dba9d573e..e6bccfe51e 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlUtil.java
@@ -17,6 +17,7 @@
package org.apache.calcite.sql;
import org.apache.calcite.avatica.util.ByteString;
+import org.apache.calcite.config.CalciteSystemProperty;
import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.linq4j.function.Functions;
import org.apache.calcite.rel.RelNode;
@@ -58,6 +59,7 @@
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
+import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
@@ -962,6 +964,18 @@ public static String getAliasedSignature(
return ret.toString();
}
+ /**
+ * Returns whether {@code value}'s plain-notation expansion fits within
+ * the configured bound
+ * ({@link CalciteSystemProperty#MAX_DECIMAL_LITERAL_PLAIN_DIGITS}).
+ * Callers that are about to feed {@code value} to {@code toPlainString}
+ * (or the equivalent) must gate on this method first.
+ */
+ public static boolean isBoundedDecimal(BigDecimal value) {
+ final long limit =
CalciteSystemProperty.MAX_DECIMAL_LITERAL_PLAIN_DIGITS.value();
+ return (long) value.precision() + Math.abs((long) value.scale()) <= limit;
+ }
+
/**
* Wraps an exception with context.
*/
diff --git
a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index c6eed1bce2..3c0b6bc99b 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -338,15 +338,18 @@ public static SqlDateLiteral parseDateLiteral(String s,
SqlParserPos pos) {
}
public static SqlNumericLiteral parseDecimalLiteral(String s, SqlParserPos
pos) {
+ final BigDecimal value;
try {
- // The s maybe scientific notation string,e.g. 1.2E-3,
- // we need to convert it to 0.0012
- s = new BigDecimal(s).toPlainString();
+ value = new BigDecimal(s);
} catch (NumberFormatException e) {
throw SqlUtil.newContextException(pos,
RESOURCE.invalidLiteral(s, "DECIMAL"));
}
- return SqlLiteral.createExactNumeric(s, pos);
+ if (!SqlUtil.isBoundedDecimal(value)) {
+ throw SqlUtil.newContextException(pos,
+ RESOURCE.invalidLiteral(s, "DECIMAL"));
+ }
+ return SqlLiteral.createExactNumeric(value.toPlainString(), pos);
}
public static SqlTimeLiteral parseTimeLiteral(String s, SqlParserPos pos) {
diff --git
a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 32f52812ce..a64edcd6fb 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -1056,6 +1056,25 @@ private void checkLarge(int n) {
.ok("SELECT 999");
}
+ /** Test case for <a
href="https://issues.apache.org/jira/browse/CALCITE-7731">[CALCITE-7731]
+ * Bound plain-notation expansion of DECIMAL literals to prevent parse-time
+ * OutOfMemoryError</a>. */
+ @Test void testDecimalLiteralWithOutOfRangeExponent() {
+ sql("select DECIMAL ^'1E2147483647'^")
+ .fails("(?s)Literal '1E2147483647' can not be parsed to type
'DECIMAL'.*");
+ sql("select DECIMAL ^'1E-2147483647'^")
+ .fails("(?s)Literal '1E-2147483647' can not be parsed to type
'DECIMAL'.*");
+ sql("select DECIMAL ^'1E1000000000'^")
+ .fails("(?s)Literal '1E1000000000' can not be parsed to type
'DECIMAL'.*");
+ sql("select DECIMAL ^'-9.9E999999999'^")
+ .fails("(?s)Literal '-9.9E999999999' can not be parsed to type
'DECIMAL'.*");
+ // Exponents within the bound still expand to plain notation.
+ sql("select DECIMAL '1E10'")
+ .ok("SELECT 10000000000");
+ sql("select DECIMAL '1E-10'")
+ .ok("SELECT 0.0000000001");
+ }
+
@Test void testDecimalWithScale() {
sql("select cast(15 as decimal(3, 1))")
.ok("SELECT CAST(15 AS DECIMAL(3, 1))");