risingwavelabs/risingwave · error · SinkError::Config

Turbopuffer attribute column must not be named id

Error message

Turbopuffer attribute column must not be named id

What it means

Turbopuffer treats `id` specially in write requests as the document ID, so it cannot be sent as a regular attribute. The sink reserves the primary-key/id column and, in `try_from`, rejects any remaining attribute column literally named `id`.

Source

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

            (None, None) => {
                return Err(SinkError::Config(anyhow!(
                    "Turbopuffer sink requires either namespace or namespace_column"
                )));
            }
        };

        // Turbopuffer treats `id` as the document ID in write requests; it is not a schema
        // attribute. Dynamic namespace is also metadata for routing, not a document attribute.
        let excluded_indices = match &namespace {
            TurbopufferNamespace::Static(_) => HashSet::from([pk_index]),
            TurbopufferNamespace::Dynamic { index } => HashSet::from([pk_index, *index]),
        };
        let attribute_indices = (0..schema.len())
            .filter(|idx| !excluded_indices.contains(idx))
            .collect_vec();
        for index in &attribute_indices {
            if schema[*index].name == "id" {
                return Err(SinkError::Config(anyhow!(
                    "Turbopuffer attribute column must not be named id"
                )));
            }
        }
        let full_text_search_columns = parse_column_selection(
            config.full_text_search_columns.as_deref(),
            &schema,
            &attribute_indices,
        )?;
        let filterable_columns = parse_column_selection(
            config.filterable_columns.as_deref(),
            &schema,
            &attribute_indices,
        )?;
        let has_vector = attribute_indices
            .iter()
            .any(|idx| matches!(schema[*idx].data_type, DataType::Vector(_)));
        if has_vector && config.distance_metric.is_none() {

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Rename the attribute column (e.g. `record_id`, `item_id`) in the materialization feeding the sink.
  2. Make the `id` column the sink's primary key so it is consumed as the document ID instead of an attribute.
  3. Drop the redundant `id` column from the sink schema if it duplicates the pk.
  4. Alias it during SELECT, e.g. `id AS external_id` in the upstream MV.

Example fix

// before
CREATE SINK s FROM mv WITH (connector='turbopuffer', pk='doc_pk');
-- mv has both doc_pk (pk) and id (attribute)

// after
CREATE MATERIALIZED VIEW mv2 AS
SELECT doc_pk, id AS external_id, ... FROM mv;
CREATE SINK s FROM mv2 WITH (connector='turbopuffer', pk='doc_pk');
Defensive patterns

Strategy: validation

Validate before calling

// SQL: ensure no attribute column is literally 'id'
SELECT name FROM rw_columns WHERE relation = 'mv_for_sink' AND name = 'id';
// must return no rows unless 'id' is the designated pk

Prevention

When it happens

Trigger: Creating a Turbopuffer sink whose schema contains a column named `id` that is not the primary key designated for the document ID (it is neither excluded nor used as pk), e.g. an extra `id` varchar column distinct from the pk column.

Common situations: Source tables carry their own `id` column while the user also defines a separate pk for the sink; renaming a source column to `id` during schema evolution; copying tables that already have an `id` attribute.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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