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.

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.

Appendix: 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)