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

HappenLee 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 231b5345bab [fix](be) Skip condition cache for complex object literals 
(#68930)
231b5345bab is described below

commit 231b5345bab954fa9d937ab3b8dda5b34ac454cb
Author: HappenLee <[email protected]>
AuthorDate: Thu Oct 15 11:03:07 2026 +0800

    [fix](be) Skip condition cache for complex object literals (#68930)
    
    Problem Summary: With the condition cache enabled, valid scan predicates
    containing unsupported literal columns can fail while preparing their
    digest. For example, `bitmap_count(IF(id = 1, to_bitmap(1), CAST(NULL AS
    BITMAP))) > 0` reaches a column xxHash method that is not implemented.
    Checking only the top-level SQL type also misses VARBINARY, nested
    containers, and AGG_STATE values serialized as fixed-length object
    columns.
    
    Inspect the literal's actual column and recursively visit its subcolumns
    using the existing read-only traversal interface. Return the existing
    unsupported-digest sentinel (0) for bitmap, HLL, quantile state,
    VARBINARY, and fixed-length object columns, including through constant,
    nullable, array, map, and struct wrappers. Parent expressions propagate
    zero and the affected scan evaluates its predicates without the
    condition cache. Ordinary scalar/container literals and AGG_STATE values
    serialized as hashable strings retain their existing digest behavior.
    
    Keep the general column hashing contract unchanged: it also serves
    aggregation and does not define zero as an unsupported value. The extra
    check walks the literal's column structure rather than table rows.
    
    ### Release note
    
    Fix valid queries containing unsupported object, binary,
    aggregate-state, or nested literals in scan predicates failing when the
    condition cache is enabled. Such scans now bypass the condition cache.
    
    ### Check List (For Author)
    
    - Test:
    - Unit Test: Add typed NULL and non-NULL object/VARBINARY literals,
    parent digest propagation, nested Array/Map/Struct coverage, supported
    and unsupported AGG_STATE serialization, and scalar/container digest
    controls.
    - Regression test: Compare six ordered queries with condition cache
    disabled and across repeated enabled execution and cache transitions.
    Include IF/CASE, VARBINARY, folded object arrays with row-dependent
    indexing, and supported array/projection controls.
    - Local validation: clang-format 16, build hygiene, git diff --check,
    and Groovy syntax compilation passed.
    - clang-tidy passed the repository's changed-line check using the
    generated UT compilation flags with the missing PCH removed and the
    toolchain resource directory supplied. The production file also has no
    full-file diagnostics. Full-file test analysis reports an existing
    constructor virtual-call diagnostic in unchanged
    data_type_agg_state.h:66.
    - BE UT execution was attempted with run-be-ut.sh -j48 --run
    --filter='VLiteralDigestTest.*', but Ninja stopped on the missing local
    third-party archive libsimdutf.a. The SQL regression was not run locally
    because this worktree has no runnable build of the change. No runtime
    pass is claimed; CI validation is required before merging.
    - Behavior changed: Yes. Unsupported literal digests disable the
    affected scan's condition cache instead of failing the query.
    - Does this need documentation: No.
---
 be/src/exprs/vliteral.cpp                          |  28 ++++
 be/test/exprs/vliteral_digest_test.cpp             | 159 +++++++++++++++++++++
 .../test_condition_cache_complex_literal.groovy    |  74 ++++++++++
 3 files changed, 261 insertions(+)

diff --git a/be/src/exprs/vliteral.cpp b/be/src/exprs/vliteral.cpp
index 9b93d709727..ee8f7e7641e 100644
--- a/be/src/exprs/vliteral.cpp
+++ b/be/src/exprs/vliteral.cpp
@@ -28,6 +28,9 @@
 
 #include "core/block/block.h"
 #include "core/column/column.h"
+#include "core/column/column_complex.h"
+#include "core/column/column_fixed_length_object.h"
+#include "core/column/column_varbinary.h"
 #include "core/data_type/data_type_decimal.h"
 #include "core/field.h"
 #include "core/types.h"
@@ -35,6 +38,26 @@
 
 namespace doris {
 
+namespace {
+
+bool supports_literal_digest(const IColumn& column) {
+    // These concrete columns inherit IColumn's throwing xxHash implementation.
+    // Inspect the column rather than the SQL type: AGG_STATE can use either a
+    // hashable string column or an unsupported fixed-length object column.
+    if (is_column<ColumnBitmap>(column) || is_column<ColumnHLL>(column) ||
+        is_column<ColumnQuantileState>(column) || 
is_column<ColumnVarbinary>(column) ||
+        is_column<ColumnFixedLengthObject>(column)) {
+        return false;
+    }
+
+    bool supported = true;
+    column.for_each_subcolumn(
+            [&](const IColumn& child) { supported = supported && 
supports_literal_digest(child); });
+    return supported;
+}
+
+} // namespace
+
 class VExprContext;
 
 Status VLiteral::prepare(RuntimeState* state, const RowDescriptor& desc, 
VExprContext* context) {
@@ -100,6 +123,11 @@ bool VLiteral::equals(const VExpr& other) {
 }
 
 uint64_t VLiteral::get_digest(uint64_t seed) const {
+    // Wrappers and containers delegate hashing to their nested columns. A zero
+    // digest disables the condition cache without changing the column hash 
contract.
+    if (!supports_literal_digest(*_column_ptr)) {
+        return 0;
+    }
     _column_ptr->update_xxHash_with_value(0, 1, seed, nullptr);
     return seed;
 }
diff --git a/be/test/exprs/vliteral_digest_test.cpp 
b/be/test/exprs/vliteral_digest_test.cpp
new file mode 100644
index 00000000000..d520cbf23aa
--- /dev/null
+++ b/be/test/exprs/vliteral_digest_test.cpp
@@ -0,0 +1,159 @@
+// 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.
+
+#include <gtest/gtest.h>
+
+#include <utility>
+#include <vector>
+
+#include "agent/be_exec_version_manager.h"
+#include "core/data_type/data_type_agg_state.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_struct.h"
+#include "exprs/vliteral.h"
+#include "testutil/mock/mock_fn_call.h"
+
+namespace doris {
+
+TEST(VLiteralDigestTest, ComplexNullDisablesParentDigest) {
+    for (auto type : {TYPE_BITMAP, TYPE_HLL, TYPE_QUANTILE_STATE, 
TYPE_VARBINARY}) {
+        SCOPED_TRACE(type_to_string(type));
+        auto data_type = DataTypeFactory::instance().create_data_type(type, 
true);
+        auto literal = VLiteral::create_shared(data_type, Field());
+        EXPECT_EQ(literal->get_digest(42), 0);
+
+        // A later scalar child must not revive a digest disabled by the 
complex literal.
+        auto parent = MockFnCall::create("if");
+        parent->add_child(literal);
+        
parent->add_child(VLiteral::create_shared(std::make_shared<DataTypeInt32>(),
+                                                  
Field::create_field<TYPE_INT>(1)));
+        EXPECT_EQ(parent->get_digest(42), 0);
+    }
+}
+
+TEST(VLiteralDigestTest, NonNullComplexValuesDisableDigest) {
+    auto bitmap = VLiteral::create_shared(
+            DataTypeFactory::instance().create_data_type(TYPE_BITMAP, false),
+            Field::create_field<TYPE_BITMAP>(BitmapValue(1)));
+    auto hll =
+            
VLiteral::create_shared(DataTypeFactory::instance().create_data_type(TYPE_HLL, 
false),
+                                    
Field::create_field<TYPE_HLL>(HyperLogLog()));
+    auto quantile = VLiteral::create_shared(
+            DataTypeFactory::instance().create_data_type(TYPE_QUANTILE_STATE, 
false),
+            Field::create_field<TYPE_QUANTILE_STATE>(QuantileState()));
+    EXPECT_EQ(bitmap->get_digest(42), 0);
+    EXPECT_EQ(hll->get_digest(42), 0);
+    EXPECT_EQ(quantile->get_digest(42), 0);
+
+    auto binary = VLiteral::create_shared(
+            DataTypeFactory::instance().create_data_type(TYPE_VARBINARY, 
false),
+            Field::create_field<TYPE_VARBINARY>(StringView("a", 1)));
+    EXPECT_EQ(binary->get_digest(42), 0);
+}
+
+TEST(VLiteralDigestTest, NestedUnsupportedColumnsDisableDigest) {
+    auto bitmap_type = 
DataTypeFactory::instance().create_data_type(TYPE_BITMAP, true);
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_type = std::make_shared<DataTypeArray>(bitmap_type);
+    auto array = Field::create_field<TYPE_ARRAY>(
+            Array {Field::create_field<TYPE_BITMAP>(BitmapValue(1)), Field()});
+    auto nested_array_type = std::make_shared<DataTypeArray>(array_type);
+    auto nested_array = Field::create_field<TYPE_ARRAY>(Array {array});
+    auto map_type = std::make_shared<DataTypeMap>(int_type, array_type);
+    auto map = Field::create_field<TYPE_MAP>(
+            Map {Field::create_field<TYPE_ARRAY>(Array 
{Field::create_field<TYPE_INT>(1)}),
+                 Field::create_field<TYPE_ARRAY>(Array {array})});
+    auto struct_type = std::make_shared<DataTypeStruct>(DataTypes {int_type, 
map_type});
+    auto structure =
+            Field::create_field<TYPE_STRUCT>(Struct 
{Field::create_field<TYPE_INT>(1), map});
+
+    for (const auto& [type, value] :
+         std::vector<std::pair<DataTypePtr, Field>> {{array_type, array},
+                                                     {nested_array_type, 
nested_array},
+                                                     {map_type, map},
+                                                     {struct_type, structure},
+                                                     
{make_nullable(array_type), Field()}}) {
+        SCOPED_TRACE(type->get_name());
+        auto literal = VLiteral::create_shared(type, value);
+        EXPECT_EQ(literal->get_digest(42), 0);
+    }
+}
+
+TEST(VLiteralDigestTest, AggStateDigestUsesSerializedColumn) {
+    const auto version = BeExecVersionManager::get_newest_version();
+    auto sum_type = std::make_shared<DataTypeAggState>(
+            DataTypes {std::make_shared<DataTypeInt32>()}, false, "sum", 
version);
+    auto sum_null = VLiteral::create_shared(make_nullable(sum_type), Field());
+    EXPECT_EQ(sum_null->get_digest(42), 0);
+
+    // HLL aggregate states serialize as ColumnString, unlike direct HLL 
literals.
+    auto hll_type = DataTypeFactory::instance().create_data_type(TYPE_HLL, 
false);
+    auto hll_state_type =
+            std::make_shared<DataTypeAggState>(DataTypes {hll_type}, false, 
"hll_union", version);
+    auto hll_state_null = 
VLiteral::create_shared(make_nullable(hll_state_type), Field());
+    EXPECT_NE(hll_state_null->get_digest(42), 0);
+    EXPECT_EQ(hll_state_null->get_digest(42), hll_state_null->get_digest(42));
+}
+
+TEST(VLiteralDigestTest, SupportedContainersKeepDistinctDigests) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto array_type = std::make_shared<DataTypeArray>(int_type);
+    auto one = Field::create_field<TYPE_INT>(1);
+    auto two = Field::create_field<TYPE_INT>(2);
+    auto first = VLiteral::create_shared(array_type, 
Field::create_field<TYPE_ARRAY>(Array {one}));
+    auto second = VLiteral::create_shared(array_type, 
Field::create_field<TYPE_ARRAY>(Array {two}));
+    EXPECT_NE(first->get_digest(42), 0);
+    EXPECT_NE(first->get_digest(42), second->get_digest(42));
+    EXPECT_EQ(first->get_digest(42), first->get_digest(42));
+
+    auto map_type = std::make_shared<DataTypeMap>(int_type, int_type);
+    auto map = Field::create_field<TYPE_MAP>(Map 
{Field::create_field<TYPE_ARRAY>(Array {one}),
+                                                  
Field::create_field<TYPE_ARRAY>(Array {two})});
+    auto map_literal = VLiteral::create_shared(map_type, map);
+    EXPECT_NE(map_literal->get_digest(42), 0);
+    auto struct_type = std::make_shared<DataTypeStruct>(DataTypes {int_type, 
map_type});
+    auto struct_literal = VLiteral::create_shared(
+            struct_type, Field::create_field<TYPE_STRUCT>(Struct {one, map}));
+    EXPECT_NE(struct_literal->get_digest(42), 0);
+}
+
+TEST(VLiteralDigestTest, ScalarValuesKeepDistinctStableDigests) {
+    auto int_type = std::make_shared<DataTypeInt32>();
+    auto one = VLiteral::create_shared(int_type, 
Field::create_field<TYPE_INT>(1));
+    auto two = VLiteral::create_shared(int_type, 
Field::create_field<TYPE_INT>(2));
+    EXPECT_NE(one->get_digest(42), 0);
+    EXPECT_EQ(one->get_digest(42), one->get_digest(42));
+    EXPECT_NE(one->get_digest(42), two->get_digest(42));
+    EXPECT_NE(one->get_digest(42), one->get_digest(43));
+
+    auto string_type = std::make_shared<DataTypeString>();
+    auto a = VLiteral::create_shared(string_type, 
Field::create_field<TYPE_STRING>(String("a")));
+    auto b = VLiteral::create_shared(string_type, 
Field::create_field<TYPE_STRING>(String("b")));
+    EXPECT_NE(a->get_digest(42), 0);
+    EXPECT_NE(a->get_digest(42), b->get_digest(42));
+
+    auto scalar_null = VLiteral::create_shared(
+            DataTypeFactory::instance().create_data_type(TYPE_INT, true), 
Field());
+    EXPECT_NE(scalar_null->get_digest(42), 0);
+}
+
+} // namespace doris
diff --git 
a/regression-test/suites/query_p0/condition_cache/test_condition_cache_complex_literal.groovy
 
b/regression-test/suites/query_p0/condition_cache/test_condition_cache_complex_literal.groovy
new file mode 100644
index 00000000000..269adb82cc0
--- /dev/null
+++ 
b/regression-test/suites/query_p0/condition_cache/test_condition_cache_complex_literal.groovy
@@ -0,0 +1,74 @@
+// 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_condition_cache_complex_literal") {
+    sql "set enable_sql_cache=false"
+    sql "drop table if exists condition_cache_complex_literal"
+    sql """
+        create table condition_cache_complex_literal (id int)
+        duplicate key(id)
+        distributed by hash(id) buckets 1
+        properties("replication_num"="1")
+    """
+    sql "insert into condition_cache_complex_literal values (1), (2)"
+
+    def queries = [
+        """
+            select id from condition_cache_complex_literal
+            where bitmap_count(if(id=1, to_bitmap(1), cast(null as bitmap))) > 0
+            order by id
+        """,
+        """
+            select id from condition_cache_complex_literal
+            where bitmap_count(case when id=1 then to_bitmap(1)
+                              else cast(null as bitmap) end) > 0
+            order by id
+        """,
+        """
+            select id from condition_cache_complex_literal
+            where md5sum(X'61', to_binary(cast(id as string))) <> ''
+            order by id
+        """,
+        // FE folds this array into a literal, while the row-dependent index
+        // keeps it in the scan predicate. Even its NULL bitmap element lacks 
xxHash.
+        """
+            select id from condition_cache_complex_literal
+            where bitmap_count(element_at(array(cast(null as bitmap)), id)) = 0
+            order by id
+        """,
+        """
+            select id from condition_cache_complex_literal
+            where element_at(array(1, 2), id) = id
+            order by id
+        """,
+        """
+            select id, bitmap_count(if(id=1, to_bitmap(1), cast(null as 
bitmap)))
+            from condition_cache_complex_literal order by id
+        """
+    ]
+
+    // Compare each query with uncached execution to verify that cache 
transitions
+    // and repeated enabled execution preserve the row results.
+    sql "set enable_condition_cache=false"
+    def uncachedResults = queries.collect { query -> sql(query) }
+    for (boolean enabled : [true, true, false, true]) {
+        sql "set enable_condition_cache=${enabled}"
+        queries.eachWithIndex { query, index ->
+            assertEquals(uncachedResults[index], sql(query))
+        }
+    }
+}


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

Reply via email to