{"record":{"id":"21c96658b702fc5c","repo":"risingwavelabs/risingwave","slug":"turbopuffer-namespace-must-match-a-za-z0-9-1","errorCode":null,"errorMessage":"Turbopuffer namespace must match [A-Za-z0-9-_.]{{1,128}}","messagePattern":"Turbopuffer namespace must match \\[A-Za-z0-9-_\\.\\](.+?)\\}","errorType":"validation","errorClass":"SinkError::Config","httpStatus":null,"severity":"error","filePath":"src/connector/src/sink/turbopuffer.rs","lineNumber":734,"sourceCode":"        tracing::warn!(\n            value,\n            \"cast negative turbopuffer integer document id to unsigned integer\"\n        );\n    }\n    DocumentId::U64(value as u64)\n}\n\nfn validate_namespace(namespace: &str) -> Result<()> {\n    if namespace.is_empty() || namespace.len() > 128 {\n        return Err(SinkError::Config(anyhow!(\n            \"Turbopuffer namespace must be 1 to 128 bytes\"\n        )));\n    }\n    if !namespace\n        .bytes()\n        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))\n    {\n        return Err(SinkError::Config(anyhow!(\n            \"Turbopuffer namespace must match [A-Za-z0-9-_.]{{1,128}}\"\n        )));\n    }\n    Ok(())\n}\n\nfn parse_column_selection(\n    value: Option<&str>,\n    schema: &Schema,\n    attribute_indices: &[usize],\n) -> Result<HashSet<String>> {\n    let attribute_names = attribute_indices\n        .iter()\n        .map(|index| schema[*index].name.as_str())\n        .collect::<HashSet<_>>();\n    let columns: HashSet<String> = match value {\n        Some(value) if value.trim() == \"*\" => {\n            return Ok(attribute_names","sourceCodeStart":716,"sourceCodeEnd":752,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/connector/src/sink/turbopuffer.rs#L716-L752","documentation":"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.","triggerScenarios":"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-_.].","commonSituations":"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.","solutions":["Restrict the namespace to [A-Za-z0-9-_.] characters.","Sanitize/replace invalid characters in the namespace_column via a materialized view (e.g. regexp_replace).","Hash or encode arbitrary strings (e.g. md5 hex) before using them as namespaces."],"exampleFix":"// before: namespace from raw URL path\nWITH (turbopuffer.namespace_column='path')\n// after\nCREATE MVIEW mv AS SELECT regexp_replace(path, '[^A-Za-z0-9_.-]', '_', 'g') AS path, * FROM src;\nWITH (turbopuffer.namespace_column='path')","handlingStrategy":"validation","validationCode":"const NS_RE: once_cell::sync::Lazy<regex::Regex> =\n    once_cell::sync::Lazy::new(|| regex::Regex::new(\"^[A-Za-z0-9-_.]{1,128}$\").unwrap());\nfn namespace_ok(ns: &str) -> bool { NS_RE.is_match(ns) }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Sanitize data-driven namespaces with regexp_replace before sinking.","Use only [A-Za-z0-9-_.] in namespace options.","Hash arbitrary strings (md5/sha hex) when namespaces derive from user data."],"tags":["turbopuffer","namespace","validation","regex"],"backgroundTag":"invalid-identifier-format","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}