Storage Maintenance¶
The maintenance package wraps PyIceberg’s lower-level primitives in four operations. They are basic helpers for self-managed tables on the local filesystem; delete files and v3 deletion vectors are out of scope. Catalog services that run table maintenance themselves (for example AWS S3 Tables) do not need them. These APIs may change before 1.0.
The four helpers¶
iceberg_ops.maintenance.compact_small_files_streaming()– per-partition compaction. Reads each qualifying partition viaTable.scan().to_arrow_batch_reader, writes pre-staged Parquet files of roughlytarget_size_mb, then commits one PyIceberg transaction (overwrite per touched partition +add_files). Identity-transformed partitions (and unpartitioned tables) only; other partitions are skipped with a warning. Peak memory is not bounded bytarget_size_mb: PyIceberg’s batch reader decodes whole source files (apache/iceberg-python#2407, apache/iceberg-python#4028), so memory grows with the number and size of files per partition.iceberg_ops.maintenance.expire_snapshots()– max-age + min-count snapshot expiry via PyIceberg’sTable.maintenance.expire_snapshots(). All arguments aftertableare keyword-only. Branch and tag heads (including the current snapshot) are never expired; those old enough to expire are reported inprotected_ids. Unset arguments fall back to the table’shistory.expire.max-snapshot-age-msandhistory.expire.min-snapshots-to-keepproperties (as written byMigrationContext.set_snapshot_retention), then to the Iceberg defaults of 5 days and 1 snapshot.min_snapshotsbelow 1 or a negative age raiseValueError. Expiry is metadata-only: data files of expired snapshots stay on storage until orphan cleanup reclaims them (file://only).iceberg_ops.maintenance.remove_orphan_files()– recursiveos.walkover a configurable sweep list (defaults todata/+metadata/). Defaults todry_run=True: it reports and deletes nothing unless you passdry_run=False. A file is kept if any table metadata references it: manifest lists, manifests, every live manifest entry of every snapshot (data files, position- and equality-delete files, deletion-vector Puffin files), table and partition statistics files, and every metadata JSON. Matching is by basename and is conservative (a file is kept if any referenced path shares its name). Names are compared case-insensitively after Unicode NFC normalisation, because Windows and default macOS filesystems resolve them that way. The walk does not follow directory symlinks or Windows junctions, and a file whose real path lies outside the table location is never deleted. Only files with known Iceberg suffixes (.parquet/.avro/.metadata.json/.puffin/.stats) whose mtime predates the cutoff are candidates. Tables withgc.enabled=falseare refused withValueError(in dry-run too), a negativeolder_than_hoursis rejected, sweep entries must be relative and stay inside the table location, and deletions that fail are reported infailed_paths. Never run it against a location shared with another table.iceberg_ops.maintenance.table_health_report()– read-only diagnostic. File sizes are aggregated withpyarrow.compute, but the input comes frominspect.files(), which builds one Python dict per file, so cost grows with the file count. Returns aTableHealthReportwith file counts and sizes, snapshot count, oldest-snapshot age, small-file count, and a list of human-readable recommendations.
The examples below each build a small local table first, so they run as written; with your own table, skip that part.
Compaction example¶
import pyarrow as pa
from iceberg_ops.core.catalog import CatalogFactory
from iceberg_ops.maintenance import compact_small_files_streaming
# A local table with six small data files (one per append).
catalog = CatalogFactory.build("memory")
catalog.create_namespace("sales")
table = catalog.create_table("sales.orders", pa.schema([("order_id", pa.int64())]))
for i in range(6):
table.append(pa.table({"order_id": [i]}))
result = compact_small_files_streaming(
table,
target_size_mb=512,
min_file_count=5,
sort_by_iceberg_order=True,
dry_run=False,
)
print(
f"compacted {result.files_rewritten} files; "
f"{result.bytes_before:,} -> {result.bytes_after:,} bytes"
)
If the commit fails with CommitFailedException or
ValidationException, nothing was committed and the staged files are
removed. Any other commit error (for example
CommitStateUnknownException) means the commit may have landed, so
the staged files are kept and the error is re-raised: refresh the table
and check. If the commit did not land, remove_orphan_files reclaims
the staged files once they are older than older_than_hours.
Snapshot expiry example¶
import pyarrow as pa
from iceberg_ops.core.catalog import CatalogFactory
from iceberg_ops.maintenance import expire_snapshots
# A local table with three snapshots.
catalog = CatalogFactory.build("memory")
catalog.create_namespace("sales")
table = catalog.create_table("sales.orders", pa.schema([("order_id", pa.int64())]))
for i in range(3):
table.append(pa.table({"order_id": [i]}))
result = expire_snapshots(
table,
max_age_hours=120,
min_snapshots=2,
dry_run=False,
)
print(f"expired {result.expired_count}/{result.total_snapshots} snapshots")
print(f"kept because a branch or tag points at them: {result.protected_ids}")
Omit max_age_hours / min_snapshots to use the table’s
history.expire.* properties. max_age_hours accepts fractional
hours.
Orphan cleanup example¶
import pyarrow as pa
from iceberg_ops.core.catalog import CatalogFactory
from iceberg_ops.maintenance import remove_orphan_files
# A local table to sweep.
catalog = CatalogFactory.build("memory")
catalog.create_namespace("sales")
table = catalog.create_table("sales.orders", pa.schema([("order_id", pa.int64())]))
table.append(pa.table({"order_id": [1, 2, 3]}))
# Report first (the default is dry_run=True) ...
preview = remove_orphan_files(table, older_than_hours=72)
print(f"would delete {preview.orphan_count} orphan files: {preview.orphan_paths[:10]}")
# ... then delete explicitly.
result = remove_orphan_files(
table,
older_than_hours=72,
sweep=("data", "metadata"),
dry_run=False,
)
print(f"found {result.orphan_count} orphan files; failed: {result.failed_paths}")
older_than_hours below 24 logs a warning: files written by an
in-flight writer that has not committed yet look like orphans.
Health report example¶
import pyarrow as pa
from iceberg_ops.core.catalog import CatalogFactory
from iceberg_ops.maintenance import table_health_report
# A local table with a few small files.
catalog = CatalogFactory.build("memory")
catalog.create_namespace("sales")
table = catalog.create_table("sales.orders", pa.schema([("order_id", pa.int64())]))
for i in range(3):
table.append(pa.table({"order_id": [i]}))
report = table_health_report(table, small_file_threshold_mb=100)
print(f"snapshots: {report.snapshot_count}")
print(f"small files: {report.small_file_count}/{report.total_files}")
for rec in report.recommendations:
print(f" - {rec}")
Known limitations¶
A compaction run writes
2N + 1snapshots forNtouched partitions (a delete and an empty append per partition, then one append). Incremental readers see the rewritten rows as new data.Qualifying partitions are rewritten whole, including files that are already large.
Compacted files use PyArrow’s default codec (Snappy), not the table’s configured
write.parquet.compression-codec.A concurrent append to a partition being compacted fails the run with
ValidationExceptionand the staged files are removed. PyIceberg retries the commit after appends to other partitions; those appends are preserved and counted infiles_after.Compaction supports Identity-transformed partitions (and unpartitioned tables) only; other partitions are skipped.
Snapshot expiry is metadata-only. It also commits without asserting the table’s refs (a PyIceberg limitation), so do not create tags while an expiry runs.
Delete files and v3 deletion vectors are out of scope. Compaction and the health report read
inspect.files(), which lists delete files next to data files, and neither checks for them: do not run compaction on a table that has position or equality delete files or deletion vectors. Orphan cleanup keeps every delete file the metadata references.
Filesystem support matrix¶
Helper |
|
|
|
|---|---|---|---|
|
yes |
not tested |
not tested |
|
yes |
not tested (metadata only) |
not tested (metadata only) |
|
yes |
no |
no |
|
yes |
no |
no |
The metadata-only helpers (table_health_report and
expire_snapshots) only go through PyIceberg’s metadata APIs, never
listing or deleting objects themselves, so they are expected to work
wherever PyIceberg can read the table; the test suite runs them on
local tables only. The two helpers that do touch storage raise
NotImplementedError with the scheme named in the message when the
table location is not local. Plain paths, including Windows
drive-letter paths such as a .pyiceberg.yaml warehouse of
C:\warehouse, count as local.
See Maintenance for the full reference.