Iceberg, Explained
metadata/
JSON 00004-5a81aac7-9cbf-4921-88f9-91bfe8643e88.metadata.json 3772 B
Avro snap-6837741135159966129-0-3208d62c-7a08-4314-baaf-827fa54bcc40.avro 1884 B
Avro 3208d62c-7a08-4314-baaf-827fa54bcc40-m0.avro 4665 B
Avro 3208d62c-7a08-4314-baaf-827fa54bcc40-m1.avro 4665 B
Avro 3ac6b924-605e-42e5-96bc-68baa90fc30d-m0.avro 4886 B
Avro 3208d62c-7a08-4314-baaf-827fa54bcc40-m2.avro 4666 B
Start with the artefact
A warehouse holds more than a table.
5 Parquet files remain in this history. Follow the latest metadata pointer to find which ones belong to its table.
What Iceberg is
Iceberg is an open table format: metadata that turns a collection of Parquet files into a table with versions. Parquet describes the contents of one file. Iceberg decides which files belong to a snapshot, including which replaced files an older reader still needs. A catalog points through metadata JSON, a manifest list and manifests to the data. Engines such as Spark, Trino and Flink read and write tables through those references.
About this fixture
Written by PyIceberg 0.10.0 / PyArrow 25.0.0; checked with DuckDB 1.4.4 + iceberg extension. JSON, Avro and Parquet originals are hash-checked and decoded in this browser. Native delete and lifecycle experiments retain their own version pins. Concurrency controls model commit decisions over recorded states.
Copy-on-write table Data snapshot
Verified fixture preview Every arrow has evidence.
The catalog identifies a metadata file. That JSON records snapshots; each snapshot names a manifest list. The Avro list names manifests, and those manifests describe the data files.
Catalog pointerSelected metadata location
↓ metadata location ↓ snapshots[] Initial write1295454848827360255operation: append
↓ manifest-list ↓ manifest_path ↓ data_file.file_path 0 original artifact bytes fetched in this session (cached per file). Loading a snapshot reads its complete tiny table to verify the demonstration; the pruning model below describes potential savings separately.
The table is a selection.
These are all 5 Parquet files retained in the bundled history. Only the files referenced by the selected snapshot contribute rows. The directory listing below is a comparison; it is never used to decide membership.
12 table rows come from 2 of 5 data files, containing 3,678 stored bytes.
Read the selected table · 12 rows
The source CSV has 24 synthetic observations; this story uses the first 18. Timestamps are Iceberg timestamps without a time zone. ISO serialization provides a consistent display for those synthetic timestamps.
Query the logical result.
These Parquet exports contain complete logical results from the recorded native reads, after file selection and applicable deletes. The workbench runs SQL on the selected export. The export already contains the rows selected by the native table reader.
Exports preserve the recorded schema names and values. Iceberg’s renamed export uses the current metadata after the rename; historical exports retain their original names. Native delete cases include the dependencies needed by their original readers even when your SQL projects only a value column.
Choose from 14 verified snapshot exports
- Query Initial write → 12 logical rows · 2,191 export bytes
Version, schema and evidence
Version / snapshot: 1295454848827360255
Published metadata: warehouse/weather/metadata/00001-2b5db7e2-8443-4c54-8ea0-7fccc861418f.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 62c5be0af71d79e3ea931abc8f2a810837321dd8a46daa8ce19fd8ab3f1fc7f5
Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591
- Query Append six → 18 logical rows · 2,288 export bytes
Version, schema and evidence
Version / snapshot: 142073692612853544
Published metadata: warehouse/weather/metadata/00002-8aec4d25-67cb-4464-9fe9-844ca8255b17.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 92666a4c7c7738d5bd54af02f6200da232e46650146b6488587d9da8c8e3c262
Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591
- Query Remove observation 3 → 17 logical rows · 2,278 export bytes
Version, schema and evidence
Version / snapshot: 6837741135159966129
Published metadata: warehouse/weather/metadata/00003-35b3c799-6435-4079-bc89-c4fd3e21b174.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 7dc459ecc729b43c1687c687e6f6deac01f5b26545e9fa717e0dfcfd966bff76
Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591
- Query Remove observation 3 · renamed schema → 17 logical rows · 2,286 export bytes
Version, schema and evidence
Version / snapshot: 6837741135159966129
Published metadata: warehouse/weather/metadata/00004-5a81aac7-9cbf-4921-88f9-91bfe8643e88.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · air_temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 25df145c385b87be65f4f3654745829d7db99abc2a27251ad63a3cb129ddcc79
Source proof SHA-256: 73ba462558afa542fd78f6b1df70f89d6889323800336558974087605ce68591
- Query position · Eighteen original observations → 18 logical rows · 2,289 export bytes
Version, schema and evidence
Version / snapshot: 4629318251272514798
Published metadata: tables/position/metadata/v2.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 09671b8223e3ebe9a8efa080dd9d760a284868e923d27dee3836357d1ae7f10d
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query position · Delete A at position 2 → 17 logical rows · 2,278 export bytes
Version, schema and evidence
Version / snapshot: 1551675423107445728
Published metadata: tables/position/metadata/v3.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 286a694491ff17688c32643b7f54e95138d1d62f7d55f94f84dc2b0b1a80e8c9
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query position · Append a new observation 3 → 18 logical rows · 2,289 export bytes
Version, schema and evidence
Version / snapshot: 4243774134377942860
Published metadata: tables/position/metadata/v4.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 9b0fea4ffb44e87b373ba023f3c77c2b8ffeba913d498bbc6dfc36ea91d5ea3d
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query equality · Eighteen original observations → 18 logical rows · 2,289 export bytes
Version, schema and evidence
Version / snapshot: 5833448318538228973
Published metadata: tables/equality/metadata/v2.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: e8521a3d92a7a6a90ad4cbd5054af028373cb2b42e39ee2a4072dc36da00c598
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query equality · Delete key 3 and insert its replacement together → 18 logical rows · 2,289 export bytes
Version, schema and evidence
Version / snapshot: 7654658913322873365
Published metadata: tables/equality/metadata/v3.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: e8db8268a64200225023c197c29b43ead39738e68fcfe0580ea698699d59d9bb
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query position-same · Eighteen original observations → 18 logical rows · 2,288 export bytes
Version, schema and evidence
Version / snapshot: 966253210586484786
Published metadata: tables/position-same/metadata/v2.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: c7edc5705e1c5586227e15fd24e356688ecbaaf58a1897812d105262e5b7591f
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query position-same · Insert D and delete its position 0 together → 18 logical rows · 2,289 export bytes
Version, schema and evidence
Version / snapshot: 3304934347579782469
Published metadata: tables/position-same/metadata/v3.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 543dd84fde919f36a1a104d051042d081e813b39b59880750a0846aaa43b61b3
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query partition · Eighteen original observations → 18 logical rows · 2,289 export bytes
Version, schema and evidence
Version / snapshot: 3019292698062751979
Published metadata: tables/partition/metadata/v2.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 6790685eb834844d4ecf320d11191eec1572bf11b5f1d7aabe24b8cc8c3ad668
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query partition · Delete key 3 scoped to South → 18 logical rows · 2,289 export bytes
Version, schema and evidence
Version / snapshot: 4855542995429446986
Published metadata: tables/partition/metadata/v3.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 5451f9ea337edee1d3715082109d233e0b5f3cab2a829521163675711f6fcd37
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
- Query partition · Delete key 3 scoped to North → 17 logical rows · 2,278 export bytes
Version, schema and evidence
Version / snapshot: 6414387081102762729
Published metadata: tables/partition/metadata/v4.metadata.json
observation_id: int64 · station: string · observed_at: timestamp[us] · temperature_c: double
Original native proof · Download derived Parquet result
Export SHA-256: 62bc853ba798602f5399c67879e67d289a0347325519f66c2bfea0eb4e9dae39
Source proof SHA-256: 49c3829cb4d30d5b1b1f763e81e0388c566d60d27c286348649389fd32110a93
Export manifest · Pinned export recipe. Source proofs are checked by the existing native Spark/DuckDB verifiers; PyArrow and DuckDB check exported records and narrow projections. Reproduction instructions and scope.
A manifest is a file inventory with meaning.
Each manifest entry carries a status: 0 existing, 1 added, 2 deleted. The reader keeps 0 and 1 and skips 2. In the final snapshot, File 3 has status 2. Its replacement preserves the surviving observations. A later snapshot can reuse a manifest, so “added” describes when a file entered that manifest.
Partition values come from the manifest’s partition struct. Bounds are typed values keyed by field ID: field 4 is the temperature, stored here as little-endian doubles.
A manifest entry is not automatically a live file.
Return to the original weather table and the snapshot selected at the top. A manifest entry records a file and its status: existing (0), added (1), or deleted (2). Existing and added entries can supply this snapshot’s rows; deleted entries describe removal and must not be counted as active data.
Added · 6 rows · live in the selected snapshot
Added · 6 rows · live in the selected snapshot
Counting live entries gives 12 rows. The verified snapshot has 12 rows.
Choose the delete snapshot to expose the difference. Click an entry to inspect its original manifest in the existing reference workbench. Read status within the selected snapshot’s manifests. Those references identify the inventory to inspect.
Sequence numbers have another job: they track ordering for changes and, in formats that support them, delete applicability. They are distinct from snapshot IDs, file paths, partition spec IDs and schema field IDs. This fixture uses copy-on-write, so no equality or position delete file is being silently applied.
New files join. Old files keep their identity.
The first write contains 12 rows across two days. Appending six observations adds 2 files and reuses all 2 original files. Observation 13 arrives late for January 1; the other five observations introduce January 3.
The new snapshot names its own manifest list. A reused manifest keeps its references to the same data files. Uploading new files alone would not change the table.
Remove one row. Rewrite its file.
Observation 3 shares a file with five other rows. Removing it writes a replacement containing those five survivors. The snapshot operation is overwrite, and the current table now has 17 rows.
The original six-row file still exists for time travel. A deleted manifest entry drops a file reference. A delete file instead records which rows to remove while retaining their data file. Iceberg v2 also supports position and equality delete files; the delete-files chapter follows their sequence and applicability rules through actual files.
Change the snapshot, change the table.
The selector above drives the file graph, inventory, rows and pruning together. Snapshot identifiers are exact 64-bit values; they are never rounded to JavaScript numbers.
An old snapshot remains readable while its required files are retained. Snapshot expiration and physical file cleanup are separate maintenance operations; this fixture keeps the complete history.
A field’s name can change. Its identity stays.
After the deletion, the writer renames temperature_c to air_temperature_c. Both names identify field 4. The data files are unchanged, and the current snapshot ID is unchanged.
Field ID 4→temperature_c
Checking the box selects the latest metadata context, which can still reference every retained snapshot. The row header changes; its values still come from the original physical column. Historical reads may use the snapshot’s original schema.
Separate metadata timeline
snapshot 1295454848827360255
snapshot 142073692612853544
snapshot 6837741135159966129
snapshot 6837741135159966129
Partition evolution is another metadata capability. The mixed-specification lesson in the pruning chapter uses a second real table to demonstrate it; this original fixture has one day partition specification.
Reusing an old name must not resurrect an old column.
Use the alternate layout’s weather observations to see how a rename preserves values. Rename temperature_c to air_temperature_c, move it to the first display position, and add a new nullable temperature_c field. No data file changes. The renamed field keeps ID 4, matching the main story; the newly added field receives ID 5.
Original physical columns
ID 1 · observation_id1
ID 2 · stationNorth
ID 3 · observed_at2026-01-01T00:00:00
ID 4 · temperature_c5
Current schema order
ID 4 · air_temperature_c5
ID 1 · observation_id1
ID 2 · stationNorth
ID 3 · observed_at2026-01-01T00:00:00
ID 5 · temperature_cNULL: no field 5 in old file
Tracking ID 3. It is absent from the final seventeen-row table; the schema demonstration currently shows ID 1.
Renaming and reordering preserve values. The newly added nullable field reads as null in historical files.
Names are for people and queries; field IDs preserve identity across compatible evolution. Dropping a field and later adding the same name must allocate a new identity. Defaults and required-field rules complicate other cases; this native fixture specifically adds an optional field with no non-null default.
Inspect the actual evolved schema
{
"type": "struct",
"fields": [
{
"id": 4,
"name": "air_temperature_c",
"type": "double",
"required": false
},
{
"id": 1,
"name": "observation_id",
"type": "long",
"required": false
},
{
"id": 2,
"name": "station",
"type": "string",
"required": false
},
{
"id": 3,
"name": "observed_at",
"type": "timestamp",
"required": false
},
{
"id": 5,
"name": "temperature_c",
"type": "double",
"required": false
}
],
"schema-id": 3,
"identifier-field-ids": []
} Rule out files before opening them.
First, the manifest list’s day bounds can rule out manifests. Inside those retained manifests, the day partition and upper temperature bound can prove a data file has no matches. A surviving file remains a candidate until its rows are evaluated. Missing bounds keep the file.
1 / 1 candidate manifests after the day filter.
The filter retains 2 of 2 files. Decoded rows yield 12 matches; excluded files contain 0 bytes.
File 3Candidate2026-01-01 · 5–10 °C
File 4Candidate2026-01-02 · 11–16 °C
Matching observation IDs: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.
The first pass checks manifest-list partition summaries. The second checks each file’s partition and temperature bounds. Decoded rows decide which candidates actually match.
Change physical organization, preserve the answer
The same seventeen rows, two partition specifications.
Keep the final weather records and their field IDs. This native experiment changes when the writer switches from day to month; every source value remains the same.
Partition evolution changes new writes, not old files.
Keep the same seventeen observations from the delete snapshot. Change only how their files are partitioned: write the eleven surviving observations from the first two days under the day specification, then write the six appended observations, IDs 13–18, under a month specification. ID 13 is a late observation from January 1; IDs 14–18 belong to January 3. This is a separate native layout of the same logical answer, with observation 3 still absent. Both specifications remain in metadata. Each manifest identifies the specification needed to interpret its files.
Timestamp predicate
observed_at ≥ 2026-01-02T12:00:00 AND observed_at < 2026-01-03T00:00:00
Spec 0: day(observed_at)
File A · prunedPartition value 20454
Physical IDs: 1, 2, 4, 5, 6
Original timestamps
2026-01-01T00:00:00
2026-01-01T03:00:00
2026-01-01T09:00:00
2026-01-01T12:00:00
2026-01-01T15:00:00
File B · candidatePartition value 20455
Physical IDs: 7, 8, 9, 10, 11, 12
Original timestamps
2026-01-02T00:00:00
2026-01-02T03:00:00
2026-01-02T06:00:00
2026-01-02T09:00:00
2026-01-02T12:00:00
2026-01-02T15:00:00
Spec 1: month(observed_at)
File C · candidatePartition value 672
Physical IDs: 13, 14, 15, 16, 17, 18
Original timestamps
2026-01-01T21:00:00
2026-01-03T03:00:00
2026-01-03T06:00:00
2026-01-03T09:00:00
2026-01-03T12:00:00
2026-01-03T15:00:00
Day partition values count days since 1970-01-01; month values count months since January 1970. The integer 672 means January 2026 under a month transform. Interpreting it as a day would route the file to a different date. The spec ID supplies the missing meaning.
The residual is the part still owed by the reader.
File BGreaterThanOrEqual(term=Reference(name='observed_at'), literal=literal('2026-01-02T12:00:00'))Read row values to evaluate the remaining timestamp condition.
File CAnd(left=GreaterThanOrEqual(term=Reference(name='observed_at'), literal=literal('2026-01-02T12:00:00')), right=LessThan(term=Reference(name='observed_at'), literal=literal('2026-01-03T00:00:00')))Read row values to evaluate the remaining timestamp condition.
Native result IDs: 11, 12. Planned files: B, C.
Predict the change from a full day to noon onward
File B contains January 2 observations 7–12, from midnight to 15:00. A full-day predicate needs no timestamp residual there. Starting at noon keeps the file but only IDs 11 and 12 pass. File C contains late January 1 observation 13 and January 3 observations 14–18. It has no January 2 rows at all, yet its month partition and broad timestamp interval retain it. The residual rejects every row for a January 2 query.
Hidden partitioning lets queries use the source timestamp. Planning translates that condition through each file’s partition transform. Previous files retain their own partition specification when the current spec changes. PyIceberg’s actual task residuals are shown above; DuckDB independently verifies each final result.
Keep the data file. Change which rows can be read.
Two kinds of delete file, two different identities.
The copy-on-write example replaced a data file. These four independent Iceberg v2 tables start again with the same eighteen weather observations. They commit actual Parquet delete files through Apache Iceberg’s Java API. Spark and DuckDB read every resulting snapshot. Each branch has its own sequence numbers; these are alternate histories, not continuations of the seventeen-row table above.
A position delete names a data-file path and a zero-based row position. An equality delete names values of fields identified by equality_ids. Both remain separate from the data bytes. An entry with status DELETED removes a file from membership; a live entry with content 1 or 2 adds a file that removes rows.
Delete experiment
Snapshot 1551675423107445728 · sequence 2 · 17 visible / 18 physical rows
In these added entries, a null sequence inherits the manifest’s sequence. The data sequence controls delete applicability. The file sequence records when the file was added. They happen to match here; a rewrite can preserve data age while changing file age.
Follow observation 3 through the read
Observation to trace
For each physical occurrence, first check partition scope and sequence, then the delete’s row identity. Removal requires every applicability check to pass, including the key match. These branches each use a single partition specification; the partition experiment uses identity(station).
P: partition passes; sequence 1 ≤ 2 passes; path + position matches. Remove this occurrence.
A contains observation 3 at ordinal 2, data sequence 1. Its stored values remain readable in the physical file, but do not contribute a table row. Position: data sequence ≤ delete sequence
It can remove a row from a file added in the same commit. In the third branch, D and its position delete both have sequence 2, so D:0 is masked. The original ID 3 in A survives: the path is different.
Equality: data sequence < delete sequence
It applies to older data. In the second branch, E at sequence 2 removes ID 3 from A at sequence 1. D at sequence 2 contains the same ID, now 30°C, and survives. An update can therefore delete the old key and add its replacement in one commit.
The fourth branch shows why partition scope comes first. Key 3 belongs to North. The South-scoped equality delete leaves it visible: partition scope fails despite the matching sequence and key. Adding the North delete removes it. An equality delete written with an unpartitioned specification has global scope; the next native experiment executes that cross-spec case.
Trace the reference into the representation
Open the delete, down to its value bytes.
Use the currently selected branch and snapshot. Read the original metadata JSON, follow its manifest list, open the delete manifest, then decode the referenced Parquet file. Every read is checked against its captured SHA-256. The full local paths inside the original v2 files identify the captured table; the download links below serve those exact bytes.
→→→
Choose an artifact to inspect.
Position deletes use reserved field IDs 2147483546 (file_path) and 2147483545 (pos). Our equality delete stores field ID 1, observation_id. A renamed column keeps its ID; a newly added column with the old name gets a different ID. Matching only names would apply the wrong key.
The byte view is deliberately specific: these one-row delete files use uncompressed Parquet V1 PLAIN encoding. The reader skips the optional column’s definition-level stream before showing the integer. Other encodings require their own decoding steps.
Continue into delete boundaries and Puffin vectors →
The pointer update is the commit.
Conceptual simulation: two writers start from the same metadata. Their files can coexist in storage; only a successful catalog commit publishes a new table state.
Step 1 / 5Both writers read metadata M0.
Catalog pointer→M0
A catalog provides atomic publication and conflict detection. Before retrying, the writer validates its operation against the newly committed state.
Refreshing the pointer is not enough.
Use the real delete operation as writer A’s change: replace File 3 with File 1. Writer
B either rewrites that same old file or appends an independent file D. D and the competing
writers are hypothetical; the file replacement comes from the verified history.
Published catalog root
M0
Reader R pinned M0 before either writer committed. R stays on M0’s snapshot and reads File 3 through the end of its scan. The replacement belongs to the newly published snapshot.
B's validation assumptions
File 3 must still be live because B is replacing the rows it read from that file.
Expected catalog root: M0. Staged files are not published by their
existence.
Both writers prepare against M0.
Blindly installing B’s original M2 would lose A’s committed change. A successful retry rebuilds
metadata from the refreshed state and checks the operation’s assumptions. A passing validation
is still separate from winning the next atomic publication attempt.
The model follows publication and file-rewrite validation. The native schedules below show the corresponding decisions in executed transactions. Iceberg’s concurrency and retry model.
Removing a snapshot reference does not make every old file garbage.
Snapshots share unchanged data files. In this reachability model, retain or expire historical snapshots and optionally pin the first snapshot with a branch. The current snapshot always remains. A file stays reachable if any retained reference needs it.
The catalog’s atomic metadata-pointer update is the publication boundary. Data and metadata files can be staged beforehand, but readers resolve one committed root and follow its references. A competing writer validates against the current root, then atomically publishes its new metadata pointer.
Download the mixed-spec and field-identity evidence · Iceberg specification
Run the writers, inspect the outcome
A stale snapshot can be safe for one operation and unsafe for another.
These are actual Apache Iceberg Java operations over local Hadoop tables. Two independent handles read the same eighteen-row snapshot before either commits. Writer B prepares its operation; A commits first; then B attempts to commit. Snapshot lifetimes overlap in a controlled schedule. The local Hadoop tables publish metadata through the filesystem.
Native Iceberg writer schedule
1. Both writers read the same snapshot18 rowsRead by A and B 3706799091859689728
2. Writer A commits17 rowsNative snapshot 6423870635082758303
3. B rejects; A remains committed17 rowsNo new snapshot 6423870635082758303
4. B recomputes from current rows16 rowsNative snapshot 5809130760053370453
Both writers read the same snapshot. The prepared file is private until its metadata commit succeeds.
Native scan tasks: data files and attached deletes| Data file | Spec | Physical records | Deletes the reader must apply |
|---|
| A.parquet | 0 | 18 | None |
18 native result rows · IDs 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18
Snapshot 3706799091859689728 · sequence 1 · Original metadata JSON
Inspect the native manifest entries and complete result
B originally read snapshot 3706799091859689728. Its staged file contains IDs 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18. It still contains ID 3, which A removed. Publishing that stale replacement would resurrect 3. Recomputing from A’s seventeen rows and removing 4 produces sixteen rows.
The rewrite declares the snapshot from which it read and validates data/delete conflicts. Compatible appends can rebase metadata on the newer state. The recorded schedules expose both rejected changes and successful rebasing against a newer snapshot. The abstract control above remains useful for exploring another publication race.
Observe actual removal in an isolated table
Expiration and orphan cleanup remove different things.
Main replaces A after deleting ID 3, but an audit branch still points to the original snapshot. A separate Parquet file was written and never committed. Predict whether expiring history should remove that unpublished file.
Cleanup prediction
Original snapshot
Retained references
main
3456057876572685767
Retained snapshots
3456057876572685767
Parquet files physically present
A.parquet
Native scan tasks: data files and attached deletes| Data file | Spec | Physical records | Deletes the reader must apply |
|---|
| A.parquet | 0 | 18 | None |
18 native result rows · IDs 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18
Snapshot 3456057876572685767 · sequence 1 · Original metadata JSON
Inspect the native manifest entries and complete result
The generator ran these destructive maintenance operations only on fresh temporary tables and checked existence before and after. The download preserves copies of artifacts removed during cleanup, so you can inspect what the operation deleted. In a real table, choose retention long enough for active readers and in-progress writers before running cleanup.
Reproduce every native boundary
Download lifecycle tables, source and pinned native recipe. The verifier checks original artifact hashes and regenerates every schedule and maintenance operation in a new temporary directory, comparing complete rows, rejection types and scan tasks. It requires Java 17 and uv. The v2 deletion-file archive above separately reopens relocated copies of its original tables.
The reproduction bundle preserves the local publication schedule and the original vector bytes.
Iceberg selects it. Parquet stores it.
Open an exact bundled file in the existing explorer. The return link preserves this snapshot selection, including when you inspect a historical file.
Take the evidence with you.
Written with PyIceberg 0.10.0 / PyArrow 25.0.0. Every snapshot’s exact rows and values were independently checked with DuckDB 1.4.4 + iceberg extension (extension 1095c1fa). The downloadable bundle contains the original artifacts, source CSV and a hash index.
Read the Apache Iceberg specification, schema evolution guide and reliability model.
Your turn.
Choose a metadata JSON from your own table. Follow its snapshot and manifest references, then compare the files it selects.