|
75 | 75 | _ConvertToArrowSchema, |
76 | 76 | _determine_partitions, |
77 | 77 | _primitive_to_physical, |
| 78 | + _read_all_delete_files, |
78 | 79 | _read_deletes, |
79 | 80 | _task_to_record_batches, |
80 | 81 | _to_requested_schema, |
@@ -1840,6 +1841,83 @@ def test_read_deletes(deletes_file: str, request: pytest.FixtureRequest) -> None |
1840 | 1841 | assert list(deletes.values())[0] == pa.chunked_array([[1, 3, 5]]) |
1841 | 1842 |
|
1842 | 1843 |
|
| 1844 | +def _delta_dv_entry(positions: list[int]) -> bytes: |
| 1845 | + """One deletion vector framed as Delta's DeletionVectorStore writes it in a .bin file. |
| 1846 | +
|
| 1847 | + Layout: <length(4 BE)> <DV_MAGIC + 64-bit RoaringBitmapArray in the portable format shared by |
| 1848 | + Delta and Iceberg> <CRC-32(4 BE)>. The bitmap holds `positions` in a single bitmap keyed at 0. |
| 1849 | + """ |
| 1850 | + import zlib |
| 1851 | + |
| 1852 | + from pyroaring import BitMap |
| 1853 | + |
| 1854 | + from pyiceberg.table.deletion_vector import DV_MAGIC |
| 1855 | + |
| 1856 | + bitmap = (1).to_bytes(8, "little") + (0).to_bytes(4, "little") + BitMap(positions).serialize() |
| 1857 | + data = DV_MAGIC + bitmap |
| 1858 | + return len(data).to_bytes(4, "big") + data + (zlib.crc32(data) & 0xFFFFFFFF).to_bytes(4, "big") |
| 1859 | + |
| 1860 | + |
| 1861 | +def test_read_deletes_deletion_vector_in_delta_bin_file(tmp_path: Path) -> None: |
| 1862 | + entry = _delta_dv_entry([1, 3, 5]) |
| 1863 | + dv_path = f"{tmp_path}/deletion_vector.bin" |
| 1864 | + with open(dv_path, "wb") as f: |
| 1865 | + f.write(b"\x01" + entry) |
| 1866 | + |
| 1867 | + referenced_data_file = "s3://bucket/data.parquet" |
| 1868 | + data_file = DataFile.from_args( |
| 1869 | + file_path=dv_path, |
| 1870 | + file_format=FileFormat.PUFFIN, |
| 1871 | + content=DataFileContent.POSITION_DELETES, |
| 1872 | + referenced_data_file=referenced_data_file, |
| 1873 | + # Iceberg's content_offset points at the length prefix (past Delta's 1-byte version header), |
| 1874 | + # and content_size_in_bytes counts the length prefix and CRC that Delta's sizeInBytes omits. |
| 1875 | + content_offset=1, |
| 1876 | + content_size_in_bytes=len(entry), |
| 1877 | + ) |
| 1878 | + |
| 1879 | + deletes = _read_deletes(PyArrowFileIO(), data_file) |
| 1880 | + |
| 1881 | + assert deletes.keys() == {referenced_data_file} |
| 1882 | + assert deletes[referenced_data_file] == pa.chunked_array([[1, 3, 5]]) |
| 1883 | + |
| 1884 | + |
| 1885 | +def test_read_all_delete_files_reads_multiple_deletion_vectors_in_one_file(tmp_path: Path) -> None: |
| 1886 | + # Two deletion vectors packed into one file: same file_path, different content_offset. DataFile |
| 1887 | + # equality keys only on file_path, so deduplication must not collapse them into a single read. |
| 1888 | + entry_a = _delta_dv_entry([1, 3, 5]) |
| 1889 | + entry_b = _delta_dv_entry([2, 4]) |
| 1890 | + dv_path = f"{tmp_path}/deletion_vectors.bin" |
| 1891 | + with open(dv_path, "wb") as f: |
| 1892 | + f.write(b"\x01" + entry_a + entry_b) |
| 1893 | + |
| 1894 | + def _dv(referenced_data_file: str, offset: int, size: int) -> DataFile: |
| 1895 | + return DataFile.from_args( |
| 1896 | + file_path=dv_path, |
| 1897 | + file_format=FileFormat.PUFFIN, |
| 1898 | + content=DataFileContent.POSITION_DELETES, |
| 1899 | + referenced_data_file=referenced_data_file, |
| 1900 | + content_offset=offset, |
| 1901 | + content_size_in_bytes=size, |
| 1902 | + ) |
| 1903 | + |
| 1904 | + def _task(data_file_path: str, delete_file: DataFile) -> FileScanTask: |
| 1905 | + data_file = DataFile.from_args(file_path=data_file_path, file_format=FileFormat.PARQUET) |
| 1906 | + return FileScanTask(data_file=data_file, delete_files={delete_file}) |
| 1907 | + |
| 1908 | + deletes = _read_all_delete_files( |
| 1909 | + PyArrowFileIO(), |
| 1910 | + [ |
| 1911 | + _task("s3://bucket/a.parquet", _dv("s3://bucket/a.parquet", 1, len(entry_a))), |
| 1912 | + _task("s3://bucket/b.parquet", _dv("s3://bucket/b.parquet", 1 + len(entry_a), len(entry_b))), |
| 1913 | + ], |
| 1914 | + ) |
| 1915 | + |
| 1916 | + assert deletes.keys() == {"s3://bucket/a.parquet", "s3://bucket/b.parquet"} |
| 1917 | + assert deletes["s3://bucket/a.parquet"][0] == pa.chunked_array([[1, 3, 5]]) |
| 1918 | + assert deletes["s3://bucket/b.parquet"][0] == pa.chunked_array([[2, 4]]) |
| 1919 | + |
| 1920 | + |
1843 | 1921 | def test_delete(deletes_file: str, request: pytest.FixtureRequest, table_schema_simple: Schema) -> None: |
1844 | 1922 | # Determine file format from the file extension |
1845 | 1923 | file_format = FileFormat.PARQUET if deletes_file.endswith(".parquet") else FileFormat.ORC |
|
0 commit comments