#5031·cognee

graph: get_nodeset_subgraph AND filter, operator validation and edge orientation diverge across graph backends

Author: ptimizeroracleCreated Sep 11, 2026Updated Sep 11, 2026

summary

get_nodeset_subgraph(node_type, node_name, node_name_filter_operator) returns different subgraphs depending on which graph backend is configured. three independent divergences, all reproduced live on embedded backends (no services, no LLM):

  1. the AND filter counts different things per backend: turso counts requested names, ladybug/neo4j/postgres_demo count matched nodes
  2. an invalid operator string (typo like "and") silently means AND on ladybug/turso/neo4j, raises ValueError on postgres_demo
  3. ladybug returns every undirected edge twice (both orientations), the others return it once

environment

  • dev @ bb76f1c2 (v1.5.4), python 3.12.13, macOS arm64
  • defaults: LadybugAdapter on an embedded file db, TursoAdapter on a local sqlite+aiosqlite file
  • no LLM, no embeddings, no services

finding 1: AND denominator divergence

backend counts code
turso HAVING COUNT(DISTINCT primary_id) = :primary_count with primary_count = len(node_name) (requested) cognee/infrastructure/databases/graph/turso/adapter.py:632
ladybug matched_count = $primary_count with primary_count = len(primary_ids) (matched) cognee/infrastructure/databases/graph/ladybug/adapter.py:3122
neo4j matched_count = size(primary) (matched) cognee/infrastructure/databases/graph/neo4j_driver/adapter.py:2087
postgres_demo connections >= primary_ids (matched set) cognee/infrastructure/databases/graph/postgres_demo/adapter.py:166

live result, graph = alice-bob, alice-carol, bob-dave, carol-dave (KNOWS), query get_nodeset_subgraph(Person, ["alice", "ghost"], "AND") where ghost does not exist:

backend nodes edges
ladybug alice, bob, carol 4
turso alice 0

same call with a duplicated name that does exist, ["alice", "alice"]:

backend nodes edges
ladybug alice, bob, carol 4
turso alice 0

so on turso one stale or duplicated name makes AND unsatisfiable and returns a node with no edges, while the other three backends silently degrade the AND to the matched subset and return the full neighborhood. callers doing retrieval over a nodeset cannot get consistent results across backends.

finding 2: operator validation divergence

postgres_demo/adapter.py:153 raises ValueError("node_name_filter_operator must be 'OR' or 'AND'") for anything else. ladybug (== "OR" ... else) and turso (!= "OR") treat any non-"OR" string as AND. neo4j follows the same == "OR" ... else shape.

live, get_nodeset_subgraph(Person, ["alice"], "and") (lowercase typo): ladybug and turso both return the AND neighborhood without complaint, postgres_demo raises ValueError. a typo silently changes semantics on three of four backends.

finding 3: ladybug returns both orientations of each edge

the ladybug edge query unwinds an undirected pattern (a:Node)-[r:EDGE]-(b:Node) over both endpoints (ladybug/adapter.py:3154), and the python-side filter keeps both (u,v) and (v,u).

live, get_nodeset_subgraph(Person, ["alice", "bob"], "AND"):

  • ladybug: [(alice, bob, KNOWS), (bob, alice, KNOWS)]
  • turso: [(alice, bob, KNOWS)]

consumers that count edges or aggregate over them double-count on ladybug. result was stable across repeated runs.

repro

python
import asyncio, os, tempfile
from uuid import UUID
from pydantic import Field
from cognee.infrastructure.databases.graph.ladybug.adapter import LadybugAdapter
from cognee.infrastructure.databases.graph.turso.adapter import TursoAdapter
from cognee.infrastructure.engine import DataPoint

class Person(DataPoint):
    name: str
    type: str = "Person"
    id: UUID = Field()

def make_nodes():
    ids = {"alice": UUID(int=1), "bob": UUID(int=2), "carol": UUID(int=3), "dave": UUID(int=4)}
    return [Person(id=ids[n], name=n) for n in ids]

NODE_IDS = {n.name: str(n.id) for n in make_nodes()}
EDGES = [(NODE_IDS["alice"], NODE_IDS["bob"]), (NODE_IDS["alice"], NODE_IDS["carol"]),
         (NODE_IDS["bob"], NODE_IDS["dave"]), (NODE_IDS["carol"], NODE_IDS["dave"])]

async def build_ladybug(path):
    a = LadybugAdapter(path)
    await a.add_nodes(make_nodes())
    for s, t in EDGES:
        await a.add_edge(s, t, "KNOWS", {"weight": 1.0})
    return a

async def build_turso(uri):
    a = TursoAdapter(uri)
    await a.initialize()
    await a.add_nodes(make_nodes())
    for s, t in EDGES:
        await a.add_edge(s, t, "KNOWS", {"weight": 1.0})
    return a

async def main():
    tmp = tempfile.mkdtemp(prefix="cognee_probe_")
    lady = await build_ladybug(os.path.join(tmp, "ladybug_db"))
    turs = await build_turso(f"sqlite+aiosqlite:///{tmp}/turso.db")
    for label, names, op in [
        ("missing-name AND", ["alice", "ghost"], "AND"),
        ("duplicate-name AND", ["alice", "alice"], "AND"),
        ("sanity AND", ["alice", "bob"], "AND"),
        ("typo operator", ["alice"], "and"),
    ]:
        print(f"== {label} ==")
        for tag, adapter in [("ladybug", lady), ("turso", turs)]:
            try:
                nodes, edges = await adapter.get_nodeset_subgraph(Person, names, op)
                names_out = sorted(n[1].get("name", "?") for n in nodes)
                print(f"  {tag}: nodes={names_out} edges={len(edges)}")
            except Exception as e:
                print(f"  {tag}: RAISED {type(e).__name__}: {e}")
    await lady.close(); await turs.close()

asyncio.run(main())

impact

get_nodeset_subgraph feeds subgraph-shaped retrieval contexts. with the same data and the same call, the returned context depends on graph_database_provider: turso silently drops the neighborhood when a name is stale or duplicated, ladybug doubles every edge, and a typo'd operator changes semantics on three backends while erroring on the fourth.

open questions for maintainers

  • should the AND denominator be requested names (then ladybug/neo4j/postgres_demo need to change) or matched nodes (then turso needs to change)?
  • should unknown operators raise everywhere (postgres_demo behavior) or be normalized?
  • should the returned edge list be orientation-deduplicated on ladybug?

happy to work on any of the three once the intended contract is settled.