lxy-9602 commented on code in PR #191:
URL: https://github.com/apache/paimon-cpp/pull/191#discussion_r3755752353
##########
src/paimon/core/operation/commit/conflict_detection_test.cpp:
##########
@@ -572,41 +572,58 @@ TEST_F(ConflictDetectionTest,
TestBucketKeepSameCacheEviction) {
ASSERT_EQ(1U, evicted_partition_buckets.size());
}
-TEST_F(ConflictDetectionTest,
TestDeletionVectorsNotSupportedWithBucketUnawareMode) {
- ASSERT_OK_AND_ASSIGN(
- std::shared_ptr<TableSchema> table_schema,
- TableSchema::Create(/*schema_id=*/0, arrow::schema(fields_),
/*partition_keys=*/{"f1"},
- /*primary_keys=*/{}, /*options=*/{}));
- ASSERT_OK_AND_ASSIGN(CoreOptions core_options,
- CoreOptions::FromMap({{Options::BUCKET, "0"},
-
{Options::DELETION_VECTORS_ENABLED, "true"}}));
- ConflictDetection detection(table_schema, core_options,
/*snapshot_manager=*/nullptr,
- /*manifest_list=*/nullptr,
/*manifest_file=*/nullptr,
- /*commit_scanner=*/nullptr, "test_user",
"test_table",
- /*path_factory=*/nullptr);
+TEST_F(ConflictDetectionTest, TestDeletionVectorsAllowedWithBucketUnawareMode)
{
+ // an unaware bucket table, however its bucket resolves, may carry
deletion vectors, told
+ // apart by index file name rather than by bucket. Adding files commits;
dropping one needs
+ // the pairing this class does not build and is refused
+ for (const char* bucket : {"0", "-1"}) {
+ ASSERT_OK_AND_ASSIGN(
Review Comment:
If I understand correctly, bucket cannot be set to 0 in practice, because
table creation validation would fail. In that case, it seems
`ResolveBucketMode` might not need to support the `bucket == 0` branch.
##########
src/paimon/core/operation/data_evolution_split_read.cpp:
##########
@@ -48,9 +50,84 @@
#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/range_helper.h"
#include "paimon/core/core_options.h"
+#include "paimon/core/deletionvectors/apply_deletion_vector_batch_reader.h"
#include "paimon/core/global_index/indexed_split_impl.h"
#include "paimon/core/utils/blob_view_lookup.h"
+#include "paimon/core/utils/data_evolution_utils.h"
+
namespace paimon {
+namespace {
+/// A read-only view over one file's window of a row range group's deletion
vector. The reader
+/// probes with positions local to its file; IsDeleted(p) forwards them as
IsDeleted(p + offset),
+/// the anchor-relative positions the group deletion vector is indexed by.
+///
+/// The shift rides on a full DeletionVector because that is what
DeletionVector::Factory hands
+/// to ApplyIndexAndDvReaderIfNeeded; narrowing it to Paimon Java's read-only
DeletionVectorJudger
+/// would reach every caller of the factory. The mutating and serializing
halves reject instead.
+class PositionShiftedDeletionVector : public DeletionVector {
+ public:
+ PositionShiftedDeletionVector(const std::shared_ptr<DeletionVector>&
inner, int64_t offset,
+ int64_t length)
+ : inner_(inner), offset_(offset), length_(length) {}
+
+ Result<bool> IsDeleted(int64_t position) const override {
+ if (position < 0 || position >= length_) {
+ return Status::Invalid(
+ fmt::format("PositionShiftedDeletionVector position {} out of
window [0, {})",
+ position, length_));
+ }
+ return inner_->IsDeleted(position + offset_);
+ }
+
+ /// Conservative: false only means the group deletion vector holds
deletions somewhere, not
+ /// necessarily inside this window. Over-reporting just makes the wrapping
filter a no-op.
+ bool IsEmpty() const override {
+ return inner_->IsEmpty();
+ }
+
+ /// Deleted positions inside this window. Off the read path, which only
calls IsDeleted and
+ /// IsEmpty; the row counting that does consult a vector's cardinality
builds its factory
+ /// from the split's deletion files, never from this view.
+ ///
+ /// The interface cannot report a failure, so a failed IsValid is answered
with "everything
+ /// is deleted". That is the safe direction for a row count, which
subtracts it, but it is a
+ /// guess: the assert keeps it from passing silently where asserts are on.
+ int64_t GetCardinality() const override {
+ Result<RoaringBitmap32> valid = inner_->IsValid(offset_, length_);
+ if (!valid.ok()) {
+ assert(false);
+ return length_;
+ }
+ return length_ - valid.value().Cardinality();
Review Comment:
Would it make sense to change the base class to return `Result` instead, so
we can avoid `assert(false)`?
##########
test/inte/data_evolution_table_test.cpp:
##########
@@ -2108,6 +2353,618 @@ TEST_P(DataEvolutionTableTest, TestWithRowIds) {
}
}
+TEST_P(DataEvolutionTableTest, TestReadWithDeletionVectors) {
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+ auto schema = arrow::schema(fields_);
+
+ // full-row write assigns row ids 0-3, producing the anchor file of the
row range group
+ auto src_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x"],
+ [2, "b", "y"],
+ [3, "c", "z"],
+ [4, "d", "w"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(auto commit_msgs0,
+ WriteArray(table_path, schema->field_names(),
src_array));
+ SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs0);
+ ASSERT_OK(Commit(table_path, commit_msgs0));
+
+ // partial write of f2 over the same row range: the group merges columns
from two files
+ arrow::FieldVector f2_fields = {fields_[2]};
+ auto update_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(f2_fields),
R"([
+ ["x2"],
+ ["y2"],
+ ["z2"],
+ ["w2"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(auto commit_msgs1, WriteArray(table_path, {"f2"},
update_array));
+ SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs1);
+ ASSERT_OK(Commit(table_path, commit_msgs1));
+
+ auto expected_all = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x2"],
+ [2, "b", "y2"],
+ [3, "c", "z2"],
+ [4, "d", "w2"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_all));
+
+ ASSERT_OK_AND_ASSIGN(std::string anchor_file_name,
PlannedAnchorFileName(table_path));
+ ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs0[0],
+ {{anchor_file_name,
/*deleted_positions=*/{1, 3}}})
+ .status());
+
+ // both files of the group must drop the same rows to keep the column
merge aligned
+ auto expected_deleted = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x2"],
+ [3, "c", "z2"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(),
expected_deleted));
+
+ auto expected_with_row_id = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(
+ arrow::struct_({fields_[0], fields_[1], fields_[2],
SpecialFields::RowId().field_}),
+ R"([
+ [1, "a", "x2", 0],
+ [3, "c", "z2", 2]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "_ROW_ID"},
expected_with_row_id));
+
+ // a row-range selection composes with the deletion vector: rows {1, 2}
minus deleted {1}
+ auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [3, "c", "z2"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected,
+ /*predicate=*/nullptr, /*row_ranges=*/{Range(1,
2)}));
+}
+
+TEST_P(DataEvolutionTableTest, TestReadWithDeletionVectorsAcrossReadBatches) {
+ // the 12 rows below span several read batches, so the deletion vector
empties a whole
+ // batch of every file of the group: each file reader then skips that
batch entirely and
+ // the column merge has to stay aligned on the surviving row count alone
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true,
{{Options::READ_BATCH_SIZE, "4"}});
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+ auto schema = arrow::schema(fields_);
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>>
group_msgs,
+ WriteAndCommitGroup(table_path, /*first_row_id=*/0,
+ /*f0_values=*/{0, 1, 2, 3, 4, 5,
6, 7, 8, 9, 10, 11}));
+
+ // positions 4-7 cover a whole read batch, position 9 only part of the
next one
+ ASSERT_OK_AND_ASSIGN(std::string anchor_file_name,
PlannedAnchorFileName(table_path));
+ ASSERT_OK(CommitDeletionVectors(table_path, group_msgs[0],
+ {{anchor_file_name,
/*deleted_positions=*/{4, 5, 6, 7, 9}}})
+ .status());
+
+ auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [0, "a0", "y0"],
+ [1, "a1", "y1"],
+ [2, "a2", "y2"],
+ [3, "a3", "y3"],
+ [8, "a8", "y8"],
+ [10, "a10", "y10"],
+ [11, "a11", "y11"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array));
+
+ arrow::FieldVector row_id_fields = {SpecialFields::RowId().field_};
+ auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>(
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([
+ [0], [1], [2], [3], [8], [10], [11]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids));
+
+ // a row-range selection that spans the fully deleted batch composes with
it
+ auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [3, "a3", "y3"],
+ [8, "a8", "y8"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected,
+ /*predicate=*/nullptr, /*row_ranges=*/{Range(3,
8)}));
+}
+
+TEST_P(DataEvolutionTableTest,
TestReadWithDeletionVectorsOnPartOfRowRangeGroups) {
+ // one split per row range group, so a group's deletion file must not
reach the other
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true,
+ {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}});
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+ auto schema = arrow::schema(fields_);
+
+ auto src_array0 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x"],
+ [2, "b", "y"],
+ [3, "c", "z"],
+ [4, "d", "w"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(auto commit_msgs0,
+ WriteArray(table_path, schema->field_names(),
src_array0));
+ SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs0);
+ ASSERT_OK(Commit(table_path, commit_msgs0));
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [5, "e", "v"],
+ [6, "f", "u"],
+ [7, "g", "t"],
+ [8, "h", "s"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(auto commit_msgs1,
+ WriteArray(table_path, schema->field_names(),
src_array1));
+ SetFirstRowId(/*reset_first_row_id=*/4, commit_msgs1);
+ ASSERT_OK(Commit(table_path, commit_msgs1));
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names,
+ PlannedAnchorFileNames(table_path));
+ ASSERT_EQ(anchor_file_names.size(), 2);
+ ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs0[0],
+ {{anchor_file_names[0],
/*deleted_positions=*/{1, 3}}})
+ .status());
+
+ // the deletion vector applies to its own group only, the other group
keeps every row
+ auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x"],
+ [3, "c", "z"],
+ [5, "e", "v"],
+ [6, "f", "u"],
+ [7, "g", "t"],
+ [8, "h", "s"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array));
+
+ // a projection-only read drops the same rows
+ arrow::FieldVector f0_fields = {fields_[0]};
+ auto expected_f0 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(f0_fields),
R"([
+ [1], [3], [5], [6], [7], [8]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"f0"}, expected_f0));
+
+ arrow::FieldVector row_id_fields = {SpecialFields::RowId().field_};
+ auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>(
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([
+ [0], [2], [4], [5], [6], [7]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids));
+}
+
+TEST_P(DataEvolutionTableTest,
TestReadWithDeletionVectorsOnEveryRowRangeGroup) {
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+ auto schema = arrow::schema(fields_);
+
+ // one split (the default target size keeps both groups together), so a
single split
+ // deletion vector factory serves two groups anchored at different row ids
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<std::shared_ptr<CommitMessage>> group_msgs0,
+ WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0,
1, 2, 3}));
+ ASSERT_OK(
+ WriteAndCommitGroup(table_path, /*first_row_id=*/4, /*f0_values=*/{4,
5, 6, 7}).status());
+
+ ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names,
+ PlannedAnchorFileNames(table_path));
+ ASSERT_EQ(anchor_file_names.size(), 2);
+
+ // Positions are anchor-relative, so the groups deliberately delete
different ones: group 0
+ // drops {1, 3} of row ids 0-3, group 1 drops {0, 2} of row ids 4-7.
Reading a group with the
+ // other group's vector, or with its anchor range as the shift base,
cannot match below.
+ ASSERT_OK(CommitDeletionVectors(table_path, group_msgs0[0],
+ {{anchor_file_names[0],
/*deleted_positions=*/{1, 3}},
+ {anchor_file_names[1],
/*deleted_positions=*/{0, 2}}})
+ .status());
+
+ // The read looks a group's deletion vector up by its anchor file name, so
the scan has to
+ // hand it exactly that. Asserting it separates a scan-side mix-up from a
read-side one.
+ ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<Split>> planned_splits,
+ PlanSplits(table_path));
+ ASSERT_EQ(planned_splits.size(), 1);
+ auto planned_split_impl =
std::dynamic_pointer_cast<DataSplitImpl>(planned_splits[0]);
+ ASSERT_TRUE(planned_split_impl);
+
+ ASSERT_OK_AND_ASSIGN(DeletionCardinalityMap cardinality_by_file,
+ DeletionCardinalityByDataFile(planned_splits[0]));
+ DeletionCardinalityMap expected_cardinalities = {{anchor_file_names[0], 2},
+ {anchor_file_names[1],
2}};
+ ASSERT_EQ(cardinality_by_file, expected_cardinalities);
+
+ // the same split reports the surviving row count the limit push down
prunes on: the two
+ // groups hold 4 rows each and each deletion vector drops 2 of them
+ ASSERT_OK_AND_ASSIGN(std::optional<int64_t> merged_row_count,
+ planned_split_impl->MergedRowCount());
+ ASSERT_EQ(std::optional<int64_t>(4), merged_row_count);
+
+ // a count query answers from that metadata alone, never reading a row, so
the deletion
+ // vectors have to reach it too: without them it reports the 8 rows the
files hold
+ ReadContextBuilder count_context_builder(table_path);
+ count_context_builder.SetReadFieldNames(schema->field_names());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> count_context,
+ count_context_builder.Finish());
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> count_table_read,
+ TableRead::Create(std::move(count_context)));
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<CountReader> count_reader,
+ count_table_read->CreateCountReader(planned_splits));
+ ASSERT_OK_AND_ASSIGN(int64_t counted_rows, count_reader->CountRows());
+ ASSERT_EQ(counted_rows, 4);
+
+ auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [0, "a0", "y0"],
+ [2, "a2", "y2"],
+ [5, "a1", "y1"],
+ [7, "a3", "y3"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array));
+
+ arrow::FieldVector row_id_fields = {SpecialFields::RowId().field_};
+ auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>(
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([
+ [0], [2], [5], [7]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids));
+
+ // a row-range selection straddling the group boundary composes with both
deletion vectors:
+ // row ids {2, 3, 4, 5} minus the deleted {3, 4}
+ auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [2, "a2", "y2"],
+ [5, "a1", "y1"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected,
+ /*predicate=*/nullptr, /*row_ranges=*/{Range(2,
5)}));
+}
+
+TEST_P(DataEvolutionTableTest, TestReadWithFullyDeletedRowRangeGroup) {
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+ auto schema = arrow::schema(fields_);
+
+ // one split (the default target size keeps both groups together), so the
emptied group's
+ // readers are concatenated with the surviving group's
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<std::shared_ptr<CommitMessage>> group_msgs0,
+ WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0,
1, 2, 3}));
+ ASSERT_OK(
+ WriteAndCommitGroup(table_path, /*first_row_id=*/4, /*f0_values=*/{4,
5, 6, 7}).status());
+
+ // both file readers of the first group then yield nothing, and its column
merge must
+ // produce no rows at all instead of misaligning
+ ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names,
+ PlannedAnchorFileNames(table_path));
+ ASSERT_EQ(anchor_file_names.size(), 2);
+ ASSERT_OK(CommitDeletionVectors(table_path, group_msgs0[0],
+ {{anchor_file_names[0],
/*deleted_positions=*/{0, 1, 2, 3}}})
+ .status());
+
+ auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [4, "a0", "y0"],
+ [5, "a1", "y1"],
+ [6, "a2", "y2"],
+ [7, "a3", "y3"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_array));
+
+ arrow::FieldVector row_id_fields = {SpecialFields::RowId().field_};
+ auto expected_row_ids = std::dynamic_pointer_cast<arrow::StructArray>(
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(row_id_fields), R"([
+ [4], [5], [6], [7]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"_ROW_ID"}, expected_row_ids));
+
+ // a row-range selection spanning both groups keeps only what survives in
the second one
+ auto expected_selected = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [4, "a0", "y0"],
+ [5, "a1", "y1"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(), expected_selected,
+ /*predicate=*/nullptr, /*row_ranges=*/{Range(0,
5)}));
+
+ // a selection covering only deleted rows returns nothing. The plan is
asserted non-empty
+ // too: the scan cannot prune the split on row ids alone, so the emptiness
comes from the
+ // deletion vector rather than from a plan with nothing to read.
+ ASSERT_OK_AND_ASSIGN(LimitScanResult only_deleted,
+ ScanAndReadWithLimit(table_path,
schema->field_names(), /*limit=*/100,
+ /*predicate=*/nullptr,
+ /*row_ranges=*/{Range(0, 3)}));
+ ASSERT_FALSE(only_deleted.splits.empty());
+ ASSERT_FALSE(only_deleted.rows);
+}
+
+TEST_P(DataEvolutionTableTest, TestReadAfterUpdatingDeletionVectors) {
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+ auto schema = arrow::schema(fields_);
+
+ auto src_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x"],
+ [2, "b", "y"],
+ [3, "c", "z"],
+ [4, "d", "w"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(auto commit_msgs,
+ WriteArray(table_path, schema->field_names(),
src_array));
+ SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs);
+ ASSERT_OK(Commit(table_path, commit_msgs));
+
+ ASSERT_OK_AND_ASSIGN(std::string anchor_file_name,
PlannedAnchorFileName(table_path));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<CommitMessage> first_dv_msg,
+ CommitDeletionVectors(table_path, commit_msgs[0],
+ {{anchor_file_name,
/*deleted_positions=*/{1}}}));
+
+ auto expected_after_first = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x"],
+ [3, "c", "z"],
+ [4, "d", "w"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(),
expected_after_first));
+
+ // a second deletion vector replaces the first one instead of both staying
live
+ ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs[0],
+ {{anchor_file_name,
/*deleted_positions=*/{1, 3}}},
+ /*replaced_commit_msg=*/first_dv_msg)
+ .status());
+
+ auto expected_after_update = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x"],
+ [3, "c", "z"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, schema->field_names(),
expected_after_update));
+}
+
+TEST_P(DataEvolutionTableTest, TestReadWithDeletionVectorsAfterAddingColumn) {
+ if (FileFormat() == "avro") {
+ GTEST_SKIP() << "Avro has no stats, which the added column's scan
pruning relies on";
+ }
+ std::map<std::string, std::string> options =
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true);
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+
+ auto src_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), R"([
+ [1, "a", "x"],
+ [2, "b", "y"],
+ [3, "c", "z"],
+ [4, "d", "w"]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(auto commit_msgs,
+ WriteArray(table_path,
arrow::schema(fields_)->field_names(), src_array));
+ SetFirstRowId(/*reset_first_row_id=*/0, commit_msgs);
+ ASSERT_OK(Commit(table_path, commit_msgs));
+
+ // add column f3, then fill it for the same row range: the group merges
columns from two
+ // files written under different schema ids
+ auto f3 = arrow::field("f3", arrow::int64());
+ ASSERT_OK(TestHelper::WriteNextSchema(dir_->GetFileSystem(), table_path,
+ {DataField(0, fields_[0]),
DataField(1, fields_[1]),
+ DataField(2, fields_[2]),
DataField(3, f3)},
+ /*highest_field_id=*/3, options));
+
+ auto f3_array = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f3}), R"([
+ [10], [20], [30], [40]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK_AND_ASSIGN(auto f3_commit_msgs, WriteArray(table_path, {"f3"},
f3_array));
+ SetFirstRowId(/*reset_first_row_id=*/0, f3_commit_msgs);
+ ASSERT_OK(Commit(table_path, f3_commit_msgs));
+
+ // the deletion vector is still anchored on the oldest normal file,
written before the
+ // column was added
+ ASSERT_OK_AND_ASSIGN(std::string anchor_file_name,
PlannedAnchorFileName(table_path));
+ ASSERT_OK(CommitDeletionVectors(table_path, commit_msgs[0],
+ {{anchor_file_name,
/*deleted_positions=*/{1, 3}}})
+ .status());
+
+ arrow::FieldVector evolved_fields = {fields_[0], fields_[1], fields_[2],
f3};
+ auto expected_array = std::dynamic_pointer_cast<arrow::StructArray>(
+
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(evolved_fields), R"([
+ [1, "a", "x", 10],
+ [3, "c", "z", 30]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"f0", "f1", "f2", "f3"},
expected_array));
+
+ // projecting only the added column keeps the same surviving rows
+ auto expected_f3 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({f3}), R"([
+ [10], [30]
+ ])")
+ .ValueOrDie());
+ ASSERT_OK(ScanAndRead(table_path, {"f3"}, expected_f3));
+}
+
+TEST_P(DataEvolutionTableTest,
TestLimitPushDownWithHeavilyDeletedFirstRowRangeGroup) {
+ // one split per row range group, so the limit has to span both to be
satisfied
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/true,
+ {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}});
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<std::shared_ptr<CommitMessage>> group_msgs0,
+ WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0,
1, 2, 3}));
+ ASSERT_OK(
+ WriteAndCommitGroup(table_path, /*first_row_id=*/4, /*f0_values=*/{4,
5, 6, 7}).status());
+
+ // the first group keeps a single surviving row
+ ASSERT_OK_AND_ASSIGN(std::vector<std::string> anchor_file_names,
+ PlannedAnchorFileNames(table_path));
+ ASSERT_EQ(anchor_file_names.size(), 2);
+ ASSERT_OK(CommitDeletionVectors(table_path, group_msgs0[0],
+ {{anchor_file_names[0],
/*deleted_positions=*/{0, 1, 2}}})
+ .status());
+
+ // the first split alone satisfies a limit of 1: it still holds the one
row that survived
+ // the deletion vector
+ ASSERT_OK_AND_ASSIGN(LimitScanResult limit_1,
+ ScanAndReadWithLimit(table_path, {"f0"},
/*limit=*/1));
+ ASSERT_EQ(limit_1.splits.size(), 1);
+ ASSERT_OK_AND_ASSIGN(std::vector<int32_t> limit_1_values,
CollectF0Values(limit_1.rows));
+ ASSERT_EQ(limit_1_values, (std::vector<int32_t>{3}));
+
+ // the first split contributes only one surviving row, so a limit of 3
needs the second one
+ ASSERT_OK_AND_ASSIGN(LimitScanResult limit_3,
+ ScanAndReadWithLimit(table_path, {"f0"},
/*limit=*/3));
+ ASSERT_EQ(limit_3.splits.size(), 2);
+ ASSERT_OK_AND_ASSIGN(std::vector<int32_t> limit_3_values,
CollectF0Values(limit_3.rows));
+ ASSERT_EQ(limit_3_values, (std::vector<int32_t>{3, 4, 5, 6, 7}));
+}
+
+TEST_P(DataEvolutionTableTest, TestLimitPushDownDisabledByNonPartitionFilter) {
+ // one split per row range group, so the plan can drop the group holding
the matches
+ CreateDataEvolutionTable(/*deletion_vectors_enabled=*/false,
+ {{Options::SOURCE_SPLIT_TARGET_SIZE, "1"}});
+ std::string table_path = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
+
+ // First row range group: no f0 value lies in [100, 200], but the 300
keeps the group's stats
+ // range straddling the filter so the scan cannot prune it. It therefore
reaches the plan
+ // reporting four rows, and contributes none of them to the result.
+ ASSERT_OK(
+ WriteAndCommitGroup(table_path, /*first_row_id=*/0, /*f0_values=*/{0,
1, 2, 300}).status());
+ // second row range group: every f0 value matches
+ ASSERT_OK(WriteAndCommitGroup(table_path, /*first_row_id=*/4,
+ /*f0_values=*/{100, 101, 102, 103})
+ .status());
Review Comment:
It looks like we could replace `ASSERT_OK(Func.status())` with
`ASSERT_OK(Func())` here as well. Could you please adjust it?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]