{"record":{"id":"6980963fc777a038","repo":"zeroclaw-labs/zeroclaw","slug":"field-name-must-start-with-an-ascii-letter-or-un","errorCode":null,"errorMessage":"{field_name} must start with an ASCII letter or underscore; got '{value}'","messagePattern":"(.+?) must start with an ASCII letter or underscore; got '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-memory/src/postgres.rs","lineNumber":342,"sourceCode":"                .with_outcome(::zeroclaw_log::EventOutcome::Failure),\n            \"PostgreSQL operation thread terminated unexpectedly\"\n        );\n        anyhow::Error::msg(\"PostgreSQL operation thread terminated unexpectedly\")\n    })?\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!(","sourceCodeStart":324,"sourceCodeEnd":360,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-memory/src/postgres.rs#L324-L360","documentation":"The second identifier rule: PostgreSQL identifiers used by the memory store must start with an ASCII letter or underscore. Unquoted SQL identifiers cannot start with a digit, and since these names are formatted into query text, a leading digit or symbol would produce broken or dangerous SQL, so the constructor rejects it.","triggerScenarios":"Schema or table names like \"2026_archive\" or \"-prod\", or identifiers derived from agent/storage aliases beginning with a digit (agents.7writer), passed to the postgres store constructor or validated_schema_identifier.","commonSituations":"Numeric-prefixed autogenerated names (counters, years); kebab or symbol-prefixed aliases reused verbatim as table/schema names; names pasted with leading punctuation.","solutions":["Rename to start with a letter or underscore (archive_2026, _2026_archive).","Apply a sanitizer that prefixes an underscore when the first character is a digit.","Keep storage alias names in the same [A-Za-z_][A-Za-z0-9_]* shape so derived identifiers inherit it."],"exampleFix":"# before\n[storage.postgres.main]\ntable_prefix = \"2026-memories\"\n\n# after\n[storage.postgres.main]\ntable_prefix = \"memories_2026\"","handlingStrategy":"type-guard","validationCode":"if !is_valid_pg_identifier(&name) {\n    anyhow::bail!(\"identifier '{name}' must match [A-Za-z_][A-Za-z0-9_]*\");\n}","typeGuard":"fn is_valid_pg_identifier(name: &str) -> bool {\n    let mut chars = name.chars();\n    matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')\n        && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')\n        && !name.is_empty()\n}","tryCatchPattern":null,"preventionTips":["Constrain agent and storage alias names to [A-Za-z_][A-Za-z0-9_]* at creation time so derived SQL identifiers are always valid.","Add a config-lint regex check for schema/table fields before constructing the postgres store."],"tags":["postgres","validation","identifier","naming","sql"],"backgroundTag":"invalid-sql-identifier","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}