Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

455 lines
15 KiB
Python

"""Unit tests for the Postgres hybrid adapter.
Requires a running Postgres instance with pgvector extension.
Connection defaults: DB_HOST=localhost, DB_PORT=5432,
DB_USERNAME=cognee, DB_PASSWORD=cognee, DB_NAME=cognee_db.
"""
import os
import pytest
import pytest_asyncio
from cognee.infrastructure.databases.graph.postgres.adapter import PostgresAdapter
from cognee.infrastructure.databases.vector.pgvector.PGVectorAdapter import PGVectorAdapter
from cognee.infrastructure.databases.vector.embeddings import get_embedding_engine
from cognee.infrastructure.databases.hybrid.postgres.adapter import PostgresHybridAdapter
# -- Session-scoped event loop so async engines stay on a single loop.
@pytest.fixture(scope="session")
def event_loop():
import asyncio
loop = asyncio.new_event_loop()
yield loop
loop.close()
# -- Fixture --
@pytest_asyncio.fixture
async def adapter():
"""Create hybrid adapter backed by real Postgres with pgvector."""
db_username = os.environ.get("DB_USERNAME", "cognee")
db_password = os.environ.get("DB_PASSWORD", "cognee")
db_host = os.environ.get("DB_HOST", "localhost")
db_port = os.environ.get("DB_PORT", "5432")
db_name = os.environ.get("DB_NAME", "cognee_db")
connection_string = (
f"postgresql+asyncpg://{db_username}:{db_password}@{db_host}:{db_port}/{db_name}"
)
graph_adapter = PostgresAdapter(connection_string=connection_string)
vector_adapter = PGVectorAdapter(
connection_string=connection_string,
api_key=None,
embedding_engine=get_embedding_engine(),
)
# Enable pgvector extension -- normally done by cognee's create_db_and_tables()
# during startup, but this test constructs adapters directly.
from sqlalchemy import text as sa_text
async with graph_adapter.engine.begin() as conn:
await conn.execute(sa_text("CREATE EXTENSION IF NOT EXISTS vector"))
a = PostgresHybridAdapter(
graph_adapter=graph_adapter,
vector_adapter=vector_adapter,
)
await a.initialize()
await a.delete_graph()
yield a
# Clean up graph tables; ignore errors from tables already dropped
try:
await a.delete_graph()
except Exception:
pass
# Drop any vector collection tables created during the test
try:
async with graph_adapter.sessionmaker() as session:
from sqlalchemy import text as sa_text
for table_name in [
"EdgeType_relationship_name",
"TestEntity_name",
"test_hybrid_collection",
]:
await session.execute(sa_text(f'DROP TABLE IF EXISTS "{table_name}" CASCADE'))
await session.commit()
except Exception:
pass
# -- Helpers --
class _FakeDataPoint:
"""Minimal DataPoint-like object for testing (no vector indexing)."""
def __init__(self, id, name="", type="", **extra):
self._data = {"id": str(id), "name": name, "type": type, **extra}
self.id = str(id)
self.metadata = {"index_fields": []}
self.belongs_to_set = []
def model_dump(self):
return dict(self._data)
def model_copy(self):
return _FakeDataPoint(**self._data)
class TestEntity:
"""DataPoint-like object with an indexable 'name' field for vector embedding.
Class name becomes the collection prefix: TestEntity_name.
"""
def __init__(self, id, name="", type="", belongs_to_set=None, **extra):
self._data = {"id": str(id), "name": name, "type": type, **extra}
self.id = str(id)
self.name = name
self.type = type
self.metadata = {"index_fields": ["name"]}
self.belongs_to_set = belongs_to_set or []
def model_dump(self):
return dict(self._data)
def model_copy(self):
cp = TestEntity(**self._data)
cp.metadata = dict(self.metadata)
return cp
# -- Tests: graph delegation --
@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="h1", name="Alice", type="Person")
await adapter.add_node(node)
result = await adapter.get_node("h1")
assert result is not None
assert result["name"] == "Alice"
@pytest.mark.asyncio
async def test_add_nodes_and_edges(adapter):
nodes = [
_FakeDataPoint(id="ha", name="A", type="T"),
_FakeDataPoint(id="hb", name="B", type="T"),
]
await adapter.add_nodes(nodes)
await adapter.add_edge("ha", "hb", "KNOWS")
assert await adapter.has_edge("ha", "hb", "KNOWS") is True
neighbors = await adapter.get_neighbors("ha")
assert len(neighbors) == 1
assert neighbors[0]["id"] == "hb"
@pytest.mark.asyncio
async def test_get_graph_data(adapter):
await adapter.add_nodes(
[
_FakeDataPoint(id="hg1", name="X", type="T"),
_FakeDataPoint(id="hg2", name="Y", type="T"),
]
)
await adapter.add_edge("hg1", "hg2", "R")
nodes, edges = await adapter.get_graph_data()
assert len(nodes) == 2
assert len(edges) == 1
@pytest.mark.asyncio
async def test_delete_graph(adapter):
await adapter.add_nodes([_FakeDataPoint(id="hd1", name="Z", type="T")])
await adapter.delete_graph()
assert await adapter.is_empty() is True
# -- Tests: vector delegation --
@pytest.mark.asyncio
async def test_has_collection_false(adapter):
assert await adapter.has_collection("nonexistent_collection_xyz") is False
@pytest.mark.asyncio
async def test_embed_data(adapter):
"""Vector delegation: embedding engine is accessible."""
vectors = await adapter.embed_data(["hello world"])
assert len(vectors) == 1
assert len(vectors[0]) > 0
# -- 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")
# -- Tests: combined write methods --
# Note: tests that create vector collections (add_edges_with_vectors,
# add_nodes_with_vectors with index fields) are grouped at the end
# because PGVectorAdapter's connection pool has ordering sensitivity
# when creating/dropping tables across tests.
@pytest.mark.asyncio
async def test_add_nodes_with_vectors_graph_only(adapter):
"""Data points with no index_fields should still insert into graph."""
nodes = [
_FakeDataPoint(id="nv1", name="NoVec", type="T"),
_FakeDataPoint(id="nv2", name="NoVec2", type="T"),
]
await adapter.add_nodes_with_vectors(nodes)
result = await adapter.get_nodes(["nv1", "nv2"])
assert len(result) == 2
# -- Tests: combined delete methods --
@pytest.mark.asyncio
async def test_delete_nodes_with_vectors(adapter):
await adapter.add_nodes(
[
_FakeDataPoint(id="dn1", name="A", type="T"),
_FakeDataPoint(id="dn2", name="B", type="T"),
]
)
await adapter.add_edge("dn1", "dn2", "R")
await adapter.delete_nodes_with_vectors(["dn1", "dn2"])
assert await adapter.get_node("dn1") is None
assert await adapter.get_node("dn2") is None
assert await adapter.has_edge("dn1", "dn2", "R") is False
# -- Tests: content verification across graph and vector --
@pytest.mark.asyncio
async def test_combined_write_content_integrity(adapter):
"""Verify that data written via combined methods is stored correctly
in both graph and vector tables, with matching IDs and intact contents.
"""
from uuid import uuid5, NAMESPACE_OID
# Generate stable UUIDs from names (pgvector tables require UUID IDs)
id1 = str(uuid5(NAMESPACE_OID, "Quantum Computing"))
id2 = str(uuid5(NAMESPACE_OID, "Machine Learning"))
id3 = str(uuid5(NAMESPACE_OID, "Neural Networks"))
# Add nodes with indexable fields (will create TestEntity_name collection)
nodes = [
TestEntity(id=id1, name="Quantum Computing", type="Topic"),
TestEntity(id=id2, name="Machine Learning", type="Topic"),
TestEntity(id=id3, name="Neural Networks", type="Topic"),
]
await adapter.add_nodes_with_vectors(nodes)
# Add edges with properties
edges = [
(id1, id2, "RELATED_TO", {"weight": 0.8, "edge_text": "quantum ML"}),
(id2, id3, "CONTAINS", {"weight": 0.9, "edge_text": "neural nets in ML"}),
]
await adapter.add_edges_with_vectors(edges)
# -- Verify graph node contents --
node1 = await adapter.get_node(id1)
assert node1["id"] == id1
assert node1["name"] == "Quantum Computing"
assert node1["type"] == "Topic"
node3 = await adapter.get_node(id3)
assert node3["name"] == "Neural Networks"
# -- Verify graph edge contents --
connections = await adapter.get_connections(id1)
rel_names = {conn[1]["relationship_name"] for conn in connections}
assert "RELATED_TO" in rel_names
# Verify edge properties preserved
for _, edge, _ in connections:
if edge["relationship_name"] == "RELATED_TO":
assert edge.get("weight") == 0.8
assert edge.get("edge_text") == "quantum ML"
# -- Verify graph data completeness --
all_nodes, all_edges = await adapter.get_graph_data()
node_ids = {n[0] for n in all_nodes}
assert node_ids == {id1, id2, id3}
assert len(all_edges) == 2
# -- Verify vector collection was created --
assert await adapter.has_collection("TestEntity_name") is True
assert await adapter.has_collection("EdgeType_relationship_name") is True
# -- Verify vector entries have correct IDs --
vector_results = await adapter.retrieve("TestEntity_name", [id1, id2, id3])
vector_ids = {str(r.id) for r in vector_results}
assert id1 in vector_ids
assert id2 in vector_ids
assert id3 in vector_ids
# -- Verify vector payloads contain original data --
for result in vector_results:
assert result.payload is not None
assert "text" in result.payload
if str(result.id) == id1:
assert result.payload["text"] == "Quantum Computing"
elif str(result.id) == id3:
assert result.payload["text"] == "Neural Networks"
# -- Verify vector search returns relevant results --
search_results = await adapter.search(
"TestEntity_name",
query_text="quantum physics computing",
limit=3,
include_payload=True,
)
assert len(search_results) > 0
# The closest result should be "Quantum Computing"
assert search_results[0].payload["text"] == "Quantum Computing"
@pytest.mark.asyncio
async def test_hybrid_payload_matches_separate_path(adapter):
"""Vector payloads from add_nodes_with_vectors + add_edges_with_vectors
must be identical to what the separate add_nodes + index_data_points
and add_edges + index_data_points paths produce."""
from uuid import uuid5, NAMESPACE_OID
# Build two parallel graphs with the same structure but different IDs
def make_ids(prefix):
return {k: str(uuid5(NAMESPACE_OID, f"{prefix}_{k}")) for k in ("physics", "math", "cs")}
h = make_ids("hybrid")
s = make_ids("separate")
def make_nodes(ids):
return [
TestEntity(id=ids["physics"], name="Quantum Physics", type="Science"),
TestEntity(id=ids["math"], name="Linear Algebra", type="Mathematics"),
TestEntity(id=ids["cs"], name="Machine Learning", type="Computer Science"),
]
def make_edges(ids):
return [
(ids["physics"], ids["math"], "REQUIRES", {"weight": 0.9}),
(ids["math"], ids["cs"], "ENABLES", {"weight": 0.7}),
(ids["physics"], ids["cs"], "INSPIRES", {"weight": 0.5}),
]
# -- Path A: hybrid --
await adapter.add_nodes_with_vectors(make_nodes(h))
await adapter.add_edges_with_vectors(make_edges(h))
# -- Path B: separate --
sep_nodes = make_nodes(s)
await adapter._graph.add_nodes(sep_nodes)
await adapter._vector.index_data_points("TestEntity", "name", sep_nodes)
sep_edges = make_edges(s)
await adapter._graph.add_edges(sep_edges)
from cognee.tasks.storage.index_graph_edges import index_graph_edges
await index_graph_edges(sep_edges, vector_engine=adapter._vector)
# -- Compare node payloads --
hybrid_node_results = await adapter.retrieve("TestEntity_name", list(h.values()))
separate_node_results = await adapter.retrieve("TestEntity_name", list(s.values()))
assert len(hybrid_node_results) == 3
assert len(separate_node_results) == 3
# Sort by text so we compare matching nodes
hybrid_node_results.sort(key=lambda r: r.payload["text"])
separate_node_results.sort(key=lambda r: r.payload["text"])
skip_keys = {"id", "created_at", "updated_at"}
for h_res, s_res in zip(hybrid_node_results, separate_node_results):
assert set(h_res.payload.keys()) == set(s_res.payload.keys()), (
f"node key mismatch: hybrid={set(h_res.payload.keys())} "
f"separate={set(s_res.payload.keys())}"
)
for key in h_res.payload:
if key in skip_keys:
continue
assert h_res.payload[key] == s_res.payload[key], (
f"node key {key!r}: hybrid={h_res.payload[key]!r} "
f"!= separate={s_res.payload[key]!r}"
)
# -- Compare edge type payloads --
hybrid_edge_results = await adapter.search(
"EdgeType_relationship_name",
query_text="REQUIRES ENABLES INSPIRES",
limit=10,
include_payload=True,
)
# Both paths write to the same EdgeType collection; verify at least 3 types
edge_texts = {r.payload["text"] for r in hybrid_edge_results}
assert {"REQUIRES", "ENABLES", "INSPIRES"} <= edge_texts
# -- Tests: combined writes with vector collections (run last) --
@pytest.mark.asyncio
async def test_add_edges_with_vectors(adapter):
"""Edges should be inserted into graph and edge types into vector table."""
await adapter.add_nodes(
[
_FakeDataPoint(id="ev1", name="A", type="T"),
_FakeDataPoint(id="ev2", name="B", type="T"),
_FakeDataPoint(id="ev3", name="C", type="T"),
]
)
edges = [
("ev1", "ev2", "KNOWS", {}),
("ev2", "ev3", "LIKES", {}),
("ev1", "ev3", "KNOWS", {}),
]
await adapter.add_edges_with_vectors(edges)
# Graph edges present
assert await adapter.has_edge("ev1", "ev2", "KNOWS") is True
assert await adapter.has_edge("ev2", "ev3", "LIKES") is True
# EdgeType vector collection should exist
assert await adapter.has_collection("EdgeType_relationship_name") is True
@pytest.mark.asyncio
async def test_delete_graph_clears_nodes_and_edges(adapter):
"""Graph-side prune: truncate graph tables."""
await adapter.add_nodes([_FakeDataPoint(id="pa1", name="A", type="T")])
await adapter.add_nodes([_FakeDataPoint(id="pa2", name="B", type="T")])
await adapter.add_edge("pa1", "pa2", "R")
await adapter.delete_graph()
assert await adapter.is_empty() is True
assert await adapter.has_edge("pa1", "pa2", "R") is False