risingwavelabs/risingwave · error · SinkError::Config

`primary-key` must not be empty in {}

Error message

`primary-key` must not be empty in {}

What it means

This error is raised by IcebergSinkConfig::from_btreemap when a sink is created with type = 'upsert' and the `primary_key` option is present but set to an empty string. RisingWave requires an explicit non-empty primary key for upsert sinks so it can deduplicate and merge rows in the Iceberg table. The check rejects empty strings before the sink is constructed.

Source

Thrown at src/connector/src/sink/iceberg/config.rs:592

                .map_err(|e| SinkError::Config(anyhow!(e)))?;

        if config.enable_compaction && !values.contains_key(COMPACTION_MAX_SNAPSHOTS_NUM) {
            config.max_snapshots_num_before_compaction = Some(DEFAULT_COMPACTION_MAX_SNAPSHOTS_NUM);
        }

        if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {
            return Err(SinkError::Config(anyhow!(
                "`{}` must be {}, or {}",
                SINK_TYPE_OPTION,
                SINK_TYPE_APPEND_ONLY,
                SINK_TYPE_UPSERT
            )));
        }

        if config.r#type == SINK_TYPE_UPSERT {
            if let Some(primary_key) = &config.primary_key {
                if primary_key.is_empty() {
                    return Err(SinkError::Config(anyhow!(
                        "`primary-key` must not be empty in {}",
                        SINK_TYPE_UPSERT
                    )));
                }
            } else if !config.enable_pk_index {
                // When `enable_pk_index = true`, the planner auto-derives the iceberg pk
                // from the upstream stream key, so the user does not need to spell it out
                // in WITH options. The derived pk is written back into properties before
                // this validation is consulted again at sink-construction time.
                return Err(SinkError::Config(anyhow!(
                    "Must set `primary-key` in {}",
                    SINK_TYPE_UPSERT
                )));
            }
        }

        // Enforce merge-on-read for append-only sinks
        Self::validate_append_only_write_mode(&config.r#type, config.write_mode)?;

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Provide a non-empty `primary_key` in the WITH options listing the upsert key columns, e.g. `primary_key='id'` or `primary_key='id,name'`.
  2. If no meaningful pk exists, switch the sink `type` from `upsert` to `append-only`.
  3. If the stream key should be used automatically, set `enable_pk_index=true` (or omit primary_key with pk index enabled) so the planner derives the pk.

Example fix

// before
WITH (
  connector = 'iceberg',
  type = 'upsert',
  primary_key = ''
)
// after
WITH (
  connector = 'iceberg',
  type = 'upsert',
  primary_key = 'user_id'
)
Defensive patterns

Strategy: validation

Validate before calling

const pk = opts['primary_key'];
if (opts.type === 'upsert' && pk !== undefined && pk.trim() === '') {
  throw new Error("`primary-key` must not be empty in upsert");
}

Type guard

function hasNonEmptyPk(opts) {
  return typeof opts.primary_key === 'string' && opts.primary_key.length > 0;
}

Try / catch

try {
  await createIcebergSink(opts);
} catch (e) {
  if (String(e).includes('`primary-key` must not be empty')) {
    opts.primary_key = inferPrimaryKeyColumns(opts);
    return createIcebergSink(opts);
  }
  throw e;
}

Prevention

When it happens

Trigger: Creating an Iceberg sink with `connector='iceberg'`, `type='upsert'`, and `primary_key=''` (empty string) in the WITH options, e.g. via CREATE SINK. The value parses as Some("") which fails `primary_key.is_empty()`.

Common situations: Templates or generated SQL that interpolate a pk column list variable that is empty; users copying upsert sink DDL and deleting the key list; tooling that writes `primary_key` unconditionally even when no pk columns were selected.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/0950ef83f5e7cdc8. Report an issue: GitHub.