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
- 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'`.
- If no meaningful pk exists, switch the sink `type` from `upsert` to `append-only`.
- 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
- Only emit the primary_key option when a non-empty column list is available.
- Default to append-only sink type when no key exists.
- Validate WITH options in your SQL-generation tooling before submitting DDL.
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
- Must set `primary-key` in
- `commit-checkpoint-interval` must be greater than 0
- `compaction.delete_files_count_threshold` must be greater…
- `compaction_interval_sec` must be greater than 0
- `compaction.max_snapshots_num` must be greater than 0
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)