Skip to content

Apache Iceberg

IcebergIOHandler

Persists asset outputs as Apache Iceberg tables, through a pyiceberg catalog. See the Iceberg guide.

from rivers.io_handlers.iceberg import IcebergIOHandler

io = IcebergIOHandler(
    catalog_name="lake",
    catalog_properties={"type": "rest", "uri": "https://catalog.example.com"},
    namespace="analytics",
)

Constructor:

Parameter Type Default Description
namespace str required Namespace of the asset tables. Dots separate nested namespaces.
catalog_name str "default" pyiceberg catalog name.
catalog_properties dict[str, str] \| None None Catalog properties for load_catalog, such as type, uri, and warehouse, and the storage properties of pyiceberg's FileIO (s3.*, adls.*, gcs.*; see Storage). pyiceberg adds the ones from .pyiceberg.yaml and PYICEBERG_CATALOG__<NAME>__<KEY> environment variables.
mode str "overwrite" Write mode: "overwrite", "append", or "upsert".
schema_mode str \| None None Schema evolution: "merge" or "overwrite".
upsert_config UpsertConfig \| None None Join columns and clauses for mode="upsert".
table_properties dict[str, str] \| None None Iceberg table properties, set on create and when they change.
snapshot_properties dict[str, str] \| None None Properties added to the summary of each snapshot that the handler writes.
handler_config dict[str, Any] \| None None Type handler config: a DuckDBResource as {"duckdb": ...}, or the Spark session and catalog name as {"spark_session": ..., "spark_catalog": ...}.

Asset metadata overrides:

Key Type Description
iceberg/namespace str Namespace of the table.
iceberg/table str Table name, instead of the asset name.
iceberg/mode str Write mode.
iceberg/schema_mode str Schema mode.
iceberg/partition_expr str \| JSON dict Partition column, or a JSON object from dimension to column.
iceberg/join_cols JSON list Upsert join columns.
iceberg/table_properties JSON dict Table properties, on top of table_properties.
iceberg/snapshot_properties JSON dict Snapshot properties, on top of snapshot_properties.
iceberg/columns JSON list Columns to read.
iceberg/snapshot_id str Snapshot to read.

Output metadata:

Key Type Description
iceberg/table str Table identifier, such as analytics.users.
iceberg/mode str Write mode used.
iceberg/snapshot_id int Current snapshot after the write. Also the data version.
iceberg/num_rows int Rows in the table after the write.
iceberg/size_bytes int Size of the table's data files.
iceberg/write_duration_s float Write duration in seconds.
iceberg/metadata_location str Location of the table's current metadata file.
iceberg/rows_updated int Upserts only: rows updated.
iceberg/rows_inserted int Upserts only: rows inserted.
rivers/schema Schema Arrow schema of the table.

Methods for action bodies

An action gets the asset's resolved handler as ctx.io_handler. These methods use the same table and partition resolution as the writes.

pyiceberg_catalog()

Loads the pyiceberg catalog: load_catalog(catalog_name, **catalog_properties).

table_identifier(asset_name, asset_metadata=None)

Returns (*namespace, table), honoring iceberg/namespace and iceberg/table.

load_table(asset_name, asset_metadata=None)

Returns the asset's pyiceberg Table. Raises NoSuchTableError when the table does not exist yet.

partition_filter(asset_metadata, partition, schema)

Returns the pyiceberg expression for the rows of the partition(s), honoring iceberg/partition_expr. Pass ctx.asset_metadata, ctx.partition, and table.schema(); the schema types the time-window bounds.

@rs.action(outcome=rs.Outcome.Unmaterialize)
@classmethod
def delete(cls, ctx: rs.ActionContext) -> None:
    table = ctx.io_handler.load_table(ctx.asset_name, ctx.asset_metadata)
    table.delete(ctx.io_handler.partition_filter(ctx.asset_metadata, ctx.partition, table.schema()))

partition is required: a keyless action run raises ValueError.

resolved_snapshot_properties(asset_metadata)

Returns snapshot_properties with the asset's iceberg/snapshot_properties on top.


UpsertConfig

How mode="upsert" matches rows, with pyiceberg's Table.upsert.

from rivers.io_handlers.iceberg import UpsertConfig

config = UpsertConfig(join_cols=["id"])
Attribute Type Default Description
join_cols list[str] \| None None Columns that identify a row. None uses the table's identifier fields. iceberg/join_cols overrides it per asset.
when_matched_update_all bool True Update matched rows whose values changed.
when_not_matched_insert_all bool True Insert rows without a match.

IcebergAsset

Asset base class with Iceberg maintenance verbs. Subclass it and define materialize; delete and expire_snapshots appear as actions, run against the asset's IcebergIOHandler (its own io_handler or the repository default). Sets kinds = "iceberg".

from rivers.io_handlers.iceberg import IcebergAsset  # also exported as rs.IcebergAsset

class Orders(IcebergAsset):
    io_handler = LAKE

    @classmethod
    def materialize(cls) -> pl.DataFrame: ...
Verb Declaration Behavior
delete Unmaterialize + Exclusive + DownstreamFirst + Optional key Deletes the keyed partition's rows with partition_filter, or every row without a key. A keyed delete needs iceberg/partition_expr.
expire_snapshots Unchanged + Exclusive + Keyless Removes old snapshots from the table's metadata; their data files stay. Branch and tag heads stay. Reports snapshots_expired.

A verb needs the asset to resolve an IcebergIOHandler; another handler fails the step with a TypeError. On a table that does not exist yet, expire_snapshots reports ActionResult.unchanged(), and delete still applies its Unmaterialize outcome. A subclass that defines a verb replaces the built-in one.

ExpireSnapshotsConfig

Per-asset overrides via run_action("expire_snapshots", config={"assets": {"<asset>": {"config": {...}}}}):

Field Type Default Description
retention_hours float \| None None Expire snapshots older than this. None uses the table's history.expire.max-snapshot-age-ms (5 days by default).
retain_last int \| None None Keep this many of the newest snapshots of the table's history. None uses history.expire.min-snapshots-to-keep (1 by default).

IcebergTypeHandler

Abstract base for adding type support to IcebergIOHandler.

from rivers.io_handlers.iceberg import IcebergReadRequest, IcebergTypeHandler, IcebergWriteRequest

class MyTypeHandler(IcebergTypeHandler[MyType]):
    @property
    def supported_types(self) -> Sequence[type[MyType]]:
        return [MyType]

    def load_input(self, request: IcebergReadRequest, target_type: type[MyType]) -> MyType:
        ...

    def handle_output(self, context: OutputContext, obj: MyType, request: IcebergWriteRequest) -> None:
        ...

Abstract members:

Member Description
supported_types Property returning the types this handler supports.
load_input(request, target_type) Read the table.
handle_output(context, obj, request) Write the table and record output metadata.

IcebergReadRequest has the catalog, the table identifier, the partition and partition_expr, the columns, and the snapshot_id. Its helpers are table (the pyiceberg table, loaded on first use), scan() (a pyiceberg scan with the partition filter, the columns, and the snapshot), and predicate (the partition as a SQL WHERE clause). IcebergWriteRequest has the resolved write settings.

ArrowIcebergTypeHandler

IcebergTypeHandler for types that convert to Arrow. Writes with handle_output are implemented with pyiceberg; subclasses implement to_arrow and load_input.

from rivers.io_handlers.iceberg import ArrowIcebergTypeHandler

class MyTypeHandler(ArrowIcebergTypeHandler[MyType]):
    @property
    def supported_types(self) -> Sequence[type[MyType]]:
        return [MyType]

    def to_arrow(self, obj: MyType) -> pa.Table | pa.RecordBatchReader:
        ...

    def load_input(self, request: IcebergReadRequest, target_type: type[MyType]) -> MyType:
        return convert(request.scan().to_arrow())

Built-in handlers (registered when their library is installed):

Handler Module Types
PyArrowTypeHandler rivers.io_handlers.iceberg.pyarrow pyarrow.Table, pyarrow.RecordBatchReader
PolarsTypeHandler rivers.io_handlers.iceberg.polars polars.DataFrame, polars.LazyFrame
PandasTypeHandler rivers.io_handlers.iceberg.pandas pandas.DataFrame
DuckDBTypeHandler rivers.io_handlers.iceberg.duckdb duckdb.DuckDBPyRelation

PySparkIcebergTypeHandler

IcebergTypeHandler-based handler class for adding Spark-based type support to IcebergIOHandler. Reads with load_input and writes with handle_output, both are implemented with Spark.

from rivers.io_handlers.iceberg import PySparkIcebergTypeHandler

class MyTypeHandler(PySparkIcebergTypeHandler[MyType]):
    @property
    def supported_types(self) -> Sequence[type[MyType]]:
        return [MyType]

    def load_input(
        self, request: IcebergReadRequest, target_type: type[MyType]
    ) -> MyType:
        """Load a lazy Spark DataFrame with projection, partition, and snapshot filters."""
        ...

    def handle_output(
        self,
        context: OutputContext,
        obj: DataFrame,
        request: IcebergWriteRequest,
    ) -> None:
        """Write through Spark and attach Rivers output metadata."""
        ...