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

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 404e5d9b perf(predicate): evaluate IN and NOT IN with 
arrow::compute::is_in (#265)
404e5d9b is described below

commit 404e5d9b8e6ccafd8e65a0804a734ee65eda1e06
Author: Yonghao Fang <[email protected]>
AuthorDate: Wed Sep 2 16:48:49 2026 +0800

    perf(predicate): evaluate IN and NOT IN with arrow::compute::is_in (#265)
---
 src/paimon/CMakeLists.txt                          |   2 +
 src/paimon/common/predicate/and.h                  |   8 +-
 src/paimon/common/predicate/compound_function.h    |   8 +-
 .../common/predicate/compound_predicate_impl.h     |   5 +-
 src/paimon/common/predicate/leaf_function.h        |   5 +-
 src/paimon/common/predicate/leaf_predicate_impl.h  |   5 +-
 src/paimon/common/predicate/leaf_unary_function.h  |   4 +-
 src/paimon/common/predicate/literal_converter.cpp  | 167 +++++++
 src/paimon/common/predicate/literal_converter.h    |  24 +
 .../common/predicate/literal_converter_test.cpp    | 157 ++++++
 .../predicate/multi_literals_leaf_function.cpp     | 245 ++++++++++
 .../predicate/multi_literals_leaf_function.h       |  21 +-
 .../multi_literals_leaf_function_test.cpp          | 524 +++++++++++++++++++++
 .../predicate/null_false_leaf_binary_function.h    |   4 +-
 src/paimon/common/predicate/or.h                   |   8 +-
 src/paimon/common/predicate/predicate_filter.h     |   5 +-
 src/paimon/common/predicate/predicate_test.cpp     | 193 ++++++--
 .../late_materializing_file_batch_reader.cpp       |   3 +-
 .../common/reader/predicate_batch_reader.cpp       |   6 +-
 19 files changed, 1323 insertions(+), 71 deletions(-)

diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt
index 54b5aafb..4aca72ab 100644
--- a/src/paimon/CMakeLists.txt
+++ b/src/paimon/CMakeLists.txt
@@ -125,6 +125,7 @@ set(PAIMON_COMMON_SRCS
     common/predicate/like.cpp
     common/predicate/literal_converter.cpp
     common/predicate/literal.cpp
+    common/predicate/multi_literals_leaf_function.cpp
     common/predicate/not_equal.cpp
     common/predicate/not_in.cpp
     common/predicate/or.cpp
@@ -609,6 +610,7 @@ if(PAIMON_BUILD_TESTS)
                     common/options/time_duration_test.cpp
                     common/predicate/literal_converter_test.cpp
                     common/predicate/literal_test.cpp
+                    common/predicate/multi_literals_leaf_function_test.cpp
                     common/predicate/predicate_test.cpp
                     common/predicate/predicate_utils_test.cpp
                     common/predicate/predicate_validator_test.cpp
diff --git a/src/paimon/common/predicate/and.h 
b/src/paimon/common/predicate/and.h
index b3663ea8..f0da67d7 100644
--- a/src/paimon/common/predicate/and.h
+++ b/src/paimon/common/predicate/and.h
@@ -49,9 +49,9 @@ class And : public CompoundFunction {
         return instance;
     }
 
-    Result<std::vector<char>> Test(
-        const arrow::Array& array,
-        const std::vector<std::shared_ptr<Predicate>>& children) const 
override {
+    Result<std::vector<char>> Test(const arrow::Array& array,
+                                   const 
std::vector<std::shared_ptr<Predicate>>& children,
+                                   arrow::MemoryPool* pool) const override {
         std::vector<char> is_valid(array.length(), true);
         for (const auto& child : children) {
             auto child_filter = 
std::dynamic_pointer_cast<PredicateFilter>(child);
@@ -59,7 +59,7 @@ class And : public CompoundFunction {
                 return Status::Invalid(
                     fmt::format("child filter {} does not support Test", 
child->ToString()));
             }
-            PAIMON_ASSIGN_OR_RAISE(std::vector<char> child_valid, 
child_filter->Test(array));
+            PAIMON_ASSIGN_OR_RAISE(std::vector<char> child_valid, 
child_filter->Test(array, pool));
             for (size_t i = 0; i < is_valid.size(); i++) {
                 is_valid[i] = (is_valid[i] & child_valid[i]);
             }
diff --git a/src/paimon/common/predicate/compound_function.h 
b/src/paimon/common/predicate/compound_function.h
index f093fc40..e32a3188 100644
--- a/src/paimon/common/predicate/compound_function.h
+++ b/src/paimon/common/predicate/compound_function.h
@@ -22,6 +22,7 @@
 #include <vector>
 
 #include "arrow/array/array_base.h"
+#include "arrow/type_fwd.h"
 #include "paimon/common/data/internal_array.h"
 #include "paimon/common/data/internal_row.h"
 #include "paimon/predicate/function.h"
@@ -32,9 +33,10 @@ namespace paimon {
 class CompoundFunction : public Function {
  public:
     // input array is the struct array of all fields
-    virtual Result<std::vector<char>> Test(
-        const arrow::Array& array,
-        const std::vector<std::shared_ptr<Predicate>>& children) const = 0;
+    // `pool` is where any arrow buffer the evaluation allocates comes from, 
it must not be null.
+    virtual Result<std::vector<char>> Test(const arrow::Array& array,
+                                           const 
std::vector<std::shared_ptr<Predicate>>& children,
+                                           arrow::MemoryPool* pool) const = 0;
 
     virtual Result<bool> Test(const std::shared_ptr<arrow::Schema>& schema, 
const InternalRow& row,
                               const std::vector<std::shared_ptr<Predicate>>& 
children) const = 0;
diff --git a/src/paimon/common/predicate/compound_predicate_impl.h 
b/src/paimon/common/predicate/compound_predicate_impl.h
index 3cf6d9c7..7ab5b613 100644
--- a/src/paimon/common/predicate/compound_predicate_impl.h
+++ b/src/paimon/common/predicate/compound_predicate_impl.h
@@ -32,8 +32,9 @@ class CompoundPredicateImpl : public CompoundPredicate, 
public PredicateFilter {
                           const std::vector<std::shared_ptr<Predicate>>& 
children)
         : CompoundPredicate(compound_function, children) {}
 
-    Result<std::vector<char>> Test(const arrow::Array& array) const override {
-        return compound_function_.Test(array, children_);
+    Result<std::vector<char>> Test(const arrow::Array& array,
+                                   arrow::MemoryPool* pool) const override {
+        return compound_function_.Test(array, children_, pool);
     }
 
     Result<bool> Test(const std::shared_ptr<arrow::Schema>& schema,
diff --git a/src/paimon/common/predicate/leaf_function.h 
b/src/paimon/common/predicate/leaf_function.h
index 9fdb4d8f..bdc83699 100644
--- a/src/paimon/common/predicate/leaf_function.h
+++ b/src/paimon/common/predicate/leaf_function.h
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "arrow/array/array_base.h"
+#include "arrow/type_fwd.h"
 #include "paimon/predicate/function.h"
 #include "paimon/predicate/literal.h"
 #include "paimon/status.h"
@@ -28,8 +29,10 @@ namespace paimon {
 class LeafFunction : public Function {
  public:
     // input array is the exact single field array
+    // `pool` is where any arrow buffer the evaluation allocates comes from, 
it must not be null.
     virtual Result<std::vector<char>> Test(const arrow::Array& array,
-                                           const std::vector<Literal>& 
literals) const = 0;
+                                           const std::vector<Literal>& 
literals,
+                                           arrow::MemoryPool* pool) const = 0;
 
     virtual Result<bool> Test(const Literal& value, const 
std::vector<Literal>& literals) const = 0;
 
diff --git a/src/paimon/common/predicate/leaf_predicate_impl.h 
b/src/paimon/common/predicate/leaf_predicate_impl.h
index c9a8758f..0d298409 100644
--- a/src/paimon/common/predicate/leaf_predicate_impl.h
+++ b/src/paimon/common/predicate/leaf_predicate_impl.h
@@ -41,7 +41,8 @@ class LeafPredicateImpl : public LeafPredicate, public 
PredicateFilter {
         return leaf_function_;
     }
 
-    Result<std::vector<char>> Test(const arrow::Array& array) const override {
+    Result<std::vector<char>> Test(const arrow::Array& array,
+                                   arrow::MemoryPool* pool) const override {
         const auto& struct_array = checked_cast<const 
arrow::StructArray&>(array);
         if (field_index_ >= 
static_cast<int32_t>(struct_array.fields().size())) {
             return Status::Invalid(
@@ -49,7 +50,7 @@ class LeafPredicateImpl : public LeafPredicate, public 
PredicateFilter {
                             struct_array.fields().size()));
         }
         const auto& field_array = struct_array.field(field_index_);
-        return leaf_function_.Test(*field_array, literals_);
+        return leaf_function_.Test(*field_array, literals_, pool);
     }
 
     Result<bool> Test(const std::shared_ptr<arrow::Schema>& schema,
diff --git a/src/paimon/common/predicate/leaf_unary_function.h 
b/src/paimon/common/predicate/leaf_unary_function.h
index 544ce673..04a939ae 100644
--- a/src/paimon/common/predicate/leaf_unary_function.h
+++ b/src/paimon/common/predicate/leaf_unary_function.h
@@ -30,8 +30,8 @@
 namespace paimon {
 class LeafUnaryFunction : public LeafFunction {
  public:
-    Result<std::vector<char>> Test(const arrow::Array& array,
-                                   const std::vector<Literal>& literals) const 
override {
+    Result<std::vector<char>> Test(const arrow::Array& array, const 
std::vector<Literal>& literals,
+                                   arrow::MemoryPool* pool) const override {
         std::vector<char> is_valid(array.length(), false);
         PAIMON_ASSIGN_OR_RAISE(
             std::vector<Literal> array_values,
diff --git a/src/paimon/common/predicate/literal_converter.cpp 
b/src/paimon/common/predicate/literal_converter.cpp
index 102348d7..4129fe70 100644
--- a/src/paimon/common/predicate/literal_converter.cpp
+++ b/src/paimon/common/predicate/literal_converter.cpp
@@ -25,12 +25,18 @@
 #include "arrow/array/array_decimal.h"
 #include "arrow/array/array_dict.h"
 #include "arrow/array/array_primitive.h"
+#include "arrow/array/builder_binary.h"
+#include "arrow/array/builder_decimal.h"
+#include "arrow/array/builder_primitive.h"
+#include "arrow/array/builder_time.h"
+#include "arrow/memory_pool.h"
 #include "arrow/type.h"
 #include "arrow/type_traits.h"
 #include "arrow/util/decimal.h"
 #include "fmt/format.h"
 #include "paimon/common/data/binary_string.h"
 #include "paimon/common/data/internal_row.h"
+#include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/common/utils/checked_cast.h"
 #include "paimon/common/utils/date_time_utils.h"
 #include "paimon/common/utils/field_type_utils.h"
@@ -284,4 +290,165 @@ std::vector<Literal> 
LiteralConverter::GetLiteralFromTimestampArray(const arrow:
     }
     return literals;
 }
+
+arrow::TimeUnit::type LiteralConverter::MinRequiredTimeUnit(const 
std::vector<Literal>& literals) {
+    bool needs_micro = false;
+    for (const auto& literal : literals) {
+        if (literal.IsNull()) {
+            continue;
+        }
+        const int32_t nano = 
literal.GetValue<Timestamp>().GetNanoOfMillisecond();
+        if (nano % 1000 != 0) {
+            return arrow::TimeUnit::NANO;
+        }
+        needs_micro = needs_micro || nano != 0;
+    }
+    return needs_micro ? arrow::TimeUnit::MICRO : arrow::TimeUnit::MILLI;
+}
+
+namespace {
+// Appends every literal, read by `extract`, to a `BuilderType`, keeping a 
null literal a null.
+template <typename BuilderType, typename Extract>
+Result<std::shared_ptr<arrow::Array>> BuildArray(const std::vector<Literal>& 
literals,
+                                                 Extract extract, 
arrow::MemoryPool* pool) {
+    BuilderType builder(pool);
+    
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Reserve(static_cast<int64_t>(literals.size())));
+    for (const auto& literal : literals) {
+        if (literal.IsNull()) {
+            PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.AppendNull());
+            continue;
+        }
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Append(extract(literal)));
+    }
+    std::shared_ptr<arrow::Array> array;
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&array));
+    return array;
+}
+
+// Writes the literals to a decimal array of the precision and the scale they 
carry themselves. One
+// array holds one arrow type, so a non null literal is needed to settle it, 
and every non null
+// literal has to carry the same pair: rescaling a value to another scale 
loses digits or overflows,
+// which is not something a conversion decides on its own.
+Result<std::shared_ptr<arrow::Array>> BuildDecimalArray(const 
std::vector<Literal>& literals,
+                                                        arrow::MemoryPool* 
pool) {
+    std::optional<Decimal> typed_value;
+    for (const auto& literal : literals) {
+        if (!literal.IsNull()) {
+            typed_value = literal.GetValue<Decimal>();
+            break;
+        }
+    }
+    if (typed_value == std::nullopt) {
+        return Status::Invalid(
+            "Not support converting literals of DECIMAL type to an arrow array 
without a non null "
+            "literal to take the precision and the scale from");
+    }
+    int32_t precision = typed_value->Precision();
+    int32_t scale = typed_value->Scale();
+    // `arrow::decimal128` checks the precision fatally, `Make` reports it 
instead.
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::DataType> type,
+                                      arrow::Decimal128Type::Make(precision, 
scale));
+    arrow::Decimal128Builder builder(type, pool);
+    
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Reserve(static_cast<int64_t>(literals.size())));
+    for (const auto& literal : literals) {
+        if (literal.IsNull()) {
+            PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.AppendNull());
+            continue;
+        }
+        auto value = literal.GetValue<Decimal>();
+        if (value.Precision() != precision || value.Scale() != scale) {
+            return Status::Invalid(fmt::format(
+                "Not support converting literals of DECIMAL type to an arrow 
array, {} and {} do "
+                "not share one precision and scale",
+                typed_value->ToString(), value.ToString()));
+        }
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(
+            builder.Append(arrow::Decimal128(value.HighBits(), 
value.LowBits())));
+    }
+    std::shared_ptr<arrow::Array> array;
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&array));
+    return array;
+}
+
+// Writes the literals to a timestamp array of the finest time unit that keeps 
every value, one
+// `is_in` can only compare against a column of that very unit. One array 
holds one arrow type, so
+// a non null literal is needed to settle the unit.
+Result<std::shared_ptr<arrow::Array>> BuildTimestampArray(const 
std::vector<Literal>& literals,
+                                                          arrow::MemoryPool* 
pool) {
+    bool has_value = false;
+    for (const auto& literal : literals) {
+        if (!literal.IsNull()) {
+            has_value = true;
+            break;
+        }
+    }
+    if (!has_value) {
+        return Status::Invalid(
+            "Not support converting literals of TIMESTAMP type to an arrow 
array without a non "
+            "null literal to take the time unit from");
+    }
+    arrow::TimestampBuilder builder(
+        arrow::timestamp(LiteralConverter::MinRequiredTimeUnit(literals)), 
pool);
+    const DateTimeUtils::TimeType time_type = 
DateTimeUtils::GetTimeTypeFromArrowType(
+        checked_pointer_cast<arrow::TimestampType>(builder.type()));
+    
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Reserve(static_cast<int64_t>(literals.size())));
+    for (const auto& literal : literals) {
+        if (literal.IsNull()) {
+            PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.AppendNull());
+            continue;
+        }
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Append(
+            DateTimeUtils::TimestampToInteger(literal.GetValue<Timestamp>(), 
time_type)));
+    }
+    std::shared_ptr<arrow::Array> array;
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&array));
+    return array;
+}
+}  // namespace
+
+Result<std::shared_ptr<arrow::Array>> LiteralConverter::ConvertLiteralsToArray(
+    const FieldType& field_type, const std::vector<Literal>& literals, 
arrow::MemoryPool* pool) {
+    switch (field_type) {
+        case FieldType::BOOLEAN:
+            return BuildArray<arrow::BooleanBuilder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<bool>(); }, pool);
+        case FieldType::TINYINT:
+            return BuildArray<arrow::Int8Builder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<int8_t>(); }, pool);
+        case FieldType::SMALLINT:
+            return BuildArray<arrow::Int16Builder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<int16_t>(); }, pool);
+        case FieldType::INT:
+            return BuildArray<arrow::Int32Builder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<int32_t>(); }, pool);
+        case FieldType::BIGINT:
+            return BuildArray<arrow::Int64Builder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<int64_t>(); }, pool);
+        case FieldType::FLOAT:
+            return BuildArray<arrow::FloatBuilder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<float>(); }, pool);
+        case FieldType::DOUBLE:
+            return BuildArray<arrow::DoubleBuilder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<double>(); }, pool);
+        case FieldType::DATE:
+            return BuildArray<arrow::Date32Builder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<int32_t>(); }, pool);
+        case FieldType::STRING:
+            return BuildArray<arrow::StringBuilder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<std::string>(); },
+                pool);
+        case FieldType::BINARY:
+            return BuildArray<arrow::BinaryBuilder>(
+                literals, [](const Literal& literal) { return 
literal.GetValue<std::string>(); },
+                pool);
+        case FieldType::DECIMAL:
+            return BuildDecimalArray(literals, pool);
+        case FieldType::TIMESTAMP:
+            return BuildTimestampArray(literals, pool);
+        default:
+            return Status::Invalid(
+                fmt::format("Not support converting literals of {} type to an 
arrow array",
+                            FieldTypeUtils::FieldTypeToString(field_type)));
+    }
+}
 }  // namespace paimon
diff --git a/src/paimon/common/predicate/literal_converter.h 
b/src/paimon/common/predicate/literal_converter.h
index 09739533..a3fde588 100644
--- a/src/paimon/common/predicate/literal_converter.h
+++ b/src/paimon/common/predicate/literal_converter.h
@@ -27,6 +27,7 @@
 #include <vector>
 
 #include "arrow/array/array_dict.h"
+#include "arrow/type_fwd.h"
 #include "arrow/type_traits.h"
 #include "paimon/common/utils/checked_cast.h"
 #include "paimon/predicate/literal.h"
@@ -50,6 +51,29 @@ class PAIMON_EXPORT LiteralConverter {
     static Result<std::vector<Literal>> ConvertLiteralsFromArray(const 
arrow::Array& array,
                                                                  bool 
own_data);
 
+    /// Collects the literals into an arrow array, the reverse of 
`ConvertLiteralsFromArray`.
+    ///
+    /// @param field_type The field type shared by every literal, it picks the 
arrow type.
+    /// @param literals The literals to convert, a null literal is written as 
a null, so the result
+    ///                 has one entry per literal.
+    /// @param pool The pool every buffer of the result is allocated from, it 
must not be null.
+    /// @return `Status::Invalid` for a field type this does not write, which 
is every one outside
+    ///         `BOOLEAN`, `TINYINT`, `SMALLINT`, `INT`, `BIGINT`, `FLOAT`, 
`DOUBLE`, `DATE`,
+    ///         `STRING`, `BINARY`, `DECIMAL` and `TIMESTAMP`. `DECIMAL` is 
written with the
+    ///         precision and the scale its literals carry, so at least one 
literal has to be non
+    ///         null and every non null one has to carry the same pair. 
`TIMESTAMP` is written with
+    ///         the finest time unit that keeps every value, so at least one 
literal has to be non
+    ///         null to settle the unit.
+    static Result<std::shared_ptr<arrow::Array>> ConvertLiteralsToArray(
+        const FieldType& field_type, const std::vector<Literal>& literals, 
arrow::MemoryPool* pool);
+
+    /// The finest time unit any of the non null timestamp literals needs to 
keep its value, which
+    /// is the unit `ConvertLiteralsToArray` writes them with. A null literal 
does not constrain the
+    /// unit, and literals that are all null leave it at the coarsest one.
+    ///
+    /// Every literal has to be of `TIMESTAMP` type.
+    static arrow::TimeUnit::type MinRequiredTimeUnit(const 
std::vector<Literal>& literals);
+
     static Result<Literal> ConvertLiteralsFromString(const FieldType& type,
                                                      const std::string& 
value_str);
 
diff --git a/src/paimon/common/predicate/literal_converter_test.cpp 
b/src/paimon/common/predicate/literal_converter_test.cpp
index 7d03f9cb..0d36cfe7 100644
--- a/src/paimon/common/predicate/literal_converter_test.cpp
+++ b/src/paimon/common/predicate/literal_converter_test.cpp
@@ -77,6 +77,17 @@ class LiteralConverterTest : public ::testing::Test {
             ASSERT_EQ(result, expected[i]);
         }
     }
+
+    void CheckLiteralsToArray(const FieldType& type, const 
std::vector<Literal>& literals,
+                              const std::shared_ptr<arrow::Array>& expected) 
const {
+        ASSERT_OK_AND_ASSIGN(
+            std::shared_ptr<arrow::Array> array,
+            LiteralConverter::ConvertLiteralsToArray(type, literals, 
arrow::default_memory_pool()));
+        ASSERT_TRUE(array->Equals(*expected))
+            << "actual: " << array->ToString() << ", expected: " << 
expected->ToString();
+        // the array reads back as the literals it was built from, the null 
ones included
+        CheckResult(array, literals);
+    }
 };
 
 TEST_F(LiteralConverterTest, TestBooleanLiteral) {
@@ -438,4 +449,150 @@ TEST_F(LiteralConverterTest, TestDictType) {
                      Literal(FieldType::STRING, "foo", 3), 
Literal(FieldType::STRING)}));
 }
 
+TEST_F(LiteralConverterTest, TestLiteralsToArray) {
+    // Every writable field type converts to an array and back, the null 
literal included, so each
+    // case asserts `ConvertLiteralsToArray` and `ConvertLiteralsFromArray` 
agree on every type.
+    CheckLiteralsToArray(
+        FieldType::BOOLEAN, {Literal(true), Literal(FieldType::BOOLEAN), 
Literal(false)},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::boolean(), R"([true, 
null, false])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::TINYINT,
+        {Literal(static_cast<int8_t>(4)), Literal(FieldType::TINYINT),
+         Literal(static_cast<int8_t>(-5))},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int8(), R"([4, null, 
-5])").ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::SMALLINT,
+        {Literal(static_cast<int16_t>(45)), Literal(FieldType::SMALLINT),
+         Literal(static_cast<int16_t>(-55))},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int16(), R"([45, 
null, -55])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::INT, {Literal(456), Literal(FieldType::INT), Literal(-567)},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([456, 
null, -567])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::BIGINT, {Literal(4l), Literal(FieldType::BIGINT), 
Literal(-5l)},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), R"([4, null, 
-5])").ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::FLOAT, {Literal(4.5f), Literal(FieldType::FLOAT), 
Literal(-5.25f)},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::float32(), R"([4.5, 
null, -5.25])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::DOUBLE, {Literal(4.5), Literal(FieldType::DOUBLE), 
Literal(-5.25)},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::float64(), R"([4.5, 
null, -5.25])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::DATE,
+        {Literal(FieldType::DATE, 0), Literal(FieldType::DATE), 
Literal(FieldType::DATE, -5)},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::date32(), R"([0, 
null, -5])")
+            .ValueOrDie());
+    std::string str = "苹果";
+    CheckLiteralsToArray(
+        FieldType::STRING,
+        {Literal(FieldType::STRING, "apple", 5), Literal(FieldType::STRING),
+         Literal(FieldType::STRING, str.data(), str.size())},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"(["apple", 
null, "苹果"])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::BINARY,
+        {Literal(FieldType::BINARY, "apple", 5), Literal(FieldType::BINARY),
+         Literal(FieldType::BINARY, str.data(), str.size())},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::binary(), 
R"(["apple", null, "苹果"])")
+            .ValueOrDie());
+    // a decimal is written with the precision and the scale its literals carry
+    CheckLiteralsToArray(
+        FieldType::DECIMAL,
+        {Literal(Decimal(21, 3, 
DecimalUtils::StrToInt128("-123456789987654321234").value())),
+         Literal(FieldType::DECIMAL), Literal(Decimal(21, 3, 123456))},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::decimal128(21, 3),
+                                                  
R"(["-123456789987654321.234", null, "123.456"])")
+            .ValueOrDie());
+    // a timestamp is written with the finest time unit that keeps the values, 
one per case, and
+    // the JSON reader takes a raw integer in the unit of the type it is given
+    CheckLiteralsToArray(
+        FieldType::TIMESTAMP,
+        {Literal(Timestamp::FromEpochMillis(123000l)), 
Literal(FieldType::TIMESTAMP),
+         Literal(Timestamp::FromEpochMillis(-456000l))},
+        
arrow::ipc::internal::json::ArrayFromJSON(arrow::timestamp(arrow::TimeUnit::MILLI),
+                                                  R"([123000, null, -456000])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::TIMESTAMP,
+        {Literal(Timestamp(123000l, 1000)), Literal(FieldType::TIMESTAMP),
+         Literal(Timestamp(-456000l, 2000))},
+        
arrow::ipc::internal::json::ArrayFromJSON(arrow::timestamp(arrow::TimeUnit::MICRO),
+                                                  R"([123000001, null, 
-455999998])")
+            .ValueOrDie());
+    CheckLiteralsToArray(
+        FieldType::TIMESTAMP,
+        {Literal(Timestamp(123000l, 456789)), Literal(FieldType::TIMESTAMP),
+         Literal(Timestamp(-456000l, 1))},
+        
arrow::ipc::internal::json::ArrayFromJSON(arrow::timestamp(arrow::TimeUnit::NANO),
+                                                  R"([123000456789, null, 
-455999999999])")
+            .ValueOrDie());
+}
+
+TEST_F(LiteralConverterTest, TestLiteralsToArrayWithoutValue) {
+    // no literal at all still gives an array typed after the field type
+    CheckLiteralsToArray(
+        FieldType::INT, {},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), 
R"([])").ValueOrDie());
+    // and a field type is all a null literal needs to be written
+    CheckLiteralsToArray(
+        FieldType::STRING, {Literal(FieldType::STRING), 
Literal(FieldType::STRING)},
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"([null, 
null])").ValueOrDie());
+}
+
+TEST_F(LiteralConverterTest, TestLiteralsToArrayUnsupportedType) {
+    // a timestamp takes its arrow type from a literal, so it needs one that 
is not null
+    
ASSERT_NOK_WITH_MSG(LiteralConverter::ConvertLiteralsToArray(FieldType::TIMESTAMP,
 {},
+                                                                 
arrow::default_memory_pool()),
+                        "without a non null literal");
+    ASSERT_NOK_WITH_MSG(
+        LiteralConverter::ConvertLiteralsToArray(
+            FieldType::TIMESTAMP, {Literal(FieldType::TIMESTAMP)}, 
arrow::default_memory_pool()),
+        "without a non null literal");
+    // a decimal takes its arrow type from a literal, so it needs one that is 
not null
+    
ASSERT_NOK_WITH_MSG(LiteralConverter::ConvertLiteralsToArray(FieldType::DECIMAL,
 {},
+                                                                 
arrow::default_memory_pool()),
+                        "without a non null literal");
+    ASSERT_NOK_WITH_MSG(
+        LiteralConverter::ConvertLiteralsToArray(FieldType::DECIMAL, 
{Literal(FieldType::DECIMAL)},
+                                                 arrow::default_memory_pool()),
+        "without a non null literal");
+    // and every non null literal has to carry the same precision and scale
+    ASSERT_NOK_WITH_MSG(
+        LiteralConverter::ConvertLiteralsToArray(
+            FieldType::DECIMAL, {Literal(Decimal(21, 3, 123456)), 
Literal(Decimal(21, 5, 123456))},
+            arrow::default_memory_pool()),
+        "do not share one precision and scale");
+    ASSERT_NOK_WITH_MSG(
+        LiteralConverter::ConvertLiteralsToArray(
+            FieldType::DECIMAL, {Literal(Decimal(21, 3, 123456)), 
Literal(Decimal(20, 3, 123456))},
+            arrow::default_memory_pool()),
+        "do not share one precision and scale");
+    // a precision arrow rejects is reported instead of checked fatally
+    ASSERT_NOK_WITH_MSG(
+        LiteralConverter::ConvertLiteralsToArray(FieldType::DECIMAL, 
{Literal(Decimal(0, 0, 1))},
+                                                 arrow::default_memory_pool()),
+        "Decimal precision out of range");
+    // the field types no caller asks for yet
+    ASSERT_NOK_WITH_MSG(
+        LiteralConverter::ConvertLiteralsToArray(FieldType::BLOB, {}, 
arrow::default_memory_pool()),
+        "Not support converting literals of BLOB type to an arrow array");
+    
ASSERT_NOK_WITH_MSG(LiteralConverter::ConvertLiteralsToArray(FieldType::ARRAY, 
{},
+                                                                 
arrow::default_memory_pool()),
+                        "Not support converting literals of ARRAY type to an 
arrow array");
+    ASSERT_NOK_WITH_MSG(
+        LiteralConverter::ConvertLiteralsToArray(FieldType::MAP, {}, 
arrow::default_memory_pool()),
+        "Not support converting literals of MAP type to an arrow array");
+    
ASSERT_NOK_WITH_MSG(LiteralConverter::ConvertLiteralsToArray(FieldType::STRUCT, 
{},
+                                                                 
arrow::default_memory_pool()),
+                        "Not support converting literals of STRUCT type to an 
arrow array");
+    
ASSERT_NOK_WITH_MSG(LiteralConverter::ConvertLiteralsToArray(FieldType::UNKNOWN,
 {},
+                                                                 
arrow::default_memory_pool()),
+                        "Not support converting literals of UNKNOWN");
+}
+
 }  // namespace paimon::test
diff --git a/src/paimon/common/predicate/multi_literals_leaf_function.cpp 
b/src/paimon/common/predicate/multi_literals_leaf_function.cpp
new file mode 100644
index 00000000..c8a41812
--- /dev/null
+++ b/src/paimon/common/predicate/multi_literals_leaf_function.cpp
@@ -0,0 +1,245 @@
+/*
+ * 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/common/predicate/multi_literals_leaf_function.h"
+
+#include <cmath>
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "arrow/array/array_base.h"
+#include "arrow/array/array_primitive.h"
+#include "arrow/compute/api_scalar.h"
+#include "arrow/compute/exec.h"
+#include "arrow/datum.h"
+#include "arrow/memory_pool.h"
+#include "arrow/type.h"
+#include "paimon/common/predicate/literal_converter.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/checked_cast.h"
+#include "paimon/data/decimal.h"
+
+namespace paimon {
+namespace {
+/// The field types `arrow::compute::is_in` can stand in for 
`Literal::CompareTo`. These are also
+/// the field types `LiteralConverter::ConvertLiteralsToArray` writes today, 
and the check keeps one
+/// it starts writing from reaching `is_in` on its own.
+///
+/// `TIMESTAMP` is parameterized by a time unit, but a timestamp literal 
settles one itself: the
+/// finest unit that keeps the values of the literals, which `MakeInValueSet` 
only probes against
+/// a column of that very unit. `DECIMAL` is parameterized too, by precision 
and scale, but a
+/// decimal literal carries both, and `MakeInValueSet` only probes a column 
that carries the same
+/// scale. `FLOAT` and `DOUBLE` agree on every value but NaN, which 
`MakeInValueSet` keeps off this
+/// path on its own.
+bool CanProbeWithIsIn(FieldType field_type) {
+    switch (field_type) {
+        case FieldType::BOOLEAN:
+        case FieldType::TINYINT:
+        case FieldType::SMALLINT:
+        case FieldType::INT:
+        case FieldType::BIGINT:
+        case FieldType::FLOAT:
+        case FieldType::DOUBLE:
+        case FieldType::DATE:
+        case FieldType::STRING:
+        case FieldType::BINARY:
+        case FieldType::DECIMAL:
+        case FieldType::TIMESTAMP:
+            return true;
+        default:
+            return false;
+    }
+}
+
+/// Whether `data_type` is the arrow type of a decimal column of `scale`.
+bool IsDecimalOfScale(const arrow::DataType& data_type, int32_t scale) {
+    return data_type.id() == arrow::Type::DECIMAL128 &&
+           checked_cast<const arrow::Decimal128Type&>(data_type).scale() == 
scale;
+}
+
+/// Whether `data_type` is the arrow type of a timestamp column of `unit` 
without a time zone.
+bool IsTimestampOfUnit(const arrow::DataType& data_type, arrow::TimeUnit::type 
unit) {
+    return data_type.id() == arrow::Type::TIMESTAMP &&
+           checked_cast<const arrow::TimestampType&>(data_type).unit() == unit 
&&
+           checked_cast<const 
arrow::TimestampType&>(data_type).timezone().empty();
+}
+
+/// Whether `literal` holds a floating point NaN.
+bool IsNanLiteral(const Literal& literal) {
+    switch (literal.GetType()) {
+        case FieldType::FLOAT:
+            return std::isnan(literal.GetValue<float>());
+        case FieldType::DOUBLE:
+            return std::isnan(literal.GetValue<double>());
+        default:
+            return false;
+    }
+}
+
+/// Builds the value set that `arrow::compute::is_in` takes from the literals 
of an `IN` / `NOT IN`
+/// predicate. The arrow type comes from the literals themselves, which all 
share one `FieldType`.
+///
+/// @param negate `false` for `IN`, `true` for `NOT IN`.
+/// @param data_type The arrow type of the column the predicate is evaluated 
against, which settles
+///                  whether a decimal or a timestamp value set can be probed 
against it.
+/// @param pool The pool the value set is allocated from.
+/// @return `nullptr` when the literals cannot be probed by `is_in`, which 
covers the field types
+///         `CanProbeWithIsIn` rejects, a NaN literal, a decimal column of 
another scale, a
+///         timestamp column of another unit or with a time zone, and `NOT IN` 
holding a null
+///         literal, which `NotIn::InnerTest` makes false for every row. This 
never fails.
+std::shared_ptr<arrow::Array> MakeInValueSet(const std::vector<Literal>& 
literals, bool negate,
+                                             const arrow::DataType& data_type,
+                                             arrow::MemoryPool* pool) {
+    if (literals.empty()) {
+        return nullptr;
+    }
+    // The literals of one predicate share a type, so take it from the first 
non-null one. When
+    // every literal is null the value set is all nulls, which `is_in` 
ignores, but it still needs a
+    // type to compare against the column, and a null `Literal` carries its 
type too.
+    const Literal* typed_literal = &literals.front();
+    for (const auto& literal : literals) {
+        if (!literal.IsNull()) {
+            typed_literal = &literal;
+            break;
+        }
+    }
+    const FieldType field_type = typed_literal->GetType();
+    if (!CanProbeWithIsIn(field_type)) {
+        return nullptr;
+    }
+    // A decimal value set carries the precision and the scale of its 
literals, and `is_in` compares
+    // it against a column of another scale by casting one side to the other. 
Such a cast fails on a
+    // value that does not fit the other scale, where `Literal::CompareTo` 
rescales one value at a
+    // time and merely finds no match, so only a column carrying the scale of 
the literals is
+    // probed. Nothing but null literals leaves no scale to compare and no 
arrow type to write.
+    if (field_type == FieldType::DECIMAL &&
+        (typed_literal->IsNull() ||
+         !IsDecimalOfScale(data_type, 
typed_literal->GetValue<Decimal>().Scale()))) {
+        return nullptr;
+    }
+    // A timestamp value set carries the time unit the values of the literals 
need, and `is_in`
+    // compares it against a column of another unit by casting one side to the 
other. Casting to a
+    // coarser unit fails on a value the unit does not keep, casting to a 
finer one can overflow
+    // int64, where `Literal::CompareTo` merely finds no match, so only a 
column of that very unit
+    // is probed. A column with a time zone is one as well: `is_in` refuses to 
compare a zoned
+    // timestamp against an unzoned one, and a literal has no zone to speak 
of. Nothing but null
+    // literals leaves no unit to compare and no arrow type to write.
+    if (field_type == FieldType::TIMESTAMP &&
+        (typed_literal->IsNull() ||
+         !IsTimestampOfUnit(data_type, 
LiteralConverter::MinRequiredTimeUnit(literals)))) {
+        return nullptr;
+    }
+    for (const auto& literal : literals) {
+        // A literal typed differently makes `Literal::CompareTo` fail, keep 
that on the row by row
+        // path.
+        if (!literal.IsNull() && literal.GetType() != field_type) {
+            return nullptr;
+        }
+        // `is_in` hashes the raw bits of a float, so a NaN literal would only 
match the column NaNs
+        // carrying the very same bit pattern, while 
`FieldsComparator::CompareFloatingPoint` makes
+        // every NaN equal. Keep a NaN literal on the row by row path. With 
none in the value set
+        // the two agree, because a column NaN then matches no literal either 
way.
+        if (!literal.IsNull() && IsNanLiteral(literal)) {
+            return nullptr;
+        }
+        // `NotIn::InnerTest` returns false as soon as it meets a null 
literal, so no row can match
+        // and there is nothing worth building.
+        if (negate && literal.IsNull()) {
+            return nullptr;
+        }
+    }
+    Result<std::shared_ptr<arrow::Array>> value_set =
+        LiteralConverter::ConvertLiteralsToArray(field_type, literals, pool);
+    // A failure only says the value set is not there, and the row by row path 
still is. Decimal
+    // literals of mixed precision and scale end up here, anything else is an 
arrow failure.
+    if (!value_set.ok()) {
+        return nullptr;
+    }
+    return std::move(value_set).value();
+}
+
+/// Probes every non-null row of `array` against `value_set`.
+///
+/// @param negate `false` for `IN` semantics, `true` for `NOT IN` semantics.
+/// @param pool The pool the match bitmap `is_in` writes is allocated from.
+/// @return One entry per row, with the null rows left at 0 because `IN` and 
`NOT IN` are both false
+///         on null.
+///
+/// `is_in` resolves the comparison itself: it decodes a dictionary column, 
and promotes both sides
+/// to their common type when the column is read as a wider or narrower arrow 
type than the one
+/// `value_set` was built with. It fails when the two types have no common 
type at all, which only
+/// happens when the field type disagrees with the column the predicate is 
evaluated against.
+Result<std::vector<char>> ProbeInValueSet(const arrow::Array& array, const 
arrow::Array& value_set,
+                                          bool negate, arrow::MemoryPool* 
pool) {
+    // `EMIT_NULL` ignores the nulls of the value set and turns a null input 
into a null output, so
+    // the validity of `matches` marks exactly the rows that `In` / `NotIn` 
consider null. That also
+    // covers a dictionary column, whose null rows come either from the 
indices or from a null
+    // dictionary value once `is_in` decodes it.
+    arrow::compute::SetLookupOptions options(value_set.data(),
+                                             
arrow::compute::SetLookupOptions::EMIT_NULL);
+    arrow::compute::ExecContext exec_context(pool);
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+        arrow::Datum matches, arrow::compute::IsIn(arrow::Datum(array), 
options, &exec_context));
+    // `make_array` hands out a new `shared_ptr` that owns the array, so it 
has to be held for as
+    // long as the array is read. Binding a reference straight to what it 
points at would drop the
+    // last owner at the end of the statement and leave that reference 
dangling.
+    std::shared_ptr<arrow::Array> matches_array = matches.make_array();
+    const auto& matched = checked_cast<const 
arrow::BooleanArray&>(*matches_array);
+    std::vector<char> is_valid(matched.length(), 0);
+    for (int64_t i = 0; i < matched.length(); i++) {
+        if (matched.IsNull(i)) {
+            // `IN` and `NOT IN` are both false on a null value, leave the row 
at 0.
+            continue;
+        }
+        is_valid[i] = static_cast<char>(matched.Value(i) != negate);
+    }
+    return is_valid;
+}
+}  // namespace
+
+Result<std::vector<char>> MultiLiteralsLeafFunction::Test(const arrow::Array& 
array,
+                                                          const 
std::vector<Literal>& literals,
+                                                          arrow::MemoryPool* 
pool) const {
+    const Function::Type type = GetType();
+    // `In` and `NotIn` are the only subclasses today. The check keeps a 
future one off this path,
+    // because `MakeInValueSet` only looks at the literals and would silently 
give it `IN`
+    // semantics.
+    if (type == Function::Type::IN || type == Function::Type::NOT_IN) {
+        const bool negate = type == Function::Type::NOT_IN;
+        std::shared_ptr<arrow::Array> value_set =
+            MakeInValueSet(literals, negate, *array.type(), pool);
+        if (value_set != nullptr) {
+            return ProbeInValueSet(array, *value_set, negate, pool);
+        }
+    }
+
+    // Materializing the column into `Literal` objects costs one heap 
allocation per row and then
+    // every row scans the literals linearly, so this only runs when `is_in` 
cannot probe them.
+    PAIMON_ASSIGN_OR_RAISE(std::vector<Literal> array_values,
+                           LiteralConverter::ConvertLiteralsFromArray(array, 
/*own_data=*/false));
+    std::vector<char> is_valid(array.length(), false);
+    for (int64_t i = 0; i < array.length(); i++) {
+        if (!array.IsNull(i)) {
+            PAIMON_ASSIGN_OR_RAISE(is_valid[i], Test(array_values[i], 
literals));
+        }
+    }
+    return is_valid;
+}
+
+}  // namespace paimon
diff --git a/src/paimon/common/predicate/multi_literals_leaf_function.h 
b/src/paimon/common/predicate/multi_literals_leaf_function.h
index 80446647..c94b2384 100644
--- a/src/paimon/common/predicate/multi_literals_leaf_function.h
+++ b/src/paimon/common/predicate/multi_literals_leaf_function.h
@@ -23,27 +23,18 @@
 #include "arrow/array/array_nested.h"
 #include "arrow/c/bridge.h"
 #include "paimon/common/predicate/leaf_function.h"
-#include "paimon/common/predicate/literal_converter.h"
-#include "paimon/common/utils/arrow/status_utils.h"
 #include "paimon/status.h"
 
 namespace paimon {
 
 class MultiLiteralsLeafFunction : public LeafFunction {
  public:
-    Result<std::vector<char>> Test(const arrow::Array& array,
-                                   const std::vector<Literal>& literals) const 
override {
-        PAIMON_ASSIGN_OR_RAISE(
-            std::vector<Literal> array_values,
-            LiteralConverter::ConvertLiteralsFromArray(array, 
/*own_data=*/false));
-        std::vector<char> is_valid(array.length(), false);
-        for (int64_t i = 0; i < array.length(); i++) {
-            if (!array.IsNull(i)) {
-                PAIMON_ASSIGN_OR_RAISE(is_valid[i], Test(array_values[i], 
literals));
-            }
-        }
-        return is_valid;
-    }
+    /// Probes the whole batch with `arrow::compute::is_in` when the literals 
allow it, and falls
+    /// back to comparing every row against every literal otherwise. Every 
`LeafFunction` is a
+    /// shared stateless singleton, so the value set is built per batch; that 
costs `O(literals)`
+    /// and buys an `O(rows)` probe.
+    Result<std::vector<char>> Test(const arrow::Array& array, const 
std::vector<Literal>& literals,
+                                   arrow::MemoryPool* pool) const override;
 
     Result<bool> Test(int64_t row_count, const Literal& min_value, const 
Literal& max_value,
                       const std::optional<int64_t>& null_count,
diff --git a/src/paimon/common/predicate/multi_literals_leaf_function_test.cpp 
b/src/paimon/common/predicate/multi_literals_leaf_function_test.cpp
new file mode 100644
index 00000000..981334ee
--- /dev/null
+++ b/src/paimon/common/predicate/multi_literals_leaf_function_test.cpp
@@ -0,0 +1,524 @@
+/*
+ * 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/common/predicate/multi_literals_leaf_function.h"
+
+#include <cmath>
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/array/array_dict.h"
+#include "arrow/ipc/json_simple.h"
+#include "gtest/gtest.h"
+#include "paimon/common/predicate/in.h"
+#include "paimon/common/predicate/literal_converter.h"
+#include "paimon/common/predicate/not_in.h"
+#include "paimon/common/utils/decimal_utils.h"
+#include "paimon/data/decimal.h"
+#include "paimon/data/timestamp.h"
+#include "paimon/defs.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+class MultiLiteralsLeafFunctionTest : public ::testing::Test {
+ public:
+    void SetUp() override {}
+    void TearDown() override {}
+
+    static Literal StringLiteral(const std::string& value) {
+        return Literal(FieldType::STRING, value.data(), value.size());
+    }
+
+    static Literal BinaryLiteral(const std::string& value) {
+        return Literal(FieldType::BINARY, value.data(), value.size());
+    }
+
+    // A decimal literal of `unscaled` as written by the digits of `precision` 
and `scale`.
+    static Literal DecimalLiteral(int32_t precision, int32_t scale, const 
std::string& unscaled) {
+        EXPECT_OK_AND_ASSIGN(Decimal::int128_t value, 
DecimalUtils::StrToInt128(unscaled));
+        return Literal(Decimal(precision, scale, value));
+    }
+
+    // A timestamp literal of `millis` since the epoch and `nanos` within the 
millisecond.
+    static Literal TimestampLiteral(int64_t millis, int32_t nanos = 0) {
+        return Literal(Timestamp::FromEpochMillis(millis, nanos));
+    }
+
+    // Evaluates the whole batch and returns the per row result, asserting the 
call succeeded.
+    static std::vector<char> Eval(const LeafFunction& function,
+                                  const std::vector<Literal>& literals,
+                                  const std::shared_ptr<arrow::Array>& array) {
+        EXPECT_OK_AND_ASSIGN(std::vector<char> is_valid,
+                             function.Test(*array, literals, 
arrow::default_memory_pool()));
+        return is_valid;
+    }
+
+    static std::vector<char> EvalIn(const std::vector<Literal>& literals,
+                                    const std::shared_ptr<arrow::Array>& 
array) {
+        return Eval(In::Instance(), literals, array);
+    }
+
+    static std::vector<char> EvalNotIn(const std::vector<Literal>& literals,
+                                       const std::shared_ptr<arrow::Array>& 
array) {
+        return Eval(NotIn::Instance(), literals, array);
+    }
+};
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestInt) {
+    const std::vector<Literal> literals = {Literal(1), Literal(2), Literal(3), 
Literal(5)};
+    auto array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([0, 1, 2, 
3, 4, 5, 6, null])")
+            .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({0, 1, 1, 1, 0, 1, 0, 
0}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({1, 0, 0, 0, 1, 0, 
1, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestBigInt) {
+    const std::vector<Literal> literals = {Literal(int64_t{-1000000}), 
Literal(int64_t{0}),
+                                           Literal(int64_t{1000000})};
+    auto array = arrow::ipc::internal::json::ArrayFromJSON(
+                     arrow::int64(), R"([-1000000, -999999, 0, 1, 1000000, 
null])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 1, 0, 1, 0}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 1, 0, 1, 0, 
0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestInt64Boundaries) {
+    const std::vector<Literal> literals = 
{Literal(std::numeric_limits<int64_t>::min()),
+                                           
Literal(std::numeric_limits<int64_t>::max())};
+    auto array = arrow::ipc::internal::json::ArrayFromJSON(
+                     arrow::int64(), R"([-9223372036854775808, 0, 
9223372036854775807, null])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 1, 0}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 1, 0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestTinyIntAndSmallInt) {
+    auto tinyint_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int8(), R"([-128, 
-127, 0, 126, 127])")
+            .ValueOrDie();
+    ASSERT_EQ(
+        EvalIn({Literal(int8_t{-128}), Literal(int8_t{0}), 
Literal(int8_t{127})}, tinyint_array),
+        std::vector<char>({1, 0, 1, 0, 1}));
+
+    auto smallint_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int16(), R"([-30000, 
0, 30000, null])")
+            .ValueOrDie();
+    ASSERT_EQ(EvalIn({Literal(int16_t{-30000}), Literal(int16_t{30000})}, 
smallint_array),
+              std::vector<char>({1, 0, 1, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestDate) {
+    const std::vector<Literal> literals = {Literal(FieldType::DATE, 100),
+                                           Literal(FieldType::DATE, 20000)};
+    auto array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::date32(), R"([100, 
101, 20000, null])")
+            .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 1, 0}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 1, 0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestBoolean) {
+    auto array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::boolean(), R"([true, 
false, null])")
+            .ValueOrDie();
+    ASSERT_EQ(EvalIn({Literal(true)}, array), std::vector<char>({1, 0, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(true)}, array), std::vector<char>({0, 1, 0}));
+
+    const std::vector<Literal> both = {Literal(true), Literal(false)};
+    ASSERT_EQ(EvalIn(both, array), std::vector<char>({1, 1, 0}));
+    ASSERT_EQ(EvalNotIn(both, array), std::vector<char>({0, 0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestString) {
+    const std::vector<Literal> literals = {StringLiteral("apple"), 
StringLiteral(""),
+                                           StringLiteral("banana")};
+    auto array = arrow::ipc::internal::json::ArrayFromJSON(
+                     arrow::utf8(), R"(["apple", "", "banana", "cherry", 
"app", null])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 1, 1, 0, 0, 0}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 0, 0, 1, 1, 
0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestStringWithoutEmptyLiteral) {
+    // An empty column value must not match when no empty literal was given.
+    auto array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"(["a", "", 
"b"])").ValueOrDie();
+    ASSERT_EQ(EvalIn({StringLiteral("a")}, array), std::vector<char>({1, 0, 
0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestBinary) {
+    // Binary literals keep their embedded zero bytes, the value set must not 
truncate them. The
+    // JSON reader cannot spell a value like that, so the column is built with 
a builder.
+    const std::vector<Literal> literals = 
{BinaryLiteral(std::string("\x00\x01", 2)),
+                                           BinaryLiteral("xyz")};
+
+    arrow::BinaryBuilder builder;
+    ASSERT_TRUE(builder.Append(std::string("\x00\x01", 2)).ok());
+    ASSERT_TRUE(builder.Append("xyz").ok());
+    ASSERT_TRUE(builder.Append("xyw").ok());
+    ASSERT_TRUE(builder.Append(std::string("\x00", 1)).ok());
+    ASSERT_TRUE(builder.AppendNull().ok());
+    std::shared_ptr<arrow::Array> array;
+    ASSERT_TRUE(builder.Finish(&array).ok());
+
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 1, 0, 0, 0}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 0, 1, 1, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestDictionaryString) {
+    const std::vector<Literal> literals = {StringLiteral("a"), 
StringLiteral("c")};
+    auto dictionary =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"(["a", "b", 
"c"])").ValueOrDie();
+    auto indices =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([0, 1, 2, 
2, null, 0])")
+            .ValueOrDie();
+    auto dict_type = arrow::dictionary(arrow::int32(), arrow::utf8());
+    std::shared_ptr<arrow::Array> array =
+        arrow::DictionaryArray::FromArrays(dict_type, indices, 
dictionary).ValueOrDie();
+
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 1, 1, 0, 1}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 1, 0, 0, 0, 
0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestLargeStringDictionary) {
+    auto dictionary =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::large_utf8(), 
R"(["a", "b", "c"])")
+            .ValueOrDie();
+    auto indices = arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), 
R"([0, 2, null, 1])")
+                       .ValueOrDie();
+    auto dict_type = arrow::dictionary(arrow::int64(), arrow::large_utf8());
+    std::shared_ptr<arrow::Array> array =
+        arrow::DictionaryArray::FromArrays(dict_type, indices, 
dictionary).ValueOrDie();
+
+    ASSERT_EQ(EvalIn({StringLiteral("c")}, array), std::vector<char>({0, 1, 0, 
0}));
+    ASSERT_EQ(EvalNotIn({StringLiteral("c")}, array), std::vector<char>({1, 0, 
0, 1}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestDictionaryWithNullValue) {
+    // `is_in` decodes the dictionary, so a row pointing at a null dictionary 
value becomes a null
+    // row and is false for both `IN` and `NOT IN`. An empty literal must not 
match it.
+    auto dictionary =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"([null, 
"a"])").ValueOrDie();
+    auto indices = arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), 
R"([0, 1, null, 0])")
+                       .ValueOrDie();
+    auto dict_type = arrow::dictionary(arrow::int32(), arrow::utf8());
+    std::shared_ptr<arrow::Array> array =
+        arrow::DictionaryArray::FromArrays(dict_type, indices, 
dictionary).ValueOrDie();
+
+    const std::vector<Literal> with_empty = {StringLiteral("a"), 
StringLiteral("")};
+    ASSERT_EQ(EvalIn(with_empty, array), std::vector<char>({0, 1, 0, 0}));
+    ASSERT_EQ(EvalNotIn(with_empty, array), std::vector<char>({0, 0, 0, 0}));
+
+    ASSERT_EQ(EvalIn({StringLiteral("a")}, array), std::vector<char>({0, 1, 0, 
0}));
+    ASSERT_EQ(EvalNotIn({StringLiteral("a")}, array), std::vector<char>({0, 0, 
0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestNullLiteralIgnoredForIn) {
+    const std::vector<Literal> literals = {Literal(int64_t{1}), 
Literal(FieldType::BIGINT),
+                                           Literal(int64_t{3})};
+    auto array = arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), 
R"([1, 2, 3, null])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 1, 0}));
+    // A null literal makes `NOT IN` false for every row.
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 0, 0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestOnlyNullLiterals) {
+    const std::vector<Literal> int_literals = {Literal(FieldType::BIGINT)};
+    auto int_array = arrow::ipc::internal::json::ArrayFromJSON(
+                         arrow::int64(), R"([-9007199254740993, 0, 
9007199254740993, null])")
+                         .ValueOrDie();
+    ASSERT_EQ(EvalIn(int_literals, int_array), std::vector<char>({0, 0, 0, 
0}));
+    ASSERT_EQ(EvalNotIn(int_literals, int_array), std::vector<char>({0, 0, 0, 
0}));
+
+    auto string_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"(["", "a", 
null])").ValueOrDie();
+    ASSERT_EQ(EvalIn({Literal(FieldType::STRING)}, string_array), 
std::vector<char>({0, 0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestEmptyLiterals) {
+    auto array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([1, 2, 
null])").ValueOrDie();
+    ASSERT_EQ(EvalIn({}, array), std::vector<char>({0, 0, 0}));
+    ASSERT_EQ(EvalNotIn({}, array), std::vector<char>({1, 1, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestTimestamp) {
+    // A timestamp literal settles the time unit of its value set itself, the 
finest unit that
+    // keeps every literal, so a column of that very unit is probed by the 
value set.
+    auto milli_array = arrow::ipc::internal::json::ArrayFromJSON(
+                           arrow::timestamp(arrow::TimeUnit::MILLI), R"([1000, 
2000, 3000, null])")
+                           .ValueOrDie();
+    const std::vector<Literal> milli_literals = {TimestampLiteral(1000), 
TimestampLiteral(3000)};
+    ASSERT_EQ(EvalIn(milli_literals, milli_array), std::vector<char>({1, 0, 1, 
0}));
+    ASSERT_EQ(EvalNotIn(milli_literals, milli_array), std::vector<char>({0, 1, 
0, 0}));
+
+    // A nanos-of-millisecond outside a microsecond forces the nanosecond 
unit, which the values
+    // keep through the value set.
+    auto nano_array =
+        
arrow::ipc::internal::json::ArrayFromJSON(arrow::timestamp(arrow::TimeUnit::NANO),
+                                                  R"([1000000000000, 
1000000456789, null])")
+            .ValueOrDie();
+    const std::vector<Literal> nano_literals = {TimestampLiteral(1000000, 
456789)};
+    ASSERT_EQ(EvalIn(nano_literals, nano_array), std::vector<char>({0, 1, 0}));
+    ASSERT_EQ(EvalNotIn(nano_literals, nano_array), std::vector<char>({1, 0, 
0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestDecimal) {
+    // A decimal literal carries the precision and the scale of its own value, 
so a column of that
+    // scale is probed by the value set.
+    const std::vector<Literal> literals = {DecimalLiteral(10, 2, "100"),
+                                           DecimalLiteral(10, 2, "-250")};
+    auto array = arrow::ipc::internal::json::ArrayFromJSON(
+                     arrow::decimal128(10, 2), R"(["1.00", "1.01", "-2.50", 
"0.00", null])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 1, 0, 0}));
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 1, 0, 1, 0}));
+
+    // A column of the same scale but another precision is probed too, the 
value set is widened to
+    // it without ever changing a value.
+    auto wider_array = arrow::ipc::internal::json::ArrayFromJSON(
+                           arrow::decimal128(38, 2), R"(["1.00", "1.01", 
"-2.50", null])")
+                           .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, wider_array), std::vector<char>({1, 0, 1, 0}));
+    ASSERT_EQ(EvalNotIn(literals, wider_array), std::vector<char>({0, 1, 0, 
0}));
+
+    // A value beyond 64 bits keeps its high bits through the value set.
+    const std::vector<Literal> wide_literals = {DecimalLiteral(38, 2, 
"12345678998765432134567"),
+                                                DecimalLiteral(38, 2, 
"-12345678998765432134567")};
+    ASSERT_EQ(EvalIn(wide_literals,
+                     arrow::ipc::internal::json::ArrayFromJSON(
+                         arrow::decimal128(38, 2),
+                         R"(["123456789987654321345.67", 
"-123456789987654321345.67", "1.00"])")
+                         .ValueOrDie()),
+              std::vector<char>({1, 1, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestDecimalNullLiterals) {
+    const std::vector<Literal> literals = {DecimalLiteral(10, 2, "100"),
+                                           Literal(FieldType::DECIMAL)};
+    auto array = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::decimal128(10, 2),
+                                                           R"(["1.00", "2.00", 
null])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 0}));
+    // A null literal makes `NOT IN` false for every row.
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 0, 0}));
+
+    // Nothing but null literals leaves no precision and scale to write the 
value set with, so the
+    // row by row path takes over and matches nothing either way.
+    ASSERT_EQ(EvalIn({Literal(FieldType::DECIMAL)}, array), 
std::vector<char>({0, 0, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(FieldType::DECIMAL)}, array), 
std::vector<char>({0, 0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestTimestampNullLiterals) {
+    auto array = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::timestamp(arrow::TimeUnit::MILLI),
+                                                           R"([1000, 2000, 
null])")
+                     .ValueOrDie();
+    const std::vector<Literal> literals = {TimestampLiteral(1000), 
Literal(FieldType::TIMESTAMP)};
+    ASSERT_EQ(EvalIn(literals, array), std::vector<char>({1, 0, 0}));
+    // A null literal makes `NOT IN` false for every row.
+    ASSERT_EQ(EvalNotIn(literals, array), std::vector<char>({0, 0, 0}));
+
+    // Nothing but null literals leaves no time unit to write the value set 
with, so the row by row
+    // path takes over and matches nothing either way.
+    ASSERT_EQ(EvalIn({Literal(FieldType::TIMESTAMP)}, array), 
std::vector<char>({0, 0, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(FieldType::TIMESTAMP)}, array), 
std::vector<char>({0, 0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestDecimalOffTheValueSetPath) {
+    // `Literal::CompareTo` compares two decimals by value, so a literal of 
another scale still
+    // matches the same number. The value set would have `is_in` cast one side 
to the other, so a
+    // column of another scale keeps the row by row path and the result stays 
the same either way.
+    auto array = 
arrow::ipc::internal::json::ArrayFromJSON(arrow::decimal128(10, 2),
+                                                           R"(["1.00", "2.00", 
null])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn({DecimalLiteral(20, 4, "10000")}, array), 
std::vector<char>({1, 0, 0}));
+    ASSERT_EQ(EvalNotIn({DecimalLiteral(20, 4, "10000")}, array), 
std::vector<char>({0, 1, 0}));
+
+    // Literals that do not share one precision and scale cannot be one arrow 
array, so they keep
+    // the row by row path as well.
+    ASSERT_EQ(EvalIn({DecimalLiteral(10, 2, "100"), DecimalLiteral(20, 4, 
"20000")}, array),
+              std::vector<char>({1, 1, 0}));
+
+    // Casting between those scales is what the row by row path spares: 
neither side of this pair
+    // fits the scale of the other, which would make `is_in` report an error 
where comparing by
+    // value merely finds no match.
+    auto lossy_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::decimal128(38, 2), 
R"(["1.01", "2.00"])")
+            .ValueOrDie();
+    const std::vector<Literal> huge_literals = {
+        DecimalLiteral(38, 0, "99999999999999999999999999999999999999")};
+    ASSERT_EQ(EvalIn(huge_literals, lossy_array), std::vector<char>({0, 0}));
+    ASSERT_EQ(EvalNotIn(huge_literals, lossy_array), std::vector<char>({1, 
1}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestTimestampOffTheValueSetPath) {
+    // A column of a coarser unit than the literals need keeps the row by row 
path, because `is_in`
+    // would cast the value set to the column unit and fail on a value the 
unit does not keep. A
+    // value that needs the finer unit cannot exist in the coarser column, so 
comparing by value
+    // merely finds no match.
+    auto milli_array = arrow::ipc::internal::json::ArrayFromJSON(
+                           arrow::timestamp(arrow::TimeUnit::MILLI), R"([1000, 
2000, null])")
+                           .ValueOrDie();
+    ASSERT_EQ(EvalIn({TimestampLiteral(1, 500000)}, milli_array), 
std::vector<char>({0, 0, 0}));
+    ASSERT_EQ(EvalNotIn({TimestampLiteral(1, 500000)}, milli_array), 
std::vector<char>({1, 1, 0}));
+
+    // A column of a finer unit than the literals need keeps the row by row 
path as well, casting
+    // to it can overflow int64 where comparing by value still matches the 
same instant.
+    auto nano_array = arrow::ipc::internal::json::ArrayFromJSON(
+                          arrow::timestamp(arrow::TimeUnit::NANO), 
R"([1000000000, null])")
+                          .ValueOrDie();
+    ASSERT_EQ(EvalIn({TimestampLiteral(1000)}, nano_array), 
std::vector<char>({1, 0}));
+    ASSERT_EQ(EvalNotIn({TimestampLiteral(1000)}, nano_array), 
std::vector<char>({0, 0}));
+
+    // A column with a time zone as well: `is_in` refuses to compare a zoned 
timestamp against an
+    // unzoned value set, so the row by row path compares by value instead.
+    auto zoned_array = arrow::ipc::internal::json::ArrayFromJSON(
+                           arrow::timestamp(arrow::TimeUnit::MILLI, "UTC"), 
R"([1000, 2000, null])")
+                           .ValueOrDie();
+    ASSERT_EQ(EvalIn({TimestampLiteral(1000)}, zoned_array), 
std::vector<char>({1, 0, 0}));
+    ASSERT_EQ(EvalNotIn({TimestampLiteral(1000)}, zoned_array), 
std::vector<char>({0, 1, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestFloatAndDouble) {
+    auto double_array = arrow::ipc::internal::json::ArrayFromJSON(
+                            arrow::float64(), R"([1.0, 2.5, -3.25, Inf, -Inf, 
null])")
+                            .ValueOrDie();
+    const std::vector<Literal> double_literals = {Literal(1.0), Literal(-3.25),
+                                                  
Literal(std::numeric_limits<double>::infinity())};
+    ASSERT_EQ(EvalIn(double_literals, double_array), std::vector<char>({1, 0, 
1, 1, 0, 0}));
+    ASSERT_EQ(EvalNotIn(double_literals, double_array), std::vector<char>({0, 
1, 0, 0, 1, 0}));
+
+    auto float_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::float32(), R"([1.5, 
2.5, -Inf, null])")
+            .ValueOrDie();
+    const std::vector<Literal> float_literals = {Literal(1.5f),
+                                                 
Literal(-std::numeric_limits<float>::infinity())};
+    ASSERT_EQ(EvalIn(float_literals, float_array), std::vector<char>({1, 0, 1, 
0}));
+    ASSERT_EQ(EvalNotIn(float_literals, float_array), std::vector<char>({0, 1, 
0, 0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, 
TestNegativeZeroDoesNotMatchPositiveZero) {
+    // `FieldsComparator::CompareFloatingPoint` orders `-0.0 < +0.0`, so they 
are two distinct
+    // values. `is_in` agrees only because it hashes the raw bits of a float, 
which arrow's own
+    // comment marks as something it would rather change. Should it ever hash 
equal floats alike,
+    // `-0.0` would start matching `IN (0.0)` and this assertion is what 
catches the divergence.
+    auto array = arrow::ipc::internal::json::ArrayFromJSON(arrow::float64(), 
R"([0.0, -0.0, 1.0])")
+                     .ValueOrDie();
+    ASSERT_EQ(EvalIn({Literal(0.0)}, array), std::vector<char>({1, 0, 0}));
+    ASSERT_EQ(EvalIn({Literal(-0.0)}, array), std::vector<char>({0, 1, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(0.0)}, array), std::vector<char>({0, 1, 1}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestNanLiteralStaysOffTheValueSetPath) {
+    // `is_in` hashes the raw bits of a float, so a NaN literal would only 
match the column NaNs
+    // carrying the very same bit pattern, while 
`FieldsComparator::CompareFloatingPoint` makes
+    // every NaN equal. A NaN literal therefore keeps the row by row path, 
where the sign flipped
+    // NaN below still matches. The JSON reader cannot spell a sign flipped 
NaN, so the column is
+    // built with a builder.
+    const double canonical_nan = std::numeric_limits<double>::quiet_NaN();
+    arrow::DoubleBuilder builder;
+    ASSERT_TRUE(builder.Append(canonical_nan).ok());
+    ASSERT_TRUE(builder.Append(-canonical_nan).ok());
+    ASSERT_TRUE(builder.Append(1.0).ok());
+    ASSERT_TRUE(builder.AppendNull().ok());
+    std::shared_ptr<arrow::Array> array;
+    ASSERT_TRUE(builder.Finish(&array).ok());
+
+    ASSERT_EQ(EvalIn({Literal(canonical_nan)}, array), std::vector<char>({1, 
1, 0, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(canonical_nan)}, array), 
std::vector<char>({0, 0, 1, 0}));
+    ASSERT_EQ(EvalIn({Literal(1.0), Literal(canonical_nan)}, array),
+              std::vector<char>({1, 1, 1, 0}));
+
+    // With no NaN in the value set the two paths agree, because a column NaN 
then matches no
+    // literal either way.
+    ASSERT_EQ(EvalIn({Literal(1.0)}, array), std::vector<char>({0, 0, 1, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(1.0)}, array), std::vector<char>({1, 1, 0, 
0}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestMixedLiteralTypesReportTheError) {
+    // Literals of mixed types make `Literal::CompareTo` fail, so they must 
keep reporting the error
+    // through the row by row path instead of being built into one typed value 
set. The column value
+    // matches none of them, otherwise the comparison would stop before 
reaching the odd literal.
+    auto array = arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), 
R"([0])").ValueOrDie();
+    ASSERT_NOK(In::Instance().Test(*array, {Literal(1), Literal(int64_t{2})},
+                                   arrow::default_memory_pool()));
+    ASSERT_NOK(In::Instance().Test(*array, {Literal(1), StringLiteral("a")},
+                                   arrow::default_memory_pool()));
+    ASSERT_NOK(NotIn::Instance().Test(*array, {Literal(1), 
Literal(int64_t{2})},
+                                      arrow::default_memory_pool()));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, 
TestDictionaryLayoutsLiteralConversionRejects) {
+    // Dictionary layouts that `LiteralConverter::ConvertLiteralsFromArray` 
rejects are decoded by
+    // `is_in`, so they no longer fail the whole evaluation.
+    auto int64_dictionary =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), R"([10, 
20])").ValueOrDie();
+    auto int32_indices =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([0, 
1])").ValueOrDie();
+    std::shared_ptr<arrow::Array> int64_dict_array =
+        arrow::DictionaryArray::FromArrays(arrow::dictionary(arrow::int32(), 
arrow::int64()),
+                                           int32_indices, int64_dictionary)
+            .ValueOrDie();
+    ASSERT_NOK(LiteralConverter::ConvertLiteralsFromArray(*int64_dict_array, 
/*own_data=*/false));
+    ASSERT_EQ(EvalIn({Literal(int64_t{10})}, int64_dict_array), 
std::vector<char>({1, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(int64_t{10})}, int64_dict_array), 
std::vector<char>({0, 1}));
+
+    auto string_dictionary =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"(["a", 
"b"])").ValueOrDie();
+    auto int8_indices =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int8(), R"([0, 
1])").ValueOrDie();
+    std::shared_ptr<arrow::Array> int8_dict_array =
+        arrow::DictionaryArray::FromArrays(arrow::dictionary(arrow::int8(), 
arrow::utf8()),
+                                           int8_indices, string_dictionary)
+            .ValueOrDie();
+    ASSERT_NOK(LiteralConverter::ConvertLiteralsFromArray(*int8_dict_array, 
/*own_data=*/false));
+    ASSERT_EQ(EvalIn({StringLiteral("a")}, int8_dict_array), 
std::vector<char>({1, 0}));
+    ASSERT_EQ(EvalNotIn({StringLiteral("a")}, int8_dict_array), 
std::vector<char>({0, 1}));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestProbeFailsOnUnrelatedArrowType) {
+    // Nothing casts an int32 value set to a string column, so the probe 
reports the mismatch
+    // instead of matching by chance. `PredicateValidator` already rejects 
such a predicate against
+    // the schema, this only pins what the evaluation does if one reaches it 
anyway.
+    auto string_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), 
R"(["1"])").ValueOrDie();
+    ASSERT_NOK(In::Instance().Test(*string_array, {Literal(1)}, 
arrow::default_memory_pool()));
+}
+
+TEST_F(MultiLiteralsLeafFunctionTest, TestSlicedArray) {
+    auto array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), R"([1, 2, 3, 
4, 5, null])")
+            .ValueOrDie();
+    auto sliced = array->Slice(2, 4);
+    ASSERT_EQ(EvalIn({Literal(2), Literal(4)}, sliced), std::vector<char>({0, 
1, 0, 0}));
+    ASSERT_EQ(EvalNotIn({Literal(2), Literal(4)}, sliced), 
std::vector<char>({1, 0, 1, 0}));
+
+    auto string_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::utf8(), R"(["a", "b", 
"c", "d"])")
+            .ValueOrDie();
+    auto sliced_string = string_array->Slice(1, 3);
+    ASSERT_EQ(EvalIn({StringLiteral("c")}, sliced_string), 
std::vector<char>({0, 1, 0}));
+}
+}  // namespace paimon::test
diff --git a/src/paimon/common/predicate/null_false_leaf_binary_function.h 
b/src/paimon/common/predicate/null_false_leaf_binary_function.h
index 87de3249..b9b6850b 100644
--- a/src/paimon/common/predicate/null_false_leaf_binary_function.h
+++ b/src/paimon/common/predicate/null_false_leaf_binary_function.h
@@ -31,8 +31,8 @@
 namespace paimon {
 class NullFalseLeafBinaryFunction : public LeafFunction {
  public:
-    Result<std::vector<char>> Test(const arrow::Array& array,
-                                   const std::vector<Literal>& literals) const 
override {
+    Result<std::vector<char>> Test(const arrow::Array& array, const 
std::vector<Literal>& literals,
+                                   arrow::MemoryPool* pool) const override {
         if (literals.size() < LITERAL_LIMIT) {
             return Status::Invalid("NullFalseLeafBinaryFunction needs single 
literal for field");
         }
diff --git a/src/paimon/common/predicate/or.h b/src/paimon/common/predicate/or.h
index fb180913..f7404640 100644
--- a/src/paimon/common/predicate/or.h
+++ b/src/paimon/common/predicate/or.h
@@ -45,9 +45,9 @@ class Or : public CompoundFunction {
  public:
     static const Or& Instance();
 
-    Result<std::vector<char>> Test(
-        const arrow::Array& array,
-        const std::vector<std::shared_ptr<Predicate>>& children) const 
override {
+    Result<std::vector<char>> Test(const arrow::Array& array,
+                                   const 
std::vector<std::shared_ptr<Predicate>>& children,
+                                   arrow::MemoryPool* pool) const override {
         std::vector<char> is_valid(array.length(), false);
         for (const auto& child : children) {
             auto child_filter = 
std::dynamic_pointer_cast<PredicateFilter>(child);
@@ -55,7 +55,7 @@ class Or : public CompoundFunction {
                 return Status::Invalid(
                     fmt::format("child filter {} does not support Test", 
child->ToString()));
             }
-            PAIMON_ASSIGN_OR_RAISE(std::vector<char> child_valid, 
child_filter->Test(array));
+            PAIMON_ASSIGN_OR_RAISE(std::vector<char> child_valid, 
child_filter->Test(array, pool));
             for (size_t i = 0; i < is_valid.size(); i++) {
                 is_valid[i] = (is_valid[i] | child_valid[i]);
             }
diff --git a/src/paimon/common/predicate/predicate_filter.h 
b/src/paimon/common/predicate/predicate_filter.h
index 799ed156..d14b4747 100644
--- a/src/paimon/common/predicate/predicate_filter.h
+++ b/src/paimon/common/predicate/predicate_filter.h
@@ -30,7 +30,10 @@ namespace paimon {
 class PredicateFilter : virtual public Predicate {
  public:
     /// @param array is the struct array of all fields
-    virtual Result<std::vector<char>> Test(const arrow::Array& array) const = 
0;
+    /// @param pool is where any arrow buffer the evaluation allocates comes 
from, it must not be
+    ///        null
+    virtual Result<std::vector<char>> Test(const arrow::Array& array,
+                                           arrow::MemoryPool* pool) const = 0;
     virtual Result<bool> Test(const std::shared_ptr<arrow::Schema>& schema,
                               const InternalRow& row) const = 0;
     virtual Result<bool> Test(const std::shared_ptr<arrow::Schema>& schema, 
int64_t row_count,
diff --git a/src/paimon/common/predicate/predicate_test.cpp 
b/src/paimon/common/predicate/predicate_test.cpp
index fa4733cb..332fa276 100644
--- a/src/paimon/common/predicate/predicate_test.cpp
+++ b/src/paimon/common/predicate/predicate_test.cpp
@@ -20,6 +20,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <limits>
 #include <memory>
 #include <optional>
 #include <string>
@@ -29,10 +30,12 @@
 #include "arrow/api.h"
 #include "arrow/array/array_nested.h"
 #include "arrow/ipc/json_simple.h"
+#include "fmt/format.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/leaf_predicate_impl.h"
 #include "paimon/common/predicate/predicate_filter.h"
 #include "paimon/defs.h"
 #include "paimon/memory/memory_pool.h"
@@ -185,7 +188,7 @@ TEST_F(PredicateTest, TestInvalidFieldIndex) {
         arrow::StructArray::Make({f0, f1}, src_type->fields()).ValueOrDie();
 
     // with array
-    ASSERT_NOK_WITH_MSG(predicate->Test(*struct_array),
+    ASSERT_NOK_WITH_MSG(predicate->Test(*struct_array, 
arrow::default_memory_pool()),
                         "field index 2 exceed field count 2 in struct array");
 
     // with internal row
@@ -208,7 +211,8 @@ TEST_F(PredicateTest, TestEqual) {
 
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 1, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -248,7 +252,8 @@ TEST_F(PredicateTest, TestEqualNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -276,7 +281,8 @@ TEST_F(PredicateTest, TestNotEqual) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 0, 0}));
 
     auto predicate_negate = 
std::dynamic_pointer_cast<PredicateFilter>(predicate->Negate());
@@ -312,7 +318,8 @@ TEST_F(PredicateTest, TestNotEqualNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
     // with internal row
     auto arrow_schema = arrow::schema(arrow::FieldVector({arrow::field("f0", 
bigint_type)}));
@@ -340,7 +347,8 @@ TEST_F(PredicateTest, TestGreater) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -375,7 +383,8 @@ TEST_F(PredicateTest, TestGreaterNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -404,7 +413,8 @@ TEST_F(PredicateTest, TestGreaterOrEqual) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 1, 1, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -439,7 +449,8 @@ TEST_F(PredicateTest, TestGreaterOrEqualNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -467,7 +478,8 @@ TEST_F(PredicateTest, TestLess) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 0, 0, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -501,7 +513,8 @@ TEST_F(PredicateTest, TestLessNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -529,7 +542,8 @@ TEST_F(PredicateTest, TestLessOrEqual) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 1, 0, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -563,7 +577,8 @@ TEST_F(PredicateTest, TestLessOrEqualNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -589,7 +604,8 @@ TEST_F(PredicateTest, TestIsNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 1}));
 
     ASSERT_EQ(*predicate->Negate(), *PredicateBuilder::IsNotNull(
@@ -618,7 +634,8 @@ TEST_F(PredicateTest, TestIsNotNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -650,7 +667,8 @@ TEST_F(PredicateTest, TestIn) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -686,7 +704,8 @@ TEST_F(PredicateTest, TestInNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));
 
     // with internal row
@@ -717,7 +736,8 @@ TEST_F(PredicateTest, TestNotIn) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 1, 0, 0}));
 
     ASSERT_EQ(*predicate->Negate(),
@@ -756,7 +776,8 @@ TEST_F(PredicateTest, TestNotInNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0, 0, 0}));
 
     // with internal row
@@ -797,7 +818,8 @@ TEST_F(PredicateTest, TestLargeIn) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));
 
     // with internal row
@@ -837,7 +859,8 @@ TEST_F(PredicateTest, TestLargeInNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));
 
     // with internal row
@@ -876,7 +899,8 @@ TEST_F(PredicateTest, TestLargeNotIn) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 1, 0, 0}));
 
     // with internal row
@@ -919,7 +943,8 @@ TEST_F(PredicateTest, TestLargeNotInNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0, 0, 0}));
 
     // with internal row
@@ -938,6 +963,98 @@ TEST_F(PredicateTest, TestLargeNotInNull) {
     ASSERT_FALSE(StatsCheck(*predicate, 3ll, {FieldStats(29ll, 32ll, 0ll)}));
 }
 
+TEST_F(PredicateTest, TestLargeStringIn) {
+    auto string_type = arrow::utf8();
+    std::vector<Literal> literals;
+    literals.reserve(1000);
+    for (int32_t i = 0; i < 1000; i++) {
+        std::string value = fmt::format("key-{}", i);
+        literals.emplace_back(FieldType::STRING, value.data(), value.size());
+    }
+    auto in_base =
+        PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0", 
FieldType::STRING, literals);
+    auto in_predicate = std::dynamic_pointer_cast<PredicateFilter>(in_base);
+    ASSERT_TRUE(in_predicate);
+    auto not_in_base = PredicateBuilder::NotIn(/*field_index=*/0, 
/*field_name=*/"f0",
+                                               FieldType::STRING, literals);
+    auto not_in_predicate = 
std::dynamic_pointer_cast<PredicateFilter>(not_in_base);
+    ASSERT_TRUE(not_in_predicate);
+
+    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(
+                  string_type, R"(["key-0", "key-999", "key-1000", "other", 
"", null])")
+                  .ValueOrDie();
+    std::shared_ptr<arrow::DataType> src_type = 
arrow::struct_({arrow::field("f0", string_type)});
+    std::shared_ptr<arrow::Array> struct_array =
+        arrow::StructArray::Make({f0}, src_type->fields()).ValueOrDie();
+
+    ASSERT_OK_AND_ASSIGN(auto in_valid,
+                         in_predicate->Test(*struct_array, 
arrow::default_memory_pool()));
+    ASSERT_EQ(in_valid, std::vector<char>({1, 1, 0, 0, 0, 0}));
+    ASSERT_OK_AND_ASSIGN(auto not_in_valid,
+                         not_in_predicate->Test(*struct_array, 
arrow::default_memory_pool()));
+    ASSERT_EQ(not_in_valid, std::vector<char>({0, 0, 1, 1, 1, 0}));
+}
+
+TEST_F(PredicateTest, TestInAfterRebind) {
+    auto bigint_type = arrow::int64();
+    auto predicate_base = PredicateBuilder::In(/*field_index=*/0, 
/*field_name=*/"f0",
+                                               FieldType::BIGINT, 
{Literal(1l), Literal(3l)});
+    auto leaf_predicate = 
std::dynamic_pointer_cast<LeafPredicateImpl>(predicate_base);
+    ASSERT_TRUE(leaf_predicate);
+
+    // Rebinding builds a new predicate around the same literals, results must 
stay identical.
+    auto renamed = leaf_predicate->NewLeafPredicate(/*new_field_name=*/"f1");
+    ASSERT_EQ(renamed->FieldName(), "f1");
+    auto rebound = renamed->NewLeafPredicate(/*new_field_index=*/1);
+    ASSERT_EQ(rebound->FieldIndex(), 1);
+
+    auto f0 =
+        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([3, 2, 1, 
0])").ValueOrDie();
+    auto f1 =
+        arrow::ipc::internal::json::ArrayFromJSON(bigint_type, R"([1, 2, 3, 
null])").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, rebound->Test(*struct_array, 
arrow::default_memory_pool()));
+    ASSERT_EQ(is_valid, std::vector<char>({1, 0, 1, 0}));
+
+    auto arrow_schema = arrow::schema(
+        arrow::FieldVector({arrow::field("f0", bigint_type), 
arrow::field("f1", bigint_type)}));
+    ASSERT_TRUE(rebound->Test(arrow_schema, CreateBigIntRow({0, 1})).value());
+    ASSERT_FALSE(rebound->Test(arrow_schema, CreateBigIntRow({1, 2})).value());
+}
+
+TEST_F(PredicateTest, TestInt64BoundaryIn) {
+    // The value set carries the whole int64 range, the boundary values 
included.
+    std::vector<Literal> literals = 
{Literal(std::numeric_limits<int64_t>::min()),
+                                     
Literal(std::numeric_limits<int64_t>::max())};
+    auto in_base =
+        PredicateBuilder::In(/*field_index=*/0, /*field_name=*/"f0", 
FieldType::BIGINT, literals);
+    auto in_predicate = std::dynamic_pointer_cast<PredicateFilter>(in_base);
+    ASSERT_TRUE(in_predicate);
+    auto not_in_base = PredicateBuilder::NotIn(/*field_index=*/0, 
/*field_name=*/"f0",
+                                               FieldType::BIGINT, literals);
+    auto not_in_predicate = 
std::dynamic_pointer_cast<PredicateFilter>(not_in_base);
+    ASSERT_TRUE(not_in_predicate);
+
+    auto f0 = arrow::ipc::internal::json::ArrayFromJSON(
+                  arrow::int64(), R"([-9223372036854775808, 0, 
9223372036854775807, null])")
+                  .ValueOrDie();
+    std::shared_ptr<arrow::DataType> src_type =
+        arrow::struct_({arrow::field("f0", arrow::int64())});
+    std::shared_ptr<arrow::Array> struct_array =
+        arrow::StructArray::Make({f0}, src_type->fields()).ValueOrDie();
+
+    ASSERT_OK_AND_ASSIGN(auto in_valid,
+                         in_predicate->Test(*struct_array, 
arrow::default_memory_pool()));
+    ASSERT_EQ(in_valid, std::vector<char>({1, 0, 1, 0}));
+    ASSERT_OK_AND_ASSIGN(auto not_in_valid,
+                         not_in_predicate->Test(*struct_array, 
arrow::default_memory_pool()));
+    ASSERT_EQ(not_in_valid, std::vector<char>({0, 1, 0, 0}));
+}
+
 TEST_F(PredicateTest, TestAnd) {
     auto bigint_type = arrow::int64();
     ASSERT_OK_AND_ASSIGN(
@@ -957,7 +1074,8 @@ TEST_F(PredicateTest, TestAnd) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));
 
     ASSERT_OK_AND_ASSIGN(
@@ -1003,7 +1121,8 @@ TEST_F(PredicateTest, TestOr) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 1, 1, 1}));
 
     ASSERT_OK_AND_ASSIGN(
@@ -1045,7 +1164,8 @@ TEST_F(PredicateTest, TestBetween) {
 
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({1, 1, 1, 0, 0, 0}));
 
     auto less_than = PredicateBuilder::LessThan(/*field_index=*/0, 
/*field_name=*/"f0",
@@ -1087,7 +1207,8 @@ TEST_F(PredicateTest, TestBetweenNull) {
 
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -1120,7 +1241,8 @@ TEST_F(PredicateTest, TestStartsWith) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));
 
     ASSERT_EQ(predicate->Negate(), nullptr);
@@ -1165,7 +1287,8 @@ TEST_F(PredicateTest, TestStartsWithNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -1199,7 +1322,8 @@ TEST_F(PredicateTest, TestEndsWith) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));
 
     ASSERT_EQ(predicate->Negate(), nullptr);
@@ -1236,7 +1360,8 @@ TEST_F(PredicateTest, TestEndsWithNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
@@ -1270,7 +1395,8 @@ TEST_F(PredicateTest, TestContains) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0, 1, 0}));
 
     ASSERT_EQ(predicate->Negate(), nullptr);
@@ -1307,7 +1433,8 @@ TEST_F(PredicateTest, TestContainsNull) {
     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_OK_AND_ASSIGN(auto is_valid,
+                         predicate->Test(*struct_array, 
arrow::default_memory_pool()));
     ASSERT_EQ(is_valid, std::vector<char>({0, 0}));
 
     // with internal row
diff --git a/src/paimon/common/reader/late_materializing_file_batch_reader.cpp 
b/src/paimon/common/reader/late_materializing_file_batch_reader.cpp
index 23b6960f..a5984c70 100644
--- a/src/paimon/common/reader/late_materializing_file_batch_reader.cpp
+++ b/src/paimon/common/reader/late_materializing_file_batch_reader.cpp
@@ -90,7 +90,8 @@ Result<RoaringBitmap32> 
LateMaterializingFileBatchReader::FilterProbeBatch(
     const std::shared_ptr<arrow::Array>& array,
     const std::shared_ptr<PredicateFilter>& bound_filter) {
     // TODO(zhouhonfeng.zhf): use arrow::compute::Filter instead of 
PredicateFilter
-    PAIMON_ASSIGN_OR_RAISE(std::vector<char> results, 
bound_filter->Test(*array));
+    PAIMON_ASSIGN_OR_RAISE(std::vector<char> results,
+                           bound_filter->Test(*array, arrow_pool_.get()));
     if (results.size() != static_cast<size_t>(array->length())) {
         return Status::Invalid(
             fmt::format("predicate result size {} does not match probe batch 
length {}",
diff --git a/src/paimon/common/reader/predicate_batch_reader.cpp 
b/src/paimon/common/reader/predicate_batch_reader.cpp
index 621d7568..fb9a5f97 100644
--- a/src/paimon/common/reader/predicate_batch_reader.cpp
+++ b/src/paimon/common/reader/predicate_batch_reader.cpp
@@ -61,6 +61,9 @@ Result<std::unique_ptr<PredicateBatchReader>> 
PredicateBatchReader::Create(
         return Status::Invalid(
             fmt::format("predicate {} does not support Test", 
predicate->ToString()));
     }
+    if (arrow_pool == nullptr) {
+        return Status::Invalid("create predicate batch reader failed. arrow 
pool is nullptr");
+    }
     return std::unique_ptr<PredicateBatchReader>(
         new PredicateBatchReader(std::move(reader), predicate, arrow_pool));
 }
@@ -124,7 +127,8 @@ Result<RoaringBitmap32> PredicateBatchReader::Filter(const 
std::shared_ptr<arrow
     if (!predicate_filter_) {
         PAIMON_RETURN_NOT_OK(BindPredicateToArray(*array));
     }
-    PAIMON_ASSIGN_OR_RAISE(std::vector<char> result, 
predicate_filter_->Test(*array));
+    PAIMON_ASSIGN_OR_RAISE(std::vector<char> result,
+                           predicate_filter_->Test(*array, arrow_pool_.get()));
     assert(result.size() == static_cast<size_t>(array->length()));
     RoaringBitmap32 is_valid;
     for (int32_t i = 0; i < static_cast<int32_t>(result.size()); i++) {

Reply via email to