Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions be/src/exprs/function/if.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -430,17 +430,22 @@ class FunctionIf : public IFunction {
DCHECK(remove_nullable(arg_cond.type)->get_primitive_type() ==
PrimitiveType::TYPE_BOOLEAN);

// update nested column by null map
// Treat NULL as false. The nested column may be shared with other columns of the
// block (e.g. NULLIF wraps its first argument as the nested column), so build a
// new condition column instead of mutating the nested column in place.
const auto rows = nullable->size();
const auto* __restrict null_map = nullable->get_null_map_data().data();
auto* __restrict nested_bool_data =
((ColumnUInt8&)(nullable->get_nested_column())).get_data().data();
auto rows = nullable->size();
const auto* __restrict nested_bool_data =
assert_cast<const ColumnUInt8&>(nullable->get_nested_column())
.get_data()
.data();
auto cond_column = ColumnUInt8::create(rows);
auto* __restrict cond_data = cond_column->get_data().data();
for (size_t i = 0; i < rows; i++) {
nested_bool_data[i] &= !null_map[i];
cond_data[i] = nested_bool_data[i] & !null_map[i];
}
auto column_size = block.columns();
block.insert({nullable->get_nested_column_ptr(), remove_nullable(arg_cond.type),
arg_cond.name});
block.insert({std::move(cond_column), remove_nullable(arg_cond.type), arg_cond.name});

handled = true;
return _execute_impl_internal(context, block, {column_size, arguments[1], arguments[2]},
Expand Down
20 changes: 13 additions & 7 deletions be/src/exprs/vcondition_expr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@

#include <glog/logging.h>

#include "core/assert_cast.h"
#include "core/column/column.h"
#include "core/column/column_const.h"
#include "core/column/column_nullable.h"
#include "core/column/column_vector.h"
#include "exprs/function_context.h"
#include "util/simd/bits.h"

Expand Down Expand Up @@ -377,17 +380,20 @@ Status VectorizedIfExpr::execute_for_null_condition(Block& block, const ColumnNu
if (const auto* nullable = check_and_get_column<ColumnNullable>(*arg_cond.column)) {
DCHECK(remove_nullable(arg_cond.type)->get_primitive_type() == PrimitiveType::TYPE_BOOLEAN);

// update nested column by null map
// Treat NULL as false. The nested column may be shared with other columns of the
// block (e.g. NULLIF wraps its first argument as the nested column), so build a new
// condition column instead of mutating the nested column in place.
const auto rows = nullable->size();
const auto* __restrict null_map = nullable->get_null_map_data().data();
auto* __restrict nested_bool_data =
((ColumnUInt8&)(nullable->get_nested_column())).get_data().data();
auto rows = nullable->size();
const auto* __restrict nested_bool_data =
assert_cast<const ColumnUInt8&>(nullable->get_nested_column()).get_data().data();
auto cond_column = ColumnUInt8::create(rows);
auto* __restrict cond_data = cond_column->get_data().data();
for (size_t i = 0; i < rows; i++) {
nested_bool_data[i] &= !null_map[i];
cond_data[i] = nested_bool_data[i] & !null_map[i];
}
auto column_size = block.columns();
block.insert(
{nullable->get_nested_column_ptr(), remove_nullable(arg_cond.type), arg_cond.name});
block.insert({std::move(cond_column), remove_nullable(arg_cond.type), arg_cond.name});

handled = true;
return _execute_impl_internal(block, {column_size, arguments[1], arguments[2]}, result,
Expand Down
101 changes: 101 additions & 0 deletions be/test/exprs/function/function_if_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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 <memory>
#include <vector>

#include "common/status.h"
#include "core/assert_cast.h"
#include "core/block/block.h"
#include "core/column/column.h"
#include "core/column/column_nullable.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_number.h"
#include "core/types.h"
#include "exprs/function/function.h"
#include "exprs/function/simple_function_factory.h"
#include "exprs/function_context.h"
#include "testutil/function_utils.h"

namespace doris {

static ColumnPtr make_bool_column(const std::vector<UInt8>& values) {
auto column = ColumnUInt8::create();
for (auto v : values) {
column->insert_value(v);
}
return column;
}

// Same shape as `IF(NULLIF(b, p), f, b)`: NULLIF is if(b = p, NULL, b) and wraps b's
// column itself as the nested column of its Nullable(Boolean) result. The outer IF treats
// a NULL condition as false; that normalization must not be written into the shared
// nested column, otherwise both the else branch and every other user of b see the
// polluted values.
// Input:
// b (non-nullable bool): [1, 1]
// cond = Nullable(nested = b's column, null_map = [0, 1]), logically [true, NULL]
// f (non-nullable bool): [0, 0]
// Expected IF(cond, f, b): [0, 1]; b (and cond's nested column) must stay [1, 1].
TEST(FunctionIfTest, NullableConditionNotPolluteSharedNestedColumn) {
auto bool_type = std::make_shared<DataTypeUInt8>();
auto nullable_bool_type = make_nullable(bool_type);

ColumnPtr b_column = make_bool_column({1, 1});
ColumnPtr f_column = make_bool_column({0, 0});
ColumnPtr cond_column = ColumnNullable::create(b_column, make_bool_column({0, 1}));

Block block({{cond_column, nullable_bool_type, "cond"},
{f_column, bool_type, "f"},
{b_column, bool_type, "b"},
{nullptr, bool_type, "result"}});

auto func = SimpleFunctionFactory::instance().get_function(
"if", {block.get_by_position(0), block.get_by_position(1), block.get_by_position(2)},
bool_type);
ASSERT_TRUE(func != nullptr);

FunctionUtils fn_utils(bool_type, {nullable_bool_type, bool_type, bool_type}, false);
auto* fn_ctx = fn_utils.get_fn_ctx();
ASSERT_TRUE(func->open(fn_ctx, FunctionContext::FRAGMENT_LOCAL).ok());
ASSERT_TRUE(func->open(fn_ctx, FunctionContext::THREAD_LOCAL).ok());
auto st = func->execute(fn_ctx, block, {0, 1, 2}, 3, 2);
ASSERT_TRUE(st.ok()) << st.to_string();
static_cast<void>(func->close(fn_ctx, FunctionContext::THREAD_LOCAL));
static_cast<void>(func->close(fn_ctx, FunctionContext::FRAGMENT_LOCAL));

const auto& result_data =
assert_cast<const ColumnUInt8&>(*block.get_by_position(3).column).get_data();
ASSERT_EQ(result_data.size(), 2);
EXPECT_EQ(result_data[0], 0);
EXPECT_EQ(result_data[1], 1);

const auto& b_data = assert_cast<const ColumnUInt8&>(*b_column).get_data();
EXPECT_EQ(b_data[0], 1);
EXPECT_EQ(b_data[1], 1);
const auto& cond_nested_data =
assert_cast<const ColumnUInt8&>(
assert_cast<const ColumnNullable&>(*cond_column).get_nested_column())
.get_data();
EXPECT_EQ(cond_nested_data[0], 1);
EXPECT_EQ(cond_nested_data[1], 1);
}

} // namespace doris
74 changes: 69 additions & 5 deletions be/test/exprs/vcondition_expr_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
#include <cmath>
#include <limits>
#include <memory>
#include <string>
#include <vector>

#include "core/assert_cast.h"
#include "core/column/column_nullable.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_date_or_datetime_v2.h"
Expand All @@ -38,9 +40,10 @@

namespace doris {

// Build a minimal TExprNode as the input of VectorizedCoalesceExpr.
// Build a minimal TExprNode as the input of a VConditionExpr.
// Only fields required by the VExpr base ctor (so that create_data_type works) are set.
static TExprNode make_coalesce_node(TPrimitiveType::type ptype, bool is_nullable, int scale = -1) {
static TExprNode make_function_node(const std::string& fn_name, TPrimitiveType::type ptype,
bool is_nullable, int scale = -1) {
TExprNode node;
node.node_type = TExprNodeType::FUNCTION_CALL;
node.num_children = 0;
Expand All @@ -59,14 +62,18 @@ static TExprNode make_coalesce_node(TPrimitiveType::type ptype, bool is_nullable
node.__set_type(type_desc);

TFunction fn;
TFunctionName fn_name;
fn_name.function_name = "coalesce";
fn.name = fn_name;
TFunctionName function_name;
function_name.function_name = fn_name;
fn.name = function_name;
node.__set_fn(fn);

return node;
}

static TExprNode make_coalesce_node(TPrimitiveType::type ptype, bool is_nullable, int scale = -1) {
return make_function_node("coalesce", ptype, is_nullable, scale);
}

// Mock child expression: returns the pre-injected ColumnPtr / DataTypePtr to the parent expr.
// Behavior is modeled after MockVExprForTryCast in try_cast_expr_test.cpp.
class MockChildVExpr : public VExpr {
Expand Down Expand Up @@ -119,6 +126,15 @@ static ColumnPtr make_float64_column(const std::vector<double>& values) {
return col;
}

// Helper: build a non-nullable Boolean column from a list of 0/1 values.
static ColumnPtr make_bool_column(const std::vector<UInt8>& values) {
auto col = ColumnUInt8::create();
for (auto v : values) {
col->insert_value(v);
}
return col;
}

// Helper: extract the Float64 value at `row` from the result column,
// handling both nullable and non-nullable cases.
static double get_float64_value(const ColumnPtr& column, size_t row, bool* is_null = nullptr) {
Expand Down Expand Up @@ -409,4 +425,52 @@ TEST_F(VConditionExprCoalesceTest, TimeStampNs) {
EXPECT_EQ(values[3].epoch_nanos(), std::numeric_limits<int64_t>::min());
}

class VConditionExprIfTest : public ::testing::Test {};

// Same shape as `IF(NULLIF(b, p), f, b)`: NULLIF wraps b's column itself as the nested
// column of its Nullable(Boolean) result. IF treats a NULL condition as false; that
// normalization must not be written into the shared nested column, otherwise the else
// branch (and every other user of b in the block) reads polluted values.
// Input:
// b (non-nullable bool): [1, 1]
// cond = Nullable(nested = b's column, null_map = [0, 1]), logically [true, NULL]
// f (non-nullable bool): [0, 0]
// Expected IF(cond, f, b): [0, 1]; b (and cond's nested column) must stay [1, 1].
TEST_F(VConditionExprIfTest, NullableCondition_NotPolluteSharedNestedColumn) {
auto if_node = make_function_node("if", TPrimitiveType::BOOLEAN, /*is_nullable=*/false);
auto if_expr = VectorizedIfExpr::create_shared(if_node);
auto bool_type = std::make_shared<DataTypeUInt8>();
if_expr->data_type() = bool_type;

ColumnPtr b_column = make_bool_column({1, 1});
ColumnPtr f_column = make_bool_column({0, 0});
ColumnPtr cond_column = ColumnNullable::create(b_column, make_bool_column({0, 1}));

if_expr->add_child(std::make_shared<MockChildVExpr>(
cond_column, std::make_shared<DataTypeNullable>(bool_type)));
if_expr->add_child(std::make_shared<MockChildVExpr>(f_column, bool_type));
if_expr->add_child(std::make_shared<MockChildVExpr>(b_column, bool_type));

VExprContext context(if_expr);
ColumnPtr result;
auto st = if_expr->execute_column_impl(&context, /*block=*/nullptr, /*selector=*/nullptr,
/*count=*/2, result);
ASSERT_TRUE(st.ok()) << st.to_string();
ASSERT_TRUE(result.get() != nullptr);
const auto& result_data = assert_cast<const ColumnUInt8&>(*result).get_data();
ASSERT_EQ(result_data.size(), 2);
EXPECT_EQ(result_data[0], 0);
EXPECT_EQ(result_data[1], 1);

const auto& b_data = assert_cast<const ColumnUInt8&>(*b_column).get_data();
EXPECT_EQ(b_data[0], 1);
EXPECT_EQ(b_data[1], 1);
const auto& cond_nested_data =
assert_cast<const ColumnUInt8&>(
assert_cast<const ColumnNullable&>(*cond_column).get_nested_column())
.get_data();
EXPECT_EQ(cond_nested_data[0], 1);
EXPECT_EQ(cond_nested_data[1], 1);
}

} // namespace doris
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- This file is automatically generated. You should know what you did if you want to edit this
-- !if_nullif --
1 false
2 true

-- !if_nullif_projection --
1 true false false true false
2 true true false \N true

-- !if_nullif_short_circuit --
1 false
2 true

-- !if_nullif_projection_short_circuit --
1 true false false true false
2 true true false \N true

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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_if_nullable_condition") {
sql "drop table if exists test_if_nullable_condition"
sql """
create table test_if_nullable_condition (
id int,
b boolean not null,
p boolean not null,
f boolean not null
) duplicate key(id)
distributed by hash(id) buckets 1
properties ("replication_num" = "1")
"""
sql """
insert into test_if_nullable_condition values
(1, true, false, false),
(2, true, true, false)
"""

// nullif(b, p) is [true, NULL] and reuses b as the nested column of its result.
// IF treats the NULL condition as false; that normalization must not be written into
// the column shared with the else branch and with the other projected columns.
sql "set short_circuit_evaluation = false"
qt_if_nullif """
select id, if(nullif(b, p), f, b) as r
from test_if_nullable_condition order by id
"""
qt_if_nullif_projection """
select id, b, p, f, nullif(b, p) as cond, if(nullif(b, p), f, b) as r
from test_if_nullable_condition order by id
"""

sql "set short_circuit_evaluation = true"
qt_if_nullif_short_circuit """
select id, if(nullif(b, p), f, b) as r
from test_if_nullable_condition order by id
"""
qt_if_nullif_projection_short_circuit """
select id, b, p, f, nullif(b, p) as cond, if(nullif(b, p), f, b) as r
from test_if_nullable_condition order by id
"""
}
Loading