This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new a0561d4045 [Fix][Transform-V2] Preserve exact binary value when
CASTing floats to DECIMAL (#11523)
a0561d4045 is described below
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;