Skip to content

Commit 76e1b17

Browse files
committed
Extract DeletionVector logic from PuffinFile
1 parent ec1413d commit 76e1b17

3 files changed

Lines changed: 56 additions & 48 deletions

File tree

pyiceberg/io/pyarrow.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
from pyiceberg.table.locations import load_location_provider
148148
from pyiceberg.table.metadata import TableMetadata
149149
from pyiceberg.table.name_mapping import NameMapping, apply_name_mapping
150-
from pyiceberg.table.puffin import PuffinFile
150+
from pyiceberg.table.puffin import DeletionVector, PuffinFile
151151
from pyiceberg.transforms import IdentityTransform, TruncateTransform
152152
from pyiceberg.typedef import EMPTY_DICT, Properties, Record, TableVersion
153153
from pyiceberg.types import (
@@ -1141,7 +1141,7 @@ def _read_deletes(io: FileIO, data_file: DataFile) -> dict[str, pa.ChunkedArray]
11411141
with io.new_input(data_file.file_path).open() as fi:
11421142
payload = fi.read()
11431143

1144-
return PuffinFile(payload).to_vector()
1144+
return DeletionVector(PuffinFile(payload)).to_vector()
11451145
else:
11461146
raise ValueError(f"Delete file format not supported: {data_file.file_format}")
11471147

pyiceberg/table/puffin.py

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,36 +32,6 @@
3232
PROPERTY_REFERENCED_DATA_FILE = "referenced-data-file"
3333

3434

35-
def _deserialize_bitmap(pl: bytes) -> list[BitMap]:
36-
number_of_bitmaps = int.from_bytes(pl[0:8], byteorder="little")
37-
pl = pl[8:]
38-
39-
bitmaps = []
40-
last_key = -1
41-
for _ in range(number_of_bitmaps):
42-
key = int.from_bytes(pl[0:4], byteorder="little")
43-
if key < 0:
44-
raise ValueError(f"Invalid unsigned key: {key}")
45-
if key <= last_key:
46-
raise ValueError("Keys must be sorted in ascending order")
47-
if key > MAX_JAVA_SIGNED:
48-
raise ValueError(f"Key {key} is too large, max {MAX_JAVA_SIGNED} to maintain compatibility with Java impl")
49-
pl = pl[4:]
50-
51-
while last_key < key - 1:
52-
bitmaps.append(EMPTY_BITMAP)
53-
last_key += 1
54-
55-
bm = BitMap().deserialize(pl)
56-
# TODO: Optimize this
57-
pl = pl[len(bm.serialize()) :]
58-
bitmaps.append(bm)
59-
60-
last_key = key
61-
62-
return bitmaps
63-
64-
6535
class PuffinBlobMetadata(IcebergBaseModel):
6636
type: Literal["deletion-vector-v1"] = Field()
6737
fields: list[int] = Field()
@@ -78,15 +48,9 @@ class Footer(IcebergBaseModel):
7848
properties: dict[str, str] = Field(default_factory=dict)
7949

8050

81-
def _bitmaps_to_chunked_array(bitmaps: list[BitMap]) -> "pa.ChunkedArray":
82-
import pyarrow as pa
83-
84-
return pa.chunked_array([(key_pos << 32) + pos for pos in bitmap] for key_pos, bitmap in enumerate(bitmaps))
85-
86-
8751
class PuffinFile:
8852
footer: Footer
89-
_deletion_vectors: dict[str, list[BitMap]]
53+
_payload: bytes
9054

9155
def __init__(self, puffin: bytes) -> None:
9256
for magic_bytes in [puffin[:4], puffin[-4:]]:
@@ -105,12 +69,56 @@ def __init__(self, puffin: bytes) -> None:
10569
footer_payload_size_int = int.from_bytes(puffin[-12:-8], byteorder="little")
10670

10771
self.footer = Footer.model_validate_json(puffin[-(footer_payload_size_int + 12) : -12])
108-
puffin = puffin[8:]
72+
self._payload = puffin[8:]
10973

74+
def get_blob_payload(self, blob: PuffinBlobMetadata) -> bytes:
75+
return self._payload[blob.offset : blob.offset + blob.length]
76+
77+
78+
class DeletionVector:
79+
_deletion_vectors: dict[str, list[BitMap]]
80+
81+
def __init__(self, puffin_file: PuffinFile) -> None:
11082
self._deletion_vectors = {
111-
blob.properties[PROPERTY_REFERENCED_DATA_FILE]: _deserialize_bitmap(puffin[blob.offset : blob.offset + blob.length])
112-
for blob in self.footer.blobs
83+
blob.properties[PROPERTY_REFERENCED_DATA_FILE]: self._deserialize_bitmap(puffin_file.get_blob_payload(blob))
84+
for blob in puffin_file.footer.blobs
11385
}
11486

87+
@staticmethod
88+
def _deserialize_bitmap(pl: bytes) -> list[BitMap]:
89+
number_of_bitmaps = int.from_bytes(pl[0:8], byteorder="little")
90+
pl = pl[8:]
91+
92+
bitmaps = []
93+
last_key = -1
94+
for _ in range(number_of_bitmaps):
95+
key = int.from_bytes(pl[0:4], byteorder="little")
96+
if key < 0:
97+
raise ValueError(f"Invalid unsigned key: {key}")
98+
if key <= last_key:
99+
raise ValueError("Keys must be sorted in ascending order")
100+
if key > MAX_JAVA_SIGNED:
101+
raise ValueError(f"Key {key} is too large, max {MAX_JAVA_SIGNED} to maintain compatibility with Java impl")
102+
pl = pl[4:]
103+
104+
while last_key < key - 1:
105+
bitmaps.append(EMPTY_BITMAP)
106+
last_key += 1
107+
108+
bm = BitMap().deserialize(pl)
109+
# TODO: Optimize this
110+
pl = pl[len(bm.serialize()) :]
111+
bitmaps.append(bm)
112+
113+
last_key = key
114+
115+
return bitmaps
116+
117+
@staticmethod
118+
def _bitmaps_to_chunked_array(bitmaps: list[BitMap]) -> "pa.ChunkedArray":
119+
import pyarrow as pa
120+
121+
return pa.chunked_array([(key_pos << 32) + pos for pos in bitmap] for key_pos, bitmap in enumerate(bitmaps))
122+
115123
def to_vector(self) -> dict[str, "pa.ChunkedArray"]:
116-
return {path: _bitmaps_to_chunked_array(bitmaps) for path, bitmaps in self._deletion_vectors.items()}
124+
return {path: self._bitmaps_to_chunked_array(bitmaps) for path, bitmaps in self._deletion_vectors.items()}

tests/table/test_puffin.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import pytest
2020
from pyroaring import BitMap
2121

22-
from pyiceberg.table.puffin import _deserialize_bitmap
22+
from pyiceberg.table.puffin import DeletionVector
2323

2424

2525
def _open_file(file: str) -> bytes:
@@ -32,7 +32,7 @@ def test_map_empty() -> None:
3232
puffin = _open_file("64mapempty.bin")
3333

3434
expected: list[BitMap] = []
35-
actual = _deserialize_bitmap(puffin)
35+
actual = DeletionVector._deserialize_bitmap(puffin)
3636

3737
assert expected == actual
3838

@@ -41,7 +41,7 @@ def test_map_bitvals() -> None:
4141
puffin = _open_file("64map32bitvals.bin")
4242

4343
expected = [BitMap([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])]
44-
actual = _deserialize_bitmap(puffin)
44+
actual = DeletionVector._deserialize_bitmap(puffin)
4545

4646
assert expected == actual
4747

@@ -61,7 +61,7 @@ def test_map_spread_vals() -> None:
6161
BitMap([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
6262
BitMap([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
6363
]
64-
actual = _deserialize_bitmap(puffin)
64+
actual = DeletionVector._deserialize_bitmap(puffin)
6565

6666
assert expected == actual
6767

@@ -70,4 +70,4 @@ def test_map_high_vals() -> None:
7070
puffin = _open_file("64maphighvals.bin")
7171

7272
with pytest.raises(ValueError, match="Key 4022190063 is too large, max 2147483647 to maintain compatibility with Java impl"):
73-
_ = _deserialize_bitmap(puffin)
73+
_ = DeletionVector._deserialize_bitmap(puffin)

0 commit comments

Comments
 (0)