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

zclllyybb 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 38fb05eaaab [fix](be) Normalize constant nullable TopN predicate 
results (#66442)
38fb05eaaab is described below

commit 38fb05eaaab72739b34c942503bb0000f0967dd5
Author: HappenLee <[email protected]>
AuthorDate: Thu Aug 6 21:35:07 2026 +0800

    [fix](be) Normalize constant nullable TopN predicate results (#66442)
    
    Problem Summary: Nullable TopN comparisons can return a `ColumnConst`
    that wraps a `ColumnNullable` result, especially when an input batch is
    entirely NULL. The previous normalization only handled a top-level
    `ColumnNullable`, so the constant nullable result could reach filter
    execution without converting SQL NULL into the NULLS FIRST or NULLS LAST
    filter decision.
    
    This change recursively normalizes constant wrappers, collapses nullable
    Boolean results according to the TopN null ordering, preserves the
    constant shape, and aligns the unit-test expression metadata with
    production nullability. The new unit test covers both NULLS FIRST and
    NULLS LAST through `execute_column` and `execute_filter`.
---
 be/src/exprs/vtopn_pred.h                       | 37 +++++++++++++--------
 be/test/format_v2/parquet/parquet_scan_test.cpp |  2 +-
 be/test/runtime/runtime_predicate_test.cpp      | 44 ++++++++++++++++++++++++-
 3 files changed, 68 insertions(+), 15 deletions(-)

diff --git a/be/src/exprs/vtopn_pred.h b/be/src/exprs/vtopn_pred.h
index c2fe7c2fd0d..af680955d05 100644
--- a/be/src/exprs/vtopn_pred.h
+++ b/be/src/exprs/vtopn_pred.h
@@ -121,19 +121,7 @@ public:
         RETURN_IF_ERROR(_function->execute(nullptr, temp_block, arguments,
                                            num_columns_without_result, 
temp_block.rows()));
         result_column = 
std::move(temp_block.get_by_position(num_columns_without_result).column);
-        if (auto mutable_result = IColumn::mutate(std::move(result_column));
-            auto* nullable = 
check_and_get_column<ColumnNullable>(*mutable_result)) {
-            auto& values = 
assert_cast<ColumnUInt8&>(*nullable->get_nested_column_ptr()).get_data();
-            const auto& null_map = nullable->get_null_map_data();
-            // Master validates execute_type() against the physical result 
column. Collapse SQL
-            // NULL to the filter decision here: NULLS FIRST keeps it, while 
NULLS LAST rejects it.
-            for (size_t row = 0; row < values.size(); ++row) {
-                values[row] = null_map[row] ? _predicate->nulls_first() : 
values[row];
-            }
-            result_column = nullable->get_nested_column_ptr();
-        } else {
-            result_column = std::move(mutable_result);
-        }
+        result_column = _normalize_filter_result(std::move(result_column));
         DCHECK_EQ(result_column->size(), count);
         return Status::OK();
     }
@@ -284,6 +272,29 @@ public:
     }
 
 private:
+    ColumnPtr _normalize_filter_result(ColumnPtr column) const {
+        const size_t rows = column->size();
+        if (const auto* constant = check_and_get_column<ColumnConst>(*column)) 
{
+            auto nested = 
_normalize_filter_result(constant->get_data_column_ptr());
+            return ColumnConst::create(std::move(nested), rows);
+        }
+
+        auto mutable_column = IColumn::mutate(std::move(column));
+        if (auto* nullable = 
check_and_get_column<ColumnNullable>(*mutable_column)) {
+            auto& values = 
assert_cast<ColumnUInt8&>(*nullable->get_nested_column_ptr()).get_data();
+            auto& null_map = nullable->get_null_map_data();
+            // Collapse SQL NULL to the filter decision: NULLS FIRST keeps it, 
while NULLS LAST
+            // rejects it. Preserve the nullable wrapper so strict block 
execution still matches
+            // the expression's declared Nullable(Boolean) type.
+            for (size_t row = 0; row < values.size(); ++row) {
+                values[row] = null_map[row] ? _predicate->nulls_first() : 
values[row];
+            }
+            nullable->fill_false_to_nullmap(rows);
+            return mutable_column;
+        }
+        return mutable_column;
+    }
+
     int _source_node_id;
     std::string _expr_name;
     RuntimePredicate* _predicate = nullptr;
diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp 
b/be/test/format_v2/parquet/parquet_scan_test.cpp
index cc95b51cebf..eaca8f772c8 100644
--- a/be/test/format_v2/parquet/parquet_scan_test.cpp
+++ b/be/test/format_v2/parquet/parquet_scan_test.cpp
@@ -931,7 +931,7 @@ PreparedTopNConjunct create_topn_conjunct(RuntimeState* 
state, int column_id,
 
     TExprNode topn_node;
     topn_node.__set_type(std::make_shared<DataTypeUInt8>()->to_thrift());
-    topn_node.__set_is_nullable(false);
+    topn_node.__set_is_nullable(data_type->is_nullable());
     auto topn = VTopNPred::create_shared(topn_node, 10, nullptr);
     topn->add_child(VSlotRef::create_shared(column_id, column_id, -1, 
data_type, "topn_column"));
     auto conjunct = VExprContext::create_shared(std::move(topn));
diff --git a/be/test/runtime/runtime_predicate_test.cpp 
b/be/test/runtime/runtime_predicate_test.cpp
index c6298b91a5b..baf27e72ddc 100644
--- a/be/test/runtime/runtime_predicate_test.cpp
+++ b/be/test/runtime/runtime_predicate_test.cpp
@@ -76,7 +76,7 @@ VExprContextSPtr create_prepared_topn_expr(MockRuntimeState* 
state, const DataTy
 
     TExprNode node;
     node.__set_type(create_type_desc(PrimitiveType::TYPE_BOOLEAN));
-    node.__set_is_nullable(false);
+    node.__set_is_nullable(data_type->is_nullable());
     auto expr = VTopNPred::create_shared(node, SOURCE_NODE_ID, nullptr);
     expr->add_child(VSlotRef::create_shared(SLOT_ID, 0, -1, data_type, 
"topn_column"));
     auto context = VExprContext::create_shared(std::move(expr));
@@ -306,4 +306,46 @@ TEST(RuntimePredicateTest, 
NullableNullsFirstTopNSupportsRawNullSemantics) {
     context->close();
 }
 
+TEST(RuntimePredicateTest, 
TopNPredicateNormalizesConstNullableComparisonResult) {
+    constexpr size_t rows = 3;
+    const auto type = make_nullable(std::make_shared<DataTypeInt32>());
+
+    for (const bool nulls_first : {false, true}) {
+        SCOPED_TRACE(nulls_first ? "NULLS FIRST" : "NULLS LAST");
+        MockRuntimeState state;
+        auto context = create_prepared_topn_expr(&state, type, 
Field::create_field<TYPE_INT>(3),
+                                                 true, nulls_first);
+
+        Block block;
+        block.insert(
+                {ColumnNullable::create(ColumnInt32::create(rows, 0), 
ColumnUInt8::create(rows, 1)),
+                 type, "topn_column"});
+
+        ColumnPtr result_column;
+        auto status = context->root()->execute_column(context.get(), &block, 
nullptr, block.rows(),
+                                                      result_column);
+        ASSERT_TRUE(status.ok()) << status;
+        ASSERT_TRUE(is_column_const(*result_column));
+        EXPECT_FALSE(result_column->is_null_at(0));
+        EXPECT_EQ(result_column->get_bool(0), nulls_first);
+
+        int result_column_id = -1;
+        status = context->execute(&block, &result_column_id);
+        ASSERT_TRUE(status.ok()) << status;
+        const auto& strict_result = block.get_by_position(result_column_id);
+        ASSERT_TRUE(is_column_const(*strict_result.column));
+        EXPECT_NE(nullptr, 
check_and_get_column_with_const<ColumnNullable>(*strict_result.column));
+        EXPECT_FALSE(strict_result.column->is_null_at(0));
+        EXPECT_EQ(strict_result.column->get_bool(0), nulls_first);
+
+        IColumn::Filter filter(rows, 1);
+        bool can_filter_all = false;
+        status = context->execute_filter(&block, filter.data(), rows, false, 
&can_filter_all);
+        ASSERT_TRUE(status.ok()) << status;
+        EXPECT_EQ(filter, IColumn::Filter(rows, nulls_first));
+        EXPECT_EQ(can_filter_all, !nulls_first);
+        context->close();
+    }
+}
+
 } // namespace doris


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

Reply via email to