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]