Skip to content

Commit 8daafcb

Browse files
claude[bot]groksrc
andauthored
Fix UNIQUE constraint failed: entity.permalink issue #139
Improved error handling in create_entity_from_markdown() to properly handle permalink conflicts when different files generate the same permalink. Changes: - Distinguish between file_path and permalink constraint failures - For same file conflicts: update existing entity - For different file conflicts: generate unique permalink with suffix - Added comprehensive test case reproducing the exact issue scenario This fixes the issue where write_note would fail when trying to create notes with titles that generate the same permalink as existing notes. Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
1 parent 3fdce68 commit 8daafcb

2 files changed

Lines changed: 90 additions & 5 deletions

File tree

src/basic_memory/services/entity_service.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,14 +302,36 @@ async def create_entity_from_markdown(
302302
try:
303303
return await self.repository.add(model)
304304
except IntegrityError as e:
305-
# Handle race condition where entity was created by another process
306-
if "UNIQUE constraint failed: entity.file_path" in str(
307-
e
308-
) or "UNIQUE constraint failed: entity.permalink" in str(e):
305+
# Handle different types of UNIQUE constraint failures
306+
if "UNIQUE constraint failed: entity.file_path" in str(e):
307+
# File path conflict - update existing entity
309308
logger.info(
310-
f"Entity already exists for file_path={file_path} (file_path or permalink conflict), updating instead of creating"
309+
f"Entity already exists for file_path={file_path}, updating instead of creating"
311310
)
312311
return await self.update_entity_and_observations(file_path, markdown)
312+
elif "UNIQUE constraint failed: entity.permalink" in str(e):
313+
# Permalink conflict - check if it's the same file or different file
314+
existing_entity = await self.repository.get_by_permalink(model.permalink)
315+
if existing_entity and existing_entity.file_path == str(file_path):
316+
# Same file - update existing entity
317+
logger.info(
318+
f"Entity already exists for file_path={file_path}, updating instead of creating"
319+
)
320+
return await self.update_entity_and_observations(file_path, markdown)
321+
else:
322+
# Different file with same permalink - generate unique permalink
323+
logger.info(
324+
f"Permalink conflict for {model.permalink}, generating unique permalink"
325+
)
326+
# Generate unique permalink
327+
base_permalink = model.permalink
328+
suffix = 1
329+
while await self.repository.get_by_permalink(model.permalink):
330+
model.permalink = f"{base_permalink}-{suffix}"
331+
suffix += 1
332+
logger.debug(f"Using unique permalink: {model.permalink}")
333+
# Try to create with unique permalink
334+
return await self.repository.add(model)
313335
else:
314336
# Re-raise if it's a different integrity error
315337
raise

tests/mcp/test_tool_write_note.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,3 +413,66 @@ async def test_write_note_preserves_content_frontmatter(app):
413413
).strip()
414414
in content
415415
)
416+
417+
418+
@pytest.mark.asyncio
419+
async def test_write_note_permalink_collision_fix_issue_139(app):
420+
"""Test fix for GitHub Issue #139: UNIQUE constraint failed: entity.permalink.
421+
422+
This reproduces the exact scenario described in the issue:
423+
1. Create a note with title "Note 1"
424+
2. Create another note with title "Note 2"
425+
3. Try to create/replace first note again with same title "Note 1"
426+
427+
Before the fix, step 3 would fail with UNIQUE constraint error.
428+
After the fix, it should either update the existing note or create with unique permalink.
429+
"""
430+
# Step 1: Create first note
431+
result1 = await write_note.fn(
432+
title="Note 1",
433+
folder="test",
434+
content="Original content for note 1"
435+
)
436+
assert "# Created note" in result1
437+
assert "permalink: test/note-1" in result1
438+
439+
# Step 2: Create second note with different title
440+
result2 = await write_note.fn(
441+
title="Note 2",
442+
folder="test",
443+
content="Content for note 2"
444+
)
445+
assert "# Created note" in result2
446+
assert "permalink: test/note-2" in result2
447+
448+
# Step 3: Try to create/replace first note again
449+
# This scenario would trigger the UNIQUE constraint failure before the fix
450+
result3 = await write_note.fn(
451+
title="Note 1", # Same title as first note
452+
folder="test", # Same folder as first note
453+
content="Replacement content for note 1" # Different content
454+
)
455+
456+
# This should not raise a UNIQUE constraint failure error
457+
# It should succeed and either:
458+
# 1. Update the existing note (preferred behavior)
459+
# 2. Create a new note with unique permalink (fallback behavior)
460+
461+
assert result3 is not None
462+
assert ("Updated note" in result3 or "Created note" in result3)
463+
464+
# The result should contain either the original permalink or a unique one
465+
assert ("permalink: test/note-1" in result3 or "permalink: test/note-1-1" in result3)
466+
467+
# Verify we can read back the content
468+
if "permalink: test/note-1" in result3:
469+
# Updated existing note case
470+
content = await read_note.fn("test/note-1")
471+
assert "Replacement content for note 1" in content
472+
else:
473+
# Created new note with unique permalink case
474+
content = await read_note.fn("test/note-1-1")
475+
assert "Replacement content for note 1" in content
476+
# Original note should still exist
477+
original_content = await read_note.fn("test/note-1")
478+
assert "Original content for note 1" in original_content

0 commit comments

Comments
 (0)