This is an automated email from the ASF dual-hosted git repository.
SteNicholas 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 da24fbdf feat(scan): support file indexes in real-time and PK scans
(#370)
da24fbdf is described below
commit da24fbdf55f9264076b43dac4eeba2f64514620a
Author: lxy <[email protected]>
AuthorDate: Mon Sep 21 14:52:24 2026 +0800
feat(scan): support file indexes in real-time and PK scans (#370)
---
.../bitmap/apply_bitmap_index_batch_reader.h | 18 +-
src/paimon/core/operation/abstract_split_read.cpp | 121 ++++++++-
src/paimon/core/operation/abstract_split_read.h | 18 +-
.../core/operation/append_only_file_store_scan.cpp | 43 +---
.../core/operation/append_only_file_store_scan.h | 6 -
src/paimon/core/operation/file_store_scan.cpp | 31 +++
src/paimon/core/operation/file_store_scan.h | 8 +
.../core/operation/key_value_file_store_scan.cpp | 12 +-
.../core/operation/merge_file_split_read.cpp | 63 +----
src/paimon/core/operation/merge_file_split_read.h | 8 -
src/paimon/core/operation/raw_file_split_read.cpp | 96 -------
src/paimon/core/operation/raw_file_split_read.h | 8 -
test/inte/realtime_write_inte_test.cpp | 286 +++++++++++++++++++++
test/inte/scan_and_read_inte_test.cpp | 161 ++++++++++++
14 files changed, 652 insertions(+), 227 deletions(-)
diff --git
a/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader.h
b/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader.h
index efb159ea..114e56b9 100644
--- a/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader.h
+++ b/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader.h
@@ -101,11 +101,25 @@ class ApplyBitmapIndexBatchReader : public
FileBatchReader {
private:
Result<RoaringBitmap32> Filter(int32_t batch_size) const {
RoaringBitmap32 result;
- auto bitmap_iter = bitmap_.Begin();
+ if (batch_size == 0) {
+ return result;
+ }
+
+ PAIMON_ASSIGN_OR_RAISE(uint64_t first_file_row_id,
+
reader_->GetPreviousBatchFileRowId(/*batch_row_id=*/0));
+ if (first_file_row_id >
static_cast<uint64_t>(RoaringBitmap32::MAX_VALUE)) {
+ return result;
+ }
+ // Avoid rescanning bitmap entries before this batch while retaining a
single linear
+ // iterator for all rows in the batch.
+ auto bitmap_iter =
bitmap_.EqualOrLarger(static_cast<int32_t>(first_file_row_id));
auto bitmap_end = bitmap_.End();
for (int32_t i = 0; i < batch_size; ++i) {
- PAIMON_ASSIGN_OR_RAISE(uint64_t file_row_id,
reader_->GetPreviousBatchFileRowId(i));
+ uint64_t file_row_id = first_file_row_id;
+ if (i > 0) {
+ PAIMON_ASSIGN_OR_RAISE(file_row_id,
reader_->GetPreviousBatchFileRowId(i));
+ }
while (bitmap_iter != bitmap_end &&
static_cast<uint64_t>(*bitmap_iter) < file_row_id) {
++bitmap_iter;
}
diff --git a/src/paimon/core/operation/abstract_split_read.cpp
b/src/paimon/core/operation/abstract_split_read.cpp
index fadf5178..53b9a40e 100644
--- a/src/paimon/core/operation/abstract_split_read.cpp
+++ b/src/paimon/core/operation/abstract_split_read.cpp
@@ -26,6 +26,8 @@
#include <set>
#include <utility>
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
#include "arrow/type.h"
#include "fmt/format.h"
#include "paimon/common/data/blob_defs.h"
@@ -37,6 +39,7 @@
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/executor/future.h"
#include "paimon/common/executor/reader_build_executor.h"
+#include "paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader.h"
#include "paimon/common/reader/data_file_reader_factory.h"
#include "paimon/common/reader/delegating_prefetch_reader.h"
#include "paimon/common/reader/late_materializing_reader_builder.h"
@@ -44,11 +47,14 @@
#include "paimon/common/reader/prefetch_file_batch_reader_impl.h"
#include "paimon/common/table/special_fields.h"
#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/object_utils.h"
+#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
#include "paimon/core/io/complete_row_tracking_fields_reader.h"
#include "paimon/core/io/data_file_meta.h"
#include "paimon/core/io/data_file_path_factory.h"
#include "paimon/core/io/field_mapping_reader.h"
+#include "paimon/core/io/file_index_evaluator.h"
#include "paimon/core/io/vector_file_batch_reader.h"
#include "paimon/core/operation/internal_read_context.h"
#include "paimon/core/partition/partition_info.h"
@@ -56,10 +62,13 @@
#include "paimon/core/table/source/data_split_impl.h"
#include "paimon/core/utils/field_mapping.h"
#include "paimon/core/utils/nested_projection_utils.h"
+#include "paimon/file_index/bitmap_index_result.h"
+#include "paimon/file_index/file_index_result.h"
#include "paimon/format/file_format.h"
#include "paimon/format/file_format_factory.h"
#include "paimon/fs/file_system.h"
#include "paimon/status.h"
+#include "paimon/utils/roaring_bitmap32.h"
namespace paimon {
class BinaryRow;
@@ -83,19 +92,38 @@ AbstractSplitRead::AbstractSplitRead(const
std::shared_ptr<FileStorePathFactory>
schema_manager_(std::move(schema_manager)) {}
Result<std::vector<std::unique_ptr<FileBatchReader>>>
AbstractSplitRead::CreateRawFileReaders(
+ const BinaryRow& partition, const
std::vector<std::shared_ptr<DataFileMeta>>& data_files,
+ const std::shared_ptr<arrow::Schema>& read_schema, const
std::shared_ptr<Predicate>& predicate,
+ DeletionVector::Factory dv_factory, const
std::optional<std::vector<Range>>& row_ranges,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory,
+ const std::map<std::string, std::string>& extra_format_options) const {
+ PAIMON_ASSIGN_OR_RAISE(
+ std::vector<RawFileReaderWithMeta> readers_with_meta,
+ CreateRawFileReadersWithMeta(partition, data_files, read_schema,
predicate, dv_factory,
+ row_ranges, data_file_path_factory,
extra_format_options));
+ std::vector<std::unique_ptr<FileBatchReader>> raw_file_readers;
+ raw_file_readers.reserve(readers_with_meta.size());
+ for (auto& reader_with_meta : readers_with_meta) {
+ raw_file_readers.push_back(std::move(reader_with_meta.reader));
+ }
+ return raw_file_readers;
+}
+
+Result<std::vector<AbstractSplitRead::RawFileReaderWithMeta>>
+AbstractSplitRead::CreateRawFileReadersWithMeta(
const BinaryRow& partition, const
std::vector<std::shared_ptr<DataFileMeta>>& data_files,
const std::shared_ptr<arrow::Schema>& read_schema, const
std::shared_ptr<Predicate>& predicate,
DeletionVector::Factory dv_factory, const
std::optional<std::vector<Range>>& row_ranges,
const std::shared_ptr<DataFilePathFactory>& data_file_path_factory,
const std::map<std::string, std::string>& extra_format_options) const {
if (data_files.empty()) {
- return std::vector<std::unique_ptr<FileBatchReader>>();
+ return std::vector<RawFileReaderWithMeta>();
}
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<FieldMappingBuilder> field_mapping_builder,
FieldMappingBuilder::Create(read_schema, context_->GetPartitionKeys(),
predicate));
- std::vector<std::unique_ptr<FileBatchReader>> raw_file_readers;
+ std::vector<RawFileReaderWithMeta> raw_file_readers;
raw_file_readers.reserve(data_files.size());
const uint32_t parallel_num =
static_cast<uint32_t>(std::min<size_t>(kReaderBuildMaxParallelNum,
data_files.size()));
@@ -106,7 +134,7 @@ Result<std::vector<std::unique_ptr<FileBatchReader>>>
AbstractSplitRead::CreateR
CreateRawFileReader(partition, file,
field_mapping_builder.get(), dv_factory,
row_ranges, data_file_path_factory,
extra_format_options));
if (file_reader) {
- raw_file_readers.push_back(std::move(file_reader));
+ raw_file_readers.push_back({file, std::move(file_reader)});
}
}
return std::move(raw_file_readers);
@@ -129,7 +157,9 @@ Result<std::vector<std::unique_ptr<FileBatchReader>>>
AbstractSplitRead::CreateR
}
// Readers keep the file order of the split, and CollectAll preserves the
submit order.
Status first_error;
+ size_t file_index = 0;
for (auto& built : CollectAll(futures)) {
+ const std::shared_ptr<DataFileMeta>& file = data_files[file_index++];
if (!built.ok()) {
if (first_error.ok()) {
first_error = built.status();
@@ -138,13 +168,96 @@ Result<std::vector<std::unique_ptr<FileBatchReader>>>
AbstractSplitRead::CreateR
}
std::unique_ptr<FileBatchReader> file_reader =
std::move(built).value();
if (file_reader) {
- raw_file_readers.push_back(std::move(file_reader));
+ raw_file_readers.push_back({file, std::move(file_reader)});
}
}
PAIMON_RETURN_NOT_OK(first_error);
return std::move(raw_file_readers);
}
+Result<std::unique_ptr<FileBatchReader>>
AbstractSplitRead::ApplyIndexAndDvReaderIfNeeded(
+ std::unique_ptr<FileBatchReader>&& file_reader, const
std::shared_ptr<DataFileMeta>& file,
+ const std::shared_ptr<arrow::Schema>& data_schema,
+ const std::shared_ptr<arrow::Schema>& read_schema, const
std::shared_ptr<Predicate>& predicate,
+ DeletionVector::Factory dv_factory, const
std::optional<std::vector<Range>>& row_ranges,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
+ std::shared_ptr<FileIndexResult> file_index_result;
+ if (options_.FileIndexReadEnabled()) {
+ PAIMON_ASSIGN_OR_RAISE(
+ file_index_result,
+ FileIndexEvaluator::Evaluate(data_schema, predicate,
data_file_path_factory, file,
+ options_.GetFileSystem(), pool_));
+ PAIMON_ASSIGN_OR_RAISE(bool is_remain, file_index_result->IsRemain());
+ if (!is_remain) {
+ return std::unique_ptr<FileBatchReader>();
+ }
+ }
+
+ // prepare selection bitmap for index
+ const RoaringBitmap32* selection = nullptr;
+ if (auto* bitmap_file_index =
dynamic_cast<BitmapIndexResult*>(file_index_result.get())) {
+ PAIMON_ASSIGN_OR_RAISE(selection, bitmap_file_index->GetBitmap());
+ }
+
+ // narrow the selection to the file-local row positions of an indexed split
+ std::optional<RoaringBitmap32> row_ranges_selection;
+ if (row_ranges) {
+ RoaringBitmap32 row_ranges_bitmap;
+ for (const Range& range : row_ranges.value()) {
+ row_ranges_bitmap.AddRange(static_cast<int32_t>(range.from),
+ static_cast<int32_t>(range.to + 1));
+ }
+ row_ranges_selection = selection ? RoaringBitmap32::And(*selection,
row_ranges_bitmap)
+ : std::move(row_ranges_bitmap);
+ selection = &row_ranges_selection.value();
+ }
+
+ // prepare deletion bitmap for deletion vector
+ std::shared_ptr<DeletionVector> deletion_vector;
+ if (dv_factory) {
+ PAIMON_ASSIGN_OR_RAISE(deletion_vector, dv_factory(file->file_name));
+ }
+ const RoaringBitmap32* deletion = nullptr;
+ if (auto* bitmap_dv =
dynamic_cast<BitmapDeletionVector*>(deletion_vector.get())) {
+ deletion = bitmap_dv->GetBitmap();
+ }
+
+ // merge deletion and bitmap index selection
+ std::optional<RoaringBitmap32> actual_selection;
+ if (selection && deletion) {
+ actual_selection = RoaringBitmap32::AndNot(*selection, *deletion);
+ } else if (selection) {
+ actual_selection = *selection;
+ } else if (deletion) {
+ actual_selection = *deletion;
+ PAIMON_ASSIGN_OR_RAISE(uint64_t num_rows,
file_reader->GetNumberOfRows());
+ actual_selection->Flip(0, num_rows);
+ }
+
+ if (actual_selection && actual_selection->IsEmpty()) {
+ return std::unique_ptr<FileBatchReader>();
+ }
+
+ ::ArrowSchema c_read_schema;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema,
&c_read_schema));
+ PAIMON_RETURN_NOT_OK(file_reader->SetReadSchema(&c_read_schema, predicate,
actual_selection));
+
+ std::unique_ptr<FileBatchReader> reader;
+ if (!file_reader->SupportPreciseBitmapSelection() && actual_selection) {
+ reader =
std::make_unique<ApplyBitmapIndexBatchReader>(std::move(file_reader),
+
std::move(actual_selection).value());
+ } else {
+ reader = std::move(file_reader);
+ }
+
+ if (deletion_vector && !deletion && !deletion_vector->IsEmpty()) {
+ // TODO(xinyu.lxy): if deletion vector is bitmap64, use
ApplyBitmapIndexBatchReader to
+ // filter result
+ return Status::NotImplemented("Only support BitmapDeletionVector");
+ }
+ return reader;
+}
+
Result<std::unique_ptr<FileBatchReader>>
AbstractSplitRead::CreateRawFileReader(
const BinaryRow& partition, const std::shared_ptr<DataFileMeta>& file,
const FieldMappingBuilder* field_mapping_builder, DeletionVector::Factory
dv_factory,
diff --git a/src/paimon/core/operation/abstract_split_read.h
b/src/paimon/core/operation/abstract_split_read.h
index a52bed66..4e1ef86d 100644
--- a/src/paimon/core/operation/abstract_split_read.h
+++ b/src/paimon/core/operation/abstract_split_read.h
@@ -77,11 +77,27 @@ class AbstractSplitRead : public SplitRead {
const std::map<std::string, std::string>& extra_format_options) const;
protected:
+ struct RawFileReaderWithMeta {
+ std::shared_ptr<DataFileMeta> file;
+ std::unique_ptr<FileBatchReader> reader;
+ };
+
AbstractSplitRead(const std::shared_ptr<FileStorePathFactory>&
path_factory,
const std::shared_ptr<InternalReadContext>& context,
std::unique_ptr<SchemaManager>&& schema_manager,
const std::shared_ptr<MemoryPool>& memory_pool,
const std::shared_ptr<Executor>& executor);
+
+ /// Creates raw readers while preserving the metadata associated with
every reader which was
+ /// not eliminated by a file index or deletion vector.
+ Result<std::vector<RawFileReaderWithMeta>> CreateRawFileReadersWithMeta(
+ const BinaryRow& partition, const
std::vector<std::shared_ptr<DataFileMeta>>& data_files,
+ const std::shared_ptr<arrow::Schema>& read_schema,
+ const std::shared_ptr<Predicate>& predicate, DeletionVector::Factory
dv_factory,
+ const std::optional<std::vector<Range>>& row_ranges,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory,
+ const std::map<std::string, std::string>& extra_format_options) const;
+
Result<std::unique_ptr<BatchReader>> ApplyPredicateFilterIfNeeded(
std::unique_ptr<BatchReader>&& reader, const
std::shared_ptr<Predicate>& predicate) const;
@@ -93,7 +109,7 @@ class AbstractSplitRead : public SplitRead {
const std::shared_ptr<arrow::Schema>& read_schema,
const std::shared_ptr<Predicate>& predicate, DeletionVector::Factory
dv_factory,
const std::optional<std::vector<Range>>& row_ranges,
- const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const = 0;
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const;
// 1. project write cols to data schema
// 2. add partition fields (if write cols not contain)
diff --git a/src/paimon/core/operation/append_only_file_store_scan.cpp
b/src/paimon/core/operation/append_only_file_store_scan.cpp
index 435ece57..78bf3c7a 100644
--- a/src/paimon/core/operation/append_only_file_store_scan.cpp
+++ b/src/paimon/core/operation/append_only_file_store_scan.cpp
@@ -18,31 +18,18 @@
#include "paimon/core/operation/append_only_file_store_scan.h"
-#include <cassert>
#include <cstdint>
#include <exception>
-#include <map>
-#include <optional>
-#include <set>
-#include <string>
-#include <utility>
-#include <vector>
-#include "arrow/type.h"
#include "fmt/format.h"
#include "paimon/common/predicate/predicate_filter.h"
-#include "paimon/common/types/data_field.h"
#include "paimon/core/core_options.h"
#include "paimon/core/io/data_file_meta.h"
-#include "paimon/core/io/file_index_evaluator.h"
#include "paimon/core/manifest/manifest_entry.h"
#include "paimon/core/schema/schema_manager.h"
#include "paimon/core/schema/table_schema.h"
#include "paimon/core/stats/simple_stats_evolution.h"
#include "paimon/core/stats/simple_stats_evolutions.h"
-#include "paimon/core/utils/field_mapping.h"
-#include "paimon/file_index/file_index_result.h"
-#include "paimon/predicate/predicate_utils.h"
#include "paimon/scan_context.h"
#include "paimon/status.h"
@@ -130,35 +117,7 @@ Result<bool> AppendOnlyFileStoreScan::FilterByStats(const
ManifestEntry& entry)
fmt::format("FilterByStats failed for file {}, with unknown
error", meta->file_name));
}
- if (!core_options_.FileIndexReadEnabled()) {
- return true;
- }
-
- return TestFileIndex(meta, evolution, data_schema);
-}
-
-Result<bool> AppendOnlyFileStoreScan::TestFileIndex(
- const std::shared_ptr<DataFileMeta>& meta,
- const std::shared_ptr<SimpleStatsEvolution>& evolution,
- const std::shared_ptr<TableSchema>& data_schema) const {
- std::shared_ptr<Predicate> data_predicate = predicates_;
- if (data_schema->Id() != table_schema_->Id()) {
- PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<Predicate>>
reconstruct_predicate,
-
FieldMappingBuilder::ReconstructPredicateWithDataFields(
- predicates_,
evolution->GetFieldNameToTableField(),
- evolution->GetFieldIdToDataField()));
-
- if (reconstruct_predicate == std::nullopt) {
- return true;
- }
- data_predicate = reconstruct_predicate.value();
- }
- assert(data_predicate);
- auto data_arrow_schema =
DataField::ConvertDataFieldsToArrowSchema(data_schema->Fields());
- PAIMON_ASSIGN_OR_RAISE(
- std::shared_ptr<FileIndexResult> index_result,
- FileIndexEvaluator::Evaluate(data_arrow_schema, data_predicate, meta,
pool_));
- return index_result->IsRemain();
+ return TestFileIndex(predicates_, meta, evolution, data_schema);
}
} // namespace paimon
diff --git a/src/paimon/core/operation/append_only_file_store_scan.h
b/src/paimon/core/operation/append_only_file_store_scan.h
index ba4feb1f..c7bac6bf 100644
--- a/src/paimon/core/operation/append_only_file_store_scan.h
+++ b/src/paimon/core/operation/append_only_file_store_scan.h
@@ -41,11 +41,9 @@ class ManifestList;
class MemoryPool;
class ScanFilter;
class SchemaManager;
-class SimpleStatsEvolution;
class SimpleStatsEvolutions;
class SnapshotManager;
class TableSchema;
-struct DataFileMeta;
/// `FileStoreScan` for `AppendOnlyFileStore`.
class AppendOnlyFileStoreScan : public FileStoreScan {
@@ -64,10 +62,6 @@ class AppendOnlyFileStoreScan : public FileStoreScan {
Result<bool> FilterByStats(const ManifestEntry& entry) const override;
private:
- Result<bool> TestFileIndex(const std::shared_ptr<DataFileMeta>& meta,
- const std::shared_ptr<SimpleStatsEvolution>&
evolution,
- const std::shared_ptr<TableSchema>&
data_schema) const;
-
AppendOnlyFileStoreScan(const std::shared_ptr<SnapshotManager>&
snapshot_manager,
const std::shared_ptr<SchemaManager>&
schema_manager,
const std::shared_ptr<ManifestList>& manifest_list,
diff --git a/src/paimon/core/operation/file_store_scan.cpp
b/src/paimon/core/operation/file_store_scan.cpp
index 45376b22..fa22ac56 100644
--- a/src/paimon/core/operation/file_store_scan.cpp
+++ b/src/paimon/core/operation/file_store_scan.cpp
@@ -38,6 +38,7 @@
#include "paimon/common/types/data_field.h"
#include "paimon/common/utils/field_type_utils.h"
#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/file_index_evaluator.h"
#include "paimon/core/manifest/file_entry.h"
#include "paimon/core/manifest/file_kind.h"
#include "paimon/core/manifest/manifest_file.h"
@@ -45,12 +46,14 @@
#include "paimon/core/manifest/manifest_list.h"
#include "paimon/core/manifest/snapshot_live_manifest_entries.h"
#include "paimon/core/partition/partition_info.h"
+#include "paimon/core/schema/table_schema.h"
#include "paimon/core/stats/simple_stats.h"
#include "paimon/core/stats/simple_stats_evolution.h"
#include "paimon/core/utils/branch_manager.h"
#include "paimon/core/utils/duration.h"
#include "paimon/core/utils/field_mapping.h"
#include "paimon/core/utils/snapshot_manager.h"
+#include "paimon/file_index/file_index_result.h"
#include "paimon/memory/bytes.h"
#include "paimon/memory/memory_segment.h"
#include "paimon/predicate/literal.h"
@@ -90,6 +93,34 @@ Result<std::shared_ptr<Predicate>>
FileStoreScan::ReconstructPredicateWithNonCas
return PredicateUtils::ExcludePredicateWithFields(predicate,
excluded_field_names);
}
+Result<bool> FileStoreScan::TestFileIndex(const std::shared_ptr<Predicate>&
predicate,
+ const std::shared_ptr<DataFileMeta>&
meta,
+ const
std::shared_ptr<SimpleStatsEvolution>& evolution,
+ const std::shared_ptr<TableSchema>&
data_schema) const {
+ if (!core_options_.FileIndexReadEnabled() || meta->embedded_index ==
nullptr) {
+ return true;
+ }
+
+ std::shared_ptr<Predicate> data_predicate = predicate;
+ if (data_schema->Id() != table_schema_->Id()) {
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<Predicate>>
reconstructed_predicate,
+
FieldMappingBuilder::ReconstructPredicateWithDataFields(
+ predicate,
evolution->GetFieldNameToTableField(),
+ evolution->GetFieldIdToDataField()));
+ if (!reconstructed_predicate) {
+ return true;
+ }
+ data_predicate = reconstructed_predicate.value();
+ }
+ assert(data_predicate);
+ std::shared_ptr<arrow::Schema> data_arrow_schema =
+ DataField::ConvertDataFieldsToArrowSchema(data_schema->Fields());
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<FileIndexResult> index_result,
+ FileIndexEvaluator::Evaluate(data_arrow_schema, data_predicate, meta,
pool_));
+ return index_result->IsRemain();
+}
+
std::vector<ManifestEntry> FileStoreScan::RawPlan::Files(const FileKind& kind)
{
std::vector<ManifestEntry> entries = Files();
std::vector<ManifestEntry> filtered_entries;
diff --git a/src/paimon/core/operation/file_store_scan.h
b/src/paimon/core/operation/file_store_scan.h
index 0eef2909..5fe128da 100644
--- a/src/paimon/core/operation/file_store_scan.h
+++ b/src/paimon/core/operation/file_store_scan.h
@@ -73,6 +73,7 @@ class SchemaManager;
class SimpleStatsEvolution;
class SnapshotManager;
class TableSchema;
+struct DataFileMeta;
/// Scan operation which produces a plan.
class FileStoreScan {
@@ -257,6 +258,13 @@ class FileStoreScan {
const std::shared_ptr<Predicate>& predicate,
const std::shared_ptr<SimpleStatsEvolution>& evolution);
+ /// Tests only an index embedded in DataFileMeta. External index files are
evaluated while
+ /// reading the retained files.
+ Result<bool> TestFileIndex(const std::shared_ptr<Predicate>& predicate,
+ const std::shared_ptr<DataFileMeta>& meta,
+ const std::shared_ptr<SimpleStatsEvolution>&
evolution,
+ const std::shared_ptr<TableSchema>&
data_schema) const;
+
private:
Status ReadManifests(std::optional<Snapshot>* snapshot_ptr,
std::vector<ManifestFileMeta>* all_manifests_ptr,
diff --git a/src/paimon/core/operation/key_value_file_store_scan.cpp
b/src/paimon/core/operation/key_value_file_store_scan.cpp
index b89a147a..e541fce3 100644
--- a/src/paimon/core/operation/key_value_file_store_scan.cpp
+++ b/src/paimon/core/operation/key_value_file_store_scan.cpp
@@ -171,9 +171,6 @@ Result<bool>
KeyValueFileStoreScan::FilterByValueFilter(const ManifestEntry& ent
if (!value_filter_) {
return true;
}
- if (entry.File()->embedded_index != nullptr) {
- return Status::NotImplemented("do not support embedded index in
DataFileMeta");
- }
const auto& meta = entry.File();
@@ -201,12 +198,12 @@ Result<bool>
KeyValueFileStoreScan::FilterByValueFilter(const ManifestEntry& ent
SimpleStatsEvolution::EvolutionStats new_stats,
evolution->Evolution(meta->value_stats, meta->row_count,
meta->value_stats_cols));
+ bool predicate_result = false;
try {
PAIMON_ASSIGN_OR_RAISE(
- bool predicate_result,
+ predicate_result,
predicate_filter->Test(schema_, meta->row_count,
*(new_stats.min_values),
*(new_stats.max_values),
*(new_stats.null_counts)));
- return predicate_result;
} catch (const std::exception& e) {
return Status::Invalid(fmt::format("FilterByValueFilter failed for
file {}, with {} error",
meta->file_name, e.what()));
@@ -214,6 +211,11 @@ Result<bool>
KeyValueFileStoreScan::FilterByValueFilter(const ManifestEntry& ent
return Status::Invalid(fmt::format(
"FilterByValueFilter failed for file {}, with unknown error",
meta->file_name));
}
+ if (!predicate_result) {
+ return false;
+ }
+
+ return TestFileIndex(value_filter_, meta, evolution, data_schema);
}
bool KeyValueFileStoreScan::NoOverlapping(const std::vector<ManifestEntry>&
entries) {
diff --git a/src/paimon/core/operation/merge_file_split_read.cpp
b/src/paimon/core/operation/merge_file_split_read.cpp
index 7c6b1724..679c48ca 100644
--- a/src/paimon/core/operation/merge_file_split_read.cpp
+++ b/src/paimon/core/operation/merge_file_split_read.cpp
@@ -26,8 +26,6 @@
#include <set>
#include <utility>
-#include "arrow/c/abi.h"
-#include "arrow/c/bridge.h"
#include "arrow/type.h"
#include "fmt/format.h"
#include "paimon/common/reader/complete_row_kind_batch_reader.h"
@@ -38,8 +36,6 @@
#include "paimon/common/utils/object_utils.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/core/core_options.h"
-#include "paimon/core/deletionvectors/apply_deletion_vector_batch_reader.h"
-#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
#include "paimon/core/deletionvectors/deletion_vector.h"
#include "paimon/core/io/async_key_value_projection_reader.h"
#include "paimon/core/io/concat_key_value_record_reader.h"
@@ -69,7 +65,6 @@
#include "paimon/predicate/predicate_utils.h"
#include "paimon/reader/file_batch_reader.h"
#include "paimon/table/source/data_split.h"
-#include "paimon/utils/roaring_bitmap32.h"
namespace paimon {
class BinaryRow;
@@ -482,47 +477,6 @@ MergeFileSplitRead::CreateMergeFunctionWrapper(const
CoreOptions& core_options,
return
std::make_shared<ReducerMergeFunctionWrapper>(std::move(merge_function));
}
-Result<std::unique_ptr<FileBatchReader>>
MergeFileSplitRead::ApplyIndexAndDvReaderIfNeeded(
- std::unique_ptr<FileBatchReader>&& file_reader, const
std::shared_ptr<DataFileMeta>& file,
- const std::shared_ptr<arrow::Schema>& data_schema,
- const std::shared_ptr<arrow::Schema>& read_schema, const
std::shared_ptr<Predicate>& predicate,
- DeletionVector::Factory dv_factory, const
std::optional<std::vector<Range>>& ranges,
- const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
- // merge read does not use index
- std::shared_ptr<DeletionVector> deletion_vector;
- if (dv_factory) {
- PAIMON_ASSIGN_OR_RAISE(deletion_vector, dv_factory(file->file_name));
- }
-
- const RoaringBitmap32* deletion = nullptr;
- if (auto* bitmap_dv =
dynamic_cast<BitmapDeletionVector*>(deletion_vector.get())) {
- deletion = bitmap_dv->GetBitmap();
- }
-
- std::optional<RoaringBitmap32> actual_selection;
- if (deletion) {
- actual_selection = *deletion;
- PAIMON_ASSIGN_OR_RAISE(uint64_t num_rows,
file_reader->GetNumberOfRows());
- actual_selection.value().Flip(0, num_rows);
- }
-
- ::ArrowSchema c_read_schema;
- PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema,
&c_read_schema));
-
- PAIMON_RETURN_NOT_OK(file_reader->SetReadSchema(&c_read_schema, predicate,
actual_selection));
-
- if (!file_reader->SupportPreciseBitmapSelection() && actual_selection) {
- return
std::make_unique<ApplyDeletionVectorBatchReader>(std::move(file_reader),
-
deletion_vector);
- }
- if (deletion_vector && !deletion && !deletion_vector->IsEmpty()) {
- // TODO(xinyu.lxy): if deletion vector is bitmap64, use
ApplyBitmapIndexBatchReader to
- // filter result
- return Status::NotImplemented("Only support BitmapDeletionVector");
- }
- return std::move(file_reader);
-}
-
Result<std::unique_ptr<BatchReader>> MergeFileSplitRead::CreateMergeReader(
const std::shared_ptr<DataSplitImpl>& data_split,
const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) {
@@ -867,19 +821,18 @@ Result<std::unique_ptr<KeyValueRecordReader>>
MergeFileSplitRead::CreateReaderFo
// no overlap in a run
const auto& data_files = sorted_run.Files();
PAIMON_ASSIGN_OR_RAISE(
- std::vector<std::unique_ptr<FileBatchReader>> raw_file_readers,
- CreateRawFileReaders(partition, data_files, read_schema_, predicate,
dv_factory,
- /*row_ranges=*/{}, data_file_path_factory,
- /*extra_format_options=*/{}));
+ std::vector<RawFileReaderWithMeta> raw_file_readers,
+ CreateRawFileReadersWithMeta(partition, data_files, read_schema_,
predicate, dv_factory,
+ /*row_ranges=*/{}, data_file_path_factory,
+ /*extra_format_options=*/{}));
- assert(data_files.size() == raw_file_readers.size());
// KeyValueDataFileRecordReader converts arrow array from format reader to
KeyValue objects
std::vector<std::unique_ptr<KeyValueRecordReader>> file_record_readers;
- file_record_readers.reserve(data_files.size());
- for (size_t i = 0; i < data_files.size(); i++) {
+ file_record_readers.reserve(raw_file_readers.size());
+ for (auto& raw_file_reader : raw_file_readers) {
file_record_readers.push_back(std::make_unique<KeyValueDataFileRecordReader>(
- std::move(raw_file_readers[i]), key_schema_, value_schema_,
data_files[i]->level,
- pool_));
+ std::move(raw_file_reader.reader), key_schema_, value_schema_,
+ raw_file_reader.file->level, pool_));
}
return
std::make_unique<ConcatKeyValueRecordReader>(std::move(file_record_readers));
}
diff --git a/src/paimon/core/operation/merge_file_split_read.h
b/src/paimon/core/operation/merge_file_split_read.h
index 07b5e70b..45f19a46 100644
--- a/src/paimon/core/operation/merge_file_split_read.h
+++ b/src/paimon/core/operation/merge_file_split_read.h
@@ -91,14 +91,6 @@ class MergeFileSplitRead : public AbstractSplitRead {
force_keep_delete_ = force_keep_delete;
}
- Result<std::unique_ptr<FileBatchReader>> ApplyIndexAndDvReaderIfNeeded(
- std::unique_ptr<FileBatchReader>&& file_reader, const
std::shared_ptr<DataFileMeta>& file,
- const std::shared_ptr<arrow::Schema>& data_schema,
- const std::shared_ptr<arrow::Schema>& read_schema,
- const std::shared_ptr<Predicate>& predicate, DeletionVector::Factory
dv_factory,
- const std::optional<std::vector<Range>>& ranges,
- const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const override;
-
Result<std::unique_ptr<SortMergeReader>> CreateSortMergeReaderForSection(
const std::vector<SortedRun>& section, const BinaryRow& partition,
DeletionVector::Factory dv_factory, const std::shared_ptr<Predicate>&
predicate,
diff --git a/src/paimon/core/operation/raw_file_split_read.cpp
b/src/paimon/core/operation/raw_file_split_read.cpp
index 8439ba19..8830b8ad 100644
--- a/src/paimon/core/operation/raw_file_split_read.cpp
+++ b/src/paimon/core/operation/raw_file_split_read.cpp
@@ -23,32 +23,23 @@
#include <utility>
#include <vector>
-#include "arrow/c/abi.h"
-#include "arrow/c/bridge.h"
#include "fmt/format.h"
-#include "paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader.h"
#include "paimon/common/reader/complete_row_kind_batch_reader.h"
#include "paimon/common/reader/concat_batch_reader.h"
-#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/object_utils.h"
#include "paimon/core/core_options.h"
-#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
#include "paimon/core/deletionvectors/deletion_vector.h"
#include "paimon/core/global_index/indexed_split_impl.h"
#include "paimon/core/io/data_file_meta.h"
-#include "paimon/core/io/file_index_evaluator.h"
#include "paimon/core/operation/internal_read_context.h"
#include "paimon/core/schema/schema_manager.h"
#include "paimon/core/schema/table_schema.h"
#include "paimon/core/table/source/data_split_impl.h"
#include "paimon/core/utils/file_store_path_factory.h"
-#include "paimon/file_index/bitmap_index_result.h"
-#include "paimon/file_index/file_index_result.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/reader/file_batch_reader.h"
#include "paimon/status.h"
#include "paimon/table/source/data_split.h"
-#include "paimon/utils/roaring_bitmap32.h"
namespace paimon {
class DataFilePathFactory;
@@ -189,91 +180,4 @@ Result<bool> RawFileSplitRead::Match(const
std::shared_ptr<Split>& split,
return matched;
}
-Result<std::unique_ptr<FileBatchReader>>
RawFileSplitRead::ApplyIndexAndDvReaderIfNeeded(
- std::unique_ptr<FileBatchReader>&& file_reader, const
std::shared_ptr<DataFileMeta>& file,
- const std::shared_ptr<arrow::Schema>& data_schema,
- const std::shared_ptr<arrow::Schema>& read_schema, const
std::shared_ptr<Predicate>& predicate,
- DeletionVector::Factory dv_factory, const
std::optional<std::vector<Range>>& ranges,
- const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
- std::shared_ptr<FileIndexResult> file_index_result;
- if (options_.FileIndexReadEnabled()) {
- PAIMON_ASSIGN_OR_RAISE(
- file_index_result,
- FileIndexEvaluator::Evaluate(data_schema, predicate,
data_file_path_factory, file,
- options_.GetFileSystem(), pool_));
- PAIMON_ASSIGN_OR_RAISE(bool is_remain, file_index_result->IsRemain());
- if (!is_remain) {
- return std::unique_ptr<FileBatchReader>();
- }
- }
- // prepare selection bitmap for index
- const RoaringBitmap32* selection = nullptr;
- if (auto* bitmap_file_index =
dynamic_cast<BitmapIndexResult*>(file_index_result.get())) {
- PAIMON_ASSIGN_OR_RAISE(selection, bitmap_file_index->GetBitmap());
- }
-
- // narrow the selection to the file-local row positions of an indexed split
- std::optional<RoaringBitmap32> ranges_selection;
- if (ranges != std::nullopt) {
- RoaringBitmap32 ranges_bitmap;
- for (const Range& range : ranges.value()) {
- ranges_bitmap.AddRange(static_cast<int32_t>(range.from),
- static_cast<int32_t>(range.to + 1));
- }
- if (selection != nullptr) {
- ranges_selection = RoaringBitmap32::And(*selection, ranges_bitmap);
- } else {
- ranges_selection = std::move(ranges_bitmap);
- }
- selection = &ranges_selection.value();
- }
-
- // prepare deletion bitmap for deletion vector
- std::shared_ptr<DeletionVector> deletion_vector;
- if (dv_factory) {
- PAIMON_ASSIGN_OR_RAISE(deletion_vector, dv_factory(file->file_name));
- }
- const RoaringBitmap32* deletion = nullptr;
- if (auto* bitmap_dv =
dynamic_cast<BitmapDeletionVector*>(deletion_vector.get())) {
- deletion = bitmap_dv->GetBitmap();
- }
-
- // merge deletion and bitmap index selection
- std::optional<RoaringBitmap32> actual_selection;
- if (selection && deletion) {
- actual_selection = RoaringBitmap32::AndNot(*selection, *deletion);
- } else if (selection) {
- actual_selection = *selection;
- } else if (deletion) {
- actual_selection = *deletion;
- PAIMON_ASSIGN_OR_RAISE(uint64_t num_rows,
file_reader->GetNumberOfRows());
- actual_selection.value().Flip(0, num_rows);
- }
-
- if (actual_selection && actual_selection.value().IsEmpty()) {
- return std::unique_ptr<FileBatchReader>();
- }
-
- ::ArrowSchema c_read_schema;
- PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema,
&c_read_schema));
- PAIMON_RETURN_NOT_OK(file_reader->SetReadSchema(&c_read_schema, predicate,
actual_selection));
-
- std::unique_ptr<FileBatchReader> reader;
- if (!file_reader->SupportPreciseBitmapSelection() && actual_selection) {
- // Some formats (for example blob) return an accurate batch result,
where
- // ApplyBitmapIndexBatchReader is not necessary
- reader =
std::make_unique<ApplyBitmapIndexBatchReader>(std::move(file_reader),
-
std::move(actual_selection).value());
- } else {
- reader = std::move(file_reader);
- }
-
- if (deletion_vector && !deletion && !deletion_vector->IsEmpty()) {
- // TODO(xinyu.lxy): if deletion vector is bitmap64, use
ApplyBitmapIndexBatchReader to
- // filter result
- return Status::NotImplemented("Only support BitmapDeletionVector");
- }
- return std::move(reader);
-}
-
} // namespace paimon
diff --git a/src/paimon/core/operation/raw_file_split_read.h
b/src/paimon/core/operation/raw_file_split_read.h
index 646f24ac..537232f0 100644
--- a/src/paimon/core/operation/raw_file_split_read.h
+++ b/src/paimon/core/operation/raw_file_split_read.h
@@ -83,14 +83,6 @@ class RawFileSplitRead : public AbstractSplitRead {
const std::optional<std::vector<Range>>& local_row_ranges);
Result<bool> Match(const std::shared_ptr<Split>& split, bool
force_keep_delete) const override;
-
- Result<std::unique_ptr<FileBatchReader>> ApplyIndexAndDvReaderIfNeeded(
- std::unique_ptr<FileBatchReader>&& file_reader, const
std::shared_ptr<DataFileMeta>& file,
- const std::shared_ptr<arrow::Schema>& data_schema,
- const std::shared_ptr<arrow::Schema>& read_schema,
- const std::shared_ptr<Predicate>& predicate, DeletionVector::Factory
dv_factory,
- const std::optional<std::vector<Range>>& ranges,
- const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const override;
};
} // namespace paimon
diff --git a/test/inte/realtime_write_inte_test.cpp
b/test/inte/realtime_write_inte_test.cpp
index 569a9d03..ee598ea4 100644
--- a/test/inte/realtime_write_inte_test.cpp
+++ b/test/inte/realtime_write_inte_test.cpp
@@ -2660,6 +2660,88 @@ TEST_F(RealtimeWriteInteTest,
TestAppendScanKeepsDiskSplitsIndependent) {
ASSERT_OK(PrepareAndClose(writer.get()));
}
+TEST_F(RealtimeWriteInteTest,
TestAppendRealtimeReadUsesEmbeddedAndExternalBitmapIndexes) {
+ options_["file-index.bitmap.columns"] = "payload";
+ options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1MB";
+ options_[Options::REALTIME_STORE_STATS_MODE] = "full";
+ CreateTable(/*partition_keys=*/{});
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
+ RealtimeContext::Create());
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> embedded_writer,
+ CreateRealtimeWriter(realtime_context));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> embedded_batch,
+ MakeBatch(
+ {{1, "embedded-one", "p0"}, {2, "embedded-match", "p0"}, {3,
"embedded-three", "p0"}},
+ /*partitioned=*/false));
+ ASSERT_OK(embedded_writer->Write(std::move(embedded_batch)));
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> embedded_progress,
+
embedded_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
+ std::vector<std::shared_ptr<DataFileMeta>> embedded_files =
NewFiles(embedded_progress);
+ ASSERT_EQ(1, embedded_files.size());
+ ASSERT_NE(nullptr, embedded_files[0]->embedded_index);
+ ASSERT_TRUE(embedded_files[0]->extra_files.empty());
+ ASSERT_OK_AND_ASSIGN(int64_t embedded_snapshot_id,
+ Commit(embedded_progress, /*commit_identifier=*/0));
+ ASSERT_OK(embedded_writer->RefreshCommittedSnapshot(embedded_snapshot_id));
+ ASSERT_OK(embedded_writer->Close());
+
+ options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> external_writer,
+ CreateRealtimeWriter(realtime_context));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> external_batch,
+ MakeBatch(
+ {{4, "external-four", "p0"}, {5, "external-match", "p0"}, {6,
"external-six", "p0"}},
+ /*partitioned=*/false));
+ ASSERT_OK(external_writer->Write(std::move(external_batch)));
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> external_progress,
+
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
+ std::vector<std::shared_ptr<DataFileMeta>> external_files =
NewFiles(external_progress);
+ ASSERT_EQ(1, external_files.size());
+ ASSERT_EQ(nullptr, external_files[0]->embedded_index);
+ ASSERT_EQ(1, external_files[0]->extra_files.size());
+ ASSERT_TRUE(external_files[0]->extra_files[0].has_value());
+ std::string external_index_path =
+ PathUtil::JoinPath(table_path_, "bucket-0/" +
external_files[0]->extra_files[0].value());
+ ASSERT_OK_AND_ASSIGN(bool external_index_exists,
+ dir_->GetFileSystem()->Exists(external_index_path));
+ ASSERT_TRUE(external_index_exists);
+ ASSERT_OK_AND_ASSIGN(int64_t external_snapshot_id,
+ Commit(external_progress, /*commit_identifier=*/1));
+ ASSERT_OK(external_writer->RefreshCommittedSnapshot(external_snapshot_id));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
+ MakeBatch({{7, "memory-seven", "p0"}},
/*partitioned=*/false));
+ ASSERT_OK(external_writer->Write(std::move(memory_batch)));
+
+ const std::string embedded_match = "embedded-match";
+ const std::string external_match = "external-match";
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<Predicate> predicate,
+ PredicateBuilder::Or(
+ {PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"payload",
FieldType::STRING,
+ Literal(FieldType::STRING, embedded_match.data(),
embedded_match.size())),
+ PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"payload",
FieldType::STRING,
+ Literal(FieldType::STRING, external_match.data(),
external_match.size()))}));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, predicate));
+ bool has_realtime_split = false;
+ for (const std::shared_ptr<Split>& split : plan->Splits()) {
+ has_realtime_split |= std::dynamic_pointer_cast<RealtimeSplit>(split)
!= nullptr;
+ }
+ ASSERT_TRUE(has_realtime_split);
+ // Residual filtering is disabled. Batch statistics remove the
non-matching memory batch, while
+ // the absence of the other disk rows verifies that both bitmap index
storage forms are used.
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan,
realtime_context, predicate,
+
/*enable_predicate_filter=*/false));
+ std::sort(actual_rows.begin(), actual_rows.end());
+ ASSERT_EQ((std::vector<Row>{{2, "embedded-match", "p0"}, {5,
"external-match", "p0"}}),
+ actual_rows);
+ ASSERT_OK(PrepareAndClose(external_writer.get()));
+}
+
TEST_F(RealtimeWriteInteTest, TestCommitOrdersPreparedOffsetRanges) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter());
@@ -2820,6 +2902,210 @@ TEST_F(RealtimeWriteInteTest,
TestRealtimeWriteAcrossAppendCompaction) {
ASSERT_OK(writer->Close());
}
+TEST_F(RealtimeWriteInteTest,
TestPkDvExternalBitmapCanEliminateEntireDiskFile) {
+ options_[Options::FILE_FORMAT] = "parquet";
+ options_[Options::DELETION_VECTORS_ENABLED] = "true";
+ options_[Options::FILE_INDEX_READ_ENABLED] = "true";
+ options_["file-index.bitmap.columns"] = "payload";
+ options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
+ options_[Options::REALTIME_STORE_STATS_MODE] = "full";
+ CreatePkTable();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
+ RealtimeContext::Create());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
+ CreateRealtimeWriter(realtime_context));
+
+ // "middle" is inside the file's min/max range but absent from its bitmap
index. Keeping the
+ // index external makes scan planning retain the file and lets the merge
reader eliminate it.
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
+ MakeBatch({{1, "aaa", "p0"}, {2, "zzz", "p0"}},
/*partitioned=*/false));
+ ASSERT_OK(writer->Write(std::move(disk_batch)));
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_progress,
+
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
+ ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id, Commit(disk_progress,
/*commit_identifier=*/0));
+ ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id));
+
+ ASSERT_OK_AND_ASSIGN(Snapshot compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
+
/*commit_identifier=*/1));
+ ASSERT_OK(writer->RefreshCommittedSnapshot(compact_snapshot.Id()));
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
+ MakeBatch({{3, "middle", "p0"}},
/*partitioned=*/false));
+ ASSERT_OK(writer->Write(std::move(memory_batch)));
+
+ const std::string middle = "middle";
+ const std::shared_ptr<Predicate> predicate = PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
+ Literal(FieldType::STRING, middle.data(), middle.size()));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, predicate));
+ ASSERT_EQ(1, plan->Splits().size());
+ std::shared_ptr<RealtimeSplit> realtime_split =
+ std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
+ ASSERT_NE(nullptr, realtime_split);
+
+ size_t external_high_level_file_count = 0;
+ for (const std::shared_ptr<Split>& split : realtime_split->DiskSplits()) {
+ std::shared_ptr<DataSplitImpl> data_split =
std::dynamic_pointer_cast<DataSplitImpl>(split);
+ ASSERT_NE(nullptr, data_split);
+ for (const std::shared_ptr<DataFileMeta>& file :
data_split->DataFiles()) {
+ if (file->level > 0 && file->embedded_index == nullptr &&
!file->extra_files.empty() &&
+ file->extra_files[0].has_value()) {
+ ++external_high_level_file_count;
+ }
+ }
+ }
+ ASSERT_EQ(1, external_high_level_file_count);
+
+ // The external bitmap removes the only disk reader. The in-memory
matching row must still be
+ // returned, and constructing the empty disk run must not rely on
positional file/reader pairs.
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan,
realtime_context, predicate,
+
/*enable_predicate_filter=*/false));
+ ASSERT_EQ((std::vector<Row>{{3, "middle", "p0"}}), actual_rows);
+ ASSERT_OK(PrepareAndClose(writer.get()));
+}
+
+TEST_F(RealtimeWriteInteTest,
TestPkDvRealtimeReadUsesEmbeddedAndExternalBitmapIndexes) {
+ options_[Options::FILE_FORMAT] = "parquet";
+ options_[Options::FILE_COMPRESSION] = "none";
+ options_[Options::DELETION_VECTORS_ENABLED] = "true";
+ options_["file-index.bitmap.columns"] = "payload";
+ options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1MB";
+ options_[Options::REALTIME_STORE_STATS_MODE] = "full";
+ CreatePkTable();
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
+ RealtimeContext::Create());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> embedded_writer,
+ CreateRealtimeWriter(realtime_context));
+
+ // Keep the max-level file substantially larger than the later L0 files.
Otherwise the
+ // size-ratio picker may rewrite all levels together instead of producing
a deletion vector.
+ // Put the padding in a non-indexed column so the data file is much larger
than both L0 files
+ // without making the embedded bitmap index itself large.
+ const std::string first_padding(256 * 1024, 'X');
+ const std::string third_padding(256 * 1024, 'Y');
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> base_batch,
+ MakeBatch({{1, "high-one", first_padding},
+ {2, "high-match", "p0"},
+ {3, "high-old-three", third_padding}},
+ /*partitioned=*/false));
+ ASSERT_OK(embedded_writer->Write(std::move(base_batch)));
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> base_progress,
+
embedded_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
+ std::vector<std::shared_ptr<DataFileMeta>> base_files =
NewFiles(base_progress);
+ ASSERT_EQ(1, base_files.size());
+ ASSERT_NE(nullptr, base_files[0]->embedded_index);
+ ASSERT_TRUE(base_files[0]->extra_files.empty());
+ ASSERT_OK_AND_ASSIGN(int64_t base_snapshot_id, Commit(base_progress,
/*commit_identifier=*/0));
+ ASSERT_OK(embedded_writer->RefreshCommittedSnapshot(base_snapshot_id));
+
+ ASSERT_OK_AND_ASSIGN(Snapshot full_compact_snapshot,
+ CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
+ /*commit_identifier=*/1));
+
ASSERT_OK(embedded_writer->RefreshCommittedSnapshot(full_compact_snapshot.Id()));
+ ASSERT_OK(embedded_writer->Close());
+
+ options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> external_writer,
+ CreateRealtimeWriter(realtime_context));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> external_batch,
+ MakeBatch({{3, "external-current-three", "p0"},
+ {4, "external-four", "p0"},
+ {5, "external-match", "p0"}},
+ /*partitioned=*/false, /*bucket=*/0,
+ {RecordBatch::RowKind::UPDATE_AFTER,
+ RecordBatch::RowKind::INSERT,
RecordBatch::RowKind::INSERT}));
+ ASSERT_OK(external_writer->Write(std::move(external_batch)));
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> external_progress,
+
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
+ std::vector<std::shared_ptr<DataFileMeta>> external_files =
NewFiles(external_progress);
+ ASSERT_EQ(1, external_files.size());
+ ASSERT_EQ(0, external_files[0]->level);
+ ASSERT_EQ(nullptr, external_files[0]->embedded_index);
+ ASSERT_EQ(1, external_files[0]->extra_files.size());
+ ASSERT_TRUE(external_files[0]->extra_files[0].has_value());
+ std::string external_index_path =
+ PathUtil::JoinPath(table_path_, "bucket-0/" +
external_files[0]->extra_files[0].value());
+ ASSERT_OK_AND_ASSIGN(bool external_index_exists,
+ dir_->GetFileSystem()->Exists(external_index_path));
+ ASSERT_TRUE(external_index_exists);
+ ASSERT_OK_AND_ASSIGN(int64_t external_snapshot_id,
+ Commit(external_progress, /*commit_identifier=*/2));
+ ASSERT_OK(external_writer->RefreshCommittedSnapshot(external_snapshot_id));
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_external_batch,
+ MakeBatch({{6, "external-six", "p0"}},
/*partitioned=*/false));
+ ASSERT_OK(external_writer->Write(std::move(second_external_batch)));
+ ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress>
second_external_progress,
+
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/3));
+ ASSERT_OK_AND_ASSIGN(int64_t second_external_snapshot_id,
+ Commit(second_external_progress,
/*commit_identifier=*/3));
+
ASSERT_OK(external_writer->RefreshCommittedSnapshot(second_external_snapshot_id));
+
+ ASSERT_OK_AND_ASSIGN(Snapshot dv_compact_snapshot,
+ CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
+ /*commit_identifier=*/4,
+ /*full_compaction=*/false));
+
ASSERT_OK(external_writer->RefreshCommittedSnapshot(dv_compact_snapshot.Id()));
+
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
+ MakeBatch({{7, "memory-seven", "p0"}},
/*partitioned=*/false));
+ ASSERT_OK(external_writer->Write(std::move(memory_batch)));
+
+ const std::string high_match = "high-match";
+ const std::string external_match = "external-match";
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<Predicate> predicate,
+ PredicateBuilder::Or(
+ {PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"payload",
FieldType::STRING,
+ Literal(FieldType::STRING, high_match.data(),
high_match.size())),
+ PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"payload",
FieldType::STRING,
+ Literal(FieldType::STRING, external_match.data(),
external_match.size()))}));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, predicate));
+ ASSERT_EQ(1, plan->Splits().size());
+ std::shared_ptr<RealtimeSplit> realtime_split =
+ std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
+ ASSERT_NE(nullptr, realtime_split);
+ ASSERT_OK_AND_ASSIGN(DiskFileLayout layout,
InspectDiskFiles(realtime_split->DiskSplits()));
+ ASSERT_TRUE(layout.has_high_level_file);
+ ASSERT_TRUE(layout.has_high_level_deletion_vector);
+ bool has_embedded_high_level_index_with_dv = false;
+ bool has_external_high_level_index = false;
+ for (const std::shared_ptr<Split>& split : realtime_split->DiskSplits()) {
+ std::shared_ptr<DataSplitImpl> data_split =
std::dynamic_pointer_cast<DataSplitImpl>(split);
+ ASSERT_NE(nullptr, data_split);
+ const std::vector<std::shared_ptr<DataFileMeta>>& files =
data_split->DataFiles();
+ const std::vector<std::optional<DeletionFile>>& deletion_files =
+ data_split->DeletionFiles();
+ for (size_t i = 0; i < files.size(); ++i) {
+ has_embedded_high_level_index_with_dv |=
+ files[i]->level > 0 && files[i]->embedded_index != nullptr &&
+ !deletion_files.empty() && deletion_files[i].has_value();
+ if (files[i]->level > 0 && files[i]->embedded_index == nullptr &&
+ !files[i]->extra_files.empty() &&
files[i]->extra_files[0].has_value()) {
+ has_external_high_level_index = true;
+ std::string index_path =
+ PathUtil::JoinPath(data_split->BucketPath(),
files[i]->extra_files[0].value());
+ ASSERT_OK_AND_ASSIGN(bool index_exists,
dir_->GetFileSystem()->Exists(index_path));
+ ASSERT_TRUE(index_exists);
+ }
+ }
+ }
+ ASSERT_TRUE(has_embedded_high_level_index_with_dv);
+ ASSERT_TRUE(has_external_high_level_index);
+
+ // Only primary-key predicates are sent to L0 and memory in PK MOR reads.
The non-key predicate
+ // still reaches both high-level bitmap indexes, while the unfiltered
memory row remains.
+ ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan,
realtime_context, predicate,
+
/*enable_predicate_filter=*/false));
+ std::sort(actual_rows.begin(), actual_rows.end());
+ ASSERT_EQ((std::vector<Row>{
+ {2, "high-match", "p0"}, {5, "external-match", "p0"}, {7,
"memory-seven", "p0"}}),
+ actual_rows);
+ ASSERT_OK(PrepareAndClose(external_writer.get()));
+}
+
TEST_F(RealtimeWriteInteTest, TestPkDvPredicateAcrossHighLevelLevel0AndMemory)
{
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
diff --git a/test/inte/scan_and_read_inte_test.cpp
b/test/inte/scan_and_read_inte_test.cpp
index 96f528bf..a8e3a6ed 100644
--- a/test/inte/scan_and_read_inte_test.cpp
+++ b/test/inte/scan_and_read_inte_test.cpp
@@ -3123,6 +3123,167 @@ TEST_P(ScanAndReadInteTest,
TestScanAndReadWithDisableIndex) {
ASSERT_TRUE(expected->Equals(read_result)) << read_result->ToString();
}
+TEST(ScanAndReadFileIndexInteTest, TestPkDvEmbeddedBitmapFiltersScanAndRead) {
+ std::unique_ptr<UniqueTestDirectory> dir =
UniqueTestDirectory::Create("local");
+ ASSERT_NE(nullptr, dir);
+ const std::string table_path = PathUtil::JoinPath(dir->Str(),
"foo.db/bar");
+ const arrow::FieldVector fields = {arrow::field("id", arrow::int32()),
+ arrow::field("indexed_value",
arrow::int32()),
+ arrow::field("payload", arrow::utf8())};
+ const std::shared_ptr<arrow::Schema> schema = arrow::schema(fields);
+ const std::map<std::string, std::string> options = {
+ {Options::FILE_FORMAT, "parquet"},
+ {Options::FILE_SYSTEM, "local"},
+ {Options::BUCKET, "1"},
+ {Options::BUCKET_KEY, "id"},
+ {Options::DELETION_VECTORS_ENABLED, "true"},
+ {Options::COMMIT_FORCE_COMPACT, "true"},
+ {Options::FILE_INDEX_READ_ENABLED, "true"},
+ {Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"},
+ {"file-index.bitmap.columns", "indexed_value"},
+ };
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<TestHelper> helper,
+ TestHelper::Create(dir->Str(), schema, /*partition_keys=*/{},
/*primary_keys=*/{"id"},
+ options, /*is_streaming_mode=*/true,
/*ignore_if_exists=*/false,
+ PathUtil::JoinPath(dir->Str(), "tmp")));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields),
+ R"([[1, 10, "one"], [2, 20, "two"], [3,
30, "three"]])",
+ /*partition_map=*/{}, /*bucket=*/0,
/*row_kinds=*/{}));
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<std::shared_ptr<CommitMessage>> commit_messages,
+ helper->WriteAndCommit(std::move(batch), /*commit_identifier=*/0,
std::nullopt));
+ ASSERT_FALSE(commit_messages.empty());
+
+ // 15 falls inside the file's [10, 30] min/max range, so statistics alone
must retain the
+ // file. Disabling file-index reads verifies that baseline explicitly.
+ const std::shared_ptr<Predicate> absent_predicate =
PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"indexed_value", FieldType::INT,
Literal(15));
+ ScanContextBuilder stats_only_scan_builder(table_path);
+ stats_only_scan_builder.SetOptions(options)
+ .AddOption(Options::FILE_INDEX_READ_ENABLED, "false")
+ .SetPredicate(absent_predicate);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> stats_only_scan_context,
+ stats_only_scan_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableScan> stats_only_scan,
+
TableScan::Create(std::move(stats_only_scan_context)));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> stats_only_plan,
stats_only_scan->CreatePlan());
+ ASSERT_FALSE(stats_only_plan->Splits().empty());
+
+ // The embedded bitmap knows that 15 is absent and eliminates the file
during scan planning.
+ ScanContextBuilder indexed_scan_builder(table_path);
+ indexed_scan_builder.SetOptions(options).SetPredicate(absent_predicate);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> indexed_scan_context,
+ indexed_scan_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableScan> indexed_scan,
+ TableScan::Create(std::move(indexed_scan_context)));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> indexed_plan,
indexed_scan->CreatePlan());
+ ASSERT_TRUE(indexed_plan->Splits().empty());
+
+ // For a present value, scan retains the embedded-index file and read
applies its precise
+ // bitmap. Residual predicate filtering stays disabled so only file-index
selection can remove
+ // the other rows.
+ const std::shared_ptr<Predicate> present_predicate =
PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"indexed_value", FieldType::INT,
Literal(20));
+ ScanContextBuilder retained_scan_builder(table_path);
+ retained_scan_builder.SetOptions(options).SetPredicate(present_predicate);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> retained_scan_context,
+ retained_scan_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableScan> retained_scan,
+ TableScan::Create(std::move(retained_scan_context)));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> retained_plan,
retained_scan->CreatePlan());
+ ASSERT_FALSE(retained_plan->Splits().empty());
+
+ ReadContextBuilder read_context_builder(table_path);
+ read_context_builder.SetOptions(options)
+ .SetPredicate(present_predicate)
+ .EnablePredicateFilter(false);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context,
read_context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
+ TableRead::Create(std::move(read_context)));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> batch_reader,
+ table_read->CreateReader(retained_plan->Splits()));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> actual,
+
ReadResultCollector::CollectResult(std::move(batch_reader)));
+ const std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
+ {arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("id",
arrow::int32()),
+ arrow::field("indexed_value", arrow::int32()),
arrow::field("payload", arrow::utf8())});
+ const std::shared_ptr<arrow::ChunkedArray> expected =
std::make_shared<arrow::ChunkedArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([[0, 2, 20,
"two"]])")
+ .ValueOrDie());
+ ASSERT_TRUE(expected->Equals(actual)) << actual->ToString();
+}
+
+TEST(ScanAndReadFileIndexInteTest,
TestPkDvReconstructsFileIndexPredicateAfterSchemaEvolution) {
+ std::unique_ptr<UniqueTestDirectory> dir =
UniqueTestDirectory::Create("local");
+ ASSERT_NE(nullptr, dir);
+ const std::string table_path = PathUtil::JoinPath(dir->Str(),
"foo.db/bar");
+ const arrow::FieldVector fields = {arrow::field("id", arrow::int32()),
+ arrow::field("indexed_value",
arrow::int32()),
+ arrow::field("payload", arrow::utf8())};
+ const std::shared_ptr<arrow::Schema> schema = arrow::schema(fields);
+ std::map<std::string, std::string> options = {
+ {Options::FILE_FORMAT, "parquet"},
+ {Options::FILE_SYSTEM, "local"},
+ {Options::BUCKET, "1"},
+ {Options::BUCKET_KEY, "id"},
+ {Options::DELETION_VECTORS_ENABLED, "true"},
+ {Options::COMMIT_FORCE_COMPACT, "true"},
+ {Options::FILE_INDEX_READ_ENABLED, "true"},
+ {Options::FILE_INDEX_IN_MANIFEST_THRESHOLD, "1MB"},
+ {"file-index.bitmap.columns", "indexed_value"},
+ };
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<TestHelper> helper,
+ TestHelper::Create(dir->Str(), schema, /*partition_keys=*/{},
/*primary_keys=*/{"id"},
+ options, /*is_streaming_mode=*/true,
/*ignore_if_exists=*/false,
+ PathUtil::JoinPath(dir->Str(), "tmp")));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<RecordBatch> batch,
+ TestHelper::MakeRecordBatch(arrow::struct_(fields),
+ R"([[1, 10, "one"], [2, 20, "two"], [3,
30, "three"]])",
+ /*partition_map=*/{}, /*bucket=*/0,
/*row_kinds=*/{}));
+ ASSERT_OK(helper->WriteAndCommit(std::move(batch),
/*commit_identifier=*/0, std::nullopt));
+ helper.reset();
+
+ // Keep field IDs and types unchanged while renaming the indexed value
field. The old file and
+ // its embedded bitmap still use "indexed_value", whereas scans use
"renamed_value".
+ options["file-index.bitmap.columns"] = "renamed_value";
+ const std::vector<DataField> evolved_fields = {
+ DataField(0, arrow::field("id", arrow::int32(), /*nullable=*/false)),
+ DataField(1, arrow::field("renamed_value", arrow::int32())),
+ DataField(2, arrow::field("payload", arrow::utf8()))};
+ ASSERT_OK(TestHelper::WriteNextSchema(dir->GetFileSystem(), table_path,
evolved_fields,
+ /*highest_field_id=*/2, options));
+
+ const std::shared_ptr<Predicate> predicate = PredicateBuilder::Equal(
+ /*field_index=*/1, /*field_name=*/"renamed_value", FieldType::INT,
Literal(15));
+
+ // The evolved min/max range is still [10, 30], so statistics alone retain
the old file.
+ ScanContextBuilder stats_only_scan_builder(table_path);
+ stats_only_scan_builder.SetOptions(options)
+ .AddOption(Options::FILE_INDEX_READ_ENABLED, "false")
+ .SetPredicate(predicate);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> stats_only_scan_context,
+ stats_only_scan_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableScan> stats_only_scan,
+
TableScan::Create(std::move(stats_only_scan_context)));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> stats_only_plan,
stats_only_scan->CreatePlan());
+ ASSERT_FALSE(stats_only_plan->Splits().empty());
+
+ // Reconstructing the predicate to schema-0 lets its embedded bitmap prove
that 15 is absent.
+ ScanContextBuilder indexed_scan_builder(table_path);
+ indexed_scan_builder.SetOptions(options).SetPredicate(predicate);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> indexed_scan_context,
+ indexed_scan_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableScan> indexed_scan,
+ TableScan::Create(std::move(indexed_scan_context)));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> indexed_plan,
indexed_scan->CreatePlan());
+ ASSERT_TRUE(indexed_plan->Splits().empty());
+}
+
TEST_P(ScanAndReadInteTest, TestPkDvTableIndexInDataAndWithExternalPath) {
auto file_format = FileFormat();
std::string table_path =