mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix: fastmcp deprecation warning (#150)
Signed-off-by: Drew Cain <groksrc@gmail.com> Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com> Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
This commit is contained in:
@@ -78,6 +78,7 @@ def mcp(
|
||||
if transport == "stdio":
|
||||
mcp_server.run(
|
||||
transport=transport,
|
||||
log_level="INFO",
|
||||
)
|
||||
elif transport == "streamable-http" or transport == "sse":
|
||||
mcp_server.run(
|
||||
@@ -85,4 +86,5 @@ def mcp(
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level="INFO",
|
||||
)
|
||||
|
||||
@@ -95,11 +95,12 @@ async def get_or_create_db(
|
||||
|
||||
if _engine is None:
|
||||
_engine, _session_maker = _create_engine_and_session(db_path, db_type)
|
||||
|
||||
|
||||
# Run migrations automatically unless explicitly disabled
|
||||
if ensure_migrations:
|
||||
if app_config is None:
|
||||
from basic_memory.config import app_config as global_app_config
|
||||
|
||||
app_config = global_app_config
|
||||
await run_migrations(app_config, db_type)
|
||||
|
||||
@@ -170,12 +171,12 @@ async def run_migrations(
|
||||
): # pragma: no cover
|
||||
"""Run any pending alembic migrations."""
|
||||
global _migrations_completed
|
||||
|
||||
|
||||
# Skip if migrations already completed unless forced
|
||||
if _migrations_completed and not force:
|
||||
logger.debug("Migrations already completed in this session, skipping")
|
||||
return
|
||||
|
||||
|
||||
logger.info("Running database migrations...")
|
||||
try:
|
||||
# Get the absolute path to the alembic directory relative to this file
|
||||
@@ -206,7 +207,7 @@ async def run_migrations(
|
||||
# initialize the search Index schema
|
||||
# the project_id is not used for init_search_index, so we pass a dummy value
|
||||
await SearchRepository(session_maker, 1).init_search_index()
|
||||
|
||||
|
||||
# Mark migrations as completed
|
||||
_migrations_completed = True
|
||||
except Exception as e: # pragma: no cover
|
||||
|
||||
@@ -105,6 +105,5 @@ auth_settings, auth_provider = create_auth_config()
|
||||
# Create the shared server instance
|
||||
mcp = FastMCP(
|
||||
name="Basic Memory",
|
||||
log_level="DEBUG",
|
||||
auth=auth_provider,
|
||||
)
|
||||
|
||||
@@ -102,14 +102,14 @@ class EntityRepository(Repository[Entity]):
|
||||
|
||||
async def upsert_entity(self, entity: Entity) -> Entity:
|
||||
"""Insert or update entity using a hybrid approach.
|
||||
|
||||
|
||||
This method provides a cleaner alternative to the try/catch approach
|
||||
for handling permalink and file_path conflicts. It first tries direct
|
||||
for handling permalink and file_path conflicts. It first tries direct
|
||||
insertion, then handles conflicts intelligently.
|
||||
|
||||
|
||||
Args:
|
||||
entity: The entity to insert or update
|
||||
|
||||
|
||||
Returns:
|
||||
The inserted or updated entity
|
||||
"""
|
||||
@@ -117,29 +117,28 @@ class EntityRepository(Repository[Entity]):
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
# Set project_id if applicable and not already set
|
||||
self._set_project_id_if_needed(entity)
|
||||
|
||||
|
||||
# Check for existing entity with same file_path first
|
||||
existing_by_path = await session.execute(
|
||||
select(Entity).where(
|
||||
Entity.file_path == entity.file_path,
|
||||
Entity.project_id == entity.project_id
|
||||
Entity.file_path == entity.file_path, Entity.project_id == entity.project_id
|
||||
)
|
||||
)
|
||||
existing_path_entity = existing_by_path.scalar_one_or_none()
|
||||
|
||||
|
||||
if existing_path_entity:
|
||||
# Update existing entity with same file path
|
||||
for key, value in {
|
||||
'title': entity.title,
|
||||
'entity_type': entity.entity_type,
|
||||
'entity_metadata': entity.entity_metadata,
|
||||
'content_type': entity.content_type,
|
||||
'permalink': entity.permalink,
|
||||
'checksum': entity.checksum,
|
||||
'updated_at': entity.updated_at,
|
||||
"title": entity.title,
|
||||
"entity_type": entity.entity_type,
|
||||
"entity_metadata": entity.entity_metadata,
|
||||
"content_type": entity.content_type,
|
||||
"permalink": entity.permalink,
|
||||
"checksum": entity.checksum,
|
||||
"updated_at": entity.updated_at,
|
||||
}.items():
|
||||
setattr(existing_path_entity, key, value)
|
||||
|
||||
|
||||
await session.flush()
|
||||
# Return with relationships loaded
|
||||
query = (
|
||||
@@ -150,15 +149,17 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await session.execute(query)
|
||||
found = result.scalar_one_or_none()
|
||||
if not found: # pragma: no cover
|
||||
raise RuntimeError(f"Failed to retrieve entity after update: {entity.file_path}")
|
||||
raise RuntimeError(
|
||||
f"Failed to retrieve entity after update: {entity.file_path}"
|
||||
)
|
||||
return found
|
||||
|
||||
|
||||
# No existing entity with same file_path, try insert
|
||||
try:
|
||||
# Simple insert for new entity
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
|
||||
|
||||
# Return with relationships loaded
|
||||
query = (
|
||||
select(Entity)
|
||||
@@ -168,36 +169,37 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await session.execute(query)
|
||||
found = result.scalar_one_or_none()
|
||||
if not found: # pragma: no cover
|
||||
raise RuntimeError(f"Failed to retrieve entity after insert: {entity.file_path}")
|
||||
raise RuntimeError(
|
||||
f"Failed to retrieve entity after insert: {entity.file_path}"
|
||||
)
|
||||
return found
|
||||
|
||||
|
||||
except IntegrityError:
|
||||
# Could be either file_path or permalink conflict
|
||||
await session.rollback()
|
||||
|
||||
|
||||
# Check if it's a file_path conflict (race condition)
|
||||
existing_by_path_check = await session.execute(
|
||||
select(Entity).where(
|
||||
Entity.file_path == entity.file_path,
|
||||
Entity.project_id == entity.project_id
|
||||
Entity.file_path == entity.file_path, Entity.project_id == entity.project_id
|
||||
)
|
||||
)
|
||||
race_condition_entity = existing_by_path_check.scalar_one_or_none()
|
||||
|
||||
|
||||
if race_condition_entity:
|
||||
# Race condition: file_path conflict detected after our initial check
|
||||
# Update the existing entity instead
|
||||
for key, value in {
|
||||
'title': entity.title,
|
||||
'entity_type': entity.entity_type,
|
||||
'entity_metadata': entity.entity_metadata,
|
||||
'content_type': entity.content_type,
|
||||
'permalink': entity.permalink,
|
||||
'checksum': entity.checksum,
|
||||
'updated_at': entity.updated_at,
|
||||
"title": entity.title,
|
||||
"entity_type": entity.entity_type,
|
||||
"entity_metadata": entity.entity_metadata,
|
||||
"content_type": entity.content_type,
|
||||
"permalink": entity.permalink,
|
||||
"checksum": entity.checksum,
|
||||
"updated_at": entity.updated_at,
|
||||
}.items():
|
||||
setattr(race_condition_entity, key, value)
|
||||
|
||||
|
||||
await session.flush()
|
||||
# Return the updated entity with relationships loaded
|
||||
query = (
|
||||
@@ -208,7 +210,9 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await session.execute(query)
|
||||
found = result.scalar_one_or_none()
|
||||
if not found: # pragma: no cover
|
||||
raise RuntimeError(f"Failed to retrieve entity after race condition update: {entity.file_path}")
|
||||
raise RuntimeError(
|
||||
f"Failed to retrieve entity after race condition update: {entity.file_path}"
|
||||
)
|
||||
return found
|
||||
else:
|
||||
# Must be permalink conflict - generate unique permalink
|
||||
@@ -218,14 +222,13 @@ class EntityRepository(Repository[Entity]):
|
||||
"""Handle permalink conflicts by generating a unique permalink."""
|
||||
base_permalink = entity.permalink
|
||||
suffix = 1
|
||||
|
||||
|
||||
# Find a unique permalink
|
||||
while True:
|
||||
test_permalink = f"{base_permalink}-{suffix}"
|
||||
existing = await session.execute(
|
||||
select(Entity).where(
|
||||
Entity.permalink == test_permalink,
|
||||
Entity.project_id == entity.project_id
|
||||
Entity.permalink == test_permalink, Entity.project_id == entity.project_id
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none() is None:
|
||||
@@ -233,11 +236,11 @@ class EntityRepository(Repository[Entity]):
|
||||
entity.permalink = test_permalink
|
||||
break
|
||||
suffix += 1
|
||||
|
||||
|
||||
# Insert with unique permalink (no conflict possible now)
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
|
||||
|
||||
# Return the inserted entity with relationships loaded
|
||||
query = (
|
||||
select(Entity)
|
||||
|
||||
@@ -302,7 +302,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
Creates the entity with null checksum to indicate sync not complete.
|
||||
Relations will be added in second pass.
|
||||
|
||||
|
||||
Uses UPSERT approach to handle permalink/file_path conflicts cleanly.
|
||||
"""
|
||||
logger.debug(f"Creating entity: {markdown.frontmatter.title} file_path: {file_path}")
|
||||
@@ -310,7 +310,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
# Mark as incomplete because we still need to add relations
|
||||
model.checksum = None
|
||||
|
||||
|
||||
# Use UPSERT to handle conflicts cleanly
|
||||
try:
|
||||
return await self.repository.upsert_entity(model)
|
||||
|
||||
@@ -21,9 +21,9 @@ async def initialize_database(app_config: BasicMemoryConfig) -> None:
|
||||
|
||||
Args:
|
||||
app_config: The Basic Memory project configuration
|
||||
|
||||
|
||||
Note:
|
||||
Database migrations are now handled automatically when the database
|
||||
Database migrations are now handled automatically when the database
|
||||
connection is first established via get_or_create_db().
|
||||
"""
|
||||
# Trigger database initialization and migrations by getting the database connection
|
||||
@@ -50,7 +50,9 @@ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
|
||||
|
||||
# Get database session - migrations handled centrally
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM, ensure_migrations=False
|
||||
db_path=app_config.database_path,
|
||||
db_type=db.DatabaseType.FILESYSTEM,
|
||||
ensure_migrations=False,
|
||||
)
|
||||
project_repository = ProjectRepository(session_maker)
|
||||
|
||||
@@ -71,7 +73,9 @@ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
|
||||
async def migrate_legacy_projects(app_config: BasicMemoryConfig):
|
||||
# Get database session - migrations handled centrally
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM, ensure_migrations=False
|
||||
db_path=app_config.database_path,
|
||||
db_type=db.DatabaseType.FILESYSTEM,
|
||||
ensure_migrations=False,
|
||||
)
|
||||
logger.info("Migrating legacy projects...")
|
||||
project_repository = ProjectRepository(session_maker)
|
||||
@@ -140,7 +144,9 @@ async def initialize_file_sync(
|
||||
|
||||
# Load app configuration - migrations handled centrally
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=app_config.database_path, db_type=db.DatabaseType.FILESYSTEM, ensure_migrations=False
|
||||
db_path=app_config.database_path,
|
||||
db_type=db.DatabaseType.FILESYSTEM,
|
||||
ensure_migrations=False,
|
||||
)
|
||||
project_repository = ProjectRepository(session_maker)
|
||||
|
||||
|
||||
@@ -418,59 +418,53 @@ async def test_write_note_preserves_content_frontmatter(app):
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_permalink_collision_fix_issue_139(app):
|
||||
"""Test fix for GitHub Issue #139: UNIQUE constraint failed: entity.permalink.
|
||||
|
||||
|
||||
This reproduces the exact scenario described in the issue:
|
||||
1. Create a note with title "Note 1"
|
||||
1. Create a note with title "Note 1"
|
||||
2. Create another note with title "Note 2"
|
||||
3. Try to create/replace first note again with same title "Note 1"
|
||||
|
||||
|
||||
Before the fix, step 3 would fail with UNIQUE constraint error.
|
||||
After the fix, it should either update the existing note or create with unique permalink.
|
||||
"""
|
||||
# Step 1: Create first note
|
||||
result1 = await write_note.fn(
|
||||
title="Note 1",
|
||||
folder="test",
|
||||
content="Original content for note 1"
|
||||
title="Note 1", folder="test", content="Original content for note 1"
|
||||
)
|
||||
assert "# Created note" in result1
|
||||
assert "permalink: test/note-1" in result1
|
||||
|
||||
|
||||
# Step 2: Create second note with different title
|
||||
result2 = await write_note.fn(
|
||||
title="Note 2",
|
||||
folder="test",
|
||||
content="Content for note 2"
|
||||
)
|
||||
result2 = await write_note.fn(title="Note 2", folder="test", content="Content for note 2")
|
||||
assert "# Created note" in result2
|
||||
assert "permalink: test/note-2" in result2
|
||||
|
||||
|
||||
# Step 3: Try to create/replace first note again
|
||||
# This scenario would trigger the UNIQUE constraint failure before the fix
|
||||
result3 = await write_note.fn(
|
||||
title="Note 1", # Same title as first note
|
||||
folder="test", # Same folder as first note
|
||||
content="Replacement content for note 1" # Different content
|
||||
folder="test", # Same folder as first note
|
||||
content="Replacement content for note 1", # Different content
|
||||
)
|
||||
|
||||
|
||||
# This should not raise a UNIQUE constraint failure error
|
||||
# It should succeed and either:
|
||||
# 1. Update the existing note (preferred behavior)
|
||||
# 2. Create a new note with unique permalink (fallback behavior)
|
||||
|
||||
|
||||
assert result3 is not None
|
||||
assert ("Updated note" in result3 or "Created note" in result3)
|
||||
|
||||
assert "Updated note" in result3 or "Created note" in result3
|
||||
|
||||
# The result should contain either the original permalink or a unique one
|
||||
assert ("permalink: test/note-1" in result3 or "permalink: test/note-1-1" in result3)
|
||||
|
||||
assert "permalink: test/note-1" in result3 or "permalink: test/note-1-1" in result3
|
||||
|
||||
# Verify we can read back the content
|
||||
if "permalink: test/note-1" in result3:
|
||||
# Updated existing note case
|
||||
content = await read_note.fn("test/note-1")
|
||||
assert "Replacement content for note 1" in content
|
||||
else:
|
||||
# Created new note with unique permalink case
|
||||
# Created new note with unique permalink case
|
||||
content = await read_note.fn("test/note-1-1")
|
||||
assert "Replacement content for note 1" in content
|
||||
# Original note should still exist
|
||||
|
||||
@@ -22,7 +22,7 @@ async def test_upsert_entity_new_entity(entity_repository: EntityRepository):
|
||||
)
|
||||
|
||||
result = await entity_repository.upsert_entity(entity)
|
||||
|
||||
|
||||
assert result.id is not None
|
||||
assert result.title == "Test Entity"
|
||||
assert result.permalink == "test/test-entity"
|
||||
@@ -60,7 +60,7 @@ async def test_upsert_entity_same_file_update(entity_repository: EntityRepositor
|
||||
)
|
||||
|
||||
result2 = await entity_repository.upsert_entity(entity2)
|
||||
|
||||
|
||||
# Should update existing entity (same ID)
|
||||
assert result2.id == original_id
|
||||
assert result2.title == "Updated Title"
|
||||
@@ -92,20 +92,20 @@ async def test_upsert_entity_permalink_conflict_different_file(entity_repository
|
||||
title="Second Entity",
|
||||
entity_type="note",
|
||||
permalink="test/shared-permalink", # Same permalink
|
||||
file_path="test/second-file.md", # Different file_path
|
||||
file_path="test/second-file.md", # Different file_path
|
||||
content_type="text/markdown",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
result2 = await entity_repository.upsert_entity(entity2)
|
||||
|
||||
|
||||
# Should create new entity with unique permalink
|
||||
assert result2.id != first_id
|
||||
assert result2.title == "Second Entity"
|
||||
assert result2.permalink == "test/shared-permalink-1" # Should get suffix
|
||||
assert result2.file_path == "test/second-file.md"
|
||||
|
||||
|
||||
# Original entity should be unchanged
|
||||
original = await entity_repository.get_by_permalink("test/shared-permalink")
|
||||
assert original is not None
|
||||
@@ -117,30 +117,30 @@ async def test_upsert_entity_permalink_conflict_different_file(entity_repository
|
||||
async def test_upsert_entity_multiple_permalink_conflicts(entity_repository: EntityRepository):
|
||||
"""Test upserting multiple entities with permalink conflicts."""
|
||||
base_permalink = "test/conflict"
|
||||
|
||||
|
||||
# Create entities with conflicting permalinks
|
||||
entities = []
|
||||
for i in range(3):
|
||||
entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title=f"Entity {i+1}",
|
||||
title=f"Entity {i + 1}",
|
||||
entity_type="note",
|
||||
permalink=base_permalink, # All try to use same permalink
|
||||
file_path=f"test/file-{i+1}.md", # Different file paths
|
||||
file_path=f"test/file-{i + 1}.md", # Different file paths
|
||||
content_type="text/markdown",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
result = await entity_repository.upsert_entity(entity)
|
||||
entities.append(result)
|
||||
|
||||
|
||||
# Verify permalinks are unique
|
||||
expected_permalinks = ["test/conflict", "test/conflict-1", "test/conflict-2"]
|
||||
actual_permalinks = [entity.permalink for entity in entities]
|
||||
|
||||
|
||||
assert set(actual_permalinks) == set(expected_permalinks)
|
||||
|
||||
|
||||
# Verify all entities were created (different IDs)
|
||||
entity_ids = [entity.id for entity in entities]
|
||||
assert len(set(entity_ids)) == 3
|
||||
@@ -151,7 +151,7 @@ async def test_upsert_entity_race_condition_file_path(entity_repository: EntityR
|
||||
"""Test that upsert handles race condition where file_path conflict occurs after initial check."""
|
||||
from unittest.mock import patch
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
|
||||
# Create an entity first
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
@@ -163,26 +163,26 @@ async def test_upsert_entity_race_condition_file_path(entity_repository: EntityR
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
result1 = await entity_repository.upsert_entity(entity1)
|
||||
original_id = result1.id
|
||||
|
||||
|
||||
# Create another entity with different file_path and permalink
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Race Condition Test",
|
||||
entity_type="note",
|
||||
entity_type="note",
|
||||
permalink="test/race-entity",
|
||||
file_path="test/different-file.md", # Different initially
|
||||
content_type="text/markdown",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
# Now simulate race condition: change file_path to conflict after the initial check
|
||||
original_add = entity_repository.session_maker().add
|
||||
call_count = 0
|
||||
|
||||
|
||||
def mock_add(obj):
|
||||
nonlocal call_count
|
||||
if isinstance(obj, Entity) and call_count == 0:
|
||||
@@ -192,12 +192,12 @@ async def test_upsert_entity_race_condition_file_path(entity_repository: EntityR
|
||||
# This should trigger IntegrityError for file_path constraint
|
||||
raise IntegrityError("UNIQUE constraint failed: entity.file_path", None, None)
|
||||
return original_add(obj)
|
||||
|
||||
|
||||
# Mock session.add to simulate the race condition
|
||||
with patch.object(entity_repository.session_maker().__class__, 'add', side_effect=mock_add):
|
||||
with patch.object(entity_repository.session_maker().__class__, "add", side_effect=mock_add):
|
||||
# This should handle the race condition gracefully by updating the existing entity
|
||||
result2 = await entity_repository.upsert_entity(entity2)
|
||||
|
||||
|
||||
# Should return the updated original entity (same ID)
|
||||
assert result2.id == original_id
|
||||
assert result2.title == "Race Condition Test" # Updated title
|
||||
@@ -210,24 +210,24 @@ async def test_upsert_entity_gap_in_suffixes(entity_repository: EntityRepository
|
||||
"""Test that upsert finds the next available suffix even with gaps."""
|
||||
# Manually create entities with non-sequential suffixes
|
||||
base_permalink = "test/gap"
|
||||
|
||||
|
||||
# Create entities with permalinks: "test/gap", "test/gap-1", "test/gap-3"
|
||||
# (skipping "test/gap-2")
|
||||
permalinks = [base_permalink, f"{base_permalink}-1", f"{base_permalink}-3"]
|
||||
|
||||
|
||||
for i, permalink in enumerate(permalinks):
|
||||
entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title=f"Entity {i+1}",
|
||||
title=f"Entity {i + 1}",
|
||||
entity_type="note",
|
||||
permalink=permalink,
|
||||
file_path=f"test/gap-file-{i+1}.md",
|
||||
file_path=f"test/gap-file-{i + 1}.md",
|
||||
content_type="text/markdown",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
await entity_repository.add(entity) # Use direct add to set specific permalinks
|
||||
|
||||
|
||||
# Now try to upsert a new entity that should get "test/gap-2"
|
||||
new_entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
@@ -239,10 +239,10 @@ async def test_upsert_entity_gap_in_suffixes(entity_repository: EntityRepository
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
result = await entity_repository.upsert_entity(new_entity)
|
||||
|
||||
|
||||
# Should get the next available suffix - our implementation finds gaps
|
||||
# so it should be "test/gap-2" (filling the gap)
|
||||
assert result.permalink == "test/gap-2"
|
||||
assert result.title == "Gap Filler"
|
||||
assert result.title == "Gap Filler"
|
||||
|
||||
@@ -46,18 +46,18 @@ async def test_migration_deduplication_single_call(
|
||||
|
||||
# First call should run migrations
|
||||
await db.run_migrations(app_config)
|
||||
|
||||
|
||||
# Verify migrations were called
|
||||
mock_alembic_command.upgrade.assert_called_once_with(mock_alembic_config, "head")
|
||||
mock_search_repository.init_search_index.assert_called_once()
|
||||
|
||||
|
||||
# Reset mocks for second call
|
||||
mock_alembic_command.reset_mock()
|
||||
mock_search_repository.reset_mock()
|
||||
|
||||
|
||||
# Second call should skip migrations
|
||||
await db.run_migrations(app_config)
|
||||
|
||||
|
||||
# Verify migrations were NOT called again
|
||||
mock_alembic_command.upgrade.assert_not_called()
|
||||
mock_search_repository.init_search_index.assert_not_called()
|
||||
@@ -75,18 +75,18 @@ async def test_migration_force_parameter(
|
||||
|
||||
# First call should run migrations
|
||||
await db.run_migrations(app_config)
|
||||
|
||||
|
||||
# Verify migrations were called
|
||||
mock_alembic_command.upgrade.assert_called_once_with(mock_alembic_config, "head")
|
||||
mock_search_repository.init_search_index.assert_called_once()
|
||||
|
||||
|
||||
# Reset mocks for forced call
|
||||
mock_alembic_command.reset_mock()
|
||||
mock_search_repository.reset_mock()
|
||||
|
||||
|
||||
# Forced call should run migrations again
|
||||
await db.run_migrations(app_config, force=True)
|
||||
|
||||
|
||||
# Verify migrations were called again
|
||||
mock_alembic_command.upgrade.assert_called_once_with(mock_alembic_config, "head")
|
||||
mock_search_repository.init_search_index.assert_called_once()
|
||||
@@ -99,10 +99,10 @@ async def test_migration_state_reset_on_shutdown():
|
||||
db._migrations_completed = True
|
||||
db._engine = AsyncMock()
|
||||
db._session_maker = AsyncMock()
|
||||
|
||||
|
||||
# Shutdown should reset state
|
||||
await db.shutdown_db()
|
||||
|
||||
|
||||
# Verify state was reset
|
||||
assert db._migrations_completed is False
|
||||
assert db._engine is None
|
||||
@@ -123,11 +123,11 @@ async def test_get_or_create_db_runs_migrations_automatically(
|
||||
engine, session_maker = await db.get_or_create_db(
|
||||
app_config.database_path, app_config=app_config
|
||||
)
|
||||
|
||||
|
||||
# Verify we got valid objects
|
||||
assert engine is not None
|
||||
assert session_maker is not None
|
||||
|
||||
|
||||
# Verify migrations were called
|
||||
mock_alembic_command.upgrade.assert_called_once_with(mock_alembic_config, "head")
|
||||
mock_search_repository.init_search_index.assert_called_once()
|
||||
@@ -147,11 +147,11 @@ async def test_get_or_create_db_skips_migrations_when_disabled(
|
||||
engine, session_maker = await db.get_or_create_db(
|
||||
app_config.database_path, ensure_migrations=False
|
||||
)
|
||||
|
||||
|
||||
# Verify we got valid objects
|
||||
assert engine is not None
|
||||
assert session_maker is not None
|
||||
|
||||
|
||||
# Verify migrations were NOT called
|
||||
mock_alembic_command.upgrade.assert_not_called()
|
||||
mock_search_repository.init_search_index.assert_not_called()
|
||||
@@ -169,19 +169,19 @@ async def test_multiple_get_or_create_db_calls_deduplicated(
|
||||
|
||||
# First call should create engine and run migrations
|
||||
await db.get_or_create_db(app_config.database_path, app_config=app_config)
|
||||
|
||||
|
||||
# Verify migrations were called
|
||||
mock_alembic_command.upgrade.assert_called_once_with(mock_alembic_config, "head")
|
||||
mock_search_repository.init_search_index.assert_called_once()
|
||||
|
||||
|
||||
# Reset mocks for subsequent calls
|
||||
mock_alembic_command.reset_mock()
|
||||
mock_search_repository.reset_mock()
|
||||
|
||||
|
||||
# Subsequent calls should not run migrations again
|
||||
await db.get_or_create_db(app_config.database_path, app_config=app_config)
|
||||
await db.get_or_create_db(app_config.database_path, app_config=app_config)
|
||||
|
||||
|
||||
# Verify migrations were NOT called again
|
||||
mock_alembic_command.upgrade.assert_not_called()
|
||||
mock_search_repository.init_search_index.assert_not_called()
|
||||
mock_search_repository.init_search_index.assert_not_called()
|
||||
|
||||
Reference in New Issue
Block a user