55"""
66import os
77import json
8- import time
98import sqlite3
109import importlib .resources
1110from typing import List , Dict , Any , Optional
1211from utils .logger import get_logger
12+ from utils .retry import retry_on_exception
1313
1414logger = get_logger (__name__ )
1515
1818SQLITE_VECTOR_RESOURCE = "vector"
1919SQLITE_VECTOR_VERSION_FN = "vector_version" # SELECT vector_version();
2020
21- # Retry policy for DB-locked operations
21+ # Retry policy for DB-locked operations (used by insert_chunk_vector_with_retry)
2222DB_LOCK_RETRY_COUNT = 6
2323DB_LOCK_RETRY_BASE_DELAY = 0.05 # seconds, exponential backoff multiplier
2424
@@ -27,21 +27,18 @@ def connect_db(db_path: str, timeout: float = 30.0) -> sqlite3.Connection:
2727 """
2828 Create a database connection with appropriate timeout and settings.
2929
30+ DEPRECATED: Use db.connection.get_db_connection() instead.
31+ This function is maintained for backward compatibility.
32+
3033 Args:
3134 db_path: Path to the SQLite database file
3235 timeout: Timeout in seconds for waiting on locks
3336
3437 Returns:
3538 sqlite3.Connection object configured for vector operations
3639 """
37- # timeout instructs sqlite to wait up to `timeout` seconds for locks
38- conn = sqlite3 .connect (db_path , timeout = timeout , check_same_thread = False )
39- conn .row_factory = sqlite3 .Row
40- try :
41- conn .execute ("PRAGMA busy_timeout = 30000;" ) # 30s
42- except Exception :
43- pass
44- return conn
40+ from .connection import get_db_connection
41+ return get_db_connection (db_path , timeout = timeout , enable_vector = False )
4542
4643
4744def load_sqlite_vector_extension (conn : sqlite3 .Connection ) -> None :
@@ -163,30 +160,38 @@ def insert_chunk_vector_with_retry(conn: sqlite3.Connection, file_id: int, path:
163160
164161 q_vec = json .dumps (vector )
165162
166- attempt = 0
167- while True :
163+ # Use retry decorator for the actual insert operation
164+ @retry_on_exception (
165+ exceptions = (sqlite3 .OperationalError ,),
166+ max_retries = DB_LOCK_RETRY_COUNT ,
167+ base_delay = DB_LOCK_RETRY_BASE_DELAY ,
168+ exponential_backoff = True
169+ )
170+ def _insert_with_retry ():
171+ """Inner function with retry logic."""
172+ # Check if it's a database locked error
168173 try :
169- # use vector_as_f32(json) as per API so extension formats blob
170174 cur .execute ("INSERT INTO chunks (file_id, path, chunk_index, embedding) VALUES (?, ?, ?, vector_as_f32(?))" ,
171- (file_id , path , chunk_index , q_vec ))
175+ (file_id , path , chunk_index , q_vec ))
172176 conn .commit ()
173177 rowid = int (cur .lastrowid )
174178 logger .debug (f"Inserted chunk vector for { path } chunk { chunk_index } , rowid={ rowid } " )
175179 return rowid
176180 except sqlite3 .OperationalError as e :
177- msg = str (e ).lower ()
178- if "database is locked" in msg and attempt < DB_LOCK_RETRY_COUNT :
179- attempt += 1
180- delay = DB_LOCK_RETRY_BASE_DELAY * (2 ** (attempt - 1 ))
181- logger .warning (f"Database locked, retrying in { delay } s (attempt { attempt } /{ DB_LOCK_RETRY_COUNT } )" )
182- time .sleep (delay )
183- continue
184- else :
185- logger .error (f"Failed to insert chunk vector after { attempt } retries: { e } " )
181+ # Only retry on database locked errors
182+ if "database is locked" not in str (e ).lower ():
183+ logger .error (f"Failed to insert chunk vector: { e } " )
186184 raise RuntimeError (f"Failed to INSERT chunk vector (vector_as_f32 call): { e } " ) from e
185+ raise # Re-raise for retry decorator to handle
187186 except Exception as e :
188187 logger .error (f"Failed to insert chunk vector: { e } " )
189188 raise RuntimeError (f"Failed to INSERT chunk vector (vector_as_f32 call): { e } " ) from e
189+
190+ try :
191+ return _insert_with_retry ()
192+ except sqlite3 .OperationalError as e :
193+ logger .error (f"Failed to insert chunk vector after { DB_LOCK_RETRY_COUNT } retries: { e } " )
194+ raise RuntimeError (f"Failed to INSERT chunk vector after retries: { e } " ) from e
190195
191196
192197def search_vectors (database_path : str , q_vector : List [float ], top_k : int = 5 ) -> List [Dict [str , Any ]]:
@@ -204,10 +209,11 @@ def search_vectors(database_path: str, q_vector: List[float], top_k: int = 5) ->
204209 Raises:
205210 RuntimeError: If vector search operations fail
206211 """
212+ from .connection import db_connection
213+
207214 logger .debug (f"Searching vectors in database: { database_path } , top_k={ top_k } " )
208- conn = connect_db (database_path )
209- try :
210- load_sqlite_vector_extension (conn )
215+
216+ with db_connection (database_path , enable_vector = True ) as conn :
211217 ensure_chunks_and_meta (conn )
212218
213219 # Ensure vector index is initialized before searching
@@ -253,8 +259,6 @@ def search_vectors(database_path: str, q_vector: List[float], top_k: int = 5) ->
253259 score = float (distance )
254260 results .append ({"file_id" : int (file_id ), "path" : path , "chunk_index" : int (chunk_index ), "score" : score })
255261 return results
256- finally :
257- conn .close ()
258262
259263
260264def get_chunk_text (database_path : str , file_id : int , chunk_index : int ) -> Optional [str ]:
@@ -271,9 +275,9 @@ def get_chunk_text(database_path: str, file_id: int, chunk_index: int) -> Option
271275 The chunk text, or None if not found
272276 """
273277 from .operations import get_project_metadata
278+ from .connection import db_connection
274279
275- conn = connect_db (database_path )
276- try :
280+ with db_connection (database_path ) as conn :
277281 cur = conn .cursor ()
278282 # Get file path from database
279283 cur .execute ("SELECT path FROM files WHERE id = ?" , (file_id ,))
@@ -337,5 +341,3 @@ def get_chunk_text(database_path: str, file_id: int, chunk_index: int) -> Option
337341 start = chunk_index * step
338342 end = min (start + CHUNK_SIZE , len (content ))
339343 return content [start :end ]
340- finally :
341- conn .close ()
0 commit comments