This is an automated email from the ASF dual-hosted git repository.

snuyanzin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 5a04a76f538 [FLINK-40832][core] Validate decimal range in 
`VariantBuilder.of(BigDecimal)`
5a04a76f538 is described below

commit 5a04a76f538aad3afb721631acf42ce4233dd081
Author: Moritz Manner <[email protected]>
AuthorDate: Wed Sep 30 19:58:22 2026 +0200

    [FLINK-40832][core] Validate decimal range in 
`VariantBuilder.of(BigDecimal)`
---
 .../variant/BinaryVariantInternalBuilder.java      | 49 ++++++++++++++++++---
 .../flink/types/variant/BinaryVariantUtil.java     | 10 ++++-
 .../apache/flink/types/variant/VariantBuilder.java |  8 +++-
 .../variant/BinaryVariantInternalBuilderTest.java  | 13 ++++++
 .../flink/types/variant/BinaryVariantTest.java     | 51 ++++++++++++++++++++++
 5 files changed, 122 insertions(+), 9 deletions(-)

diff --git 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
index a61763a93b6..5e3520985d3 100644
--- 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
+++ 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantInternalBuilder.java
@@ -73,6 +73,7 @@ import static 
org.apache.flink.types.variant.BinaryVariantUtil.U8_MAX;
 import static org.apache.flink.types.variant.BinaryVariantUtil.VERSION;
 import static org.apache.flink.types.variant.BinaryVariantUtil.arrayHeader;
 import static org.apache.flink.types.variant.BinaryVariantUtil.checkIndex;
+import static 
org.apache.flink.types.variant.BinaryVariantUtil.fitsVariantDecimal;
 import static org.apache.flink.types.variant.BinaryVariantUtil.getMetadataKey;
 import static org.apache.flink.types.variant.BinaryVariantUtil.handleArray;
 import static org.apache.flink.types.variant.BinaryVariantUtil.handleObject;
@@ -237,23 +238,28 @@ public class BinaryVariantInternalBuilder {
         writePos += 8;
     }
 
-    // Append a decimal value to the variant builder. The caller should 
guarantee that its precision
-    // and scale fit into `MAX_DECIMAL16_PRECISION`.
-    public void appendDecimal(BigDecimal d) {
+    /**
+     * Appends a decimal value to the variant builder. A negative scale is 
rescaled to 0, which
+     * keeps the numeric value.
+     *
+     * @throws VariantTypeException if the precision or the scale of the 
rescaled decimal exceeds
+     *     {@link BinaryVariantUtil#MAX_DECIMAL16_PRECISION}
+     */
+    public void appendDecimal(BigDecimal decimal) {
+        final BigDecimal d = toVariantDecimal(decimal);
         checkCapacity(2 + 16);
         BigInteger unscaled = d.unscaledValue();
-        if (d.scale() <= MAX_DECIMAL4_PRECISION && d.precision() <= 
MAX_DECIMAL4_PRECISION) {
+        if (fitsVariantDecimal(d, MAX_DECIMAL4_PRECISION)) {
             writeBuffer[writePos++] = primitiveHeader(DECIMAL4);
             writeBuffer[writePos++] = (byte) d.scale();
             writeLong(writeBuffer, writePos, unscaled.intValueExact(), 4);
             writePos += 4;
-        } else if (d.scale() <= MAX_DECIMAL8_PRECISION && d.precision() <= 
MAX_DECIMAL8_PRECISION) {
+        } else if (fitsVariantDecimal(d, MAX_DECIMAL8_PRECISION)) {
             writeBuffer[writePos++] = primitiveHeader(DECIMAL8);
             writeBuffer[writePos++] = (byte) d.scale();
             writeLong(writeBuffer, writePos, unscaled.longValueExact(), 8);
             writePos += 8;
         } else {
-            assert d.scale() <= MAX_DECIMAL16_PRECISION && d.precision() <= 
MAX_DECIMAL16_PRECISION;
             writeBuffer[writePos++] = primitiveHeader(DECIMAL16);
             writeBuffer[writePos++] = (byte) d.scale();
             // `toByteArray` returns a big-endian representation. We need to 
copy it reversely and
@@ -271,6 +277,35 @@ public class BinaryVariantInternalBuilder {
         }
     }
 
+    // The variant spec requires a scale in [0, 38] and a precision of at most 
38.
+    private static BigDecimal toVariantDecimal(BigDecimal d) {
+        BigDecimal result = d;
+        if (d.scale() < 0) {
+            // A non-zero value with a scale below -38 has more than 38 digits 
after rescaling.
+            // Reject it upfront because setScale is slow for exponents like 
1e9999999 and throws
+            // an ArithmeticException for exponents like 1e999999999.
+            if (d.signum() != 0 && d.scale() < -MAX_DECIMAL16_PRECISION) {
+                throw decimalOutOfRange(d);
+            }
+            // Rescaling a non-zero value gives it precision - scale digits. 
For example, 12345e34
+            // has precision 5 and scale -34, so it has 5 - (-34) = 39 digits 
after rescaling.
+            result = d.setScale(0);
+        }
+        if (!fitsVariantDecimal(result, MAX_DECIMAL16_PRECISION)) {
+            throw decimalOutOfRange(d);
+        }
+        return result;
+    }
+
+    private static VariantTypeException decimalOutOfRange(BigDecimal d) {
+        return new VariantTypeException(
+                String.format(
+                        "Decimal with precision %d and scale %d is outside the 
range supported by "
+                                + "variant decimals. After rescaling a 
negative scale to 0, the "
+                                + "precision and scale must not exceed %d.",
+                        d.precision(), d.scale(), MAX_DECIMAL16_PRECISION));
+    }
+
     public void appendDate(int daysSinceEpoch) {
         checkCapacity(1 + 4);
         writeBuffer[writePos++] = primitiveHeader(DATE);
@@ -685,7 +720,7 @@ public class BinaryVariantInternalBuilder {
             }
         }
         BigDecimal d = new BigDecimal(input);
-        if (d.scale() <= MAX_DECIMAL16_PRECISION && d.precision() <= 
MAX_DECIMAL16_PRECISION) {
+        if (fitsVariantDecimal(d, MAX_DECIMAL16_PRECISION)) {
             appendDecimal(d);
             return true;
         }
diff --git 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
index 0451f370415..98866958125 100644
--- 
a/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
+++ 
b/flink-core/src/main/java/org/apache/flink/types/variant/BinaryVariantUtil.java
@@ -626,9 +626,17 @@ public class BinaryVariantUtil {
         return Double.longBitsToDouble(readLong(value, pos + 1, 8));
     }
 
+    /**
+     * Returns whether a decimal can be stored as a variant decimal: its 
precision and its
+     * non-negative scale must both be at most {@code maxPrecision}.
+     */
+    static boolean fitsVariantDecimal(BigDecimal d, int maxPrecision) {
+        return d.scale() >= 0 && d.scale() <= maxPrecision && d.precision() <= 
maxPrecision;
+    }
+
     /** Check whether the precision and scale of the decimal are within the 
limit. */
     private static void checkDecimal(BigDecimal d, int maxPrecision) {
-        if (d.precision() > maxPrecision || d.scale() > maxPrecision) {
+        if (!fitsVariantDecimal(d, maxPrecision)) {
             throw malformedVariant();
         }
     }
diff --git 
a/flink-core/src/main/java/org/apache/flink/types/variant/VariantBuilder.java 
b/flink-core/src/main/java/org/apache/flink/types/variant/VariantBuilder.java
index abda3b8491d..5774aa0856f 100644
--- 
a/flink-core/src/main/java/org/apache/flink/types/variant/VariantBuilder.java
+++ 
b/flink-core/src/main/java/org/apache/flink/types/variant/VariantBuilder.java
@@ -58,7 +58,13 @@ public interface VariantBuilder {
     /** Create a variant from a boolean. */
     Variant of(boolean b);
 
-    /** Create a variant from a BigDecimal. */
+    /**
+     * Create a variant from a BigDecimal. A negative scale is rescaled to 0, 
which keeps the
+     * numeric value.
+     *
+     * @throws VariantTypeException if the precision or the scale of the 
rescaled decimal exceeds
+     *     {@link BinaryVariantUtil#MAX_DECIMAL16_PRECISION}
+     */
     Variant of(BigDecimal bigDecimal);
 
     /** Create a variant from an Instant. */
diff --git 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
index 3ce271896a2..a1b3eb5ef06 100644
--- 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
+++ 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantInternalBuilderTest.java
@@ -143,6 +143,19 @@ class BinaryVariantInternalBuilderTest {
                 .isInstanceOf(IOException.class);
     }
 
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "123456789012345678901234567890123456789",
+                "0.000000000000000000000000000000000000001"
+            })
+    void testParseJsonStoresNumbersOutsideDecimalRangeAsDouble(final String 
number)
+            throws IOException {
+        BinaryVariant variant = BinaryVariantInternalBuilder.parseJson(number, 
false);
+        assertThat(variant.getType()).isSameAs(Variant.Type.DOUBLE);
+        assertThat(variant.getDouble()).isEqualTo(Double.parseDouble(number));
+    }
+
     @Test
     void testAppendFloat() {
         BinaryVariantInternalBuilder builder = new 
BinaryVariantInternalBuilder(false);
diff --git 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
index 5379d34cd86..e3725c1d202 100644
--- 
a/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
+++ 
b/flink-core/src/test/java/org/apache/flink/types/variant/BinaryVariantTest.java
@@ -23,6 +23,7 @@ import org.apache.flink.core.testutils.CommonTestUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
 
 import java.math.BigDecimal;
@@ -114,6 +115,56 @@ class BinaryVariantTest {
         assertThat(builder.ofNull().isNull()).isTrue();
     }
 
+    @Test
+    void testDecimalWithNegativeScale() {
+        Variant variant = builder.of(new BigDecimal("-1e5"));
+        assertThat(variant.getType()).isSameAs(Variant.Type.DECIMAL);
+        assertThat(variant.getDecimal()).isEqualByComparingTo("-100000");
+        assertThat(variant.toJson()).isEqualTo("-100000");
+
+        // 38 digits after rescaling
+        assertThat(builder.of(new 
BigDecimal("1e37")).getDecimal()).isEqualByComparingTo("1e37");
+        assertThat(builder.of(new BigDecimal("0e999999999")).getDecimal())
+                .isEqualByComparingTo(BigDecimal.ZERO);
+
+        // getDecimal() strips trailing zeros, so its result can have a 
negative scale
+        BigDecimal stripped = builder.of(BigDecimal.valueOf(100)).getDecimal();
+        assertThat(stripped.scale()).isNegative();
+        
assertThat(builder.of(stripped).getDecimal()).isEqualByComparingTo("100");
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "12345678901234567890123456789012345678",
+                "-0.12345678901234567890123456789012345678"
+            })
+    void testDecimalWithMaxPrecisionAndScale(final String decimal) {
+        assertThat(builder.of(new 
BigDecimal(decimal)).getDecimal()).isEqualByComparingTo(decimal);
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        // precision 39
+        "123456789012345678901234567890123456789, 39, 0",
+        // scale 39
+        "0.000000000000000000000000000000000000001, 1, 39",
+        // 39 digits after rescaling the negative scale
+        "1e38, 1, -38",
+        "-1e999999999, 1, -999999999",
+        // the scale does not fit into a byte
+        "1e2147483647, 1, -2147483647"
+    })
+    void testDecimalOutOfRange(final String decimal, final int precision, 
final int scale) {
+        assertThatThrownBy(() -> builder.of(new BigDecimal(decimal)))
+                .isInstanceOf(VariantTypeException.class)
+                .hasMessage(
+                        "Decimal with precision %d and scale %d is outside the 
range supported by "
+                                + "variant decimals. After rescaling a 
negative scale to 0, the "
+                                + "precision and scale must not exceed 38.",
+                        precision, scale);
+    }
+
     @Test
     void testNanosecondPrecisionVariant() {
         // Microsecond-precision values keep using the compact 
TIMESTAMP/TIMESTAMP_LTZ encoding,

Reply via email to