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

Reply via email to