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>_CURRENTview (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
- 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). - 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.
- The table therefore grows with every change: an
UPDATEadds a row, and so does aDELETE. The connector never rewrites or deletes rows in the table itself. - 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.
- 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:- Iceberg REST
- AWS Glue
- Amazon S3 Tables
- SQL catalog
- Hive Metastore
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: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 scopePRINCIPAL_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.
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 alongcolumn becomes123), and a value that cannot be cast without losing information ("1.5"into alongcolumn) 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:- Group the rows by the subscription’s primary key.
- Keep the latest change of each key: the highest
__ts_ms(the source change time). With metadata columns on,__kafka_offsetbreaks ties between two changes made in the same millisecond. - 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.
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 BYlists the subscription’s primary key columns. The__kafka_offsettie-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,
__opand__ts_msmust 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.
- Snowflake Horizon
- Open Catalog, Polaris, Glue, S3 Tables, other REST
Opt in with A rebuild after a schema change happens about 10 seconds after the table gains a column.
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:Creating the view yourself
On other engines, or on Snowflake without the settings above, create the view once yourself. Replaceid with your primary key columns, and drop the __kafka_offset tie-breaker if metadata columns are off.
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:- 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.
- Compacts small data files into files of about 128 MB, without changing a row.
- Expires snapshots older than 7 days, always keeping the last 20.
- Deletes orphan files, files no snapshot references, once they are older than 72 hours. Skipped on Amazon S3 Tables, which does this itself.
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_msand__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 namespacepopsink. 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_timestampand__ingested_atin place of the previous custom columns, which are not reproduced.
- 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
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.