c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
456 lines
12 KiB
Python
456 lines
12 KiB
Python
"""Unit tests for the Postgres graph adapter.
|
|
|
|
Requires a running Postgres instance. Connection defaults:
|
|
DB_HOST=localhost, DB_PORT=5432, DB_USERNAME=cognee,
|
|
DB_PASSWORD=cognee, DB_NAME=cognee_db
|
|
|
|
Override via environment variables if needed.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
from cognee.infrastructure.databases.graph.postgres.adapter import PostgresAdapter
|
|
|
|
|
|
# -- Session-scoped event loop so the async engine's connection pool
|
|
# stays on a single loop across all tests.
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def event_loop():
|
|
import asyncio
|
|
|
|
loop = asyncio.new_event_loop()
|
|
yield loop
|
|
loop.close()
|
|
|
|
|
|
# -- Fixture: real Postgres --
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def adapter():
|
|
"""Create adapter backed by a real Postgres database.
|
|
|
|
Initializes schema, yields the adapter, then cleans up all graph
|
|
tables so tests are isolated.
|
|
"""
|
|
username = os.environ.get("DB_USERNAME", "cognee")
|
|
password = os.environ.get("DB_PASSWORD", "cognee")
|
|
host = os.environ.get("DB_HOST", "localhost")
|
|
port = os.environ.get("DB_PORT", "5432")
|
|
database = os.environ.get("DB_NAME", "cognee_db")
|
|
connection_string = f"postgresql+asyncpg://{username}:{password}@{host}:{port}/{database}"
|
|
a = PostgresAdapter(connection_string=connection_string)
|
|
|
|
# Create tables and indexes
|
|
await a.initialize()
|
|
|
|
# Clean slate before each test
|
|
await a.delete_graph()
|
|
|
|
yield a
|
|
|
|
# Clean up after each test
|
|
await a.delete_graph()
|
|
|
|
|
|
# -- Helpers --
|
|
|
|
|
|
class _FakeDataPoint:
|
|
"""Minimal DataPoint-like object for testing."""
|
|
|
|
def __init__(self, id, name="", type="", **extra):
|
|
self._data = {"id": str(id), "name": name, "type": type, **extra}
|
|
|
|
def model_dump(self):
|
|
return dict(self._data)
|
|
|
|
|
|
# -- Tests: node operations --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_empty_on_fresh_db(adapter):
|
|
assert await adapter.is_empty() is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_and_get_node(adapter):
|
|
node = _FakeDataPoint(id="n1", name="Alice", type="Person", age=30)
|
|
await adapter.add_node(node)
|
|
|
|
result = await adapter.get_node("n1")
|
|
assert result is not None
|
|
assert result["id"] == "n1"
|
|
assert result["name"] == "Alice"
|
|
assert result["type"] == "Person"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_node_string_form(adapter):
|
|
await adapter.add_node("n2", properties={"name": "Bob", "type": "Person"})
|
|
result = await adapter.get_node("n2")
|
|
assert result is not None
|
|
assert result["name"] == "Bob"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_nodes_batch(adapter):
|
|
nodes = [
|
|
_FakeDataPoint(id="a", name="A", type="X"),
|
|
_FakeDataPoint(id="b", name="B", type="Y"),
|
|
_FakeDataPoint(id="c", name="C", type="X"),
|
|
]
|
|
await adapter.add_nodes(nodes)
|
|
|
|
results = await adapter.get_nodes(["a", "b", "c"])
|
|
assert len(results) == 3
|
|
names = {r["name"] for r in results}
|
|
assert names == {"A", "B", "C"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_nodes_upsert(adapter):
|
|
"""add_nodes should update existing nodes, not fail."""
|
|
await adapter.add_nodes([_FakeDataPoint(id="u1", name="V1", type="T")])
|
|
await adapter.add_nodes([_FakeDataPoint(id="u1", name="V2", type="T")])
|
|
|
|
result = await adapter.get_node("u1")
|
|
assert result["name"] == "V2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_empty_after_add(adapter):
|
|
await adapter.add_nodes([_FakeDataPoint(id="x", name="X", type="T")])
|
|
assert await adapter.is_empty() is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_node(adapter):
|
|
await adapter.add_nodes([_FakeDataPoint(id="d1", name="D", type="T")])
|
|
await adapter.delete_node("d1")
|
|
assert await adapter.get_node("d1") is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_nodes_batch(adapter):
|
|
nodes = [_FakeDataPoint(id=f"dn{i}", name=f"N{i}", type="T") for i in range(3)]
|
|
await adapter.add_nodes(nodes)
|
|
await adapter.delete_nodes(["dn0", "dn1", "dn2"])
|
|
results = await adapter.get_nodes(["dn0", "dn1", "dn2"])
|
|
assert len(results) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_nodes_empty_ids(adapter):
|
|
assert await adapter.get_nodes([]) == []
|
|
|
|
|
|
# -- Tests: edge operations --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_and_has_edge(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="e1", name="A", type="T"),
|
|
_FakeDataPoint(id="e2", name="B", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edge("e1", "e2", "KNOWS", {"since": 2020})
|
|
|
|
assert await adapter.has_edge("e1", "e2", "KNOWS") is True
|
|
assert await adapter.has_edge("e1", "e2", "LIKES") is False
|
|
assert await adapter.has_edge("e2", "e1", "KNOWS") is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_edges_batch(adapter):
|
|
nodes = [_FakeDataPoint(id=f"be{i}", name=f"N{i}", type="T") for i in range(3)]
|
|
await adapter.add_nodes(nodes)
|
|
|
|
edges = [
|
|
("be0", "be1", "R1", {"w": 1}),
|
|
("be1", "be2", "R2", {"w": 2}),
|
|
("be0", "be2", "R3", {}),
|
|
]
|
|
await adapter.add_edges(edges)
|
|
|
|
existing = await adapter.has_edges(
|
|
[
|
|
("be0", "be1", "R1"),
|
|
("be1", "be2", "R2"),
|
|
("be0", "be2", "R3"),
|
|
("be2", "be0", "R1"), # does not exist
|
|
]
|
|
)
|
|
assert len(existing) == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_edges_upsert(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="eu1", name="A", type="T"),
|
|
_FakeDataPoint(id="eu2", name="B", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edges([("eu1", "eu2", "R", {"v": 1})])
|
|
await adapter.add_edges([("eu1", "eu2", "R", {"v": 2})])
|
|
|
|
# Should not fail, and edge should be updated (not duplicated)
|
|
existing = await adapter.has_edges([("eu1", "eu2", "R")])
|
|
assert len(existing) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_edges(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="ge1", name="A", type="T"),
|
|
_FakeDataPoint(id="ge2", name="B", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edge("ge1", "ge2", "LINKS")
|
|
|
|
edges = await adapter.get_edges("ge1")
|
|
assert len(edges) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cascade_delete(adapter):
|
|
"""Deleting a node should cascade-delete its edges."""
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="cd1", name="A", type="T"),
|
|
_FakeDataPoint(id="cd2", name="B", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edge("cd1", "cd2", "R")
|
|
await adapter.delete_node("cd1")
|
|
|
|
assert await adapter.has_edge("cd1", "cd2", "R") is False
|
|
|
|
|
|
# -- Tests: neighbor and connection queries --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_neighbors(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="nb1", name="Center", type="T"),
|
|
_FakeDataPoint(id="nb2", name="Left", type="T"),
|
|
_FakeDataPoint(id="nb3", name="Right", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edges(
|
|
[
|
|
("nb1", "nb2", "R1", {}),
|
|
("nb3", "nb1", "R2", {}),
|
|
]
|
|
)
|
|
|
|
neighbors = await adapter.get_neighbors("nb1")
|
|
neighbor_ids = {n["id"] for n in neighbors}
|
|
assert neighbor_ids == {"nb2", "nb3"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_connections(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="cn1", name="A", type="T"),
|
|
_FakeDataPoint(id="cn2", name="B", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edge("cn1", "cn2", "LINKED")
|
|
|
|
connections = await adapter.get_connections("cn1")
|
|
assert len(connections) == 1
|
|
src, edge, tgt = connections[0]
|
|
assert edge["relationship_name"] == "LINKED"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_neighborhood_with_asyncpg_seed_array(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
("nh1", {"name": "A", "type": "Entity"}),
|
|
("nh2", {"name": "B", "type": "Entity"}),
|
|
]
|
|
)
|
|
await adapter.add_edges([("nh1", "nh2", "next", {})])
|
|
|
|
nodes, edges = await adapter.get_neighborhood(["nh1"], depth=1)
|
|
|
|
assert {node_id for node_id, _ in nodes} == {"nh1", "nh2"}
|
|
assert ("nh1", "nh2", "next", {}) in edges
|
|
|
|
|
|
# -- Tests: graph-wide reads --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_graph_data(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="gd1", name="A", type="T"),
|
|
_FakeDataPoint(id="gd2", name="B", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edge("gd1", "gd2", "R")
|
|
|
|
nodes, edges = await adapter.get_graph_data()
|
|
assert len(nodes) == 2
|
|
assert len(edges) == 1
|
|
assert edges[0][2] == "R"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_graph_data_empty(adapter):
|
|
nodes, edges = await adapter.get_graph_data()
|
|
assert nodes == []
|
|
assert edges == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_nodeset_subgraph(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="ns1", name="Alpha", type="Entity"),
|
|
_FakeDataPoint(id="ns2", name="Beta", type="Entity"),
|
|
_FakeDataPoint(id="ns3", name="Gamma", type="Other"),
|
|
]
|
|
)
|
|
await adapter.add_edges(
|
|
[
|
|
("ns1", "ns2", "R", {}),
|
|
("ns1", "ns3", "R", {}),
|
|
]
|
|
)
|
|
|
|
# Create a fake type with __name__ == "Entity"
|
|
class Entity:
|
|
pass
|
|
|
|
nodes, edges = await adapter.get_nodeset_subgraph(Entity, ["Alpha"])
|
|
node_ids = {n[0] for n in nodes}
|
|
|
|
# Should include ns1 (primary) plus neighbors ns2 and ns3
|
|
assert "ns1" in node_ids
|
|
assert len(node_ids) >= 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_filtered_graph_data(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="fg1", name="A", type="X"),
|
|
_FakeDataPoint(id="fg2", name="B", type="Y"),
|
|
_FakeDataPoint(id="fg3", name="C", type="X"),
|
|
]
|
|
)
|
|
await adapter.add_edge("fg1", "fg3", "R")
|
|
|
|
nodes, edges = await adapter.get_filtered_graph_data([{"type": ["X"]}])
|
|
node_ids = {n[0] for n in nodes}
|
|
assert node_ids == {"fg1", "fg3"}
|
|
assert len(edges) == 1
|
|
|
|
|
|
# -- Tests: metrics --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_graph_metrics_basic(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="m1", name="A", type="T"),
|
|
_FakeDataPoint(id="m2", name="B", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edge("m1", "m2", "R")
|
|
|
|
metrics = await adapter.get_graph_metrics()
|
|
assert metrics["num_nodes"] == 2
|
|
assert metrics["num_edges"] == 1
|
|
|
|
|
|
# -- Tests: delete_graph --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_graph(adapter):
|
|
await adapter.add_nodes([_FakeDataPoint(id="dg1", name="A", type="T")])
|
|
await adapter.add_nodes([_FakeDataPoint(id="dg2", name="B", type="T")])
|
|
await adapter.add_edge("dg1", "dg2", "R")
|
|
|
|
await adapter.delete_graph()
|
|
assert await adapter.is_empty() is True
|
|
|
|
|
|
# -- Tests: triplets --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_triplets_batch(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="t1", name="Start", type="T"),
|
|
_FakeDataPoint(id="t2", name="End", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edge("t1", "t2", "CONNECTS")
|
|
|
|
triplets = await adapter.get_triplets_batch(offset=0, limit=10)
|
|
assert len(triplets) == 1
|
|
assert triplets[0]["start_node"]["name"] == "Start"
|
|
assert triplets[0]["end_node"]["name"] == "End"
|
|
assert triplets[0]["relationship_properties"]["relationship_name"] == "CONNECTS"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_triplets_batch_offset(adapter):
|
|
await adapter.add_nodes(
|
|
[
|
|
_FakeDataPoint(id="to1", name="A", type="T"),
|
|
_FakeDataPoint(id="to2", name="B", type="T"),
|
|
_FakeDataPoint(id="to3", name="C", type="T"),
|
|
]
|
|
)
|
|
await adapter.add_edges(
|
|
[
|
|
("to1", "to2", "R1", {}),
|
|
("to1", "to3", "R2", {}),
|
|
]
|
|
)
|
|
|
|
all_triplets = await adapter.get_triplets_batch(offset=0, limit=10)
|
|
assert len(all_triplets) == 2
|
|
|
|
one_triplet = await adapter.get_triplets_batch(offset=1, limit=1)
|
|
assert len(one_triplet) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_triplets_batch_validation(adapter):
|
|
with pytest.raises(ValueError):
|
|
await adapter.get_triplets_batch(offset=-1, limit=10)
|
|
with pytest.raises(ValueError):
|
|
await adapter.get_triplets_batch(offset=0, limit=-1)
|
|
|
|
|
|
# -- Tests: query raises --
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_raises_not_implemented(adapter):
|
|
with pytest.raises(NotImplementedError):
|
|
await adapter.query("MATCH (n) RETURN n")
|