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

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


The following commit(s) were added to refs/heads/master by this push:
     new 99877f5ce96 [fix](RF) fix RF cast expr nullable (#62627)
99877f5ce96 is described below

commit 99877f5ce9636f9c92e8cfbed465f0034d76de13
Author: TengJianPing <[email protected]>
AuthorDate: Wed May 13 10:33:15 2026 +0800

    [fix](RF) fix RF cast expr nullable (#62627)
---
 .../doris/nereids/analyzer/UnboundFunction.java    |   4 -
 .../glue/translator/RuntimeFilterTranslator.java   |  23 +--
 .../runtime_filter/runtime_filter_cast.out         |  47 +++++
 .../runtime_filter/runtime_filter_cast.groovy      | 193 +++++++++++++++++++++
 4 files changed, 253 insertions(+), 14 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundFunction.java
index 0f665ee8b05..d0952f281fa 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/analyzer/UnboundFunction.java
@@ -112,10 +112,6 @@ public class UnboundFunction extends Function implements 
Unbound, PropagateNulla
         return isSkew;
     }
 
-    public List<Expression> getArguments() {
-        return children();
-    }
-
     @Override
     public String computeToSql() throws UnboundException {
         String params = children.stream()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/RuntimeFilterTranslator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/RuntimeFilterTranslator.java
index 5192df15508..ae48b631014 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/RuntimeFilterTranslator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/RuntimeFilterTranslator.java
@@ -195,12 +195,7 @@ public class RuntimeFilterTranslator {
                             new 
RuntimeFilterExpressionTranslator(targetSlotRef);
                     targetExpr = curTargetExpression.accept(translator, ctx);
                 }
-                if (!src.getType().equals(targetExpr.getType()) && 
head.getType() != TRuntimeFilterType.BITMAP) {
-                    targetExpr = new CastExpr(src.getType(), targetExpr,
-                            Cast.castNullable(src.isNullable(),
-                                    DataType.fromCatalogType(src.getType()),
-                                    
DataType.fromCatalogType(targetExpr.getType())));
-                }
+                targetExpr = castTargetToSourceTypeIfNeeded(src, targetExpr, 
head.getType());
                 TupleId targetTupleId = targetSlotRef.getDesc().getParentId();
                 SlotId targetSlotId = targetSlotRef.getSlotId();
                 scanNodeList.add(scanNode);
@@ -290,10 +285,7 @@ public class RuntimeFilterTranslator {
                 }
 
                 // adjust data type
-                if (!src.getType().equals(targetExpr.getType()) && 
filter.getType() != TRuntimeFilterType.BITMAP) {
-                    targetExpr = new CastExpr(src.getType(), targetExpr, 
Cast.castNullable(src.isNullable(),
-                            DataType.fromCatalogType(src.getType()), 
DataType.fromCatalogType(targetExpr.getType())));
-                }
+                targetExpr = castTargetToSourceTypeIfNeeded(src, targetExpr, 
filter.getType());
                 TupleId targetTupleId = targetSlotRef.getDesc().getParentId();
                 SlotId targetSlotId = targetSlotRef.getSlotId();
                 scanNodeList.add(scanNode);
@@ -350,4 +342,15 @@ public class RuntimeFilterTranslator {
         origFilter.extractTargetsPosition();
         return origFilter;
     }
+
+    private Expr castTargetToSourceTypeIfNeeded(Expr src, Expr targetExpr, 
TRuntimeFilterType filterType) {
+        // The cast is: CAST(targetExpr AS src.getType()), so the child of the 
cast
+        // is targetExpr and the destination type is src.getType().
+        // castNullable(srcNullable, srcType, targetType) expects: child 
nullable, child type, dest type.
+        if (!src.getType().equals(targetExpr.getType()) && filterType != 
TRuntimeFilterType.BITMAP) {
+            return new CastExpr(src.getType(), targetExpr, 
Cast.castNullable(targetExpr.isNullable(),
+                    DataType.fromCatalogType(targetExpr.getType()), 
DataType.fromCatalogType(src.getType())));
+        }
+        return targetExpr;
+    }
 }
diff --git 
a/regression-test/data/query_p0/runtime_filter/runtime_filter_cast.out 
b/regression-test/data/query_p0/runtime_filter/runtime_filter_cast.out
new file mode 100644
index 00000000000..4f9aacc8fae
--- /dev/null
+++ b/regression-test/data/query_p0/runtime_filter/runtime_filter_cast.out
@@ -0,0 +1,47 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !decimal_rf_join --
+300.00 300
+
+-- !decimal_rf_join_var --
+300.00 300
+
+-- !decimal_rf_except --
+100.0
+200.0
+
+-- !decimal_rf_except_var --
+100.0
+200.0
+
+-- !decimal_rf_eq --
+2      100.50  b       1       100.5000000000
+3      150.25  c       3       150.2500000000
+5      200.75  e       2       200.7500000000
+
+-- !decimal_rf_le --
+1      50.12   200.7500000000
+2      100.50  200.7500000000
+3      150.25  200.7500000000
+4      175.99  200.7500000000
+5      200.75  200.7500000000
+
+-- !decimal_rf_ge --
+2      100.50  100.5000000000
+3      150.25  100.5000000000
+4      175.99  100.5000000000
+5      200.75  100.5000000000
+7      300.00  100.5000000000
+
+-- !decimal_rf_implicit_cast --
+2      100.50  100.5000000000
+3      150.25  150.2500000000
+5      200.75  200.7500000000
+
+-- !decimal_rf_nullable_single_target --
+17     3       300.00  300
+
+-- !decimal_rf_grouped_targets --
+2      12      1       100.50  100.50
+3      13      3       150.25  150.25
+5      15      2       200.75  200.75
+
diff --git 
a/regression-test/suites/query_p0/runtime_filter/runtime_filter_cast.groovy 
b/regression-test/suites/query_p0/runtime_filter/runtime_filter_cast.groovy
new file mode 100644
index 00000000000..18699b17e1a
--- /dev/null
+++ b/regression-test/suites/query_p0/runtime_filter/runtime_filter_cast.groovy
@@ -0,0 +1,193 @@
+// 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.
+
+// Regression test for crash in DecimalComparison due to column/type
+// nullability mismatch when runtime filter pushes MIN/MAX predicates
+// involving decimal type casts on nullable columns.
+suite("runtime_filter_cast") {
+    String db = context.config.getDbNameByFile(context.file)
+    sql "use ${db}"
+    sql "SET enable_nereids_planner=true"
+    sql "SET enable_fallback_to_original_planner=false"
+    sql "set disable_join_reorder=true"
+    sql "set runtime_filter_type=4"
+    sql "set runtime_filter_mode=GLOBAL"
+    sql "set runtime_filter_wait_time_ms=10000"
+    sql "set disable_nereids_rules=PRUNE_EMPTY_PARTITION"
+    sql "set enable_runtime_filter_prune=false"
+    sql "set expand_runtime_filter_by_inner_join=true"
+
+    // Build-side table: small dimension table with decimal column
+    sql "drop table if exists decimal_rf_build"
+    sql """
+        CREATE TABLE decimal_rf_build (
+            `id` INT NOT NULL,
+            `val_int_not_null` INT NOT NULL,
+            `val` DECIMALV3(38, 10) NOT NULL
+        ) ENGINE=OLAP
+        DUPLICATE KEY(`id`)
+        DISTRIBUTED BY HASH(`id`) BUCKETS 1
+        PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1",
+            "storage_format" = "V2"
+        )
+    """
+
+    // Probe-side table: fact table with nullable decimal of different 
precision
+    // The different precision forces VCastExpr in the runtime filter 
comparison
+    sql "drop table if exists decimal_rf_probe"
+    sql """
+        CREATE TABLE decimal_rf_probe (
+            `id` INT NOT NULL,
+            `val` DECIMALV3(15, 2) NOT NULL,
+            `data` VARCHAR(20) NULL
+        ) ENGINE=OLAP
+        DUPLICATE KEY(`id`)
+        DISTRIBUTED BY HASH(`id`) BUCKETS 10
+        PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1",
+            "storage_format" = "V2"
+        )
+    """
+
+    sql """
+        INSERT INTO decimal_rf_build VALUES
+        (1, 100, 100.5000000000),
+        (2, 200, 200.7500000000),
+        (3, 300, 150.2500000000)
+    """
+
+    sql """
+        INSERT INTO decimal_rf_probe VALUES
+        (1, 50.12, 'a'),
+        (2, 100.50, 'b'),
+        (3, 150.25, 'c'),
+        (4, 175.99, 'd'),
+        (5, 200.75, 'e'),
+        (7, 300.00, 'g')
+    """
+
+    sql "drop table if exists decimal_rf_probe_nullable"
+    sql """
+        CREATE TABLE decimal_rf_probe_nullable (
+            `id` INT NOT NULL,
+            `val` DECIMALV3(15, 2) NULL,
+            `data` VARCHAR(20) NULL
+        ) ENGINE=OLAP
+        DUPLICATE KEY(`id`)
+        DISTRIBUTED BY HASH(`id`) BUCKETS 10
+        PROPERTIES (
+            "replication_allocation" = "tag.location.default: 1",
+            "storage_format" = "V2"
+        )
+    """
+
+    sql """
+        INSERT INTO decimal_rf_probe_nullable VALUES
+        (12, 100.50, 'bb'),
+        (13, 150.25, 'cc'),
+        (15, 200.75, 'ee'),
+        (17, 300.00, 'gg'),
+        (99, NULL, 'null')
+    """
+
+    sql "SET @v0 = 1 ;"
+
+    order_qt_decimal_rf_join """
+        SELECT p.val, b.val_int_not_null
+        FROM decimal_rf_probe p
+        INNER JOIN decimal_rf_build b
+        ON round(p.val, 1) = b.val_int_not_null
+    """
+    order_qt_decimal_rf_join_var """
+        SELECT p.val, b.val_int_not_null
+        FROM decimal_rf_probe p
+        INNER JOIN decimal_rf_build b
+        ON round(p.val, @v0) = b.val_int_not_null
+    """
+
+    order_qt_decimal_rf_except """
+    select t1.val_int_not_null
+    from decimal_rf_build t1
+    except
+    select round(t2.val, 1)
+    from decimal_rf_probe t2
+    """
+
+    order_qt_decimal_rf_except_var """
+    select t1.val_int_not_null
+    from decimal_rf_build t1
+    except
+    select round(t2.val, @v0)
+    from decimal_rf_probe t2
+    """
+
+    // Test 1: equi-join triggers runtime filter on probe decimal column
+    // The different decimal precision/scale triggers VCastExpr in the
+    // runtime filter's comparison, which previously crashed due to
+    // nullable column / non-nullable type mismatch.
+    order_qt_decimal_rf_eq """
+        SELECT p.id, p.val, p.data, b.id as bid, b.val as bval
+        FROM decimal_rf_probe p
+        INNER JOIN decimal_rf_build b
+        ON CAST(p.val AS DECIMALV3(38, 10)) = b.val
+    """
+
+    // Test 2: range join with <= comparison (matches crash stack trace)
+    order_qt_decimal_rf_le """
+        SELECT p.id, p.val, b.val as bval
+        FROM decimal_rf_probe p
+        INNER JOIN decimal_rf_build b
+        ON CAST(p.val AS DECIMALV3(38, 10)) <= b.val
+        WHERE b.id = 2
+    """
+
+    // Test 3: range join with >= comparison
+    order_qt_decimal_rf_ge """
+        SELECT p.id, p.val, b.val as bval
+        FROM decimal_rf_probe p
+        INNER JOIN decimal_rf_build b
+        ON CAST(p.val AS DECIMALV3(38, 10)) >= b.val
+        WHERE b.id = 1
+    """
+
+    // Test 4: implicit cast via different precision in join condition
+    // (no explicit CAST, but FE inserts one due to precision mismatch)
+    order_qt_decimal_rf_implicit_cast """
+        SELECT p.id, p.val, b.val as bval
+        FROM decimal_rf_probe p
+        INNER JOIN decimal_rf_build b
+        ON p.val = b.val
+    """
+
+    order_qt_decimal_rf_nullable_single_target """
+        SELECT n.id, b.id as bid, n.val, b.val_int_not_null
+        FROM decimal_rf_probe_nullable n
+        INNER JOIN decimal_rf_build b
+        ON round(n.val, 1) = b.val_int_not_null
+    """
+
+    // The top join's RF on p.val expands through the lower inner join to 
n.val.
+    // This creates grouped legacy RF targets and covers the same nullable cast
+    // calculation as the single-target path.
+    order_qt_decimal_rf_grouped_targets """
+        SELECT /*+ leading(p n b) */ p.id, n.id, b.id as bid, p.val, n.val
+        FROM decimal_rf_probe p
+        INNER JOIN decimal_rf_probe_nullable n ON p.val = n.val
+        INNER JOIN decimal_rf_build b ON p.val = b.val
+    """
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to