Skip to content

Commit 82ba12f

Browse files
fix: resolve Copilot review issues for test compatibility
- Fix asyncio.Lock event loop binding issue by using lazy initialization (prevents binding to wrong event loop at module import time) - Replace PostgreSQL-specific upsert with database-agnostic get-then-update pattern (enables SQLite compatibility for tests) - Remove sqlalchemy.dialects.postgresql import dependency Addresses GitHub Copilot review comments on PR #4127. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c4d18d1 commit 82ba12f

2 files changed

Lines changed: 69 additions & 56 deletions

File tree

core/database/connection.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,23 @@
3636
_connector = None
3737

3838
# Thread safety locks for lazy initialization
39-
_async_init_lock = asyncio.Lock()
39+
# Note: asyncio.Lock is created lazily to avoid event loop binding issues at module import
40+
_async_init_lock: Optional[asyncio.Lock] = None
4041
_sync_init_lock = threading.Lock()
4142

4243

44+
def _get_async_lock() -> asyncio.Lock:
45+
"""Get or create the async initialization lock.
46+
47+
Creates the lock lazily to avoid event loop binding issues
48+
when the module is imported before an event loop exists.
49+
"""
50+
global _async_init_lock
51+
if _async_init_lock is None:
52+
_async_init_lock = asyncio.Lock()
53+
return _async_init_lock
54+
55+
4356
class Base(DeclarativeBase):
4457
"""Base class for all SQLAlchemy models."""
4558

@@ -224,7 +237,7 @@ async def get_db() -> AsyncGenerator[AsyncSession | None, None]:
224237
"""
225238
# Thread-safe lazy initialization using asyncio.Lock
226239
if engine is None:
227-
async with _async_init_lock:
240+
async with _get_async_lock():
228241
# Double-check after acquiring lock
229242
if engine is None:
230243
await init_db()
@@ -253,7 +266,7 @@ async def get_db_context() -> AsyncGenerator[AsyncSession, None]:
253266
"""
254267
# Thread-safe lazy initialization using asyncio.Lock
255268
if engine is None:
256-
async with _async_init_lock:
269+
async with _get_async_lock():
257270
# Double-check after acquiring lock
258271
if engine is None:
259272
await init_db()

core/database/repositories.py

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Optional
88

99
from sqlalchemy import select
10-
from sqlalchemy.dialects.postgresql import insert
1110
from sqlalchemy.ext.asyncio import AsyncSession
1211
from sqlalchemy.orm import selectinload
1312

@@ -189,7 +188,10 @@ async def delete(self, spec_id: str) -> bool:
189188

190189
async def upsert(self, spec_data: dict) -> Spec:
191190
"""
192-
Create or update a spec atomically using INSERT ... ON CONFLICT.
191+
Create or update a spec.
192+
193+
Uses get-then-update pattern for database compatibility (works with both
194+
PostgreSQL and SQLite).
193195
194196
Args:
195197
spec_data: Dict with spec attributes including 'id'
@@ -208,22 +210,20 @@ async def upsert(self, spec_data: dict) -> Spec:
208210
if not spec_id:
209211
raise ValueError("spec_data must include 'id' field")
210212

211-
# Build update set with only allowed fields
212-
update_set = {k: v for k, v in spec_data.items() if k in SPEC_UPDATABLE_FIELDS}
213-
214-
# Atomic upsert using PostgreSQL INSERT ... ON CONFLICT
215-
stmt = (
216-
insert(Spec)
217-
.values(**spec_data)
218-
.on_conflict_do_update(index_elements=["id"], set_=update_set)
219-
.returning(Spec)
220-
)
213+
# Try to find existing spec
214+
existing = await self.get_by_id(spec_id)
221215

222-
result = await self.session.execute(stmt)
223-
await self.session.commit()
224-
spec = result.scalar_one()
225-
await self.session.refresh(spec)
226-
return spec
216+
if existing:
217+
# Update existing spec with only allowed fields
218+
for key, value in spec_data.items():
219+
if key in SPEC_UPDATABLE_FIELDS:
220+
setattr(existing, key, value)
221+
await self.session.commit()
222+
await self.session.refresh(existing)
223+
return existing
224+
else:
225+
# Create new spec
226+
return await self.create(spec_data)
227227

228228

229229
class LibraryRepository:
@@ -316,7 +316,10 @@ async def delete(self, library_id: str) -> bool:
316316

317317
async def upsert(self, library_data: dict) -> Library:
318318
"""
319-
Create or update a library atomically using INSERT ... ON CONFLICT.
319+
Create or update a library.
320+
321+
Uses get-then-update pattern for database compatibility (works with both
322+
PostgreSQL and SQLite).
320323
321324
Args:
322325
library_data: Dict with library attributes including 'id'
@@ -328,22 +331,20 @@ async def upsert(self, library_data: dict) -> Library:
328331
if not library_id:
329332
raise ValueError("library_data must include 'id' field")
330333

331-
# Build update set with only allowed fields
332-
update_set = {k: v for k, v in library_data.items() if k in LIBRARY_UPDATABLE_FIELDS}
334+
# Try to find existing library
335+
existing = await self.get_by_id(library_id)
333336

334-
# Atomic upsert using PostgreSQL INSERT ... ON CONFLICT
335-
stmt = (
336-
insert(Library)
337-
.values(**library_data)
338-
.on_conflict_do_update(index_elements=["id"], set_=update_set)
339-
.returning(Library)
340-
)
341-
342-
result = await self.session.execute(stmt)
343-
await self.session.commit()
344-
library = result.scalar_one()
345-
await self.session.refresh(library)
346-
return library
337+
if existing:
338+
# Update existing library with only allowed fields
339+
for key, value in library_data.items():
340+
if key in LIBRARY_UPDATABLE_FIELDS:
341+
setattr(existing, key, value)
342+
await self.session.commit()
343+
await self.session.refresh(existing)
344+
return existing
345+
else:
346+
# Create new library
347+
return await self.create(library_data)
347348

348349

349350
class ImplRepository:
@@ -467,7 +468,10 @@ async def delete(self, impl_id: str) -> bool:
467468

468469
async def upsert(self, spec_id: str, library_id: str, impl_data: dict) -> Impl:
469470
"""
470-
Create or update an implementation atomically using INSERT ... ON CONFLICT.
471+
Create or update an implementation.
472+
473+
Uses get-then-update pattern for database compatibility (works with both
474+
PostgreSQL and SQLite).
471475
472476
Args:
473477
spec_id: The specification ID
@@ -483,22 +487,18 @@ async def upsert(self, spec_id: str, library_id: str, impl_data: dict) -> Impl:
483487
"quality_score": 95
484488
})
485489
"""
486-
# Build the full data with spec_id and library_id
487-
full_data = {**impl_data, "spec_id": spec_id, "library_id": library_id}
488-
489-
# Build update set with only allowed fields
490-
update_set = {k: v for k, v in impl_data.items() if k in IMPL_UPDATABLE_FIELDS}
491-
492-
# Atomic upsert using PostgreSQL INSERT ... ON CONFLICT on the unique constraint
493-
stmt = (
494-
insert(Impl)
495-
.values(**full_data)
496-
.on_conflict_do_update(constraint="uq_impl", set_=update_set)
497-
.returning(Impl)
498-
)
499-
500-
result = await self.session.execute(stmt)
501-
await self.session.commit()
502-
impl = result.scalar_one()
503-
await self.session.refresh(impl)
504-
return impl
490+
# Try to find existing implementation
491+
existing = await self.get_by_spec_and_library(spec_id, library_id)
492+
493+
if existing:
494+
# Update existing implementation with only allowed fields
495+
for key, value in impl_data.items():
496+
if key in IMPL_UPDATABLE_FIELDS:
497+
setattr(existing, key, value)
498+
await self.session.commit()
499+
await self.session.refresh(existing)
500+
return existing
501+
else:
502+
# Create new implementation
503+
full_data = {**impl_data, "spec_id": spec_id, "library_id": library_id}
504+
return await self.create(full_data)

0 commit comments

Comments
 (0)