Skip to content

Spec: V4 Adaptive Metadata Tree Spec Changes for Entry Structures - #16025

Open
amogh-jahagirdar wants to merge 21 commits into
apache:mainfrom
amogh-jahagirdar:v4-amt-changes
Open

Spec: V4 Adaptive Metadata Tree Spec Changes for Entry Structures#16025
amogh-jahagirdar wants to merge 21 commits into
apache:mainfrom
amogh-jahagirdar:v4-amt-changes

Conversation

@amogh-jahagirdar

@amogh-jahagirdaramogh-jahagirdar commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

This is one PR for V4 Adaptive Metadata Tree spec changes. The focus of this PR is to update the proposed entry structure in https://docs.google.com/document/d/1k4x8utgh41Sn1tr98eynDKCWq035SV_f75rtNHcerVw/edit?tab=t.wd1z5eeup025#heading=h.80fbnuij9rhg

There will be other PRs for updating the scan planning section, adding implementation notes for CDC etc.

@github-actionsgithub-actionsBot added the Specification Issues that may introduce spec changes. label Apr 18, 2026
@amogh-jahagirdaramogh-jahagirdar changed the title V4 amt spec changesSpec: V4 Adaptive Metadata Tree Spec Changes for Entry StructuresApr 28, 2026
@amogh-jahagirdar

Copy link
Copy Markdown
ContributorAuthor

Two general working principals:

  1. Trying to make it easy for someone who just wants to implement v4 to click the v4 tabs, and have it be self describing.
  2. Try to do 1 without shifting too much of the existing spec contents, to avoid the risk of changing the spec for v1-v3.

Comment threadformat/spec.md
| _optional_ | _optional_ | **`min-snapshots-to-keep`** | `int` | For `branch` type only, a positive number for the minimum number of snapshots to keep in a branch while expiring snapshots. Defaults to table property `history.expire.min-snapshots-to-keep`. |
| _optional_ | _optional_ | **`max-snapshot-age-ms`** | `long` | For `branch` type only, a positive number for the max age of snapshots to keep when expiring, including the latest snapshot. Defaults to table property `history.expire.max-snapshot-age-ms`. |
| _optional_ | _optional_ | **`max-ref-age-ms`** | `long` | For snapshot references except the `main` branch, a positive number for the max age of the snapshot reference to keep while expiring snapshots. Defaults to table property `history.expire.max-ref-age-ms`. The `main` branch never expires. |
=== "v1 - v3"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If people like the two tabbed approach then we should get #14656 in first, that'll make this diff easier to review

Comment threadformat/spec.md Outdated
@@ -130,8 +130,10 @@ Tables do not require rename, except for tables that use atomic rename to implem
* **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.
* **Manifest list** -- (V1-V3 only) A file that lists manifest files; one per snapshot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: most of the time, lower case v1, v2 are used in the spec

Comment threadformat/spec.md
* **Manifest** -- A file that lists data or delete files; a subset of a snapshot.
* **Manifest list** -- (V1-V3 only) A file that lists manifest files; one per snapshot.
* **Root Manifest** -- (V4+) A manifest that can reference data files, delete files, and other data and delete manifests; one per snapshot. Replaces manifest lists in V4.
* **Data manifest** -- A file that lists data files; a subset of a snapshot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to want to mention colocated DVs and column files for data manifests in v4+?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems reasonable to me.

Comment threadformat/spec.md Outdated
@@ -484,7 +486,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 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. In V1-V3, manifests store data files from any partition, as long as the partition spec is the same for the data files. In V4, manifests can store data files from different partition specs because partition values are stored as column statistics.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because partition values are stored as column statistics.

This sentence probably needs to be updated based on the last community sync.

Comment threadformat/spec.md Outdated

| Field id | Name | Type | Write | Read | Description |
|----------|------|------|-------|------|-------------|
| 134 | **`content_type`** | `int` (0: DATA, 2: EQUALITY DELETES, 3: DATA_MANIFEST, 4: DELETE_MANIFEST) | *required* | *required* | Type of content stored in the entry. Content types 3 and 4 are only valid in root manifests. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POSITION_DELETES handling — the table omits value 1 entirely, but the comment two paragraphs below ("Value 1 (POSITION_DELETES) no longer applies in entries") is the only place that says so. Worth making the table itself unambiguous: either list 1: RESERVED (writers must not produce; readers must reject) or call out in the table cell that 1 is intentionally skipped. Otherwise a reader reaches for the int and wonders if the spec just forgot it.

Comment threadformat/spec.md Outdated
| Field id | Name | Type | Write | Read | Description |
|----------|------|------|-------|------|-------------|
| 134 | **`content_type`** | `int` (0: DATA, 2: EQUALITY DELETES, 3: DATA_MANIFEST, 4: DELETE_MANIFEST) | *required* | *required* | Type of content stored in the entry. Content types 3 and 4 are only valid in root manifests. |
| 157 | **`writer_format_version`** | `int` (0: PRE-V4, 1: V4) | *required* | *required* | Writer format version. V4 writers must produce `writer_format_version` 1. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

V4 writers must produce writer_format_version 1.

why not 4 for V4 writers? null for entries written before V4.

Also, the 0: PRE-V4 enum is odd: PRE-V4 manifests use the manifest_entry schema, not content_entry, so they can't have this field. When does value 0 ever appear?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should always be 1, right? Shouldn't we remove this and allocate field ID 157 to column_files? cc @amogh-jahagirdar

Comment threadformat/spec.md Outdated
| 158 | **`column_files`** | `list<column_file>` | *optional* | *optional* | Column update files associated with this entry. |
| 101 | **`file_format`** | `string` | *required* | *required* | String file format name: `avro`, `orc`, `parquet`, or `puffin` |
| 147 | **`tracking`** | `tracking` struct | *required* | *required* | Groups status, snapshot, and sequence number. See tracking struct below. |
| 148 | **`deletion_vector`** | `deletion_vector` struct | *optional* | *optional* | Row-level deletion vector for a data file. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the deletion_vector schema is only for external data DVs with location field. the inline manifest DV is stored as part of the manifest_info, which is a bit inconsistent to me. Ideally, the DV struct can capture both outline (data DV) and inline (manifest DV) at the top level. E.g. the cardinality field can be shared.

Comment threadformat/spec.md Outdated

Value 1 (POSITION_DELETES) no longer applies in entries because deletion vector metadata is colocated with data files (`content_type` 0).

Leaf data manifests may only contain entries with `content_type` 0 (DATA); leaf delete manifests may only contain entries with `content_type` 2 (EQUALITY DELETES).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaf delete manifest files can also contain entries with position deletes written back when the format version is 2. that also means the line 683 above is also inaccurate

Comment threadformat/spec.md

Leaf data manifests may only contain entries with `content_type` 0 (DATA); leaf delete manifests may only contain entries with `content_type` 2 (EQUALITY DELETES).

The following constraints apply based on `content_type`:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is better to capture these in the description column in the table (instead of a separate bullet list here).

Comment threadformat/spec.md Outdated
Comment threadformat/spec.md
| | | _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 manifest list key metadata |
=== "v1 - v3"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also need a table for v4

Comment threadformat/spec.md Outdated
@@ -75,9 +75,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 files in its manifests. 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 associated with partitions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add anything up front about V4 unifying this structure? I just note that you extended this paragraph to say Data Manifests and Delete Manifests, but now we will only have one type of manifest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a good idea to me.

Comment threadformat/spec.md Outdated

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. In V4, manifest lists are replaced by a single root manifest per snapshot, which can contain references to data files, delete files, and other data and delete manifests in a unified structure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought technically we aren't allowing a reference to a delete file in the Root Manifest or in any V4 Manifest except for V4 Delete manifests for equality deletes. Shouldn't it always be a coupled entry of DV and DataFile or DV and Manifest?

Comment threadformat/spec.md

A manifest is a valid Iceberg data file: files must use valid Iceberg formats, schemas, and column projection.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we are changing the pluralization here but the change is ok. Just wondering because we immediately switch back to singular in the next paragraph.

Comment threadformat/spec.md Outdated
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.
- V1-V3: 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. The partition spec of each manifest is used to transform predicates on the table’s data rows into predicates on partition values during job planning.
- V4: Manifests are not bound to a single partition spec. Files with different partition specs can coexist in the same manifest because partition values are stored in column statistics using source column IDs rather than in a partition-spec-specific struct. The `partition-spec-id` in manifest metadata is tracked for informational purposes but does not constrain the contents.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave the rational out of this paragraph. I think it's find to just say that they are not bound to a partition spec, I think partition-spec-id needs a better description here ... The spec id used by the writer when generating this data file?

Comment threadformat/spec.md Outdated

#### Manifest File Format

Manifests are Avro files in V1-V3. Starting in V4, writers must produce manifests in Parquet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I support this for simplicity, I know @rdblue still wants to have the Avro option. currently the code in my PR lets you write either in the SDK and i'm not sure it is much more expensive to allow both in the spec. Worth having a community discussion though.

Comment threadformat/spec.md Outdated
| | _required_ | `content` | Type of content files tracked by the manifest: "data" or "deletes" |

=== "v4"
| Write | Read | Key | Value |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there write and read? What would optional "read" be? I assume this was probably an LLM just trying to make a balanced table.

More importantly, do we want to relax the write requirements? In V2/3 these were all required, but now they are optional

Comment threadformat/spec.md Outdated
| Write | Read | Key | Value |
|------------|------------|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
| _optional_ | _optional_ | `schema-id` | ID of the schema used to write the manifest as a string |
| _optional_ | _optional_ | `partition-spec-id` | ID of the partition spec used to write the manifest as a string |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this one makes sense now? Entries should all have a spec, but i'm not sure it makes sense to have a global spec id for the manifest anymore?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Miss on my part, yes manifests are no longer bound to a partition spec!

Comment threadformat/spec.md Outdated
| _optional_ | _optional_ | `schema-id` | ID of the schema used to write the manifest as a string |
| _optional_ | _optional_ | `partition-spec-id` | ID of the partition spec used to write the manifest as a string |
| _optional_ | _optional_ | `format-version` | Table format version number of the manifest as a string |
| _optional_ | _optional_ | `content` | Type of content files tracked by the manifest: "data" or "deletes" |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In V4 this is data or "equality deletes" but I think it's fine to just call it deletes

Comment threadformat/spec.md Outdated
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add the MODIFIED status also? Also, would you please update the AMT proposal doc to include the status? (Also we need to move the content from the Colocated entry tab into the main tab?)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I still need to update these when I get a chance, will do

@stevenzwustevenzwu mentioned this pull request Jun 6, 2026
3 tasks
stevenzwu added a commit to stevenzwu/iceberg that referenced this pull request Jun 6, 2026
Add the writer_format_version field (id 157, required int) to the v4
content_entry schema per spec PR apache#16025. Values: 0 = pre-v4 (entries
referencing legacy leaf manifests carried over during a v3->v4 upgrade),
1 = v4 (entries produced by a v4 writer).
The field is positioned after content_type and before location in
TrackedFile.schemaWithContentStats(...). TrackedFileStruct gains a
positional slot, default value 1, a setter, and copy-preservation.
Tests cover schema field-id presence, default-1, explicit-0, and
round-trip through copy/copyWithStats/copyWithoutStats.
No validation is added at this layer; the read-side bounds check
(SUPPORTED_WRITER_FORMAT_VERSION = 1) lands with the v4 leaf reader in
a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stevenzwu added a commit to stevenzwu/iceberg that referenced this pull request Jun 7, 2026
…add matching reader
V4Writer and V4DeleteWriter now emit content_entry Parquet rows via
TrackedFileWrapper/ContentEntryAdapter rather than the legacy manifest_entry
Avro shape. ContentEntryReader and ContentEntryManifestReaderAdapter project
content_entry rows back to ManifestEntry<DataFile/DeleteFile> so all downstream
consumers (ManifestGroup, MergingSnapshotProducer rewrite paths) work
unchanged.
Read-path dispatch in ManifestFiles is layered:
1. Avro manifests are always legacy (no file inspection).
2. Snapshot-tree callers thread an Integer writerFormatVersion hint through
the new package-private read overloads: 1 routes to ContentEntryReader,
0 routes to legacy.
3. Callers without a hint (tests writing-then-reading, ad-hoc tooling) fall
back to inspecting the Parquet footer schema for field id 134 (content_type)
or 147 (tracking). The footer read is delegated to InternalParquet via
DynMethods so core has no compile-time dependency on iceberg-parquet.
Key design choices:
- TrackedFile.schemaWithContentStats omits partition and content_stats when
their struct types are empty (Parquet rejects empty groups).
- TrackedFileWrapper uses hasPartition/hasContentStats flags to map positions
dynamically when either optional group is absent.
- V4Writer.add(DataFile) bypasses Delegates.suppressFirstRowId so per-entry
firstRowId is stored in the tracking struct rather than at manifest level.
- ContentEntryReader.setEntry uses wrapAppendPreservingFirstRowId for ADDED
entries so firstRowId read from the tracking struct is not re-suppressed.
- ContentEntryAdapter preserves firstRowId for EXISTING entries so uncommitted
manifests can round-trip per-entry row IDs.
- ContentEntryManifestReaderAdapter applies the same committed/uncommitted
firstRowId nullification logic as ManifestReader.idAssigner.
- ContentEntryManifestReaderAdapter.iterator tracks ordinal position and sets
fileOrdinal and manifestLocation on each BaseFile to match Avro reader behavior.
- Parquet.readSchema(InputFile) is a new public helper that returns just the
Iceberg-converted file schema; InternalParquet.readSchema delegates to it
for the DynMethods entry point.
- v4 spec forbids content_type=POSITION_DELETES (PR apache#16025); three
TestManifestReader tests that write standalone position-delete files / DV
delete files are guarded with assumeThat isLessThan(4) and will be removed
once PR apache#16677 (or its successor) gates v4 out of the broad parameterized
test suite during incubation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stevenzwu added a commit to stevenzwu/iceberg that referenced this pull request Jun 11, 2026
Add the writer_format_version field (id 157, required int) to the v4
content_entry schema per spec PR apache#16025. Values: 0 = pre-v4 (entries
referencing legacy leaf manifests carried over during a v3->v4 upgrade),
1 = v4 (entries produced by a v4 writer).
The field is positioned after content_type and before location in
TrackedFile.schemaWithContentStats(...). TrackedFileStruct gains a
positional slot, default value 1, a setter, and copy-preservation.
Tests cover schema field-id presence, default-1, explicit-0, and
round-trip through copy/copyWithStats/copyWithoutStats.
No validation is added at this layer; the read-side bounds check
(SUPPORTED_WRITER_FORMAT_VERSION = 1) lands with the v4 leaf reader in
a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadformat/spec.md
| 167 | **`key_metadata`** | `binary` | *optional* | *optional* | Implementation-specific key metadata for encryption. |
| 168 | **`split_offsets`** | `list<169: long>` | *optional* | *optional* | Split offsets for the column file. Must be sorted ascending. |

When a file is added to the dataset, its content entry must set status to ADDED (1) and store the snapshot ID in which the file was added.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we cover the status transitions: e.g. a file/manifest starts with ADDED, but the next commit it flips to EXISTING (unless it got deleted/replaced). Same for MODIFIED -> EXISTING transition.

But for files that live in leaf manifests, we don't want to rewrite manifests for the sake of doing the status transition. For instance, if we modified DVs on a bunch of files, we might write a new leaf manifest with a bunch of MODIFIED entries (and the manifest status itself would have an ADDED?), but in the next commit, the leaf status might change, but the entries inside the leaf stays MODIFIED.

@amogh-jahagirdaramogh-jahagirdarAug 7, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we cover the status transitions: e.g. a file/manifest starts with ADDED, but the next commit it flips to EXISTING (unless it got deleted/replaced). Same for MODIFIED -> EXISTING transition.

I'm not sure if we explicitly need to call out the ADDED/MODIFIED -> EXISTING transitions? I think the only state transitions we need to call out are the ones where it's a net new change in a given snapshot.

If you look at the current spec (not these changes but the ones for v1-v3), we never really needed to call out the explicit EXISTING transition because anything that was a new change, like ADDED/DELETED had explicit mentions. After defining those, it's very reasonably implied and via the naming of the status EXISTING that any carry over should just be marked as EXISTING.

If we want to have EXISTING be this explicit transition I think we can do it but we'll want to be not overly-specified to the point where we prevent certain write patterns (more on that in the next response).

But for files that live in leaf manifests, we don't want to rewrite manifests for the sake of doing the status transition. For instance, if we modified DVs on a bunch of files, we might write a new leaf manifest with a bunch of MODIFIED entries (and the manifest status itself would have an ADDED?), but in the next commit, the leaf status might change, but the entries inside the leaf stays MODIFIED.

Agree, we definitley don't want to have to rewrite manifests just to update statuses, that would be a crazy bad requirement but I don't know if that means we need to codify explicit transition rules for EXISTING in the spec. I just worry that if we do that too strictly it may be misinterpreted and prevent valid write scenarios.

Every additional statement we make in the spec bears the risk of misinterpretation which is where I'm coming from that we need to be careful.

Concretely:

Status just indicates what the status of that file in the tree is (whether it's a data file or a manifest is inconsequential)

Assume we stick with the status quo of not epxlicitly defining when to state transition to Existing and rather it's implied that anything that's not a new change (not ADDED/DELETED/MODIFIED/REPLACED) is EXISTING:
Both options of the following options are valid:

1/ In the scenario you described, a writer can choose to mark the leaf manifest as EXISTING
or
2/ A writer can choose to actually just write a new manifest with EXISTING entries and mark the old manifest as DELETED.

Could we explicitly enumerate every single state transition or find some wording that generalizes across use cases? Probably, but I think I'd rather just be explicit cases where we know specific statuses are required to be produce (like DV changes/column updates producing REPLACED + MODIFIED, file removals explicitly producing DELETED).

That's a smaller state to enumerate and less chance of misinterpretation preventing legitimate write scenarios. Every other status is just naturally implied and readers/change detection should correctly handle those, unless there's some ambiguous case we're worried about.

Let me know what you think @anoopj , cc @stevenzwu@RussellSpitzer@rdblue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like how we manage the state transitions is rather straightforward: transitions only happen when the metadata is modified. So if a leaf file is not being rewritten, all of the status remain as they were when originally produced.

The important aspect of this is that the status of entries within the manifest file represents the state as of the snapshot where the manifest was written. If you later rewrite a manifest and see that there were entries that had a ADDED, MODIFIED or REPLACED they would then transition to EXISTING within the new manifest and that is now the effective state as of that new snapshot.

This isn't different than how v3 and prior work. Consider an append only table that only produces new files and new metadata files (no metadata rewrite). All entries will have a status of ADDED forever. We don't go back and rewrite all of the entries for every commit to be EXISTING.

We shouldn't have to rewrite older metadata, rather we should interpret in context of the when it was written.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you later rewrite a manifest and see that there were entries that had a ADDED, MODIFIED or REPLACED they would then transition to EXISTING

@danielcweeks is there a typo here? A REPLACED entry can't go to EXISTING. REPLACED is a tombstone and not a live entry.

dhruvarya-db added a commit to dhruvarya-db/iceberg that referenced this pull request Aug 7, 2026
Opening this to drive discussion on what the REST catalog spec needs for
format version 4. It is a proposal rather than a merge candidate: parts of it
depend on table spec changes that are still in review, and one change is
deliberately breaking. Details and open questions below.
Format version 4 restructures table metadata, and three constraints in the
current spec prevent a catalog from serving a v4 table at all: format-version
is capped at 3, Snapshot requires manifest-list, and CommitTableResponse
requires metadata-location.
Changes:
- TableMetadata: raise the format-version maximum to 4, and document that
location is optional in v4, where it may be supplied by the catalog rather
than carried in metadata.
- Snapshot: add root-manifest and make manifest-list optional, with a oneOf
requiring exactly one of the two. A snapshot with neither is invalid and must
not be read as a snapshot with no files. Which of the two applies is
determined by format-version, which cannot be expressed here because that
field lives in the enclosing TableMetadata, so it is stated normatively.
- LoadTableResult and CommitTableResponse: add table-location, which carries the
table's base location for resolving relative paths. Format version 4 makes
TableMetadata.location optional while allowing relative paths, so a table can
have metadata that is unreadable without a catalog-supplied base.
CommitTableResponse no longer requires metadata-location, and both responses
document that it may be absent when the catalog is the source of truth for
table state and no client-visible metadata pointer exists.
- UnregisterTableResult: add table-location, so the endpoint that hands back a
metadata location cannot return metadata with no base to resolve against.
- Move the table endpoints to /v2. Every path whose response can carry table
metadata moves: tables, tables/{table}, register, and tables/{table}/unregister.
Open questions:
- root-manifest is not in the ratified spec. It comes from the adaptive metadata
tree proposal (apache#16025), which is still open, so the field name and shape may
change. Note also that SnapshotParser currently writes manifest-list for v4
tables, so the oneOf as written would reject snapshots this repository
produces today. Whether to keep the field, defer it, or make it purely
additive is the main thing worth discussing.
- Moving the table endpoints to /v2 rather than adding /v2 alongside /v1 is a
breaking change, and assumes v1 is no longer supported. An additive version
that leaves /v1 in place is the obvious alternative if that assumption does
not hold.
- Whether path versioning is the right mechanism at all, versus a header or a
content-type parameter.
- Relative path support and the typed content stats are already in the spec for
v4; snapshot offloading is not yet, and would interact with the snapshots
query parameter on loadTable if it lands later.
make lint and make generate both pass.
Comment threadformat/spec.md
| 167 | **`key_metadata`** | `binary` | *optional* | *optional* | Implementation-specific key metadata for encryption. |
| 168 | **`split_offsets`** | `list<169: long>` | *optional* | *optional* | Split offsets for the column file. Must be sorted ascending. |

When a file is added to the dataset, its content entry must set status to ADDED (1) and store the snapshot ID in which the file was added.

@prakharjain09prakharjain09Aug 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a new ADDED leaf as part of commit X, which has:

5 ADDED DataEntry
5 EXISTING DataEntry
5 DELETED DataEntry

My understanding is the above is allowed.

On next commit (vx+1), this needs to become EXISTING.
But then the 5 DELETE DataEntry needs to be hide somehow in the vx+1 tree.

5 ADDS DataEntry
5 EXISTING DataEntry
5 DELETED DataEntry --------------- this should not be visible as they are not part of v`x+1` CDF

How will this work? Does the writer have to mark this leaf as EXISTING with MDV on last 5 rows??

Comment threadformat/spec.md Outdated

| Field id | Name | Type | Write | Read | Description |
|----------|------|------|-------|------|-------------|
| 0 | **`status`** | `int` (0: EXISTING, 1: ADDED, 2: DELETED, 3: REPLACED, 4: MODIFIED) | *required* | *required* | Used to track additions, deletions, replacements, and modifications. When a data file's `deletion_vector` or `column_files` change, REPLACED marks the prior version of the entry and MODIFIED marks the new, live version. For leaf manifest entries, MODIFIED marks a live manifest whose `dv` changed. Deletes are not used in scans. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhere we should explicitly mention that REPLACED applies only for data files. It is not applicable for leaf manifest entries?

Comment threadformat/spec.md Outdated
|----------|------|------|-------|------|-------------|
| 0 | **`status`** | `int` (0: EXISTING, 1: ADDED, 2: DELETED, 3: REPLACED, 4: MODIFIED) | *required* | *required* | Used to track additions, deletions, replacements, and modifications. When a data file's `deletion_vector` or `column_files` change, REPLACED marks the prior version of the entry and MODIFIED marks the new, live version. For leaf manifest entries, MODIFIED marks a live manifest whose `dv` changed. Deletes are not used in scans. |
| 1 | **`snapshot_id`** | `long` | *optional* | *optional* | Snapshot ID where the file was added or deleted. Inherited when null. Optional for leaf manifests, required for root. |
| 5 | **`dv_snapshot_id`** | `long` | *optional* | *optional* | Snapshot ID where the deletion vector was added. Inherited when null. Must be null when `deletion_vector` is null. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snapshot ID where the deletion vector was added. Inherited when null. Must be null when deletion_vector is null.

Can we explicitly talk about whether the line is for data file or a leaf manifest entry or both?

Comment threadformat/spec.md Outdated

Value 1 (POSITION_DELETES) is not used in v4. Writers must not produce `content_type` 1.

V4 leaf data manifests must only contain entries with `content_type` 0 (DATA); V4 leaf delete manifests must only contain entries with `content_type` 2 (EQUALITY DELETES). A root manifest may reference V1-V3 manifests; V1-V3 leaf manifest references must have `format_version` set to 0.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If would be great if we could have a formal definition for leaf manifests in this document. This is the first place that this concept is introduced and we drop the data / delete distinction in other places later on.

Comment threadformat/spec.md
Comment threadformat/spec.md Outdated
Comment threadformat/spec.md
| 0 | **`status`** | `int` (0: EXISTING, 1: ADDED, 2: DELETED, 3: REPLACED, 4: MODIFIED) | *required* | Used to track additions, deletions, replacements, and modifications. When a data file's `deletion_vector` or `column_files` change, REPLACED marks the prior version of the entry and MODIFIED marks the new, live version. For leaf manifest entries, MODIFIED marks a live manifest whose `dv` changed. Deletes are not used in scans. |
| 1 | **`snapshot_id`** | `long` | *optional* | Snapshot ID where the file was added or deleted. Inherited when null. Optional for leaf manifests, required for root. |
| 5 | **`dv_snapshot_id`** | `long` | *optional* | Snapshot ID where the deletion vector was added. Must be null when `deletion_vector` is null and `dv` is null. |
| 160 | **`latest_column_file_snapshot_id`** | `long` | *optional* | Snapshot ID where the latest column file was added. Inherited when null. Must be null when `column_files` is null. |

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is fine but for sequence number inheritance to work correclty for column updates, I think we'll also need a latest_column_file_sequence_number? cc @anuragmantri@gaborkaszab@anoopj@rdblue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we agreed to bump the base file's sequence number (the one right below) repurposing it for column files. With this we could also avoid applying the previous eq-deletes for that file (we rewrite them to DVs anyway when adding a column file). Does this make sense, @amogh-jahagirdar ?

I'm not entirely sure what we discussed for file_sequence_number. Should that be untouched when adding a column file opposed to (data) sequence number?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah yeah I realized I replied long ago on just reusing the existing data sequence number and the requirement to just rewrite eq. as DV. https://lists.apache.org/thread/gqcnoh5mrohg129dzl1qwflhfgxytow4 and I had forgotten my reasoning here :).

Ok though, when I was having offline conversations with others I was under the impression that people were thinking that there was another seq. number specific to last updated for columns. But let me confirm with folks (maybe just my misunderstanding) and get this wording tightened up.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaborkaszab Confirmed, what we discussed on the mailing list a few months ago is still the consensus. Was just my misinterpretation of a statement in an offline conversation, we can reuse data sequence number here. I do need to add the rule that it should be set to the snapshot sequence number when there's a column update (and the rule you mentioned about rewriting equality deletes as DV). CC @anoopj .

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the confirmation, @amogh-jahagirdar !
Note, I recall we said rewriting any non-dv delete including pos-deletes into a DV.

Comment threadformat/spec.md
* `tracking.deleted_positions`: the positions deleted in that snapshot.
* `tracking.replaced_positions`: the positions replaced in that snapshot.

`deleted_positions` and `replaced_positions` are disjoint.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure this needs to be said

@amogh-jahagirdar
amogh-jahagirdarforce-pushed the v4-amt-changes branch 2 times, most recently from f7657cb to 8a2c0e8CompareSeptember 4, 2026 04:49
Comment threadformat/spec.md Outdated
Comment threadformat/spec.md
@@ -83,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 associated with partitions.
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 files in its manifests. Manifest files are reused across snapshots to avoid rewriting metadata that is slow-changing. In v1-v3, data manifests and delete manifests are separate and can track files with any subset of a table; they are not associated with partitions. In v4, a data file and its deletion vector are stored in the same entry, so there is only a single manifest type, described below.

@rdbluerdblueSep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 files in its manifests. Manifest files are reused across snapshots to avoid rewriting metadata that is slow-changing. In v1-v3, data manifests and delete manifests are separate and can track files with any subset of a table; they are not associated with partitions. In v4, a data file and its deletion vector are stored in the same entry, so there is only a single manifest type, described below.
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 deletion vector (v4), and its metrics. The data in a snapshot is the union of all live rows in live data files tracked by the snapshot's manifests. Manifest files are reused across snapshots to avoid rewriting metadata that is slow-changing.
  • I think we should add the co-located DV to the first sentence, rather than at the end
  • We may also want to note that the data in the table is live rows in live data files?
  • I would remove the v1-v3 sentence since it is complicated to explain the differences here. This only needs to be high level. We also no longer have the confusion over a manifest-per-partition so we can remove it.

If we still want to note that there are delete manifests, maybe a separate paragraph that covers when they were used would make this easier to read. Something like this:

In v2 and v3, row-level deletes could be added using delete manifests tracking delete files. Existing delete files and delete manifests are valid in v4, but cannot be written and must be replaced by co-located deletion vectors.

Comment threadformat/spec.md
* **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.
* **Manifest list** -- (v1-v3 only) A file that lists manifest files; one per snapshot.
* **Root Manifest** -- (v4+) A manifest that can reference data files, delete files, and other data and delete manifests; one per snapshot. Replaces manifest lists in v4.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is where I'd introduce a Snapshot root or Root metadata file that we can use to refer to both a manifest list or a root manifest.

Comment threadformat/spec.md
* **Manifest list** -- (v1-v3 only) A file that lists manifest files; one per snapshot.
* **Root Manifest** -- (v4+) A manifest that can reference data files, delete files, and other data and delete manifests; one per snapshot. Replaces manifest lists in v4.
* **Data manifest** -- A file that lists data files; a subset of a snapshot.
* **Delete manifest** -- (v1-v3 only) A file that lists delete files; a subset of a snapshot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
***Delete manifest** -- (v1-v3 only) A file that lists delete files; a subset of a snapshot.
***Delete manifest** -- A file that lists delete files to be associated with data files at planning time.

No need to mention v1-v3 here because they are still valid in v4.

Comment threadformat/spec.md
@@ -546,7 +551,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 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. In v1-v3, manifests store data files from any partition, as long as the partition spec is the same for the data files. In v4, manifests can store data files from different partition specs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Manifests store data files from any partition" is true, so I'd like to keep the invariant.

Manifests store data files from any partition. v4 manifests may store partitions from any spec, but manifests in v3 and earlier store files for a single spec.

Also, why remove "a tuple of"? That is specifying that there are multiple values, not a single value. This is distinguishing Iceberg from Hive, where a "partition" was a string: "category=explosives/dt=2021-03-09".

Comment threadformat/spec.md
@@ -656,15 +661,22 @@ 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 manifest is an immutable file that lists data files or delete files, along with each file’s partition data, 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. In v1-v3, manifests are tracked by a [manifest list](#manifest-lists) for each table snapshot. In v4, a single root manifest per snapshot can directly reference data files, delete files, and other data and delete manifests.

@rdbluerdblueSep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the comment above, I'd phrase this first using what is common:

Manifests are tracked by a root metadata file for each table snapshot. In v4, the root metadata file is a root manifest that may track data files in addition to leaf manifest files.

Comment threadformat/spec.md

A manifest file must store the partition spec and other metadata as properties in the Avro file's key-value metadata:
- v1-v3: 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. The partition spec of each manifest is used to transform predicates on the table’s data rows into predicates on partition values during job planning.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- v1-v3: 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. The partition spec of each manifest is used to transform predicates on the table’s data rows into predicates on partition values during job planning.
- v1-v3: 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.

I would move the last sentence out to its own paragraph:

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. In v3, the same partition spec is used for all data files in a manifest.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Iceberg V4Iceberg Table Format Version 4SpecificationIssues that may introduce spec changes.

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

10 participants

@amogh-jahagirdar@rdblue@anoopj@RussellSpitzer@rambleraptor@stevenzwu@prakharjain09@danielcweeks@anuragmantri@gaborkaszab