mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
243 lines
8 KiB
Python
243 lines
8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright (c) 2025 Allaun
|
|
|
|
"""Database connection management with connection pooling.
|
|
|
|
This module provides SQLite database connection management with
|
|
basic connection pooling using only standard library.
|
|
|
|
Key Features:
|
|
- SQLite connection management
|
|
- Basic connection pooling
|
|
- Thread-safe connection handling
|
|
- Transaction management
|
|
- Error handling with resilience
|
|
|
|
Dependencies:
|
|
- sqlite3 (standard library only)
|
|
- threading (standard library only)
|
|
"""
|
|
|
|
import sqlite3
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Optional, Any, Dict, List, Tuple, Union, TypeVar
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
class DatabaseConnection:
|
|
"""SQLite database connection with basic pooling.
|
|
|
|
Responsibilities:
|
|
- Manage database connections
|
|
- Provide thread-safe access
|
|
- Handle transactions
|
|
- Manage connection lifecycle
|
|
|
|
Thread Safety:
|
|
- Singleton access via get_instance() is fully thread-safe
|
|
- Instance creation happens inside a lock to prevent race conditions
|
|
- Each thread gets its own connection via thread-local storage
|
|
- Direct instantiation is allowed but not recommended for singleton pattern
|
|
"""
|
|
|
|
_instances: Dict[str, 'DatabaseConnection'] = {}
|
|
_lock = threading.Lock()
|
|
|
|
def __init__(self, db_path: str = "output/index.db"):
|
|
"""Initialize database connection.
|
|
|
|
Args:
|
|
db_path: Path to SQLite database file
|
|
|
|
Note:
|
|
For thread-safe singleton access, use get_instance() instead of
|
|
direct instantiation. Direct instantiation bypasses the singleton
|
|
pattern and may create multiple connections to the same database.
|
|
"""
|
|
if db_path == ":memory:":
|
|
self.db_path = ":memory:"
|
|
else:
|
|
self.db_path = str(Path(db_path).absolute())
|
|
self._local = threading.local()
|
|
# Track initialization to detect potential issues
|
|
self._initialized = True
|
|
|
|
@classmethod
|
|
def get_instance(cls, db_path: str = "output/index.db") -> 'DatabaseConnection':
|
|
"""Get singleton instance of DatabaseConnection.
|
|
|
|
Thread-safe singleton access. Only ONE instance per db_path is created,
|
|
even under concurrent load. Instance creation happens inside a lock to
|
|
prevent race conditions that could cause:
|
|
- Multiple connections to the same database
|
|
- WAL file corruption
|
|
- Transaction isolation violations
|
|
- Memory leaks from orphaned connections
|
|
|
|
Args:
|
|
db_path: Path to SQLite database file
|
|
|
|
Returns:
|
|
DatabaseConnection instance for the specified db_path
|
|
|
|
Example:
|
|
>>> db1 = DatabaseConnection.get_instance("mydb.db")
|
|
>>> db2 = DatabaseConnection.get_instance("mydb.db")
|
|
>>> db1 is db2 # True - same instance
|
|
"""
|
|
with cls._lock:
|
|
if db_path not in cls._instances:
|
|
# Create instance inside lock to prevent race conditions
|
|
cls._instances[db_path] = cls(db_path)
|
|
return cls._instances[db_path]
|
|
|
|
def get_connection(self) -> sqlite3.Connection:
|
|
"""Get thread-local database connection.
|
|
|
|
Returns:
|
|
sqlite3.Connection instance
|
|
"""
|
|
if not hasattr(self._local, 'connection'):
|
|
# Create database directory if it doesn't exist
|
|
db_dir = Path(self.db_path).parent
|
|
if db_dir and not db_dir.exists():
|
|
db_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Connect to database with timeout and isolation level
|
|
connection = sqlite3.connect(
|
|
self.db_path,
|
|
timeout=30.0,
|
|
isolation_level='IMMEDIATE',
|
|
check_same_thread=False
|
|
)
|
|
|
|
# Configure connection for better performance
|
|
connection.execute('PRAGMA journal_mode=WAL')
|
|
connection.execute('PRAGMA synchronous=NORMAL')
|
|
connection.execute('PRAGMA temp_store=MEMORY')
|
|
connection.execute('PRAGMA cache_size=-20000') # 20MB cache
|
|
|
|
# Enable foreign key constraints
|
|
connection.execute('PRAGMA foreign_keys=ON')
|
|
|
|
self._local.connection = connection
|
|
|
|
return self._local.connection
|
|
|
|
def execute(
|
|
self,
|
|
query: str,
|
|
params: Optional[Union[Tuple[Any, ...], Dict[str, Any]]] = None
|
|
) -> sqlite3.Cursor:
|
|
"""Execute SQL query with parameters.
|
|
|
|
Args:
|
|
query: SQL query to execute
|
|
params: Query parameters
|
|
|
|
Returns:
|
|
sqlite3.Cursor with results
|
|
"""
|
|
conn = self.get_connection()
|
|
try:
|
|
if params:
|
|
return conn.execute(query, params)
|
|
else:
|
|
return conn.execute(query)
|
|
except sqlite3.Error as e:
|
|
print(f"[ERROR] Database query failed: {e}")
|
|
raise
|
|
|
|
def executemany(
|
|
self,
|
|
query: str,
|
|
params_list: List[Union[Tuple[Any, ...], Dict[str, Any]]]
|
|
) -> sqlite3.Cursor:
|
|
"""Execute SQL query with multiple parameter sets.
|
|
|
|
Args:
|
|
query: SQL query to execute
|
|
params_list: List of parameter tuples
|
|
|
|
Returns:
|
|
sqlite3.Cursor with results
|
|
"""
|
|
conn = self.get_connection()
|
|
try:
|
|
return conn.executemany(query, params_list)
|
|
except sqlite3.Error as e:
|
|
print(f"[ERROR] Database batch query failed: {e}")
|
|
raise
|
|
|
|
def commit(self) -> None:
|
|
"""Commit current transaction."""
|
|
try:
|
|
self.get_connection().commit()
|
|
except sqlite3.Error as e:
|
|
print(f"[ERROR] Database commit failed: {e}")
|
|
raise
|
|
|
|
def rollback(self) -> None:
|
|
"""Roll back current transaction."""
|
|
try:
|
|
self.get_connection().rollback()
|
|
except sqlite3.Error as e:
|
|
print(f"[ERROR] Database rollback failed: {e}")
|
|
raise
|
|
|
|
def close(self) -> None:
|
|
"""Close database connection."""
|
|
if hasattr(self._local, 'connection'):
|
|
try:
|
|
self._local.connection.close()
|
|
except sqlite3.Error as e:
|
|
print(f"[ERROR] Database connection close failed: {e}")
|
|
finally:
|
|
del self._local.connection
|
|
|
|
def __del__(self) -> None:
|
|
"""Clean up database connection when object is destroyed."""
|
|
self.close()
|
|
|
|
def initialize_database(self) -> None:
|
|
"""Initialize database schema if it doesn't exist."""
|
|
conn = self.get_connection()
|
|
|
|
# Delegate to schema manager to ensure canonical schema and migrations
|
|
try:
|
|
from nodupe.tools.databases.schema import DatabaseSchema
|
|
schema = DatabaseSchema(conn)
|
|
schema.create_schema()
|
|
except Exception:
|
|
# Fallback: create minimal tables if schema manager fails
|
|
conn.execute('''
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
path TEXT NOT NULL UNIQUE,
|
|
size INTEGER NOT NULL,
|
|
modified_time INTEGER NOT NULL,
|
|
hash TEXT,
|
|
is_duplicate BOOLEAN DEFAULT FALSE,
|
|
duplicate_of INTEGER,
|
|
FOREIGN KEY (duplicate_of) REFERENCES files(id)
|
|
)
|
|
''')
|
|
conn.execute('CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)')
|
|
conn.execute('CREATE INDEX IF NOT EXISTS idx_files_hash ON files(hash)')
|
|
conn.execute('CREATE INDEX IF NOT EXISTS idx_files_size ON files(size)')
|
|
conn.execute('CREATE INDEX IF NOT EXISTS idx_files_duplicate ON files(is_duplicate)')
|
|
conn.commit()
|
|
|
|
|
|
def get_connection(db_path: str = "output/index.db") -> DatabaseConnection:
|
|
"""Get database connection instance.
|
|
|
|
Args:
|
|
db_path: Path to SQLite database file
|
|
|
|
Returns:
|
|
DatabaseConnection instance
|
|
"""
|
|
return DatabaseConnection.get_instance(db_path)
|