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

morningman pushed a commit to branch branch-4.1.4
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1.4 by this push:
     new fc344110d10 branch-4.1.4: [fix](expr) Canonicalize logical OR results 
(#68401)
fc344110d10 is described below

commit fc344110d103312a6bfafb55e936b76f26177b8e
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Sep 23 10:57:14 2026 +0800

    branch-4.1.4: [fix](expr) Canonicalize logical OR results (#68401)
    
    ### What problem does this PR solve?
    
    This backports #68308 to branch-4.1.4.
---
 be/src/exprs/function/functions_logical.h          |   4 +-
 be/src/exprs/vcompound_pred.h                      |   7 +-
 be/test/exprs/function/functions_logical_test.cpp  | 143 +++++++++++++++++++
 .../test_json_extract_bool_null_payload.out        |  41 ++++++
 .../test_json_extract_bool_null_payload.groovy     | 153 +++++++++++++++++++++
 5 files changed, 345 insertions(+), 3 deletions(-)

diff --git a/be/src/exprs/function/functions_logical.h 
b/be/src/exprs/function/functions_logical.h
index 9b684376ac4..783e661422e 100644
--- a/be/src/exprs/function/functions_logical.h
+++ b/be/src/exprs/function/functions_logical.h
@@ -84,7 +84,9 @@ struct AndImpl {
 struct OrImpl {
     using ResultType = UInt8;
 
-    static inline constexpr ResultType apply(UInt8 a, UInt8 b) { return a | b; 
}
+    // A NULL row may carry an arbitrary nested byte. Canonicalize logical OR 
to 0 or 1 so that
+    // such a byte cannot become visible when the other operand makes the 
result non-NULL.
+    static inline constexpr ResultType apply(UInt8 a, UInt8 b) { return (a | 
b) != 0; }
     static inline constexpr ResultType apply_null(UInt8 a, UInt8 l_null, UInt8 
b, UInt8 r_null) {
         // (<> || true) is true, (false || NULL) is NULL
         return (l_null & r_null) | (r_null & (r_null ^ a)) | (l_null & (l_null 
^ b));
diff --git a/be/src/exprs/vcompound_pred.h b/be/src/exprs/vcompound_pred.h
index df5217901e3..44e0ef1fbc9 100644
--- a/be/src/exprs/vcompound_pred.h
+++ b/be/src/exprs/vcompound_pred.h
@@ -610,7 +610,8 @@ private:
             if constexpr (is_and) {
                 lhs[i] &= rhs[i];
             } else {
-                lhs[i] |= rhs[i];
+                // Logical OR must produce a canonical Boolean instead of 
preserving input bits.
+                lhs[i] = (lhs[i] | rhs[i]) != 0;
             }
         }
     }
@@ -633,7 +634,9 @@ private:
                 res_data[i] = lhs_data[i] & rhs_data[i];
             } else {
                 res_null[i] = apply_or_null(lhs_data[i], lhs_null[i], 
rhs_data[i], rhs_null[i]);
-                res_data[i] = lhs_data[i] | rhs_data[i];
+                // A NULL row may carry an arbitrary nested byte. If the 
result remains NULL the
+                // byte is ignored; otherwise normalization prevents it from 
becoming visible.
+                res_data[i] = (lhs_data[i] | rhs_data[i]) != 0;
             }
         }
     }
diff --git a/be/test/exprs/function/functions_logical_test.cpp 
b/be/test/exprs/function/functions_logical_test.cpp
new file mode 100644
index 00000000000..90d7cea69c4
--- /dev/null
+++ b/be/test/exprs/function/functions_logical_test.cpp
@@ -0,0 +1,143 @@
+// 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 "exprs/function/functions_logical.h"
+
+#include <gen_cpp/Exprs_types.h>
+#include <gtest/gtest.h>
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "core/block/block.h"
+#include "core/column/column_const.h"
+#include "core/column/column_nullable.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/primitive_type.h"
+#include "exprs/vcompound_pred.h"
+#include "exprs/vexpr_context.h"
+#include "testutil/column_helper.h"
+
+namespace doris {
+
+namespace {
+
+ColumnWithTypeAndName nullable_boolean_column(std::vector<UInt8> data, 
std::vector<UInt8> null_map,
+                                              std::string name) {
+    return {ColumnHelper::create_nullable_column<DataTypeUInt8>(data, 
null_map),
+            make_nullable(std::make_shared<DataTypeUInt8>()), std::move(name)};
+}
+
+ColumnPtr execute_or(ColumnWithTypeAndName left, ColumnWithTypeAndName right, 
size_t rows) {
+    auto result_type = make_nullable(std::make_shared<DataTypeUInt8>());
+    Block block({std::move(left), std::move(right), {nullptr, result_type, 
"result"}});
+    auto status = FunctionOr::create()->execute_impl(nullptr, block, {0, 1}, 
2, rows);
+    EXPECT_TRUE(status.ok()) << status.to_string();
+    return block.get_by_position(2).column;
+}
+
+class ColumnExpr final : public VExpr {
+public:
+    ColumnExpr(ColumnPtr column, DataTypePtr type)
+            : VExpr(std::move(type), false), _column(std::move(column)) {}
+
+    bool is_constant() const override { return false; }
+
+    const std::string& expr_name() const override {
+        static const std::string name = "ColumnExpr";
+        return name;
+    }
+
+    Status execute_column_impl(VExprContext*, const Block*, const Selector*, 
size_t,
+                               ColumnPtr& result_column) const override {
+        result_column = _column;
+        return Status::OK();
+    }
+
+private:
+    ColumnPtr _column;
+};
+
+ColumnPtr execute_compound_or(ColumnWithTypeAndName left, 
ColumnWithTypeAndName right,
+                              size_t rows) {
+    TExprNode node;
+    node.__set_type(create_type_desc(TYPE_BOOLEAN));
+    node.__set_node_type(TExprNodeType::COMPOUND_PRED);
+    node.__set_opcode(TExprOpcode::COMPOUND_OR);
+    node.__set_num_children(2);
+    node.__set_is_nullable(true);
+
+    auto compound = VCompoundPred::create_shared(node);
+    compound->add_child(std::make_shared<ColumnExpr>(std::move(left.column), 
left.type));
+    compound->add_child(std::make_shared<ColumnExpr>(std::move(right.column), 
right.type));
+    VExprContext context(compound);
+    ColumnPtr result;
+    auto status = compound->execute_column(&context, nullptr, nullptr, rows, 
result);
+    EXPECT_TRUE(status.ok()) << status.to_string();
+    return result;
+}
+
+void expect_boolean(const ColumnNullable& result, size_t row, UInt8 value) {
+    EXPECT_FALSE(result.is_null_at(row));
+    EXPECT_EQ(assert_cast<const 
ColumnUInt8&>(result.get_nested_column()).get_data()[row], value);
+}
+
+} // namespace
+
+TEST(FunctionsLogicalTest, NullableOrIgnoresNullPayload) {
+    auto result =
+            execute_or(nullable_boolean_column({65, 0, 65, 1, 65}, {1, 0, 1, 
0, 1}, "left"),
+                       nullable_boolean_column({1, 0, 0, 65, 127}, {0, 0, 0, 
1, 1}, "right"), 5);
+
+    const auto& nullable_result = assert_cast<const ColumnNullable&>(*result);
+    expect_boolean(nullable_result, 0, 1);
+    expect_boolean(nullable_result, 1, 0);
+    EXPECT_TRUE(nullable_result.is_null_at(2));
+    expect_boolean(nullable_result, 3, 1);
+    EXPECT_TRUE(nullable_result.is_null_at(4));
+}
+
+TEST(FunctionsLogicalTest, NullableOrWithTrueConstantProducesCanonicalTrue) {
+    constexpr size_t rows = 4;
+    auto left = nullable_boolean_column({3, 65, 0, 255}, {1, 1, 1, 1}, "left");
+    ColumnWithTypeAndName right {
+            
ColumnConst::create(ColumnHelper::create_column<DataTypeUInt8>({1}), rows),
+            std::make_shared<DataTypeUInt8>(), "right"};
+
+    auto result = execute_or(std::move(left), std::move(right), rows);
+    const auto& nullable_result = assert_cast<const ColumnNullable&>(*result);
+    for (size_t row = 0; row < rows; ++row) {
+        expect_boolean(nullable_result, row, 1);
+    }
+}
+
+TEST(FunctionsLogicalTest, CompoundNullableOrIgnoresNullPayload) {
+    auto result = execute_compound_or(
+            nullable_boolean_column({65, 0, 65, 1, 65}, {1, 0, 1, 0, 1}, 
"left"),
+            nullable_boolean_column({1, 0, 0, 65, 127}, {0, 0, 0, 1, 1}, 
"right"), 5);
+
+    const auto& nullable_result = assert_cast<const ColumnNullable&>(*result);
+    expect_boolean(nullable_result, 0, 1);
+    expect_boolean(nullable_result, 1, 0);
+    EXPECT_TRUE(nullable_result.is_null_at(2));
+    expect_boolean(nullable_result, 3, 1);
+    EXPECT_TRUE(nullable_result.is_null_at(4));
+}
+
+} // namespace doris
diff --git 
a/regression-test/data/query_p0/sql_functions/json_functions/test_json_extract_bool_null_payload.out
 
b/regression-test/data/query_p0/sql_functions/json_functions/test_json_extract_bool_null_payload.out
new file mode 100644
index 00000000000..5c37ac4894d
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/json_functions/test_json_extract_bool_null_payload.out
@@ -0,0 +1,41 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !or_payload_no_short_circuit --
+\N     66000
+true   34000
+
+-- !and_or_payload_no_short_circuit --
+\N     66000
+true   34000
+
+-- !case_count_no_short_circuit --
+100000
+
+-- !case_branches_no_short_circuit --
+then_a 66000
+then_b 34000
+
+-- !or_payload_short_circuit --
+\N     66000
+true   34000
+
+-- !and_or_payload_short_circuit --
+\N     66000
+true   34000
+
+-- !case_count_short_circuit --
+100000
+
+-- !case_branches_short_circuit --
+then_a 66000
+then_b 34000
+
+-- !nullable_or_truth_table --
+false  false   false
+false  null    \N
+false  true    true
+null   false   \N
+null   null    \N
+null   true    true
+true   false   true
+true   null    true
+true   true    true
diff --git 
a/regression-test/suites/query_p0/sql_functions/json_functions/test_json_extract_bool_null_payload.groovy
 
b/regression-test/suites/query_p0/sql_functions/json_functions/test_json_extract_bool_null_payload.groovy
new file mode 100644
index 00000000000..7550fe0d6b6
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/json_functions/test_json_extract_bool_null_payload.groovy
@@ -0,0 +1,153 @@
+// 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_json_extract_bool_null_payload") {
+    sql "DROP TABLE IF EXISTS test_json_extract_bool_null_payload"
+    sql """
+        CREATE TABLE test_json_extract_bool_null_payload (
+            id BIGINT,
+            j STRING,
+            a BIGINT,
+            b BIGINT
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        INSERT INTO test_json_extract_bool_null_payload
+        SELECT number,
+               CASE WHEN number % 100 = 0
+                    THEN '{"flag": true}'
+                    ELSE CONCAT('{"n": ', number, '}')
+               END,
+               number + CASE number % 3
+                            WHEN 0 THEN 10
+                            WHEN 1 THEN -100
+                            ELSE -200000
+                        END,
+               number
+        FROM numbers("number" = "100000")
+    """
+
+    sql "SET enable_sql_cache = false"
+    sql "SET short_circuit_evaluation = false"
+
+    order_qt_or_payload_no_short_circuit """
+        SELECT v, count(*) FROM (
+            SELECT JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), '\$.flag') 
OR (a > b) AS v
+            FROM test_json_extract_bool_null_payload
+        ) t
+        GROUP BY v
+        ORDER BY v
+    """
+
+    order_qt_and_or_payload_no_short_circuit """
+        SELECT v, count(*) FROM (
+            SELECT (JSON_PARSE_ERROR_TO_NULL(j) IS NOT NULL
+                    AND JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), 
'\$.flag'))
+                   OR (a > b) AS v
+            FROM test_json_extract_bool_null_payload
+        ) t
+        GROUP BY v
+        ORDER BY v
+    """
+
+    qt_case_count_no_short_circuit """
+        SELECT count(*)
+        FROM test_json_extract_bool_null_payload
+        WHERE (CASE
+                 WHEN JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), 
'\$.flag') OR a > b THEN b
+                 WHEN a < b THEN a
+               END) IS NOT NULL
+    """
+
+    order_qt_case_branches_no_short_circuit """
+        SELECT CASE
+                 WHEN JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), 
'\$.flag') OR a > b
+                   THEN 'then_b'
+                 WHEN a < b THEN 'then_a'
+                 ELSE 'else'
+               END AS branch,
+               count(*)
+        FROM test_json_extract_bool_null_payload
+        GROUP BY branch
+        ORDER BY branch
+    """
+
+    sql "SET short_circuit_evaluation = true"
+
+    order_qt_or_payload_short_circuit """
+        SELECT v, count(*) FROM (
+            SELECT JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), '\$.flag') 
OR (a > b) AS v
+            FROM test_json_extract_bool_null_payload
+        ) t
+        GROUP BY v
+        ORDER BY v
+    """
+
+    order_qt_and_or_payload_short_circuit """
+        SELECT v, count(*) FROM (
+            SELECT (JSON_PARSE_ERROR_TO_NULL(j) IS NOT NULL
+                    AND JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), 
'\$.flag'))
+                   OR (a > b) AS v
+            FROM test_json_extract_bool_null_payload
+        ) t
+        GROUP BY v
+        ORDER BY v
+    """
+
+    qt_case_count_short_circuit """
+        SELECT count(*)
+        FROM test_json_extract_bool_null_payload
+        WHERE (CASE
+                 WHEN JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), 
'\$.flag') OR a > b THEN b
+                 WHEN a < b THEN a
+               END) IS NOT NULL
+    """
+
+    order_qt_case_branches_short_circuit """
+        SELECT CASE
+                 WHEN JSON_EXTRACT_BOOL(JSON_PARSE_ERROR_TO_NULL(j), 
'\$.flag') OR a > b
+                   THEN 'then_b'
+                 WHEN a < b THEN 'then_a'
+                 ELSE 'else'
+               END AS branch,
+               count(*)
+        FROM test_json_extract_bool_null_payload
+        GROUP BY branch
+        ORDER BY branch
+    """
+
+    order_qt_nullable_or_truth_table """
+        SELECT lhs.label, rhs.label, lhs.v OR rhs.v AS result
+        FROM (
+            SELECT 'false' AS label, CAST(false AS BOOLEAN) AS v
+            UNION ALL
+            SELECT 'null', CAST(NULL AS BOOLEAN)
+            UNION ALL
+            SELECT 'true', CAST(true AS BOOLEAN)
+        ) lhs
+        CROSS JOIN (
+            SELECT 'false' AS label, CAST(false AS BOOLEAN) AS v
+            UNION ALL
+            SELECT 'null', CAST(NULL AS BOOLEAN)
+            UNION ALL
+            SELECT 'true', CAST(true AS BOOLEAN)
+        ) rhs
+        ORDER BY lhs.label, rhs.label
+    """
+}


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

Reply via email to