@@ -1161,6 +1161,11 @@ def test_write_manifest_v3(compression: AvroCompressionCodec) -> None:
11611161 partition = Record (),
11621162 record_count = 100 ,
11631163 file_size_in_bytes = 1024 ,
1164+ column_sizes = {1 : 10 },
1165+ value_counts = {1 : 100 },
1166+ null_value_counts = {1 : 0 },
1167+ split_offsets = [4 ],
1168+ sort_order_id = 1 ,
11641169 first_row_id = 1000 ,
11651170 )
11661171 v2_data_file = DataFile .from_args (
@@ -1197,6 +1202,19 @@ def test_write_manifest_v3(compression: AvroCompressionCodec) -> None:
11971202 for entry in entries :
11981203 for v3_field in ("first_row_id" , "referenced_data_file" , "content_offset" , "content_size_in_bytes" ):
11991204 assert v3_field in entry ["data_file" ]
1205+ assert entries [0 ]["data_file" ]["sort_order_id" ] == 1
1206+ assert entries [0 ]["data_file" ]["split_offsets" ] == [4 ]
1207+
1208+ # the V3 manifest must remain readable by the current reader
1209+ read_entries = writer .to_manifest_file ().fetch_manifest_entry (io , discard_deleted = False )
1210+ assert len (read_entries ) == 2
1211+ assert read_entries [0 ].status == ManifestEntryStatus .ADDED
1212+ assert read_entries [0 ].data_file .file_path == "/data/file-v3.parquet"
1213+ assert read_entries [0 ].data_file .record_count == 100
1214+ assert read_entries [0 ].data_file .column_sizes == {1 : 10 }
1215+ assert read_entries [0 ].data_file .value_counts == {1 : 100 }
1216+ assert read_entries [0 ].data_file .sort_order_id == 1
1217+ assert read_entries [1 ].data_file .file_path == "/data/file-v2.parquet"
12001218
12011219
12021220@pytest .mark .parametrize ("compression" , ["null" , "deflate" ])
@@ -1261,6 +1279,13 @@ def manifest(path: str, content: ManifestContent, first_row_id: int | None = Non
12611279
12621280 assert [r ["first_row_id" ] for r in records ] == [1000 , 77 , None , 1125 ]
12631281
1282+ # the V3 manifest list must remain readable by the current reader
1283+ read_back = list (read_manifest_list (io .new_input (path )))
1284+ assert [m .manifest_path for m in read_back ] == ["/m1.avro" , "/m2.avro" , "/m3.avro" , "/m4.avro" ]
1285+ assert read_back [0 ].added_rows_count == 100
1286+ assert read_back [0 ].existing_rows_count == 25
1287+ assert read_back [2 ].content == ManifestContent .DELETES
1288+
12641289
12651290def test_write_manifest_list_v3_requires_first_row_id () -> None :
12661291 io = load_file_io ()
@@ -1275,3 +1300,112 @@ def test_write_manifest_list_v3_requires_first_row_id() -> None:
12751300 avro_compression = "null" ,
12761301 first_row_id = None ,
12771302 )
1303+
1304+
1305+ @pytest .mark .parametrize ("compression" , ["null" , "deflate" ])
1306+ def test_write_manifest_v3_carries_first_row_id (compression : AvroCompressionCodec ) -> None :
1307+ io = load_file_io ()
1308+ test_schema = Schema (NestedField (1 , "foo" , IntegerType (), False ))
1309+ data_file = DataFile .from_args (
1310+ content = DataFileContent .DATA ,
1311+ file_path = "/data/file.parquet" ,
1312+ file_format = FileFormat .PARQUET ,
1313+ partition = Record (),
1314+ record_count = 100 ,
1315+ file_size_in_bytes = 1024 ,
1316+ )
1317+
1318+ with TemporaryDirectory () as tmp_dir :
1319+ with write_manifest (
1320+ format_version = 3 ,
1321+ spec = UNPARTITIONED_PARTITION_SPEC ,
1322+ schema = test_schema ,
1323+ output_file = io .new_output (tmp_dir + "/manifest-rewrite.avro" ),
1324+ snapshot_id = 25 ,
1325+ avro_compression = compression ,
1326+ first_row_id = 100 ,
1327+ ) as writer :
1328+ writer .add (ManifestEntry .from_args (status = ManifestEntryStatus .ADDED , snapshot_id = 25 , data_file = data_file ))
1329+
1330+ manifest_file = writer .to_manifest_file ()
1331+ assert manifest_file .first_row_id == 100
1332+
1333+ # a manifest list writer must preserve the carried first_row_id and not advance next_row_id for it
1334+ path = tmp_dir + "/manifest-list.avro"
1335+ with write_manifest_list (
1336+ format_version = 3 ,
1337+ output_file = io .new_output (path ),
1338+ snapshot_id = 25 ,
1339+ parent_snapshot_id = 19 ,
1340+ sequence_number = 2 ,
1341+ avro_compression = compression ,
1342+ first_row_id = 1000 ,
1343+ ) as list_writer :
1344+ list_writer .add_manifests ([manifest_file ])
1345+ assert list_writer .next_row_id == 1000 # type: ignore[attr-defined]
1346+
1347+ with open (path , "rb" ) as f :
1348+ records = list (fastavro .reader (f ))
1349+ assert [r ["first_row_id" ] for r in records ] == [100 ]
1350+
1351+
1352+ def test_write_manifest_first_row_id_requires_v3 () -> None :
1353+ io = load_file_io ()
1354+ test_schema = Schema (NestedField (1 , "foo" , IntegerType (), False ))
1355+ with TemporaryDirectory () as tmp_dir :
1356+ with pytest .raises (ValueError , match = "First-row-id is only supported for V3 tables" ):
1357+ write_manifest (
1358+ format_version = 2 ,
1359+ spec = UNPARTITIONED_PARTITION_SPEC ,
1360+ schema = test_schema ,
1361+ output_file = io .new_output (tmp_dir + "/manifest.avro" ),
1362+ snapshot_id = 25 ,
1363+ avro_compression = "null" ,
1364+ first_row_id = 100 ,
1365+ )
1366+
1367+
1368+ def test_read_v2_manifest_list_with_v3_layout () -> None :
1369+ from pyiceberg .avro .file import AvroFile
1370+ from pyiceberg .manifest import MANIFEST_LIST_FILE_SCHEMAS
1371+
1372+ io = load_file_io ()
1373+ manifest = ManifestFile .from_args (
1374+ manifest_path = "/m1.avro" ,
1375+ manifest_length = 100 ,
1376+ partition_spec_id = 0 ,
1377+ content = ManifestContent .DATA ,
1378+ sequence_number = 1 ,
1379+ min_sequence_number = 1 ,
1380+ added_snapshot_id = 25 ,
1381+ added_files_count = 1 ,
1382+ existing_files_count = 0 ,
1383+ deleted_files_count = 0 ,
1384+ added_rows_count = 100 ,
1385+ existing_rows_count = 0 ,
1386+ deleted_rows_count = 0 ,
1387+ )
1388+
1389+ with TemporaryDirectory () as tmp_dir :
1390+ path = tmp_dir + "/manifest-list-v2.avro"
1391+ with write_manifest_list (
1392+ format_version = 2 ,
1393+ output_file = io .new_output (path ),
1394+ snapshot_id = 25 ,
1395+ parent_snapshot_id = 19 ,
1396+ sequence_number = 2 ,
1397+ avro_compression = "null" ,
1398+ ) as writer :
1399+ writer .add_manifests ([manifest ])
1400+
1401+ # reading a V2 manifest list with the V3 layout yields a null first_row_id
1402+ with AvroFile [ManifestFile ](
1403+ io .new_input (path ),
1404+ MANIFEST_LIST_FILE_SCHEMAS [3 ],
1405+ read_types = {- 1 : ManifestFile },
1406+ read_enums = {517 : ManifestContent },
1407+ ) as reader :
1408+ entries = list (reader )
1409+ assert len (entries ) == 1
1410+ assert entries [0 ].first_row_id is None
1411+ assert entries [0 ].manifest_path == "/m1.avro"
0 commit comments