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

nzw921rx 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 403efeee0b [Fix][Transform-V2] Preserve exact numeric comparisons in 
SQL predicates (#12318)
403efeee0b is described below

commit 403efeee0b3806c77e55c38368abdd41fbf828bd
Author: Goutam Adwant <[email protected]>
AuthorDate: Tue Oct 27 10:09:31 2026 -0700

    [Fix][Transform-V2] Preserve exact numeric comparisons in SQL predicates 
(#12318)
    
    Signed-off-by: Goutam Adwant <[email protected]>
---
 .../introduction/concepts/incompatible-changes.md  |  26 ++
 docs/en/transforms/sql.md                          |  40 +++
 .../introduction/concepts/incompatible-changes.md  |  21 ++
 docs/zh/transforms/sql.md                          |  33 ++
 .../src/test/resources/sql_transform.conf          |  15 +-
 .../transform/sql/zeta/ZetaSQLFilter.java          |  62 +++-
 .../transform/sql/SQLNumericComparisonTest.java    | 341 +++++++++++++++++++++
 7 files changed, 530 insertions(+), 8 deletions(-)

diff --git a/docs/en/introduction/concepts/incompatible-changes.md 
b/docs/en/introduction/concepts/incompatible-changes.md
index 3984ea546a..9b699028c5 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -266,6 +266,32 @@ The Zeta SQL ARRAY function now declares TINYINT elements 
as `ARRAY<TINYINT>`, m
   values and previously ran successfully will now fail fast at startup with a 
validation
   error.
 
+### SQL Numeric Predicate Precision
+
+- **Behavior change: exact numeric operands no longer compare through 
`double`**
+  - **Affected component**: `seatunnel-transforms-v2`, `Sql` transform (`ZETA` 
and `INTERNAL`).
+  - **Description**: Comparisons and `IN`/`NOT IN` checks preserve precision 
when both
+    evaluated operands are integral or `DECIMAL`, including comparisons in 
searched
+    and simple `CASE` expressions. For example, `BIGINT` `9007199254740993` no 
longer
+    equals the integer literal `9007199254740992`.
+  - **Impact**: Filters can select different rows, and `CASE` expressions can 
choose
+    different branches. Comparisons involving `FLOAT` or `DOUBLE` retain their 
existing
+    behavior. Literals containing a decimal point still evaluate as `DOUBLE`. 
Unary minus
+    preserves `DECIMAL` precision for both `-decimal_col` and
+    `-CAST('...' AS DECIMAL(p, s))`. Each `IN` element independently selects 
the
+    exact or floating path, so mixing exact and floating operands can still 
match
+    distinct large values and does not guarantee transitive equality.
+  - **Migration Guide**: Review affected predicates and `CASE` expressions, 
and reconcile
+    previously written data that relied on rounded comparison results. For 
exact decimal
+    constants, use quoted strings with sufficient scale, such as
+    `CAST('-123456789012345678.99' AS DECIMAL(38, 2))` or
+    `-CAST('123456789012345678.99' AS DECIMAL(38, 2))`. Existing CAST rounding 
is unchanged:
+    string/`DECIMAL` inputs use
+    `CEILING` when reducing scale, so negation before or after CAST can 
produce different results.
+    `FLOAT`/`DOUBLE` inputs use `HALF_UP` and cannot recover
+    precision already lost to floating-point evaluation. See [Numeric 
comparisons](../../transforms/sql.md#numeric-comparisons)
+    for examples and limits. No configuration, output schema or state format 
changes are required.
+
 ### DataValidator Array Length
 
 The `LENGTH` rule now counts array elements rather than characters in the
diff --git a/docs/en/transforms/sql.md b/docs/en/transforms/sql.md
index 6b6691b219..be4c6ffff3 100644
--- a/docs/en/transforms/sql.md
+++ b/docs/en/transforms/sql.md
@@ -32,6 +32,46 @@ or `select c_row.c_inner_row.column_b` to query the inline 
struct column that na
 
 The SQL engine used by this transform. Supported values are `ZETA` and 
`INTERNAL`. If this option is not configured, `ZETA` is used.
 
+## Numeric comparisons
+
+Comparisons preserve numeric precision when both evaluated operands are 
integral
+types (`TINYINT`, `SMALLINT`, `INT`, `BIGINT`) or `DECIMAL`. This applies to 
`=`,
+`!=`, `<>`, `<`, `<=`, `>` and `>=`, numeric `IN` and `NOT IN` membership 
checks,
+and comparisons in searched and simple `CASE` expressions.
+For example, a `BIGINT` value of `9007199254740993` does not match
+`WHERE id = 9007199254740992`.
+
+If either operand is `FLOAT` or `DOUBLE`, comparisons continue to use
+floating-point conversion, retaining the existing behavior for NaN, infinity
+and signed zero. Numeric literals containing a decimal point are evaluated as
+`DOUBLE`, not `DECIMAL`: comparing a `DECIMAL` column to
+`123456789012345678.98` can still match `123456789012345678.99`. Integer 
literals
+evaluated as integral values, such as `-9007199254740993`, use the exact path
+when the other operand is also exact. Unary minus preserves `DECIMAL` 
precision,
+so both `-decimal_col` and `-CAST('...' AS DECIMAL(p, s))` retain exact decimal
+comparison semantics when the other operand is also exact.
+
+For an exact decimal constant, use a quoted string with sufficient scale, for
+example `amount = CAST('123456789012345678.99' AS DECIMAL(38, 2))`.
+For a negative constant, either `CAST('-123456789012345678.99' AS DECIMAL(38, 
2))`
+or `-CAST('123456789012345678.99' AS DECIMAL(38, 2))` preserves decimal 
precision.
+Existing CAST rounding is unchanged: converting a string or `DECIMAL` to
+`DECIMAL(p, s)` uses `CEILING` (toward positive infinity) if scale is reduced,
+so `CAST('1.231' AS DECIMAL(10, 2))` is `1.24` and
+`CAST('-1.239' AS DECIMAL(10, 2))` is `-1.23`. Negating after CAST instead 
gives
+`-1.24` for `-CAST('1.239' AS DECIMAL(10, 2))`: reducing scale rounds before
+negation, so the two forms are not interchangeable in that case. Converting
+`FLOAT` or `DOUBLE` uses `HALF_UP`; casting a fractional numeric literal 
cannot recover precision
+already lost during its evaluation as `DOUBLE`.
+
+The exact-or-floating choice is made for each operand pair, including each
+element of an `IN` list. For `id = 9007199254740993`,
+`id IN (9007199254740992)` is false, but
+`id IN (9007199254740992, 9007199254740992.0)` is true because the second
+comparison uses `DOUBLE`. Equality is therefore not necessarily transitive
+across mixed exact and floating types; use exact operands consistently when
+precision matters. These rules apply to both `ZETA` and `INTERNAL`.
+
 ## Example
 
 The data read from source is a table like this:
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md 
b/docs/zh/introduction/concepts/incompatible-changes.md
index 3a654648af..7a5ceb6b78 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -224,6 +224,27 @@ Zeta SQL ARRAY 函数现在将 TINYINT 元素声明为 `ARRAY<TINYINT>`,与实
   必须满足 min <= max。注意 `row.num = 0`(空 Source)仍然有效。此前设置了无效值且成功运行的现有作业,
   将在启动时快速抛出校验错误并失败。
 
+### SQL 数值条件的精度
+
+- **行为变更:精确数值操作数不再通过 `double` 进行比较**
+  - **影响范围**:`seatunnel-transforms-v2`,`Sql` 转换(`ZETA` 和 `INTERNAL`)。
+  - **变更说明**:当求值后的两个操作数均为整型或 `DECIMAL` 时,比较及 `IN`/`NOT IN` 判断保留精度,
+    包括搜索式和简单式 `CASE` 表达式中的比较。例如,`BIGINT` 值 `9007199254740993`
+    不再等于整数字面量 `9007199254740992`。
+  - **影响**:过滤条件可能选择不同的行,`CASE` 表达式也可能选择不同的分支。
+    涉及 `FLOAT` 或 `DOUBLE` 的比较保持原有行为。包含小数点的字面量仍求值为 `DOUBLE`,
+    一元负号会保留 `DECIMAL` 精度,`-decimal_col` 和 `-CAST('...' AS DECIMAL(p, s))` 均如此。
+    每个 `IN` 元素独立选择精确或浮点比较,
+    因此混用精确类型和浮点类型时,仍可能匹配不同的大数值,且等号不保证传递性。
+  - **迁移指南**:请检查相关条件和 `CASE` 表达式,并核对、修正依赖此前舍入比较结果的已写入数据。
+    如需精确的小数常量,请使用带引号的字符串并保留足够的小数位,例如
+    `CAST('-123456789012345678.99' AS DECIMAL(38, 2))` 或
+    `-CAST('123456789012345678.99' AS DECIMAL(38, 2))`。
+    现有 CAST 舍入行为不变:字符串/`DECIMAL` 输入在减少小数位时使用 `CEILING`,
+    因此在 CAST 之前或之后取负可能产生不同结果;
+    `FLOAT`/`DOUBLE` 输入使用 `HALF_UP`,且无法恢复浮点求值时已丢失的精度。
+    示例与限制见[数值比较](../../transforms/sql.md#数值比较)。无需更改配置、输出结构或状态格式。
+
 ### DataValidator 数组长度
 
 `LENGTH` 规则现在计算数组元素个数,而非数组的 Java 标识字符串长度。
diff --git a/docs/zh/transforms/sql.md b/docs/zh/transforms/sql.md
index f92ed74b4e..1760ebc0ea 100644
--- a/docs/zh/transforms/sql.md
+++ b/docs/zh/transforms/sql.md
@@ -32,6 +32,39 @@ SQL 转换使用内存中的 SQL 引擎,我们可以通过 SQL 函数和 SQL 
 
 该 Transform 使用的 SQL 引擎。支持 `ZETA` 和 `INTERNAL`。如果不配置,默认使用 `ZETA`。
 
+## 数值比较
+
+当求值后的两个操作数均为整型(`TINYINT`、`SMALLINT`、`INT`、`BIGINT`)或 `DECIMAL` 时,比较保留数值精度。
+此规则适用于 `=`、`!=`、`<>`、`<`、`<=`、`>`、`>=`、数值的 `IN` 和 `NOT IN` 判断,
+以及搜索式和简单式 `CASE` 表达式中的比较。
+例如,`BIGINT` 值 `9007199254740993` 不会匹配 `WHERE id = 9007199254740992`。
+
+当任一操作数为 `FLOAT` 或 `DOUBLE` 时,仍使用浮点转换进行比较,保留 NaN、无穷大和带符号零的原有行为。
+包含小数点的数值字面量求值为 `DOUBLE`,而非 `DECIMAL`:因此,将 `DECIMAL` 列与
+`123456789012345678.98` 比较时,仍可能匹配 `123456789012345678.99`。
+求值为整型的整数字面量(如 `-9007199254740993`)在另一操作数也是精确类型时使用精确比较。
+对 `DECIMAL` 表达式应用一元负号会保留 `DECIMAL` 精度,因此在另一操作数也是精确类型时,
+`-decimal_col` 和 `-CAST('...' AS DECIMAL(p, s))` 均保留精确的小数比较语义。
+
+如需精确的小数常量,请使用带引号的字符串并保留足够的小数位,例如
+`amount = CAST('123456789012345678.99' AS DECIMAL(38, 2))`。
+对于负数常量,`CAST('-123456789012345678.99' AS DECIMAL(38, 2))` 和
+`-CAST('123456789012345678.99' AS DECIMAL(38, 2))` 均保留小数精度。
+现有 CAST 舍入行为不变:将字符串或 `DECIMAL` 转换为 `DECIMAL(p, s)` 时,
+如果减少小数位,则采用 `CEILING`(向正无穷方向舍入)。因此,
+`CAST('1.231' AS DECIMAL(10, 2))` 为 `1.24`,
+`CAST('-1.239' AS DECIMAL(10, 2))` 为 `-1.23`。如果在 CAST 之后取负,
+`-CAST('1.239' AS DECIMAL(10, 2))` 则为 `-1.24`:减少小数位时先舍入再取负,
+因此这两种写法在该情况下不能互换。
+转换 `FLOAT` 或 `DOUBLE` 时采用 `HALF_UP`;对带小数点的数值字面量进行 CAST,
+无法恢复其求值为 `DOUBLE` 时已丢失的精度。
+
+每一对操作数独立选择精确或浮点比较,包括 `IN` 列表中的每个元素。
+当 `id = 9007199254740993` 时,`id IN (9007199254740992)` 为假,
+但 `id IN (9007199254740992, 9007199254740992.0)` 为真,因为第二次比较使用 `DOUBLE`。
+因此,在精确类型与浮点类型混用时,等号不一定满足传递性;需要精度保证时,请统一使用精确类型的操作数。
+以上规则适用于 `ZETA` 和 `INTERNAL` 两种 SQL 引擎设置。
+
 ## 示例
 
 源端数据读取的表格如下:
diff --git 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform.conf
 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform.conf
index 4bba0cacba..b2de7b4c8c 100644
--- 
a/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform.conf
+++ 
b/seatunnel-e2e/seatunnel-transforms-v2-e2e/seatunnel-transforms-v2-e2e-part-2/src/test/resources/sql_transform.conf
@@ -26,9 +26,12 @@ source {
   FakeSource {
     plugin_output = "fake"
     row.num = 100
+    bigint.fake.mode = "template"
+    bigint.template = [9007199254740993]
     schema = {
       fields {
         id = "int"
+        large_id = "bigint"
         name = "string"
         age = "int"
         c_time = "time"
@@ -52,7 +55,7 @@ transform {
     plugin_input = "fake"
     plugin_output = "fake1"
     # the query table name must same as field 'plugin_input'
-    query = "select id, regexp_replace(name, '.+', 'b') as name, age+1 as age, 
pi() as pi, c_time, c_timestamp, c_date, c_map, c_array, c_decimal, c_row from 
dual"
+    query = "select id, regexp_replace(name, '.+', 'b') as name, age+1 as age, 
pi() as pi, c_time, c_timestamp, c_date, c_map, c_array, c_decimal, c_row, 
large_id, case large_id when 9007199254740992 then 'rounded' else 'exact' end 
as comparison_result from dual where large_id > 9007199254740992 and large_id 
not in (9007199254740992)"
   }
   # The SQL transform support base function and criteria operation
   # But the complex SQL unsupported yet, include: multi source table/rows JOIN 
and AGGREGATE operation and the like
@@ -70,6 +73,16 @@ sink {
           }
         ]
         field_rules = [
+          {
+            field_name = large_id
+            field_type = bigint
+            field_value = [{equals_to = 9007199254740993}]
+          },
+          {
+            field_name = comparison_result
+            field_type = string
+            field_value = [{equals_to = "exact"}]
+          },
           {
             field_name = id
             field_type = int
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFilter.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFilter.java
index 9446e323d0..8bf3a145fc 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFilter.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFilter.java
@@ -22,6 +22,7 @@ import 
org.apache.seatunnel.shade.org.apache.commons.lang3.tuple.Pair;
 import org.apache.seatunnel.api.table.type.BasicType;
 import org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated;
 import org.apache.seatunnel.transform.exception.TransformException;
+import org.apache.seatunnel.transform.sql.zeta.functions.NumericFunction;
 
 import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.Function;
@@ -41,6 +42,7 @@ import 
net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
 import 
net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
 import net.sf.jsqlparser.schema.Column;
 
+import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
@@ -154,7 +156,13 @@ public class ZetaSQLFilter {
             }
             if (leftValue != null) {
                 if (leftValue instanceof Number && rightValue instanceof 
Number) {
-                    if (((Number) leftValue).doubleValue() == ((Number) 
rightValue).doubleValue()) {
+                    boolean equal =
+                            areExactNumbers(leftValue, rightValue)
+                                    ? compareExactNumbers((Number) leftValue, 
(Number) rightValue)
+                                            == 0
+                                    : ((Number) leftValue).doubleValue()
+                                            == ((Number) 
rightValue).doubleValue();
+                    if (equal) {
                         return !inExpression.isNot();
                     }
                 } else if (leftValue instanceof OffsetDateTime
@@ -277,7 +285,9 @@ public class ZetaSQLFilter {
             return false;
         }
         if (leftVal instanceof Number && rightVal instanceof Number) {
-            return ((Number) leftVal).doubleValue() == ((Number) 
rightVal).doubleValue();
+            return areExactNumbers(leftVal, rightVal)
+                    ? compareExactNumbers((Number) leftVal, (Number) rightVal) 
== 0
+                    : ((Number) leftVal).doubleValue() == ((Number) 
rightVal).doubleValue();
         }
         if (leftVal instanceof OffsetDateTime && rightVal instanceof 
OffsetDateTime) {
             return ((OffsetDateTime) leftVal).isEqual((OffsetDateTime) 
rightVal);
@@ -292,7 +302,9 @@ public class ZetaSQLFilter {
             return rightVal != null;
         }
         if (leftVal instanceof Number && rightVal instanceof Number) {
-            return ((Number) leftVal).doubleValue() != ((Number) 
rightVal).doubleValue();
+            return areExactNumbers(leftVal, rightVal)
+                    ? compareExactNumbers((Number) leftVal, (Number) rightVal) 
!= 0
+                    : ((Number) leftVal).doubleValue() != ((Number) 
rightVal).doubleValue();
         }
         if (leftVal instanceof OffsetDateTime && rightVal instanceof 
OffsetDateTime) {
             return !((OffsetDateTime) leftVal).isEqual((OffsetDateTime) 
rightVal);
@@ -307,7 +319,9 @@ public class ZetaSQLFilter {
             return false;
         }
         if (leftVal instanceof Number && rightVal instanceof Number) {
-            return ((Number) leftVal).doubleValue() > ((Number) 
rightVal).doubleValue();
+            return areExactNumbers(leftVal, rightVal)
+                    ? compareExactNumbers((Number) leftVal, (Number) rightVal) 
> 0
+                    : ((Number) leftVal).doubleValue() > ((Number) 
rightVal).doubleValue();
         }
         if (leftVal instanceof String && rightVal instanceof String) {
             return ((String) leftVal).compareTo((String) rightVal) > 0;
@@ -338,7 +352,9 @@ public class ZetaSQLFilter {
             return false;
         }
         if (leftVal instanceof Number && rightVal instanceof Number) {
-            return ((Number) leftVal).doubleValue() >= ((Number) 
rightVal).doubleValue();
+            return areExactNumbers(leftVal, rightVal)
+                    ? compareExactNumbers((Number) leftVal, (Number) rightVal) 
>= 0
+                    : ((Number) leftVal).doubleValue() >= ((Number) 
rightVal).doubleValue();
         }
         if (leftVal instanceof String && rightVal instanceof String) {
             return ((String) leftVal).compareTo((String) rightVal) >= 0;
@@ -384,7 +400,9 @@ public class ZetaSQLFilter {
             return ((LocalTime) leftVal).isBefore((LocalTime) rightVal);
         }
         if (leftVal instanceof Number && rightVal instanceof Number) {
-            return ((Number) leftVal).doubleValue() < ((Number) 
rightVal).doubleValue();
+            return areExactNumbers(leftVal, rightVal)
+                    ? compareExactNumbers((Number) leftVal, (Number) rightVal) 
< 0
+                    : ((Number) leftVal).doubleValue() < ((Number) 
rightVal).doubleValue();
         }
         if (leftVal instanceof String && rightVal instanceof String) {
             return ((String) leftVal).compareTo((String) rightVal) < 0;
@@ -418,7 +436,9 @@ public class ZetaSQLFilter {
             return ((LocalTime) leftVal).isBefore((LocalTime) rightVal) || 
leftVal.equals(rightVal);
         }
         if (leftVal instanceof Number && rightVal instanceof Number) {
-            return ((Number) leftVal).doubleValue() <= ((Number) 
rightVal).doubleValue();
+            return areExactNumbers(leftVal, rightVal)
+                    ? compareExactNumbers((Number) leftVal, (Number) rightVal) 
<= 0
+                    : ((Number) leftVal).doubleValue() <= ((Number) 
rightVal).doubleValue();
         }
         if (leftVal instanceof String && rightVal instanceof String) {
             return ((String) leftVal).compareTo((String) rightVal) <= 0;
@@ -430,6 +450,34 @@ public class ZetaSQLFilter {
                         leftVal.getClass().getSimpleName(), 
rightVal.getClass().getSimpleName()));
     }
 
+    private static boolean areExactNumbers(Object left, Object right) {
+        return isExactNumber(left) && isExactNumber(right);
+    }
+
+    /**
+     * Recognizes integral wrappers and BigDecimal. Float/Double and other 
Number subtypes
+     * (including BigInteger) retain the legacy double path. Callers use 
native floating operators,
+     * not Double.compare, to preserve NaN, infinity and signed-zero behavior.
+     */
+    private static boolean isExactNumber(Object value) {
+        return value instanceof Byte
+                || value instanceof Short
+                || value instanceof Integer
+                || value instanceof Long
+                || value instanceof BigDecimal;
+    }
+
+    /** Compares exact operands; callers must first establish {@link 
#areExactNumbers}. */
+    private static int compareExactNumbers(Number left, Number right) {
+        // Integral pairs need no allocation. Mixed decimal/integral pairs may 
allocate a BigDecimal
+        // per comparison (including each IN item) to preserve precision 
instead of using double.
+        if (left instanceof BigDecimal || right instanceof BigDecimal) {
+            return NumericFunction.toBigDecimal(left)
+                    .compareTo(NumericFunction.toBigDecimal(right));
+        }
+        return Long.compare(left.longValue(), right.longValue());
+    }
+
     private boolean andExpr(AndExpression andExpression, Object[] inputFields) 
{
         Expression leftExpr = andExpression.getLeftExpression();
         boolean leftRes = executeFilter(leftExpr, inputFields);
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLNumericComparisonTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLNumericComparisonTest.java
new file mode 100644
index 0000000000..f9d3628480
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLNumericComparisonTest.java
@@ -0,0 +1,341 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.transform.sql;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.DecimalType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Exercises SQLTransform deliberately: parsing and operand evaluation 
determine numeric types, and
+ * both searched and simple CASE expressions route back through the comparison 
filter.
+ */
+class SQLNumericComparisonTest {
+    private static final String[] OPERATORS = {
+        "=", "!=", "<>", ">", ">=", "<", "<=", "IN", "NOT IN"
+    };
+
+    static Stream<Arguments> exactComparisons() {
+        List<Arguments> cases = new ArrayList<>();
+        addPair(cases, 9007199254740993L, 9007199254740992L, 1);
+        addPair(cases, -9007199254740993L, -9007199254740992L, -1);
+        addPair(cases, Long.MAX_VALUE, Long.MAX_VALUE - 1, 1);
+        addPair(cases, Long.MIN_VALUE, Long.MIN_VALUE + 1, -1);
+        addPair(cases, Long.MIN_VALUE, Long.MAX_VALUE, -1);
+        addPair(
+                cases,
+                new BigDecimal("123456789012345678.99"),
+                new BigDecimal("123456789012345678.98"),
+                1);
+        addPair(
+                cases,
+                new BigDecimal("1.00000000000000000002"),
+                new BigDecimal("1.00000000000000000001"),
+                1);
+        addPair(
+                cases,
+                new BigDecimal("-123456789012345678.99"),
+                new BigDecimal("-123456789012345678.98"),
+                -1);
+        addPair(
+                cases,
+                new BigDecimal("-1.00000000000000000002"),
+                new BigDecimal("-1.00000000000000000001"),
+                -1);
+        addPair(cases, new BigDecimal("9007199254740993"), 9007199254740992L, 
1);
+        addPair(cases, new BigDecimal("2.00"), 2L, 0);
+        addPair(cases, new BigDecimal("2.0"), new BigDecimal("2.00"), 0);
+        addPair(cases, (byte) 1, (short) 2, -1);
+        addPair(cases, Integer.MAX_VALUE, (long) Integer.MAX_VALUE, 0);
+        return cases.stream();
+    }
+
+    private static void addPair(List<Arguments> cases, Number a, Number b, int 
comparison) {
+        for (String operator : OPERATORS) {
+            cases.add(Arguments.of(a, b, operator, accepts(operator, 
comparison)));
+            cases.add(Arguments.of(b, a, operator, accepts(operator, 
-comparison)));
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("exactComparisons")
+    void comparesExactNumbers(Number a, Number b, String operator, boolean 
expected) {
+        assertEquals(expected, matches(predicate(operator), a, b));
+    }
+
+    static Stream<Arguments> floatingComparisons() {
+        List<Arguments> cases = new ArrayList<>();
+        Number[][] pairs = {
+            {Double.NaN, Double.NaN},
+            {Double.NaN, 1L},
+            {1L, Double.NaN},
+            {Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY},
+            {Double.NEGATIVE_INFINITY, 1L},
+            {-0.0d, 0.0d},
+            {-0.0f, 0L},
+            {0.1f, 0.1d},
+            {9007199254740993L, 9007199254740992d},
+            {new BigDecimal("0.1"), 0.1d},
+            {Float.NaN, 0f}
+        };
+        for (Number[] pair : pairs) {
+            for (String op : OPERATORS) {
+                double a = pair[0].doubleValue();
+                double b = pair[1].doubleValue();
+                boolean expected;
+                switch (op) {
+                    case "=":
+                    case "IN":
+                        expected = a == b;
+                        break;
+                    case "!=":
+                    case "<>":
+                    case "NOT IN":
+                        expected = a != b;
+                        break;
+                    case ">":
+                        expected = a > b;
+                        break;
+                    case ">=":
+                        expected = a >= b;
+                        break;
+                    case "<":
+                        expected = a < b;
+                        break;
+                    default:
+                        expected = a <= b;
+                }
+                cases.add(Arguments.of(pair[0], pair[1], op, expected));
+            }
+        }
+        return cases.stream();
+    }
+
+    @ParameterizedTest
+    @MethodSource("floatingComparisons")
+    void preservesFloatingPointComparisons(Number a, Number b, String 
operator, boolean expected) {
+        assertEquals(expected, matches(predicate(operator), a, b));
+    }
+
+    @Test
+    void comparesLargeIntegerLiteralAndMultipleInValues() {
+        assertEquals(false, matches("a = 9007199254740992", 9007199254740993L, 
0L));
+        assertEquals(true, matches("a IN (0, 9007199254740993)", 
9007199254740993L, 0L));
+        assertEquals(false, matches("a IN (0, 9007199254740992)", 
9007199254740993L, 0L));
+        assertEquals(true, matches("a NOT IN (0, 9007199254740992)", 
9007199254740993L, 0L));
+    }
+
+    @Test
+    void preservesFloatingComparisonForFractionalLiterals() {
+        BigDecimal value = new BigDecimal("123456789012345678.99");
+        assertEquals(true, matches("a = 123456789012345678.98", value, 0L));
+        assertEquals(false, matches("a <> 123456789012345678.98", value, 0L));
+        assertEquals(false, matches("a > 123456789012345678.98", value, 0L));
+    }
+
+    @Test
+    void comparesExplicitDecimalCastsExactly() {
+        BigDecimal value = new BigDecimal("123456789012345678.99");
+        SQLTransform transform =
+                new SQLTransform(
+                        ReadonlyConfig.fromMap(
+                                Collections.singletonMap(
+                                        "query",
+                                        "select CAST('123456789012345678.99' 
AS DECIMAL(38, 2)) as exact_value, "
+                                                + 
"CAST('123456789012345678.99' AS DECIMAL(38, 4)) as scaled_value from dual")),
+                        CatalogTableUtil.getCatalogTable(
+                                "test",
+                                new SeaTunnelRowType(
+                                        new String[] {"a"},
+                                        new SeaTunnelDataType<?>[] {new 
DecimalType(38, 2)})));
+        List<SeaTunnelRow> output = transform.transformRow(new 
SeaTunnelRow(new Object[] {value}));
+        assertEquals(1, output.size());
+        assertEquals(value, output.get(0).getField(0));
+        assertEquals(new BigDecimal("123456789012345678.9900"), 
output.get(0).getField(1));
+        assertEquals(
+                true, matches("a = CAST('123456789012345678.99' AS DECIMAL(38, 
2))", value, 0L));
+        assertEquals(
+                false, matches("a = CAST('123456789012345678.98' AS 
DECIMAL(38, 2))", value, 0L));
+        assertEquals(
+                true, matches("a > CAST('123456789012345678.98' AS DECIMAL(38, 
2))", value, 0L));
+        assertEquals(
+                true,
+                matches("a = CAST('9007199254740993' AS DECIMAL(38, 0))", 
9007199254740993L, 0L));
+        assertEquals(
+                false,
+                matches("a = CAST('9007199254740992' AS DECIMAL(38, 0))", 
9007199254740993L, 0L));
+    }
+
+    @Test
+    void comparesNegativeIntegerLiteralsExactly() {
+        assertEquals(true, matches("a = -9007199254740993", 
-9007199254740993L, 0L));
+        assertEquals(false, matches("a = -9007199254740992", 
-9007199254740993L, 0L));
+        assertEquals(true, matches("a < -9007199254740992", 
-9007199254740993L, 0L));
+    }
+
+    @Test
+    void preservesPerElementComparisonForMixedInLists() {
+        assertEquals(false, matches("a = 9007199254740992", 9007199254740993L, 
0L));
+        assertEquals(true, matches("a = 9007199254740992.0", 
9007199254740993L, 0L));
+        assertEquals(false, matches("a IN (9007199254740992)", 
9007199254740993L, 0L));
+        assertEquals(
+                true,
+                matches("a IN (9007199254740992, 9007199254740992.0)", 
9007199254740993L, 0L));
+        assertEquals(
+                true,
+                matches("a IN (9007199254740992.0, 9007199254740992)", 
9007199254740993L, 0L));
+        assertEquals(
+                false,
+                matches("a NOT IN (9007199254740992, 9007199254740992.0)", 
9007199254740993L, 0L));
+    }
+
+    @Test
+    void comparesNegatedDecimalOperandsExactly() {
+        BigDecimal value = new BigDecimal("-123456789012345678.99");
+        assertEquals(
+                false, matches("a = -CAST('123456789012345678.98' AS 
DECIMAL(38, 2))", value, 0L));
+        assertEquals(
+                false, matches("-a = CAST('123456789012345678.98' AS 
DECIMAL(38, 2))", value, 0L));
+        assertEquals(
+                true, matches("a = -CAST('123456789012345678.99' AS 
DECIMAL(38, 2))", value, 0L));
+        assertEquals(
+                true, matches("-a = CAST('123456789012345678.99' AS 
DECIMAL(38, 2))", value, 0L));
+        assertEquals(
+                false, matches("a = CAST('-123456789012345678.98' AS 
DECIMAL(38, 2))", value, 0L));
+        assertEquals(
+                true, matches("a = CAST('-123456789012345678.99' AS 
DECIMAL(38, 2))", value, 0L));
+        assertEquals(true, matches("a = -123456789012345678.98", value, 0L));
+    }
+
+    @Test
+    void preservesDecimalCastScaleRounding() {
+        assertEquals(
+                true, matches("a = CAST('1.231' AS DECIMAL(10, 2))", new 
BigDecimal("1.24"), 0L));
+        assertEquals(
+                true, matches("a = CAST('-1.239' AS DECIMAL(10, 2))", new 
BigDecimal("-1.23"), 0L));
+        assertEquals(
+                true, matches("a = CAST(1.231 AS DECIMAL(10, 2))", new 
BigDecimal("1.23"), 0L));
+        assertEquals(
+                true, matches("a = CAST(-1.239 AS DECIMAL(10, 2))", new 
BigDecimal("-1.24"), 0L));
+    }
+
+    @Test
+    void comparesExactNumbersInCaseExpressions() {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"a", "b"},
+                        new SeaTunnelDataType<?>[] {BasicType.LONG_TYPE, 
BasicType.LONG_TYPE});
+        SQLTransform transform =
+                new SQLTransform(
+                        ReadonlyConfig.fromMap(
+                                Collections.singletonMap(
+                                        "query",
+                                        "select case when a > b then 'higher' 
else 'lower' end as ordering, "
+                                                + "case a when b then 'same' 
else 'different' end as equality from dual")),
+                        CatalogTableUtil.getCatalogTable("test", rowType));
+        List<SeaTunnelRow> output =
+                transform.transformRow(
+                        new SeaTunnelRow(new Object[] {9007199254740993L, 
9007199254740992L}));
+        assertEquals("higher", output.get(0).getField(0));
+        assertEquals("different", output.get(0).getField(1));
+    }
+
+    @Test
+    void leavesNullAndStringHandlingUnchanged() {
+        assertEquals(false, matches("a = b", null, 1L));
+        assertEquals(false, matches("a > b", 1L, null));
+        assertEquals(true, matches("a IS NULL", null, 1L));
+        assertEquals(true, matches("a = b", "same", "same"));
+        assertEquals(true, matches("a < b", "a", "b"));
+    }
+
+    private static String predicate(String operator) {
+        return "a " + operator + (operator.endsWith("IN") ? " (b)" : " b");
+    }
+
+    private static boolean accepts(String op, int comparison) {
+        switch (op) {
+            case "=":
+            case "IN":
+                return comparison == 0;
+            case "!=":
+            case "<>":
+            case "NOT IN":
+                return comparison != 0;
+            case ">":
+                return comparison > 0;
+            case ">=":
+                return comparison >= 0;
+            case "<":
+                return comparison < 0;
+            default:
+                return comparison <= 0;
+        }
+    }
+
+    private static boolean matches(String predicate, Object a, Object b) {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"a", "b"}, new SeaTunnelDataType<?>[] 
{type(a), type(b)});
+        SQLTransform transform =
+                new SQLTransform(
+                        ReadonlyConfig.fromMap(
+                                Collections.singletonMap(
+                                        "query", "select a, b from dual where 
" + predicate)),
+                        CatalogTableUtil.getCatalogTable("test", rowType));
+        List<SeaTunnelRow> output = transform.transformRow(new 
SeaTunnelRow(new Object[] {a, b}));
+        if (output == null || output.isEmpty()) {
+            return false;
+        }
+        assertEquals(1, output.size());
+        assertEquals(a, output.get(0).getField(0));
+        assertEquals(b, output.get(0).getField(1));
+        return true;
+    }
+
+    private static SeaTunnelDataType<?> type(Object value) {
+        if (value instanceof BigDecimal) {
+            BigDecimal decimal = (BigDecimal) value;
+            return new DecimalType(38, Math.max(0, decimal.scale()));
+        }
+        if (value instanceof Byte) return BasicType.BYTE_TYPE;
+        if (value instanceof Short) return BasicType.SHORT_TYPE;
+        if (value instanceof Integer) return BasicType.INT_TYPE;
+        if (value instanceof Float) return BasicType.FLOAT_TYPE;
+        if (value instanceof Double) return BasicType.DOUBLE_TYPE;
+        if (value instanceof String) return BasicType.STRING_TYPE;
+        return BasicType.LONG_TYPE;
+    }
+}

Reply via email to