risingwavelabs/risingwave · error · SinkError::Http

Turbopuffer namespace_column cannot be null

Error message

Turbopuffer namespace_column cannot be null

What it means

When the namespace is chosen per row via `namespace_column`, the URL for each write is built from that column's value in `url_for_row`. If the datum at the namespace index is NULL at write time, there is no namespace to route the document to, so an Http error is returned for the chunk.

Source

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

            row_encoder,
            write_batch_size,
            max_linger,
            pending_batches: BTreeMap::new(),
        })
    }

    fn url_for_row(&self, row: &impl Row) -> Result<String> {
        match &self.namespace {
            TurbopufferNamespace::Static(namespace) => Ok(format!(
                "{}/v2/namespaces/{}",
                self.base_url,
                namespace.as_str()
            )),
            TurbopufferNamespace::Dynamic { index } => {
                let namespace = match row.datum_at(*index) {
                    Some(ScalarRefImpl::Utf8(namespace)) => namespace,
                    None => {
                        return Err(SinkError::Http(anyhow!(
                            "Turbopuffer namespace_column cannot be null"
                        )));
                    }
                    Some(_) => {
                        return Err(SinkError::Http(anyhow!(
                            "unexpected namespace_column type, expected varchar"
                        )));
                    }
                };
                validate_namespace(namespace)?;
                Ok(format!("{}/v2/namespaces/{}", self.base_url, namespace))
            }
        }
    }

    // Turbopuffer document IDs are unsigned 64-bit integers, UUIDs, or strings up to 64 bytes.
    // RisingWave UUID IDs can be represented with varchar.
    fn id_for_row(&self, row: &impl Row) -> Result<DocumentId> {

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Make the namespace column NOT NULL in the upstream MV/table (e.g. COALESCE(ns, 'default')).
  2. Backfill existing NULL rows with a default namespace before enabling the sink.
  3. Add a WHERE filter upstream to exclude rows with NULL namespace.
  4. If a single namespace works, switch to the static `namespace` option.

Example fix

// before
CREATE MATERIALIZED VIEW mv AS SELECT tenant_id::varchar AS ns, * FROM t;

// after
CREATE MATERIALIZED VIEW mv AS
SELECT COALESCE(tenant_id::varchar, 'default') AS ns, * FROM t;
Defensive patterns

Strategy: validation

Validate before calling

// SQL: detect NULL namespaces before the sink processes them
SELECT count(*) FROM mv_for_sink WHERE ns IS NULL; -- should be 0

Type guard

// in a row-processing pipeline, before building the sink payload
function getNamespace(row) {
  const ns = row.ns;
  if (typeof ns !== 'string') throw new TypeError('namespace_column must be a non-null string');
  return ns;
}

Try / catch

// route failing rows to a dead-letter path
try {
  await sink.write(row);
} catch (e) {
  if (String(e).includes('namespace_column cannot be null')) {
    deadLetter.push({ row, reason: 'null-namespace' });
  } else { throw e; }
}

Prevention

When it happens

Trigger: Writing a row where the varchar namespace_column is NULL; rows inserted with an unspecified namespace column after the sink was created.

Common situations: Nullable namespace column in the MV feeding the sink; batch backfills that omit the namespace field; LEFT JOIN results producing NULL namespaces for unmatched rows.

Related errors


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