@@ -853,6 +853,17 @@ def partitions(self) -> list[PartitionFieldSummary] | None:
853853 def key_metadata (self ) -> bytes | None :
854854 return self ._data [14 ]
855855
856+ @property
857+ def first_row_id (self ) -> int | None :
858+ # Only present when the record is bound to the V3 manifest list schema
859+ return self ._data [15 ] if len (self ._data ) > 15 else None
860+
861+ @first_row_id .setter
862+ def first_row_id (self , value : int | None ) -> None :
863+ if len (self ._data ) <= 15 :
864+ raise ValueError ("Cannot set first_row_id on a manifest file not bound to the V3 schema" )
865+ self ._data [15 ] = value
866+
856867 def has_added_files (self ) -> bool :
857868 return self .added_files_count is None or self .added_files_count > 0
858869
@@ -1284,6 +1295,51 @@ def prepare_entry(self, entry: ManifestEntry) -> ManifestEntry:
12841295 return entry
12851296
12861297
1298+ class ManifestWriterV3 (ManifestWriterV2 ):
1299+ """Writes V3 manifest files.
1300+
1301+ The writer inherits the V2 sequence-number semantics; the V3 manifest entry
1302+ schema additionally carries `first_row_id`, `referenced_data_file`,
1303+ `content_offset` and `content_size_in_bytes` on the data file struct.
1304+ """
1305+
1306+ @property
1307+ def version (self ) -> TableVersion :
1308+ return 3
1309+
1310+ def new_writer (self ) -> AvroOutputFile [ManifestEntry ]:
1311+ # Use the V3 record layout so the V3-only data file fields are written
1312+ return AvroOutputFile [ManifestEntry ](
1313+ output_file = self ._output_file ,
1314+ file_schema = self ._with_partition (3 ),
1315+ record_schema = self ._with_partition (3 ),
1316+ schema_name = "manifest_entry" ,
1317+ metadata = self ._meta ,
1318+ )
1319+
1320+ def _wrap_data_file (self , data_file : DataFile ) -> DataFile :
1321+ """Rebind a data file to the V3 record layout."""
1322+ if len (data_file ._data ) >= len (DATA_FILE_TYPE [3 ].fields ):
1323+ return data_file
1324+ args = {
1325+ field .name : value for field , value in zip (DATA_FILE_TYPE [DEFAULT_READ_VERSION ].fields , data_file ._data , strict = True )
1326+ }
1327+ return DataFile .from_args (_table_format_version = 3 , ** args )
1328+
1329+ def prepare_entry (self , entry : ManifestEntry ) -> ManifestEntry :
1330+ entry = super ().prepare_entry (entry )
1331+ if len (entry .data_file ._data ) < len (DATA_FILE_TYPE [3 ].fields ):
1332+ entry = ManifestEntry .from_args (
1333+ _table_format_version = 3 ,
1334+ status = entry .status ,
1335+ snapshot_id = entry .snapshot_id ,
1336+ sequence_number = entry .sequence_number ,
1337+ file_sequence_number = entry .file_sequence_number ,
1338+ data_file = self ._wrap_data_file (entry .data_file ),
1339+ )
1340+ return entry
1341+
1342+
12871343def write_manifest (
12881344 format_version : TableVersion ,
12891345 spec : PartitionSpec ,
@@ -1296,6 +1352,8 @@ def write_manifest(
12961352 return ManifestWriterV1 (spec , schema , output_file , snapshot_id , avro_compression )
12971353 elif format_version == 2 :
12981354 return ManifestWriterV2 (spec , schema , output_file , snapshot_id , avro_compression )
1355+ elif format_version == 3 :
1356+ return ManifestWriterV3 (spec , schema , output_file , snapshot_id , avro_compression )
12991357 else :
13001358 raise ValueError (f"Cannot write manifest for table version: { format_version } " )
13011359
@@ -1419,19 +1477,91 @@ def prepare_manifest(self, manifest_file: ManifestFile) -> ManifestFile:
14191477 return wrapped_manifest_file
14201478
14211479
1480+ class ManifestListWriterV3 (ManifestListWriterV2 ):
1481+ """Writes V3 manifest lists, assigning `first_row_id` to data manifests.
1482+
1483+ Follows the spec's First Row ID Assignment: existing `first_row_id` values are
1484+ preserved, delete manifests are never assigned one, and data manifests without a
1485+ `first_row_id` are assigned a running value starting at the snapshot's
1486+ `first-row-id`, advanced by each assigned manifest's existing and added row counts.
1487+ """
1488+
1489+ _next_row_id : int
1490+
1491+ def __init__ (
1492+ self ,
1493+ output_file : OutputFile ,
1494+ snapshot_id : int ,
1495+ parent_snapshot_id : int | None ,
1496+ sequence_number : int ,
1497+ compression : AvroCompressionCodec ,
1498+ first_row_id : int ,
1499+ ):
1500+ super ().__init__ (output_file , snapshot_id , parent_snapshot_id , sequence_number , compression )
1501+ self ._format_version = 3
1502+ self ._meta = {
1503+ ** self ._meta ,
1504+ "first-row-id" : str (first_row_id ),
1505+ "format-version" : "3" ,
1506+ }
1507+ self ._next_row_id = first_row_id
1508+
1509+ def __enter__ (self ) -> ManifestListWriter :
1510+ """Open the writer for writing, using the V3 record layout so `first_row_id` is written."""
1511+ self ._writer = AvroOutputFile [ManifestFile ](
1512+ output_file = self ._output_file ,
1513+ record_schema = MANIFEST_LIST_FILE_SCHEMAS [3 ],
1514+ file_schema = MANIFEST_LIST_FILE_SCHEMAS [3 ],
1515+ schema_name = "manifest_file" ,
1516+ metadata = self ._meta ,
1517+ )
1518+ self ._writer .__enter__ ()
1519+ return self
1520+
1521+ @property
1522+ def next_row_id (self ) -> int :
1523+ """The row ID after the last assigned one; the table's `next-row-id` after this snapshot."""
1524+ return self ._next_row_id
1525+
1526+ def _wrap (self , manifest_file : ManifestFile ) -> ManifestFile :
1527+ """Rebind a manifest file to the V3 record layout."""
1528+ if len (manifest_file ._data ) >= len (MANIFEST_LIST_FILE_SCHEMAS [3 ].fields ):
1529+ return copy (manifest_file )
1530+ args = {
1531+ field .name : value
1532+ for field , value in zip (MANIFEST_LIST_FILE_SCHEMAS [DEFAULT_READ_VERSION ].fields , manifest_file ._data , strict = True )
1533+ }
1534+ return ManifestFile .from_args (_table_format_version = 3 , ** args )
1535+
1536+ def prepare_manifest (self , manifest_file : ManifestFile ) -> ManifestFile :
1537+ wrapped_manifest_file = super ().prepare_manifest (self ._wrap (manifest_file ))
1538+
1539+ if wrapped_manifest_file .content == ManifestContent .DATA and wrapped_manifest_file .first_row_id is None :
1540+ wrapped_manifest_file .first_row_id = self ._next_row_id
1541+ self ._next_row_id += (wrapped_manifest_file .existing_rows_count or 0 ) + (wrapped_manifest_file .added_rows_count or 0 )
1542+ return wrapped_manifest_file
1543+
1544+
14221545def write_manifest_list (
14231546 format_version : TableVersion ,
14241547 output_file : OutputFile ,
14251548 snapshot_id : int ,
14261549 parent_snapshot_id : int | None ,
14271550 sequence_number : int | None ,
14281551 avro_compression : AvroCompressionCodec ,
1552+ first_row_id : int | None = None ,
14291553) -> ManifestListWriter :
14301554 if format_version == 1 :
14311555 return ManifestListWriterV1 (output_file , snapshot_id , parent_snapshot_id , avro_compression )
14321556 elif format_version == 2 :
14331557 if sequence_number is None :
14341558 raise ValueError (f"Sequence-number is required for V2 tables: { sequence_number } " )
14351559 return ManifestListWriterV2 (output_file , snapshot_id , parent_snapshot_id , sequence_number , avro_compression )
1560+ elif format_version == 3 :
1561+ if sequence_number is None :
1562+ raise ValueError (f"Sequence-number is required for V3 tables: { sequence_number } " )
1563+ if first_row_id is None :
1564+ raise ValueError (f"First-row-id is required for V3 tables: { first_row_id } " )
1565+ return ManifestListWriterV3 (output_file , snapshot_id , parent_snapshot_id , sequence_number , avro_compression , first_row_id )
14361566 else :
14371567 raise ValueError (f"Cannot write manifest list for table version: { format_version } " )
0 commit comments