diff --git a/dbms/src/Flash/Coprocessor/DAGUtils.cpp b/dbms/src/Flash/Coprocessor/DAGUtils.cpp index fd9aff4bb8f..e9ca7f0d780 100644 --- a/dbms/src/Flash/Coprocessor/DAGUtils.cpp +++ b/dbms/src/Flash/Coprocessor/DAGUtils.cpp @@ -216,13 +216,14 @@ const std::unordered_map scalar_func_map({ //{tipb::ScalarFuncSig::NEJson, "notEquals"}, {tipb::ScalarFuncSig::NEVectorFloat32, "notEquals"}, - //{tipb::ScalarFuncSig::NullEQInt, "cast"}, - //{tipb::ScalarFuncSig::NullEQReal, "cast"}, - //{tipb::ScalarFuncSig::NullEQString, "cast"}, - //{tipb::ScalarFuncSig::NullEQDecimal, "cast"}, - //{tipb::ScalarFuncSig::NullEQTime, "cast"}, - //{tipb::ScalarFuncSig::NullEQDuration, "cast"}, - //{tipb::ScalarFuncSig::NullEQJson, "cast"}, + {tipb::ScalarFuncSig::NullEQInt, "tidbNullEQ"}, + {tipb::ScalarFuncSig::NullEQReal, "tidbNullEQ"}, + {tipb::ScalarFuncSig::NullEQString, "tidbNullEQ"}, + {tipb::ScalarFuncSig::NullEQDecimal, "tidbNullEQ"}, + {tipb::ScalarFuncSig::NullEQTime, "tidbNullEQ"}, + {tipb::ScalarFuncSig::NullEQDuration, "tidbNullEQ"}, + //{tipb::ScalarFuncSig::NullEQJson, "tidbNullEQ"}, + {tipb::ScalarFuncSig::NullEQVectorFloat32, "tidbNullEQ"}, {tipb::ScalarFuncSig::PlusReal, "plus"}, {tipb::ScalarFuncSig::PlusDecimal, "plus"}, diff --git a/dbms/src/Flash/Coprocessor/tests/gtest_tidb_null_eq_func.cpp b/dbms/src/Flash/Coprocessor/tests/gtest_tidb_null_eq_func.cpp new file mode 100644 index 00000000000..6cf0f67208d --- /dev/null +++ b/dbms/src/Flash/Coprocessor/tests/gtest_tidb_null_eq_func.cpp @@ -0,0 +1,56 @@ +// Copyright 2023 PingCAP, Inc. +// +// Licensed 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 +#include + +namespace DB::tests +{ +TEST(TiDBNullEQFuncTest, DagUtilsMappedToTidbNullEQ) +{ + { + tipb::Expr expr; + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.set_sig(tipb::ScalarFuncSig::NullEQInt); + + ASSERT_TRUE(isScalarFunctionExpr(expr)); + ASSERT_EQ(getFunctionName(expr), "tidbNullEQ"); + } + { + tipb::Expr expr; + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.set_sig(tipb::ScalarFuncSig::NullEQString); + + ASSERT_TRUE(isScalarFunctionExpr(expr)); + ASSERT_EQ(getFunctionName(expr), "tidbNullEQ"); + } + { + tipb::Expr expr; + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.set_sig(tipb::ScalarFuncSig::NullEQDecimal); + + ASSERT_TRUE(isScalarFunctionExpr(expr)); + ASSERT_EQ(getFunctionName(expr), "tidbNullEQ"); + } + { + tipb::Expr expr; + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.set_sig(tipb::ScalarFuncSig::NullEQVectorFloat32); + + ASSERT_TRUE(isScalarFunctionExpr(expr)); + ASSERT_EQ(getFunctionName(expr), "tidbNullEQ"); + } +} + +} // namespace DB::tests diff --git a/dbms/src/Functions/FunctionsComparison.cpp b/dbms/src/Functions/FunctionsComparison.cpp index e57443f809d..09350a4a52a 100644 --- a/dbms/src/Functions/FunctionsComparison.cpp +++ b/dbms/src/Functions/FunctionsComparison.cpp @@ -12,12 +12,214 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include #include #include #include namespace DB { +namespace ErrorCodes +{ +extern const int NUMBER_OF_ARGUMENTS_DOESNT_MATCH; +extern const int ILLEGAL_COLUMN; +extern const int LOGICAL_ERROR; +} // namespace ErrorCodes + +class FunctionTiDBNullEQ : public IFunction +{ +public: + static constexpr auto name = "tidbNullEQ"; + + static FunctionPtr create(const Context &) { return std::make_shared(); } + + String getName() const override { return name; } + + size_t getNumberOfArguments() const override { return 2; } + + bool useDefaultImplementationForNulls() const override { return false; } + bool useDefaultImplementationForConstants() const override { return true; } + + void setCollator(const TiDB::TiDBCollatorPtr & collator_) override + { + collator = collator_; + equals_function->setCollator(collator_); + } + + DataTypePtr getReturnTypeImpl(const DataTypes & arguments) const override + { + if (arguments.size() != 2) + throw Exception( + ErrorCodes::NUMBER_OF_ARGUMENTS_DOESNT_MATCH, + "Number of arguments for function {} doesn't match: passed {}, should be 2.", + getName(), + arguments.size()); + + /// `NULL <=> x` is always true/false (never NULL), even if `NULL` is represented as `Nothing`. + if (arguments[0]->onlyNull() || arguments[1]->onlyNull()) + return std::make_shared(); + + /// Use equals to validate that the input types are comparable. + /// Always return non-nullable UInt8 because `NULL <=> x` is always true/false (not NULL). + FunctionEquals().getReturnTypeImpl({removeNullable(arguments[0]), removeNullable(arguments[1])}); + return std::make_shared(); + } + + void executeImpl(Block & block, const ColumnNumbers & arguments, size_t result) const override + { + const auto & left = block.getByPosition(arguments[0]); + const auto & right = block.getByPosition(arguments[1]); + + ColumnPtr left_col = left.column; + ColumnPtr right_col = right.column; + + const size_t rows = left_col->size(); + if (unlikely(right_col->size() != rows)) + throw Exception( + ErrorCodes::ILLEGAL_COLUMN, + "Columns sizes are different in function {}: left {}, right {}.", + getName(), + rows, + right_col->size()); + + /// Fast path for always-NULL columns (Nullable(Nothing)). + /// `NULL <=> x` equals to `isNull(x)`; `NULL <=> NULL` is always 1. + if (left_col->onlyNull() || right_col->onlyNull()) + { + if (left_col->onlyNull() && right_col->onlyNull()) + { + block.getByPosition(result).column = ColumnUInt8::create(rows, 1); + return; + } + + const ColumnPtr & other_col = left_col->onlyNull() ? right_col : left_col; + if (other_col->isColumnNullable()) + { + const auto & other_nullmap = assert_cast(*other_col).getNullMapData(); + auto res_col = ColumnUInt8::create(); + auto & res_data = res_col->getData(); + res_data.assign(other_nullmap.begin(), other_nullmap.end()); + block.getByPosition(result).column = std::move(res_col); + } + else + { + block.getByPosition(result).column = ColumnUInt8::create(rows, 0); + } + return; + } + + auto unwrap_nullable_column = [rows](const ColumnPtr & col, ColumnPtr & nested_col, const NullMap *& nullmap) { + nested_col = col; + nullmap = nullptr; + + if (const auto * const_col = typeid_cast(col.get())) + { + const auto & data_col = const_col->getDataColumn(); + if (data_col.isColumnNullable()) + { + /// `ColumnConst(ColumnNullable(NULL))` is handled by the `onlyNull()` fast path above. + /// If we reach here, the nullable constant must be non-NULL, so there is no nullmap to apply. + const auto & nullable_col = assert_cast(data_col); + nested_col = ColumnConst::create(nullable_col.getNestedColumnPtr(), rows); + } + return; + } + + if (col->isColumnNullable()) + { + const auto & nullable_col = assert_cast(*col); + nested_col = nullable_col.getNestedColumnPtr(); + nullmap = &nullable_col.getNullMapData(); + } + }; + + ColumnPtr left_nested_col = left_col; + const NullMap * left_nullmap = nullptr; + unwrap_nullable_column(left_col, left_nested_col, left_nullmap); + + ColumnPtr right_nested_col = right_col; + const NullMap * right_nullmap = nullptr; + unwrap_nullable_column(right_col, right_nested_col, right_nullmap); + + /// Execute `equals` on nested columns. + Block temp_block; + temp_block.insert({left_nested_col, removeNullable(left.type), "a"}); + temp_block.insert({right_nested_col, removeNullable(right.type), "b"}); + temp_block.insert({nullptr, std::make_shared(), "res"}); + DefaultExecutable(equals_function).execute(temp_block, {0, 1}, 2); + + ColumnPtr eq_col = temp_block.getByPosition(2).column; + if (left_nullmap == nullptr && right_nullmap == nullptr) + { + block.getByPosition(result).column = std::move(eq_col); + return; + } + + if (ColumnPtr converted = eq_col->convertToFullColumnIfConst()) + eq_col = converted; + + /// Adjust for NULL values: + /// - both NULL => 1 + /// - one NULL => 0 + /// - no NULL => equals result + auto eq_mutable = (*std::move(eq_col)).mutate(); + auto * eq_vec_col = typeid_cast(eq_mutable.get()); + if (unlikely(eq_vec_col == nullptr)) + throw Exception( + ErrorCodes::LOGICAL_ERROR, + "Unexpected result column type {} for equals inside {}.", + eq_mutable->getName(), + getName()); + + auto & res_data = eq_vec_col->getData(); + if (left_nullmap != nullptr && right_nullmap != nullptr) + { + const auto & left_data = *left_nullmap; + const auto & right_data = *right_nullmap; + for (size_t i = 0; i < rows; ++i) + { + const UInt8 left_is_null = left_data[i] != 0; + const UInt8 right_is_null = right_data[i] != 0; + + const UInt8 any_null = left_is_null | right_is_null; + const UInt8 both_null = left_is_null & right_is_null; + + /// Keep equals result when `any_null == 0`, otherwise override it to 0. + /// Finally, override to 1 when `both_null == 1`. + const auto eq = static_cast(res_data[i] != 0); + res_data[i] = (eq & static_cast(!any_null)) | both_null; + } + } + else if (left_nullmap != nullptr) + { + const auto & left_data = *left_nullmap; + for (size_t i = 0; i < rows; ++i) + { + const UInt8 left_is_null = left_data[i] != 0; + const auto eq = static_cast(res_data[i] != 0); + res_data[i] = eq & static_cast(!left_is_null); + } + } + else if (right_nullmap != nullptr) + { + const auto & right_data = *right_nullmap; + for (size_t i = 0; i < rows; ++i) + { + const UInt8 right_is_null = right_data[i] != 0; + const auto eq = static_cast(res_data[i] != 0); + res_data[i] = eq & static_cast(!right_is_null); + } + } + + block.getByPosition(result).column = std::move(eq_mutable); + } + +private: + TiDB::TiDBCollatorPtr collator = nullptr; + std::shared_ptr equals_function = std::make_shared(); +}; + void registerFunctionsComparison(FunctionFactory & factory) { factory.registerFunction(); @@ -31,6 +233,7 @@ void registerFunctionsComparison(FunctionFactory & factory) factory.registerFunction(); factory.registerFunction(); factory.registerFunction(); + factory.registerFunction(); } template <> diff --git a/dbms/src/Functions/tests/gtest_tidb_null_eq.cpp b/dbms/src/Functions/tests/gtest_tidb_null_eq.cpp new file mode 100644 index 00000000000..a9c222c36ed --- /dev/null +++ b/dbms/src/Functions/tests/gtest_tidb_null_eq.cpp @@ -0,0 +1,124 @@ +// Copyright 2023 PingCAP, Inc. +// +// Licensed 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 +#include + +namespace DB::tests +{ +class TestTiDBNullEQ : public DB::tests::FunctionTest +{ +}; + +TEST_F(TestTiDBNullEQ, Basic) +try +{ + auto a = createColumn({1, 2, 2}); + auto b = createColumn({1, 3, 2}); + auto res = executeFunction("tidbNullEQ", a, b); + ASSERT_EQ(res.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({1, 0, 1}), res); +} +CATCH + +TEST_F(TestTiDBNullEQ, NullableInputs) +try +{ + auto a = createColumn>({1, std::nullopt, std::nullopt, 2}); + auto b = createColumn>({1, std::nullopt, 3, std::nullopt}); + auto res = executeFunction("tidbNullEQ", a, b); + ASSERT_EQ(res.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({1, 1, 0, 0}), res); +} +CATCH + +TEST_F(TestTiDBNullEQ, OnlyNullColumns) +try +{ + auto a = createOnlyNullColumn(5); + auto b = createOnlyNullColumn(5); + auto res = executeFunction("tidbNullEQ", a, b); + ASSERT_EQ(res.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({1, 1, 1, 1, 1}), res); +} +CATCH + +TEST_F(TestTiDBNullEQ, OneSideOnlyNull) +try +{ + auto a = createOnlyNullColumn(3); + auto b = createColumn>({1, std::nullopt, 3}); + auto res = executeFunction("tidbNullEQ", a, b); + ASSERT_EQ(res.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({0, 1, 0}), res); +} +CATCH + +TEST_F(TestTiDBNullEQ, ConstOnlyNull) +try +{ + auto a = createOnlyNullColumnConst(4); + auto b = createConstColumn>(4, 1); + auto res = executeFunction("tidbNullEQ", a, b); + ASSERT_EQ(res.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createConstColumn(4, 0), res); +} +CATCH + +TEST_F(TestTiDBNullEQ, ConstNullableNonNull) +try +{ + auto a = createConstColumn>(4, 1); + auto b = createColumn>({1, std::nullopt, 2, 1}); + auto res = executeFunction("tidbNullEQ", a, b); + ASSERT_EQ(res.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({1, 0, 0, 1}), res); + + auto res2 = executeFunction("tidbNullEQ", b, a); + ASSERT_EQ(res2.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({1, 0, 0, 1}), res2); +} +CATCH + +TEST_F(TestTiDBNullEQ, ConstNullableNull) +try +{ + auto a = createConstColumn>(4, std::nullopt); + auto b = createColumn>({1, std::nullopt, 2, std::nullopt}); + + auto res = executeFunction("tidbNullEQ", a, b); + ASSERT_EQ(res.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({0, 1, 0, 1}), res); + + auto res2 = executeFunction("tidbNullEQ", b, a); + ASSERT_EQ(res2.type->getName(), "UInt8"); + ASSERT_COLUMN_EQ(createColumn({0, 1, 0, 1}), res2); +} +CATCH + +TEST_F(TestTiDBNullEQ, CollatorIsForwardedToEquals) +try +{ + auto a = createColumn>({"a", "A", std::nullopt}); + auto b = createColumn>({"A", "a", std::nullopt}); + + auto ci_collator = TiDB::ITiDBCollator::getCollator(TiDB::ITiDBCollator::UTF8MB4_GENERAL_CI); + ASSERT_COLUMN_EQ(createColumn({1, 1, 1}), executeFunction("tidbNullEQ", {a, b}, ci_collator)); + + auto bin_collator = TiDB::ITiDBCollator::getCollator(TiDB::ITiDBCollator::BINARY); + ASSERT_COLUMN_EQ(createColumn({0, 0, 1}), executeFunction("tidbNullEQ", {a, b}, bin_collator)); +} +CATCH + +} // namespace DB::tests diff --git a/dbms/src/Storages/DeltaMerge/FilterParser/FilterParser.cpp b/dbms/src/Storages/DeltaMerge/FilterParser/FilterParser.cpp index 7b635dd0dcd..92fb1c5bdd9 100644 --- a/dbms/src/Storages/DeltaMerge/FilterParser/FilterParser.cpp +++ b/dbms/src/Storages/DeltaMerge/FilterParser/FilterParser.cpp @@ -212,6 +212,12 @@ inline RSOperatorPtr parseTiCompareExpr( // switch (filter_type) { case FilterParser::RSFilterType::Equal: + if ((expr.sig() == tipb::ScalarFuncSig::NullEQInt || expr.sig() == tipb::ScalarFuncSig::NullEQReal + || expr.sig() == tipb::ScalarFuncSig::NullEQString || expr.sig() == tipb::ScalarFuncSig::NullEQDecimal + || expr.sig() == tipb::ScalarFuncSig::NullEQTime || expr.sig() == tipb::ScalarFuncSig::NullEQDuration + || expr.sig() == tipb::ScalarFuncSig::NullEQVectorFloat32) + && values[0].isNull()) + return createIsNull(attr); return createEqual(attr, values[0]); case FilterParser::RSFilterType::NotEqual: return createNotEqual(attr, values[0]); @@ -579,13 +585,14 @@ std::unordered_map FilterParser {tipb::ScalarFuncSig::NEDuration, FilterParser::RSFilterType::NotEqual}, {tipb::ScalarFuncSig::NEJson, FilterParser::RSFilterType::NotEqual}, - //{tipb::ScalarFuncSig::NullEQInt, "cast"}, - //{tipb::ScalarFuncSig::NullEQReal, "cast"}, - //{tipb::ScalarFuncSig::NullEQString, "cast"}, - //{tipb::ScalarFuncSig::NullEQDecimal, "cast"}, - //{tipb::ScalarFuncSig::NullEQTime, "cast"}, - //{tipb::ScalarFuncSig::NullEQDuration, "cast"}, - //{tipb::ScalarFuncSig::NullEQJson, "cast"}, + {tipb::ScalarFuncSig::NullEQInt, FilterParser::RSFilterType::Equal}, + {tipb::ScalarFuncSig::NullEQReal, FilterParser::RSFilterType::Equal}, + {tipb::ScalarFuncSig::NullEQString, FilterParser::RSFilterType::Equal}, + {tipb::ScalarFuncSig::NullEQDecimal, FilterParser::RSFilterType::Equal}, + {tipb::ScalarFuncSig::NullEQTime, FilterParser::RSFilterType::Equal}, + {tipb::ScalarFuncSig::NullEQDuration, FilterParser::RSFilterType::Equal}, + {tipb::ScalarFuncSig::NullEQJson, FilterParser::RSFilterType::Equal}, + {tipb::ScalarFuncSig::NullEQVectorFloat32, FilterParser::RSFilterType::Equal}, // {tipb::ScalarFuncSig::PlusReal, "plus"}, // {tipb::ScalarFuncSig::PlusDecimal, "plus"}, diff --git a/dbms/src/Storages/DeltaMerge/tests/gtest_dm_filter_parser_nulleq.cpp b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_filter_parser_nulleq.cpp new file mode 100644 index 00000000000..e904531464a --- /dev/null +++ b/dbms/src/Storages/DeltaMerge/tests/gtest_dm_filter_parser_nulleq.cpp @@ -0,0 +1,149 @@ +// Copyright 2023 PingCAP, Inc. +// +// Licensed 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 +#include +#include +#include +#include +#include +#include +#include +#include + +namespace DB::DM::tests +{ + +namespace +{ +tipb::Expr buildColumnRefExpr(Int64 column_index, Int32 field_type) +{ + tipb::Expr col; + col.set_tp(tipb::ExprType::ColumnRef); + { + WriteBufferFromOwnString ss; + encodeDAGInt64(column_index, ss); + col.set_val(ss.releaseStr()); + } + auto * field_type_pb = col.mutable_field_type(); + field_type_pb->set_tp(field_type); + field_type_pb->set_flag(0); + return col; +} + +tipb::Expr buildInt64LiteralExpr(Int64 value) +{ + tipb::Expr lit; + lit.set_tp(tipb::ExprType::Int64); + { + WriteBufferFromOwnString ss; + encodeDAGInt64(value, ss); + lit.set_val(ss.releaseStr()); + } + return lit; +} + +tipb::Expr buildNullLiteralExpr() +{ + tipb::Expr lit; + lit.set_tp(tipb::ExprType::Null); + return lit; +} + +String parseToDebugString(Context & context, const tipb::Expr & filter_expr) +{ + google::protobuf::RepeatedPtrField filters; + filters.Add()->CopyFrom(filter_expr); + + const google::protobuf::RepeatedPtrField pushed_down_filters{}; + + TiDB::ColumnInfo col; + col.id = 1; + TiDB::ColumnInfos column_infos = {col}; + + const ColumnDefines columns_to_read = {ColumnDefine{1, "a", std::make_shared()}}; + auto create_attr_by_column_id = [&columns_to_read](ColumnID column_id) -> Attr { + auto iter + = std::find_if(columns_to_read.begin(), columns_to_read.end(), [column_id](const ColumnDefine & d) -> bool { + return d.id == column_id; + }); + if (iter != columns_to_read.end()) + return Attr{.col_name = iter->name, .col_id = iter->id, .type = iter->type}; + return Attr{.col_name = "", .col_id = column_id, .type = DataTypePtr{}}; + }; + + const auto ann_query_info = tipb::ANNQueryInfo{}; + auto dag_query = std::make_unique( + filters, + ann_query_info, + pushed_down_filters, + column_infos, + std::vector{}, + 0, + context.getTimezoneInfo()); + + const auto op + = DB::DM::FilterParser::parseDAGQuery(*dag_query, column_infos, create_attr_by_column_id, Logger::get()); + return op->toDebugString(); +} +} // namespace + +TEST(DMFilterParserTest, ParseNullEQ) +try +{ + auto context = DMTestEnv::getContext(); + + { + // a <=> 1 -> equal(a, 1) + tipb::Expr expr; + expr.set_sig(tipb::ScalarFuncSig::NullEQInt); + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.add_children()->CopyFrom(buildColumnRefExpr(/*column_index*/ 0, TiDB::TypeLongLong)); + expr.add_children()->CopyFrom(buildInt64LiteralExpr(1)); + EXPECT_EQ(parseToDebugString(*context, expr), R"raw({"op":"equal","col":"a","value":"1"})raw"); + } + + { + // a <=> NULL -> isnull(a) + tipb::Expr expr; + expr.set_sig(tipb::ScalarFuncSig::NullEQInt); + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.add_children()->CopyFrom(buildColumnRefExpr(/*column_index*/ 0, TiDB::TypeLongLong)); + expr.add_children()->CopyFrom(buildNullLiteralExpr()); + EXPECT_EQ(parseToDebugString(*context, expr), R"raw({"op":"isnull","col":"a"})raw"); + } + + { + // NULL <=> a -> isnull(a) + tipb::Expr expr; + expr.set_sig(tipb::ScalarFuncSig::NullEQInt); + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.add_children()->CopyFrom(buildNullLiteralExpr()); + expr.add_children()->CopyFrom(buildColumnRefExpr(/*column_index*/ 0, TiDB::TypeLongLong)); + EXPECT_EQ(parseToDebugString(*context, expr), R"raw({"op":"isnull","col":"a"})raw"); + } + + { + // 1 <=> a -> equal(a, 1) + tipb::Expr expr; + expr.set_sig(tipb::ScalarFuncSig::NullEQInt); + expr.set_tp(tipb::ExprType::ScalarFunc); + expr.add_children()->CopyFrom(buildInt64LiteralExpr(1)); + expr.add_children()->CopyFrom(buildColumnRefExpr(/*column_index*/ 0, TiDB::TypeLongLong)); + EXPECT_EQ(parseToDebugString(*context, expr), R"raw({"op":"equal","col":"a","value":"1"})raw"); + } +} +CATCH + +} // namespace DB::DM::tests