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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-11523-02b156e4573bae30cd17982adb0965f07d3b0657
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit a0561d4045281c8e31ca96c6812b5024534db481
Author: Jast <[email protected]>
AuthorDate: Thu Sep 10 17:15:17 2026 +0000

    [Fix][Transform-V2] Preserve exact binary value when CASTing floats to 
DECIMAL (#11523)
    
    Co-authored-by: zhangshenghang <[email protected]>
    Co-authored-by: zhangshenghang <[email protected]>
---
 .../sql/zeta/functions/SystemFunction.java         |  30 ++++-
 .../sql/zeta/functions/SystemFunctionTest.java     | 137 +++++++++++++++++++++
 2 files changed, 165 insertions(+), 2 deletions(-)

diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunction.java
index 7917613b2a..c482045052 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunction.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunction.java
@@ -233,9 +233,35 @@ public class SystemFunction {
                         CommonErrorCodeDeprecated.UNSUPPORTED_OPERATION,
                         String.format("Unsupported CAST AS type: %s", v2));
             case "DECIMAL":
-                BigDecimal bigDecimal = new BigDecimal(v1.toString());
+                BigDecimal bigDecimal;
+                RoundingMode roundingMode;
+                if (v1 instanceof BigDecimal) {
+                    bigDecimal = (BigDecimal) v1;
+                    roundingMode = RoundingMode.CEILING;
+                } else if (v1 instanceof Float) {
+                    // Translate the exact binary value, mirroring CAST 
semantics in
+                    // databases (e.g. MySQL). Float.toString() returns the 
shortest
+                    // round-trip representation and drops the hidden binary 
digits
+                    // (126.752251f -> "126.75225"), which would turn CAST(... 
AS
+                    // DECIMAL(20,10)) into 126.7522500000 instead of the exact
+                    // 126.7522506714 (issue #10198). Round half away from 
zero to
+                    // match MySQL CAST semantics; CEILING would otherwise 
push the
+                    // hidden binary tail of 0.1f (0.10000000149...) up to 0.11
+                    // at scale 2 and silently corrupt ordinary DECIMAL casts.
+                    bigDecimal = new BigDecimal((Float) v1);
+                    roundingMode = RoundingMode.HALF_UP;
+                } else if (v1 instanceof Double) {
+                    // Same reasoning as for Float above; the exact binary 
tail of
+                    // 0.1d (0.10000000000000000555...) plus CEILING at scale 2
+                    // would also become 0.11 instead of 0.10.
+                    bigDecimal = new BigDecimal((Double) v1);
+                    roundingMode = RoundingMode.HALF_UP;
+                } else {
+                    bigDecimal = new BigDecimal(v1.toString());
+                    roundingMode = RoundingMode.CEILING;
+                }
                 Integer scale = (Integer) args.get(3);
-                return bigDecimal.setScale(scale, RoundingMode.CEILING);
+                return bigDecimal.setScale(scale, roundingMode);
             case "BOOLEAN":
                 if (v1 instanceof Number) {
                     if (Arrays.asList(1, 0).contains(((Number) 
v1).intValue())) {
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunctionTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunctionTest.java
index b980df25d3..6f87410205 100644
--- 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunctionTest.java
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/zeta/functions/SystemFunctionTest.java
@@ -212,6 +212,143 @@ public class SystemFunctionTest {
         Assertions.assertEquals(new BigDecimal("1.24"), result);
     }
 
+    @Test
+    public void testCastAsDecimalKeepsExactBinaryValueOfFloats() {
+        // The binary value of 126.752251f is 126.75225067138671875. Going 
through
+        // Float.toString() first would lose the hidden binary digits 
("126.75225")
+        // and yield 126.7522500000, while MySQL's CAST(float AS 
DECIMAL(20,10))
+        // converts the exact binary value and yields 126.7522506714 (issue 
#10198).
+        List<Object> args = new ArrayList<>();
+        args.add(126.752251f);
+        args.add("DECIMAL");
+        args.add(20);
+        args.add(10);
+        Assertions.assertEquals(new BigDecimal("126.7522506714"), 
SystemFunction.castAs(args));
+
+        // Scaling to the 6 digits of a numeric(10,6) sink column must round 
the
+        // exact binary value to 126.752251 instead of 126.752250.
+        args.clear();
+        args.add(126.752251f);
+        args.add("DECIMAL");
+        args.add(10);
+        args.add(6);
+        Assertions.assertEquals(new BigDecimal("126.752251"), 
SystemFunction.castAs(args));
+
+        // Double inputs must not be stringified either.
+        args.clear();
+        args.add(44.916103d);
+        args.add("DECIMAL");
+        args.add(20);
+        args.add(10);
+        Assertions.assertEquals(new BigDecimal("44.9161030000"), 
SystemFunction.castAs(args));
+
+        // BigDecimal inputs are passed through without re-parsing.
+        args.clear();
+        args.add(new BigDecimal("126.7522506714"));
+        args.add("DECIMAL");
+        args.add(20);
+        args.add(10);
+        Assertions.assertEquals(new BigDecimal("126.7522506714"), 
SystemFunction.castAs(args));
+    }
+
+    @Test
+    public void testCastAsDecimalRoundsCommonFloatsHalfAwayFromZero() {
+        // The exact binary value of 0.1f is 0.10000000149011612..., and the 
exact
+        // binary value of 0.1d is 0.10000000000000000555.... Combined with the
+        // legacy CEILING rounding, these would both round up to 0.11 at scale 
2
+        // and silently corrupt ordinary DECIMAL casts. Round half away from
+        // zero (the contract used by MySQL's CAST(float AS DECIMAL(...))) so 
the
+        // common positive tail is preserved.
+        List<Object> args = new ArrayList<>();
+        args.add(0.1f);
+        args.add("DECIMAL");
+        args.add(10);
+        args.add(2);
+        Assertions.assertEquals(new BigDecimal("0.10"), 
SystemFunction.castAs(args));
+
+        args.clear();
+        args.add(0.1d);
+        args.add("DECIMAL");
+        args.add(10);
+        args.add(2);
+        Assertions.assertEquals(new BigDecimal("0.10"), 
SystemFunction.castAs(args));
+
+        // Negative tails round symmetrically: -0.1f is -0.10000000149... which
+        // rounds to -0.10 at scale 2 because the absolute value's tail is 
below
+        // the next half-step at the second decimal digit.
+        args.clear();
+        args.add(-0.1f);
+        args.add("DECIMAL");
+        args.add(10);
+        args.add(2);
+        Assertions.assertEquals(new BigDecimal("-0.10"), 
SystemFunction.castAs(args));
+
+        args.clear();
+        args.add(-0.1d);
+        args.add("DECIMAL");
+        args.add(10);
+        args.add(2);
+        Assertions.assertEquals(new BigDecimal("-0.10"), 
SystemFunction.castAs(args));
+
+        // Half-rounding boundary: 0.5f is exactly representable as 0.5 and
+        // rounds up to 1 at scale 0; -0.5d is exactly -0.5 and HALF_UP pushes
+        // it away from zero to -1 (CEILING would have stayed at 0).
+        args.clear();
+        args.add(0.5f);
+        args.add("DECIMAL");
+        args.add(2);
+        args.add(0);
+        Assertions.assertEquals(new BigDecimal("1"), 
SystemFunction.castAs(args));
+
+        args.clear();
+        args.add(-0.5d);
+        args.add("DECIMAL");
+        args.add(2);
+        args.add(0);
+        Assertions.assertEquals(new BigDecimal("-1"), 
SystemFunction.castAs(args));
+
+        // Below-half rounding: 0.49f rounds down to 0 at scale 0 (CEILING
+        // would have wrongly raised it to 1).
+        args.clear();
+        args.add(0.49f);
+        args.add("DECIMAL");
+        args.add(2);
+        args.add(0);
+        Assertions.assertEquals(new BigDecimal("0"), 
SystemFunction.castAs(args));
+
+        // Above-half rounding: 0.51f rounds up to 1 at scale 0.
+        args.clear();
+        args.add(0.51f);
+        args.add("DECIMAL");
+        args.add(2);
+        args.add(0);
+        Assertions.assertEquals(new BigDecimal("1"), 
SystemFunction.castAs(args));
+    }
+
+    @Test
+    public void testCastAsDecimalThroughParsedSqlCast() {
+        // Exercise the parsed SQL CAST(... AS DECIMAL(...)) path end-to-end so
+        // the SystemFunction.castAs helper is reached through ZetaSQLEngine,
+        // not only through the direct unit-test helper call.
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"value"}, new SeaTunnelDataType[] 
{BasicType.FLOAT_TYPE});
+
+        // 0.1f is the regression case: it must stay 0.10 instead of becoming
+        // 0.11 after going through Float -> BigDecimal -> setScale(2).
+        SeaTunnelRow floatRow =
+                runSql("SELECT CAST(value AS DECIMAL(10,2)) AS out_value FROM 
dual", rowType, 0.1f);
+        Assertions.assertEquals(new BigDecimal("0.10"), floatRow.getField(0));
+
+        // 126.752251f -> DECIMAL(10,6) must round to 126.752251 (issue 
#10198).
+        SeaTunnelRow issueRow =
+                runSql(
+                        "SELECT CAST(value AS DECIMAL(10,6)) AS out_value FROM 
dual",
+                        rowType,
+                        126.752251f);
+        Assertions.assertEquals(new BigDecimal("126.752251"), 
issueRow.getField(0));
+    }
+
     @Test
     public void testCoalesceRespectsTargetType() {
         SeaTunnelDataType<?> targetType = BasicType.INT_TYPE;

Reply via email to