mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-12 07:35:00 +00:00
160 lines
6.3 KiB
Python
160 lines
6.3 KiB
Python
# db_utils/base.py
|
|
# Base database connection and transaction management
|
|
|
|
import sqlite3
|
|
import time
|
|
from contextlib import contextmanager
|
|
from threading import RLock
|
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
import logging
|
|
|
|
from logger import Logger
|
|
|
|
logger = Logger(name="db_utils.base", level=logging.DEBUG)
|
|
|
|
|
|
class DatabaseBase:
|
|
"""
|
|
Base database manager providing connection, transaction, and query primitives.
|
|
All specialized operation modules inherit access to these primitives.
|
|
"""
|
|
|
|
def __init__(self, db_path: str):
|
|
self.db_path = db_path
|
|
|
|
# Connection with optimized settings for constrained devices (e.g., Raspberry Pi)
|
|
self._conn = sqlite3.connect(
|
|
self.db_path,
|
|
check_same_thread=False,
|
|
isolation_level=None # Autocommit mode (we manage transactions explicitly)
|
|
)
|
|
self._conn.row_factory = sqlite3.Row
|
|
self._lock = RLock()
|
|
|
|
# Small in-process cache for frequently refreshed UI counters
|
|
self._cache_ttl = 5.0 # seconds
|
|
self._stats_cache = {'data': None, 'timestamp': 0}
|
|
|
|
# Apply PRAGMA tuning
|
|
with self._lock:
|
|
cur = self._conn.cursor()
|
|
# Optimize SQLite for Raspberry Pi / flash storage
|
|
cur.execute("PRAGMA journal_mode=WAL;")
|
|
cur.execute("PRAGMA synchronous=NORMAL;")
|
|
cur.execute("PRAGMA foreign_keys=ON;")
|
|
cur.execute("PRAGMA cache_size=2000;") # Increase page cache
|
|
cur.execute("PRAGMA temp_store=MEMORY;") # Use RAM for temporary objects
|
|
cur.close()
|
|
|
|
logger.info(f"DatabaseBase initialized: {db_path}")
|
|
|
|
# =========================================================================
|
|
# CORE CONCURRENCY + SQL PRIMITIVES
|
|
# =========================================================================
|
|
|
|
@contextmanager
|
|
def _cursor(self):
|
|
"""Thread-safe cursor context manager"""
|
|
with self._lock:
|
|
cur = self._conn.cursor()
|
|
try:
|
|
yield cur
|
|
finally:
|
|
cur.close()
|
|
|
|
@contextmanager
|
|
def transaction(self, immediate: bool = True):
|
|
"""Transactional block with automatic rollback on error"""
|
|
with self._lock:
|
|
try:
|
|
self._conn.execute("BEGIN IMMEDIATE;" if immediate else "BEGIN;")
|
|
yield
|
|
self._conn.execute("COMMIT;")
|
|
except Exception:
|
|
self._conn.execute("ROLLBACK;")
|
|
raise
|
|
|
|
def execute(self, sql: str, params: Iterable[Any] = (), many: bool = False) -> int:
|
|
"""Execute a DML statement. Supports batch mode via `many=True`"""
|
|
with self._cursor() as c:
|
|
if many and params and isinstance(params, (list, tuple)) and isinstance(params[0], (list, tuple)):
|
|
c.executemany(sql, params)
|
|
return c.rowcount if c.rowcount is not None else 0
|
|
c.execute(sql, params)
|
|
return c.rowcount if c.rowcount is not None else 0
|
|
|
|
def executemany(self, sql: str, seq_of_params: Iterable[Iterable[Any]]) -> int:
|
|
"""Convenience wrapper around `execute(..., many=True)`"""
|
|
return self.execute(sql, seq_of_params, many=True)
|
|
|
|
def query(self, sql: str, params: Iterable[Any] = ()) -> List[Dict[str, Any]]:
|
|
"""Execute a SELECT and return rows as list[dict]"""
|
|
with self._cursor() as c:
|
|
c.execute(sql, params)
|
|
rows = c.fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
def query_one(self, sql: str, params: Iterable[Any] = ()) -> Optional[Dict[str, Any]]:
|
|
"""Execute a SELECT and return a single row as dict (or None)"""
|
|
with self._cursor() as c:
|
|
c.execute(sql, params)
|
|
row = c.fetchone()
|
|
return dict(row) if row else None
|
|
|
|
# =========================================================================
|
|
# CACHE MANAGEMENT
|
|
# =========================================================================
|
|
|
|
def invalidate_stats_cache(self):
|
|
"""Invalidate the small in-memory stats cache"""
|
|
self._stats_cache = {'data': None, 'timestamp': 0}
|
|
|
|
# =========================================================================
|
|
# SCHEMA HELPERS
|
|
# =========================================================================
|
|
|
|
def _table_exists(self, name: str) -> bool:
|
|
"""Return True if a table exists in the current database"""
|
|
row = self.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (name,))
|
|
return bool(row)
|
|
|
|
def _column_names(self, table: str) -> List[str]:
|
|
"""Return a list of column names for a given table (empty if table missing)"""
|
|
with self._cursor() as c:
|
|
c.execute(f"PRAGMA table_info({table});")
|
|
return [r[1] for r in c.fetchall()]
|
|
|
|
def _ensure_column(self, table: str, column: str, ddl: str) -> None:
|
|
"""Add a column with the provided DDL if it does not exist yet"""
|
|
cols = self._column_names(table) if self._table_exists(table) else []
|
|
if column not in cols:
|
|
self.execute(f"ALTER TABLE {table} ADD COLUMN {ddl};")
|
|
|
|
# =========================================================================
|
|
# MAINTENANCE OPERATIONS
|
|
# =========================================================================
|
|
|
|
def checkpoint(self, mode: str = "TRUNCATE") -> Tuple[int, int, int]:
|
|
"""
|
|
Force a WAL checkpoint. Returns (busy, log_frames, checkpointed_frames).
|
|
mode ∈ {PASSIVE, FULL, RESTART, TRUNCATE}
|
|
"""
|
|
mode = (mode or "PASSIVE").upper()
|
|
if mode not in {"PASSIVE", "FULL", "RESTART", "TRUNCATE"}:
|
|
mode = "PASSIVE"
|
|
with self._cursor() as c:
|
|
c.execute(f"PRAGMA wal_checkpoint({mode});")
|
|
row = c.fetchone()
|
|
if not row:
|
|
return (0, 0, 0)
|
|
vals = tuple(row)
|
|
return (int(vals[0]), int(vals[1]), int(vals[2]))
|
|
|
|
def optimize(self) -> None:
|
|
"""Run PRAGMA optimize to help the query planner update statistics"""
|
|
self.execute("PRAGMA optimize;")
|
|
|
|
def vacuum(self) -> None:
|
|
"""Vacuum the database to reclaim space (use sparingly on flash media)"""
|
|
self.execute("VACUUM;")
|