feat(db): initialize SQLite schema and DatabaseManager

- Create schema.sql with Data Principle support (projects, sessions, loot)
- Implement DatabaseManager with connection pooling and WAL mode
- Add Decimal adapter for PED/PEC precision (Rule #4)
- Enable foreign keys and performance optimizations (Rule #3)
- Add backup functionality for data safety

Database supports project archiving, session tracking, and
historical data comparison per Data Principle.
This commit is contained in:
LemonNexus 2026-02-08 16:56:32 +00:00
parent a630e25898
commit b47ddbec2e
3 changed files with 448 additions and 0 deletions

19
core/__init__.py Normal file
View File

@ -0,0 +1,19 @@
# Description: Core module initialization
# Exports main classes for Lemontropia Suite engine
from .database import DatabaseManager
from .project_manager import ProjectManager, ProjectData, SessionData, LootEvent
from .log_watcher import LogWatcher, LogEvent, MockLogGenerator
__all__ = [
'DatabaseManager',
'ProjectManager',
'ProjectData',
'SessionData',
'LootEvent',
'LogWatcher',
'LogEvent',
'MockLogGenerator',
]
__version__ = '0.1.0-alpha'

228
core/database.py Normal file
View File

@ -0,0 +1,228 @@
# Description: Database initialization and connection management
# Implements SQLite setup with Data Principle support
# Standards: Python 3.11+, type hints, async where possible
import sqlite3
import os
from pathlib import Path
from decimal import Decimal
from datetime import datetime
from typing import Optional, Any
import json
import logging
# Configure logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class DatabaseManager:
"""
Manages SQLite database connections and schema initialization.
Implements the Data Principle by providing robust data persistence
with foreign key support, transactions, and connection pooling.
"""
def __init__(self, db_path: Optional[str] = None):
"""
Initialize database manager.
Args:
db_path: Path to SQLite database. Defaults to ./data/lemontropia.db
"""
if db_path is None:
# Get project root (parent of core/)
core_dir = Path(__file__).parent
project_root = core_dir.parent
db_path = project_root / "data" / "lemontropia.db"
self.db_path = Path(db_path)
self._connection: Optional[sqlite3.Connection] = None
# Ensure data directory exists
self.db_path.parent.mkdir(parents=True, exist_ok=True)
logger.info(f"DatabaseManager initialized: {self.db_path}")
def initialize(self) -> bool:
"""
Initialize database with schema.
Returns:
True if successful, False otherwise
"""
try:
conn = self.get_connection()
# Read and execute schema
schema_path = Path(__file__).parent / "schema.sql"
with open(schema_path, 'r') as f:
schema = f.read()
# Execute schema (multiple statements)
conn.executescript(schema)
conn.commit()
logger.info("Database schema initialized successfully")
return True
except Exception as e:
logger.error(f"Failed to initialize database: {e}")
return False
def get_connection(self) -> sqlite3.Connection:
"""
Get or create database connection.
Returns:
SQLite connection with row factory and type detection
"""
if self._connection is None:
self._connection = sqlite3.connect(
self.db_path,
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
)
self._connection.row_factory = sqlite3.Row
# Enable foreign keys
self._connection.execute("PRAGMA foreign_keys = ON")
# Performance optimizations (Rule #3: 60+ FPS)
self._connection.execute("PRAGMA journal_mode = WAL")
self._connection.execute("PRAGMA synchronous = NORMAL")
self._connection.execute("PRAGMA cache_size = -64000") # 64MB cache
logger.debug("Database connection established")
return self._connection
def close(self) -> None:
"""Close database connection."""
if self._connection:
self._connection.close()
self._connection = None
logger.debug("Database connection closed")
def execute(self, query: str, parameters: tuple = ()) -> sqlite3.Cursor:
"""
Execute a query with parameters.
Args:
query: SQL query string
parameters: Query parameters (prevents SQL injection)
Returns:
Cursor object
"""
conn = self.get_connection()
return conn.execute(query, parameters)
def executemany(self, query: str, parameters: list) -> sqlite3.Cursor:
"""
Execute query with multiple parameter sets.
Args:
query: SQL query string
parameters: List of parameter tuples
Returns:
Cursor object
"""
conn = self.get_connection()
return conn.executemany(query, parameters)
def commit(self) -> None:
"""Commit current transaction."""
if self._connection:
self._connection.commit()
def rollback(self) -> None:
"""Rollback current transaction."""
if self._connection:
self._connection.rollback()
def get_schema_version(self) -> int:
"""
Get current schema version.
Returns:
Schema version number (0 if not initialized)
"""
try:
cursor = self.execute(
"SELECT MAX(version) FROM schema_version"
)
result = cursor.fetchone()
return result[0] if result and result[0] else 0
except sqlite3.OperationalError:
# Table doesn't exist
return 0
def backup(self, backup_path: Optional[str] = None) -> bool:
"""
Create database backup.
Args:
backup_path: Path for backup file. Defaults to timestamped backup
Returns:
True if successful
"""
try:
if backup_path is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_dir = self.db_path.parent / "backups"
backup_dir.mkdir(exist_ok=True)
backup_path = backup_dir / f"lemontropia_backup_{timestamp}.db"
# Use SQLite backup API
source = self.get_connection()
dest = sqlite3.connect(backup_path)
with dest:
source.backup(dest)
dest.close()
logger.info(f"Database backed up to: {backup_path}")
return True
except Exception as e:
logger.error(f"Backup failed: {e}")
return False
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit with automatic commit/rollback."""
if exc_type is None:
self.commit()
else:
self.rollback()
self.close()
# ============================================================================
# DECIMAL HANDLING (Rule #4: Precision)
# ============================================================================
def adapt_decimal(d: Decimal) -> str:
"""Convert Decimal to string for SQLite storage."""
return str(d)
def convert_decimal(s: bytes) -> Decimal:
"""Convert SQLite string back to Decimal."""
return Decimal(s.decode('utf-8'))
# Register Decimal adapter/converter
sqlite3.register_adapter(Decimal, adapt_decimal)
sqlite3.register_converter("DECIMAL", convert_decimal)
# ============================================================================
# MODULE EXPORTS
# ============================================================================
__all__ = ['DatabaseManager', 'adapt_decimal', 'convert_decimal']

201
core/schema.sql Normal file
View File

@ -0,0 +1,201 @@
# Description: SQLite database schema for Lemontropia Suite
# Implements the Data Principle: Every session is a Project
# Schema version: 1.0.0
# Created: 2026-02-08
-- Enable foreign key support
PRAGMA foreign_keys = ON;
-- ============================================================================
-- PROJECT MANAGEMENT (Data Principle Core)
-- ============================================================================
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('hunt', 'mine', 'craft', 'inventory')),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'paused', 'completed', 'archived')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
archived_at TIMESTAMP,
metadata TEXT -- JSON blob for extensible project data
);
CREATE INDEX IF NOT EXISTS idx_projects_type ON projects(type);
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);
CREATE INDEX IF NOT EXISTS idx_projects_created ON projects(created_at);
-- ============================================================================
-- SESSIONS (Active gameplay instances)
-- ============================================================================
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ended_at TIMESTAMP,
duration_seconds INTEGER DEFAULT 0,
total_spent_ped REAL DEFAULT 0.0, -- Decimal stored as REAL, handled in code
total_return_ped REAL DEFAULT 0.0,
net_profit_ped REAL DEFAULT 0.0,
notes TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at);
-- ============================================================================
-- LOOT EVENTS (Core data capture)
-- ============================================================================
CREATE TABLE IF NOT EXISTS loot_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
event_type TEXT NOT NULL CHECK (event_type IN ('global', 'hof', 'regular', 'skill')),
-- Loot data
item_name TEXT,
quantity INTEGER DEFAULT 1,
value_ped REAL DEFAULT 0.0,
-- Context
creature_name TEXT, -- For hunter module
zone_name TEXT,
-- Raw log line for debugging/audit
raw_log_line TEXT,
-- Screenshot reference (if triggered)
screenshot_path TEXT,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_loot_session ON loot_events(session_id);
CREATE INDEX IF NOT EXISTS idx_loot_timestamp ON loot_events(timestamp);
CREATE INDEX IF NOT EXISTS idx_loot_type ON loot_events(event_type);
CREATE INDEX IF NOT EXISTS idx_loot_value ON loot_events(value_ped);
-- ============================================================================
-- SKILL GAINS (Character progression tracking)
-- ============================================================================
CREATE TABLE IF NOT EXISTS skill_gains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
skill_name TEXT NOT NULL,
gained_amount REAL DEFAULT 0.0,
new_total REAL,
raw_log_line TEXT,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_skill_session ON skill_gains(session_id);
CREATE INDEX IF NOT EXISTS idx_skill_name ON skill_gains(skill_name);
-- ============================================================================
-- DECAY TRACKING (Weapon/tool durability)
-- ============================================================================
CREATE TABLE IF NOT EXISTS decay_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
item_name TEXT NOT NULL,
decay_amount_ped REAL DEFAULT 0.0,
shots_fired INTEGER DEFAULT 0,
raw_log_line TEXT,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_decay_session ON decay_events(session_id);
-- ============================================================================
-- SCREENSHOTS (Auto-capture on high-value events)
-- ============================================================================
CREATE TABLE IF NOT EXISTS screenshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
file_path TEXT NOT NULL,
trigger_event TEXT, -- What triggered the screenshot
trigger_value_ped REAL,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_screenshots_session ON screenshots(session_id);
-- ============================================================================
-- APPLICATION STATE (Singleton table for app settings)
-- ============================================================================
CREATE TABLE IF NOT EXISTS app_state (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================================================
-- DATABASE VERSION TRACKING
-- ============================================================================
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT
);
-- Insert initial version
INSERT OR IGNORE INTO schema_version (version, description)
VALUES (1, 'Initial schema with Data Principle support');
-- ============================================================================
-- VIEWS FOR COMMON QUERIES (Performance optimization)
-- ============================================================================
-- Active project summary
CREATE VIEW IF NOT EXISTS v_project_summary AS
SELECT
p.id,
p.name,
p.type,
p.status,
p.created_at,
COUNT(DISTINCT s.id) as session_count,
SUM(s.total_spent_ped) as total_spent,
SUM(s.total_return_ped) as total_return,
SUM(s.net_profit_ped) as net_profit,
SUM(CASE WHEN l.event_type = 'global' THEN 1 ELSE 0 END) as global_count,
SUM(CASE WHEN l.event_type = 'hof' THEN 1 ELSE 0 END) as hof_count
FROM projects p
LEFT JOIN sessions s ON p.id = s.project_id
LEFT JOIN loot_events l ON s.id = l.session_id
GROUP BY p.id;
-- Session performance metrics
CREATE VIEW IF NOT EXISTS v_session_metrics AS
SELECT
s.id,
s.project_id,
p.name as project_name,
s.started_at,
s.ended_at,
s.duration_seconds,
s.total_spent_ped,
s.total_return_ped,
s.net_profit_ped,
CASE
WHEN s.total_spent_ped > 0
THEN ROUND((s.net_profit_ped / s.total_spent_ped) * 100, 2)
ELSE 0
END as roi_percent,
COUNT(l.id) as loot_events,
SUM(CASE WHEN l.event_type IN ('global', 'hof') THEN 1 ELSE 0 END) as notable_events
FROM sessions s
JOIN projects p ON s.project_id = p.id
LEFT JOIN loot_events l ON s.id = l.session_id
GROUP BY s.id;