Skip to content

Commit ed369c5

Browse files
SteNicholas子懿
authored andcommitted
feat(read): support deletion vectors for data-evolution tables
Port the read side of Apache Paimon d2be7eace (#8380). A data-evolution table may now enable deletion-vectors.enabled: the deletion vector of a row range group is maintained against the group's anchor file, so every reader of the group applies it shifted by the file's offset inside the anchor row id range, and the blob fallback path drops the deleted row ids from its placeholder gap segments. That keeps the readers of a group positionally aligned for the column merge. - Add DataEvolutionUtils::RetrieveAnchorFile, matching Java's rule of the oldest normal file compared by (max_sequence_number, file_name) and skipping blob and vector-store files. - Drop the SchemaValidation rule forbidding the combination. - Support committing a deletion vector index file for a table without buckets, which a data-evolution table always is: combine such index files by file name, as Java's GlobalCombiner does, and drop the conflict detection rule that refused deletion vectors in BUCKET_UNAWARE mode. Without this the deletes another engine issues could not reach the table through this commit path at all. - Subtract deletion vector cardinality from DataSplit::MergedRowCount and report it unavailable when a deletion file has no cardinality. - Skip the limit push down when a non-partition filter or a row range index is present, since a split's metadata row count is then only an upper bound of what the read returns. - Apply the row ranges selection and the group deletion vectors to the blob view pre-read, so a reference held only by a dropped row is not resolved. Paimon C++ does not produce these deletion vectors; bitmap64 vectors and compaction of such a table remain unsupported. See docs/source/user_guide/compaction.rst. Behavior change worth calling out, since it reaches append and primary key tables too: ApplyPushDownLimit used to consider only raw convertible splits, silently leaving a merged split out of the pruned plan. It now asks every split for its row count and abandons the push down as soon as one cannot be counted from metadata, which is the same conservative answer the raw convertible path already gave. That is what lets a data-evolution split, which is not raw convertible but does have an exact count, take part in the pruning.
1 parent 3663237 commit ed369c5

24 files changed

Lines changed: 2437 additions & 179 deletions

docs/source/user_guide/compaction.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ not block writes.
4747
(``bucket > 0``). Dynamic bucketing (``bucket = -1``) does not support
4848
compaction. Tables with blob columns also skip compaction.
4949

50+
.. note::
51+
A data-evolution table (``data-evolution.enabled = true``) may enable
52+
``deletion-vectors.enabled``. Paimon C++ reads such a table: the deletion
53+
vector of a row range group is applied to every file of the group, so a
54+
deleted row disappears from all merged columns. Only the default 32-bit
55+
deletion vectors can be read; ``deletion-vectors.bitmap64`` is not supported
56+
yet, and a read fails when it actually encounters a 64-bit deletion vector.
57+
Paimon C++ currently reads but does not write deletion vectors for
58+
data-evolution tables, so the deletes themselves have to be issued by
59+
another engine. Compacting such a table is supported by Paimon Java but is
60+
not ported to Paimon C++ yet: auto compaction never runs on it, since data
61+
evolution requires ``bucket = -1``, and the dedicated compaction entry point
62+
rejects it outright rather than dropping the deletes.
63+
5064
Auto Compaction
5165
~~~~~~~~~~~~~~~
5266
During each flush, the writer triggers a best-effort auto compaction. The

src/paimon/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ set(PAIMON_CORE_SRCS
411411
core/utils/branch_manager.cpp
412412
core/utils/blob_view_lookup.cpp
413413
core/utils/consumer_manager.cpp
414+
core/utils/data_evolution_utils.cpp
414415
core/utils/field_mapping.cpp
415416
core/utils/nested_projection_utils.cpp
416417
core/utils/file_store_path_factory.cpp
@@ -862,6 +863,7 @@ if(PAIMON_BUILD_TESTS)
862863
core/utils/blob_view_lookup_test.cpp
863864
core/utils/branch_manager_test.cpp
864865
core/utils/consumer_manager_test.cpp
866+
core/utils/data_evolution_utils_test.cpp
865867
core/utils/file_store_path_factory_cache_test.cpp
866868
core/utils/field_mapping_test.cpp
867869
core/utils/nested_projection_utils_test.cpp

src/paimon/core/manifest/index_manifest_file_handler.cpp

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@
1919
#include "paimon/core/manifest/index_manifest_file_handler.h"
2020

2121
#include <set>
22+
#include <string>
2223
#include <unordered_map>
2324
#include <utility>
2425

26+
#include "fmt/format.h"
2527
#include "paimon/core/deletionvectors/deletion_vectors_index_file.h"
2628
namespace paimon {
2729

2830
using BucketIdentifier = std::tuple<BinaryRow, int32_t, std::string>;
2931

30-
std::vector<IndexManifestEntry> IndexManifestFileHandler::BucketedCombiner::Combine(
32+
Result<std::vector<IndexManifestEntry>> IndexManifestFileHandler::BucketedCombiner::Combine(
3133
const std::vector<IndexManifestEntry>& prev_index_files,
3234
const std::vector<IndexManifestEntry>& new_index_files) const {
3335
std::unordered_map<BucketIdentifier, IndexManifestEntry> index_entries;
@@ -67,7 +69,7 @@ std::vector<IndexManifestEntry> IndexManifestFileHandler::BucketedCombiner::Comb
6769
return result_entries;
6870
}
6971

70-
std::vector<IndexManifestEntry> IndexManifestFileHandler::GlobalFileNameCombiner::Combine(
72+
Result<std::vector<IndexManifestEntry>> IndexManifestFileHandler::GlobalFileNameCombiner::Combine(
7173
const std::vector<IndexManifestEntry>& prev_index_files,
7274
const std::vector<IndexManifestEntry>& new_index_files) const {
7375
std::map<std::string, IndexManifestEntry> index_entries;
@@ -104,6 +106,81 @@ std::vector<IndexManifestEntry> IndexManifestFileHandler::GlobalFileNameCombiner
104106
return result_entries;
105107
}
106108

109+
namespace {
110+
std::vector<std::string> DeletionVectorDataFiles(const IndexManifestEntry& entry) {
111+
if (entry.index_file->DvRanges() == std::nullopt) {
112+
return {};
113+
}
114+
return entry.index_file->DvRanges().value().key_vec();
115+
}
116+
} // namespace
117+
118+
Result<std::vector<IndexManifestEntry>>
119+
IndexManifestFileHandler::GlobalDeletionVectorCombiner::Combine(
120+
const std::vector<IndexManifestEntry>& prev_index_files,
121+
const std::vector<IndexManifestEntry>& new_index_files) const {
122+
std::map<std::string, IndexManifestEntry> index_entries;
123+
std::set<std::string> covered_data_files;
124+
for (const auto& entry : prev_index_files) {
125+
index_entries.insert_or_assign(entry.index_file->FileName(), entry);
126+
for (const auto& data_file : DeletionVectorDataFiles(entry)) {
127+
covered_data_files.insert(data_file);
128+
}
129+
}
130+
131+
std::vector<IndexManifestEntry> removed;
132+
removed.reserve(new_index_files.size());
133+
std::vector<IndexManifestEntry> added;
134+
added.reserve(new_index_files.size());
135+
136+
for (const auto& entry : new_index_files) {
137+
if (entry.kind == FileKind::Delete()) {
138+
removed.push_back(entry);
139+
} else if (entry.kind == FileKind::Add()) {
140+
added.push_back(entry);
141+
}
142+
}
143+
144+
// The deleted entry is processed first, so that an index file taking over the data files of
145+
// the one it replaces is not rejected as a second vector for them.
146+
for (const auto& entry : removed) {
147+
const std::string& file_name = entry.index_file->FileName();
148+
if (index_entries.erase(file_name) == 0) {
149+
return Status::Invalid(fmt::format(
150+
"Trying to delete deletion vector index file {} which does not exist.", file_name));
151+
}
152+
for (const auto& data_file : DeletionVectorDataFiles(entry)) {
153+
if (covered_data_files.erase(data_file) == 0) {
154+
return Status::Invalid(
155+
fmt::format("Trying to delete the deletion vector of data file {}, which does "
156+
"not exist.",
157+
data_file));
158+
}
159+
}
160+
}
161+
for (const auto& entry : added) {
162+
const std::string& file_name = entry.index_file->FileName();
163+
if (index_entries.find(file_name) != index_entries.end()) {
164+
return Status::Invalid(fmt::format(
165+
"Trying to add deletion vector index file {} which is already added.", file_name));
166+
}
167+
for (const auto& data_file : DeletionVectorDataFiles(entry)) {
168+
if (!covered_data_files.insert(data_file).second) {
169+
return Status::Invalid(fmt::format(
170+
"Trying to add a second deletion vector for data file {}.", data_file));
171+
}
172+
}
173+
index_entries.insert_or_assign(file_name, entry);
174+
}
175+
176+
std::vector<IndexManifestEntry> result_entries;
177+
result_entries.reserve(index_entries.size());
178+
for (const auto& [_, entry] : index_entries) {
179+
result_entries.push_back(entry);
180+
}
181+
return result_entries;
182+
}
183+
107184
Result<std::string> IndexManifestFileHandler::Write(
108185
const std::optional<std::string>& previous_index_manifest,
109186
const std::vector<IndexManifestEntry>& new_index_entries, int32_t bucket_mode,
@@ -138,8 +215,8 @@ Result<std::string> IndexManifestFileHandler::Write(
138215
GetIndexManifestFileCombine(index_type, bucket_mode));
139216
std::vector<IndexManifestEntry> typed_previous_entries = previous[index_type];
140217
std::vector<IndexManifestEntry> typed_current_entries = current[index_type];
141-
std::vector<IndexManifestEntry> combined_entries =
142-
combiner->Combine(typed_previous_entries, typed_current_entries);
218+
PAIMON_ASSIGN_OR_RAISE(std::vector<IndexManifestEntry> combined_entries,
219+
combiner->Combine(typed_previous_entries, typed_current_entries));
143220

144221
index_entries.insert(index_entries.end(), combined_entries.begin(), combined_entries.end());
145222
}
@@ -168,7 +245,7 @@ IndexManifestFileHandler::GetIndexManifestFileCombine(const std::string& index_t
168245
return std::make_unique<GlobalFileNameCombiner>();
169246
}
170247
if (index_type == DeletionVectorsIndexFile::DELETION_VECTORS_INDEX && bucket_mode == -1) {
171-
return Status::NotImplemented("not yet support dv with BUCKET_UNAWARE mode");
248+
return std::make_unique<GlobalDeletionVectorCombiner>();
172249
}
173250
return std::make_unique<BucketedCombiner>();
174251
}

src/paimon/core/manifest/index_manifest_file_handler.h

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,36 @@ class IndexManifestFileHandler {
4343
class IndexManifestFileCombiner {
4444
public:
4545
virtual ~IndexManifestFileCombiner() = default;
46-
virtual std::vector<IndexManifestEntry> Combine(
46+
virtual Result<std::vector<IndexManifestEntry>> Combine(
4747
const std::vector<IndexManifestEntry>& prev_index_files,
4848
const std::vector<IndexManifestEntry>& new_index_files) const = 0;
4949
};
5050

5151
/// Combine previous and new index files by partition, bucket and index type.
5252
class BucketedCombiner : public IndexManifestFileCombiner {
5353
public:
54-
std::vector<IndexManifestEntry> Combine(
54+
Result<std::vector<IndexManifestEntry>> Combine(
5555
const std::vector<IndexManifestEntry>& prev_index_files,
5656
const std::vector<IndexManifestEntry>& new_index_files) const override;
5757
};
5858

5959
/// Combine previous and new index files by file name.
6060
class GlobalFileNameCombiner : public IndexManifestFileCombiner {
6161
public:
62-
std::vector<IndexManifestEntry> Combine(
62+
Result<std::vector<IndexManifestEntry>> Combine(
63+
const std::vector<IndexManifestEntry>& prev_index_files,
64+
const std::vector<IndexManifestEntry>& new_index_files) const override;
65+
};
66+
67+
/// Combine previous and new deletion vector index files by file name, for a table without
68+
/// buckets, where the bucket cannot tell two index files apart.
69+
///
70+
/// A data file is covered by at most one deletion vector, so a delta that adds a second one
71+
/// for a data file, or drops one that is not there, was built against a different base and
72+
/// is rejected rather than silently changing which rows the read returns.
73+
class GlobalDeletionVectorCombiner : public IndexManifestFileCombiner {
74+
public:
75+
Result<std::vector<IndexManifestEntry>> Combine(
6376
const std::vector<IndexManifestEntry>& prev_index_files,
6477
const std::vector<IndexManifestEntry>& new_index_files) const override;
6578
};

src/paimon/core/manifest/index_manifest_file_handler_test.cpp

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#include <memory>
2222
#include <optional>
23+
#include <set>
2324
#include <string>
2425
#include <vector>
2526

@@ -218,17 +219,107 @@ TEST_F(IndexManifestFileHandlerTest, GlobalCombinerOverwritesDuplicateAddedEntri
218219
ASSERT_EQ(written_entries[0].index_file->RowCount(), 20);
219220
}
220221

221-
TEST_F(IndexManifestFileHandlerTest, DvWithBucketUnawareModeReturnsNotImplemented) {
222+
TEST_F(IndexManifestFileHandlerTest, GlobalDvCombinerReplacesIndexFileInBucketUnawareMode) {
222223
ASSERT_OK_AND_ASSIGN(auto index_manifest_file, CreateManifestFile(/*bucket_mode=*/-1));
223224

224225
auto partition = BinaryRow::EmptyRow();
226+
// an unaware bucket table writes every index file under bucket 0, so only the index file
227+
// name tells the two entries apart
228+
std::vector<IndexManifestEntry> previous_entries = {
229+
MakeDvEntry(FileKind::Add(), partition, /*bucket=*/0, "dv-0", {"data-0.orc"}, 1),
230+
MakeDvEntry(FileKind::Add(), partition, /*bucket=*/0, "dv-1", {"data-1.orc"}, 1)};
231+
232+
ASSERT_OK_AND_ASSIGN(std::string previous_manifest,
233+
IndexManifestFileHandler::Write(
234+
/*previous_index_manifest=*/std::nullopt, previous_entries,
235+
/*bucket_mode=*/-1, index_manifest_file.get()));
236+
237+
// updating the vector of data-0.orc replaces its index file, and the data file it covers
238+
// moves to the new one
225239
std::vector<IndexManifestEntry> new_entries = {
240+
MakeDvEntry(FileKind::Delete(), partition, /*bucket=*/0, "dv-0", {"data-0.orc"}, 1),
241+
MakeDvEntry(FileKind::Add(), partition, /*bucket=*/0, "dv-0-new", {"data-0.orc"}, 2)};
242+
243+
ASSERT_OK_AND_ASSIGN(
244+
std::string current_manifest,
245+
IndexManifestFileHandler::Write(previous_manifest, new_entries,
246+
/*bucket_mode=*/-1, index_manifest_file.get()));
247+
248+
std::vector<IndexManifestEntry> written_entries;
249+
ASSERT_OK(index_manifest_file->Read(current_manifest, /*filter=*/nullptr, &written_entries));
250+
ASSERT_EQ(written_entries.size(), 2);
251+
std::set<std::string> written_file_names;
252+
for (const auto& entry : written_entries) {
253+
written_file_names.insert(entry.index_file->FileName());
254+
}
255+
ASSERT_EQ(written_file_names, (std::set<std::string>{"dv-0-new", "dv-1"}));
256+
257+
// the same replacement with the added entry listed first: the delta carries no order the
258+
// commit path is bound to, so the added entry must not be read as a second vector for
259+
// data-0.orc just because its delete trails it
260+
std::vector<IndexManifestEntry> reordered_entries = {
261+
MakeDvEntry(FileKind::Add(), partition, /*bucket=*/0, "dv-0-newer", {"data-0.orc"}, 3),
262+
MakeDvEntry(FileKind::Delete(), partition, /*bucket=*/0, "dv-0-new", {"data-0.orc"}, 2)};
263+
264+
ASSERT_OK_AND_ASSIGN(
265+
std::string reordered_manifest,
266+
IndexManifestFileHandler::Write(current_manifest, reordered_entries,
267+
/*bucket_mode=*/-1, index_manifest_file.get()));
268+
269+
written_entries.clear();
270+
ASSERT_OK(index_manifest_file->Read(reordered_manifest, /*filter=*/nullptr, &written_entries));
271+
ASSERT_EQ(written_entries.size(), 2);
272+
written_file_names.clear();
273+
for (const auto& entry : written_entries) {
274+
written_file_names.insert(entry.index_file->FileName());
275+
}
276+
ASSERT_EQ(written_file_names, (std::set<std::string>{"dv-0-newer", "dv-1"}));
277+
}
278+
279+
TEST_F(IndexManifestFileHandlerTest, GlobalDvCombinerRejectsInconsistentDelta) {
280+
ASSERT_OK_AND_ASSIGN(auto index_manifest_file, CreateManifestFile(/*bucket_mode=*/-1));
281+
282+
auto partition = BinaryRow::EmptyRow();
283+
std::vector<IndexManifestEntry> previous_entries = {
226284
MakeDvEntry(FileKind::Add(), partition, /*bucket=*/0, "dv-0", {"data-0.orc"}, 1)};
227285

228-
ASSERT_NOK_WITH_MSG(IndexManifestFileHandler::Write(
229-
/*previous_index_manifest=*/std::nullopt, new_entries,
230-
/*bucket_mode=*/-1, index_manifest_file.get()),
231-
"not yet support dv with BUCKET_UNAWARE mode");
286+
ASSERT_OK_AND_ASSIGN(std::string previous_manifest,
287+
IndexManifestFileHandler::Write(
288+
/*previous_index_manifest=*/std::nullopt, previous_entries,
289+
/*bucket_mode=*/-1, index_manifest_file.get()));
290+
291+
// every delta below was built against a base the previous manifest is not, so applying it
292+
// would leave the read either with two vectors for one data file or with none
293+
{
294+
// adding a vector for data-0.orc without dropping the one it already has
295+
std::vector<IndexManifestEntry> new_entries = {
296+
MakeDvEntry(FileKind::Add(), partition, /*bucket=*/0, "dv-1", {"data-0.orc"}, 1)};
297+
298+
ASSERT_NOK_WITH_MSG(
299+
IndexManifestFileHandler::Write(previous_manifest, new_entries,
300+
/*bucket_mode=*/-1, index_manifest_file.get()),
301+
"Trying to add a second deletion vector for data file data-0.orc");
302+
}
303+
{
304+
// adding an index file the manifest already holds
305+
std::vector<IndexManifestEntry> new_entries = {
306+
MakeDvEntry(FileKind::Add(), partition, /*bucket=*/0, "dv-0", {"data-1.orc"}, 1)};
307+
308+
ASSERT_NOK_WITH_MSG(
309+
IndexManifestFileHandler::Write(previous_manifest, new_entries,
310+
/*bucket_mode=*/-1, index_manifest_file.get()),
311+
"Trying to add deletion vector index file dv-0 which is already added.");
312+
}
313+
{
314+
// deleting an index file the manifest does not hold
315+
std::vector<IndexManifestEntry> new_entries = {
316+
MakeDvEntry(FileKind::Delete(), partition, /*bucket=*/0, "dv-1", {"data-1.orc"}, 1)};
317+
318+
ASSERT_NOK_WITH_MSG(
319+
IndexManifestFileHandler::Write(previous_manifest, new_entries,
320+
/*bucket_mode=*/-1, index_manifest_file.get()),
321+
"Trying to delete deletion vector index file dv-1 which does not exist.");
322+
}
232323
}
233324

234325
} // namespace paimon::test

src/paimon/core/operation/commit/conflict_detection.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
#include "paimon/core/operation/commit/manifest_entry_changes.h"
4949
#include "paimon/core/operation/commit/row_id_column_conflict_checker.h"
5050
#include "paimon/core/schema/table_schema.h"
51-
#include "paimon/core/table/bucket_mode.h"
5251
#include "paimon/core/utils/field_mapping.h"
5352
#include "paimon/core/utils/file_store_path_factory.h"
5453
#include "paimon/core/utils/snapshot_manager.h"
@@ -115,12 +114,6 @@ Status ConflictDetection::CheckConflicts(
115114
row_id_column_conflict_checker,
116115
const Snapshot::CommitKind& commit_kind) const {
117116
std::string base_commit_user = latest_snapshot.CommitUser();
118-
if (options_.DeletionVectorsEnabled() &&
119-
ResolveBucketMode(options_.GetBucket(), table_schema_) == BucketMode::BUCKET_UNAWARE) {
120-
return Status::NotImplemented(
121-
"check conflicts failed. not yet support dv with BUCKET_UNAWARE mode");
122-
}
123-
124117
std::vector<ManifestEntry> all_entries = base_entries;
125118
all_entries.insert(all_entries.end(), delta_entries.begin(), delta_entries.end());
126119
PAIMON_RETURN_NOT_OK(CheckBucketKeepSame(all_entries, commit_kind, base_commit_user,

0 commit comments

Comments
 (0)