66import json
77import sqlite3
88import hashlib
9+ import logging
910from pathlib import Path
1011from typing import Dict , List , Optional , Any
1112from datetime import datetime
1213
14+ # Configure logging
15+ logging .basicConfig (level = logging .INFO )
16+ logger = logging .getLogger (__name__ )
17+
1318# Default projects directory
1419PROJECTS_DIR = os .path .expanduser ("~/.picocode/projects" )
1520
21+ # Retry configuration for database operations
22+ DB_RETRY_COUNT = 3
23+ DB_RETRY_DELAY = 0.1 # seconds
24+
1625
1726def _ensure_projects_dir ():
1827 """Ensure projects directory exists."""
19- os .makedirs (PROJECTS_DIR , exist_ok = True )
28+ try :
29+ os .makedirs (PROJECTS_DIR , exist_ok = True )
30+ except Exception as e :
31+ logger .error (f"Failed to create projects directory { PROJECTS_DIR } : { e } " )
32+ raise
33+
34+
35+ def _retry_on_db_locked (func , * args , max_retries = DB_RETRY_COUNT , ** kwargs ):
36+ """Retry a database operation if it's locked."""
37+ import time
38+ last_error = None
39+
40+ for attempt in range (max_retries ):
41+ try :
42+ return func (* args , ** kwargs )
43+ except sqlite3 .OperationalError as e :
44+ if "database is locked" in str (e ).lower () and attempt < max_retries - 1 :
45+ last_error = e
46+ time .sleep (DB_RETRY_DELAY * (2 ** attempt )) # Exponential backoff
47+ continue
48+ raise
49+ except Exception as e :
50+ raise
51+
52+ if last_error :
53+ raise last_error
2054
2155
2256def _get_project_id (project_path : str ) -> str :
@@ -37,29 +71,44 @@ def _get_projects_registry_path() -> str:
3771
3872
3973def _init_registry_db ():
40- """Initialize the projects registry database."""
74+ """Initialize the projects registry database with proper configuration ."""
4175 registry_path = _get_projects_registry_path ()
42- conn = sqlite3 .connect (registry_path )
43- conn .row_factory = sqlite3 .Row
44- try :
45- cur = conn .cursor ()
46- cur .execute (
47- """
48- CREATE TABLE IF NOT EXISTS projects (
49- id TEXT PRIMARY KEY,
50- name TEXT NOT NULL,
51- path TEXT NOT NULL UNIQUE,
52- database_path TEXT NOT NULL,
53- created_at TEXT DEFAULT (datetime('now')),
54- last_indexed_at TEXT,
55- status TEXT DEFAULT 'created',
56- settings TEXT
76+
77+ def _init ():
78+ conn = sqlite3 .connect (registry_path , timeout = 10.0 )
79+ conn .row_factory = sqlite3 .Row
80+ try :
81+ # Enable WAL mode for better concurrency
82+ conn .execute ("PRAGMA journal_mode=WAL;" )
83+ conn .execute ("PRAGMA synchronous=NORMAL;" )
84+
85+ cur = conn .cursor ()
86+ cur .execute (
87+ """
88+ CREATE TABLE IF NOT EXISTS projects (
89+ id TEXT PRIMARY KEY,
90+ name TEXT NOT NULL,
91+ path TEXT NOT NULL UNIQUE,
92+ database_path TEXT NOT NULL,
93+ created_at TEXT DEFAULT (datetime('now')),
94+ last_indexed_at TEXT,
95+ status TEXT DEFAULT 'created',
96+ settings TEXT
97+ )
98+ """
5799 )
58- """
59- )
60- conn .commit ()
61- finally :
62- conn .close ()
100+ conn .commit ()
101+ except Exception as e :
102+ logger .error (f"Failed to initialize registry database: { e } " )
103+ raise
104+ finally :
105+ conn .close ()
106+
107+ try :
108+ _retry_on_db_locked (_init )
109+ except Exception as e :
110+ logger .error (f"Failed to initialize registry after retries: { e } " )
111+ raise
63112
64113
65114def create_project (project_path : str , name : Optional [str ] = None ) -> Dict [str , Any ]:
@@ -72,11 +121,25 @@ def create_project(project_path: str, name: Optional[str] = None) -> Dict[str, A
72121
73122 Returns:
74123 Project metadata dictionary
124+
125+ Raises:
126+ ValueError: If project path is invalid
127+ RuntimeError: If database operations fail
75128 """
76- _init_registry_db ()
129+ try :
130+ _init_registry_db ()
131+ except Exception as e :
132+ logger .error (f"Failed to initialize registry: { e } " )
133+ raise RuntimeError (f"Database initialization failed: { e } " )
77134
78- # Normalize path
79- project_path = os .path .abspath (project_path )
135+ # Validate and normalize path
136+ if not project_path or not isinstance (project_path , str ):
137+ raise ValueError ("Project path must be a non-empty string" )
138+
139+ try :
140+ project_path = os .path .abspath (project_path )
141+ except Exception as e :
142+ raise ValueError (f"Invalid project path: { e } " )
80143
81144 if not os .path .exists (project_path ):
82145 raise ValueError (f"Project path does not exist: { project_path } " )
@@ -92,38 +155,59 @@ def create_project(project_path: str, name: Optional[str] = None) -> Dict[str, A
92155 if not name :
93156 name = os .path .basename (project_path )
94157
158+ # Sanitize project name
159+ if name and len (name ) > 255 :
160+ name = name [:255 ]
161+
95162 registry_path = _get_projects_registry_path ()
96- conn = sqlite3 .connect (registry_path )
97- conn .row_factory = sqlite3 .Row
163+
164+ def _create ():
165+ conn = sqlite3 .connect (registry_path , timeout = 10.0 )
166+ conn .row_factory = sqlite3 .Row
167+ try :
168+ cur = conn .cursor ()
169+
170+ # Check if project already exists
171+ cur .execute ("SELECT * FROM projects WHERE path = ?" , (project_path ,))
172+ existing = cur .fetchone ()
173+ if existing :
174+ logger .info (f"Project already exists: { project_path } " )
175+ return dict (existing )
176+
177+ # Insert new project
178+ cur .execute (
179+ """
180+ INSERT INTO projects (id, name, path, database_path, status)
181+ VALUES (?, ?, ?, ?, 'created')
182+ """ ,
183+ (project_id , name , project_path , db_path )
184+ )
185+ conn .commit ()
186+
187+ # Initialize project database
188+ try :
189+ from db import init_db
190+ init_db (db_path )
191+ logger .info (f"Created project { project_id } at { db_path } " )
192+ except Exception as e :
193+ logger .error (f"Failed to initialize project database: { e } " )
194+ # Rollback project creation
195+ cur .execute ("DELETE FROM projects WHERE id = ?" , (project_id ,))
196+ conn .commit ()
197+ raise RuntimeError (f"Failed to initialize project database: { e } " )
198+
199+ # Return project metadata
200+ cur .execute ("SELECT * FROM projects WHERE id = ?" , (project_id ,))
201+ row = cur .fetchone ()
202+ return dict (row ) if row else None
203+ finally :
204+ conn .close ()
205+
98206 try :
99- cur = conn .cursor ()
100-
101- # Check if project already exists
102- cur .execute ("SELECT * FROM projects WHERE path = ?" , (project_path ,))
103- existing = cur .fetchone ()
104- if existing :
105- return dict (existing )
106-
107- # Insert new project
108- cur .execute (
109- """
110- INSERT INTO projects (id, name, path, database_path, status)
111- VALUES (?, ?, ?, ?, 'created')
112- """ ,
113- (project_id , name , project_path , db_path )
114- )
115- conn .commit ()
116-
117- # Initialize project database
118- from db import init_db
119- init_db (db_path )
120-
121- # Return project metadata
122- cur .execute ("SELECT * FROM projects WHERE id = ?" , (project_id ,))
123- row = cur .fetchone ()
124- return dict (row )
125- finally :
126- conn .close ()
207+ return _retry_on_db_locked (_create )
208+ except Exception as e :
209+ logger .error (f"Failed to create project: { e } " )
210+ raise
127211
128212
129213def get_project (project_path : str ) -> Optional [Dict [str , Any ]]:
0 commit comments