Skip to content

Commit 24a9c20

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

16 files changed

Lines changed: 1137 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: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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 <format>
23+
#include <map>
24+
#include <optional>
25+
#include <string>
26+
#include <utility>
27+
#include <vector>
28+
29+
#include "iceberg/deletes/roaring_position_bitmap.h"
30+
#include "iceberg/file_io.h"
31+
#include "iceberg/manifest/manifest_entry.h"
32+
#include "iceberg/partition_spec.h"
33+
#include "iceberg/puffin/deletion_vector.h"
34+
#include "iceberg/puffin/file_metadata.h"
35+
#include "iceberg/puffin/puffin_writer.h"
36+
#include "iceberg/util/macros.h"
37+
#include "iceberg/version.h"
38+
39+
namespace iceberg {
40+
41+
class DeletionVectorWriter::Impl {
42+
public:
43+
explicit Impl(DeletionVectorWriterOptions options) : options_(std::move(options)) {}
44+
45+
Status Delete(std::string_view referenced_data_file, int64_t pos) {
46+
ICEBERG_CHECK(!closed_, "Cannot delete after the writer is closed");
47+
ICEBERG_PRECHECK(!referenced_data_file.empty(),
48+
"Deletion vector requires a non-empty referenced data file");
49+
ICEBERG_PRECHECK(pos >= 0 && pos <= RoaringPositionBitmap::kMaxPosition,
50+
"Invalid deletion vector position: {}", pos);
51+
bitmaps_[std::string(referenced_data_file)].Add(pos);
52+
return {};
53+
}
54+
55+
Status Close() {
56+
if (closed_) {
57+
return {};
58+
}
59+
60+
// No deletes: skip creating an orphan Puffin file that no metadata
61+
// references.
62+
if (bitmaps_.empty()) {
63+
closed_ = true;
64+
return {};
65+
}
66+
67+
auto properties = options_.properties;
68+
properties.try_emplace(std::string(puffin::StandardPuffinProperties::kCreatedBy),
69+
std::format("iceberg-cpp/{}.{}.{}", ICEBERG_VERSION_MAJOR,
70+
ICEBERG_VERSION_MINOR, ICEBERG_VERSION_PATCH));
71+
72+
ICEBERG_ASSIGN_OR_RAISE(auto output_file, options_.io->NewOutputFile(options_.path));
73+
ICEBERG_ASSIGN_OR_RAISE(
74+
auto writer,
75+
puffin::PuffinWriter::Make(std::move(output_file), std::move(properties)));
76+
77+
// One blob per referenced data file, in deterministic (sorted) order.
78+
struct Entry {
79+
std::string referenced_data_file;
80+
int64_t offset;
81+
int64_t length;
82+
int64_t cardinality;
83+
};
84+
std::vector<Entry> entries;
85+
entries.reserve(bitmaps_.size());
86+
for (auto& [referenced_data_file, bitmap] : bitmaps_) {
87+
bitmap.Optimize(); // run-length encode before serializing
88+
ICEBERG_ASSIGN_OR_RAISE(
89+
auto blob, puffin::MakeDeletionVectorBlob(bitmap, referenced_data_file));
90+
ICEBERG_ASSIGN_OR_RAISE(auto blob_metadata, writer->Write(blob));
91+
entries.push_back(Entry{
92+
.referenced_data_file = referenced_data_file,
93+
.offset = blob_metadata.offset,
94+
.length = blob_metadata.length,
95+
.cardinality = static_cast<int64_t>(bitmap.Cardinality()),
96+
});
97+
}
98+
99+
ICEBERG_RETURN_UNEXPECTED(writer->Finish());
100+
ICEBERG_ASSIGN_OR_RAISE(file_size_, writer->FileSize());
101+
102+
const auto spec_id =
103+
options_.spec ? std::make_optional(options_.spec->spec_id()) : std::nullopt;
104+
105+
for (auto& entry : entries) {
106+
data_files_.push_back(std::make_shared<DataFile>(DataFile{
107+
.content = DataFile::Content::kPositionDeletes,
108+
.file_path = options_.path,
109+
.file_format = FileFormatType::kPuffin,
110+
.partition = options_.partition,
111+
.record_count = entry.cardinality,
112+
.file_size_in_bytes = file_size_,
113+
.referenced_data_file = std::move(entry.referenced_data_file),
114+
.content_offset = entry.offset,
115+
.content_size_in_bytes = entry.length,
116+
.partition_spec_id = spec_id,
117+
}));
118+
}
119+
120+
closed_ = true;
121+
return {};
122+
}
123+
124+
Result<FileWriter::WriteResult> Metadata() {
125+
ICEBERG_CHECK(closed_, "Cannot get metadata before closing the writer");
126+
FileWriter::WriteResult result;
127+
result.data_files = data_files_;
128+
return result;
129+
}
130+
131+
private:
132+
DeletionVectorWriterOptions options_;
133+
// Ordered by referenced data file path for deterministic blob layout.
134+
std::map<std::string, RoaringPositionBitmap> bitmaps_;
135+
std::vector<std::shared_ptr<DataFile>> data_files_;
136+
int64_t file_size_ = -1;
137+
bool closed_ = false;
138+
};
139+
140+
DeletionVectorWriter::DeletionVectorWriter(std::unique_ptr<Impl> impl)
141+
: impl_(std::move(impl)) {}
142+
143+
DeletionVectorWriter::~DeletionVectorWriter() = default;
144+
145+
Result<std::unique_ptr<DeletionVectorWriter>> DeletionVectorWriter::Make(
146+
DeletionVectorWriterOptions options) {
147+
ICEBERG_PRECHECK(options.io != nullptr, "DeletionVectorWriter requires a FileIO");
148+
ICEBERG_PRECHECK(!options.path.empty(), "DeletionVectorWriter requires a path");
149+
return std::unique_ptr<DeletionVectorWriter>(
150+
new DeletionVectorWriter(std::make_unique<Impl>(std::move(options))));
151+
}
152+
153+
Status DeletionVectorWriter::Delete(std::string_view referenced_data_file, int64_t pos) {
154+
return impl_->Delete(referenced_data_file, pos);
155+
}
156+
157+
Status DeletionVectorWriter::Close() { return impl_->Close(); }
158+
159+
Result<FileWriter::WriteResult> DeletionVectorWriter::Metadata() {
160+
return impl_->Metadata();
161+
}
162+
163+
} // 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)