risingwavelabs/risingwave · error · SinkError::Config

Turbopuffer sink requires exactly one primary_key column

Error message

Turbopuffer sink requires exactly one primary_key column

What it means

The turbopuffer sink uses the primary key column as the document id, so it requires the sink schema to have exactly one primary key column. Schemas with zero or multiple PK columns are rejected at sink construction.

Source

Thrown at src/connector/src/sink/turbopuffer.rs:154

impl EnforceSecret for TurbopufferSink {
    fn enforce_secret<'a>(
        prop_iter: impl Iterator<Item = &'a str>,
    ) -> crate::error::ConnectorResult<()> {
        for prop in prop_iter {
            TurbopufferConfig::enforce_one(prop)?;
        }
        Ok(())
    }
}

impl TryFrom<SinkParam> for TurbopufferSink {
    type Error = SinkError;

    fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
        let schema = param.schema();
        let pk_indices = param.downstream_pk_or_empty();
        let [pk_index] = pk_indices.as_slice() else {
            return Err(SinkError::Config(anyhow!(
                "Turbopuffer sink requires exactly one primary_key column"
            )));
        };
        let pk_index = *pk_index;
        match schema[pk_index].data_type() {
            DataType::Int16
            | DataType::Int32
            | DataType::Int64
            | DataType::Serial
            | DataType::Varchar => {}
            data_type => {
                return Err(SinkError::Config(anyhow!(
                    "Turbopuffer document id column must be an integer or varchar, got {:?}",
                    data_type
                )));
            }
        };
        let config = TurbopufferConfig::from_btreemap(param.properties)?;

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Create the sink from a source with exactly one primary key column
  2. Reshape the query to produce a single-column unique key (e.g. concat/hash into one id column)
  3. Choose a different sink connector if composite keys are required

Example fix

// before
-- MV with composite PK (a, b)
CREATE SINK tp FROM mv WITH (connector='turbopuffer', ...);
// after
CREATE MATERIALIZED VIEW mv2 AS SELECT md5(concat(a::text, b::text)) AS id, ... FROM mv;
CREATE SINK tp FROM mv2 WITH (connector='turbopuffer', primary_key = 'id', ...);
Defensive patterns

Strategy: validation

Validate before calling

const pks = schema.columns.filter(c => c.isPrimaryKey); if (pks.length !== 1) throw new Error('turbopuffer needs exactly one PK column')

Type guard

const hasSinglePk = (schema) => schema.primaryKeyColumns?.length === 1;

Try / catch

catch (SinkError::Config(e)) if e.contains('exactly one primary_key') { reshape schema upstream }

Prevention

When it happens

Trigger: CREATE SINK on a materialized view/table whose primary key has 0 or 2+ columns, with connector='turbopuffer'.

Common situations: Sink over an append-only MV without a PK; composite primary keys from a join; forgetting to define a PK before creating the sink.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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