risingwavelabs/risingwave · error · SinkError::Config

Turbopuffer namespace must match [A-Za-z0-9-_.]

Error message

Turbopuffer namespace must match [A-Za-z0-9-_.]{{1,128}}

What it means

After the length check, validate_namespace enforces that every byte of the namespace is an ASCII alphanumeric, '-', '_', or '.'. Namespaces containing spaces, slashes, unicode, or other symbols are rejected with this message. It is raised both at sink creation (try_from) and per-row via url_for_row.

Solutions

  1. Restrict the namespace to [A-Za-z0-9-_.] characters.
  2. Sanitize/replace invalid characters in the namespace_column via a materialized view (e.g. regexp_replace).
  3. Hash or encode arbitrary strings (e.g. md5 hex) before using them as namespaces.

Example fix

// before: namespace from raw URL path
WITH (turbopuffer.namespace_column='path')
// after
CREATE MVIEW mv AS SELECT regexp_replace(path, '[^A-Za-z0-9_.-]', '_', 'g') AS path, * FROM src;
WITH (turbopuffer.namespace_column='path')
Defensive patterns

Strategy: validation

Validate before calling

const NS_RE: once_cell::sync::Lazy<regex::Regex> =
    once_cell::sync::Lazy::new(|| regex::Regex::new("^[A-Za-z0-9-_.]{1,128}$").unwrap());
fn namespace_ok(ns: &str) -> bool { NS_RE.is_match(ns) }

Prevention

When it happens

Trigger: Setting turbopuffer.namespace to a value with illegal characters (e.g. 'my index', 'ns/eu', unicode names); a namespace_column row value containing characters outside [A-Za-z0-9-_.].

Common situations: Using user-supplied or auto-generated strings (emails, URLs, UUIDs with dashes are fine, but slashes/spaces are not) as namespaces; forgetting to sanitize data-driven namespace values.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


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

Appendix: source

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

        tracing::warn!(
            value,
            "cast negative turbopuffer integer document id to unsigned integer"
        );
    }
    DocumentId::U64(value as u64)
}

fn validate_namespace(namespace: &str) -> Result<()> {
    if namespace.is_empty() || namespace.len() > 128 {
        return Err(SinkError::Config(anyhow!(
            "Turbopuffer namespace must be 1 to 128 bytes"
        )));
    }
    if !namespace
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
    {
        return Err(SinkError::Config(anyhow!(
            "Turbopuffer namespace must match [A-Za-z0-9-_.]{{1,128}}"
        )));
    }
    Ok(())
}

fn parse_column_selection(
    value: Option<&str>,
    schema: &Schema,
    attribute_indices: &[usize],
) -> Result<HashSet<String>> {
    let attribute_names = attribute_indices
        .iter()
        .map(|index| schema[*index].name.as_str())
        .collect::<HashSet<_>>();
    let columns: HashSet<String> = match value {
        Some(value) if value.trim() == "*" => {
            return Ok(attribute_names

View on GitHub (pinned to 6469eb736d)