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]