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

mrhhsg 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 58f8cd61fec [fix](be) Preserve DATE type in min/max runtime filters 
(#70222)
58f8cd61fec is described below

commit 58f8cd61fec797084489f270319ba2b88203e74a
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Nov 5 17:15:48 2026 +0800

    [fix](be) Preserve DATE type in min/max runtime filters (#70222)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: #69963
    
    Problem Summary: When a DATE min/max runtime filter is received through
    the protobuf path, its bounds are decoded with the DATETIME target
    because `TYPE_DATE` and `TYPE_DATETIME` share one branch. This changes
    the DATE bounds' internal type tags to DATETIME. The consumer then
    creates DATETIME literals for DATE scan predicates, which can abort the
    BE on a Field type mismatch.
    
    Decode `TYPE_DATE` bounds with the DATE target in the shared min/max
    receive path. Keep the existing `TYPE_DATETIME` decoding unchanged. The
    wire format and other runtime-filter types are unchanged.
    
    ### Release note
    
    Fix a BE crash when receiving DATEV1 min/max runtime filters.
    
    ### Check List (For Author)
    
    - Test:
    - ASAN BE+FE build (`BUILD_TYPE=ASAN ./build.sh --be --fe -j 40`):
    passed, including FE checkstyle.
    - ASAN `RuntimeFilterWrapperTest.*`: 15 cases passed, including
    DATE/DATETIME type, min/max mode, and merge/publish request coverage.
    - `test_datev1_minmax_runtime_filter`: generated expected output and
    passed a separate verification run; `test_datev1_common`: passed.
    - C++ formatting, build hygiene, and changed-line clang-tidy checks:
    passed.
    - SQL regression ran on a single-BE cluster; cross-BE and mixed-version
    execution were not exercised.
    - Behavior changed: Yes. Received DATE min/max bounds retain the DATE
    type instead of being converted to DATETIME.
    - Does this need documentation: No
---
 .../exec/runtime_filter/runtime_filter_wrapper.cpp |  16 ++-
 .../runtime_filter/runtime_filter_wrapper_test.cpp | 128 +++++++++++++++++++++
 .../datev1/test_datev1_minmax_runtime_filter.out   |   8 ++
 .../test_datev1_minmax_runtime_filter.groovy       |  82 +++++++++++++
 4 files changed, 232 insertions(+), 2 deletions(-)

diff --git a/be/src/exec/runtime_filter/runtime_filter_wrapper.cpp 
b/be/src/exec/runtime_filter/runtime_filter_wrapper.cpp
index 66fa692b65a..9125b250c32 100644
--- a/be/src/exec/runtime_filter/runtime_filter_wrapper.cpp
+++ b/be/src/exec/runtime_filter/runtime_filter_wrapper.cpp
@@ -561,8 +561,7 @@ Status RuntimeFilterWrapper::_assign(const PMinMaxFilter& 
minmax_filter, bool co
         int64_t max_val = minmax_filter.max_val().longval();
         return _minmax_func->assign(&min_val, &max_val);
     }
-    case TYPE_DATETIME:
-    case TYPE_DATE: {
+    case TYPE_DATETIME: {
         const auto& min_val_ref = minmax_filter.min_val().stringval();
         const auto& max_val_ref = minmax_filter.max_val().stringval();
         VecDateTimeValue min_val;
@@ -574,6 +573,19 @@ Status RuntimeFilterWrapper::_assign(const PMinMaxFilter& 
minmax_filter, bool co
                 {max_val_ref.c_str(), max_val_ref.length()}, max_val, nullptr, 
params);
         return _minmax_func->assign(&min_val, &max_val);
     }
+    case TYPE_DATE: {
+        const auto& min_val_ref = minmax_filter.min_val().stringval();
+        const auto& max_val_ref = minmax_filter.max_val().stringval();
+        VecDateTimeValue min_val;
+        VecDateTimeValue max_val;
+        CastParameters params;
+        // Preserve the DATE tag so the consumer creates DATE literals for 
DATE scan predicates.
+        
CastToDateOrDatetime::from_string_non_strict_mode<DatelikeTargetType::DATE>(
+                {min_val_ref.c_str(), min_val_ref.length()}, min_val, nullptr, 
params);
+        
CastToDateOrDatetime::from_string_non_strict_mode<DatelikeTargetType::DATE>(
+                {max_val_ref.c_str(), max_val_ref.length()}, max_val, nullptr, 
params);
+        return _minmax_func->assign(&min_val, &max_val);
+    }
     case TYPE_DECIMALV2: {
         const auto& min_val_ref = minmax_filter.min_val().stringval();
         const auto& max_val_ref = minmax_filter.max_val().stringval();
diff --git a/be/test/exec/runtime_filter/runtime_filter_wrapper_test.cpp 
b/be/test/exec/runtime_filter/runtime_filter_wrapper_test.cpp
index f31fcc68d67..fef8ed15ea9 100644
--- a/be/test/exec/runtime_filter/runtime_filter_wrapper_test.cpp
+++ b/be/test/exec/runtime_filter/runtime_filter_wrapper_test.cpp
@@ -20,12 +20,21 @@
 #include <glog/logging.h>
 #include <gtest/gtest.h>
 
+#include <string>
+#include <utility>
+
 #include "core/data_type/data_type_date.h"
+#include "core/data_type/data_type_date_time.h"
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_string.h"
+#include "core/field.h"
+#include "core/value/vdatetime_value.h"
+#include "exec/runtime_filter/utils.h"
 #include "exprs/bloom_filter_func.h"
 #include "exprs/hybrid_set.h"
 #include "exprs/minmax_predicate.h"
+#include "exprs/vexpr.h"
+#include "exprs/vliteral.h"
 #include "testutil/column_helper.h"
 #include "util/raw_value.h"
 
@@ -361,6 +370,125 @@ TEST_F(RuntimeFilterWrapperTest, TestMinMaxAssign) {
 #undef APPLY_FOR_PRIMITIVE_BASE_TYPE
 }
 
+TEST_F(RuntimeFilterWrapperTest, DateMinMaxAssignPreservesDateType) {
+    RuntimeFilterParams params {.filter_id = 0,
+                                .filter_type = 
RuntimeFilterType::MINMAX_FILTER,
+                                .column_return_type = TYPE_DATE,
+                                .null_aware = false};
+    PMergeFilterRequest request;
+    request.set_filter_type(PFilterType::MINMAX_FILTER);
+    
request.mutable_minmax_filter()->mutable_min_val()->set_stringval("2012-01-02");
+    
request.mutable_minmax_filter()->mutable_max_val()->set_stringval("2012-03-02");
+
+    RuntimeFilterWrapper wrapper(&params);
+    ASSERT_TRUE(wrapper.assign(request, nullptr).ok());
+    
EXPECT_EQ(static_cast<VecDateTimeValue*>(wrapper.minmax_func()->get_min())->type(),
 TIME_DATE);
+    
EXPECT_EQ(static_cast<VecDateTimeValue*>(wrapper.minmax_func()->get_max())->type(),
 TIME_DATE);
+}
+
+static void check_date_like_minmax_bound(const void* bound, TimeType time_type,
+                                         PrimitiveType column_type, const 
std::string& text,
+                                         const DataTypePtr& data_type) {
+    const auto* value = static_cast<const VecDateTimeValue*>(bound);
+    EXPECT_EQ(value->type(), time_type);
+    char buffer[30];
+    value->to_string(buffer);
+    EXPECT_EQ(std::string(buffer), text);
+
+    VExprSPtr literal;
+    ASSERT_TRUE(create_literal(data_type, value, literal).ok());
+    ASSERT_TRUE(literal->is_literal());
+    EXPECT_EQ(literal->result_type(), column_type);
+    Field field;
+    static_cast<VLiteral*>(literal.get())->get_column_ptr()->get(0, field);
+    EXPECT_EQ(field.get_type(), column_type);
+}
+
+template <typename Request>
+static void check_date_like_minmax_request(RuntimeFilterWrapper& producer,
+                                           const RuntimeFilterParams& params, 
TimeType time_type,
+                                           const std::string& min_text, const 
std::string& max_text,
+                                           const DataTypePtr& data_type) {
+    Request request;
+    request.set_filter_type(get_type(params.filter_type));
+    ASSERT_TRUE(producer.to_protobuf(request.mutable_minmax_filter()).ok());
+    RuntimeFilterWrapper receiver(&params);
+    ASSERT_TRUE(receiver.assign(request, nullptr).ok());
+    EXPECT_EQ(receiver.get_state(), RuntimeFilterWrapper::State::READY);
+    EXPECT_EQ(receiver.column_type(), params.column_return_type);
+    // MIN_FILTER and MAX_FILTER serialize only their owned bound, so the 
other bound is absent.
+    if (params.filter_type != RuntimeFilterType::MAX_FILTER) {
+        check_date_like_minmax_bound(receiver.minmax_func()->get_min(), 
time_type,
+                                     params.column_return_type, min_text, 
data_type);
+    }
+    if (params.filter_type != RuntimeFilterType::MIN_FILTER) {
+        check_date_like_minmax_bound(receiver.minmax_func()->get_max(), 
time_type,
+                                     params.column_return_type, max_text, 
data_type);
+    }
+}
+
+static void check_date_like_minmax_round_trip(
+        RuntimeFilterType filter_type, PrimitiveType column_type, TimeType 
time_type,
+        VecDateTimeValue min_value, VecDateTimeValue max_value, const 
std::string& min_text,
+        const std::string& max_text, const DataTypePtr& data_type) {
+    RuntimeFilterParams params {.filter_id = 0,
+                                .filter_type = filter_type,
+                                .column_return_type = column_type,
+                                .null_aware = false};
+    RuntimeFilterWrapper producer(&params);
+    ASSERT_TRUE(producer.minmax_func()->assign(&min_value, &max_value).ok());
+    check_date_like_minmax_request<PMergeFilterRequest>(producer, params, 
time_type, min_text,
+                                                        max_text, data_type);
+    check_date_like_minmax_request<PPublishFilterRequestV2>(producer, params, 
time_type, min_text,
+                                                            max_text, 
data_type);
+}
+
+TEST_F(RuntimeFilterWrapperTest, DateLikeMinMaxRoundTripPreservesType) {
+    for (const auto filter_type : {RuntimeFilterType::MIN_FILTER, 
RuntimeFilterType::MAX_FILTER,
+                                   RuntimeFilterType::MINMAX_FILTER}) {
+        check_date_like_minmax_round_trip(filter_type, TYPE_DATE, TIME_DATE,
+                                          VecDateTimeValue(0, TIME_DATE, 0, 0, 
0, 2012, 1, 2),
+                                          VecDateTimeValue(0, TIME_DATE, 0, 0, 
0, 2012, 3, 2),
+                                          "2012-01-02", "2012-03-02",
+                                          std::make_shared<DataTypeDate>());
+        check_date_like_minmax_round_trip(filter_type, TYPE_DATETIME, 
TIME_DATETIME,
+                                          VecDateTimeValue(0, TIME_DATETIME, 
1, 2, 3, 2012, 1, 2),
+                                          VecDateTimeValue(0, TIME_DATETIME, 
4, 5, 6, 2012, 3, 2),
+                                          "2012-01-02 01:02:03", "2012-03-02 
04:05:06",
+                                          
std::make_shared<DataTypeDateTime>());
+    }
+}
+
+TEST_F(RuntimeFilterWrapperTest, DateMinMaxAssignAcceptsMidnightDateTimeText) {
+    RuntimeFilterParams params {.filter_id = 0,
+                                .filter_type = 
RuntimeFilterType::MINMAX_FILTER,
+                                .column_return_type = TYPE_DATE,
+                                .null_aware = false};
+    PMergeFilterRequest request;
+    request.set_filter_type(PFilterType::MINMAX_FILTER);
+    
request.mutable_minmax_filter()->mutable_min_val()->set_stringval("2012-01-02 
00:00:00");
+    
request.mutable_minmax_filter()->mutable_max_val()->set_stringval("2012-03-02 
00:00:00");
+
+    RuntimeFilterWrapper wrapper(&params);
+    ASSERT_TRUE(wrapper.assign(request, nullptr).ok());
+    auto data_type = std::make_shared<DataTypeDate>();
+    for (const auto& [bound, expected] :
+         {std::pair {wrapper.minmax_func()->get_min(), "2012-01-02"},
+          std::pair {wrapper.minmax_func()->get_max(), "2012-03-02"}}) {
+        const auto* value = static_cast<const VecDateTimeValue*>(bound);
+        EXPECT_EQ(value->type(), TIME_DATE);
+        char buffer[30];
+        value->to_string(buffer);
+        EXPECT_EQ(std::string(buffer), expected);
+        VExprSPtr literal;
+        ASSERT_TRUE(create_literal(data_type, value, literal).ok());
+        EXPECT_EQ(literal->result_type(), TYPE_DATE);
+        Field field;
+        static_cast<VLiteral*>(literal.get())->get_column_ptr()->get(0, field);
+        EXPECT_EQ(field.get_type(), TYPE_DATE);
+    }
+}
+
 TEST_F(RuntimeFilterWrapperTest, TestBloom) {
     std::vector<int> data_vector(10);
     std::iota(data_vector.begin(), data_vector.end(), 0);
diff --git 
a/regression-test/data/datatype_p0/datev1/test_datev1_minmax_runtime_filter.out 
b/regression-test/data/datatype_p0/datev1/test_datev1_minmax_runtime_filter.out
new file mode 100644
index 00000000000..696ea959edf
--- /dev/null
+++ 
b/regression-test/data/datatype_p0/datev1/test_datev1_minmax_runtime_filter.out
@@ -0,0 +1,8 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !with_minmax --
+2      2012-02-02      2       2012-02-02
+3      2012-03-02      3       2012-03-02
+
+-- !without_runtime_filter --
+2      2012-02-02      2       2012-02-02
+3      2012-03-02      3       2012-03-02
diff --git 
a/regression-test/suites/datatype_p0/datev1/test_datev1_minmax_runtime_filter.groovy
 
b/regression-test/suites/datatype_p0/datev1/test_datev1_minmax_runtime_filter.groovy
new file mode 100644
index 00000000000..5ae4e0767ba
--- /dev/null
+++ 
b/regression-test/suites/datatype_p0/datev1/test_datev1_minmax_runtime_filter.groovy
@@ -0,0 +1,82 @@
+// 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.
+
+suite("test_datev1_minmax_runtime_filter", "nonConcurrent") {
+    def disableDatev1 = sql("ADMIN SHOW FRONTEND CONFIG LIKE 
'disable_datev1'")[0][1]
+    def enableDateConversion = sql("ADMIN SHOW FRONTEND CONFIG LIKE 
'enable_date_conversion'")[0][1]
+    def sessionVars = [
+            "runtime_filter_type", "runtime_filter_mode", 
"runtime_filter_wait_infinitely",
+            "enable_runtime_filter_prune", "disable_join_reorder", 
"parallel_pipeline_task_num"
+    ]
+    def previous = sessionVars.collectEntries { name -> [(name): sql("SELECT 
@@${name}")[0][0]] }
+
+    try {
+        sql 'ADMIN SET FRONTEND CONFIG ("disable_datev1" = "false")'
+        sql 'ADMIN SET FRONTEND CONFIG ("enable_date_conversion" = "false")'
+
+        sql "DROP TABLE IF EXISTS test_datev1_minmax_runtime_filter"
+        sql """
+            CREATE TABLE test_datev1_minmax_runtime_filter (
+                id INT,
+                d DATEV1 NULL
+            ) DUPLICATE KEY(id)
+            DISTRIBUTED BY HASH(id) BUCKETS 2
+            PROPERTIES ("replication_num" = "1")
+        """
+        sql """
+            INSERT INTO test_datev1_minmax_runtime_filter VALUES
+                (1, '2012-01-02'), (2, '2012-02-02'), (3, '2012-03-02'),
+                (4, '2013-01-01'), (5, NULL)
+        """
+
+        sql "SET runtime_filter_type = 4"
+        sql "SET runtime_filter_mode = 'GLOBAL'"
+        sql "SET runtime_filter_wait_infinitely = true"
+        sql "SET enable_runtime_filter_prune = false"
+        sql "SET disable_join_reorder = true"
+        sql "SET parallel_pipeline_task_num = 2"
+
+        def query = """
+            SELECT a.id, a.d, b.id, b.d
+            FROM test_datev1_minmax_runtime_filter a
+            INNER JOIN [shuffle] test_datev1_minmax_runtime_filter b ON a.d = 
b.d
+            WHERE b.id IN (2, 3)
+            ORDER BY a.id, a.d, b.id, b.d
+        """
+        explain {
+            sql query
+            contains "HASH JOIN"
+            contains "PARTITIONED"
+            contains "VEXCHANGE"
+            contains "[min_max] -> d"
+        }
+        qt_with_minmax query
+
+        sql "SET runtime_filter_type = 0"
+        qt_without_runtime_filter query
+    } finally {
+        previous.each { name, value ->
+            if (name == "runtime_filter_type" || name == 
"runtime_filter_mode") {
+                sql "SET ${name} = '${value}'"
+            } else {
+                sql "SET ${name} = ${value}"
+            }
+        }
+        sql "ADMIN SET FRONTEND CONFIG (\"enable_date_conversion\" = 
\"${enableDateConversion}\")"
+        sql "ADMIN SET FRONTEND CONFIG (\"disable_datev1\" = 
\"${disableDatev1}\")"
+    }
+}


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

Reply via email to