mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
deletes for repositories
This commit is contained in:
@@ -61,3 +61,18 @@ dev-dependencies = [
|
||||
|
||||
[tool.uv.sources]
|
||||
basic-foundation = { path = "../basic-foundation", editable = true }
|
||||
|
||||
[tool.pyright]
|
||||
include = [
|
||||
"src/",
|
||||
]
|
||||
exclude = [
|
||||
"**/__pycache__",
|
||||
]
|
||||
ignore = [
|
||||
"test/",
|
||||
]
|
||||
defineConstant = { DEBUG = true }
|
||||
reportMissingImports = "error"
|
||||
reportMissingTypeStubs = false
|
||||
pythonVersion = "3.12"
|
||||
|
||||
@@ -68,10 +68,10 @@ async def create_relations(
|
||||
|
||||
@router.delete("/relations/{from_id:path}/{to_id:path}", response_model=DeleteEntityResponse)
|
||||
async def delete_relation(
|
||||
memory_service: MemoryServiceDep,
|
||||
from_id: str,
|
||||
to_id: str,
|
||||
relation_type: str | None = None,
|
||||
memory_service: MemoryServiceDep
|
||||
) -> DeleteEntityResponse:
|
||||
"""Delete relations between entities, optionally filtered by type."""
|
||||
request = DeleteRelationRequest(from_id=from_id, to_id=to_id, relation_type=relation_type)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Base repository implementation."""
|
||||
from typing import Type, Optional, Any, Sequence, TypeVar, List
|
||||
from sqlalchemy import select, func, Select, Executable, inspect, Result, Column, insert
|
||||
from typing import Type, Optional, Any, Sequence, TypeVar, List, Dict
|
||||
from sqlalchemy import select, func, Select, Executable, inspect, Result, Column, insert, and_, delete
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped
|
||||
@@ -22,6 +22,17 @@ class Repository[T: Base]:
|
||||
|
||||
logger.debug(f"Initialized {self.__class__.__name__} for {Model.__name__}")
|
||||
logger.debug(f"Valid columns: {self.valid_columns}")
|
||||
|
||||
def select(self, *entities: Any) -> Select:
|
||||
"""Create a new SELECT statement.
|
||||
|
||||
Returns:
|
||||
A SQLAlchemy Select object configured with the provided entities
|
||||
or this repository's model if no entities provided.
|
||||
"""
|
||||
if not entities:
|
||||
entities = (self.Model,)
|
||||
return select(*entities)
|
||||
|
||||
async def refresh(self, instance: T, relationships: list[str] | None = None) -> None:
|
||||
"""Refresh instance and optionally specified relationships."""
|
||||
@@ -162,6 +173,29 @@ class Repository[T: Base]:
|
||||
logger.exception(f"Failed to delete {self.Model.__name__}: {entity_id}")
|
||||
raise
|
||||
|
||||
async def delete_by_fields(self, **filters: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Delete records matching given field values.
|
||||
|
||||
Args:
|
||||
**filters: Field names and values to filter by
|
||||
|
||||
Returns:
|
||||
bool: True if any records were deleted
|
||||
"""
|
||||
logger.debug(f"Deleting {self.Model.__name__} by fields: {filters}")
|
||||
try:
|
||||
conditions = [getattr(self.Model, field) == value for field, value in filters.items()]
|
||||
query = delete(self.Model).where(and_(*conditions))
|
||||
result = await self.execute_query(query)
|
||||
await self.session.flush()
|
||||
deleted = result.rowcount > 0
|
||||
logger.debug(f"Deleted {result.rowcount} records")
|
||||
return deleted # pyright: ignore [reportAttributeAccessIssue]
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to delete {self.Model.__name__} by fields")
|
||||
raise
|
||||
|
||||
async def count(self, query: Executable | None = None) -> int:
|
||||
"""Count entities in the database table."""
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Repository for managing Observation objects."""
|
||||
from typing import Sequence, Dict, Any
|
||||
from sqlalchemy import select, and_, delete
|
||||
from typing import Sequence
|
||||
from sqlalchemy import select
|
||||
|
||||
from basic_memory.models import Observation
|
||||
from basic_memory.repository import Repository
|
||||
@@ -22,12 +22,4 @@ class ObservationRepository(Repository[Observation]):
|
||||
"""Find observations with a specific context."""
|
||||
query = select(Observation).filter(Observation.context == context)
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def delete_by_fields(self, **filters: Dict[str, Any]) -> bool:
|
||||
"""Delete observations matching the given field values."""
|
||||
conditions = [getattr(Observation, field) == value for field, value in filters.items()]
|
||||
query = delete(Observation).where(and_(*conditions))
|
||||
result = await self.execute_query(query)
|
||||
await self.session.flush()
|
||||
return result.rowcount > 0 # pyright: ignore [reportAttributeAccessIssue]
|
||||
return result.scalars().all()
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Repository for managing Relation objects."""
|
||||
from typing import Sequence, Any, Dict
|
||||
from sqlalchemy import select, and_, delete
|
||||
from typing import Sequence
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
from basic_memory.models import Relation
|
||||
from basic_memory.repository import Repository
|
||||
@@ -27,12 +27,4 @@ class RelationRepository(Repository[Relation]):
|
||||
"""Find all relations of a specific type."""
|
||||
query = select(Relation).filter(Relation.relation_type == relation_type)
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def delete_by_fields(self, **filters: Dict[str, Any]) -> bool:
|
||||
"""Delete relations matching the given field values."""
|
||||
conditions = [getattr(Relation, field) == value for field, value in filters.items()]
|
||||
query = delete(Relation).where(and_(*conditions))
|
||||
result = await self.execute_query(query)
|
||||
await self.session.flush()
|
||||
return result.rowcount > 0
|
||||
return result.scalars().all()
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Service for managing relations in the database."""
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
from basic_memory.schemas import Entity, Relation
|
||||
@@ -46,4 +47,32 @@ class RelationService:
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseSyncError(f"Failed to delete relation: {str(e)}") from e
|
||||
raise DatabaseSyncError(f"Failed to delete relation: {str(e)}") from e
|
||||
|
||||
async def delete_relations(self, relations: List[Dict[str, Any]]) -> bool:
|
||||
"""
|
||||
Delete relations matching specified criteria.
|
||||
|
||||
Args:
|
||||
relations: List of dicts with from_id, to_id, and optional relation_type
|
||||
|
||||
Returns:
|
||||
True if any relations were deleted
|
||||
"""
|
||||
try:
|
||||
deleted = False
|
||||
for relation in relations:
|
||||
filters = {
|
||||
'from_id': relation['from_id'],
|
||||
'to_id': relation['to_id']
|
||||
}
|
||||
if 'relation_type' in relation:
|
||||
filters['relation_type'] = relation['relation_type']
|
||||
|
||||
result = await self.relation_repo.delete_by_fields(**filters)
|
||||
if result:
|
||||
deleted = True
|
||||
|
||||
return deleted
|
||||
except Exception as e:
|
||||
raise DatabaseSyncError(f"Failed to delete relations: {str(e)}") from e
|
||||
@@ -0,0 +1,410 @@
|
||||
"""Tests for the EntityRepository."""
|
||||
from datetime import datetime, UTC
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from basic_memory.models import Entity, Observation, Relation
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def entity_repo(session):
|
||||
"""Create an EntityRepository with test DB session."""
|
||||
return EntityRepository(session)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_entity(session):
|
||||
"""Create a test entity."""
|
||||
entity = Entity(
|
||||
id="test/test_entity",
|
||||
name="test_entity",
|
||||
entity_type="test",
|
||||
description="Test entity"
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
return entity
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def entity_with_observations(session, test_entity):
|
||||
"""Create an entity with observations."""
|
||||
observations = [
|
||||
Observation(entity_id=test_entity.id, content="First observation"),
|
||||
Observation(entity_id=test_entity.id, content="Second observation")
|
||||
]
|
||||
session.add_all(observations)
|
||||
await session.flush()
|
||||
return test_entity
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def related_entities(session):
|
||||
"""Create entities with relations between them."""
|
||||
source = Entity(
|
||||
id="source/test_entity",
|
||||
name="source",
|
||||
entity_type="source",
|
||||
description="Source entity"
|
||||
)
|
||||
target = Entity(
|
||||
id="target/test_entity",
|
||||
name="target",
|
||||
entity_type="target",
|
||||
description="Target entity"
|
||||
)
|
||||
session.add_all([source, target])
|
||||
await session.flush()
|
||||
|
||||
relation = Relation(
|
||||
from_id=source.id,
|
||||
to_id=target.id,
|
||||
relation_type="connects_to"
|
||||
)
|
||||
session.add(relation)
|
||||
await session.flush()
|
||||
|
||||
return source, target, relation
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_entity(entity_repository: EntityRepository):
|
||||
"""Test creating a new entity"""
|
||||
entity_data = {
|
||||
'name': 'Test',
|
||||
'entity_type': 'test',
|
||||
'description': 'Test description',
|
||||
}
|
||||
entity = await entity_repository.create(entity_data)
|
||||
|
||||
# Verify returned object
|
||||
assert entity.id == 'test/test'
|
||||
assert entity.name == 'Test'
|
||||
assert entity.description == 'Test description'
|
||||
assert isinstance(entity.created_at, datetime)
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.id == entity.id
|
||||
assert db_entity.name == entity.name
|
||||
assert db_entity.description == entity.description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entity_type_name_unique_constraint(entity_repository: EntityRepository):
|
||||
"""Test the unique constraint on entity_type + name combination."""
|
||||
# Create first entity
|
||||
entity1_data = {
|
||||
'id': '20240102-test1',
|
||||
'name': 'Test Entity',
|
||||
'entity_type': 'type1',
|
||||
'description': 'First entity'
|
||||
}
|
||||
await entity_repository.create(entity1_data)
|
||||
|
||||
# Try to create another entity with same type and name
|
||||
entity2_data = {
|
||||
'id': '20240102-test2',
|
||||
'name': 'Test Entity', # Same name
|
||||
'entity_type': 'type1', # Same type
|
||||
'description': 'Second entity'
|
||||
}
|
||||
|
||||
# Should raise IntegrityError
|
||||
with pytest.raises(IntegrityError) as exc_info:
|
||||
await entity_repository.create(entity2_data)
|
||||
assert 'UNIQUE constraint failed: entity.entity_type, entity.name' in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_entity_null_description(entity_repository: EntityRepository):
|
||||
"""Test creating an entity with null description"""
|
||||
entity_data = {
|
||||
'id': '20240102-test',
|
||||
'name': 'Test',
|
||||
'entity_type': 'test',
|
||||
'description': None,
|
||||
}
|
||||
entity = await entity_repository.create(entity_data)
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.description is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_by_id(entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test finding an entity by ID"""
|
||||
found = await entity_repository.find_by_id(sample_entity.id)
|
||||
assert found is not None
|
||||
assert found.id == sample_entity.id
|
||||
assert found.name == sample_entity.name
|
||||
|
||||
# Verify against direct database query
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.id == found.id
|
||||
assert db_entity.name == found.name
|
||||
assert db_entity.description == found.description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_by_name(entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test finding an entity by name"""
|
||||
found = await entity_repository.find_by_name(sample_entity.name)
|
||||
assert found is not None
|
||||
assert found.id == sample_entity.id
|
||||
assert found.name == sample_entity.name
|
||||
|
||||
# Verify against direct database query
|
||||
stmt = select(Entity).where(Entity.name == sample_entity.name)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.id == found.id
|
||||
assert db_entity.name == found.name
|
||||
assert db_entity.description == found.description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_entity(entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test updating an entity"""
|
||||
updated = await entity_repository.update(
|
||||
sample_entity.id,
|
||||
{'description': 'Updated description'}
|
||||
)
|
||||
assert updated is not None
|
||||
assert updated.description == 'Updated description'
|
||||
assert updated.name == sample_entity.name # Other fields unchanged
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.description == 'Updated description'
|
||||
assert db_entity.name == sample_entity.name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_entity_to_null(entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test updating an entity's description to null"""
|
||||
updated = await entity_repository.update(
|
||||
sample_entity.id,
|
||||
{'description': None}
|
||||
)
|
||||
assert updated is not None
|
||||
assert updated.description is None
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.description is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_entity_find_by_id(entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test deleting an entity"""
|
||||
success = await entity_repository.delete(sample_entity.id)
|
||||
assert success is True
|
||||
|
||||
# Verify it's gone
|
||||
found = await entity_repository.find_by_id(sample_entity.id)
|
||||
assert found is None
|
||||
|
||||
# Verify with direct query
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
assert result.first() is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search(entity_repository: EntityRepository):
|
||||
"""Test searching entities"""
|
||||
# Create test entities with observations
|
||||
entity1 = await entity_repository.create({
|
||||
'id': '20240102-test1',
|
||||
'name': 'Search Test 1',
|
||||
'entity_type': 'test',
|
||||
'description': 'First test entity'
|
||||
})
|
||||
|
||||
entity2 = await entity_repository.create({
|
||||
'id': '20240102-test2',
|
||||
'name': 'Search Test 2',
|
||||
'entity_type': 'other',
|
||||
'description': 'Second test entity'
|
||||
})
|
||||
|
||||
# Verify entities in database
|
||||
stmt = select(Entity).where(Entity.id.in_([entity1.id, entity2.id]))
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entities = result.scalars().all()
|
||||
assert len(db_entities) == 2
|
||||
|
||||
# Add observations
|
||||
stmt = text("""
|
||||
INSERT INTO observation (entity_id, content, created_at)
|
||||
VALUES (:e1_id, :e1_obs, :ts), (:e2_id, :e2_obs, :ts)
|
||||
""")
|
||||
ts = datetime.now(UTC)
|
||||
await entity_repository.session.execute(stmt, {
|
||||
"e1_id": entity1.id,
|
||||
"e1_obs": "First observation with searchable content",
|
||||
"e2_id": entity2.id,
|
||||
"e2_obs": "Another observation to find",
|
||||
"ts": ts
|
||||
})
|
||||
await entity_repository.session.commit()
|
||||
|
||||
# Test search by name
|
||||
results = await entity_repository.search('Search Test')
|
||||
assert len(results) == 2
|
||||
names = {e.name for e in results}
|
||||
assert 'Search Test 1' in names
|
||||
assert 'Search Test 2' in names
|
||||
|
||||
# Test search by type
|
||||
results = await entity_repository.search('other')
|
||||
assert len(results) == 1
|
||||
assert results[0].entity_type == 'other'
|
||||
|
||||
# Test search by observation content
|
||||
results = await entity_repository.search('searchable')
|
||||
assert len(results) == 1
|
||||
assert results[0].id == entity1.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_by_type_and_name(entity_repository: EntityRepository):
|
||||
"""Test finding an entity by type and name combination."""
|
||||
# Create two entities with same name but different types
|
||||
entity1 = await entity_repository.create({
|
||||
'id': '20240102-test1',
|
||||
'name': 'Test Entity',
|
||||
'entity_type': 'type1',
|
||||
'description': 'First test entity'
|
||||
})
|
||||
|
||||
entity2 = await entity_repository.create({
|
||||
'id': '20240102-test2',
|
||||
'name': 'Test Entity',
|
||||
'entity_type': 'type2',
|
||||
'description': 'Second test entity'
|
||||
})
|
||||
|
||||
# Should find correct entity when both type and name match
|
||||
found = await entity_repository.find_by_type_and_name('type1', 'Test Entity')
|
||||
assert found is not None
|
||||
assert found.id == entity1.id
|
||||
assert found.entity_type == 'type1'
|
||||
assert found.name == 'Test Entity'
|
||||
|
||||
# Should find other entity with same name but different type
|
||||
found = await entity_repository.find_by_type_and_name('type2', 'Test Entity')
|
||||
assert found is not None
|
||||
assert found.id == entity2.id
|
||||
assert found.entity_type == 'type2'
|
||||
assert found.name == 'Test Entity'
|
||||
|
||||
# Should return None when type doesn't match
|
||||
found = await entity_repository.find_by_type_and_name('nonexistent', 'Test Entity')
|
||||
assert found is None
|
||||
|
||||
# Should return None when name doesn't match
|
||||
found = await entity_repository.find_by_type_and_name('type1', 'Nonexistent')
|
||||
assert found is None
|
||||
|
||||
# Verify relationships are loaded
|
||||
entity3 = await entity_repository.create({
|
||||
'id': '20240102-test3',
|
||||
'name': 'Entity With Relations',
|
||||
'entity_type': 'type3',
|
||||
'description': 'Entity with observations and relations'
|
||||
})
|
||||
|
||||
# Add an observation
|
||||
stmt = text("""
|
||||
INSERT INTO observation (entity_id, content, created_at)
|
||||
VALUES (:entity_id, :content, :ts)
|
||||
""")
|
||||
ts = datetime.now(UTC)
|
||||
await entity_repository.session.execute(stmt, {
|
||||
"entity_id": entity3.id,
|
||||
"content": "Test observation",
|
||||
"ts": ts
|
||||
})
|
||||
await entity_repository.session.commit()
|
||||
|
||||
# Find entity and verify relationships are loaded
|
||||
found = await entity_repository.find_by_type_and_name('type3', 'Entity With Relations')
|
||||
assert found is not None
|
||||
assert len(found.observations) == 1
|
||||
assert found.observations[0].content == "Test observation"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_entity(entity_repo, test_entity):
|
||||
"""Test deleting an entity."""
|
||||
result = await entity_repo.delete(test_entity.id)
|
||||
assert result is True
|
||||
|
||||
# Verify deletion
|
||||
deleted = await entity_repo.find_by_id(test_entity.id)
|
||||
assert deleted is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_entity_with_observations(entity_repo, entity_with_observations):
|
||||
"""Test deleting an entity cascades to its observations."""
|
||||
entity = entity_with_observations
|
||||
|
||||
result = await entity_repo.delete(entity.id)
|
||||
assert result is True
|
||||
|
||||
# Verify entity deletion
|
||||
deleted = await entity_repo.find_by_id(entity.id)
|
||||
assert deleted is None
|
||||
|
||||
# Verify observations were cascaded
|
||||
query = select(Observation).filter(Observation.entity_id == entity.id)
|
||||
result = await entity_repo.execute_query(query)
|
||||
remaining_observations = result.scalars().all()
|
||||
assert len(remaining_observations) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_entities_by_type(entity_repo, test_entity):
|
||||
"""Test deleting entities by type."""
|
||||
result = await entity_repo.delete_by_fields(entity_type=test_entity.entity_type)
|
||||
assert result is True
|
||||
|
||||
# Verify deletion
|
||||
query = select(Entity).filter(Entity.entity_type == test_entity.entity_type)
|
||||
result = await entity_repo.execute_query(query)
|
||||
remaining = result.scalars().all()
|
||||
assert len(remaining) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_entity_with_relations(entity_repo, related_entities):
|
||||
"""Test deleting an entity cascades to its relations."""
|
||||
source, target, relation = related_entities
|
||||
|
||||
# Delete source entity
|
||||
result = await entity_repo.delete(source.id)
|
||||
assert result is True
|
||||
|
||||
# Verify relation was cascaded
|
||||
query = select(Relation).filter(Relation.from_id == source.id)
|
||||
result = await entity_repo.execute_query(query)
|
||||
remaining_relations = result.scalars().all()
|
||||
assert len(remaining_relations) == 0
|
||||
|
||||
# Verify target entity still exists
|
||||
target_exists = await entity_repo.find_by_id(target.id)
|
||||
assert target_exists is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_entity(entity_repo):
|
||||
"""Test deleting an entity that doesn't exist."""
|
||||
result = await entity_repo.delete("nonexistent/id")
|
||||
assert result is False
|
||||
@@ -0,0 +1,291 @@
|
||||
"""Tests for the RelationRepository."""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from basic_memory.models import Entity, Relation
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def relation_repo(session):
|
||||
"""Create a RelationRepository with test DB session."""
|
||||
return RelationRepository(session)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def source_entity(session):
|
||||
"""Create a source entity for testing relations."""
|
||||
entity = Entity(
|
||||
id="source/test_entity",
|
||||
name="test_source",
|
||||
entity_type="source",
|
||||
description="Source entity"
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
return entity
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def target_entity(session):
|
||||
"""Create a target entity for testing relations."""
|
||||
entity = Entity(
|
||||
id="target/test_entity",
|
||||
name="test_target",
|
||||
entity_type="target",
|
||||
description="Target entity"
|
||||
)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
return entity
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_relations(session, source_entity, target_entity):
|
||||
"""Create test relations."""
|
||||
relations = [
|
||||
Relation(
|
||||
from_id=source_entity.id,
|
||||
to_id=target_entity.id,
|
||||
relation_type="connects_to"
|
||||
),
|
||||
Relation(
|
||||
from_id=source_entity.id,
|
||||
to_id=target_entity.id,
|
||||
relation_type="depends_on"
|
||||
)
|
||||
]
|
||||
session.add_all(relations)
|
||||
await session.flush()
|
||||
return relations
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def related_entity(entity_repository):
|
||||
"""Create a second entity for testing relations"""
|
||||
entity_data = {
|
||||
'id': '20240102-related',
|
||||
'name': 'Related Entity',
|
||||
'entity_type': 'test',
|
||||
'description': 'A related test entity',
|
||||
'references': ''
|
||||
}
|
||||
return await entity_repository.create(entity_data)
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def sample_relation(
|
||||
relation_repository: RelationRepository,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Create a sample relation for testing"""
|
||||
relation_data = {
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'test_relation',
|
||||
'context': 'test-context'
|
||||
}
|
||||
return await relation_repository.create(relation_data)
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def multiple_relations(
|
||||
relation_repository: RelationRepository,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Create multiple relations for testing"""
|
||||
relations_data = [
|
||||
{
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'relation_one',
|
||||
'context': 'context_one'
|
||||
},
|
||||
{
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'relation_two',
|
||||
'context': 'context_two'
|
||||
},
|
||||
{
|
||||
'from_id': related_entity.id,
|
||||
'to_id': sample_entity.id,
|
||||
'relation_type': 'relation_one',
|
||||
'context': 'context_three'
|
||||
}
|
||||
]
|
||||
return [await relation_repository.create(data) for data in relations_data]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_relation(
|
||||
relation_repository: RelationRepository,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test creating a new relation"""
|
||||
relation_data = {
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'test_relation',
|
||||
'context': 'test-context'
|
||||
}
|
||||
relation = await relation_repository.create(relation_data)
|
||||
|
||||
assert relation.from_id == sample_entity.id
|
||||
assert relation.to_id == related_entity.id
|
||||
assert relation.relation_type == 'test_relation'
|
||||
assert relation.id is not None # Should be auto-generated
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_by_entities(
|
||||
relation_repository: RelationRepository,
|
||||
sample_relation: Relation,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test finding relations between specific entities"""
|
||||
relations = await relation_repository.find_by_entities(
|
||||
sample_entity.id,
|
||||
related_entity.id
|
||||
)
|
||||
assert len(relations) == 1
|
||||
assert relations[0].id == sample_relation.id
|
||||
assert relations[0].relation_type == sample_relation.relation_type
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_by_type(
|
||||
relation_repository: RelationRepository,
|
||||
sample_relation: Relation
|
||||
):
|
||||
"""Test finding relations by type"""
|
||||
relations = await relation_repository.find_by_type('test_relation')
|
||||
assert len(relations) == 1
|
||||
assert relations[0].id == sample_relation.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_by_fields_single_field(
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation]
|
||||
):
|
||||
"""Test deleting relations by a single field."""
|
||||
# Delete all relations of type 'relation_one'
|
||||
result = await relation_repository.delete_by_fields(relation_type='relation_one') # pyright: ignore [reportArgumentType]
|
||||
assert result is True
|
||||
|
||||
# Verify deletion
|
||||
remaining = await relation_repository.find_by_type('relation_one')
|
||||
assert len(remaining) == 0
|
||||
|
||||
# Other relations should still exist
|
||||
others = await relation_repository.find_by_type('relation_two')
|
||||
assert len(others) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_by_fields_multiple_fields(
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation],
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test deleting relations by multiple fields."""
|
||||
# Delete specific relation matching both from_id and relation_type
|
||||
result = await relation_repository.delete_by_fields(
|
||||
from_id=sample_entity.id, # pyright: ignore [reportArgumentType]
|
||||
relation_type='relation_one' # pyright: ignore [reportArgumentType]
|
||||
)
|
||||
assert result is True
|
||||
|
||||
# Verify correct relation was deleted
|
||||
remaining = await relation_repository.find_by_entities(
|
||||
sample_entity.id,
|
||||
related_entity.id
|
||||
)
|
||||
assert len(remaining) == 1 # Only relation_two should remain
|
||||
assert remaining[0].relation_type == 'relation_two'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_by_fields_no_match(
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation]
|
||||
):
|
||||
"""Test delete_by_fields when no relations match."""
|
||||
result = await relation_repository.delete_by_fields(
|
||||
relation_type='nonexistent_type' # pyright: ignore [reportArgumentType]
|
||||
)
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_by_fields_all_fields(
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation],
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test deleting relation by matching all fields."""
|
||||
# Get first relation's data
|
||||
relation = multiple_relations[0]
|
||||
|
||||
# Delete using all fields
|
||||
result = await relation_repository.delete_by_fields(
|
||||
from_id=relation.from_id, # pyright: ignore [reportArgumentType]
|
||||
to_id=relation.to_id,# pyright: ignore [reportArgumentType]
|
||||
relation_type=relation.relation_type, # pyright: ignore [reportArgumentType]
|
||||
context=relation.context # pyright: ignore [reportArgumentType]
|
||||
)
|
||||
assert result is True
|
||||
|
||||
# Verify only exact match was deleted
|
||||
remaining = await relation_repository.find_by_type(relation.relation_type)
|
||||
assert len(remaining) == 1 # One other relation_one should remain
|
||||
assert remaining[0].context != relation.context
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_relation_by_id(relation_repo, test_relations):
|
||||
"""Test deleting a relation by ID."""
|
||||
relation = test_relations[0]
|
||||
|
||||
result = await relation_repo.delete(relation.id)
|
||||
assert result is True
|
||||
|
||||
# Verify deletion
|
||||
remaining = await relation_repo.find_one(
|
||||
relation_repo.select(Relation).filter(Relation.id == relation.id)
|
||||
)
|
||||
assert remaining is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_relations_by_type(relation_repo, test_relations):
|
||||
"""Test deleting relations by type."""
|
||||
result = await relation_repo.delete_by_fields(relation_type="connects_to")
|
||||
assert result is True
|
||||
|
||||
# Verify specific type was deleted
|
||||
remaining = await relation_repo.find_by_type("connects_to")
|
||||
assert len(remaining) == 0
|
||||
|
||||
# Verify other type still exists
|
||||
others = await relation_repo.find_by_type("depends_on")
|
||||
assert len(others) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_relations_by_entities(relation_repo, test_relations, source_entity, target_entity):
|
||||
"""Test deleting relations between specific entities."""
|
||||
result = await relation_repo.delete_by_fields(
|
||||
from_id=source_entity.id,
|
||||
to_id=target_entity.id
|
||||
)
|
||||
assert result is True
|
||||
|
||||
# Verify all relations between entities were deleted
|
||||
remaining = await relation_repo.find_by_entities(source_entity.id, target_entity.id)
|
||||
assert len(remaining) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_nonexistent_relation(relation_repo):
|
||||
"""Test deleting a relation that doesn't exist."""
|
||||
result = await relation_repo.delete_by_fields(relation_type="nonexistent")
|
||||
assert result is False
|
||||
@@ -1,273 +0,0 @@
|
||||
"""Tests for EntityRepository."""
|
||||
import pytest
|
||||
from datetime import datetime, UTC
|
||||
from sqlalchemy import text, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestEntityRepository:
|
||||
async def test_create_entity(self, entity_repository: EntityRepository):
|
||||
"""Test creating a new entity"""
|
||||
entity_data = {
|
||||
'name': 'Test',
|
||||
'entity_type': 'test',
|
||||
'description': 'Test description',
|
||||
}
|
||||
entity = await entity_repository.create(entity_data)
|
||||
|
||||
# Verify returned object
|
||||
assert entity.id == f'test/test'
|
||||
assert entity.name == 'Test'
|
||||
assert entity.description == 'Test description'
|
||||
assert isinstance(entity.created_at, datetime)
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.id == entity.id
|
||||
assert db_entity.name == entity.name
|
||||
assert db_entity.description == entity.description
|
||||
|
||||
async def test_entity_type_name_unique_constraint(self, entity_repository: EntityRepository):
|
||||
"""Test the unique constraint on entity_type + name combination."""
|
||||
# Create first entity
|
||||
entity1_data = {
|
||||
'id': '20240102-test1',
|
||||
'name': 'Test Entity',
|
||||
'entity_type': 'type1',
|
||||
'description': 'First entity'
|
||||
}
|
||||
await entity_repository.create(entity1_data)
|
||||
|
||||
# Try to create another entity with same type and name
|
||||
entity2_data = {
|
||||
'id': '20240102-test2',
|
||||
'name': 'Test Entity', # Same name
|
||||
'entity_type': 'type1', # Same type
|
||||
'description': 'Second entity'
|
||||
}
|
||||
|
||||
# Should raise IntegrityError
|
||||
with pytest.raises(IntegrityError) as exc_info:
|
||||
await entity_repository.create(entity2_data)
|
||||
assert 'UNIQUE constraint failed: entity.entity_type, entity.name' in str(exc_info.value)
|
||||
|
||||
async def test_create_entity_null_description(self, entity_repository: EntityRepository):
|
||||
"""Test creating an entity with null description"""
|
||||
entity_data = {
|
||||
'id': '20240102-test',
|
||||
'name': 'Test',
|
||||
'entity_type': 'test',
|
||||
'description': None,
|
||||
}
|
||||
entity = await entity_repository.create(entity_data)
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.description is None
|
||||
|
||||
async def test_find_by_id(self, entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test finding an entity by ID"""
|
||||
found = await entity_repository.find_by_id(sample_entity.id)
|
||||
assert found is not None
|
||||
assert found.id == sample_entity.id
|
||||
assert found.name == sample_entity.name
|
||||
|
||||
# Verify against direct database query
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.id == found.id
|
||||
assert db_entity.name == found.name
|
||||
assert db_entity.description == found.description
|
||||
|
||||
async def test_find_by_name(self, entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test finding an entity by name"""
|
||||
found = await entity_repository.find_by_name(sample_entity.name)
|
||||
assert found is not None
|
||||
assert found.id == sample_entity.id
|
||||
assert found.name == sample_entity.name
|
||||
|
||||
# Verify against direct database query
|
||||
stmt = select(Entity).where(Entity.name == sample_entity.name)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.id == found.id
|
||||
assert db_entity.name == found.name
|
||||
assert db_entity.description == found.description
|
||||
|
||||
async def test_update_entity(self, entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test updating an entity"""
|
||||
updated = await entity_repository.update(
|
||||
sample_entity.id,
|
||||
{'description': 'Updated description'}
|
||||
)
|
||||
assert updated is not None
|
||||
assert updated.description == 'Updated description'
|
||||
assert updated.name == sample_entity.name # Other fields unchanged
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.description == 'Updated description'
|
||||
assert db_entity.name == sample_entity.name
|
||||
|
||||
async def test_update_entity_to_null(self, entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test updating an entity's description to null"""
|
||||
updated = await entity_repository.update(
|
||||
sample_entity.id,
|
||||
{'description': None}
|
||||
)
|
||||
assert updated is not None
|
||||
assert updated.description is None
|
||||
|
||||
# Verify in database
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entity = result.scalar_one()
|
||||
assert db_entity.description is None
|
||||
|
||||
async def test_delete_entity(self, entity_repository: EntityRepository, sample_entity: Entity):
|
||||
"""Test deleting an entity"""
|
||||
success = await entity_repository.delete(sample_entity.id)
|
||||
assert success is True
|
||||
|
||||
# Verify it's gone
|
||||
found = await entity_repository.find_by_id(sample_entity.id)
|
||||
assert found is None
|
||||
|
||||
# Verify with direct query
|
||||
stmt = select(Entity).where(Entity.id == sample_entity.id)
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
assert result.first() is None
|
||||
|
||||
async def test_search(self, entity_repository: EntityRepository):
|
||||
"""Test searching entities"""
|
||||
# Create test entities with observations
|
||||
entity1 = await entity_repository.create({
|
||||
'id': '20240102-test1',
|
||||
'name': 'Search Test 1',
|
||||
'entity_type': 'test',
|
||||
'description': 'First test entity'
|
||||
})
|
||||
|
||||
entity2 = await entity_repository.create({
|
||||
'id': '20240102-test2',
|
||||
'name': 'Search Test 2',
|
||||
'entity_type': 'other',
|
||||
'description': 'Second test entity'
|
||||
})
|
||||
|
||||
# Verify entities in database
|
||||
stmt = select(Entity).where(Entity.id.in_([entity1.id, entity2.id]))
|
||||
result = await entity_repository.session.execute(stmt)
|
||||
db_entities = result.scalars().all()
|
||||
assert len(db_entities) == 2
|
||||
|
||||
# Add observations
|
||||
stmt = text("""
|
||||
INSERT INTO observation (entity_id, content, created_at)
|
||||
VALUES (:e1_id, :e1_obs, :ts), (:e2_id, :e2_obs, :ts)
|
||||
""")
|
||||
ts = datetime.now(UTC)
|
||||
await entity_repository.session.execute(stmt, {
|
||||
"e1_id": entity1.id,
|
||||
"e1_obs": "First observation with searchable content",
|
||||
"e2_id": entity2.id,
|
||||
"e2_obs": "Another observation to find",
|
||||
"ts": ts
|
||||
})
|
||||
await entity_repository.session.commit()
|
||||
|
||||
# Test search by name
|
||||
results = await entity_repository.search('Search Test')
|
||||
assert len(results) == 2
|
||||
names = {e.name for e in results}
|
||||
assert 'Search Test 1' in names
|
||||
assert 'Search Test 2' in names
|
||||
|
||||
# Test search by type
|
||||
results = await entity_repository.search('other')
|
||||
assert len(results) == 1
|
||||
assert results[0].entity_type == 'other'
|
||||
|
||||
# Test search by observation content
|
||||
results = await entity_repository.search('searchable')
|
||||
assert len(results) == 1
|
||||
assert results[0].id == entity1.id
|
||||
|
||||
|
||||
async def test_find_by_type_and_name(entity_repository: EntityRepository):
|
||||
"""Test finding an entity by type and name combination."""
|
||||
# Create two entities with same name but different types
|
||||
entity1 = await entity_repository.create({
|
||||
'id': '20240102-test1',
|
||||
'name': 'Test Entity',
|
||||
'entity_type': 'type1',
|
||||
'description': 'First test entity'
|
||||
})
|
||||
|
||||
entity2 = await entity_repository.create({
|
||||
'id': '20240102-test2',
|
||||
'name': 'Test Entity',
|
||||
'entity_type': 'type2',
|
||||
'description': 'Second test entity'
|
||||
})
|
||||
|
||||
# Should find correct entity when both type and name match
|
||||
found = await entity_repository.find_by_type_and_name('type1', 'Test Entity')
|
||||
assert found is not None
|
||||
assert found.id == entity1.id
|
||||
assert found.entity_type == 'type1'
|
||||
assert found.name == 'Test Entity'
|
||||
|
||||
# Should find other entity with same name but different type
|
||||
found = await entity_repository.find_by_type_and_name('type2', 'Test Entity')
|
||||
assert found is not None
|
||||
assert found.id == entity2.id
|
||||
assert found.entity_type == 'type2'
|
||||
assert found.name == 'Test Entity'
|
||||
|
||||
# Should return None when type doesn't match
|
||||
found = await entity_repository.find_by_type_and_name('nonexistent', 'Test Entity')
|
||||
assert found is None
|
||||
|
||||
# Should return None when name doesn't match
|
||||
found = await entity_repository.find_by_type_and_name('type1', 'Nonexistent')
|
||||
assert found is None
|
||||
|
||||
# Verify relationships are loaded
|
||||
entity3 = await entity_repository.create({
|
||||
'id': '20240102-test3',
|
||||
'name': 'Entity With Relations',
|
||||
'entity_type': 'type3',
|
||||
'description': 'Entity with observations and relations'
|
||||
})
|
||||
|
||||
# Add an observation
|
||||
stmt = text("""
|
||||
INSERT INTO observation (entity_id, content, created_at)
|
||||
VALUES (:entity_id, :content, :ts)
|
||||
""")
|
||||
ts = datetime.now(UTC)
|
||||
await entity_repository.session.execute(stmt, {
|
||||
"entity_id": entity3.id,
|
||||
"content": "Test observation",
|
||||
"ts": ts
|
||||
})
|
||||
await entity_repository.session.commit()
|
||||
|
||||
# Find entity and verify relationships are loaded
|
||||
found = await entity_repository.find_by_type_and_name('type3', 'Entity With Relations')
|
||||
assert found is not None
|
||||
assert len(found.observations) == 1
|
||||
assert found.observations[0].content == "Test observation"
|
||||
@@ -1,190 +0,0 @@
|
||||
"""Tests for RelationRepository."""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from basic_memory.models import Entity, Relation
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
class TestRelationRepository:
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def related_entity(self, entity_repository):
|
||||
"""Create a second entity for testing relations"""
|
||||
entity_data = {
|
||||
'id': '20240102-related',
|
||||
'name': 'Related Entity',
|
||||
'entity_type': 'test',
|
||||
'description': 'A related test entity',
|
||||
'references': ''
|
||||
}
|
||||
return await entity_repository.create(entity_data)
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def sample_relation(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Create a sample relation for testing"""
|
||||
relation_data = {
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'test_relation',
|
||||
'context': 'test-context'
|
||||
}
|
||||
return await relation_repository.create(relation_data)
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def multiple_relations(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Create multiple relations for testing"""
|
||||
relations_data = [
|
||||
{
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'relation_one',
|
||||
'context': 'context_one'
|
||||
},
|
||||
{
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'relation_two',
|
||||
'context': 'context_two'
|
||||
},
|
||||
{
|
||||
'from_id': related_entity.id,
|
||||
'to_id': sample_entity.id,
|
||||
'relation_type': 'relation_one',
|
||||
'context': 'context_three'
|
||||
}
|
||||
]
|
||||
return [await relation_repository.create(data) for data in relations_data]
|
||||
|
||||
async def test_create_relation(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test creating a new relation"""
|
||||
relation_data = {
|
||||
'from_id': sample_entity.id,
|
||||
'to_id': related_entity.id,
|
||||
'relation_type': 'test_relation',
|
||||
'context': 'test-context'
|
||||
}
|
||||
relation = await relation_repository.create(relation_data)
|
||||
|
||||
assert relation.from_id == sample_entity.id
|
||||
assert relation.to_id == related_entity.id
|
||||
assert relation.relation_type == 'test_relation'
|
||||
assert relation.id is not None # Should be auto-generated
|
||||
|
||||
async def test_find_by_entities(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
sample_relation: Relation,
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test finding relations between specific entities"""
|
||||
relations = await relation_repository.find_by_entities(
|
||||
sample_entity.id,
|
||||
related_entity.id
|
||||
)
|
||||
assert len(relations) == 1
|
||||
assert relations[0].id == sample_relation.id
|
||||
assert relations[0].relation_type == sample_relation.relation_type
|
||||
|
||||
async def test_find_by_type(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
sample_relation: Relation
|
||||
):
|
||||
"""Test finding relations by type"""
|
||||
relations = await relation_repository.find_by_type('test_relation')
|
||||
assert len(relations) == 1
|
||||
assert relations[0].id == sample_relation.id
|
||||
|
||||
async def test_delete_by_fields_single_field(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation]
|
||||
):
|
||||
"""Test deleting relations by a single field."""
|
||||
# Delete all relations of type 'relation_one'
|
||||
result = await relation_repository.delete_by_fields(relation_type='relation_one')
|
||||
assert result is True
|
||||
|
||||
# Verify deletion
|
||||
remaining = await relation_repository.find_by_type('relation_one')
|
||||
assert len(remaining) == 0
|
||||
|
||||
# Other relations should still exist
|
||||
others = await relation_repository.find_by_type('relation_two')
|
||||
assert len(others) == 1
|
||||
|
||||
async def test_delete_by_fields_multiple_fields(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation],
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test deleting relations by multiple fields."""
|
||||
# Delete specific relation matching both from_id and relation_type
|
||||
result = await relation_repository.delete_by_fields(
|
||||
from_id=sample_entity.id,
|
||||
relation_type='relation_one'
|
||||
)
|
||||
assert result is True
|
||||
|
||||
# Verify correct relation was deleted
|
||||
remaining = await relation_repository.find_by_entities(
|
||||
sample_entity.id,
|
||||
related_entity.id
|
||||
)
|
||||
assert len(remaining) == 1 # Only relation_two should remain
|
||||
assert remaining[0].relation_type == 'relation_two'
|
||||
|
||||
async def test_delete_by_fields_no_match(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation]
|
||||
):
|
||||
"""Test delete_by_fields when no relations match."""
|
||||
result = await relation_repository.delete_by_fields(
|
||||
relation_type='nonexistent_type'
|
||||
)
|
||||
assert result is False
|
||||
|
||||
async def test_delete_by_fields_all_fields(
|
||||
self,
|
||||
relation_repository: RelationRepository,
|
||||
multiple_relations: list[Relation],
|
||||
sample_entity: Entity,
|
||||
related_entity: Entity
|
||||
):
|
||||
"""Test deleting relation by matching all fields."""
|
||||
# Get first relation's data
|
||||
relation = multiple_relations[0]
|
||||
|
||||
# Delete using all fields
|
||||
result = await relation_repository.delete_by_fields(
|
||||
from_id=relation.from_id,
|
||||
to_id=relation.to_id,
|
||||
relation_type=relation.relation_type,
|
||||
context=relation.context
|
||||
)
|
||||
assert result is True
|
||||
|
||||
# Verify only exact match was deleted
|
||||
remaining = await relation_repository.find_by_type(relation.relation_type)
|
||||
assert len(remaining) == 1 # One other relation_one should remain
|
||||
assert remaining[0].context != relation.context
|
||||
Reference in New Issue
Block a user