Skip to content

Commit 948bfeb

Browse files
committed
fix: support all non-negative int64 deletion vector positions
1 parent 3c5715c commit 948bfeb

10 files changed

Lines changed: 197 additions & 143 deletions

‎src/iceberg/deletes/dv_writer.cc‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919

2020
#include "iceberg/deletes/dv_writer.h"
2121

22+
#include <cstddef>
2223
#include <cstdint>
24+
#include <limits>
2325
#include <map>
2426
#include <memory>
2527
#include <optional>
@@ -31,7 +33,6 @@
3133

3234
#include "iceberg/deletes/dv_util_internal.h"
3335
#include "iceberg/deletes/position_delete_index.h"
34-
#include "iceberg/deletes/roaring_position_bitmap.h"
3536
#include "iceberg/file_format.h"
3637
#include "iceberg/file_io.h" // IWYU pragma: keep
3738
#include "iceberg/manifest/manifest_entry.h"
@@ -79,9 +80,7 @@ class DVWriter::Impl {
7980
ICEBERG_PRECHECK(!referenced_data_file.empty(),
8081
"Deletion vector requires a non-empty referenced data file");
8182
ICEBERG_PRECHECK(spec != nullptr, "Deletion vector requires a partition spec");
82-
ICEBERG_PRECHECK(pos >= 0 && pos <= RoaringPositionBitmap::kMaxPosition,
83-
"Deletion vector position out of range [0, {}]: {}",
84-
RoaringPositionBitmap::kMaxPosition, pos);
83+
ICEBERG_PRECHECK(pos >= 0, "Deletion vector position must be non-negative: {}", pos);
8584
DeletesFor(referenced_data_file, spec, partition).positions.Delete(pos);
8685
return {};
8786
}
@@ -112,6 +111,10 @@ class DVWriter::Impl {
112111
ICEBERG_RETURN_UNEXPECTED(LoadPreviousDeletes(path, deletes));
113112
}
114113

114+
for (auto& [_, deletes] : deletes_by_path_) {
115+
ICEBERG_RETURN_UNEXPECTED(deletes.positions.ValidateSerializedSize());
116+
}
117+
115118
ICEBERG_ASSIGN_OR_RAISE(auto output_file, options_.io->NewOutputFile(options_.path));
116119
const std::string output_path(options_.path);
117120
ICEBERG_ASSIGN_OR_RAISE(

‎src/iceberg/deletes/position_delete_index.cc‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ constexpr std::array<uint8_t, 4> kMagic = {0xD1, 0xD3, 0x39, 0x64};
4949
constexpr int32_t kLengthPrefixBytes = 4;
5050
constexpr int32_t kMagicBytes = 4;
5151
constexpr int32_t kCrcBytes = 4;
52+
constexpr size_t kMaxSerializedLength = std::numeric_limits<int32_t>::max();
5253

5354
uint32_t ComputeCrc32(std::span<const uint8_t> bytes) {
5455
uLong crc = crc32(0L, Z_NULL, 0);
@@ -142,16 +143,24 @@ void PositionDeleteIndex::Merge(const PositionDeleteIndex& other) {
142143
other.delete_files_.end());
143144
}
144145

145-
Result<std::vector<uint8_t>> PositionDeleteIndex::Serialize() {
146+
Status PositionDeleteIndex::ValidateSerializedSize() {
146147
bitmap_.Optimize(); // run-length encode before serializing
147-
std::vector<uint8_t> blob(kLengthPrefixBytes);
148-
blob.insert(blob.end(), kMagic.begin(), kMagic.end());
149-
ICEBERG_ASSIGN_OR_RAISE(const auto vector_size, bitmap_.SerializeTo(blob));
150-
148+
const size_t vector_size = bitmap_.SerializedSizeInBytes();
151149
const size_t magic_and_vector_size = kMagicBytes + vector_size;
152-
ICEBERG_PRECHECK(magic_and_vector_size <= std::numeric_limits<int32_t>::max(),
150+
ICEBERG_PRECHECK(magic_and_vector_size <= kMaxSerializedLength,
153151
"Deletion vector is too large to serialize: {} bytes",
154152
magic_and_vector_size);
153+
return {};
154+
}
155+
156+
Result<std::vector<uint8_t>> PositionDeleteIndex::Serialize() {
157+
ICEBERG_RETURN_UNEXPECTED(ValidateSerializedSize());
158+
const size_t vector_size = bitmap_.SerializedSizeInBytes();
159+
const size_t magic_and_vector_size = kMagicBytes + vector_size;
160+
161+
std::vector<uint8_t> blob(kLengthPrefixBytes);
162+
blob.insert(blob.end(), kMagic.begin(), kMagic.end());
163+
ICEBERG_RETURN_UNEXPECTED(bitmap_.SerializeTo(blob));
155164

156165
WriteBigEndian(static_cast<int32_t>(magic_and_vector_size), blob.data());
157166
const auto crc_offset = blob.size();

‎src/iceberg/deletes/position_delete_index.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
/// \file iceberg/deletes/position_delete_index.h
2323
/// Index of deleted row positions for a data file.
2424

25+
#include <cstddef>
2526
#include <cstdint>
2627
#include <memory>
2728
#include <span>
@@ -34,6 +35,8 @@
3435

3536
namespace iceberg {
3637

38+
class DVWriter;
39+
3740
/// \brief Tracks deleted row positions using a bitmap.
3841
///
3942
/// This class provides a domain-specific API for position deletes
@@ -53,6 +56,8 @@ class ICEBERG_EXPORT PositionDeleteIndex {
5356
/// \brief Mark a range of positions as deleted [pos_start, pos_end).
5457
/// \param pos_start Start position (inclusive)
5558
/// \param pos_end End position (exclusive)
59+
/// \note Because pos_end is an int64_t exclusive endpoint, this method cannot
60+
/// include INT64_MAX. Call Delete(INT64_MAX) separately.
5661
void Delete(int64_t pos_start, int64_t pos_end);
5762

5863
/// \brief Check if a position is deleted.
@@ -97,6 +102,8 @@ class ICEBERG_EXPORT PositionDeleteIndex {
97102
private:
98103
explicit PositionDeleteIndex(RoaringPositionBitmap bitmap);
99104

105+
Status ValidateSerializedSize();
106+
100107
// Bulk-add positions sharing high-32-bit `key`. Private hook for
101108
// `ForEachPositionDelete`'s bulk path; keeps `Delete` the sole public
102109
// mutation surface.
@@ -105,6 +112,7 @@ class ICEBERG_EXPORT PositionDeleteIndex {
105112
friend void ICEBERG_EXPORT ForEachPositionDelete(std::span<const int64_t> positions,
106113
PositionDeleteIndex& target,
107114
std::vector<uint32_t>& scratch);
115+
friend class DVWriter;
108116

109117
RoaringPositionBitmap bitmap_;
110118
std::vector<std::shared_ptr<DataFile>> delete_files_;

‎src/iceberg/deletes/position_delete_range_consumer.cc‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ namespace iceberg {
3131

3232
namespace {
3333

34-
bool IsValidPosition(int64_t pos) {
35-
return pos >= 0 && pos <= RoaringPositionBitmap::kMaxPosition;
36-
}
34+
bool IsValidPosition(int64_t pos) { return pos >= 0; }
3735

3836
// Unsigned subtraction so negative or wrap-around input can't
3937
// false-positive via signed overflow.
@@ -45,11 +43,13 @@ bool IsAdjacent(int64_t prev, int64_t next) {
4543
// bulk path groups by this key before flushing via `BulkAddForKey`.
4644
int32_t HighKeyFromPosition(int64_t pos) { return static_cast<int32_t>(pos >> 32); }
4745

48-
// Emit `[range_start, last_position]`, collapsing singletons. Callers
49-
// pre-filter via `IsValidPosition`, so `last_position + 1` cannot overflow.
46+
// Emit `[range_start, last_position]`, collapsing singletons.
5047
void EmitRange(PositionDeleteIndex& target, int64_t range_start, int64_t last_position) {
5148
if (range_start == last_position) {
5249
target.Delete(range_start);
50+
} else if (last_position == RoaringPositionBitmap::kMaxPosition) {
51+
target.Delete(range_start, last_position);
52+
target.Delete(last_position);
5353
} else {
5454
target.Delete(range_start, last_position + 1);
5555
}

‎src/iceberg/deletes/roaring_position_bitmap.cc‎

Lines changed: 25 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <cstring>
2424
#include <exception>
2525
#include <limits>
26+
#include <map>
2627
#include <string_view>
2728
#include <utility>
2829
#include <vector>
@@ -51,34 +52,20 @@ int64_t ToPosition(int32_t key, uint32_t pos32) {
5152
return (int64_t{key} << 32) | int64_t{pos32};
5253
}
5354

54-
Status ValidatePosition(int64_t pos) {
55-
if (pos < 0 || pos > RoaringPositionBitmap::kMaxPosition) {
56-
return InvalidArgument("Bitmap supports positions that are >= 0 and <= {}: {}",
57-
RoaringPositionBitmap::kMaxPosition, pos);
58-
}
59-
return {};
60-
}
61-
62-
void WriteBitmaps(const std::vector<roaring::Roaring>& bitmaps, uint8_t* buf) {
55+
void WriteBitmaps(const std::map<int32_t, roaring::Roaring>& bitmaps, uint8_t* buf) {
6356
WriteLittleEndian(static_cast<int64_t>(bitmaps.size()), buf);
6457
buf += kBitmapCountSizeBytes;
65-
for (int32_t key = 0; std::cmp_less(key, bitmaps.size()); ++key) {
58+
for (const auto& [key, bitmap] : bitmaps) {
6659
WriteLittleEndian(key, buf);
6760
buf += kBitmapKeySizeBytes;
68-
buf += bitmaps[key].write(reinterpret_cast<char*>(buf), /*portable=*/true);
61+
buf += bitmap.write(reinterpret_cast<char*>(buf), /*portable=*/true);
6962
}
7063
}
7164

7265
} // namespace
7366

7467
struct RoaringPositionBitmap::Impl {
75-
std::vector<roaring::Roaring> bitmaps;
76-
77-
void AllocateBitmapsIfNeeded(int32_t required_length) {
78-
if (std::cmp_less(bitmaps.size(), required_length)) {
79-
bitmaps.resize(static_cast<size_t>(required_length));
80-
}
81-
}
68+
std::map<int32_t, roaring::Roaring> bitmaps;
8269
};
8370

8471
RoaringPositionBitmap::RoaringPositionBitmap() : impl_(std::make_unique<Impl>()) {}
@@ -108,86 +95,85 @@ RoaringPositionBitmap::RoaringPositionBitmap(std::unique_ptr<Impl> impl)
10895
: impl_(std::move(impl)) {}
10996

11097
void RoaringPositionBitmap::Add(int64_t pos) {
111-
if (pos < 0 || pos > kMaxPosition) {
98+
if (pos < 0) {
11299
return; // Silently ignore invalid positions
113100
}
114101
int32_t key = Key(pos);
115102
uint32_t pos32 = Pos32Bits(pos);
116-
impl_->AllocateBitmapsIfNeeded(key + 1);
117103
impl_->bitmaps[key].add(pos32);
118104
}
119105

120106
void RoaringPositionBitmap::AddManyForKey(int32_t key,
121107
std::span<const uint32_t> positions) {
122-
impl_->AllocateBitmapsIfNeeded(key + 1);
108+
if (key < 0 || positions.empty()) {
109+
return;
110+
}
123111
impl_->bitmaps[key].addMany(positions.size(), positions.data());
124112
}
125113

126114
void RoaringPositionBitmap::AddRange(int64_t pos_start, int64_t pos_end) {
127115
pos_start = std::max(pos_start, int64_t{0});
128-
pos_end = std::min(pos_end, kMaxPosition + 1);
129116
if (pos_start >= pos_end) {
130117
return;
131118
}
132119

133120
int64_t pos_last = pos_end - 1;
134121
int32_t start_key = Key(pos_start);
135122
int32_t end_key = Key(pos_last);
136-
impl_->AllocateBitmapsIfNeeded(end_key + 1);
137123

138-
for (int32_t key = start_key; key <= end_key; ++key) {
124+
for (int64_t key = start_key; key <= end_key; ++key) {
139125
uint64_t low_start = (key == start_key) ? Pos32Bits(pos_start) : uint64_t{0};
140126
uint64_t low_end = (key == end_key) ? static_cast<uint64_t>(Pos32Bits(pos_last)) + 1
141127
: (uint64_t{1} << 32);
142-
impl_->bitmaps[key].addRange(low_start, low_end);
128+
impl_->bitmaps[static_cast<int32_t>(key)].addRange(low_start, low_end);
143129
}
144130
}
145131

146132
bool RoaringPositionBitmap::Contains(int64_t pos) const {
147-
if (pos < 0 || pos > kMaxPosition) {
133+
if (pos < 0) {
148134
return false; // Invalid positions are not contained
149135
}
150136
int32_t key = Key(pos);
151137
uint32_t pos32 = Pos32Bits(pos);
152-
return std::cmp_less(key, impl_->bitmaps.size()) && impl_->bitmaps[key].contains(pos32);
138+
auto it = impl_->bitmaps.find(key);
139+
return it != impl_->bitmaps.end() && it->second.contains(pos32);
153140
}
154141

155142
bool RoaringPositionBitmap::IsEmpty() const { return Cardinality() == 0; }
156143

157144
size_t RoaringPositionBitmap::Cardinality() const {
158145
size_t total = 0;
159-
for (const auto& bitmap : impl_->bitmaps) {
146+
for (const auto& [_, bitmap] : impl_->bitmaps) {
160147
total += bitmap.cardinality();
161148
}
162149
return total;
163150
}
164151

165152
void RoaringPositionBitmap::Or(const RoaringPositionBitmap& other) {
166-
impl_->AllocateBitmapsIfNeeded(static_cast<int32_t>(other.impl_->bitmaps.size()));
167-
for (size_t key = 0; key < other.impl_->bitmaps.size(); ++key) {
168-
impl_->bitmaps[key] |= other.impl_->bitmaps[key];
153+
for (const auto& [key, bitmap] : other.impl_->bitmaps) {
154+
impl_->bitmaps[key] |= bitmap;
169155
}
170156
}
171157

172158
bool RoaringPositionBitmap::Optimize() {
173159
bool changed = false;
174-
for (auto& bitmap : impl_->bitmaps) {
160+
for (auto& [_, bitmap] : impl_->bitmaps) {
175161
changed |= bitmap.runOptimize();
176162
}
177163
return changed;
178164
}
179165

180166
void RoaringPositionBitmap::ForEach(const std::function<void(int64_t)>& fn) const {
181-
for (size_t key = 0; key < impl_->bitmaps.size(); ++key) {
182-
for (uint32_t pos32 : impl_->bitmaps[key]) {
183-
fn(ToPosition(static_cast<int32_t>(key), pos32));
167+
for (const auto& [key, bitmap] : impl_->bitmaps) {
168+
for (uint32_t pos32 : bitmap) {
169+
fn(ToPosition(key, pos32));
184170
}
185171
}
186172
}
187173

188174
size_t RoaringPositionBitmap::SerializedSizeInBytes() const {
189175
size_t size = kBitmapCountSizeBytes;
190-
for (const auto& bitmap : impl_->bitmaps) {
176+
for (const auto& [_, bitmap] : impl_->bitmaps) {
191177
size += kBitmapKeySizeBytes + bitmap.getSizeInBytes(/*portable=*/true);
192178
}
193179
return size;
@@ -238,18 +224,10 @@ Result<RoaringPositionBitmap> RoaringPositionBitmap::Deserialize(std::string_vie
238224
remaining -= kBitmapKeySizeBytes;
239225

240226
ICEBERG_PRECHECK(key >= 0, "Invalid unsigned key: {}", key);
241-
ICEBERG_PRECHECK(key < std::numeric_limits<int32_t>::max(), "Key is too large: {}",
242-
key);
243227
ICEBERG_PRECHECK(key > last_key,
244228
"Keys must be sorted in ascending order, got key {} after {}", key,
245229
last_key);
246230

247-
// Fill gaps with empty bitmaps
248-
while (last_key < key - 1) {
249-
impl->bitmaps.emplace_back();
250-
++last_key;
251-
}
252-
253231
// Read bitmap using portable safe deserialization.
254232
// CRoaring's readSafe may throw on corrupted data.
255233
roaring::Roaring bitmap;
@@ -266,7 +244,9 @@ Result<RoaringPositionBitmap> RoaringPositionBitmap::Deserialize(std::string_vie
266244
buf += bitmap_size;
267245
remaining -= bitmap_size;
268246

269-
impl->bitmaps.emplace_back(std::move(bitmap));
247+
if (!bitmap.isEmpty()) {
248+
impl->bitmaps.emplace(key, std::move(bitmap));
249+
}
270250
last_key = key;
271251
--remaining_count;
272252
}

‎src/iceberg/deletes/roaring_position_bitmap.h‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020
#pragma once
2121

2222
/// \file iceberg/deletes/roaring_position_bitmap.h
23-
/// A 64-bit position bitmap using an array of 32-bit Roaring bitmaps.
23+
/// A 64-bit position bitmap using sparse 32-bit Roaring bitmaps.
2424

2525
#include <cstdint>
2626
#include <functional>
27+
#include <limits>
2728
#include <memory>
2829
#include <span>
2930
#include <string>
@@ -37,7 +38,7 @@ namespace iceberg {
3738

3839
class PositionDeleteIndex;
3940

40-
/// \brief A bitmap that supports positive 64-bit positions, optimized
41+
/// \brief A bitmap that supports non-negative 64-bit positions, optimized
4142
/// for cases where most positions fit in 32 bits.
4243
///
4344
/// Incoming 64-bit positions are divided into a 32-bit "key" using the
@@ -51,8 +52,8 @@ class PositionDeleteIndex;
5152
/// for `deletion-vector-v1` persistence.
5253
class ICEBERG_EXPORT RoaringPositionBitmap {
5354
public:
54-
/// \brief Maximum supported position (aligned with the Java implementation).
55-
static constexpr int64_t kMaxPosition = 0x7FFFFFFE80000000LL;
55+
/// \brief Maximum supported position.
56+
static constexpr int64_t kMaxPosition = std::numeric_limits<int64_t>::max();
5657

5758
RoaringPositionBitmap();
5859
~RoaringPositionBitmap();
@@ -64,21 +65,21 @@ class ICEBERG_EXPORT RoaringPositionBitmap {
6465
RoaringPositionBitmap& operator=(const RoaringPositionBitmap& other);
6566

6667
/// \brief Sets a position in the bitmap.
67-
/// \param pos the position (must be >= 0 and <= kMaxPosition)
68-
/// \note Invalid positions are silently ignored
68+
/// \param pos the position (must be non-negative)
69+
/// \note Negative positions are silently ignored.
6970
void Add(int64_t pos);
7071

7172
/// \brief Sets a range of positions [pos_start, pos_end).
7273
/// \param pos_start the start of the range (inclusive), clamped to 0
73-
/// \param pos_end the end of the range (exclusive), clamped to kMaxPosition + 1
74-
/// \note If pos_start > pos_end, the call is silently ignored.
75-
/// If pos_start == pos_end, this method does nothing.
76-
/// Positions outside [0, kMaxPosition] are silently ignored.
74+
/// \param pos_end the end of the range (exclusive)
75+
/// \note Empty and reversed ranges are silently ignored.
76+
/// \note Because pos_end is an int64_t exclusive endpoint, this method cannot
77+
/// include kMaxPosition. Call Add(kMaxPosition) separately.
7778
void AddRange(int64_t pos_start, int64_t pos_end);
7879

7980
/// \brief Checks if a position is set in the bitmap.
8081
/// \param pos the position to check
81-
/// \return true if the position is set, false otherwise (including invalid positions)
82+
/// \return true if the position is set, false otherwise (including negative positions)
8283
bool Contains(int64_t pos) const;
8384

8485
/// \brief Returns true if the bitmap has no positions set.

0 commit comments

Comments
 (0)