/*
 * 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 "paimon/predicate/predicate.h"

#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "arrow/api.h"
#include "arrow/array/array_nested.h"
#include "arrow/ipc/json_simple.h"
#include "gtest/gtest.h"
#include "paimon/common/data/binary_array.h"
#include "paimon/common/data/binary_row.h"
#include "paimon/common/data/binary_row_writer.h"
#include "paimon/common/predicate/predicate_filter.h"
#include "paimon/defs.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/predicate/function.h"
#include "paimon/predicate/literal.h"
#include "paimon/predicate/predicate_builder.h"
#include "paimon/result.h"
#include "paimon/status.h"
#include "paimon/testing/utils/testharness.h"

namespace arrow {
class Array;
}  // namespace arrow

namespace paimon::test {
class PredicateTest : public ::testing::Test {
 public:
    void SetUp() override {}
    void TearDown() override {}
    struct FieldStats {
        FieldStats(const std::optional<int64_t>& _min_value,
                   const std::optional<int64_t>& _max_value, int64_t _null_count)
            : min_value(_min_value), max_value(_max_value), null_count(_null_count) {}
        std::optional<int64_t> min_value;
        std::optional<int64_t> max_value;
        int64_t null_count;
    };

    bool StatsCheck(const PredicateFilter& predicate, int64_t row_count,
                    const std::vector<FieldStats>& field_stats) const {
        auto pool = GetDefaultPool();
        BinaryRow min_row(/*arity=*/field_stats.size());
        BinaryRowWriter min_row_writer(&min_row, 0, pool.get());
        BinaryRow max_row(/*arity=*/field_stats.size());
        BinaryRowWriter max_row_writer(&max_row, 0, pool.get());
        std::vector<int64_t> nulls;
        arrow::FieldVector fields;
        for (uint32_t i = 0; i < field_stats.size(); i++) {
            const auto& stats = field_stats[i];
            if (stats.min_value == std::nullopt) {
                min_row_writer.SetNullAt(i);
            } else {
                min_row_writer.WriteLong(i, stats.min_value.value());
            }
            if (stats.max_value == std::nullopt) {
                max_row_writer.SetNullAt(i);
            } else {
                max_row_writer.WriteLong(i, stats.max_value.value());
            }
            nulls.emplace_back(stats.null_count);
            fields.emplace_back(arrow::field("f" + std::to_string(i), arrow::int64()));
        }
        min_row_writer.Complete();
        max_row_writer.Complete();
        auto null_counts = BinaryArray::FromLongArray(nulls, pool.get());
        auto arrow_schema = arrow::schema(fields);
        EXPECT_OK_AND_ASSIGN(
            auto ret, predicate.Test(arrow_schema, row_count, min_row, max_row, null_counts));
        return ret;
    }

    struct StringFieldStats {
        StringFieldStats(const std::optional<std::string>& _min_value,
                         const std::optional<std::string>& _max_value, int64_t _null_count)
            : min_value(_min_value), max_value(_max_value), null_count(_null_count) {}
        std::optional<std::string> min_value;
        std::optional<std::string> max_value;
        int64_t null_count;
    };

    bool StringStatsCheck(const PredicateFilter& predicate, int64_t row_count,
                          const std::vector<StringFieldStats>& field_stats) const {
        auto pool = GetDefaultPool();
        BinaryRow min_row(/*arity=*/field_stats.size());
        BinaryRowWriter min_row_writer(&min_row, 0, pool.get());
        BinaryRow max_row(/*arity=*/field_stats.size());
        BinaryRowWriter max_row_writer(&max_row, 0, pool.get());
        std::vector<int64_t> nulls;
        arrow::FieldVector fields;
        for (uint32_t i = 0; i < field_stats.size(); i++) {
            const auto& stats = field_stats[i];
            if (stats.min_value == std::nullopt) {
                min_row_writer.SetNullAt(i);
            } else {
                min_row_writer.WriteString(
                    i, BinaryString::FromString(stats.min_value.value(), pool.get()));
            }
            if (stats.max_value == std::nullopt) {
                max_row_writer.SetNullAt(i);
            } else {
                max_row_writer.WriteString(
                    i, BinaryString::FromString(stats.max_value.value(), pool.get()));
            }
            nulls.emplace_back(stats.null_count);
            fields.emplace_back(arrow::field("f" + std::to_string(i), arrow::utf8()));
        }
        min_row_writer.Complete();
        max_row_writer.Complete();
        auto null_counts = BinaryArray::FromLongArray(nulls, pool.get());
        auto arrow_schema = arrow::schema(fields);
        EXPECT_OK_AND_ASSIGN(
            auto ret, predicate.Test(arrow_schema, row_count, min_row, max_row, null_counts));
        return ret;
    }

    BinaryRow CreateBigIntRow(const std::vector<std::optional<int64_t>>& value) const {
        auto pool = GetDefaultPool();
        BinaryRow row(/*arity=*/value.size());
        BinaryRowWriter row_writer(&row, 0, pool.get());
        for (size_t i = 0; i < value.size(); ++i) {
            if (value[i] == std::nullopt) {
                row_writer.SetNullAt(i);
            } else {
                row_writer.WriteLong(i, value[i].value());
            }
        }
        row_writer.Complete();
        return row;
    }

    BinaryRow CreateStringRow(const std::vector<std::optional<std::string>>& value) const {
        auto pool = GetDefaultPool();
        BinaryRow row(/*arity=*/value.size());
        BinaryRowWriter row_writer(&row, 0, pool.get());
        for (size_t i = 0; i < value.size(); ++i) {
            if (value[i] == std::nullopt) {
                row_writer.SetNullAt(i);
            } else {
                row_writer.WriteString(i, BinaryString::FromString(value[i].value(), pool.get()));
            }
        }
        row_writer.Complete();
        return row;
    }
};

TEST_F(PredicateTest, TestInvalidFieldIndex) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::Equal(/*field_index=*/2, /*field_name=*/"f0",
                                                  FieldType::BIGINT, Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);

    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 5, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});
    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    // with array
    ASSERT_NOK_WITH_MSG(predicate->Test(*struct_array),
                        "field index 2 exceed field count 2 in struct array");

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_NOK_WITH_MSG(predicate->Test(arrow_schema, CreateBigIntRow({4})),
                        "field index 2 exceed field count 1 in row");
}

TEST_F(PredicateTest, TestEqual) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                  FieldType::BIGINT, Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 5, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();
    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 1, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
                                          Literal(5l)));

    ASSERT_FALSE(*predicate->Negate() ==
                 *PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0",
                                             FieldType::BIGINT, Literal(10l)));
    ASSERT_FALSE(*predicate->Negate() == *PredicateBuilder::Equal(/*field_index=*/0,
                                                                  /*field_name=*/"f0",
                                                                  FieldType::BIGINT, Literal(10l)));
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({5})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());

    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 6ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestEqualNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                  FieldType::BIGINT, Literal(FieldType::BIGINT));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());

    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestNotEqual) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0",
                                                     FieldType::BIGINT, Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 5, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 0, 0}));

    auto predicate_negate = std::dynamic_pointer_cast<PredicateFilter>(predicate->Negate());
    ASSERT_EQ(*predicate_negate, *PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                          FieldType::BIGINT, Literal(5l)));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({5})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    ASSERT_TRUE(predicate_negate->Test(arrow_schema, CreateBigIntRow({5})).value());

    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 6ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(5ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestNotEqualNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0",
                                                     FieldType::BIGINT, Literal(FieldType::BIGINT));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());

    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestGreater) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"f0",
                                                        FieldType::BIGINT, Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 5, 6, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::LessOrEqual(/*field_index=*/0, /*field_name=*/"f0",
                                             FieldType::BIGINT, Literal(5l)));
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({5})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({6})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());

    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 4ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 6ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestGreaterNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::GreaterThan(
        /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, Literal(FieldType::BIGINT));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 4ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestGreaterOrEqual) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::GreaterOrEqual(
        /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    ASSERT_EQ(predicate->GetFunction().ToString(), "GreaterOrEqual");
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 5, 6, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 1, 1, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::LessThan(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
                                          Literal(5l)));
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({5})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({6})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());

    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 4ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 6ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestGreaterOrEqualNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::GreaterOrEqual(
        /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, Literal(FieldType::BIGINT));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 4ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestLess) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::LessThan(/*field_index=*/0, /*field_name=*/"f0",
                                                     FieldType::BIGINT, Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 5, 6, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 0, 0, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::GreaterOrEqual(
                  /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, Literal(5l)));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({5})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({6})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(5ll, 7ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(4ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestLessNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::LessThan(/*field_index=*/0, /*field_name=*/"f0",
                                                     FieldType::BIGINT, Literal(FieldType::BIGINT));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestLessOrEqual) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::LessOrEqual(/*field_index=*/0, /*field_name=*/"f0",
                                                        FieldType::BIGINT, Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 5, 6, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 1, 0, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"f0",
                                             FieldType::BIGINT, Literal(5l)));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({5})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({6})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(5ll, 7ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(4ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestLessOrEqualNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::LessOrEqual(
        /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, Literal(FieldType::BIGINT));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestIsNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base =
        PredicateBuilder::IsNull(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT);
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 1}));

    ASSERT_EQ(*predicate->Negate(), *PredicateBuilder::IsNotNull(
                                        /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(5ll, 7ll, 1ll)}));
}

TEST_F(PredicateTest, TestIsNotNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base =
        PredicateBuilder::IsNotNull(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT);
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::IsNull(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(5ll, 7ll, 1ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(std::nullopt, std::nullopt, 3ll)}));
}

TEST_F(PredicateTest, TestIn) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0",
                                               FieldType::BIGINT, {Literal(1l), Literal(3l)});
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::NotIn(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
                                       {Literal(1l), Literal(3l)}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestInNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::In(
        /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
        {Literal(1l), Literal(FieldType::BIGINT), Literal(3l)});
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestNotIn) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::NotIn(/*field_index=*/0, /*field_name=*/"f0",
                                                  FieldType::BIGINT, {Literal(1l), Literal(3l)});
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 1, 0, 0}));

    ASSERT_EQ(*predicate->Negate(),
              *PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
                                    {Literal(1l), Literal(3l)}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 1ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(3ll, 3ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 3ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestNotInNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::NotIn(
        /*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
        {Literal(1l), Literal(FieldType::BIGINT), Literal(3l)});
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0, 0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 1ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(3ll, 3ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 3ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestLargeIn) {
    auto bigint_type = arrow::int64();
    std::vector<Literal> literals;
    literals.reserve(30);
    literals.emplace_back(1l);
    literals.emplace_back(3l);
    for (int64_t i = 10; i < 30; i++) {
        literals.emplace_back(i);
    }
    auto predicate_base =
        PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, literals);
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(29ll, 32ll, 0ll)}));
}

TEST_F(PredicateTest, TestLargeInNull) {
    auto bigint_type = arrow::int64();
    std::vector<Literal> literals;
    literals.reserve(30);
    literals.emplace_back(1l);
    literals.emplace_back(FieldType::BIGINT);
    literals.emplace_back(3l);
    for (int64_t i = 10; i < 30; i++) {
        literals.emplace_back(i);
    }
    auto predicate_base =
        PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, literals);
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(29ll, 32ll, 0ll)}));
}

TEST_F(PredicateTest, TestLargeNotIn) {
    auto bigint_type = arrow::int64();
    std::vector<Literal> literals;
    literals.reserve(30);
    literals.emplace_back(1l);
    literals.emplace_back(3l);
    for (int64_t i = 10; i < 30; i++) {
        literals.emplace_back(i);
    }
    auto predicate_base = PredicateBuilder::NotIn(/*field_index=*/0, /*field_name=*/"f0",
                                                  FieldType::BIGINT, literals);
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 1, 0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 1ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(3ll, 3ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 3ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(29ll, 32ll, 0ll)}));
}

TEST_F(PredicateTest, TestLargeNotInNull) {
    auto bigint_type = arrow::int64();
    std::vector<Literal> literals;
    literals.reserve(30);
    literals.emplace_back(1l);
    literals.emplace_back(FieldType::BIGINT);
    literals.emplace_back(3l);
    for (int64_t i = 10; i < 30; i++) {
        literals.emplace_back(i);
    }
    auto predicate_base = PredicateBuilder::NotIn(/*field_index=*/0, /*field_name=*/"f0",
                                                  FieldType::BIGINT, literals);
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 0])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0, 0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({2})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({3})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 1ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(3ll, 3ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 3ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(0ll, 5ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(29ll, 32ll, 0ll)}));
}

TEST_F(PredicateTest, TestAnd) {
    auto bigint_type = arrow::int64();
    ASSERT_OK_AND_ASSIGN(
        auto predicate_base,
        PredicateBuilder::And({PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                       FieldType::BIGINT, Literal(3l)),
                               PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
                                                       FieldType::BIGINT, Literal(5l))}));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 3, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([5, 6, 5, 5])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));

    ASSERT_OK_AND_ASSIGN(
        auto negate_predicate,
        PredicateBuilder::Or({PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0",
                                                         FieldType::BIGINT, Literal(3l)),
                              PredicateBuilder::NotEqual(/*field_index=*/1, /*field_name=*/"f1",
                                                         FieldType::BIGINT, Literal(5l))}));
    ASSERT_EQ(*predicate->Negate(), *negate_predicate);

    // with internal row
    auto arrow_schema = arrow::schema(
        arrow::FieldVector({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4, 5})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({3, 6})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({3, 5})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt, 5})).value());
    // with stats
    ASSERT_TRUE(
        StatsCheck(*predicate, 3ll, {FieldStats(3ll, 6ll, 0ll), FieldStats(4ll, 6ll, 0ll)}));
    ASSERT_FALSE(
        StatsCheck(*predicate, 3ll, {FieldStats(3ll, 6ll, 0ll), FieldStats(6ll, 8ll, 0ll)}));
    ASSERT_FALSE(
        StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll), FieldStats(4ll, 6ll, 0ll)}));
}

TEST_F(PredicateTest, TestOr) {
    auto bigint_type = arrow::int64();
    ASSERT_OK_AND_ASSIGN(
        auto predicate_base,
        PredicateBuilder::Or({PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                      FieldType::BIGINT, Literal(3l)),
                              PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
                                                      FieldType::BIGINT, Literal(5l))}));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, 3, 3, null])").ValueOrDie();
    auto f1 =
        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([6, 6, 5, 5])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 1, 1, 1}));

    ASSERT_OK_AND_ASSIGN(
        auto negate_predicate,
        PredicateBuilder::And({PredicateBuilder::NotEqual(/*field_index=*/0, /*field_name=*/"f0",
                                                          FieldType::BIGINT, Literal(3l)),
                               PredicateBuilder::NotEqual(/*field_index=*/1, /*field_name=*/"f1",
                                                          FieldType::BIGINT, Literal(5l))}));
    ASSERT_EQ(*predicate->Negate(), *negate_predicate);

    // with internal row
    auto arrow_schema = arrow::schema(
        arrow::FieldVector({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4, 6})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({3, 6})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({3, 5})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt, 5})).value());
    // with stats
    ASSERT_TRUE(
        StatsCheck(*predicate, 3ll, {FieldStats(3ll, 6ll, 0ll), FieldStats(4ll, 6ll, 0ll)}));
    ASSERT_TRUE(
        StatsCheck(*predicate, 3ll, {FieldStats(3ll, 6ll, 0ll), FieldStats(6ll, 8ll, 0ll)}));
    ASSERT_FALSE(
        StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll), FieldStats(8ll, 10ll, 0ll)}));
}

TEST_F(PredicateTest, TestBetween) {
    auto bigint_type = arrow::int64();
    auto predicate_base = PredicateBuilder::Between(/*field_index=*/0, /*field_name=*/"f0",
                                                    FieldType::BIGINT, Literal(3l), Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 4, 5, 100, 1, null])")
                  .ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, 4, 5, 6])")
                  .ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();
    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({1, 1, 1, 0, 0, 0}));

    auto less_than = PredicateBuilder::LessThan(/*field_index=*/0, /*field_name=*/"f0",
                                                FieldType::BIGINT, Literal(3l));
    auto greater_than = PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"f0",
                                                      FieldType::BIGINT, Literal(5l));
    ASSERT_OK_AND_ASSIGN(auto or_predicate, PredicateBuilder::Or({less_than, greater_than}));

    auto predicate_negate = std::dynamic_pointer_cast<PredicateFilter>(predicate->Negate());
    ASSERT_EQ(*predicate_negate, *or_predicate);
    ASSERT_FALSE(*predicate_negate == *predicate_base);

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({1})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());
    ASSERT_TRUE(predicate_negate->Test(arrow_schema, CreateBigIntRow({1})).value());

    // with stats
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 10ll, 0ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(3ll, 4ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(6ll, 7ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
    ASSERT_TRUE(StatsCheck(*predicate, 3ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestBetweenNull) {
    auto bigint_type = arrow::int64();
    auto predicate_base =
        PredicateBuilder::Between(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
                                  Literal(FieldType::BIGINT), Literal(5l));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([4, null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2])").ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", bigint_type), arrow::field("f1", bigint_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();
    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", bigint_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({4})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateBigIntRow({std::nullopt})).value());

    // with stats
    ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(1ll, 10ll, 0ll)}));
    ASSERT_FALSE(StatsCheck(*predicate, 1ll, {FieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestStartsWith) {
    auto string_type = arrow::utf8();
    ASSERT_OK_AND_ASSIGN(
        const auto predicate_base,
        PredicateBuilder::StartsWith(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                                     Literal(FieldType::STRING, "aab", 3)));
    const auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(string_type,
                                                        R"(["ccddee", "bbccdd", "aabbcc", null])")
                  .ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(
                  string_type, R"(["gghhii", "ffgghh", "eeffgg", "ddeeff"])")
                  .ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", string_type), arrow::field("f1", string_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));

    ASSERT_EQ(predicate->Negate(), nullptr);
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", string_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"ccddee"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"bbccdd"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"aabbcc"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({std::nullopt})).value());

    // with stats
    // min="aaa", max="aaz" covers prefix "aab"
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "aaz", 0ll)}));
    // min="aab", max="aab" exact match prefix
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aab", "aab", 0ll)}));
    // min="aabxxx", max="aabzzz" both start with "aab"
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aabxxx", "aabzzz", 0ll)}));
    // min="bbb", max="ccc" entirely above prefix "aab"
    ASSERT_FALSE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("bbb", "ccc", 0ll)}));
    // min="aaa", max="aaa" entirely below prefix "aab"
    ASSERT_FALSE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "aaa", 0ll)}));
    // all nulls
    ASSERT_FALSE(
        StringStatsCheck(*predicate, 1ll, {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestStartsWithNull) {
    const auto string_type = arrow::utf8();
    ASSERT_OK_AND_ASSIGN(
        const auto predicate_base,
        PredicateBuilder::StartsWith(
            /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING, Literal(FieldType::STRING)));
    const auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(string_type, R"(["bbccdd", null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(string_type, R"(["ffgghh", "ccddee"])")
                  .ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", string_type), arrow::field("f1", string_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", string_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"bbccdd"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({std::nullopt})).value());

    // with stats: null literal always returns false
    ASSERT_FALSE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "zzz", 0ll)}));
    ASSERT_FALSE(
        StringStatsCheck(*predicate, 1ll, {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestEndsWith) {
    auto string_type = arrow::utf8();
    ASSERT_OK_AND_ASSIGN(
        const auto predicate_base,
        PredicateBuilder::EndsWith(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                                   Literal(FieldType::STRING, "bcc", 3)));
    const auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(string_type,
                                                        R"(["ccddee", "bbccdd", "aabbcc", null])")
                  .ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(
                  string_type, R"(["gghhii", "ffgghh", "eeffgg", "ddeeff"])")
                  .ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", string_type), arrow::field("f1", string_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));

    ASSERT_EQ(predicate->Negate(), nullptr);
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", string_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"ccddee"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"bbccdd"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"aabbcc"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({std::nullopt})).value());

    // with stats: EndsWith base class always returns true for non-null stats
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "zzz", 0ll)}));
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("xxx", "yyy", 0ll)}));
    // all nulls
    ASSERT_FALSE(
        StringStatsCheck(*predicate, 1ll, {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestEndsWithNull) {
    const auto string_type = arrow::utf8();
    ASSERT_OK_AND_ASSIGN(
        const auto predicate_base,
        PredicateBuilder::EndsWith(
            /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING, Literal(FieldType::STRING)));
    const auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(string_type, R"(["bbccdd", null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(string_type, R"(["ffgghh", "ccddee"])")
                  .ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", string_type), arrow::field("f1", string_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", string_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"bbccdd"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({std::nullopt})).value());

    // with stats: null literal always returns false
    ASSERT_FALSE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "zzz", 0ll)}));
    ASSERT_FALSE(
        StringStatsCheck(*predicate, 1ll, {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestContains) {
    auto string_type = arrow::utf8();
    ASSERT_OK_AND_ASSIGN(
        const auto predicate_base,
        PredicateBuilder::Contains(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                                   Literal(FieldType::STRING, "cde", 3)));
    const auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(string_type,
                                                        R"(["ghijkl", "defghi", "abcdef", null])")
                  .ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(
                  string_type, R"(["stuvwx", "pqrstu", "mnopqr", "jklmno"])")
                  .ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", string_type), arrow::field("f1", string_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));

    ASSERT_EQ(predicate->Negate(), nullptr);
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", string_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"ghijkl"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"defghi"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"abcdef"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({std::nullopt})).value());

    // with stats: Contains base class always returns true for non-null stats
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "zzz", 0ll)}));
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("xxx", "yyy", 0ll)}));
    // all nulls
    ASSERT_FALSE(
        StringStatsCheck(*predicate, 1ll, {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestContainsNull) {
    const auto string_type = arrow::utf8();
    ASSERT_OK_AND_ASSIGN(
        const auto predicate_base,
        PredicateBuilder::Contains(
            /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING, Literal(FieldType::STRING)));
    const auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    auto f0 =
        arrow::ipc::internal::json::ArrayFromJSON(string_type, R"(["defghi", null])").ValueOrDie();
    auto f1 = arrow::ipc::internal::json::ArrayFromJSON(string_type, R"(["pqrstu", "jklmno"])")
                  .ValueOrDie();
    std::shared_ptr<arrow::DataType> src_type =
        arrow::struct_({arrow::field("f0", string_type), arrow::field("f1", string_type)});

    std::shared_ptr<arrow::Array> struct_array =
        arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();

    ASSERT_OK_AND_ASSIGN(auto is_valid, predicate->Test(*struct_array));
    ASSERT_EQ(is_valid, std::vector<char>({0, 0}));

    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", string_type)}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"defghi"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({std::nullopt})).value());

    // with stats: null literal always returns false
    ASSERT_FALSE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "zzz", 0ll)}));
    ASSERT_FALSE(
        StringStatsCheck(*predicate, 1ll, {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestLike) {
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a.c", 3)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate);
    ASSERT_EQ(predicate->Negate(), nullptr);
    // with internal row
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"abc"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"a.c"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a.*d", 4)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"abcd"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "%c.e", 4)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"abcde"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a\\_c", 4)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"a-c"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"a_c"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "start%", 6)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"startX"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"not_startX"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "%middle%", 8)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"xxmiddleyy"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"xxmidxdleyy"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "%end", 4)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"xxend"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"xxendyy"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "equal", 5)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"equal"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"equalxx"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "st_rt%", 6)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"startxx"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"stbrtxx"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"xxstbrtxx"})).value());

    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "abc%def%", 8)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"abchahadefxx"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"abchahadafxx"})).value());

    // with stats: Like base class always returns true for non-null stats
    ASSERT_TRUE(StringStatsCheck(*predicate, 3ll, {StringFieldStats("aaa", "zzz", 0ll)}));
    // all nulls
    ASSERT_FALSE(
        StringStatsCheck(*predicate, 1ll, {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestLikeConsecutivePercent) {
    // consecutive '%' should be merged into one '%'
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // "%%" should behave the same as "%"
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "%%", 2)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"anything"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({""})).value());

    // "a%%%b" should behave the same as "a%b"
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a%%%b", 5)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"axyzb"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"ab"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"axyzc"})).value());
}

TEST_F(PredicateTest, TestLikeEmptyField) {
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // empty field should match "%"
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "%", 1)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({""})).value());

    // empty field should NOT match "_"
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "_", 1)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({""})).value());

    // empty field should NOT match a fixed pattern "abc"
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "abc", 3)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({""})).value());
}

TEST_F(PredicateTest, TestLikeMinLenExceedsField) {
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // pattern "abcdef" requires 6 literal chars, field "ab" has only 2
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "abcdef", 6)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"ab"})).value());

    // pattern "a_b_c" requires 3 literal chars, wildcards = min_len 3, field "ab" has 2
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a_b_c", 5)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"ab"})).value());

    // field length equals min_len should still be possible to match
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"axbxc"})).value());
}

TEST_F(PredicateTest, TestLikeLongPatternHeapAlloc) {
    // pattern length > STACK_LIMIT(128) uses heap allocation
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // Build a pattern with > 128 characters: "a" + 130 * "_" + "z"
    std::string long_pattern = "a";
    for (int i = 0; i < 130; ++i) {
        long_pattern += '_';
    }
    long_pattern += 'z';

    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, long_pattern.data(), long_pattern.size())));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);

    // Build a matching field: "a" + 130 * "x" + "z"
    std::string matching_field = "a";
    for (int i = 0; i < 130; ++i) {
        matching_field += 'x';
    }
    matching_field += 'z';
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({matching_field})).value());

    // Non-matching: wrong ending
    std::string non_matching_field = "a";
    for (int i = 0; i < 130; ++i) {
        non_matching_field += 'x';
    }
    non_matching_field += 'y';
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({non_matching_field})).value());
}

TEST_F(PredicateTest, TestLikeInvalidEscapeSequence) {
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // Trailing backslash is invalid (Java throws "Invalid escape sequence")
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "abc\\", 4)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_NOK_WITH_MSG(predicate->Test(arrow_schema, CreateStringRow({"abc"})),
                        "Invalid escape sequence");

    // Backslash followed by non-special char is invalid (only \_, \%, \\ are legal)
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a\\bc", 4)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_NOK_WITH_MSG(predicate->Test(arrow_schema, CreateStringRow({"abc"})),
                        "Invalid escape sequence");

    // \n is not a valid escape
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a\\nf", 4)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_NOK_WITH_MSG(predicate->Test(arrow_schema, CreateStringRow({"anf"})),
                        "Invalid escape sequence");
}

TEST_F(PredicateTest, TestLikeEscapeBackslash) {
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // \\\\ in C++ string literal = "\\" in the pattern = escaped backslash
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a\\\\b", 4)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    // Field "a\b" should match pattern "a\\b" (escaped backslash)
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"a\\b"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"axb"})).value());

    // Escaped percent: "a\%b" matches literal "a%b"
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a\\%b", 4)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"a%b"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"axb"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"axxb"})).value());
}

TEST_F(PredicateTest, TestLikeUtf8MultibyteUnderscore) {
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // Single '_' should match one Unicode character, not one byte.
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "_", 1)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"中"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"中文"})).value());

    // "a_c" where _ matches one Chinese character
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "a_c", 3)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"a中c"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"a中文c"})).value());

    // "___" should match exactly 3 Unicode characters
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "___", 3)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"中文字"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"中文"})).value());

    // '%' should still work with multi-byte characters
    std::string pattern_contains = std::string("%") + "中" + "%";
    ASSERT_OK_AND_ASSIGN(
        predicate_base,
        PredicateBuilder::Like(
            /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
            Literal(FieldType::STRING, pattern_contains.data(), pattern_contains.size())));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"hello中world"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"helloworld"})).value());
}

TEST_F(PredicateTest, TestLikeJavaRegexLineTerminatorSemantics) {
    auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", arrow::utf8())}));

    // Java regex '.' does not match line terminators, so '_' should not match them either.
    ASSERT_OK_AND_ASSIGN(auto predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "_", 1)));
    auto predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"\n"})).value());
    ASSERT_FALSE(predicate->Test(arrow_schema, CreateStringRow({"\r"})).value());

    // Java LIKE '%' uses (?s:.*), so it should still match line terminators.
    ASSERT_OK_AND_ASSIGN(predicate_base,
                         PredicateBuilder::Like(
                             /*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                             Literal(FieldType::STRING, "%", 1)));
    predicate = std::dynamic_pointer_cast<PredicateFilter>(predicate_base);
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"\n"})).value());
    ASSERT_TRUE(predicate->Test(arrow_schema, CreateStringRow({"\r"})).value());
}

TEST_F(PredicateTest, TestCompound) {
    ASSERT_OK_AND_ASSIGN(
        const auto startswith_predicate,
        PredicateBuilder::StartsWith(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                                     Literal(FieldType::STRING, "aab", 3)));
    ASSERT_OK_AND_ASSIGN(
        const auto endswith_predicate,
        PredicateBuilder::EndsWith(/*field_index=*/0, /*field_name=*/"f0", FieldType::STRING,
                                   Literal(FieldType::STRING, "bcc", 3)));
    ASSERT_OK_AND_ASSIGN(const auto compound_predicate,
                         PredicateBuilder::And({startswith_predicate, endswith_predicate}));
    ASSERT_NOK_WITH_MSG(
        PredicateBuilder::Not(compound_predicate),
        "Could not construct A NOT predicate from And([StartsWith(f0, aab), EndsWith(f0, bcc)])");

    // with stats: And of StartsWith + EndsWith
    auto compound_filter = std::dynamic_pointer_cast<PredicateFilter>(compound_predicate);
    ASSERT_TRUE(compound_filter);
    // min="aab", max="aaz" covers StartsWith("aab"), EndsWith("bcc") returns true by default
    ASSERT_TRUE(StringStatsCheck(*compound_filter, 3ll, {StringFieldStats("aab", "aaz", 0ll)}));
    // min="bbb", max="ccc" does not cover StartsWith("aab")
    ASSERT_FALSE(StringStatsCheck(*compound_filter, 3ll, {StringFieldStats("bbb", "ccc", 0ll)}));
    // all nulls
    ASSERT_FALSE(StringStatsCheck(*compound_filter, 1ll,
                                  {StringFieldStats(std::nullopt, std::nullopt, 1ll)}));
}

TEST_F(PredicateTest, TestPredicateToString) {
    {
        auto predicate = PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                 FieldType::BIGINT, Literal(5l));
        ASSERT_EQ(predicate->ToString(), "Equal(f0, 5)");
    }
    {
        auto predicate = PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"f0",
                                                       FieldType::BIGINT, Literal(5l));
        ASSERT_EQ(predicate->ToString(), "GreaterThan(f0, 5)");
    }
    {
        auto predicate =
            PredicateBuilder::IsNotNull(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT);
        ASSERT_EQ(predicate->ToString(), "IsNotNull(f0)");
    }
    {
        std::vector<Literal> literals;
        literals.reserve(30);
        for (int64_t i = 1; i <= 21; i++) {
            literals.emplace_back(i);
        }
        auto predicate = PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0",
                                              FieldType::BIGINT, literals);
        ASSERT_TRUE(predicate);
        ASSERT_EQ(
            predicate->ToString(),
            "In(f0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])");
    }
    {
        std::vector<Literal> literals;
        literals.reserve(30);
        for (int64_t i = 1; i <= 21; i++) {
            literals.emplace_back(i);
        }
        auto predicate = PredicateBuilder::NotIn(/*field_index=*/0, /*field_name=*/"f0",
                                                 FieldType::BIGINT, literals);
        ASSERT_TRUE(predicate);
        ASSERT_EQ(predicate->ToString(),
                  "NotIn(f0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, "
                  "20, 21])");
    }

    {
        ASSERT_OK_AND_ASSIGN(
            auto predicate,
            PredicateBuilder::And({PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                           FieldType::BIGINT, Literal(3l)),
                                   PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
                                                           FieldType::BIGINT, Literal(5l))}));
        ASSERT_EQ(predicate->ToString(), "And([Equal(f0, 3), Equal(f1, 5)])");
    }
    {
        ASSERT_OK_AND_ASSIGN(
            auto predicate,
            PredicateBuilder::Or({PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0",
                                                          FieldType::BIGINT, Literal(3l)),
                                  PredicateBuilder::Equal(/*field_index=*/1, /*field_name=*/"f1",
                                                          FieldType::BIGINT, Literal(5l))}));
        ASSERT_EQ(predicate->ToString(), "Or([Equal(f0, 3), Equal(f1, 5)])");
    }
}

TEST_F(PredicateTest, TestBuildAndOr) {
    {
        // literals cannot be empty
        ASSERT_NOK(PredicateBuilder::Or({}));
    }
    {
        // literals cannot be empty
        ASSERT_NOK(PredicateBuilder::And({}));
    }
}
}  // namespace paimon::test
