Skip to content

Iceberg

Portolan publishes cloud-native files under STAC. The optional [iceberg] extra adds an Apache Iceberg lakehouse, for teams that already run an Iceberg catalog server and want Portolan to write into it.

This page covers that lakehouse, the Iceberg mode the CLI provides today. A second mode is planned. See Modes.

When to use it

Reach for the lakehouse when all of these hold.

  • You already run an Iceberg catalog server: REST, Glue, Hive, SQL, or BigLake.
  • You want ACID commits and concurrent writers on a table.
  • You want version history as Iceberg snapshots, with rollback and expiry.
  • You accept a second copy of the rows, inside the warehouse.

Use the default file storage otherwise. It writes versions.json, needs no server, and every other part of Portolan assumes it.

Install

pip install "portolan-cli[iceberg]"

The extra brings PyIceberg and a SQLite catalog driver. Without it, every Iceberg command stops with an install hint.

Create a catalog that uses it

portolan init my-catalog --backend iceberg --license CC0-1.0

.portolan/config.yaml then records backend: iceberg. The default catalog is local SQLite at .portolan/iceberg.db, with a warehouse under .portolan/warehouse, so no data leaves the machine.

To use a catalog server, set the standard PyIceberg variables before you run the CLI. The CLI loads the catalog named portolake:

export PYICEBERG_CATALOG__PORTOLAKE__TYPE=rest
export PYICEBERG_CATALOG__PORTOLAKE__URI=https://catalog.example.com
export PYICEBERG_CATALOG__PORTOLAKE__WAREHOUSE=s3://my-bucket/warehouse

A ~/.pyiceberg.yaml file works too. PyIceberg reads the first one it finds in PYICEBERG_HOME, then your home directory, then the working directory. See the PyIceberg configuration reference.

Add data

Put the file in a subdirectory named after the collection. add reads the collection from that directory, and rejects a file at the catalog root.

mkdir buildings
cp ~/data/boston-open-space.parquet buildings/
portolan add buildings/boston-open-space.parquet

The rows go into the Iceberg table portolake.buildings, as one commit. The commit records the Portolan version string in its snapshot summary, so version history and table history are one history.

add also derives spatial helper columns and, above 100,000 rows, partitions the table by identity on a geohash column. Those columns are an internal storage layout, and table:columns in the STAC output leaves them out.

The GeoParquet file stays where it is, as its own asset on the collection. The table is a second copy of the rows, inside the warehouse.

Read the table

Any Iceberg client reads the table. The local warehouse writes one numbered metadata file per commit, under .portolan/warehouse/portolake/<collection>/metadata/. The highest number is the current one. Name it:

ls .portolan/warehouse/portolake/buildings/metadata/*.metadata.json | tail -1
INSTALL iceberg; LOAD iceberg;
SELECT count(*) FROM iceberg_scan(
  '.portolan/warehouse/portolake/buildings/metadata/<NNNNN>-<uuid>.metadata.json'
);
-- 1012

PyIceberg reads it through the catalog:

from pyiceberg.catalog import load_catalog

catalog = load_catalog("portolake")
table = catalog.load_table("portolake.buildings")
print(table.scan().to_arrow().num_rows)

Version commands

portolan version current and portolan version list work with every storage layer. These two need Iceberg snapshots, so they work only here:

portolan version rollback buildings 1.2.0
portolan version prune buildings --keep 3

Iceberg fields on the collection

add writes these onto collection.json:

"iceberg:catalog_type": "sql",
"iceberg:table_id": "portolake.buildings",
"iceberg:catalog_uri": "sqlite:///.../.portolan/iceberg.db",
"iceberg:format_version": 2,
"iceberg:partition_spec": [],
"iceberg:current_snapshot_id": 3024701191045327257

They follow the STAC Iceberg extension. A reader opens the table through them, because a catalog server resolves the current metadata at read time and publishes no fixed path to put in an asset.

Limits

portolan push stops here. add has already written to the warehouse, so no local tree remains to upload:

Push is not supported with the 'iceberg' backend. The iceberg backend manages versions through its catalog.

A browser and a plain HTTP client read the GeoParquet file beside the collection. Neither reads the warehouse.

Engine support for format version 3 geometry still varies. DuckDB reads geometry and answers "Geography support: not implemented" for geography. Spark and Trino need a catalog server.

PyIceberg 0.12 writes a quoted CRS in a geometry type string and parses only the quoted form, so a table it writes and a table DuckDB writes are not interchangeable.

portolan check reports no drift between the collection and the table. No check compares them yet.

Modes

The lakehouse above stores the rows itself, and you select it at init.

A second mode is planned and not built: a static Iceberg table over the GeoParquet a catalog already publishes. It copies no rows, needs no server, and leaves the STAC data asset alone. The Portolan specification describes that mode is proposed for the specification in portolan-spec#201, and is not part of the specification yet.

The reasoning behind both is recorded in context/shared/documentation/iceberg-as-optional-extra.md.