Skip to content

Commit da65ba6

Browse files
author
xuan.zhao
committed
feat(puffin): support deletion-vector-v1 blob read/write
1 parent 3c9d13d commit da65ba6

18 files changed

Lines changed: 1131 additions & 5 deletions

src/iceberg/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,15 @@ set(ICEBERG_DATA_SOURCES
188188
data/data_writer.cc
189189
data/delete_filter.cc
190190
data/delete_loader.cc
191+
data/deletion_vector_writer.cc
191192
data/equality_delete_writer.cc
192193
data/file_scan_task_reader.cc
193194
data/position_delete_writer.cc
194195
data/writer.cc
195196
deletes/position_delete_index.cc
196197
deletes/position_delete_range_consumer.cc
197198
deletes/roaring_position_bitmap.cc
199+
puffin/deletion_vector.cc
198200
puffin/file_metadata.cc
199201
puffin/json_serde.cc
200202
puffin/puffin_format.cc

src/iceberg/data/delete_loader.cc

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919

2020
#include "iceberg/data/delete_loader.h"
2121

22+
#include <cstdint>
2223
#include <cstring>
24+
#include <limits>
2325
#include <span>
2426
#include <string>
27+
#include <utility>
2528
#include <vector>
2629

2730
#include <nanoarrow/nanoarrow.h>
@@ -30,9 +33,12 @@
3033
#include "iceberg/arrow_c_data_guard_internal.h"
3134
#include "iceberg/deletes/position_delete_index.h"
3235
#include "iceberg/deletes/position_delete_range_consumer.h"
36+
#include "iceberg/deletes/roaring_position_bitmap.h"
37+
#include "iceberg/file_io.h"
3338
#include "iceberg/file_reader.h"
3439
#include "iceberg/manifest/manifest_entry.h"
3540
#include "iceberg/metadata_columns.h"
41+
#include "iceberg/puffin/deletion_vector.h"
3642
#include "iceberg/result.h"
3743
#include "iceberg/row/arrow_array_wrapper.h"
3844
#include "iceberg/schema.h"
@@ -171,7 +177,44 @@ Status DeleteLoader::LoadPositionDelete(const DataFile& file, PositionDeleteInde
171177
}
172178

173179
Status DeleteLoader::LoadDV(const DataFile& file, PositionDeleteIndex& index) const {
174-
return NotSupported("Loading deletion vectors is not yet supported");
180+
// A deletion vector must reference exactly one data file; without it the
181+
// caller cannot know which data file the positions apply to.
182+
ICEBERG_PRECHECK(file.referenced_data_file.has_value(),
183+
"Deletion vector requires referenced_data_file: {}", file.file_path);
184+
185+
// For deletion vectors, content_offset and content_size_in_bytes point directly
186+
// at the DV blob bytes within the Puffin file and are required by the spec.
187+
ICEBERG_PRECHECK(
188+
file.content_offset.has_value() && file.content_size_in_bytes.has_value(),
189+
"Deletion vector requires content_offset and content_size_in_bytes: {}",
190+
file.file_path);
191+
192+
const int64_t offset = file.content_offset.value();
193+
const int64_t length = file.content_size_in_bytes.value();
194+
ICEBERG_PRECHECK(offset >= 0 && length >= 0,
195+
"Invalid deletion vector offset/length: offset={}, length={}", offset,
196+
length);
197+
ICEBERG_PRECHECK(length <= std::numeric_limits<int32_t>::max(),
198+
"Cannot read deletion vector larger than 2GB: {}", length);
199+
200+
ICEBERG_ASSIGN_OR_RAISE(auto input_file, io_->NewInputFile(file.file_path));
201+
ICEBERG_ASSIGN_OR_RAISE(auto stream, input_file->Open());
202+
203+
std::vector<std::byte> bytes(static_cast<size_t>(length));
204+
ICEBERG_RETURN_UNEXPECTED(stream->ReadFully(offset, bytes));
205+
ICEBERG_RETURN_UNEXPECTED(stream->Close());
206+
207+
std::span<const uint8_t> blob(reinterpret_cast<const uint8_t*>(bytes.data()),
208+
bytes.size());
209+
ICEBERG_ASSIGN_OR_RAISE(auto bitmap, puffin::DeserializeDeletionVectorBlob(blob));
210+
211+
// The bitmap cardinality must match the record count recorded in metadata.
212+
ICEBERG_PRECHECK(std::cmp_equal(bitmap.Cardinality(), file.record_count),
213+
"Deletion vector cardinality {} does not match record count {}: {}",
214+
bitmap.Cardinality(), file.record_count, file.file_path);
215+
216+
bitmap.ForEach([&index](int64_t pos) { index.Delete(pos); });
217+
return {};
175218
}
176219

177220
Result<PositionDeleteIndex> DeleteLoader::LoadPositionDeletes(
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/data/deletion_vector_writer.h"
21+
22+
#include <map>
23+
#include <optional>
24+
#include <string>
25+
#include <utility>
26+
#include <vector>
27+
28+
#include "iceberg/deletes/roaring_position_bitmap.h"
29+
#include "iceberg/file_io.h"
30+
#include "iceberg/manifest/manifest_entry.h"
31+
#include "iceberg/partition_spec.h"
32+
#include "iceberg/puffin/deletion_vector.h"
33+
#include "iceberg/puffin/puffin_writer.h"
34+
#include "iceberg/util/macros.h"
35+
36+
namespace iceberg {
37+
38+
class DeletionVectorWriter::Impl {
39+
public:
40+
explicit Impl(DeletionVectorWriterOptions options) : options_(std::move(options)) {}
41+
42+
Status Delete(std::string_view referenced_data_file, int64_t pos) {
43+
ICEBERG_CHECK(!closed_, "Cannot delete after the writer is closed");
44+
ICEBERG_PRECHECK(!referenced_data_file.empty(),
45+
"Deletion vector requires a non-empty referenced data file");
46+
ICEBERG_PRECHECK(pos >= 0 && pos <= RoaringPositionBitmap::kMaxPosition,
47+
"Invalid deletion vector position: {}", pos);
48+
bitmaps_[std::string(referenced_data_file)].Add(pos);
49+
return {};
50+
}
51+
52+
Status Close() {
53+
if (closed_) {
54+
return {};
55+
}
56+
57+
// No deletes: skip creating an orphan Puffin file that no metadata would
58+
// reference, matching the Java DV writer.
59+
if (bitmaps_.empty()) {
60+
closed_ = true;
61+
return {};
62+
}
63+
64+
ICEBERG_ASSIGN_OR_RAISE(auto output_file, options_.io->NewOutputFile(options_.path));
65+
ICEBERG_ASSIGN_OR_RAISE(
66+
auto writer,
67+
puffin::PuffinWriter::Make(std::move(output_file), options_.properties));
68+
69+
// One blob per referenced data file, in deterministic (sorted) order.
70+
struct Entry {
71+
std::string referenced_data_file;
72+
int64_t offset;
73+
int64_t length;
74+
int64_t cardinality;
75+
};
76+
std::vector<Entry> entries;
77+
entries.reserve(bitmaps_.size());
78+
for (auto& [referenced_data_file, bitmap] : bitmaps_) {
79+
// Run-length encode before serializing for space efficiency, matching the
80+
// Java DV writer.
81+
bitmap.Optimize();
82+
ICEBERG_ASSIGN_OR_RAISE(
83+
auto blob, puffin::MakeDeletionVectorBlob(bitmap, referenced_data_file));
84+
ICEBERG_ASSIGN_OR_RAISE(auto blob_metadata, writer->Write(blob));
85+
entries.push_back(Entry{
86+
.referenced_data_file = referenced_data_file,
87+
.offset = blob_metadata.offset,
88+
.length = blob_metadata.length,
89+
.cardinality = static_cast<int64_t>(bitmap.Cardinality()),
90+
});
91+
}
92+
93+
ICEBERG_RETURN_UNEXPECTED(writer->Finish());
94+
ICEBERG_ASSIGN_OR_RAISE(file_size_, writer->FileSize());
95+
96+
const auto spec_id =
97+
options_.spec ? std::make_optional(options_.spec->spec_id()) : std::nullopt;
98+
99+
for (auto& entry : entries) {
100+
data_files_.push_back(std::make_shared<DataFile>(DataFile{
101+
.content = DataFile::Content::kPositionDeletes,
102+
.file_path = options_.path,
103+
.file_format = FileFormatType::kPuffin,
104+
.partition = options_.partition,
105+
.record_count = entry.cardinality,
106+
.file_size_in_bytes = file_size_,
107+
.referenced_data_file = std::move(entry.referenced_data_file),
108+
.content_offset = entry.offset,
109+
.content_size_in_bytes = entry.length,
110+
.partition_spec_id = spec_id,
111+
}));
112+
}
113+
114+
closed_ = true;
115+
return {};
116+
}
117+
118+
Result<FileWriter::WriteResult> Metadata() {
119+
ICEBERG_CHECK(closed_, "Cannot get metadata before closing the writer");
120+
FileWriter::WriteResult result;
121+
result.data_files = data_files_;
122+
return result;
123+
}
124+
125+
private:
126+
DeletionVectorWriterOptions options_;
127+
// Ordered by referenced data file path for deterministic blob layout.
128+
std::map<std::string, RoaringPositionBitmap> bitmaps_;
129+
std::vector<std::shared_ptr<DataFile>> data_files_;
130+
int64_t file_size_ = -1;
131+
bool closed_ = false;
132+
};
133+
134+
DeletionVectorWriter::DeletionVectorWriter(std::unique_ptr<Impl> impl)
135+
: impl_(std::move(impl)) {}
136+
137+
DeletionVectorWriter::~DeletionVectorWriter() = default;
138+
139+
Result<std::unique_ptr<DeletionVectorWriter>> DeletionVectorWriter::Make(
140+
DeletionVectorWriterOptions options) {
141+
ICEBERG_PRECHECK(options.io != nullptr, "DeletionVectorWriter requires a FileIO");
142+
ICEBERG_PRECHECK(!options.path.empty(), "DeletionVectorWriter requires a path");
143+
return std::unique_ptr<DeletionVectorWriter>(
144+
new DeletionVectorWriter(std::make_unique<Impl>(std::move(options))));
145+
}
146+
147+
Status DeletionVectorWriter::Delete(std::string_view referenced_data_file, int64_t pos) {
148+
return impl_->Delete(referenced_data_file, pos);
149+
}
150+
151+
Status DeletionVectorWriter::Close() { return impl_->Close(); }
152+
153+
Result<FileWriter::WriteResult> DeletionVectorWriter::Metadata() {
154+
return impl_->Metadata();
155+
}
156+
157+
} // namespace iceberg
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
/// \file iceberg/data/deletion_vector_writer.h
23+
/// Writer that emits deletion vectors as `deletion-vector-v1` blobs in a Puffin file.
24+
25+
#include <cstdint>
26+
#include <memory>
27+
#include <string>
28+
#include <string_view>
29+
#include <unordered_map>
30+
31+
#include "iceberg/data/writer.h"
32+
#include "iceberg/iceberg_data_export.h"
33+
#include "iceberg/result.h"
34+
#include "iceberg/row/partition_values.h"
35+
#include "iceberg/type_fwd.h"
36+
37+
namespace iceberg {
38+
39+
/// \brief Options for creating a DeletionVectorWriter.
40+
struct ICEBERG_DATA_EXPORT DeletionVectorWriterOptions {
41+
/// Output Puffin file location.
42+
std::string path;
43+
/// FileIO used to create the Puffin file.
44+
std::shared_ptr<FileIO> io;
45+
/// Partition spec the referenced data files belong to (optional).
46+
std::shared_ptr<PartitionSpec> spec;
47+
/// Partition the referenced data files belong to.
48+
PartitionValues partition;
49+
/// File-level Puffin properties (e.g. "created-by").
50+
std::unordered_map<std::string, std::string> properties;
51+
};
52+
53+
/// \brief Writes one or more deletion vectors into a single Puffin file.
54+
///
55+
/// Each referenced data file gets its own `deletion-vector-v1` blob. After
56+
/// Close(), Metadata() returns one DataFile per blob, each carrying the
57+
/// content_offset/content_size_in_bytes and referenced_data_file required to
58+
/// register the deletion vector in a manifest.
59+
///
60+
/// \note All referenced data files are assumed to belong to the single
61+
/// partition supplied in the options.
62+
class ICEBERG_DATA_EXPORT DeletionVectorWriter {
63+
public:
64+
~DeletionVectorWriter();
65+
66+
DeletionVectorWriter(const DeletionVectorWriter&) = delete;
67+
DeletionVectorWriter& operator=(const DeletionVectorWriter&) = delete;
68+
69+
/// \brief Create a new DeletionVectorWriter.
70+
static Result<std::unique_ptr<DeletionVectorWriter>> Make(
71+
DeletionVectorWriterOptions options);
72+
73+
/// \brief Mark a row position as deleted for the given data file.
74+
///
75+
/// Positions are accumulated per data file and serialized on Close().
76+
Status Delete(std::string_view referenced_data_file, int64_t pos);
77+
78+
/// \brief Write all accumulated deletion vectors to the Puffin file and close it.
79+
Status Close();
80+
81+
/// \brief Metadata for the DataFiles produced (one per referenced data file).
82+
/// \note Must be called after Close().
83+
Result<FileWriter::WriteResult> Metadata();
84+
85+
private:
86+
class Impl;
87+
std::unique_ptr<Impl> impl_;
88+
89+
explicit DeletionVectorWriter(std::unique_ptr<Impl> impl);
90+
};
91+
92+
} // namespace iceberg

src/iceberg/data/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ install_headers(
2020
'data_writer.h',
2121
'delete_filter.h',
2222
'delete_loader.h',
23+
'deletion_vector_writer.h',
2324
'equality_delete_writer.h',
2425
'file_scan_task_reader.h',
2526
'position_delete_writer.h',

src/iceberg/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,15 @@ iceberg_data_sources = files(
170170
'data/data_writer.cc',
171171
'data/delete_filter.cc',
172172
'data/delete_loader.cc',
173+
'data/deletion_vector_writer.cc',
173174
'data/equality_delete_writer.cc',
174175
'data/file_scan_task_reader.cc',
175176
'data/position_delete_writer.cc',
176177
'data/writer.cc',
177178
'deletes/position_delete_index.cc',
178179
'deletes/position_delete_range_consumer.cc',
179180
'deletes/roaring_position_bitmap.cc',
181+
'puffin/deletion_vector.cc',
180182
'puffin/file_metadata.cc',
181183
'puffin/json_serde.cc',
182184
'puffin/puffin_format.cc',

0 commit comments

Comments
 (0)