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

morningman 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 708531d1414 [fix](be) Support a VARBINARY result in CASE WHEN (#69568)
708531d1414 is described below

commit 708531d14147a91d0280223fa87c964871ab8553
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Oct 26 09:45:25 2026 +0000

    [fix](be) Support a VARBINARY result in CASE WHEN (#69568)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #69567
    
    Problem Summary:
    
    **In short.** A CASE WHEN whose result type is VARBINARY and that has
    two or more WHEN branches failed in BE with `argument_type VarBinary not
    supported`. BE now selects VARBINARY values the way it selects strings.
    
    **Background.** VARBINARY values reach queries through external tables
    (Iceberg, Paimon and Hive binary columns map to VARBINARY) and through
    `CAST(... AS VARBINARY)`. BE evaluates CASE WHEN in `VCaseExpr`: it
    computes, for every row, the index of the branch that the row takes, and
    then `VCaseExpr::_execute_update_result` builds the result column,
    dispatching on the result's primitive type to a typed copy loop. Nereids
    rewrites a CASE with a single WHEN and an ELSE into `if()`, whose
    generic path copies values through `IColumn::insert_from` and accepts
    any column type.
    
    **The problem, and what it cost.** The dispatch in
    `_execute_update_result` had no entry for `TYPE_VARBINARY`, so it fell
    through to `NOT_IMPLEMENTED_ERROR`. Every query with such a CASE failed,
    whatever the constant-folding mode (the CASE is evaluated by BE, not
    folded), for example
    
    ```sql
    SELECT CASE WHEN number % 3 = 0 THEN CAST(concat('a', number) AS VARBINARY)
                WHEN number % 3 = 1 THEN CAST('y' AS VARBINARY)
                ELSE X'FF' END
    FROM numbers("number" = "6");
    -- ERROR 1105 (HY000): errCode = 2, detailMessage = 
(127.0.0.1)[NOT_IMPLEMENTED_ERROR][E3]
    -- argument_type Nullable(VarBinary) not supported
    ```
    
    Only a CASE with one WHEN and an ELSE worked, because it is evaluated as
    `if()`. branch-4.2 has the same gap.
    
    **How this PR fixes it.** `TYPE_VARBINARY` now maps to
    `ColumnVarbinary`, and `ColumnVarbinary` takes the per-row copy
    (`update_result_normal`) that CASE already uses for strings, both for a
    nullable result (including CASE without ELSE, whose unmatched rows are
    NULL) and for a non-nullable one. FE needs no change.
    
    **Results.**
    
    | CASE WHEN with a VARBINARY result | Before | After |
    |---|---|---|
    | Two or more WHEN branches, with ELSE | fails: `argument_type VarBinary
    not supported` | returns the selected bytes |
    | Two or more WHEN branches, without ELSE | fails: `argument_type
    Nullable(VarBinary) not supported` | returns the selected bytes, NULL
    for unmatched rows |
    | One WHEN with ELSE (rewritten to `if()`) | works | works (unchanged) |
    
    - BE unit tests: `VCaseVarbinaryTest` adds two cases through
    `_execute_update_result`: a non-nullable result with ELSE (a column
    branch, a constant branch, NUL and non-UTF-8 bytes) and a nullable
    result without ELSE (NULL from a branch and from unmatched rows). Both
    fail without the change with the error above. `VCaseSelectionTest` still
    passes (50 cases in total).
    - Regression test:
    `query_p0/sql_functions/binary_functions/test_varbinary_case_when` runs
    CASE with and without ELSE over a table, over `numbers()` and on
    constants, in the three constant-folding modes (FE folding, folding by
    BE, no folding); the results match in all modes.
---
 be/src/exprs/vcase_expr.h                          |   3 +
 be/test/exprs/vcase_expr_test.cpp                  |  76 ++++++++++++++++
 .../binary_functions/test_varbinary_case_when.out  | 100 +++++++++++++++++++++
 .../test_varbinary_case_when.groovy                |  64 +++++++++++++
 4 files changed, 243 insertions(+)

diff --git a/be/src/exprs/vcase_expr.h b/be/src/exprs/vcase_expr.h
index ee6e237a128..d8bf48831f3 100644
--- a/be/src/exprs/vcase_expr.h
+++ b/be/src/exprs/vcase_expr.h
@@ -28,6 +28,7 @@
 #include "core/column/column_map.h"
 #include "core/column/column_nullable.h"
 #include "core/column/column_struct.h"
+#include "core/column/column_varbinary.h"
 #include "core/column/variant_v2/column_variant_v2.h"
 #include "core/data_type/data_type_nullable.h"
 #include "core/data_type/data_type_variant_v2.h"
@@ -81,6 +82,7 @@ private:
         auto result_column_ptr = data_type()->create_column();
         result_column_ptr->reserve(rows_count);
         if constexpr (std::is_same_v<ColumnType, ColumnString> ||
+                      std::is_same_v<ColumnType, ColumnVarbinary> ||
                       std::is_same_v<ColumnType, ColumnBitmap> ||
                       std::is_same_v<ColumnType, ColumnArray> ||
                       std::is_same_v<ColumnType, ColumnMap> ||
@@ -137,6 +139,7 @@ private:
             CASE_TYPE(TYPE_CHAR, ColumnString)
             CASE_TYPE(TYPE_VARCHAR, ColumnString)
             CASE_TYPE(TYPE_JSONB, ColumnString)
+            CASE_TYPE(TYPE_VARBINARY, ColumnVarbinary)
             CASE_TYPE(TYPE_DATE, ColumnDate)
             CASE_TYPE(TYPE_DATETIME, ColumnDateTime)
             CASE_TYPE(TYPE_DATEV2, ColumnDateV2)
diff --git a/be/test/exprs/vcase_expr_test.cpp 
b/be/test/exprs/vcase_expr_test.cpp
index 0412c0156c7..294c4b9dc1f 100644
--- a/be/test/exprs/vcase_expr_test.cpp
+++ b/be/test/exprs/vcase_expr_test.cpp
@@ -22,6 +22,7 @@
 #include <array>
 #include <bit>
 #include <limits>
+#include <string>
 #include <type_traits>
 
 #include "core/data_type/data_type_date.h"
@@ -30,6 +31,7 @@
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_timestamp_ns.h"
 #include "core/data_type/data_type_timestamptz.h"
+#include "core/data_type/data_type_varbinary.h"
 #include "core/type_limit.h"
 
 namespace doris {
@@ -137,4 +139,78 @@ TYPED_TEST(VCaseSelectionTest, 
MaximumAndWideBranchIndices) {
     this->check_selection(4099, branches, true);
 }
 
+// A VARBINARY result is selected like a string one. Values keep NUL and 
non-UTF-8 bytes, a
+// constant branch repeats its value, and without ELSE a row that no branch 
selects is NULL.
+class VCaseVarbinaryTest : public ::testing::Test {
+protected:
+    static TExprNode case_node(bool nullable, bool has_else) {
+        TExprNode node;
+        node.__set_node_type(TExprNodeType::CASE_EXPR);
+        node.__set_type(DataTypeVarbinary().to_thrift());
+        node.__set_is_nullable(nullable);
+        node.case_expr.__set_has_else_expr(has_else);
+        return node;
+    }
+
+    static MutableColumnPtr varbinary_column(const std::vector<std::string>& 
values) {
+        auto column = ColumnVarbinary::create();
+        for (const auto& value : values) {
+            column->insert_data(value.data(), value.size());
+        }
+        return column;
+    }
+
+    static std::string value_at(const IColumn& column, size_t row) {
+        auto ref = column.get_data_at(row);
+        return {ref.data, ref.size};
+    }
+};
+
+TEST_F(VCaseVarbinaryTest, ResultWithElse) {
+    VCaseExpr expr(case_node(false, true));
+    const std::vector<std::string> else_values {"e0", "e1", "e2", "e3", "e4", 
"e5"};
+    const std::vector<std::string> then_values {
+            std::string("a\0b", 3), "\xff\xfe", "", "x", "\x80", "y"};
+    std::vector<ColumnPtr> columns {varbinary_column(else_values), 
varbinary_column(then_values),
+                                    
ColumnConst::create(varbinary_column({"c"}), 6)};
+    const std::vector<uint8_t> indices {1, 0, 2, 1, 0, 2};
+
+    auto result = expr._execute_update_result<uint8_t>(indices.data(), 
columns, 6);
+
+    ASSERT_EQ(result->size(), 6);
+    ASSERT_NE(dynamic_cast<const ColumnVarbinary*>(result.get()), nullptr);
+    const std::vector<std::string> expected {std::string("a\0b", 3), "e1", 
"c", "x", "e4", "c"};
+    for (size_t row = 0; row < expected.size(); ++row) {
+        EXPECT_EQ(value_at(*result, row), expected[row]) << row;
+    }
+}
+
+TEST_F(VCaseVarbinaryTest, NullableResultWithoutElse) {
+    VCaseExpr expr(case_node(true, false));
+    auto null_map = ColumnUInt8::create();
+    for (uint8_t is_null : {0, 1, 0, 0, 1, 0}) {
+        null_map->insert_value(is_null);
+    }
+    std::vector<ColumnPtr> columns {
+            nullptr,
+            ColumnNullable::create(varbinary_column({"n0", "n1", "\xff", "n3", 
"n4", "n5"}),
+                                   std::move(null_map)),
+            varbinary_column({"p0", "p1", "p2", std::string("\0", 1), "p4", 
"p5"})};
+    const std::vector<uint8_t> indices {1, 1, 1, 2, 0, 0};
+
+    auto result = expr._execute_update_result<uint8_t>(indices.data(), 
columns, 6);
+
+    ASSERT_EQ(result->size(), 6);
+    const auto& nullable = assert_cast<const ColumnNullable&>(*result);
+    ASSERT_NE(dynamic_cast<const 
ColumnVarbinary*>(&nullable.get_nested_column()), nullptr);
+    const std::vector<bool> expected_null {false, true, false, false, true, 
true};
+    const std::vector<std::string> expected {"n0", "", "\xff", 
std::string("\0", 1), "", ""};
+    for (size_t row = 0; row < expected.size(); ++row) {
+        EXPECT_EQ(nullable.is_null_at(row), expected_null[row]) << row;
+        if (!expected_null[row]) {
+            EXPECT_EQ(value_at(nullable.get_nested_column(), row), 
expected[row]) << row;
+        }
+    }
+}
+
 } // namespace doris
diff --git 
a/regression-test/data/query_p0/sql_functions/binary_functions/test_varbinary_case_when.out
 
b/regression-test/data/query_p0/sql_functions/binary_functions/test_varbinary_case_when.out
new file mode 100644
index 00000000000..d2f8f5b73fd
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/binary_functions/test_varbinary_case_when.out
@@ -0,0 +1,100 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !with_else_fold --
+1      00FF
+2      6332
+3      
+4      00FF
+5      6335
+6      646566
+7      00FF
+8      6338
+9      6A
+
+-- !without_else_fold --
+1      
+2      \N
+3      
+4      
+5      \N
+6      646566
+7      
+8      \N
+9      6A
+
+-- !numbers_fold --
+0      6130
+1      79
+2      FF
+3      6133
+4      79
+5      FF
+
+-- !constant_fold --
+FF00   \N
+
+-- !with_else_fold_by_be --
+1      00FF
+2      6332
+3      
+4      00FF
+5      6335
+6      646566
+7      00FF
+8      6338
+9      6A
+
+-- !without_else_fold_by_be --
+1      
+2      \N
+3      
+4      
+5      \N
+6      646566
+7      
+8      \N
+9      6A
+
+-- !numbers_fold_by_be --
+0      6130
+1      79
+2      FF
+3      6133
+4      79
+5      FF
+
+-- !constant_fold_by_be --
+FF00   \N
+
+-- !with_else_no_fold --
+1      00FF
+2      6332
+3      
+4      00FF
+5      6335
+6      646566
+7      00FF
+8      6338
+9      6A
+
+-- !without_else_no_fold --
+1      
+2      \N
+3      
+4      
+5      \N
+6      646566
+7      
+8      \N
+9      6A
+
+-- !numbers_no_fold --
+0      6130
+1      79
+2      FF
+3      6133
+4      79
+5      FF
+
+-- !constant_no_fold --
+FF00   \N
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/binary_functions/test_varbinary_case_when.groovy
 
b/regression-test/suites/query_p0/sql_functions/binary_functions/test_varbinary_case_when.groovy
new file mode 100644
index 00000000000..b8fddfb98c9
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/binary_functions/test_varbinary_case_when.groovy
@@ -0,0 +1,64 @@
+// 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.
+
+// CASE WHEN with a VARBINARY result: with two or more WHEN branches it is not 
rewritten to if() and BE
+// selects the value by the result type. The branches mix a column cast, 
binary literals with NUL and
+// non-UTF-8 bytes and an expression; without ELSE a row that no branch 
selects is NULL. The results must
+// match whether FE folds constants (fold), FE and BE fold them (fold_by_be) 
or BE evaluates them (no_fold).
+// Each mode puts its name in a column alias, so that it is not served the 
cached result of another mode.
+
+suite("test_varbinary_case_when") {
+    sql "drop table if exists test_varbinary_case_when_t"
+    sql """
+        CREATE TABLE test_varbinary_case_when_t (k int, s string NULL)
+        DUPLICATE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1 PROPERTIES 
('replication_num' = '1')
+    """
+    sql """
+        INSERT INTO test_varbinary_case_when_t VALUES
+            (1, 'a'), (2, 'xyz'), (3, ''), (4, NULL), (5, 'bc'), (6, 'def'), 
(7, 'g'), (8, 'hi'), (9, 'j')
+    """
+
+    def modes = [
+            fold      : [false, false],
+            fold_by_be: [true, false],
+            no_fold   : [false, true]]
+    modes.each { mode, settings ->
+        sql "set enable_fold_constant_by_be = ${settings[0]}"
+        sql "set debug_skip_fold_constant = ${settings[1]}"
+        "order_qt_with_else_${mode}" """
+            SELECT k, hex(CASE WHEN k % 3 = 0 THEN cast(s AS varbinary)
+                               WHEN k % 3 = 1 THEN X'00FF'
+                               ELSE cast(concat('c', k) AS varbinary) END) AS 
with_else_${mode}
+            FROM test_varbinary_case_when_t
+        """
+        "order_qt_without_else_${mode}" """
+            SELECT k, hex(CASE WHEN k % 3 = 0 THEN cast(s AS varbinary)
+                               WHEN k % 3 = 1 THEN X'' END) AS 
without_else_${mode}
+            FROM test_varbinary_case_when_t
+        """
+        "order_qt_numbers_${mode}" """
+            SELECT number, hex(CASE WHEN number % 3 = 0 THEN cast(concat('a', 
number) AS varbinary)
+                                    WHEN number % 3 = 1 THEN cast('y' AS 
varbinary)
+                                    ELSE X'FF' END) AS numbers_${mode}
+            FROM numbers("number" = "6")
+        """
+        "order_qt_constant_${mode}" """
+            SELECT hex(CASE WHEN 1 = 2 THEN X'01' WHEN 2 = 2 THEN X'FF00' ELSE 
X'02' END) AS constant_${mode},
+                   hex(CASE WHEN 1 = 2 THEN X'01' WHEN 2 = 3 THEN X'02' END)
+        """
+    }
+}


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

Reply via email to