11"""Service for managing entities in the database."""
22
33from collections .abc import Callable
4+ from dataclasses import dataclass
45from datetime import datetime
56from pathlib import Path
67from typing import List , Optional , Sequence , Tuple , Union
5051from basic_memory .utils import build_canonical_permalink
5152
5253
54+ @dataclass (frozen = True )
55+ class EntityWriteResult :
56+ """Persisted entity plus the response/search content produced during this call."""
57+
58+ entity : EntityModel
59+ content : str
60+ search_content : str
61+
62+
5363class EntityService (BaseService [EntityModel ]):
5464 """Service for managing entities in the database."""
5565
@@ -79,7 +89,7 @@ def __init__(
7989
8090 async def detect_file_path_conflicts (
8191 self , file_path : str , skip_check : bool = False
82- ) -> List [Entity ]:
92+ ) -> List [str ]:
8393 """Detect potential file path conflicts for a given file path.
8494
8595 This checks for entities with similar file paths that might cause conflicts:
@@ -93,28 +103,19 @@ async def detect_file_path_conflicts(
93103 skip_check: If True, skip the check and return empty list (optimization for bulk operations)
94104
95105 Returns:
96- List of entities that might conflict with the given file path
106+ List of file paths that might conflict with the given file path
97107 """
98108 if skip_check :
99109 return []
100110
101111 from basic_memory .utils import detect_potential_file_conflicts
102112
103- conflicts = []
104-
105- # Get all existing file paths
106- all_entities = await self .repository .find_all ()
107- existing_paths = [entity .file_path for entity in all_entities ]
113+ # Load only file paths. Conflict detection is on the hot write path and
114+ # does not need observations or relations.
115+ existing_paths = await self .repository .get_all_file_paths ()
108116
109117 # Use the enhanced conflict detection utility
110- conflicting_paths = detect_potential_file_conflicts (file_path , existing_paths )
111-
112- # Find the entities corresponding to conflicting paths
113- for entity in all_entities :
114- if entity .file_path in conflicting_paths :
115- conflicts .append (entity )
116-
117- return conflicts
118+ return detect_potential_file_conflicts (file_path , existing_paths )
118119
119120 async def resolve_permalink (
120121 self ,
@@ -143,8 +144,7 @@ async def resolve_permalink(
143144 )
144145 if conflicts :
145146 logger .warning (
146- f"Detected potential file path conflicts for '{ file_path_str } ': "
147- f"{ [entity .file_path for entity in conflicts ]} "
147+ f"Detected potential file path conflicts for '{ file_path_str } ': { conflicts } "
148148 )
149149
150150 # If markdown has explicit permalink, try to validate it
@@ -255,6 +255,10 @@ async def create_or_update_entity(self, schema: EntitySchema) -> Tuple[EntityMod
255255
256256 async def create_entity (self , schema : EntitySchema ) -> EntityModel :
257257 """Create a new entity and write to filesystem."""
258+ return (await self .create_entity_with_content (schema )).entity
259+
260+ async def create_entity_with_content (self , schema : EntitySchema ) -> EntityWriteResult :
261+ """Create a new entity and return both the entity row and written markdown."""
258262 logger .debug (f"Creating entity: { schema .title } " )
259263
260264 # Get file path and ensure it's a Path object
@@ -328,10 +332,23 @@ async def create_entity(self, schema: EntitySchema) -> EntityModel:
328332 action = "create" ,
329333 phase = "update_checksum" ,
330334 ):
331- return await self .repository .update (entity .id , {"checksum" : checksum })
335+ updated = await self .repository .update (entity .id , {"checksum" : checksum })
336+ if not updated : # pragma: no cover
337+ raise ValueError (f"Failed to update entity checksum after create: { entity .id } " )
338+ return EntityWriteResult (
339+ entity = updated ,
340+ content = final_content ,
341+ search_content = remove_frontmatter (final_content ),
342+ )
332343
333344 async def update_entity (self , entity : EntityModel , schema : EntitySchema ) -> EntityModel :
334345 """Update an entity's content and metadata."""
346+ return (await self .update_entity_with_content (entity , schema )).entity
347+
348+ async def update_entity_with_content (
349+ self , entity : EntityModel , schema : EntitySchema
350+ ) -> EntityWriteResult :
351+ """Update an entity and return both the entity row and written markdown."""
335352 logger .debug (
336353 f"Updating entity with permalink: { entity .permalink } content-type: { schema .content_type } "
337354 )
@@ -444,8 +461,14 @@ async def update_entity(self, entity: EntityModel, schema: EntitySchema) -> Enti
444461 phase = "update_checksum" ,
445462 ):
446463 entity = await self .repository .update (entity .id , {"checksum" : checksum })
464+ if not entity : # pragma: no cover
465+ raise ValueError (f"Failed to update entity checksum after update: { file_path } " )
447466
448- return entity
467+ return EntityWriteResult (
468+ entity = entity ,
469+ content = final_content ,
470+ search_content = remove_frontmatter (final_content ),
471+ )
449472
450473 async def fast_write_entity (
451474 self ,
@@ -988,6 +1011,27 @@ async def edit_entity(
9881011 EntityNotFoundError: If the entity cannot be found
9891012 ValueError: If required parameters are missing for the operation or replacement count doesn't match expected
9901013 """
1014+ return (
1015+ await self .edit_entity_with_content (
1016+ identifier = identifier ,
1017+ operation = operation ,
1018+ content = content ,
1019+ section = section ,
1020+ find_text = find_text ,
1021+ expected_replacements = expected_replacements ,
1022+ )
1023+ ).entity
1024+
1025+ async def edit_entity_with_content (
1026+ self ,
1027+ identifier : str ,
1028+ operation : str ,
1029+ content : str ,
1030+ section : Optional [str ] = None ,
1031+ find_text : Optional [str ] = None ,
1032+ expected_replacements : int = 1 ,
1033+ ) -> EntityWriteResult :
1034+ """Edit an entity and return both the entity row and written markdown."""
9911035 logger .debug (f"Editing entity: { identifier } , operation: { operation } " )
9921036
9931037 with telemetry .scope (
@@ -1055,8 +1099,14 @@ async def edit_entity(
10551099 phase = "update_checksum" ,
10561100 ):
10571101 entity = await self .repository .update (entity .id , {"checksum" : checksum })
1102+ if not entity : # pragma: no cover
1103+ raise ValueError (f"Failed to update entity checksum after edit: { file_path } " )
10581104
1059- return entity
1105+ return EntityWriteResult (
1106+ entity = entity ,
1107+ content = new_content ,
1108+ search_content = remove_frontmatter (new_content ),
1109+ )
10601110
10611111 def apply_edit_operation (
10621112 self ,
0 commit comments