77from typing import Optional
88
99from sqlalchemy import select
10- from sqlalchemy .dialects .postgresql import insert
1110from sqlalchemy .ext .asyncio import AsyncSession
1211from 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
229229class 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
349350class 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