Skip to content

Commit c34eb80

Browse files
CopilotMte90
andcommitted
Move all database operations to db.py
Co-authored-by: Mte90 <403283+Mte90@users.noreply.github.com>
1 parent 41268b5 commit c34eb80

File tree

2 files changed

+365
-410
lines changed

2 files changed

+365
-410
lines changed

db.py

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,344 @@ def delete_analysis(database_path: str, analysis_id: int) -> None:
341341
conn.commit()
342342
finally:
343343
conn.close()
344+
345+
346+
# ============================================================================
347+
# Project Registry Database Operations
348+
# ============================================================================
349+
350+
# Default projects directory
351+
PROJECTS_DIR = os.path.expanduser("~/.picocode/projects")
352+
353+
# Retry configuration for database operations
354+
DB_RETRY_COUNT = 3
355+
DB_RETRY_DELAY = 0.1 # seconds
356+
357+
358+
def _ensure_projects_dir():
359+
"""Ensure projects directory exists."""
360+
try:
361+
os.makedirs(PROJECTS_DIR, exist_ok=True)
362+
except Exception as e:
363+
_LOG.error(f"Failed to create projects directory {PROJECTS_DIR}: {e}")
364+
raise
365+
366+
367+
def _retry_on_db_locked(func, *args, max_retries=DB_RETRY_COUNT, **kwargs):
368+
"""Retry a database operation if it's locked."""
369+
import time
370+
last_error = None
371+
372+
for attempt in range(max_retries):
373+
try:
374+
return func(*args, **kwargs)
375+
except sqlite3.OperationalError as e:
376+
if "database is locked" in str(e).lower() and attempt < max_retries - 1:
377+
last_error = e
378+
time.sleep(DB_RETRY_DELAY * (2 ** attempt)) # Exponential backoff
379+
continue
380+
raise
381+
except Exception as e:
382+
raise
383+
384+
if last_error:
385+
raise last_error
386+
387+
388+
def _get_project_id(project_path: str) -> str:
389+
"""Generate a stable project ID from the project path."""
390+
import hashlib
391+
return hashlib.sha256(project_path.encode()).hexdigest()[:16]
392+
393+
394+
def _get_project_db_path(project_id: str) -> str:
395+
"""Get the database path for a project."""
396+
_ensure_projects_dir()
397+
return os.path.join(PROJECTS_DIR, f"{project_id}.db")
398+
399+
400+
def _get_projects_registry_path() -> str:
401+
"""Get the path to the projects registry database."""
402+
_ensure_projects_dir()
403+
return os.path.join(PROJECTS_DIR, "registry.db")
404+
405+
406+
def _init_registry_db():
407+
"""Initialize the projects registry database with proper configuration."""
408+
registry_path = _get_projects_registry_path()
409+
410+
def _init():
411+
conn = _get_connection(registry_path)
412+
try:
413+
cur = conn.cursor()
414+
cur.execute(
415+
"""
416+
CREATE TABLE IF NOT EXISTS projects (
417+
id TEXT PRIMARY KEY,
418+
name TEXT NOT NULL,
419+
path TEXT NOT NULL UNIQUE,
420+
database_path TEXT NOT NULL,
421+
created_at TEXT DEFAULT (datetime('now')),
422+
last_indexed_at TEXT,
423+
status TEXT DEFAULT 'created',
424+
settings TEXT
425+
)
426+
"""
427+
)
428+
conn.commit()
429+
except Exception as e:
430+
_LOG.error(f"Failed to initialize registry database: {e}")
431+
raise
432+
finally:
433+
conn.close()
434+
435+
try:
436+
_retry_on_db_locked(_init)
437+
except Exception as e:
438+
_LOG.error(f"Failed to initialize registry after retries: {e}")
439+
raise
440+
441+
442+
def create_project(project_path: str, name: Optional[str] = None) -> Dict[str, Any]:
443+
"""
444+
Create a new project entry with its own database.
445+
446+
Args:
447+
project_path: Absolute path to the project directory
448+
name: Optional project name (defaults to directory name)
449+
450+
Returns:
451+
Project metadata dictionary
452+
453+
Raises:
454+
ValueError: If project path is invalid
455+
RuntimeError: If database operations fail
456+
"""
457+
try:
458+
_init_registry_db()
459+
except Exception as e:
460+
_LOG.error(f"Failed to initialize registry: {e}")
461+
raise RuntimeError(f"Database initialization failed: {e}")
462+
463+
# Validate and normalize path
464+
if not project_path or not isinstance(project_path, str):
465+
raise ValueError("Project path must be a non-empty string")
466+
467+
# Check for path traversal attempts
468+
if ".." in project_path or project_path.startswith("~"):
469+
raise ValueError("Path traversal not allowed in project path")
470+
471+
try:
472+
project_path = os.path.abspath(os.path.realpath(project_path))
473+
except Exception as e:
474+
raise ValueError(f"Invalid project path: {e}")
475+
476+
try:
477+
path_exists = os.path.exists(project_path) # nosec
478+
if not path_exists:
479+
raise ValueError(f"Project path does not exist")
480+
481+
is_directory = os.path.isdir(project_path) # nosec
482+
if not is_directory:
483+
raise ValueError(f"Project path is not a directory")
484+
except (OSError, ValueError) as e:
485+
if isinstance(e, ValueError):
486+
raise
487+
raise ValueError(f"Cannot access project path")
488+
489+
project_id = _get_project_id(project_path)
490+
db_path = _get_project_db_path(project_id)
491+
492+
if not name:
493+
name = os.path.basename(project_path)
494+
495+
if name and len(name) > 255:
496+
name = name[:255]
497+
498+
registry_path = _get_projects_registry_path()
499+
500+
def _create():
501+
conn = _get_connection(registry_path)
502+
try:
503+
cur = conn.cursor()
504+
505+
cur.execute("SELECT * FROM projects WHERE path = ?", (project_path,))
506+
existing = cur.fetchone()
507+
if existing:
508+
_LOG.info(f"Project already exists: {project_path}")
509+
return dict(existing)
510+
511+
cur.execute(
512+
"""
513+
INSERT INTO projects (id, name, path, database_path, status)
514+
VALUES (?, ?, ?, ?, 'created')
515+
""",
516+
(project_id, name, project_path, db_path)
517+
)
518+
conn.commit()
519+
520+
try:
521+
init_db(db_path)
522+
_LOG.info(f"Created project {project_id} at {db_path}")
523+
except Exception as e:
524+
_LOG.error(f"Failed to initialize project database: {e}")
525+
cur.execute("DELETE FROM projects WHERE id = ?", (project_id,))
526+
conn.commit()
527+
raise RuntimeError(f"Failed to initialize project database: {e}")
528+
529+
cur.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
530+
row = cur.fetchone()
531+
return dict(row) if row else None
532+
finally:
533+
conn.close()
534+
535+
try:
536+
return _retry_on_db_locked(_create)
537+
except Exception as e:
538+
_LOG.error(f"Failed to create project: {e}")
539+
raise
540+
541+
542+
def get_project(project_path: str) -> Optional[Dict[str, Any]]:
543+
"""Get project metadata by path."""
544+
_init_registry_db()
545+
project_path = os.path.abspath(project_path)
546+
547+
registry_path = _get_projects_registry_path()
548+
549+
def _get():
550+
conn = _get_connection(registry_path)
551+
try:
552+
cur = conn.cursor()
553+
cur.execute("SELECT * FROM projects WHERE path = ?", (project_path,))
554+
row = cur.fetchone()
555+
return dict(row) if row else None
556+
finally:
557+
conn.close()
558+
559+
return _retry_on_db_locked(_get)
560+
561+
562+
def get_project_by_id(project_id: str) -> Optional[Dict[str, Any]]:
563+
"""Get project metadata by ID."""
564+
_init_registry_db()
565+
566+
registry_path = _get_projects_registry_path()
567+
568+
def _get():
569+
conn = _get_connection(registry_path)
570+
try:
571+
cur = conn.cursor()
572+
cur.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
573+
row = cur.fetchone()
574+
return dict(row) if row else None
575+
finally:
576+
conn.close()
577+
578+
return _retry_on_db_locked(_get)
579+
580+
581+
def list_projects() -> List[Dict[str, Any]]:
582+
"""List all registered projects."""
583+
_init_registry_db()
584+
585+
registry_path = _get_projects_registry_path()
586+
587+
def _list():
588+
conn = _get_connection(registry_path)
589+
try:
590+
cur = conn.cursor()
591+
cur.execute("SELECT * FROM projects ORDER BY created_at DESC")
592+
rows = cur.fetchall()
593+
return [dict(row) for row in rows]
594+
finally:
595+
conn.close()
596+
597+
return _retry_on_db_locked(_list)
598+
599+
600+
def update_project_status(project_id: str, status: str, last_indexed_at: Optional[str] = None):
601+
"""Update project indexing status."""
602+
_init_registry_db()
603+
604+
registry_path = _get_projects_registry_path()
605+
606+
def _update():
607+
conn = _get_connection(registry_path)
608+
try:
609+
cur = conn.cursor()
610+
if last_indexed_at:
611+
cur.execute(
612+
"UPDATE projects SET status = ?, last_indexed_at = ? WHERE id = ?",
613+
(status, last_indexed_at, project_id)
614+
)
615+
else:
616+
cur.execute(
617+
"UPDATE projects SET status = ? WHERE id = ?",
618+
(status, project_id)
619+
)
620+
conn.commit()
621+
finally:
622+
conn.close()
623+
624+
_retry_on_db_locked(_update)
625+
626+
627+
def update_project_settings(project_id: str, settings: Dict[str, Any]):
628+
"""Update project settings (stored as JSON)."""
629+
import json
630+
_init_registry_db()
631+
632+
registry_path = _get_projects_registry_path()
633+
634+
def _update():
635+
conn = _get_connection(registry_path)
636+
try:
637+
cur = conn.cursor()
638+
cur.execute(
639+
"UPDATE projects SET settings = ? WHERE id = ?",
640+
(json.dumps(settings), project_id)
641+
)
642+
conn.commit()
643+
finally:
644+
conn.close()
645+
646+
_retry_on_db_locked(_update)
647+
648+
649+
def delete_project(project_id: str):
650+
"""Delete a project and its database."""
651+
_init_registry_db()
652+
653+
project = get_project_by_id(project_id)
654+
if not project:
655+
raise ValueError(f"Project not found: {project_id}")
656+
657+
db_path = project.get("database_path")
658+
if db_path and os.path.exists(db_path):
659+
try:
660+
os.remove(db_path)
661+
except Exception:
662+
pass
663+
664+
registry_path = _get_projects_registry_path()
665+
666+
def _delete():
667+
conn = _get_connection(registry_path)
668+
try:
669+
cur = conn.cursor()
670+
cur.execute("DELETE FROM projects WHERE id = ?", (project_id,))
671+
conn.commit()
672+
finally:
673+
conn.close()
674+
675+
_retry_on_db_locked(_delete)
676+
677+
678+
def get_or_create_project(project_path: str, name: Optional[str] = None) -> Dict[str, Any]:
679+
"""Get existing project or create new one."""
680+
project = get_project(project_path)
681+
if project:
682+
return project
683+
return create_project(project_path, name)
684+

0 commit comments

Comments
 (0)