From cc4671579f16e05c5599bb97aa436464f61240f4 Mon Sep 17 00:00:00 2001 From: Abhay Singh Date: Fri, 24 Jul 2026 18:26:10 +0530 Subject: [PATCH] fix(ts-oss/cassandra): apply every operator in a compound field filter (#6511) --- .../src/oss/src/vector_stores/cassandra.ts | 54 +++++++++++----- mem0-ts/src/oss/tests/cassandra.unit.test.ts | 63 +++++++++++++++++++ 2 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 mem0-ts/src/oss/tests/cassandra.unit.test.ts diff --git a/mem0-ts/src/oss/src/vector_stores/cassandra.ts b/mem0-ts/src/oss/src/vector_stores/cassandra.ts index ccd69c52c..6a28fe2e9 100644 --- a/mem0-ts/src/oss/src/vector_stores/cassandra.ts +++ b/mem0-ts/src/oss/src/vector_stores/cassandra.ts @@ -455,44 +455,64 @@ export class CassandraDB implements VectorStore { return value.includes(payloadValue); } + // Every operator present in a compound condition must hold (AND), so check + // them all instead of returning on the first match. Returning early meant a + // range like { gte: 10, lte: 20 } only applied `gte`. Mirrors the databricks + // store's matcher. + let sawOperator = false; + if ("eq" in value) { - return payloadValue === value.eq; + sawOperator = true; + if (payloadValue !== value.eq) return false; } if ("ne" in value) { - return payloadValue !== value.ne; + sawOperator = true; + if (payloadValue === value.ne) return false; } if ("gt" in value) { - return payloadValue > value.gt; + sawOperator = true; + if (!(payloadValue > value.gt)) return false; } if ("gte" in value) { - return payloadValue >= value.gte; + sawOperator = true; + if (!(payloadValue >= value.gte)) return false; } if ("lt" in value) { - return payloadValue < value.lt; + sawOperator = true; + if (!(payloadValue < value.lt)) return false; } if ("lte" in value) { - return payloadValue <= value.lte; + sawOperator = true; + if (!(payloadValue <= value.lte)) return false; } if ("in" in value) { - return Array.isArray(value.in) && value.in.includes(payloadValue); + sawOperator = true; + if (!Array.isArray(value.in) || !value.in.includes(payloadValue)) + return false; } if ("nin" in value) { - return !Array.isArray(value.nin) || !value.nin.includes(payloadValue); + sawOperator = true; + if (Array.isArray(value.nin) && value.nin.includes(payloadValue)) + return false; } if ("contains" in value) { - return ( - typeof payloadValue === "string" && - payloadValue.includes(value.contains) - ); + sawOperator = true; + if ( + typeof payloadValue !== "string" || + !payloadValue.includes(value.contains) + ) + return false; } if ("icontains" in value) { - return ( - typeof payloadValue === "string" && - payloadValue.toLowerCase().includes(value.icontains.toLowerCase()) - ); + sawOperator = true; + if ( + typeof payloadValue !== "string" || + !payloadValue.toLowerCase().includes(value.icontains.toLowerCase()) + ) + return false; } - return payloadValue === value; + return sawOperator ? true : payloadValue === value; } private filterVector( diff --git a/mem0-ts/src/oss/tests/cassandra.unit.test.ts b/mem0-ts/src/oss/tests/cassandra.unit.test.ts new file mode 100644 index 000000000..2cd95a06d --- /dev/null +++ b/mem0-ts/src/oss/tests/cassandra.unit.test.ts @@ -0,0 +1,63 @@ +/// +/** + * Cassandra vector store — filter matching unit tests. + * + * Cassandra has no server-side metadata filter, so search()/list() scan rows + * and apply filters in-app via matchFieldCondition(). These tests drive that + * matcher through the public list() API with an injected fake client. + */ +import { CassandraDB } from "../src/vector_stores/cassandra"; + +type Row = { id: string; payload: Record }; + +// Minimal fake driver: CREATE statements during initialize() return nothing; +// a SELECT returns the seeded rows in one page (pageState undefined => stop). +function fakeClient(rows: Row[]) { + return { + async connect() {}, + async execute(query: string) { + if (/^\s*SELECT/i.test(query)) { + return { rows, pageState: undefined }; + } + return { rows: [], pageState: undefined }; + }, + async shutdown() {}, + }; +} + +function makeStore(rows: Row[]) { + return new CassandraDB({ + keyspace: "mem0", + collectionName: "mem0", + dimension: 3, + client: fakeClient(rows) as any, + } as any); +} + +describe("CassandraDB filter matching", () => { + const rows: Row[] = [ + { id: "a", payload: { data: "a", age: 5 } }, + { id: "b", payload: { data: "b", age: 15 } }, + { id: "c", payload: { data: "c", age: 25 } }, + ]; + + it("applies every operator in a compound range filter (not just the first)", async () => { + const store = makeStore(rows); + // age in [10, 20]: only "b" (15) qualifies. The old matcher returned on the + // first operator (gte), so "c" (25) leaked through because lte was ignored. + const [results] = await store.list({ age: { gte: 10, lte: 20 } }); + expect(results.map((r) => r.id)).toEqual(["b"]); + }); + + it("still matches a single-operator filter", async () => { + const store = makeStore(rows); + const [results] = await store.list({ age: { gte: 15 } }); + expect(results.map((r) => r.id).sort()).toEqual(["b", "c"]); + }); + + it("treats a plain equality filter as before", async () => { + const store = makeStore(rows); + const [results] = await store.list({ age: 15 }); + expect(results.map((r) => r.id)).toEqual(["b"]); + }); +});