{"record":{"id":"843e15d2c252a987","repo":"zeroclaw-labs/zeroclaw","slug":"field-name-can-only-contain-ascii-letters-numbe","errorCode":null,"errorMessage":"{field_name} can only contain ASCII letters, numbers, and underscores; got '{value}'","messagePattern":"(.+?) can only contain ASCII letters, numbers, and underscores; got '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-memory/src/postgres.rs","lineNumber":346,"sourceCode":"    })?\n}\n\npub(super) fn validate_identifier(value: &str, field_name: &str) -> Result<()> {\n    if value.is_empty() {\n        anyhow::bail!(\"{field_name} must not be empty\");\n    }\n\n    let mut chars = value.chars();\n    let Some(first) = chars.next() else {\n        anyhow::bail!(\"{field_name} must not be empty\");\n    };\n\n    if !(first.is_ascii_alphabetic() || first == '_') {\n        anyhow::bail!(\"{field_name} must start with an ASCII letter or underscore; got '{value}'\");\n    }\n\n    if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {\n        anyhow::bail!(\n            \"{field_name} can only contain ASCII letters, numbers, and underscores; got '{value}'\"\n        );\n    }\n\n    Ok(())\n}\n\npub(super) fn quote_identifier(value: &str) -> String {\n    format!(\"\\\"{value}\\\"\")\n}\n\nfn recall_time_filter(since: bool, until: bool, first_placeholder: usize) -> String {\n    match (since, until) {\n        (true, true) => format!(\n            \" AND m.created_at >= ${first_placeholder}::TIMESTAMPTZ AND m.created_at <= ${}::TIMESTAMPTZ\",\n            first_placeholder + 1\n        ),\n        (true, false) => {","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-memory/src/postgres.rs#L328-L364","documentation":"The final identifier rule: after the first character, every remaining character must be an ASCII letter, digit, or underscore. Anything else — spaces, dashes, dots, quotes, non-ASCII — is rejected because these identifiers are formatted directly into SQL statements and any other character would break the statement or enable injection through identifiers.","triggerScenarios":"Identifiers containing dashes (kebab-case aliases like my-agent), dots, spaces, quotes, or Unicode characters, passed to new or validated_schema_identifier for the postgres backend.","commonSituations":"Kebab-case agent or storage alias names used directly as schema/table names; names with trailing whitespace from copy-paste; internationalized names with accented characters.","solutions":["Use snake_case for schema/table identifiers: replace dashes and spaces with underscores.","Sanitize derived identifiers — keep [A-Za-z0-9_] and substitute everything else with an underscore.","Trim whitespace when taking identifiers from user input or config."],"exampleFix":"// before: alias \"my-agent\" -> \"my-agent_memories\" (rejected)\nlet table = format!(\"{agent}_memories\", agent = alias);\n\n// after: sanitize to the allowed charset\nlet table = format!(\"{}_memories\", sanitize_identifier(&alias)); // \"my_agent_memories\"","handlingStrategy":"type-guard","validationCode":"let table = sanitize_identifier(&format!(\"{alias}_memories\"));\nassert!(is_valid_pg_identifier(&table), \"derived identifier failed validation\");","typeGuard":"fn sanitize_identifier(raw: &str) -> String {\n    let mut s: String = raw\n        .chars()\n        .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })\n        .collect();\n    if !s.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {\n        s.insert(0, '_');\n    }\n    s\n}","tryCatchPattern":null,"preventionTips":["Never interpolate raw user-controlled strings into schema/table names; sanitize to [A-Za-z0-9_] first (this also closes identifier-based SQL injection).","Prefer allowlisting identifiers against a fixed set when the set of legal names is known."],"tags":["postgres","validation","identifier","sanitization","sql-injection"],"backgroundTag":"invalid-sql-identifier","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}