@@ -106,6 +106,8 @@ def _get_connection(self):
106106 """Get database connection with proper cleanup."""
107107 conn = sqlite3 .connect (self .db_path , timeout = 30.0 )
108108 conn .row_factory = sqlite3 .Row
109+ # Enable foreign key constraints for this connection
110+ conn .execute ("PRAGMA foreign_keys=ON" )
109111 try :
110112 yield conn
111113 conn .commit ()
@@ -267,11 +269,58 @@ def list_tasks(self, filters: Optional[Dict[str, Any]] = None) -> List[Task]:
267269 cursor = conn .execute (query , values )
268270 return [Task .from_dict (dict (row )) for row in cursor .fetchall ()]
269271
272+ def _get_task_with_conn (self , task_id : str , conn : sqlite3 .Connection ) -> Optional [Task ]:
273+ """Get task using existing connection to avoid nesting."""
274+ cursor = conn .execute (
275+ "SELECT * FROM tasks WHERE id = ?" , (task_id ,)
276+ )
277+ row = cursor .fetchone ()
278+ if row :
279+ return Task .from_dict (dict (row ))
280+ return None
281+
282+ def _update_task_with_conn (self , task_id : str , updates : Dict [str , Any ], conn : sqlite3 .Connection ) -> Task :
283+ """Update task using existing connection to avoid nesting."""
284+ from datetime import timezone
285+
286+ if not updates :
287+ return self ._get_task_with_conn (task_id , conn )
288+
289+ # Build update query
290+ set_clauses = []
291+ params = []
292+
293+ for key , value in updates .items ():
294+ if key in ['id' , 'created_at' ]: # Don't allow updating immutable fields
295+ continue
296+ set_clauses .append (f"{ key } = ?" )
297+ params .append (value )
298+
299+ if not set_clauses :
300+ return self ._get_task_with_conn (task_id , conn )
301+
302+ # Always update timestamp and version
303+ set_clauses .append ("updated_at = ?" )
304+ set_clauses .append ("version = version + 1" )
305+ params .append (datetime .now (timezone .utc ).isoformat ())
306+ params .append (task_id )
307+
308+ query = f"UPDATE tasks SET { ', ' .join (set_clauses )} WHERE id = ?"
309+ cursor = conn .execute (query , params )
310+
311+ if cursor .rowcount == 0 :
312+ raise ValueError (f"Task { task_id } not found" )
313+
314+ # Log the update
315+ self ._log_event (conn , task_id , 'updated' , updates )
316+
317+ return self ._get_task_with_conn (task_id , conn )
318+
270319 def move_task (self , task_id : str , status : str ) -> Task :
271320 """Move task to new status with parent/child promotion logic."""
272321 with self ._get_connection () as conn :
273322 # Check if task exists
274- task = self .get_task (task_id )
323+ task = self ._get_task_with_conn (task_id , conn )
275324 if not task :
276325 raise ValueError (f"Task { task_id } not found" )
277326
@@ -290,7 +339,7 @@ def move_task(self, task_id: str, status: str) -> Task:
290339 raise ValueError ("Cannot move to ready: incomplete parent tasks" )
291340
292341 # Update task status
293- updated_task = self .update_task (task_id , {'status' : status })
342+ updated_task = self ._update_task_with_conn (task_id , {'status' : status }, conn )
294343
295344 # Promote children if moving to done
296345 if new_status == TaskStatus .DONE :
@@ -300,7 +349,7 @@ def move_task(self, task_id: str, status: str) -> Task:
300349
301350 for row in cursor .fetchall ():
302351 child_id = row ['child_id' ]
303- child_task = self .get_task (child_id )
352+ child_task = self ._get_task_with_conn (child_id , conn )
304353
305354 if child_task and child_task .status == TaskStatus .TODO :
306355 # Check if all other parents are done
@@ -312,7 +361,7 @@ def move_task(self, task_id: str, status: str) -> Task:
312361 """ , (child_id ,))
313362
314363 if parent_check .fetchone ()['incomplete_parents' ] == 0 :
315- self .update_task (child_id , {'status' : TaskStatus .READY .value })
364+ self ._update_task_with_conn (child_id , {'status' : TaskStatus .READY .value }, conn )
316365
317366 self ._log_event (conn , task_id , 'moved' , {
318367 'old_status' : task .status .value ,
@@ -484,12 +533,13 @@ def list_events(self, since: Optional[datetime] = None) -> List[TaskEvent]:
484533 def claim_task (self , task_id : str , worker_id : str ) -> bool :
485534 """Claim a ready task for execution (CAS operation)."""
486535 with self ._get_connection () as conn :
487- # Atomic claim with CAS on status and claim_lock
536+ # Atomic claim with CAS on status and claim_lock, increment version for optimistic locking
537+ from datetime import timezone
488538 result = conn .execute ("""
489539 UPDATE tasks
490- SET claim_lock = ?, updated_at = ?, status = 'running'
540+ SET claim_lock = ?, updated_at = ?, status = 'running', version = version + 1
491541 WHERE id = ? AND status = 'ready' AND (claim_lock IS NULL OR claim_lock = '')
492- """ , (worker_id , datetime .utcnow ( ).isoformat (), task_id ))
542+ """ , (worker_id , datetime .now ( timezone . utc ).isoformat (), task_id ))
493543
494544 if result .rowcount > 0 :
495545 self ._log_event (conn , task_id , 'claimed' , {'worker_id' : worker_id })
@@ -500,11 +550,13 @@ def claim_task(self, task_id: str, worker_id: str) -> bool:
500550 def release_claim (self , task_id : str , worker_id : str ) -> bool :
501551 """Release claim on task."""
502552 with self ._get_connection () as conn :
553+ # Release claim and increment version for optimistic locking
554+ from datetime import timezone
503555 result = conn .execute ("""
504556 UPDATE tasks
505- SET claim_lock = NULL, updated_at = ?, status = 'ready'
557+ SET claim_lock = NULL, updated_at = ?, status = 'ready', version = version + 1
506558 WHERE id = ? AND claim_lock = ?
507- """ , (datetime .utcnow ( ).isoformat (), task_id , worker_id ))
559+ """ , (datetime .now ( timezone . utc ).isoformat (), task_id , worker_id ))
508560
509561 if result .rowcount > 0 :
510562 self ._log_event (conn , task_id , 'released' , {'worker_id' : worker_id })
0 commit comments