This is an automated email from the ASF dual-hosted git repository.

amogh-jahagirdar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new fd3a81b15c Spec: V4 Adaptive Metadata Tree Spec Changes for Entry 
Structures (#16025)
fd3a81b15c is described below

commit fd3a81b15c1b3d0741ac3937f9c2c29567bd0406
Author: Amogh Jahagirdar <[email protected]>
AuthorDate: Wed Oct 14 15:15:42 2026 -0600

    Spec: V4 Adaptive Metadata Tree Spec Changes for Entry Structures (#16025)
    
    Co-authored-by: Gabor Kaszab <[email protected]>
    Co-authored-by: Anurag Mantripragada <[email protected]>
    Co-authored-by: Ryan Blue <[email protected]>
    Co-authored-by: Russell Spitzer <[email protected]>
    Co-authored-by: Steven Zhen Wu <[email protected]>
    Co-authored-by: pvary <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 format/spec.md | 333 ++++++++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 256 insertions(+), 77 deletions(-)

diff --git a/format/spec.md b/format/spec.md
index 844c2b2fb6..3e8ba7232f 100644
--- a/format/spec.md
+++ b/format/spec.md
@@ -61,6 +61,8 @@ The full set of changes are listed in [Appendix 
E](#version-3).
 
 Version 4 of the Iceberg spec restructures metadata for improved performance 
and new capabilities:
 
+* Support for an [adaptive metadata tree](#manifests), enabling efficient 
small commits, column updates, and columnar statistics representation
+* Data files and their corresponding deletion vector are stored in a combined 
entry, removing the need for two-phase planning
 * Support for [relative locations](#file-locations-in-metadata) in metadata 
fields
 * Writing new [equality deletes](#equality-delete-files) is no longer allowed
 
@@ -84,9 +86,9 @@ This table format tracks individual data files in a table 
instead of directories
 
 Table state is maintained in metadata files. All changes to table state create 
a new metadata file and replace the old metadata with an atomic swap. The table 
metadata file tracks the table schema, partitioning config, custom properties, 
and snapshots of the table contents. A snapshot represents the state of a table 
at some time and is used to access the complete set of data files in the table.
 
-Data files in snapshots are tracked by one or more manifest files that contain 
a row for each data file in the table, the file's partition data, and its 
metrics. The data in a snapshot is the union of all live files in its 
manifests; each live file may only appear once (see [Content file 
uniqueness](#content-file-uniqueness)). Manifest files are reused across 
snapshots to avoid rewriting metadata that is slow-changing. Manifests can 
track data files with any subset of a table and are not [...]
+Data files in snapshots are tracked by one or more manifest files that contain 
a row for each data file in the table, the file's partition data, an optional 
colocated deletion vector (v4), and its metrics. The data in a snapshot is the 
union of all live files in its manifests; each live file may only appear once 
(see [Content file uniqueness](#content-file-uniqueness)). Manifest files are 
reused across snapshots to avoid rewriting metadata that is slow-changing. 
Manifests can track data  [...]
 
-The manifests that make up a snapshot are stored in a manifest list file. Each 
manifest list stores metadata about manifests, including partition stats and 
data file counts. These stats are used to avoid reading manifests that are not 
required for an operation.
+In v1-v3, the manifests that make up a snapshot are stored in a manifest list 
file. Each manifest list stores metadata about manifests, including partition 
stats and data file counts. These stats are used to avoid reading manifests 
that are not required for an operation. Starting in v4, manifest lists are 
replaced by a single root manifest per snapshot, which can contain references 
to data files, data manifests, and delete manifests in a unified structure.
 
 ### Optimistic Concurrency
 
@@ -145,8 +147,12 @@ Version 4 of the Iceberg spec adds support for relative 
locations in metadata, e
 * **Schema** -- Names and types of fields in a table.
 * **Partition spec** -- A definition of how partition values are derived from 
data fields.
 * **Snapshot** -- The state of a table at some point in time, including the 
set of all data files.
-* **Manifest list** -- A file that lists manifest files; one per snapshot.
-* **Manifest** -- A file that lists data or delete files; a subset of a 
snapshot.
+* **Snapshot root file** -- The per-snapshot file that tracks a snapshot's 
manifests; a manifest list (v1-v3) or a root manifest (v4+).
+* **Manifest list** -- (v1-v3 only) A file that lists manifest files; one per 
snapshot.
+* **Root manifest** -- (v4+) A manifest that can reference data files, data 
manifests, or delete manifests; one per snapshot.
+* **Leaf manifest** -- A data or delete manifest.
+* **Data manifest** -- A file that lists data files and, in v4, their deletion 
vectors and column files; a subset of a snapshot.
+* **Delete manifest** -- A file that lists delete files to be associated with 
data files at planning time.
 * **Data file** -- A file that contains rows of a table.
 * **Delete file** -- A file that encodes rows of a table that are deleted by 
position or data values.
 * **Absolute path** -- A path string that includes a 
[URI](https://datatracker.ietf.org/doc/html/rfc3986#section-3.1) scheme and can 
be used directly.
@@ -477,7 +483,7 @@ A data file with only new rows for the table may omit the 
`_last_updated_sequenc
 
 On read, if `_last_updated_sequence_number` is `null` it is assigned the 
`sequence_number` of the data file's manifest entry. The data sequence number 
of a data file is documented in [Sequence Number 
Inheritance](#sequence-number-inheritance).
 
-When `null`, a row's `_row_id` field is assigned to the `first_row_id` from 
its containing data file plus the row position in that data file (`_pos`). A 
data file's `first_row_id` field is assigned using inheritance and is 
documented in [First Row ID Inheritance](#first-row-id-inheritance). A 
manifest's `first_row_id` is assigned when writing the manifest list for a 
snapshot and is documented in [First Row ID 
Assignment](#first-row-id-assignment). A snapshot's `first-row-id` is set to th 
[...]
+When `null`, a row's `_row_id` field is assigned to the `first_row_id` from 
its containing data file plus the row position in that data file (`_pos`). A 
data file's `first_row_id` field is assigned using inheritance and is 
documented in [First Row ID Inheritance](#first-row-id-inheritance). A 
manifest's `first_row_id` is assigned when writing the snapshot root file and 
is documented in [Snapshot Root File First Row ID 
Assignment](#snapshot-root-file-first-row-id-assignment). Starting in  [...]
 
 When an existing row is moved to a different data file for any reason, writers 
should write `_row_id` and `_last_updated_sequence_number` according to the 
following rules:
 
@@ -547,7 +553,7 @@ Note that:
 
 ### Partitioning
 
-Data files are stored in manifests with a tuple of partition values that are 
used in scans to filter out files that cannot contain records that match the 
scan’s filter predicate. Partition values for a data file must be the same for 
all records stored in the data file. (Manifests store data files from any 
partition, as long as the partition spec is the same for the data files.)
+Data files are stored in manifests with a tuple of partition values that are 
used in scans to filter out files that cannot contain records that match the 
scan’s filter predicate. Partition values for a data file must be the same for 
all records stored in the data file. Manifests store data files from any 
partition. Manifests in v3 and earlier store files for a single spec. Starting 
in v4, manifests may store files from any spec. A manifest is considered 
partitioned by a spec if every ent [...]
 
 Tables are configured with a **partition spec** that defines how to produce a 
tuple of partition values from a record. A partition spec has a list of fields 
that consist of:
 
@@ -657,15 +663,27 @@ A data or delete file is associated with a sort order by 
the sort order's id wit
 
 ### Manifests
 
-A manifest is an immutable Avro file that lists data files or delete files, 
along with each file’s partition data tuple, metrics, and tracking information. 
One or more manifest files are used to store a [snapshot](#snapshots), which 
tracks all of the files in a table at some point in time. Manifests are tracked 
by a [manifest list](#manifest-lists) for each table snapshot.
+A table's metadata tree is composed of manifests. A manifest is an immutable 
file that tracks a subset of a table metadata for a given 
[snapshot](#snapshots). Leaf manifests are the lowest level of the metadata 
tree and track data or delete files, along with each file's partition data, 
metrics, and tracking information. The snapshot root file tracks leaf 
manifests; in v4 and later the root is also a manifest and can also track data 
files.
 
 A manifest is a valid Iceberg data file: files must use valid Iceberg formats, 
schemas, and column projection.
 
-A manifest may store either data files or delete files, but not both because 
manifests that contain delete files are scanned first during job planning. 
Whether a manifest is a data manifest or a delete manifest is stored in 
manifest metadata.
+Each manifest type contains the following content:
 
-A manifest stores files for a single partition spec. When a table’s partition 
spec changes, old files remain in the older manifest and newer files are 
written to a new manifest. This is required because a manifest file’s schema is 
based on its partition spec (see below). The partition spec of each manifest is 
also used to transform predicates on the table's data rows into predicates on 
partition values that are used during job planning to select files from a 
manifest.
+| Version | Manifest type   | Contents                                        
| File format |
+|---------|-----------------|-------------------------------------------------|-------------|
+| v1-v3   | Data manifest   | Data files                                      
| Avro |
+| v2-v3   | Delete manifest | Delete files                                    
| Avro |
+| v4      | Root manifest   | Leaf manifests, data files, or v1-v3 manifests  
| Parquet |
+| v4      | Leaf manifest   | Data files and their colocated deletion vectors 
| Parquet |
 
-A manifest file must store the partition spec and other metadata as properties 
in the Avro file's key-value metadata:
+- v1-v3: All entries in a manifest use the same partition spec, because the 
manifest's partition struct schema is derived from that spec.
+- v4: A manifest may store files written with different partition specs.
+
+The partition spec used when writing each data file is used to transform 
predicates on the table's data rows into predicates on partition values during 
job planning.
+
+#### Manifest File Format
+
+A manifest file must store metadata as properties in the file's key-value 
metadata:
 
 === "v1 - v3"
     | v1         | v2 and v3  | Key                 | Value                    
                                                                                
                                   |
@@ -677,15 +695,26 @@ A manifest file must store the partition spec and other 
metadata as properties i
     | _optional_ | _required_ | `format-version`    | Table format version 
number of the manifest as a string                                              
                                       |
     |            | _required_ | `content`           | Type of content files 
tracked by the manifest: "data" or "deletes"                                    
                                      |
 
+=== "v4"
+    | Requirement | Key                 | Value |
+    |-------------|---------------------|-------|
+    | _required_  | `schema-id`         | ID of the schema used to write the 
manifest as a string |
+    | _required_  | `format-version`    | Table format version number of the 
manifest as a string |
+
 #### Content file uniqueness
 
 Within a snapshot, each content file must be referenced by at most one live 
manifest entry across all manifests; otherwise, the snapshot has undefined 
behavior. Writers should not produce multiple manifest entries for the same 
content file in a snapshot (for example, both ADDED and DELETED entries for the 
same file). Writers are not required to validate uniqueness at commit time.
 
-#### Manifest Entry Fields
+#### Manifest Schema
+
+Starting in v4, manifests store `tracked_file` records that describe data or 
manifest files and contain a `tracking` struct with metadata such as `status`. 
In v1-v3 manifests, this relationship is inverted: each `manifest_entry` 
contains tracking metadata and a `data_file` struct describing the file. The v4 
manifest schema is not compatible with the v1-v3 manifest schema.
 
-The schema of a manifest file is defined by the `manifest_entry` struct, which 
consists of the following fields:
+The contents of a file are part of a snapshot if its tracking `status` is 
**live**: ADDED, EXISTING, or MODIFIED. Changes in a snapshot are tracked using 
the additional DELETED and REPLACED statuses.
 
 === "v1 - v3"
+    <a id="manifest-entry-fields"></a>
+    The v1-v3 `manifest_entry` struct has the following fields:
+
     | v1         | v2 and v3  | Field id, name                | Type           
                                           | Description |
     | ---------- | ---------- 
|-------------------------------|-----------------------------------------------------------|-------------|
     | _required_ | _required_ | **`0  status`**               | `int` with 
meaning: `0: EXISTING` `1: ADDED` `2: DELETED` | Used to track additions and 
deletions. Deletes are informational only and not used in scans. |
@@ -694,27 +723,15 @@ The schema of a manifest file is defined by the 
`manifest_entry` struct, which c
     |            | _optional_ | **`4  file_sequence_number`** | `long`         
                                           | File sequence number indicating 
when the file was added. Inherited when null and status is 1 (added). |
     | _required_ | _required_ | **`2  data_file`**            | `data_file` 
`struct` (see below)                          | File path, partition tuple, 
metrics, ... |
 
-The manifest entry fields are used to keep track of the snapshot in which 
files were added or logically deleted. The `data_file` struct, defined below, 
is nested inside the manifest entry so that it can be easily passed to job 
planning without the manifest entry fields.
-
-When a file is added to the dataset, its manifest entry should store the 
snapshot ID in which the file was added and set status to 1 (added).
-
-When a file is replaced or deleted from the dataset, its manifest entry fields 
store the snapshot ID in which the file was deleted and status 2 (deleted). The 
file may be deleted from the file system when the snapshot in which it was 
deleted is garbage collected, assuming that older snapshots have also been 
garbage collected [1].
-
-Iceberg v2 adds data and file sequence numbers to the entry and makes the 
snapshot ID optional. Values for these fields are inherited from manifest 
metadata when `null`. That is, if the field is `null` for an entry, then the 
entry must inherit its value from the manifest file's metadata, stored in the 
manifest list.
-The `sequence_number` field represents the data sequence number and must never 
change after a file is added to the dataset. The data sequence number 
represents a relative age of the file content and should be used for planning 
which delete files apply to a data file.
-The `file_sequence_number` field represents the sequence number of the 
snapshot that added the file and must also remain unchanged upon assigning at 
commit. The file sequence number can't be used for pruning delete files as the 
data within the file may have an older data sequence number.
-The data and file sequence numbers are inherited only if the entry status is 1 
(added). If the entry status is 0 (existing) or 2 (deleted), the entry must 
include both sequence numbers explicitly.
-
-Notes:
+    The manifest entry fields are used to keep track of the snapshot in which 
files were added or logically deleted. The `data_file` struct, defined below, 
is nested inside the manifest entry so that it can be easily passed to job 
planning without the manifest entry fields.
 
-1. Technically, data files can be deleted when the last snapshot that contains 
the file as “live” data is garbage collected. But this is harder to detect and 
requires finding the diff of multiple snapshots. It is easier to track what 
files are deleted in a snapshot and delete them when that snapshot expires.  It 
is not recommended to add a deleted file back to a table. Adding a deleted file 
can lead to edge cases where incremental deletes can break table snapshots.
-2. Manifest list files are required in v2, so that the `sequence_number` and 
`snapshot_id` to inherit are always available.
+    When a file is added to the dataset, its manifest entry should store the 
snapshot ID in which the file was added and set status to 1 (added).
 
-##### Data File Fields
+    When a file is replaced or deleted from the dataset, its manifest entry 
fields store the snapshot ID in which the file was deleted and status 2 
(deleted).
 
-The `data_file` struct consists of the following fields:
+    <a id="data-file-fields"></a>
+    The `data_file` struct consists of the following fields:
 
-=== "v1 - v3"
     | v1         | v2         | v3         | Field id, name                    
| Type                                                                        | 
Description |
     | ---------- 
|------------|------------|-----------------------------------|-----------------------------------------------------------------------------|-------------|
     |            | _required_ | _required_ | **`134  content`**                
| `int` with meaning: `0: DATA`, `1: POSITION DELETES`, `2: EQUALITY DELETES` | 
Type of content stored by the data file: data, equality deletes, or position 
deletes (all v1 files are data files) |
@@ -723,7 +740,6 @@ The `data_file` struct consists of the following fields:
     | _required_ | _required_ | _required_ | **`102  partition`**              
| `struct<...>`                                                               | 
Partition data tuple, schema based on the partition spec output using partition 
field ids for the struct field ids |
     | _required_ | _required_ | _required_ | **`103  record_count`**           
| `long`                                                                      | 
Number of records in this file, or the cardinality of a deletion vector |
     | _required_ | _required_ | _required_ | **`104  file_size_in_bytes`**     
| `long`                                                                      | 
Total file size in bytes |
-    |            |            |            | **`146  content_stats`**          
| `content_stats` `struct`                                                    | 
Container struct for per-field metrics structs. See [Content 
Stats](#content-stats) |
     | _required_ |            |            | ~~**`105 block_size_in_bytes`**~~ 
| `long`                                                                      | 
**Deprecated. Always write a default in v1. Do not write in v2 or v3.** |
     | _optional_ |            |            | ~~**`106  file_ordinal`**~~       
| `int`                                                                       | 
**Deprecated. Do not write.** |
     | _optional_ |            |            | ~~**`107  sort_columns`**~~       
| `list<112: int>`                                                            | 
**Deprecated. Do not write.** |
@@ -732,35 +748,141 @@ The `data_file` struct consists of the following fields:
     | _optional_ | _optional_ | _optional_ | **`110  null_value_counts`**      
| `map<121: int, 122: long>`                                                  | 
Map from column id to number of null values in the column |
     | _optional_ | _optional_ | _optional_ | **`137  nan_value_counts`**       
| `map<138: int, 139: long>`                                                  | 
Map from column id to number of NaN values in the column |
     | _optional_ | _optional_ |            | ~~**`111  distinct_counts`**~~    
| `map<123: int, 124: long>`                                                  | 
**Deprecated. Do not write.** |
-    | _optional_ | _optional_ | _optional_ | **`125  lower_bounds`**           
| `map<126: int, 127: binary>`                                                | 
Map from column id to lower bound in the column serialized as binary [1]. Each 
value must be less than or equal to all non-null, non-NaN values in the column 
for the file [2] |
-    | _optional_ | _optional_ | _optional_ | **`128  upper_bounds`**           
| `map<129: int, 130: binary>`                                                | 
Map from column id to upper bound in the column serialized as binary [1]. Each 
value must be greater than or equal to all non-null, non-Nan values in the 
column for the file [2] |
+    | _optional_ | _optional_ | _optional_ | **`125  lower_bounds`**           
| `map<126: int, 127: binary>`                                                | 
Map from column id to lower bound in the column serialized as binary. Each 
value must be less than or equal to all non-null, non-NaN values in the column 
for the file. See [Field-level Metrics and 
Statistics](#field-level-metrics-and-statistics) |
+    | _optional_ | _optional_ | _optional_ | **`128  upper_bounds`**           
| `map<129: int, 130: binary>`                                                | 
Map from column id to upper bound in the column serialized as binary. Each 
value must be greater than or equal to all non-null, non-Nan values in the 
column for the file. See [Field-level Metrics and 
Statistics](#field-level-metrics-and-statistics) |
     | _optional_ | _optional_ | _optional_ | **`131  key_metadata`**           
| `binary`                                                                    | 
Implementation-specific key metadata for encryption |
     | _optional_ | _optional_ | _optional_ | **`132  split_offsets`**          
| `list<133: long>`                                                           | 
Split offsets for the data file. For example, all row group offsets in a 
Parquet file. Must be sorted ascending |
     |            | _optional_ | _optional_ | **`135  equality_ids`**           
| `list<136: int>`                                                            | 
Field ids used to determine row equality in equality delete files. Required 
when `content=2` and should be null otherwise. Fields with ids listed in this 
column must be present in the delete file |
-    | _optional_ | _optional_ | _optional_ | **`140  sort_order_id`**          
| `int`                                                                       | 
ID representing sort order for this file [3]. |
+    | _optional_ | _optional_ | _optional_ | **`140  sort_order_id`**          
| `int`                                                                       | 
ID representing sort order for this file [1]. |
     |            |            | _optional_ | **`142  first_row_id`**           
| `long`                                                                      | 
The `_row_id` for the first row in the data file. See [First Row ID 
Inheritance](#first-row-id-inheritance) |
-    |            | _optional_ | _optional_ | **`143  referenced_data_file`**   
| `string`                                                                    | 
Fully qualified location (URI with FS scheme) of a data file that all deletes 
reference [4] |
-    |            |            | _optional_ | **`144  content_offset`**         
| `long`                                                                      | 
The offset in the file where the content starts [5] |
-    |            |            | _optional_ | **`145  content_size_in_bytes`**  
| `long`                                                                      | 
The length of a referenced content stored in the file; required if 
`content_offset` is present [5] |
+    |            | _optional_ | _optional_ | **`143  referenced_data_file`**   
| `string`                                                                    | 
Fully qualified location (URI with FS scheme) of a data file that all deletes 
reference [2] |
+    |            |            | _optional_ | **`144  content_offset`**         
| `long`                                                                      | 
The offset in the file where the content starts [3] |
+    |            |            | _optional_ | **`145  content_size_in_bytes`**  
| `long`                                                                      | 
The length of a referenced content stored in the file; required if 
`content_offset` is present [3] |
 
-The `partition` struct stores the tuple of partition values for each file. Its 
type is derived from the partition fields of the partition spec used to write 
the manifest file. In v2, the partition struct's field ids must match the ids 
from the partition spec.
+    The `partition` struct stores the tuple of partition values for each file. 
Its type is derived from the partition fields of the partition spec used to 
write the manifest file. In v2, the partition struct's field ids must match the 
ids from the partition spec.
 
-The v4 `content_stats` container struct stores field-level metrics. Unlike the 
metrics maps, the type of `content_stats` is based on table metadata, like 
schema. Similar to the `partition` struct, the same type is used for all files 
tracked in a manifest.
+    Notes:
 
-Notes:
+    1. If sort order ID is missing or unknown, then the order is assumed to be 
unsorted. Only data files and equality delete files should be written with a 
non-null order id. [Position deletes](#position-delete-files) are required to 
be sorted by file and position, not a table order, and should set sort order id 
to null. Readers must ignore sort order id for position delete files.
+    2. Position delete metadata can use `referenced_data_file` when all 
deletes tracked by the entry are in a single data file. Setting the referenced 
file is required for deletion vectors.
+    3. The `content_offset` and `content_size_in_bytes` fields are used to 
reference a specific blob for direct access to a deletion vector. For deletion 
vectors, these values are required and must exactly match the `offset` and 
`length` stored in the Puffin footer for the deletion vector blob.
+    4. The following field ids are reserved on `data_file`: 141.
 
-1. Single-value serialization for lower and upper bounds is detailed in 
Appendix D.
-2. For `float` and `double`, the value `-0.0` must precede `+0.0`, as in the 
IEEE 754 `totalOrder` predicate. NaNs are not permitted as lower or upper 
bounds.
-3. If sort order ID is missing or unknown, then the order is assumed to be 
unsorted. Only data files and equality delete files should be written with a 
non-null order id. [Position deletes](#position-delete-files) are required to 
be sorted by file and position, not a table order, and should set sort order id 
to null. Readers must ignore sort order id for position delete files.
-4. Position delete metadata can use `referenced_data_file` when all deletes 
tracked by the entry are in a single data file. Setting the referenced file is 
required for deletion vectors.
-5. The `content_offset` and `content_size_in_bytes` fields are used to 
reference a specific blob for direct access to a deletion vector. For deletion 
vectors, these values are required and must exactly match the `offset` and 
`length` stored in the Puffin footer for the deletion vector blob.
-6. The following field ids are reserved on `data_file`: 141.
+=== "v4"
+    The `tracked_file` struct has the following fields:
+
+    | On write   | Field id | Name                     | Type                  
                                | Description |
+    
|------------|----------|--------------------------|-------------------------------------------------------|-------------|
+    | _required_ | 134      | **`content_type`**       | `int` (0: DATA, 3: 
DATA_MANIFEST, 4: DELETE_MANIFEST) | Type of content stored in the entry. |
+    | _required_ | 147      | **`tracking`**           | `tracking` struct     
                                | Tracking metadata like status, snapshot ID, 
and sequence number. See [Tracking](#tracking-fields). |
+    | _required_ | 100      | **`location`**           | `string`              
                                | Location of the file. |
+    | _required_ | 101      | **`file_format`**        | `string`              
                                | String file format name: `avro`, `orc`, or 
`parquet` |
+    | _required_ | 104      | **`file_size_in_bytes`** | `long`                
                                | Total file size in bytes. |
+    | _required_ | 103      | **`record_count`**       | `long`                
                                | Number of records in this file. |
+    | _optional_ | 131      | **`key_metadata`**       | `binary`              
                                | Key metadata for encryption; specific to the 
encryption scheme. |
+    | _optional_ | 132      | **`split_offsets`**      | `list<133: long>`     
                                | Split offsets for the data file. Must be 
sorted ascending. |
+    | _optional_ | 141      | **`spec_id`**            | `int`                 
                                | ID of the partition spec used to partition 
the file; null if unpartitioned |
+    | _optional_ | 102      | **`partition`**          | `struct<...>`         
                                | Partition data tuple for the file; null if 
unpartitioned. |
+    | _optional_ | 140      | **`sort_order_id`**      | `int`                 
                                | ID representing sort order for this file. If 
missing or unknown, the order is assumed to be unsorted. |
+    | _optional_ | 146      | **`content_stats`**      | `content_stats` 
struct                                | Field-level stats. See [Content 
Stats](#content-stats). |
+    | _optional_ | 150      | **`manifest_info`**      | `manifest_info` 
struct                                | Manifest-specific stats. See [Manifest 
Info](#manifest-info-fields). |
+    | _optional_ | 148      | **`deletion_vector`**    | `deletion_vector` 
struct                              | Row-level deletion vector for a data 
file. See [Deletion Vector](#deletion-vector-fields). |
+    | _optional_ | 158      | **`column_files`**       | `list<159: 
column_file>`                              | Column files associated with this 
file. See [Column File](#column-file-fields). |
+
+    <a id="tracking-fields"></a>
+    The `tracking` struct has the following fields:
+
+    | On write   | Field id | Name                          | Type             
                                                   | Description |
+    
|------------|----------|-------------------------------|---------------------------------------------------------------------|-------------|
+    | _required_ | 0        | **`status`**                  | `int` (0: 
EXISTING, 1: ADDED, 2: DELETED, 3: REPLACED, 4: MODIFIED) | Used to track 
additions, deletions, replacements, and modifications. |
+    | _optional_ | 1        | **`snapshot_id`**             | `long`           
                                                   | Snapshot ID where the file 
was added, replaced, or deleted. Inherited when null. |
+    | _optional_ | 5        | **`modified_snapshot_id`**    | `long`           
                                                   | Snapshot ID where the file 
was last modified. |
+    | _optional_ | 3        | **`sequence_number`**         | `long`           
                                                   | Data sequence number of 
the file. Inherited when null. See [Sequence Number 
Inheritance](#sequence-number-inheritance). |
+    | _optional_ | 4        | **`file_sequence_number`**    | `long`           
                                                   | File sequence number 
indicating when the file was added. Inherited when null. See [Sequence Number 
Inheritance](#sequence-number-inheritance). |
+    | _optional_ | 142      | **`first_row_id`**            | `long`           
                                                   | Base row ID for assigning 
`_row_id` values. See [First Row ID Inheritance](#first-row-id-inheritance). |
+    | _optional_ | 6        | **`deleted_positions`**       | `binary`         
                                                   | Positions deleted via 
manifest DV in the `modified_snapshot_id` snapshot. See [Manifest Deletion 
Vectors](#manifest-deletion-vectors). |
+    | _optional_ | 7        | **`replaced_positions`**      | `binary`         
                                                   | Positions replaced via 
manifest DV in the `modified_snapshot_id` snapshot. See [Manifest Deletion 
Vectors](#manifest-deletion-vectors). |
+
+    <a id="deletion-vector-fields"></a>
+    The `deletion_vector` struct has the following fields:
+
+    | On write   | Field id | Name                | Type     | Description |
+    |------------|----------|---------------------|----------|-------------|
+    | _required_ | 155      | **`location`**      | `string` | Location of the 
file that stores the DV. |
+    | _required_ | 144      | **`offset`**        | `long`   | Offset in the 
file where the content starts. |
+    | _required_ | 145      | **`size_in_bytes`** | `long`   | Length of the 
referenced content stored in the file. |
+    | _required_ | 156      | **`cardinality`**   | `long`   | Number of set 
bits (deleted rows) in the deletion vector. |
+    | _optional_ | 149      | **`key_metadata`**  | `binary` | Key metadata 
for encryption; specific to the encryption scheme. |
+
+    <a id="manifest-info-fields"></a>
+    The `manifest_info` struct has the following fields:
+
+    | On write   | Field id | Name                       | Type                
     | Description |
+    
|------------|----------|----------------------------|--------------------------|-------------|
+    | _required_ | 521      | **`format_version`**       | `int` (0: PRE-V4, 
4: V4) | Format version used to write the manifest. |
+    | _optional_ | 522      | **`dv`**                   | `binary`            
     | Positions in the referenced leaf manifest that are not live. See 
[Manifest Deletion Vectors](#manifest-deletion-vectors). |
+    | _required_ | 504      | **`added_files_count`**    | `int`               
     | Count of entries with status ADDED in the manifest. |
+    | _required_ | 505      | **`existing_files_count`** | `int`               
     | Count of entries with status EXISTING in the manifest. |
+    | _required_ | 525      | **`modified_files_count`** | `int`               
     | Count of entries with status MODIFIED in the manifest. |
+    | _required_ | 506      | **`deleted_files_count`**  | `int`               
     | Count of entries with status DELETED in the manifest. |
+    | _required_ | 523      | **`replaced_files_count`** | `int`               
     | Count of entries with status REPLACED in the manifest. |
+    | _required_ | 512      | **`added_rows_count`**     | `long`              
     | Total number of rows in ADDED entries. |
+    | _required_ | 513      | **`existing_rows_count`**  | `long`              
     | Total number of rows in EXISTING entries. |
+    | _required_ | 526      | **`modified_rows_count`**  | `long`              
     | Total number of rows in MODIFIED entries. |
+    | _required_ | 514      | **`deleted_rows_count`**   | `long`              
     | Total number of rows in DELETED entries. |
+    | _required_ | 524      | **`replaced_rows_count`**  | `long`              
     | Total number of rows in REPLACED entries. |
+    | _required_ | 516      | **`min_sequence_number`**  | `long`              
     | Minimum data sequence number of all live entries in the manifest. |
+
+    <a id="column-file-fields"></a>
+    The `column_file` struct has the following fields:
+
+    | On write   | Field id | Name                     | Type             | 
Description |
+    
|------------|----------|--------------------------|------------------|-------------|
+    | _required_ | 160      | **`location`**           | `string`         | 
Location of the column file. |
+    | _required_ | 161      | **`field_ids`**          | `list<162: int>` | 
Live field IDs stored in this column file. |
+    | _required_ | 163      | **`file_format`**        | `string`         | 
String file format name: `avro`, `orc`, or `parquet`. |
+    | _required_ | 164      | **`file_size_in_bytes`** | `long`           | 
Total column file size in bytes. |
+    | _optional_ | 165      | **`key_metadata`**       | `binary`         | 
Key metadata for encryption; specific to the encryption scheme. |
+
+    <a id="tracked-file-requirements"></a>
+    Tracked files must meet the following requirements:
+
+    - Manifest contents:
+        - A leaf manifest written in v4 may only contain data files.
+        - A root manifest may contain manifests from any format version, but 
only v4 leaf manifests may be created by writers.
+        - Delete files from pre-v4 tables are valid in upgraded tables and are 
tracked in delete manifests written before the upgrade.
+    - `tracking`:
+        - `tracking.snapshot_id`, `tracking.sequence_number`, and 
`tracking.file_sequence_number` are required for all tracked files in the root 
manifest. `tracking.first_row_id` is also required for data files, data 
manifests, and v4 leaf manifests in the root manifest.
+        - Writers should not write a null `tracking.snapshot_id`.
+        - `tracking.deleted_positions` and `tracking.replaced_positions` may 
only be set if the tracked file is a manifest.
+    - `spec_id`:
+        - For manifests, `spec_id` must be set to the `spec_id` of the 
manifest's entries if all entries have the same `spec_id`, and must be null 
otherwise.
+    - `manifest_info`:
+        - `manifest_info` must be set if and only if the tracked file is a 
manifest.
+        - `manifest_info.format_version` must be V4 for manifests written in 
v4 and PRE-V4 for manifests written by earlier versions.
+        - For manifests, `manifest_info.added_files_count`, 
`existing_files_count`, `deleted_files_count`, `replaced_files_count`, and 
`modified_files_count` must sum to `record_count`.
+    - `deletion_vector`:
+        - `deletion_vector` may only be set if the tracked file is a data file.
+        - Row-level deletes may only be written in v4 as deletion vectors in 
the data file's `deletion_vector`.
+        - `deletion_vector.offset` and `deletion_vector.size_in_bytes` must 
exactly match the `offset` and `length` stored in the Puffin footer for the 
deletion vector blob.
+    - `column_files`:
+        - `column_files` may only be set if the tracked file is a data file or 
a v4 leaf manifest.
+        - A field ID may appear in `field_ids` of at most one column file in a 
tracked file's `column_files`.
+
+    <a id="updating-tracking-metadata"></a>
+    When a file is added to the dataset, its tracked file must set status to 
ADDED and store the snapshot ID in which the file was added.
+
+    When a data file's deletion vector or column files are updated, the writer 
must produce two entries: a MODIFIED entry for the updated live version and a 
REPLACED entry with the previous metadata (for change detection). The MODIFIED 
entry's `modified_snapshot_id` must record the snapshot ID in which the change 
occurred. The REPLACED entry can be produced in place by setting its position 
in the leaf manifest's [`manifest_info.dv`](#manifest-deletion-vectors) (to 
remove it from planning [...]
+
+    When a file is deleted from the dataset, the deletion must be recorded in 
the snapshot that deletes the file with a DELETED entry that sets `snapshot_id` 
to the snapshot in which the file was deleted. The DELETED entry can be 
produced by setting its position in the leaf manifest's 
`tracking.deleted_positions` and 
[`manifest_info.dv`](#manifest-deletion-vectors) and updating the leaf 
manifest's `tracking.modified_snapshot_id` to the new snapshot ID.
+
+    A leaf manifest whose `manifest_info.dv` changed must have status MODIFIED 
and record the snapshot in its `modified_snapshot_id`. When new positions are 
set in `manifest_info.dv`, those positions must be set in either 
`tracking.deleted_positions` or `tracking.replaced_positions` to record what 
the entry's state changed to in the snapshot identified by 
`modified_snapshot_id`. Deleted and replaced bitmaps may be removed when the 
manifest metadata is copied to a new root.
 
 ##### Field-level Metrics and Statistics
 
 Field statistics (or, interchangeably, metrics) are used when filtering to 
select data and delete files.
 
-In v3 and earlier, metrics are stored in maps keyed by field id: 
`value_counts`, `null_value_counts`, `nan_value_counts`, `lower_bounds` and 
`upper_bounds`.
+In v3 and earlier, metrics are stored in maps keyed by field id: 
`value_counts`, `null_value_counts`, `nan_value_counts`, `lower_bounds` and 
`upper_bounds`. Bounds in these maps use the single-value serialization 
detailed in [Appendix D](#appendix-d-single-value-serialization).
 
 In v4, metrics are stored as typed values in the `content_stats` struct, 
documented in the [Content Stats](#content-stats) section.
 
@@ -768,6 +890,10 @@ Both representations store equivalent information. If a 
map or id in a map is mi
 
 For delete files, metrics must store bounds and counts for all deleted rows, 
or must be omitted. Storing metrics for deleted rows ensures that the values 
can be used during job planning to find delete files that must be merged during 
a scan.
 
+###### Bounds for Float and Double
+
+For `float` and `double`, the value `-0.0` must precede `+0.0`, as in the IEEE 
754 `totalOrder` predicate. NaNs are not permitted as lower or upper bounds.
+
 ###### Bounds for Geometry and Geography
 
 For `geometry` and `geography` types, `lower_bounds` and `upper_bounds` are 
points with coordinates X, Y, Z (optional), and M (optional) that represent the 
lower and upper bounds of a bounding box that contains all objects in the file.
@@ -925,26 +1051,33 @@ Fields with stats tracked in `content_stats` change 
based on updates like schema
 
 A simple (and recommended) way for writers to adapt existing metadata for 
table changes is to read manifests with the implementation's current 
`content_stats` type and apply schema evolution rules, such as reading `int` as 
`long` for promoted fields.
 
-#### Sequence Number Inheritance
+#### Inheritance
+
+Values for `snapshot_id`, `sequence_number`, and `file_sequence_number` are 
inherited from manifest metadata when `null`. That is, if the field is `null` 
for an entry, then the entry must inherit its value from the manifest file's 
metadata, stored in the snapshot root file.
+
+##### Sequence Number Inheritance
 
 Manifests track the sequence number when a data or delete file was added to 
the table.
 
-When adding a new file, its data and file sequence numbers are set to `null` 
because the snapshot's sequence number is not assigned until the snapshot is 
successfully committed. When reading, sequence numbers are inherited by 
replacing `null` with the manifest's sequence number from the manifest list.
+The `sequence_number` field represents the data sequence number. It is set 
when a file is added to the dataset and must not change unless a column file is 
added. The data sequence number represents a relative age of the file content 
and should be used for planning which delete files apply to a data file.
+The `file_sequence_number` field represents the sequence number of the 
snapshot that added the file and must also remain unchanged upon assigning at 
commit. The file sequence number can't be used for pruning delete files as the 
data within the file may have an older data sequence number.
+
+When adding a new file to a leaf manifest, its data and file sequence numbers 
are set to `null` because the snapshot's sequence number is not assigned until 
the snapshot is successfully committed. When reading, sequence numbers are 
inherited by replacing `null` with the manifest's sequence number from the 
snapshot root file.
 It is also possible to add a new file with data that logically belongs to an 
older sequence number. In that case, the data sequence number must be provided 
explicitly and not inherited. However, the file sequence number must be always 
assigned when the snapshot is successfully committed.
 
-When writing an existing file to a new manifest or marking an existing file as 
deleted, the data and file sequence numbers must be non-null and set to the 
original values that were either inherited or provided at the commit time.
+The data sequence number is inherited only if the entry status is ADDED, or 
MODIFIED by adding a column file. The file sequence number is inherited only if 
the entry status is ADDED. When writing an existing file to a new manifest or 
marking an existing file as deleted, the data and file sequence numbers must be 
non-null and set to the original values that were either inherited or provided 
at the commit time.
 
-Inheriting sequence numbers through the metadata tree allows writing a new 
manifest without a known sequence number, so that a manifest can be written 
once and reused in commit retries. To change a sequence number for a retry, 
only the manifest list must be rewritten.
+Inheriting sequence numbers through the metadata tree allows writing a new 
manifest without a known sequence number, so that a manifest can be written 
once and reused in commit retries. To change a sequence number for a retry, 
only the snapshot root file must be rewritten.
 
-When reading v1 manifests with no sequence number column, sequence numbers for 
all files must default to 0.
+Inheritance does not apply to v1, which does not have inherited fields like 
sequence number. When reading v1 manifests with no sequence number column, 
sequence numbers for all files must default to 0.
 
-#### First Row ID Inheritance
+##### First Row ID Inheritance
 
-When adding a new data file, its `first_row_id` field is set to `null` because 
it is not assigned until the snapshot is successfully committed.
+When adding a new data file to a leaf manifest, its `first_row_id` field is 
set to `null` because it is not assigned until the snapshot is successfully 
committed.
 
 When reading, the `first_row_id` is assigned by replacing `null` with the 
manifest's `first_row_id` plus the sum of `record_count` for all data files 
that preceded the file in the manifest that also had a null `first_row_id`.
 
-The inherited value of `first_row_id` must be written into data file metadata 
when creating existing and deleted entries. The value of `first_row_id` for 
delete files is always `null`.
+The inherited value of `first_row_id` must be written into data file metadata 
for all entries with a status other than `ADDED`. The value of `first_row_id` 
for delete files is always `null`.
 
 Any null (unassigned) `first_row_id` must be assigned via inheritance, even if 
the data file is existing. This ensures that row IDs are assigned to existing 
data files in upgraded tables in the first commit after upgrading to v3.
 
@@ -966,6 +1099,20 @@ A snapshot consists of the following fields:
     |            |            | _required_ | **`added-rows`**             | 
The upper bound of the number of rows with assigned row IDs, see [Row 
Lineage](#row-lineage) |
     |            |            | _optional_ | **`key-id`**                 | ID 
of the encryption key that encrypts the manifest list key metadata |
 
+=== "v4"
+    | v4         | Field                        | Description |
+    | ---------- |------------------------------|-------------|
+    | _required_ | **`snapshot-id`**            | A unique long ID |
+    | _optional_ | **`parent-snapshot-id`**     | The snapshot ID of the 
snapshot's parent. Omitted for any snapshot with no parent |
+    | _required_ | **`sequence-number`**        | A monotonically increasing 
long that tracks the order of changes to a table |
+    | _required_ | **`timestamp-ms`**           | A timestamp when the 
snapshot was created, used for garbage collection and table inspection |
+    | _required_ | **`root-manifest`**          | The location of the root 
manifest for this snapshot |
+    | _required_ | **`summary`**                | A string map that summarizes 
the snapshot changes, including `operation` as a _required_ field (see below) |
+    | _optional_ | **`schema-id`**              | ID of the table's current 
schema when the snapshot was created |
+    | _required_ | **`first-row-id`**           | The first `_row_id` assigned 
to the first row in the first data file in the first manifest, see [Row 
Lineage](#row-lineage) |
+    | _required_ | **`added-rows`**             | The upper bound of the 
number of rows with assigned row IDs, see [Row Lineage](#row-lineage) |
+    | _optional_ | **`key-id`**                 | ID of the encryption key 
that encrypts the root manifest key metadata |
+
 When committed while the table is at version 4 or higher, a snapshot's 
`timestamp-ms` must be strictly greater than its parent snapshot's 
`timestamp-ms` on the same branch. Snapshots with no parent, and snapshots that 
were committed before a table was upgraded to v4 or higher, do not have this 
requirement.
 
 The snapshot summary's `operation` field is used by some operations, like 
snapshot expiration, to skip processing certain snapshots. Possible `operation` 
values are:
@@ -980,10 +1127,10 @@ For other optional snapshot summary fields, see 
[Appendix F](#optional-snapshot-
 Data and delete files for a snapshot can be stored in more than one manifest. 
This enables:
 
 * Appends can add a new manifest to minimize the amount of data written, 
instead of adding new records by rewriting and appending to an existing 
manifest. (This is called a “fast append”.)
-* Tables can use multiple partition specs. A table’s partition configuration 
can evolve if, for example, its data volume changes. Each manifest uses a 
single partition spec, and queries do not need to change because partition 
filters are derived from data predicates.
+* Tables can use multiple partition specs. A table’s partition configuration 
can evolve if, for example, its data volume changes. Partition predicates for a 
partition spec are derived from data predicates and can be applied to filter 
files written using that spec. Prior to v4, a manifest stored files partitioned 
by a single spec.
 * Large tables can be split across multiple manifests so that implementations 
can parallelize job planning or reduce the cost of rewriting a manifest.
 
-Manifests for a snapshot are tracked by a manifest list.
+Manifests for a snapshot are tracked by the snapshot root file and are not 
allowed in leaf manifest files.
 
 Valid snapshots are stored as a list in table metadata. For serialization, see 
Appendix C.
 
@@ -991,16 +1138,28 @@ Valid snapshots are stored as a list in table metadata. 
For serialization, see A
 
 A snapshot's `first-row-id` is assigned to the table's current `next-row-id` 
on each commit attempt. If a commit is retried, the `first-row-id` must be 
reassigned based on the table's current `next-row-id`. The `first-row-id` field 
is required even if a commit does not assign any ID space.
 
-The snapshot's `first-row-id` is the starting `first_row_id` assigned to 
manifests in the snapshot's manifest list.
+The snapshot's `first-row-id` is the starting `first_row_id` assigned to 
manifests in the snapshot root file. In v4, this includes data files in the 
root manifest.
 
 The snapshot's `added-rows` captures the upper bound of the number of rows 
with assigned row IDs.
 It can be used safely to increment the table's `next-row-id` during a commit.
 It can be more than the number of rows added in this snapshot and include some 
existing rows,
 see [Row Lineage Example](#row-lineage-example).
 
+<a id="first-row-id-assignment"></a>
+#### Snapshot Root File First Row ID Assignment
+
+The `first_row_id` for existing manifests must be preserved when writing a new 
snapshot root file. The value of `first_row_id` for delete manifests is always 
`null`. The `first_row_id` is only assigned for data manifests that do not have 
a `first_row_id`. Assignment must account for data files that will be assigned 
`first_row_id` values when the manifest is read. Data files in the root 
manifest (v4+) must also have a `first_row_id`: existing values must be 
preserved and `first_row_id` is [...]
+
+The first file in the snapshot root file without a `first_row_id` is assigned 
a value that is greater than or equal to the `first_row_id` of the snapshot. 
Subsequent files without a `first_row_id` are assigned one based on the 
previous file to be assigned a `first_row_id`. Each assigned `first_row_id` 
must be greater than or equal to the last assigned `first_row_id` plus the row 
count of the last assigned file, where the row count is:
+
+* For a manifest, the sum of `record_count` for its data files with a null 
`first_row_id`.
+* For a data file, its `record_count`.
+
+A simple and valid approach is to estimate the number of rows in data files 
that will be assigned a `first_row_id` using the manifest's `added_rows_count` 
and `existing_rows_count`: `first_row_id = last_assigned.first_row_id + 
last_assigned.added_rows_count + last_assigned.existing_rows_count`.
+
 ### Manifest Lists
 
-Snapshots are embedded in table metadata, but the list of manifests for a 
snapshot are stored in a separate manifest list file.
+Snapshots are embedded in table metadata, but the manifests for a snapshot are 
tracked in a separate snapshot root file. Manifest lists are the snapshot root 
file used by v1-v3; v4 uses a [root manifest](#manifests).
 
 A new manifest list is written for each attempt to commit a snapshot because 
the list of manifests always changes to produce a new snapshot. When a manifest 
list is written, the (optimistic) sequence number of the snapshot is written 
for all new manifest files tracked by the list.
 
@@ -1028,7 +1187,7 @@ Manifest list files store `manifest_file`, a struct with 
the following fields:
     | _optional_ | _required_ | _required_ | **`514 deleted_rows_count`**      
  | `long`                                      | Number of rows in all of 
files in the manifest that have status `DELETED`, when `null` this is assumed 
to be non-zero |
     | _optional_ | _optional_ | _optional_ | **`507 partitions`**              
  | `list<508: field_summary>` **(see below)**  | A list of field summaries for 
each partition field in the spec. Each field in the list corresponds to a field 
in the manifest file’s partition spec. |
     | _optional_ | _optional_ | _optional_ | **`519 key_metadata`**            
  | `binary`                                    | Implementation-specific key 
metadata for encryption |
-    |            |            | _optional_ | **`520 first_row_id`**            
  | `long`                                      | The starting `_row_id` to 
assign to rows added by `ADDED` data files [First Row ID 
Assignment](#first-row-id-assignment) |
+    |            |            | _optional_ | **`520 first_row_id`**            
  | `long`                                      | The starting `_row_id` to 
assign to rows added by `ADDED` data files [Snapshot Root File First Row ID 
Assignment](#snapshot-root-file-first-row-id-assignment) |
 
 `field_summary` is a struct with the following fields:
 === "v1 - v3"
@@ -1044,37 +1203,32 @@ Notes:
 1. Lower and upper bounds are serialized to bytes using the single-object 
serialization in Appendix D. The type of used to encode the value is the type 
of the partition field data.
 2. If -0.0 is a value of the partition field, the `lower_bound` must not be 
+0.0, and if +0.0 is a value of the partition field, the `upper_bound` must not 
be -0.0.
 
-#### First Row ID Assignment
+### Scan Planning
 
-The `first_row_id` for existing manifests must be preserved when writing a new 
manifest list. The value of `first_row_id` for delete manifests is always 
`null`. The `first_row_id` is only assigned for data manifests that do not have 
a `first_row_id`. Assignment must account for data files that will be assigned 
`first_row_id` values when the manifest is read.
+A reader plans a scan by producing live data files from the snapshot root file 
and any live leaf manifests referenced by the root.
 
-The first manifest without a `first_row_id` is assigned a value that is 
greater than or equal to the `first_row_id` of the snapshot. Subsequent 
manifests without a `first_row_id` are assigned one based on the previous 
manifest to be assigned a `first_row_id`. Each assigned `first_row_id` must 
increase by the row count of all files that will be assigned a `first_row_id` 
via inheritance in the last assigned manifest. That is, each `first_row_id` 
must be greater than or equal to the last as [...]
+A scan uses only [live](#manifest-schema) entries.
 
-A simple and valid approach is to estimate the number of rows in data files 
that will be assigned a `first_row_id` using the manifest's `added_rows_count` 
and `existing_rows_count`: `first_row_id = last_assigned.first_row_id + 
last_assigned.added_rows_count + last_assigned.existing_rows_count`.
+Manifests that contain no matching files, determined using file counts, 
partition summaries, or column stats, may be skipped.
 
-### Scan Planning
+Using content stats, a manifest is filtered by evaluating scan predicates 
against the column bounds and counts of each tracked file. The same filter 
logic can be used for both data and delete files because both store metrics of 
the rows either inserted or deleted. If metrics show that a delete file has no 
rows that match a scan predicate, it may be ignored just as a data file would 
be ignored [1].
 
-Scans are planned by reading the manifest files for the current snapshot. 
Deleted entries in data and delete manifests (those marked with status 
"DELETED") are not used in a scan.
+v1-v3 manifests are filtered using scan predicates converted to partition 
predicates, which filter partition tuples. These partition predicates are used 
to select relevant data files, delete files, and deletion vector metadata. 
Conversion uses the partition spec that was used to write the manifest file 
regardless of the current partition spec. Column bounds and counts stored by 
field id in metrics maps are used in the same way as content stats.
 
-Manifests that contain no matching files, determined using either file counts 
or partition summaries, may be skipped.
-
-For each manifest, scan predicates, which filter data rows, are converted to 
partition predicates, which filter partition tuples. These partition predicates 
are used to select relevant data files, delete files, and deletion vector 
metadata. Conversion uses the partition spec that was used to write the 
manifest file regardless of the current partition spec.
-
-Scan predicates are converted to partition predicates using an _inclusive 
projection_: if a scan predicate matches a row, then the partition predicate 
must match that row’s partition. This is called _inclusive_ [1] because rows 
that do not match the scan predicate may be included in the scan by the 
partition predicate.
+Scan predicates are converted to partition predicates using an _inclusive 
projection_: if a scan predicate matches a row, then the partition predicate 
must match that row’s partition. This is called _inclusive_ [2] because rows 
that do not match the scan predicate may be included in the scan by the 
partition predicate.
 
 For example, an `events` table with a timestamp column named `ts` that is 
partitioned by `ts_day=day(ts)` is queried by users with ranges over the 
timestamp column: `ts > X`. The inclusive projection is `ts_day >= day(X)`, 
which is used to select files that may have matching rows. Note that, in most 
cases, timestamps just before `X` will be included in the scan because the file 
contains rows that match the predicate and rows that do not match the predicate.
 
 The inclusive projection for an unknown partition transform is _true_ because 
the partition field is ignored and not used in filtering.
 
-Scan predicates are also used to filter data and delete files using column 
bounds and counts that are stored by field id in manifests. The same filter 
logic can be used for both data and delete files because both store metrics of 
the rows either inserted or deleted. If metrics show that a delete file has no 
rows that match a scan predicate, it may be ignored just as a data file would 
be ignored [2].
-
 Data files that match the query filter must be read by the scan.
 
 Duplicate live manifest entries for the same content file violate [content 
file uniqueness](#content-file-uniqueness). When duplicate live entries are 
present, table behavior (including scan results) is undefined. Reader 
implementations may raise an error but are not required to do so.
 
 Delete files and deletion vector metadata that match the filters must be 
applied to data files at read time, limited by the following scope rules.
 
-* A deletion vector must be applied to a data file when all of the following 
are true:
+* A colocated deletion vector must be applied to the data file tracked by the 
same entry. No path, sequence number, or partition comparison applies because 
the vector is colocated with the data file.
+* A deletion vector tracked in a delete manifest must be applied to a data 
file when all of the following are true:
     - The data file's `file_path` is equal to the deletion vector's 
`referenced_data_file`
     - The data file's data sequence number is _less than or equal to_ the 
deletion vector's data sequence number
     - The data file's partition (both spec and partition values) is equal [4] 
to the deletion vector's partition
@@ -1094,8 +1248,8 @@ In general, deletes are applied only to data files that 
are older and in the sam
 
 Notes:
 
-1. An alternative, *strict projection*, creates a partition predicate that 
will match a file if all of the rows in the file must match the scan predicate. 
These projections are used to calculate the residual predicates for each file 
in a scan.
-2. For example, if `file_a` has rows with `id` between 1 and 10 and a delete 
file contains rows with `id` between 1 and 4, a scan for `id = 9` may ignore 
the delete file because none of the deletes can match a row that will be 
selected.
+1. For example, if `file_a` has rows with `id` between 1 and 10 and a delete 
file contains rows with `id` between 1 and 4, a scan for `id = 9` may ignore 
the delete file because none of the deletes can match a row that will be 
selected.
+2. An alternative, *strict projection*, creates a partition predicate that 
will match a file if all of the rows in the file must match the scan predicate. 
These projections are used to calculate the residual predicates for each file 
in a scan.
 3. Floating point partition values are considered equal if their IEEE 754 
floating-point "single format" bit layout are equal with NaNs normalized to 
have only the most significant mantissa bit set (the equivalent of calling 
`Float.floatToIntBits` or `Double.doubleToLongBits` in Java). The Avro 
specification requires all floating point values to be encoded in this format.
 4. Unknown partition transforms do not affect partition equality. Although 
partition fields with unknown transforms are ignored for filtering, the result 
of an unknown transform is still used when testing whether partition values are 
equal.
 
@@ -1133,6 +1287,14 @@ When expiring snapshots, retention policies in table and 
snapshot references are
     2. The snapshot is not one of the first `min-snapshots-to-keep` in the 
branch (including the branch's referenced snapshot)
 5. Expire any snapshot not in the set of snapshots to retain.
 
+#### Deleting Files
+
+A file may be deleted from the file system when the snapshot in which it was 
deleted is garbage collected, assuming that the file is not live in any 
remaining snapshot [1].
+
+Notes:
+
+1. Technically, data files can be deleted when the last snapshot that contains 
the file as "live" data is garbage collected. But this is harder to detect and 
requires finding the diff of multiple snapshots. When all snapshots are 
ancestors of a single branch, it is easier to track what files are deleted in a 
snapshot and delete them when that snapshot expires.  It is not recommended to 
add a deleted file back to a table. Adding a deleted file can lead to edge 
cases where incremental dele [...]
+
 ### Table Metadata
 
 Table metadata is stored as JSON. Each table metadata change creates a new 
table metadata file that is committed by an atomic operation. This operation is 
used to ensure that a new version of table metadata replaces the version on 
which it was based. This produces a linear history of table versions and 
ensures that concurrent writes are not lost.
@@ -1171,6 +1333,7 @@ Table metadata consists of the following fields:
     | _optional_ | _optional_ | _optional_ | **`partition-statistics`**  | A 
list (optional) of [partition statistics](#partition-statistics). |
     |            |            | _required_ | **`next-row-id`**           | A 
`long` higher than all assigned row IDs; the next snapshot’s `first-row-id`. 
See [Row Lineage](#row-lineage). |
     |            |            | _optional_ | **`encryption-keys`**       | A 
list (optional) of [encryption keys](#encryption-keys) used for table 
encryption. |
+
 === "v4"
     | v4         | Field                       | Description |
     |------------|-----------------------------|-------------|
@@ -1370,10 +1533,12 @@ When removing a data file, writers must also remove any 
deletion vector that app
 
 Row-level delete files (both equality and position delete files) are valid 
Iceberg data files: files must use valid Iceberg formats, schemas, and column 
projection. It is recommended that these delete files are written using the 
table's default file format.
 
-Row-level delete files and deletion vectors are tracked by manifests. A 
separate set of manifests is used for delete files and DVs, but the same 
manifest schema is used for both data and delete manifests. Deletion vectors 
are tracked individually by file location, offset, and length within the 
containing file. Deletion vector metadata must include the referenced data file.
-
 Both position and equality delete files allow encoding deleted row values with 
a delete. This can be used to reconstruct a stream of changes to a table.
 
+Row-level delete files and deletion vectors are tracked by manifests. Delete 
manifests (v2-v3) track delete files and DVs separately from data manifests, 
using the same manifest schema. Deletion vectors in delete manifests are 
tracked individually by file location, offset, and length within the containing 
file, and their metadata must include the referenced data file.
+
+Starting in v4, writers must colocate new deletion vectors with their data 
file using the [`deletion_vector`](#deletion-vector-fields) field. Deletion 
vectors in delete manifests written before an upgrade to v4 remain valid.
+
 #### Deletion Vectors
 
 Deletion vectors identify deleted rows of a file by encoding deleted positions 
in a bitmap. A set bit at position P indicates that the row at position P is 
deleted.
@@ -1390,6 +1555,20 @@ At most one deletion vector is allowed per data file in 
a snapshot. If a DV is w
 
 [puffin-spec]: puffin-spec.md
 
+#### Manifest Deletion Vectors
+
+Manifest deletion vectors identify entries of a leaf manifest that are no 
longer live by encoding their positions in a bitmap. A set bit at position P 
indicates that the entry at position P in the leaf manifest was either deleted 
or replaced.
+
+Manifest deletion vectors are encoded using the [Mumbling bitmap 
spec][mumbling-spec] and stored inline on the root manifest entry that 
references the leaf manifest. The snapshot in which the vector last changed is 
recorded in `tracking.modified_snapshot_id`; the three bitmaps are:
+
+* `manifest_info.dv`: every position in the leaf manifest that is not live.
+* `tracking.deleted_positions`: the positions deleted in the 
`modified_snapshot_id` snapshot.
+* `tracking.replaced_positions`: the positions replaced in the 
`modified_snapshot_id` snapshot.
+
+`deleted_positions` and `replaced_positions` are disjoint.
+
+[mumbling-spec]: https://iceberg.apache.org/mumbling-spec/
+
 #### Position Delete Files
 
 Position-based delete files identify deleted rows by file and position in one 
or more data files, and may optionally contain the deleted row.

Reply via email to