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"]);
+ });
+});