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 21146692 perf(predicate): avoid materializing all columns for field
bounds checks (#318)
21146692 is described below
commit 21146692aa6ca228f8a29e26f6a71edf9c1bede1
Author: gripleaf <[email protected]>
AuthorDate: Thu Sep 10 20:55:31 2026 +0800
perf(predicate): avoid materializing all columns for field bounds checks
(#318)
---
src/paimon/common/predicate/leaf_predicate_impl.h | 4 +-
src/paimon/common/predicate/predicate_test.cpp | 20 ++++
test/inte/write_and_read_inte_test.cpp | 108 ++++++++++++++++++++++
3 files changed, 130 insertions(+), 2 deletions(-)
diff --git a/src/paimon/common/predicate/leaf_predicate_impl.h
b/src/paimon/common/predicate/leaf_predicate_impl.h
index 0d298409..a301e19c 100644
--- a/src/paimon/common/predicate/leaf_predicate_impl.h
+++ b/src/paimon/common/predicate/leaf_predicate_impl.h
@@ -44,10 +44,10 @@ class LeafPredicateImpl : public LeafPredicate, public
PredicateFilter {
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())) {
+ if (field_index_ >= struct_array.num_fields()) {
return Status::Invalid(
fmt::format("field index {} exceed field count {} in struct
array", field_index_,
- struct_array.fields().size()));
+ struct_array.num_fields()));
}
const auto& field_array = struct_array.field(field_index_);
return leaf_function_.Test(*field_array, literals_, pool);
diff --git a/src/paimon/common/predicate/predicate_test.cpp
b/src/paimon/common/predicate/predicate_test.cpp
index 332fa276..97fef15d 100644
--- a/src/paimon/common/predicate/predicate_test.cpp
+++ b/src/paimon/common/predicate/predicate_test.cpp
@@ -172,6 +172,26 @@ class PredicateTest : public ::testing::Test {
}
};
+TEST_F(PredicateTest, TestLeafFieldBoundsOnWideSlicedBatch) {
+ auto values =
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(),
"[9,1,null,2,9]").ValueOrDie();
+ arrow::ArrayVector columns(64, values);
+ std::vector<std::string> names;
+ for (int32_t i = 0; i < 64; ++i) {
+ names.push_back(fmt::format("field_{}", i));
+ }
+ auto batch = arrow::StructArray::Make(columns, names).ValueOrDie();
+ auto sliced = batch->Slice(1, 3);
+ auto pool = arrow::default_memory_pool();
+ auto predicate = std::dynamic_pointer_cast<PredicateFilter>(
+ PredicateBuilder::Equal(63, "field_63", FieldType::BIGINT,
Literal(int64_t{2})));
+ ASSERT_OK_AND_ASSIGN(auto full, predicate->Test(*sliced, pool));
+ ASSERT_EQ(full, std::vector<char>({0, 0, 1}));
+ auto invalid = std::dynamic_pointer_cast<PredicateFilter>(
+ PredicateBuilder::Equal(64, "missing", FieldType::BIGINT,
Literal(int64_t{2})));
+ ASSERT_NOK_WITH_MSG(invalid->Test(*sliced, pool), "field index 64 exceed
field count 64");
+}
+
TEST_F(PredicateTest, TestInvalidFieldIndex) {
auto bigint_type = arrow::int64();
auto predicate_base = PredicateBuilder::Equal(/*field_index=*/2,
/*field_name=*/"f0",
diff --git a/test/inte/write_and_read_inte_test.cpp
b/test/inte/write_and_read_inte_test.cpp
index cca4f786..7d184bb8 100644
--- a/test/inte/write_and_read_inte_test.cpp
+++ b/test/inte/write_and_read_inte_test.cpp
@@ -357,6 +357,114 @@ TEST_P(WriteAndReadInteTest, TestAppendSimple) {
ASSERT_TRUE(success);
}
+TEST_P(WriteAndReadInteTest, TestAppendReadWithNestedPredicateAcrossBatches) {
+ auto [file_format, file_system] = GetParam();
+ arrow::FieldVector fields = {
+ arrow::field("id", arrow::int32()), arrow::field("key",
arrow::int64()),
+ arrow::field("value", arrow::utf8()), arrow::field("payload",
arrow::binary())};
+ std::map<std::string, std::string> options = {
+ {Options::FILE_FORMAT, file_format},
+ {Options::FILE_SYSTEM, file_system},
+ {Options::BUCKET, "-1"},
+ {Options::TARGET_FILE_SIZE, "1048576"},
+ };
+ if (file_system == "jindo") {
+ options = AddOptionsForJindo(options);
+ }
+ ASSERT_OK_AND_ASSIGN(auto helper,
+ TestHelper::Create(test_dir_, arrow::schema(fields),
/*partition_keys=*/{},
+ /*primary_keys=*/{}, options,
+ /*is_streaming_mode=*/false));
+ // For the read batch size of 4 configured below, consecutive groups of
four
+ // input rows have 0, 2, 4, 0, 2, and 4 matches for the AND predicate.
+ // Input rows at positions 4 and 6 are identical and must both survive.
+ const std::string data_json = R"([
+ [0, 0, "a", "zero"], [1, null, "b", null],
+ [2, 0, null, "two"], [3, 0, "c", "three"],
+ [4, 1, "b", "keep"], [5, 0, "a", "drop"],
+ [4, 1, "b", "keep"], [7, 1, null, "null"],
+ [8, 1, "a", "eight"], [9, 1, "b", null],
+ [10, 1, "a", "ten"], [11, 1, "b", "eleven"],
+ [12, 0, "a", "twelve"], [13, 0, "b", "thirteen"],
+ [14, null, "a", "fourteen"], [15, 1, "c", "fifteen"],
+ [16, 1, "b", "sixteen"], [17, 0, "b", "seventeen"],
+ [18, 1, "a", "eighteen"], [19, 1, null, "nineteen"],
+ [20, 1, "a", "twenty"], [21, 1, "a", "twenty-one"],
+ [22, 1, "b", "twenty-two"], [23, 1, "b", "twenty-three"]
+ ])";
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields),
data_json,
+ /*partition_map=*/{},
/*bucket=*/0, {}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
+
/*expected_commit_messages=*/std::nullopt));
+ ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> splits,
+ helper->NewScan(StartupMode::LatestFull(),
/*snapshot_id=*/std::nullopt,
+ /*is_streaming=*/false));
+ ASSERT_FALSE(splits.empty());
+
+ // Predicate indices refer to the projected read schema: payload, value,
id, key.
+ auto key = PredicateBuilder::Equal(3, "key", FieldType::BIGINT,
Literal(int64_t{1}));
+ auto a =
+ PredicateBuilder::Equal(1, "value", FieldType::STRING,
Literal(FieldType::STRING, "a", 1));
+ auto b =
+ PredicateBuilder::Equal(1, "value", FieldType::STRING,
Literal(FieldType::STRING, "b", 1));
+ ASSERT_OK_AND_ASSIGN(auto alternatives, PredicateBuilder::Or({a, b}));
+ ASSERT_OK_AND_ASSIGN(auto conjunction, PredicateBuilder::And({key,
alternatives}));
+ ASSERT_OK_AND_ASSIGN(auto both, PredicateBuilder::And({key, a}));
+ ASSERT_OK_AND_ASSIGN(auto disjunction, PredicateBuilder::Or({both, b}));
+ const std::vector<std::pair<std::shared_ptr<Predicate>,
std::vector<int64_t>>> cases = {
+ {conjunction, {4, 6, 8, 9, 10, 11, 16, 18, 20, 21, 22, 23}},
+ {disjunction, {1, 4, 6, 8, 9, 10, 11, 13, 16, 17, 18, 20, 21, 22, 23}},
+ };
+
+ // Build the oracle from written rows and explicit expected positions,
independently
+ // of predicate evaluation. Reordering both predicate columns also tests
name binding.
+ auto input = checked_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields),
data_json).ValueOrDie());
+ auto row_kinds = arrow::MakeArrayFromScalar(arrow::Int8Scalar(0),
input->length()).ValueOrDie();
+ auto projected =
+ arrow::StructArray::Make(
+ {row_kinds, input->field(3), input->field(2), input->field(0),
input->field(1)},
+ std::vector<std::string>{"_VALUE_KIND", "payload", "value", "id",
"key"})
+ .ValueOrDie();
+ const auto unfiltered = std::make_shared<arrow::ChunkedArray>(projected);
+ const std::string table_path = PathUtil::JoinPath(test_dir_, "foo.db/bar");
+ for (const auto& [predicate, expected_positions] : cases) {
+ SCOPED_TRACE(predicate->ToString());
+ arrow::ArrayVector expected_rows;
+ for (int64_t position : expected_positions) {
+ expected_rows.push_back(projected->Slice(position, 1));
+ }
+ const auto filtered =
std::make_shared<arrow::ChunkedArray>(expected_rows);
+ for (int32_t batch_size : {1, 4, 7}) {
+ SCOPED_TRACE(batch_size);
+ for (bool enable_filter : {false, true}) {
+ SCOPED_TRACE(enable_filter);
+ ReadContextBuilder builder(table_path);
+ builder.SetOptions(options)
+ .SetReadFieldNames({"payload", "value", "id", "key"})
+ .SetPredicate(predicate)
+ .EnablePredicateFilter(enable_filter)
+ .EnablePrefetch(false)
+ .EnableLateMaterializing(false)
+ .AddOption(Options::READ_BATCH_SIZE,
std::to_string(batch_size));
+ ASSERT_OK_AND_ASSIGN(auto context, builder.Finish());
+ ASSERT_OK_AND_ASSIGN(auto table_read,
TableRead::Create(std::move(context)));
+ ASSERT_OK_AND_ASSIGN(auto reader,
table_read->CreateReader(splits));
+ ASSERT_OK_AND_ASSIGN(auto actual,
ReadResultCollector::CollectResult(reader.get()));
+ ASSERT_TRUE(actual);
+ // The same predicate without precise filtering must retain
all input rows,
+ // proving that format pushdown did not remove the empty or
partial batches.
+ const auto& expected = enable_filter ? filtered : unfiltered;
+ ASSERT_TRUE(expected->Equals(actual)) << actual->ToString();
+ ASSERT_OK_AND_ASSIGN(auto eof, reader->NextBatch());
+ ASSERT_TRUE(BatchReader::IsEofBatch(eof));
+ reader->Close();
+ }
+ }
+ }
+}
+
TEST_P(WriteAndReadInteTest, TestAppendVector) {
auto [file_format, file_system] = GetParam();
if (file_format != "parquet") {