Desktop app · IntelliJ tool window · CLI · Kotlin · Apache-2.0
The table, the way the engine walks it.
Point it at a table directory and it draws the structure every query traverses — metadata files, snapshots, manifest lists, manifests, data and delete files, the rows inside — as a graph you click through, beside an inspector with every field the format records. Nothing is written back: it answers questions about a table, it never changes one.

Table-format figures are easy to state and easy to get wrong, because one manifest is carried forward by every snapshot that keeps its files. Every figure the metadata records is shown beside the same figure counted from the bytes — and the two sets, current snapshot and all retained history, are always labelled.
The whole tree, as a graph
Metadata files, snapshots, manifest lists, manifests, data and delete files, sample rows. Branches in their own columns; long sibling runs folded into pages that say what they hide. Four layouts, a snapshot filter, find, arrow keys, export as SVG, PNG or JSON.
Checked against the bytes
One click compares every recorded figure with the same figure counted — ids and lengths, manifest counts, commit summaries, snapshot totals, each file's partition against its own bounds — over the whole table. icelens check exits 1 on a disagreement.
What a scan would skip
A filter — WHERE clause or form — evaluated against partition summaries and column bounds, with the term that proved each skip. What a commit did, what differs between two snapshots, which snapshot a read as of a time lands on.
Rows and their fate
The rows a filter matches, read from the files it leaves, each marked live or deleted — by which vector, positional or equality delete on Iceberg, by which vector on Delta, by which retraction or later write on Paimon — and traced through the retained snapshots.
Maintenance, planned the engine's way
What expire_snapshots would remove and what keeps the rest; what rewrite_data_files would rewrite; a Paimon bucket as its LSM tree and what the next compaction picks; what Delta's VACUUM and OPTIMIZE would take — planned as the engines' own planners decide it.
Read-only, local, offline
No catalog, no service: a table is opened by its location — a folder, s3://, gs:// or r2:// with a session-only key, or HDFS over WebHDFS as a named user. The remote filesystem refuses writes by type.
What it answers, level by level
The same tree the formats define, and the question each level answers.

- Table
Which format, which version, what the current snapshot holds and what history still keeps on disk; the maintenance summary and both directions of the orphan question.
- Snapshots
What each commit did, folded from the manifests it wrote and checked against its own summary; rollbacks marked on the log; refs, tags, branches and consumers with their retention.
- Manifests
Partition summaries decoded against each manifest's own spec, live and deleted entries, and the snapshots that still carry a manifest forward.
- Files and rows
Recorded bounds, counts, size and split offsets beside the file itself; delete files and deletion vectors paired with the rows they reach; sample rows with their fate.
Download
Installers for macOS (.dmg), Windows (.msi) and Linux (.deb) on GitHub Releases, with their own runtime. From source you need a JDK 17 or newer.
Installers
# macOS IcebergLens.dmg
# Windows IcebergLens.msi
# Linux iceberglens.deb
# the command line ships beside the app:
ln -s /Applications/IcebergLens.app/Contents/MacOS/icelens /usr/local/bin/icelensEvery build is on GitHub Releases; the IntelliJ plugin puts the same view in a tool window.
From source
git clone https://github.com/mmdemirbas/ice-lens.git && cd ice-lens
./gradlew run # the desktop app
./gradlew :cli:installDist # cli/build/install/icelens/bin/icelens
./gradlew testThen Add to Workspace: a warehouse folder or a single table folder — metadata/ for Iceberg, snapshot/ + schema/ for Paimon, _delta_log/ for Delta.
What the numbers mean
Two sets of figures, side by side, each labelled — because a manifest is referenced by every snapshot that carries its files forward.
| Group | Answers |
|---|---|
| Current snapshot | What the table holds now — the manifest closure of current-snapshot-id, live entries only. Its record count is what a query returns. |
| All retained history | What is still on disk — every manifest and data file reachable from any retained snapshot, deduplicated. This is what expiry and orphan cleanup reason about. |
The same engine from a terminal
For a script, a cron job or a CI step. --json prints the same as an object; the answer goes to stdout, anything the engine logs to stderr.
icelens summary /wh/db/orders # versions, snapshots, the current figures
icelens tree /wh/db/orders --depth 2 # metadata → snapshots → manifests → files
icelens show /wh/db/orders snap_8331894 --json # one node's rows, deferred ones read
icelens check /wh/db/orders # every recorded figure against the same figure counted;
# exit 1 on a disagreement, so a pipeline can gate on it
icelens check /wh/db/orders --files # and every live data file and statistics file opened too
icelens lookup /wh/db/orders "id = 42" # the rows a filter matches, each with its fate
icelens plan /wh/db/orders # what each maintenance procedure would do if run now
icelens plan /wh/db/orders expire_snapshots # one of them in full: every snapshot and file it removes
icelens export /wh/db/orders --format csv --out files.csv
icelens summary webhdfs://namenode:9870/wh/db/orders --hdfs-user etl
# a table on HDFS, read over WebHDFS as a named user
Format coverage
| Iceberg | Paimon | Delta Lake | |
|---|---|---|---|
| Detection | metadata/ with *.metadata.json | snapshot/ + schema/ | _delta_log/ with a commit or a checkpoint |
| Metadata | metadata.json v1–v3, snapshot log with rollbacks marked, refs with retention, partition specs, sort orders, statistics files, row lineage | snapshot-N, schema-N, tags, branches, consumers, the index manifest, ANALYZE statistics | Commits and checkpoints — classic, multi-part, V2 with sidecars — and _last_checkpoint; column mapping, row tracking, in-commit timestamps, UniForm's Iceberg metadata |
| Manifests | Manifest list → manifests, data and delete split, partition summaries decoded against each manifest's own spec | Base, delta and changelog manifest lists, replayed delta-over-base; the changelog per key as a consumer received it | None: each version is replayed from the newest checkpoint by the protocol's rules; the change data feed per version, as table_changes returns it |
| Files | Data, positional-delete and equality-delete files, v3 deletion vectors decoded from Puffin, inherited sequence numbers and row ids | Data files with LSM level and bucket, file indexes decoded, external paths, data-evolution patch files, deletion vectors from the index file | Add, remove and change-data files with statistics and partition values; deletion vectors decoded — relative, absolute or inline |
| Rows | Parquet and Avro via DuckDB, 50 per file, deleted rows marked, read by field id with v3 defaults and the name mapping | Same, with _ROW_ID, the +I/-U/+U/-D kind of each row, and the projection by the file's own schema id | Parquet via DuckDB, partition values from the log, _row_id and _row_commit_version under row tracking, vector-marked rows struck |
Limitations
Stated plainly, because a tool you inspect internals with has to be honest about its own.
- No catalog integration. Hive, Glue, REST, Nessie and Polaris are not spoken to; a table is opened by its location. S3, GCS and R2 are read through DuckDB with a key that is never persisted; HDFS over WebHDFS under simple authentication, and a Kerberos-secured cluster not at all; ADLS is not read.
- Iceberg v3 is modelled up to what the fixture engines write. Deletion vectors, row lineage,
added-rows, column defaults andvariantare read from real tables; geometry, geography andtimestamp_nsparse without error but have no fixture yet. - Delta Lake is modelled against delta-spark 3.2.1 — its commit metrics, its maintenance rules and its checkpoint namings are the ones checked against real tables.
- A v2 positional delete file is not mapped to row cards — its targets are known only after reading the file, so the rows it removes are counted behind a click; a v3 deletion vector marks the cards.
- What an equality delete removes is known only by reading. The metadata rules a data file out, never in; the count opens every data file it may reach, behind a click.
- Sample rows are best-effort: capped at 50 per file, five drawn per data file, and slow on tables with many data files when Show Rows is on.