Skip to main content
The Iceberg Target Connector writes your pipeline data to Apache Iceberg tables, registered in the catalog you choose. Tables are visible, queryable and governable as soon as they are written, by any engine that reads that catalog. Apache Iceberg is an open table format for large analytic datasets: ACID transactions, time travel, partition evolution and schema evolution, without disturbing concurrent readers. Every change event becomes a row, so each Iceberg table holds the full change history of its source table. The current state (one row per primary key) is read through a view over that history, called the merge view. On Snowflake, Popsink can create and maintain that view for you. See Current state: the merge view.

Compatible versions

Key Features

  • Any catalog: one connector type covers REST catalogs, AWS Glue, Amazon S3 Tables, a SQL catalog (hosted by your Popsink deployment, or your own PostgreSQL) and Hive Metastore.
  • Vended or static storage credentials: on a REST catalog the connector can rely entirely on per-table credentials vended by the catalog. Otherwise it uses its own S3, ADLS or GCS credentials, or the pod’s cloud identity.
  • Typed tables: columns are typed from the topic’s Avro schema. Numbers, decimals, dates and timestamps keep their type.
  • Full change history: every insert, update and delete is appended as a row, with its operation and source timestamp.
  • Managed current-state view on Snowflake: Popsink creates and maintains a <TABLE>_CURRENT view (one row per key, deletes removed) on Snowflake Horizon, or on any catalog Snowflake reads through a catalog-linked database.
  • No duplicates on restart: the connector records the Kafka offsets it has committed inside the Iceberg snapshots, so a pod restart does not append the same batch twice.
  • Built-in table maintenance: a daily job compacts small files, expires old snapshots, removes orphan files and can purge deleted keys.
  • Backfill: MySQL and IBM i sources can load a table’s existing rows into an Iceberg subscription.

How It Works

  1. The connector reads the subscription’s topic and turns each change event into a row: the mapped source columns, plus __op (the operation) and __ts_ms (when the change happened at the source).
  2. Rows are buffered per table and committed as an Iceberg append. Each table is committed when it reaches 10,000 rows or after 60 seconds, whichever comes first. Kafka offsets are committed only once the Iceberg commit has landed, so nothing is lost if the pod stops.
  3. The table therefore grows with every change: an UPDATE adds a row, and so does a DELETE. The connector never rewrites or deletes rows in the table itself.
  4. The current state of the source table is a view that keeps the latest row of each primary key and hides deleted keys. See Current state: the merge view.
  5. A scheduled maintenance job keeps the table healthy. See Table maintenance.

Prerequisites

  • An Iceberg catalog the connector can reach, and a namespace in it. The connector can create the namespace if you allow it.
  • A storage location for the data files (S3 bucket, ADLS Gen2 container or GCS bucket), unless your REST catalog vends storage credentials.
  • Credentials for the catalog, and, when the catalog does not vend them, for the storage. The required permissions are listed per catalog below.
  • A primary key on each subscription whose current state you want to read: flag the key columns in the subscription’s column mapping, or declare them on the datamodel. Without a key the table is still written, but there is nothing to collapse the history on.

Catalog Configuration

All catalogs share these fields:
By default, a REST catalog vends a short-lived storage credential per table (X-Iceberg-Access-Delegation: vended-credentials), and the connector holds no storage credential at all. A catalog that does not vend credentials (a private Polaris, Lakekeeper or Nessie without STS) needs static storage credentials instead.

Snowflake Horizon Catalog

The client ID must stay empty: Snowflake answers 400 invalid_scope to a token request that carries any client_id together with a session:role: scope. To create a PAT:
Writing needs an external volume on the database. Without one, reads work, and the connection test fails with a bare 403 Authorization failed:

Apache Polaris and Snowflake Open Catalog

Polaris and Open Catalog use a principal instead of a user: fill in both the client ID and secret, and keep the default scope PRINCIPAL_ROLE:ALL. The principal’s catalog role needs, on the namespace: NAMESPACE_READ_PROPERTIES, TABLE_CREATE, TABLE_LIST, TABLE_READ_PROPERTIES, TABLE_WRITE_DATA, TABLE_WRITE_PROPERTIES and TABLE_DROP, or CATALOG_MANAGE_CONTENT.
Databricks Unity Catalog exposes an Iceberg REST endpoint too, and can be reached with a bearer token, but this path has not been validated yet. To write to Unity Catalog, use the Unity Catalog (Delta Lake) target.

Storage Credentials

Storage Credentials selects how the connector authenticates to the storage holding the data files:
  • Vended by the catalog, per table (REST catalogs only, default): no storage credential on the connector.
  • The pod’s own identity (other catalogs, default): the cloud identity of the worker pod (IRSA / EKS Pod Identity, Workload Identity…).
  • Static credentials of this connector: pick the storage type and authentication below.
S3 also accepts an optional S3 Region, an S3 Endpoint for S3-compatible stores, and Path-style addressing (needed by most S3-compatible stores).
An ADLS connection string that sets a custom BlobEndpoint or DfsEndpoint is refused: the endpoint is always read from the abfss:// warehouse location.

Proxy

Table Defaults

These advanced connector settings apply to tables the connector creates. A subscription can override them, and an existing table keeps its own settings.

Connection Test

Test walks the same steps the worker walks, and prefixes any failure with the step that failed, so you know which input to fix:

Subscription Options

In the subscription drawer, the Advanced tab holds the Iceberg options. They apply only when the table is created: a table that already exists keeps its format version, partitioning and properties.
__ts_ms is a long (epoch milliseconds), so time transforms such as day do not apply to it. To partition by time, use a timestamp column of your own, or __ingested_at with metadata columns on.
Table naming. Each subscription writes one Iceberg table named after its target table name, in the connector’s namespace. Characters other than letters, digits and _ are replaced with _. Column names are kept exactly as the mapping spells them, lower case by default. Several subscriptions can target the same table.

What Is Written

One Iceberg table per subscription, appended to. Every change event is a row, so the table is the history of the source table, not its current state.
Metadata columns are on by default for subscriptions created since data plane 0.1.0-alpha.204. Older subscriptions keep their setting. Like the other subscription options, the setting is meant to be chosen when the table is created.

Column types

Columns are typed from the topic’s Avro schema (the Schema Registry’s latest version, or the datamodel’s schema). On a topic without a registry, types are inferred from the values. An explicit cast in the subscription’s mapping takes precedence.

Schema evolution

  • New source columns are added to the table automatically, as optional columns.
  • Columns are never dropped or retyped. The type a column was committed with wins: each value is cast to it ("00123" into a long column becomes 123), and a value that cannot be cast without losing information ("1.5" into a long column) refuses the whole row. Refused rows are counted as skipped in the subscription’s metrics and logged without their value. They are not retried.
  • A table that already exists keeps the types it was created with.

Current State: the Merge View

The Iceberg table is the change log: an update or a delete adds a row, and the connector never applies the primary key when it writes. To read the current state of the source table, one row per key, you query a merge view that collapses the history:
  1. Group the rows by the subscription’s primary key.
  2. Keep the latest change of each key: the highest __ts_ms (the source change time). With metadata columns on, __kafka_offset breaks ties between two changes made in the same millisecond.
  3. Remove deleted keys: if the latest change of a key is a delete (__op = 'd'), the key is hidden. This filter runs after the ranking: filtering deletes first would bring a deleted row’s previous version back.
For example, the history of order 42: The merge view returns one row, order 42 with status shipped. Order 43 was deleted, so it is hidden. Because it is a plain view, the current state is computed on every query: it reflects every committed append and has no refresh delay. The history stays available in the table, together with Iceberg time travel.

Managed view on Snowflake

The connector cannot create an Iceberg view in the catalog: iceberg-rust has no view API, and most catalogs (Horizon, Unity Catalog, Glue, S3 Tables, BigLake) accept no Iceberg view. On Snowflake, Popsink can instead create and maintain the view as a Snowflake view, named <TABLE>_CURRENT in the table’s case (orders → orders_current, ORDERS → ORDERS_CURRENT). The view Popsink creates is:
  • PARTITION BY lists the subscription’s primary key columns. The __kafka_offset tie-breaker is added only when the table has metadata columns.
  • A subscription gets a view only if it has a primary key. Its key columns, __op and __ts_ms must exist in the table: until they do, the view waits.
  • Snowflake freezes SELECT * when a view is created, so the view is rebuilt when the table gains a column. Each build is checked against the table before it counts as built. Until then the view may lack the newest column.
  • Popsink only manages a view whose comment starts with popsink-current-state:v1. A view of the same name that Popsink did not create, for example one you built yourself, is left as it is and only logged: drop it to let Popsink take over. Grants on a Popsink view are kept across rebuilds.
  • If the view cannot be built (a missing grant, for instance), the table is still written: the subscription shows the view as degraded and the connector retries, after 1 hour for a missing grant.
  • The view lives in a single-level namespace: a nested namespace gets no view.
These settings are set through the API on the subscription:
Opt in with horizon_current_state_view: true on the connector, set through the API. The view is created beside the table, in the same database and schema, with the catalog’s own credential and role. That role therefore needs:
A rebuild after a schema change happens about 10 seconds after the table gains a column.

Creating the view yourself

On other engines, or on Snowflake without the settings above, create the view once yourself. Replace id with your primary key columns, and drop the __kafka_offset tie-breaker if metadata columns are off.
On Snowflake the double quotes are required: Iceberg column names are stored as the mapping spells them, lower case by default, and Snowflake folds unquoted identifiers to upper case. Use IS DISTINCT FROM 'd' rather than <> 'd', which would also drop rows whose __op is empty (events that do not come from Debezium). To materialize the current state as a table instead of a view, see Build a merge table on the Warehouse page.

Table Maintenance

Every Iceberg connector runs a scheduled maintenance job beside its worker, once a day by default, at a time derived from the connector. For each table it:
  1. Purges deleted keys (opt-in): removes every row of a key whose latest change is a delete, so its data leaves storage once older snapshots expire. A key re-inserted after its delete keeps its newer rows. Skipped on tables without a primary key.
  2. Compacts small data files into files of about 128 MB, without changing a row.
  3. Expires snapshots older than 7 days, always keeping the last 20.
  4. Deletes orphan files, files no snapshot references, once they are older than 72 hours. Skipped on Amazon S3 Tables, which does this itself.
These connector settings are set through the API:
Compaction commits as an Iceberg overwrite. An incremental reader (Spark or Flink streaming reads) may refuse to read past such a snapshot. Expiring snapshots also limits how far back time travel goes.

Backfill

A MySQL or IBM i source can backfill an Iceberg subscription. The source table’s current rows are appended with __op = 'r' and __ts_ms = 0, so any change streamed since the backfill ranks above them in the merge view, and a streamed delete removes the key.
  • The subscription needs a primary key: without one there is no merge view to collapse the loaded rows with the streamed ones.
  • Truncate history is not supported: the connector only appends, so a reload adds rows and the merge view keeps the newest row per key.
  • Sources reached through an SSH tunnel, and IBM i multi-member files, are not supported yet.
  • An IBM i table is first read into the backfill’s own scratch space (up to 55 GiB compressed), then loaded.
  • A backfill does not resume: relaunching it appends the rows again, and the merge view collapses them.

Restarts and Replays

  • Restarts do not duplicate data. Each append records the last Kafka offset it holds in the Iceberg snapshot. On restart, the connector skips rows the table already holds, so a pod restart, a rebalance or an append that landed but answered an error is not written twice.
  • A replay appends the history again. Replaying a pipeline or a subscription, or triggering a backfill from the beginning of the topic, starts a new delivery: every event Kafka still retains is appended a second time. The merge view still returns the right current state, since duplicates share a key, __ts_ms and __op, but the history table holds both copies.
  • Rows refused by typing are skipped, not retried (see Schema evolution).
  • A table that hits an error no retry can fix (an invalid table definition, a missing namespace) is quarantined: it stops buffering, its rows are dropped and the connector reports the error. Other tables keep flowing.

Upgrading from the Previous Iceberg Target

Connectors created on the former Azure-only Iceberg target (and on the former Polaris target) are migrated automatically when the data plane is upgraded.
  • Polaris connectors become Iceberg REST connectors with the same catalog, tables and Kafka offsets. Nothing is replayed.
  • Former Azure-only connectors keep the same storage account, container, service principal or connection string, and proxy. Their catalog becomes a SQL catalog hosted by the data plane:
    • New tables, at a new path. The old target kept its catalog in a file inside its pod, so there is nothing to carry over. Tables are created again under <container>/popsink/popsink/<table>, in namespace popsink. The old files stay where they were, untouched: point readers at the new path. Connectors that wrote to the same container share one catalog (popsink-<account>-<container>).
    • Only what Kafka still holds is reloaded. Every subscription replays its topic from the beginning. Rows older than the topic’s retention exist only in the old tables.
    • Load columns change. Migrated subscriptions get __kafka_partition, __kafka_offset, __kafka_timestamp and __ingested_at in place of the previous custom columns, which are not reproduced.

Limitations

  • Iceberg format v1 is not supported.
  • Format version, partitioning, table properties and metadata columns apply only when a table is created.
  • Columns are never dropped or retyped. Nested records and arrays are written as JSON text. Avro timestamps lose their sub-second part.
  • The managed merge view is available on Snowflake only, for single-level namespaces.
  • The SQL catalog supports PostgreSQL only. A Hadoop / filesystem catalog is not supported.
  • The proxy option covers the Entra ID token call only. There is no proxy for catalog, AWS or GCS traffic.
  • The maintenance job cannot use an S3 role that requires an external ID.