# Advanced usage of underlying Iceberg APIs
Most transforms can be expressed using the [standard dataframe syntax](../python-transform-basics/index.md#compute-logic). However, there are some Iceberg operations which cannot be expressed through the standard dataframe APIs or may benefit otherwise from direct use of the more specific and expressive underlying Iceberg APIs.
For example:
* **Row-level operations:** `UPDATE`, `DELETE`, and `MERGE INTO` statements that edit rows in place rather than rewriting the whole table.
* **Iceberg Spark procedures** such as `create_changelog_view`.
* **Iceberg metadata inspection:** Reading a table's snapshots, schema, and properties.
For these, you can access the open-source Iceberg APIs directly through handles on the runtime object:
| Compute type | Native handle | Returns |
|---|---|---|
| **PySpark**
(`TableTransformInput` / `TableTransformOutput`) | `.identifier`, `.catalog` | The table's fully qualified Spark identifier and catalog name (strings) |
| **Single node**
(`IcebergInput` / `IcebergOutput`) | `.table()` | The native PyIceberg `Table` object |
In each case the handle plugs into the open-source API where that API expects a table: as a table name in Iceberg Spark SQL (`.identifier`), or as the `Table` object in PyIceberg (`.table()`). Wherever the open-source Iceberg documentation refers to a table name or table object, pass the native handle in its place.
## Row edits in PySpark
This code example demonstrates how to edit rows in place without rewriting the whole table using the [Iceberg Spark APIs ↗](https://iceberg.apache.org/docs/latest/spark-writes/).
```python
from transforms.api import transform, TransformContext
from transforms.tables import TableInput, TableOutput, TableTransformInput, TableTransformOutput
@transform.spark.using(
updates=TableInput("/path/input"),
output=TableOutput("/path/output"),
)
def compute(
ctx: TransformContext,
updates: TableTransformInput,
output: TableTransformOutput,
):
# Spark identifier (use in place of a table name)
output_id = output.identifier
spark = ctx.spark_session
if spark.catalog.tableExists(output_id):
updates_id = updates.identifier
# Upsert with MERGE INTO, using an input registered as a temporary view
spark.sql(f"""
MERGE INTO {output_id} AS t
USING {updates_id} AS s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
""")
# Delete rows by predicate
spark.sql(f"DELETE FROM {output_id} WHERE as_of < '2026-04-01'")
# Update rows in place
spark.sql(f"UPDATE {output_id} SET status = 'Archived' WHERE status = 'Stale'")
else:
# First build: seed the table from the input
output.write_dataframe(updates.dataframe())
```
## Partitioned incremental writes in PySpark
This code example demonstrates how to write to a table using Iceberg Spark's [DataFrameWriterV2 API ↗](https://iceberg.apache.org/docs/latest/spark-writes/#writing-with-dataframes). The example includes Palantir incremental semantics and use of Iceberg's [partition transforms ↗](https://iceberg.apache.org/spec/#partition-transforms).
```python
from pyspark.sql.functions import days, col
from transforms.api import Input, transform, incremental, IncrementalTransformContext, IncrementalTransformInput
from transforms.tables import TableOutput, TableTransformOutput
@incremental(v2_semantics=True)
@transform(
input=Input("/path/input"),
output=TableOutput("/path/input"),
)
def partition_by_day(
ctx: IncrementalTransformContext,
input: IncrementalTransformInput,
output: TableTransformOutput,
):
writer = input.dataframe().writeTo(output.identifier).\
partitionedBy(days(col("created_at")))
if ctx.is_incremental:
writer.append()
else:
writer.createOrReplace()
```
## Row edits in PyIceberg
This code example demonstrates how to edit rows in place without rewriting the whole table using the [PyIceberg APIs ↗](https://py.iceberg.apache.org/). Access the native PyIceberg Table object with `.table()`, then use its row-level methods: `upsert()` (to mirror `MERGE INTO` in Iceberg Spark SQL), `delete()` for predicate-based deletion, and `overwrite()` to replace rows.
```python
from transforms.api import transform
from transforms.tables import IcebergInput, IcebergOutput, TableInput, TableOutput
import pyarrow as pa
@transform.using(
updates=TableInput("/path/input"),
output=TableOutput("/path/output"),
)
def compute(updates: IcebergInput, output: IcebergOutput):
# Read input into memory
updates_arrow = updates.arrow()
# Native PyIceberg table
table = output.table()
if table is None:
output.write_table(updates_arrow)
return
# Upsert with upsert(), matching on the join column(s)
table.upsert(updates_arrow, join_cols=["id"])
# Delete rows by predicate
table.delete(delete_filter="as_of < '2026-04-01'")
# Replace rows matching a filter with new rows
stale = table.scan(row_filter="status = 'Stale'").to_arrow()
archived = stale.set_column(
stale.schema.get_field_index("status"),
"status",
pa.array(["Archived"] * stale.num_rows, type=pa.string()),
)
table.overwrite(archived, overwrite_filter="status = 'Stale'")
```
## Comparing snapshots with PyIceberg
You can access finer-grained [PyIceberg APIs ↗](https://py.iceberg.apache.org/) such as metadata inspection by using `.table()` on your `IcebergInput` to return the underlying PyIceberg table. This code example demonstrates inspecting individual snapshots in PyIceberg to manually identify appended rows.
```python
from transforms.api import transform
from transforms.tables import IcebergInput, IcebergOutput, TableInput, TableOutput
@transform.using(
source_table=TableInput("/path/input"),
output_table=TableOutput("/path/output"),
)
def compute(source_table: IcebergInput, output_table: IcebergOutput):
# grab snapshot ids
current = source_table.current_snapshot_id
previous = source_table.previous_snapshot_id
# raw PyIceberg table
iceberg_table = source_table.table()
current_rows = iceberg_table.scan(snapshot_id=current).to_polars()
if previous is None:
output_table.write_table(current_rows)
return
previous_rows = iceberg_table.scan(snapshot_id=previous).to_polars()
new_rows = current_rows.join(previous_rows, on="id", how="anti")
output_table.write_table(new_rows)
```