{"record":{"id":"ae769517602409ea","repo":"mem0ai/mem0","slug":"invalid-filter-key-key-only-letters-digits","errorCode":null,"errorMessage":"Invalid filter key '${key}': only letters, digits, and underscores are allowed.","messagePattern":"Invalid filter key '(.+?)': only letters, digits, and underscores are allowed\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mem0-ts/src/oss/src/vector_stores/pgvector.ts","lineNumber":24,"sourceCode":"\nconst SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/;\n\nfunction validateIdentifier(\n  name: string,\n  label: string = \"identifier\",\n): string {\n  if (!SAFE_IDENTIFIER_RE.test(name)) {\n    throw new Error(\n      `Invalid ${label} '${name}': only letters, digits, and underscores are allowed, ` +\n        `must start with a letter or underscore, and be at most 128 characters.`,\n    );\n  }\n  return name;\n}\n\nfunction escapeFilterKey(key: string): string {\n  if (!SAFE_IDENTIFIER_RE.test(key)) {\n    throw new Error(\n      `Invalid filter key '${key}': only letters, digits, and underscores are allowed.`,\n    );\n  }\n  return key;\n}\n\ninterface FilterResult {\n  conditions: string[];\n  values: any[];\n  paramIndex: number;\n}\n\nconst OPERATOR_SQL_MAP: Record<string, { template: string; numeric: boolean }> =\n  {\n    eq: { template: \"payload->>'%KEY%' = $%IDX%\", numeric: false },\n    ne: { template: \"payload->>'%KEY%' != $%IDX%\", numeric: false },\n    gt: { template: \"(payload->>'%KEY%')::numeric > $%IDX%\", numeric: true },\n    gte: { template: \"(payload->>'%KEY%')::numeric >= $%IDX%\", numeric: true },","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/mem0ai/mem0/blob/001c235229be8795e3834520467bd0d661ed8f34/mem0-ts/src/oss/src/vector_stores/pgvector.ts#L6-L42","documentation":"When building SQL WHERE clauses from SearchFilters in the pgvector store, each filter key (the payload field name) is validated against SAFE_IDENTIFIER_RE. A filter key with characters other than letters, digits, and underscores (or one that starts with a digit or is over 128 chars) is rejected. This guards the key interpolation into SQL, since keys cannot be sent as bind parameters.","triggerScenarios":"Calling search/get with filters whose keys contain dots, hyphens, spaces, or start with a digit, e.g. { 'meta.type': 'fact' }, { 'user-id': 'u1' }, { '2024data': 'x' }, or keys built from arbitrary metadata field names written at add() time.","commonSituations":"Storing metadata keys copied from JSON APIs (dots in field names), using camelCase keys with special prefixes, or filtering on keys like 'run_id#2'. Often surfaces after data was ingested with arbitrary metadata keys.","solutions":["Rename the metadata key to snake_case/alphanumeric (letters, digits, underscore, starting with a letter or underscore) both in stored payloads and in the search filters","Re-ingest the memories with normalized metadata keys, then filter on the new keys","If keys come from external schemas, sanitize them at write time with a shared normalizeKey() function so reads and writes agree"],"exampleFix":"// before\nconst results = await memory.search('q', { filters: { 'user-id': 'u1' } });\n\n// after\nconst results = await memory.search('q', { filters: { user_id: 'u1' } });","handlingStrategy":"validation","validationCode":"const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/;\nfunction normalizeFilters(filters: Record<string, any>) {\n  const out: Record<string, any> = {};\n  for (const [k, v] of Object.entries(filters)) {\n    const key = k.replace(/[^a-zA-Z0-9_]/g, '_');\n    if (!/^[a-zA-Z_]/.test(key)) throw new Error(`Filter key '${k}' cannot be normalized safely`);\n    out[key] = v;\n  }\n  return out;\n}","typeGuard":"const isSafeFilterKey = (k: string): boolean =>\n  /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/.test(k);","tryCatchPattern":"try { await memory.search(q, { filters }); } catch (e) { if (e instanceof Error && e.message.includes('Invalid filter key')) { /* rename key, re-run */ } throw e; }","preventionTips":["Enforce a metadata key policy (snake_case identifiers) at memory add() time, not just at search time","Add a lint/unit test asserting every metadata key you write passes the identifier regex","Keep filter keys and stored payload keys in one shared constant/enum"],"tags":["pgvector","search-filters","sql-injection","validation","metadata","typescript"],"backgroundTag":null,"analyzedSha":"001c235229be8795e3834520467bd0d661ed8f34","analyzedAt":"2026-08-15T01:55:42.685Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}