mem0ai/mem0 · error · Error

Oracle filter operator '${operator}' requires a string value

Error message

Oracle filter operator '${operator}' requires a string value

What it means

The remaining string operators, contains and icontains, compile to '@ has substring' / '@.lower() has substring' JSON path predicates, which only work on string bind values. After eq/ne/in/nin are handled, any other operator reaching the string branch with a non-string operand is rejected.

Source

Thrown at mem0-ts/src/oss/src/vector_stores/oracledb.ts:223

        }
        const [variable, passing] = bindFilterValue(item, binds);
        variables.push(variable);
        listPassings.push(passing);
      }

      const membership = jsonExists(
        path,
        `@ in (${variables.join(", ")})`,
        listPassings,
      );
      additionalClauses.push(
        operator === "in" ? membership : `NOT (${membership})`,
      );
      continue;
    }

    if (typeof operand !== "string") {
      throw new Error(
        `Oracle filter operator '${operator}' requires a string value`,
      );
    }

    if (operator === "contains") {
      const [variable, passing] = bindFilterValue(operand, binds);
      predicates.push(`@ has substring ${variable}`);
      passings.push(passing);
    } else {
      const [variable, passing] = bindFilterValue(operand.toLowerCase(), binds);
      predicates.push(`@.lower() has substring ${variable}`);
      passings.push(passing);
    }
  }

  const clauses = [...additionalClauses];
  if (predicates.length > 0) {
    clauses.unshift(jsonExists(path, predicates.join(" && "), passings));

View on GitHub (pinned to 001c235229)

Solutions

  1. Convert the operand to a string: { name: { contains: String(term) } }.
  2. Use eq/in for exact matching of numbers instead of contains.
  3. Reject or coerce non-string inputs at the API boundary before building filters.
  4. For icontains, still pass a string; lowercasing of both sides is done by the adapter.

Example fix

// before
filters = { name: { contains: searchTerm } }; // searchTerm may be a number

// after
filters = { name: { contains: String(searchTerm) } };
Defensive patterns

Strategy: validation

Validate before calling

function coerceContains(filters: any): any {
  if (Array.isArray(filters)) return filters.map(coerceContains);
  if (!filters || typeof filters !== 'object') return filters;
  const out: Record<string, any> = {};
  for (const [k, v] of Object.entries(filters)) {
    if (v && typeof v === 'object' && !Array.isArray(v) && ('contains' in v || 'icontains' in v)) {
      const ops: Record<string, any> = {};
      for (const [op, val] of Object.entries(v)) {
        if (op === 'contains' || op === 'icontains') {
          if (val === null || val === undefined) continue;
          ops[op] = typeof val === 'string' ? val : String(val);
        } else ops[op] = val;
      }
      if (Object.keys(ops).length) out[k] = ops;
    } else out[k] = v;
  }
  return out;
}

Type guard

const isStringFilter = (v: unknown): v is string => typeof v === 'string';

Prevention

When it happens

Trigger: { name: { contains: 42 } } (number), { name: { icontains: ['a','b'] } } (array), { name: { contains: null } } — null is typeof 'object' and also fails the string check.

Common situations: Template-driven filter builders that concatenate whatever value the client sent into a contains clause; auto-complete/search boxes that submit numbers; forgetting to String() a numeric search term.

Related errors


AI-assisted analysis of mem0ai/mem0@001c235229 (2026-08-15). Data as JSON: /api/errors/6400edcb84451db8. Report an issue: GitHub.