Architecture¶
Where iceberg-ops fits¶
Many tools can change or maintain Apache Iceberg tables without a JVM. They take different approaches (as of November 2026):
Declarative tools, such as the Apache Iceberg Terraform provider and desired-state planners for Iceberg metadata, describe where a table should end up and compute the change for you.
Engines such as Spark, Trino and Flink run
ALTER TABLEDDL, but they do not version or track it.iceberg-ops follows the model of Alembic for relational databases. Each change is a numbered migration file with an
upgrade()and adowngrade(). The runner records which changes were applied, in what order and in which namespace, in an Iceberg history table inside your catalog. Autogenerate writes the downgrade where a step can be undone and makes it raise where it cannot.
The maintenance helpers and the cross-table data mover ship in the same package but are secondary to the migration workflow.
Capability surface¶
Module |
Responsibility |
|---|---|
|
|
|
Migration runner: discover, apply, roll back, stamp, plan, status and history for one namespace at a time. |
|
Iceberg-backed migration history table inside an
|
|
Schema, partition-spec, sort-order and property diffs turned into migration ops, and a renderer that writes a migration file for review. |
|
|
|
Click-based |
|
Catalog factory, schema diff / contract / evolution helpers, Arrow <-> Iceberg type translation, snapshot / upsert / time- travel utilities. |
|
Cross-table streaming migrator with schema reconciliation, checkpoint-based resume, and three verify methods (experimental). |
|
Basic maintenance helpers for self-managed tables:
|
|
Apache Polaris policy client and dispatcher (experimental; snapshot expiry only). |
|
Public |
Catalog backends¶
Every API that takes a catalog accepts any PyIceberg Catalog.
CatalogFactory.build is a convenience for six kinds:
Kind |
Install |
Notes |
|---|---|---|
|
|
Local file-backed catalog for development and CI. Without
|
|
|
In-memory SQLite ( |
|
|
AWS Glue Data Catalog. |
|
|
REST catalogs such as Apache Polaris, Nessie and Lakekeeper. PyIceberg’s REST client is part of the core install. |
|
|
Hive Metastore. |
|
|
AWS S3 Tables (REST + SigV4, pre-set); pass |
The remote kinds (glue, rest, hive, s3tables) are
loaded through pyiceberg.catalog.load_catalog() under a catalog
name that defaults to the kind, so .pyiceberg.yaml and
PYICEBERG_CATALOG__<NAME>__* environment variables apply; explicit
keyword arguments win. From the CLI, --catalog-name NAME opens any
catalog PyIceberg can load. Only the SQLite and in-memory kinds are
integration-tested; see Compatibility and limits.
Backends are lazy-imported: every backend lives behind a
_lazy_import helper so import iceberg_ops succeeds even when none
of the optional extras are installed. Calling CatalogFactory.build
for a backend whose extra is missing raises an ImportError that
names the pip install 'iceberg-ops[...]' line to run.
Self-managed metadata namespace¶
The runner stores migration history as a real Iceberg table inside the
user’s catalog under a meta-namespace (default _iceberg_ops):
_iceberg_ops.migration_history
version, namespace, description, applied_at, status, error
The table holds one row per (version, namespace) with the latest
status (applied, rolled_back or failed). Earlier states are
readable by time travel on the history table until its snapshots
expire. The meta-namespace and the table are created by the first
write; plan, status and history do not create them.
Keeping history in the catalog means it moves with the data and needs
no separate database to back up. The Python API can use another
meta-namespace through MigrationRunner(meta_namespace=...); the CLI
always uses _iceberg_ops.
Concurrent runs against one namespace are not guarded: run one
migration job per namespace. If two migrate runs overlap, both can
execute upgrade(); the second one to record history raises
ConcurrentMigrationError and the history keeps one row per version
and namespace. See Compatibility and limits.
Namespace-scoped runner¶
from iceberg_ops.core.catalog import CatalogFactory
from iceberg_ops.runner import MigrationRunner
catalog = CatalogFactory.build("sqlite", db_path="catalog.db", warehouse_dir="warehouse")
sales_runner = MigrationRunner(catalog, namespace="sales")
finance_runner = MigrationRunner(catalog, namespace="finance")
Each namespace has its own history rows (one shared history table keyed
by namespace) and its own pending / applied view. Migration files are
namespace-agnostic: use bare table names so the same files apply
against sales and finance without edits. Files written by
revision --autogenerate keep a bare --target-table bare, so they
follow the same rule. When migrations/<namespace>/ exists, the
runner for that namespace uses it instead of the flat layout.
Local-filesystem-only operations¶
Two of the maintenance helpers (compact_small_files_streaming and
remove_orphan_files) support local file:// table locations
only. Tables on s3:// / gs:// / abfs:// raise
NotImplementedError with the scheme named in the error message.
expire_snapshots and table_health_report only touch metadata, so
they are expected to work on any filesystem PyIceberg supports, but they
are tested on local tables only. The maintenance helpers get bug fixes
only; the roadmap is to hand these jobs to PyIceberg’s own maintenance
APIs as they land.