@@ -852,6 +852,17 @@ def partitions(self) -> list[PartitionFieldSummary] | None:
852852 def key_metadata (self ) -> bytes | None :
853853 return self ._data [14 ]
854854
855+ @property
856+ def first_row_id (self ) -> int | None :
857+ # Only present when the record is bound to the V3 manifest list schema
858+ return self ._data [15 ] if len (self ._data ) > 15 else None
859+
860+ @first_row_id .setter
861+ def first_row_id (self , value : int | None ) -> None :
862+ if len (self ._data ) <= 15 :
863+ raise ValueError ("Cannot set first_row_id on a manifest file not bound to the V3 schema" )
864+ self ._data [15 ] = value
865+
855866 def has_added_files (self ) -> bool :
856867 return self .added_files_count is None or self .added_files_count > 0
857868
@@ -1240,6 +1251,51 @@ def prepare_entry(self, entry: ManifestEntry) -> ManifestEntry:
12401251 return entry
12411252
12421253
1254+ class ManifestWriterV3 (ManifestWriterV2 ):
1255+ """Writes V3 manifest files.
1256+
1257+ The writer inherits the V2 sequence-number semantics; the V3 manifest entry
1258+ schema additionally carries `first_row_id`, `referenced_data_file`,
1259+ `content_offset` and `content_size_in_bytes` on the data file struct.
1260+ """
1261+
1262+ @property
1263+ def version (self ) -> TableVersion :
1264+ return 3
1265+
1266+ def new_writer (self ) -> AvroOutputFile [ManifestEntry ]:
1267+ # Use the V3 record layout so the V3-only data file fields are written
1268+ return AvroOutputFile [ManifestEntry ](
1269+ output_file = self ._output_file ,
1270+ file_schema = self ._with_partition (3 ),
1271+ record_schema = self ._with_partition (3 ),
1272+ schema_name = "manifest_entry" ,
1273+ metadata = self ._meta ,
1274+ )
1275+
1276+ def _wrap_data_file (self , data_file : DataFile ) -> DataFile :
1277+ """Rebind a data file to the V3 record layout."""
1278+ if len (data_file ._data ) >= len (DATA_FILE_TYPE [3 ].fields ):
1279+ return data_file
1280+ args = {
1281+ field .name : value for field , value in zip (DATA_FILE_TYPE [DEFAULT_READ_VERSION ].fields , data_file ._data , strict = True )
1282+ }
1283+ return DataFile .from_args (_table_format_version = 3 , ** args )
1284+
1285+ def prepare_entry (self , entry : ManifestEntry ) -> ManifestEntry :
1286+ entry = super ().prepare_entry (entry )
1287+ if len (entry .data_file ._data ) < len (DATA_FILE_TYPE [3 ].fields ):
1288+ entry = ManifestEntry .from_args (
1289+ _table_format_version = 3 ,
1290+ status = entry .status ,
1291+ snapshot_id = entry .snapshot_id ,
1292+ sequence_number = entry .sequence_number ,
1293+ file_sequence_number = entry .file_sequence_number ,
1294+ data_file = self ._wrap_data_file (entry .data_file ),
1295+ )
1296+ return entry
1297+
1298+
12431299def write_manifest (
12441300 format_version : TableVersion ,
12451301 spec : PartitionSpec ,
@@ -1252,6 +1308,8 @@ def write_manifest(
12521308 return ManifestWriterV1 (spec , schema , output_file , snapshot_id , avro_compression )
12531309 elif format_version == 2 :
12541310 return ManifestWriterV2 (spec , schema , output_file , snapshot_id , avro_compression )
1311+ elif format_version == 3 :
1312+ return ManifestWriterV3 (spec , schema , output_file , snapshot_id , avro_compression )
12551313 else :
12561314 raise ValueError (f"Cannot write manifest for table version: { format_version } " )
12571315
@@ -1375,19 +1433,91 @@ def prepare_manifest(self, manifest_file: ManifestFile) -> ManifestFile:
13751433 return wrapped_manifest_file
13761434
13771435
1436+ class ManifestListWriterV3 (ManifestListWriterV2 ):
1437+ """Writes V3 manifest lists, assigning `first_row_id` to data manifests.
1438+
1439+ Follows the spec's First Row ID Assignment: existing `first_row_id` values are
1440+ preserved, delete manifests are never assigned one, and data manifests without a
1441+ `first_row_id` are assigned a running value starting at the snapshot's
1442+ `first-row-id`, advanced by each assigned manifest's existing and added row counts.
1443+ """
1444+
1445+ _next_row_id : int
1446+
1447+ def __init__ (
1448+ self ,
1449+ output_file : OutputFile ,
1450+ snapshot_id : int ,
1451+ parent_snapshot_id : int | None ,
1452+ sequence_number : int ,
1453+ compression : AvroCompressionCodec ,
1454+ first_row_id : int ,
1455+ ):
1456+ super ().__init__ (output_file , snapshot_id , parent_snapshot_id , sequence_number , compression )
1457+ self ._format_version = 3
1458+ self ._meta = {
1459+ ** self ._meta ,
1460+ "first-row-id" : str (first_row_id ),
1461+ "format-version" : "3" ,
1462+ }
1463+ self ._next_row_id = first_row_id
1464+
1465+ def __enter__ (self ) -> ManifestListWriter :
1466+ """Open the writer for writing, using the V3 record layout so `first_row_id` is written."""
1467+ self ._writer = AvroOutputFile [ManifestFile ](
1468+ output_file = self ._output_file ,
1469+ record_schema = MANIFEST_LIST_FILE_SCHEMAS [3 ],
1470+ file_schema = MANIFEST_LIST_FILE_SCHEMAS [3 ],
1471+ schema_name = "manifest_file" ,
1472+ metadata = self ._meta ,
1473+ )
1474+ self ._writer .__enter__ ()
1475+ return self
1476+
1477+ @property
1478+ def next_row_id (self ) -> int :
1479+ """The row ID after the last assigned one; the table's `next-row-id` after this snapshot."""
1480+ return self ._next_row_id
1481+
1482+ def _wrap (self , manifest_file : ManifestFile ) -> ManifestFile :
1483+ """Rebind a manifest file to the V3 record layout."""
1484+ if len (manifest_file ._data ) >= len (MANIFEST_LIST_FILE_SCHEMAS [3 ].fields ):
1485+ return copy (manifest_file )
1486+ args = {
1487+ field .name : value
1488+ for field , value in zip (MANIFEST_LIST_FILE_SCHEMAS [DEFAULT_READ_VERSION ].fields , manifest_file ._data , strict = True )
1489+ }
1490+ return ManifestFile .from_args (_table_format_version = 3 , ** args )
1491+
1492+ def prepare_manifest (self , manifest_file : ManifestFile ) -> ManifestFile :
1493+ wrapped_manifest_file = super ().prepare_manifest (self ._wrap (manifest_file ))
1494+
1495+ if wrapped_manifest_file .content == ManifestContent .DATA and wrapped_manifest_file .first_row_id is None :
1496+ wrapped_manifest_file .first_row_id = self ._next_row_id
1497+ self ._next_row_id += (wrapped_manifest_file .existing_rows_count or 0 ) + (wrapped_manifest_file .added_rows_count or 0 )
1498+ return wrapped_manifest_file
1499+
1500+
13781501def write_manifest_list (
13791502 format_version : TableVersion ,
13801503 output_file : OutputFile ,
13811504 snapshot_id : int ,
13821505 parent_snapshot_id : int | None ,
13831506 sequence_number : int | None ,
13841507 avro_compression : AvroCompressionCodec ,
1508+ first_row_id : int | None = None ,
13851509) -> ManifestListWriter :
13861510 if format_version == 1 :
13871511 return ManifestListWriterV1 (output_file , snapshot_id , parent_snapshot_id , avro_compression )
13881512 elif format_version == 2 :
13891513 if sequence_number is None :
13901514 raise ValueError (f"Sequence-number is required for V2 tables: { sequence_number } " )
13911515 return ManifestListWriterV2 (output_file , snapshot_id , parent_snapshot_id , sequence_number , avro_compression )
1516+ elif format_version == 3 :
1517+ if sequence_number is None :
1518+ raise ValueError (f"Sequence-number is required for V3 tables: { sequence_number } " )
1519+ if first_row_id is None :
1520+ raise ValueError (f"First-row-id is required for V3 tables: { first_row_id } " )
1521+ return ManifestListWriterV3 (output_file , snapshot_id , parent_snapshot_id , sequence_number , avro_compression , first_row_id )
13921522 else :
13931523 raise ValueError (f"Cannot write manifest list for table version: { format_version } " )
0 commit comments