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:
parent
a630e25898
commit
b47ddbec2e
|
|
@ -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'
|
||||
|
|
@ -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']
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue