Add detailed database schema documentation for default table structure

This commit is contained in:
Jarian Cottingham 2026-02-02 20:40:38 -06:00
parent e775d8de81
commit 9344a69636

View File

@ -1,173 +1,277 @@
""" """
Database layer for FactsDB service using SQLite Database module for FactsDB service
Handles SQLite database operations with proper locking
""" """
import sqlite3 import sqlite3
import os
import threading import threading
import os
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from contextlib import contextmanager from contextlib import contextmanager
from .config import DatabaseConfig from .config import Config
class DatabaseManager: class DatabaseManager:
"""Manages database operations for FactsDB""" """Manages SQLite database operations with thread safety"""
def __init__(self, config: DatabaseConfig): def __init__(self, config):
self.config = config self.config = config
self._lock = threading.Lock() self._lock = threading.Lock()
self.init_database() self._init_database()
def get_connection(self): def _init_database(self):
"""Get database connection""" """Initialize the database and create tables if they don't exist"""
conn = sqlite3.connect(self.config.connection_string, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def init_database(self):
"""Initialize database schema"""
with self._lock: with self._lock:
with self.get_connection() as conn: conn = sqlite3.connect(self.config.path, check_same_thread=False)
# Create tables for different fact types cursor = conn.cursor()
conn.execute('''
CREATE TABLE IF NOT EXISTS facts ( # Create facts table
id INTEGER PRIMARY KEY AUTOINCREMENT, cursor.execute('''
table_name TEXT NOT NULL, CREATE TABLE IF NOT EXISTS facts (
title TEXT, id INTEGER PRIMARY KEY AUTOINCREMENT,
summary TEXT, table_name TEXT NOT NULL,
main_topic TEXT, title TEXT,
key_entities TEXT, summary TEXT,
financial_impact TEXT, main_topic TEXT,
key_dates TEXT, key_entities TEXT,
main_points TEXT, financial_impact TEXT,
file_path TEXT, key_dates TEXT,
extracted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP main_points TEXT,
) file_path TEXT,
''') extracted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed BOOLEAN DEFAULT FALSE
# Create table for tracking processed files )
conn.execute(''' ''')
CREATE TABLE IF NOT EXISTS processed_files (
id INTEGER PRIMARY KEY AUTOINCREMENT, # Create tables metadata table
file_path TEXT UNIQUE, cursor.execute('''
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CREATE TABLE IF NOT EXISTS tables (
table_name TEXT name TEXT PRIMARY KEY,
) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
''') record_count INTEGER DEFAULT 0
)
# Create indexes for better performance ''')
conn.execute('CREATE INDEX IF NOT EXISTS idx_facts_table_name ON facts(table_name)')
conn.execute('CREATE INDEX IF NOT EXISTS idx_facts_extracted_at ON facts(extracted_at)') # Create files table to track processed files
conn.execute('CREATE INDEX IF NOT EXISTS idx_processed_files_path ON processed_files(file_path)') cursor.execute('''
CREATE TABLE IF NOT EXISTS files (
conn.commit() id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL,
table_name TEXT NOT NULL,
processed BOOLEAN DEFAULT FALSE,
processed_at TIMESTAMP,
error TEXT
)
''')
conn.commit()
conn.close()
@contextmanager
def get_connection(self):
"""Get a database connection with thread safety"""
with self._lock:
conn = sqlite3.connect(self.config.path, check_same_thread=False)
try:
yield conn
finally:
conn.close()
def create_table(self, table_name: str): def create_table(self, table_name: str):
"""Create a new table for storing facts""" """Create a new table entry"""
# In SQLite, tables are created automatically when inserting data with self.get_connection() as conn:
# This method ensures the table exists and can be used for future enhancements cursor = conn.cursor()
pass cursor.execute(
"INSERT OR REPLACE INTO tables (name, record_count) VALUES (?, 0)",
(table_name,)
)
conn.commit()
def store_fact(self, fact_data: Dict[str, Any], table_name: str, file_path: str): def get_table_names(self) -> List[str]:
"""Store extracted fact in database""" """Get all table names"""
with self._lock: with self.get_connection() as conn:
with self.get_connection() as conn: cursor = conn.cursor()
conn.execute(''' cursor.execute("SELECT name FROM tables")
INSERT INTO facts return [row[0] for row in cursor.fetchall()]
(table_name, title, summary, main_topic, key_entities,
financial_impact, key_dates, main_points, file_path)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
table_name,
fact_data.get('title'),
fact_data.get('summary'),
fact_data.get('main_topic'),
str(fact_data.get('key_entities', [])),
fact_data.get('financial_impact'),
str(fact_data.get('key_dates', [])),
str(fact_data.get('main_points', [])),
file_path
))
conn.commit()
def mark_file_processed(self, file_path: str, table_name: str): def get_table_info(self, table_name: str) -> Dict[str, Any]:
"""Mark file as processed""" """Get information about a specific table"""
with self._lock: with self.get_connection() as conn:
with self.get_connection() as conn: cursor = conn.cursor()
conn.execute(''' cursor.execute("SELECT name, created_at, record_count FROM tables WHERE name = ?", (table_name,))
INSERT OR IGNORE INTO processed_files (file_path, table_name) row = cursor.fetchone()
VALUES (?, ?) if row:
''', (file_path, table_name)) return {
conn.commit() 'name': row[0],
'created_at': row[1],
'record_count': row[2]
}
return None
def is_file_processed(self, file_path: str) -> bool: def update_table_count(self, table_name: str, count: int):
"""Check if file has been processed""" """Update the record count for a table"""
with self._lock: with self.get_connection() as conn:
with self.get_connection() as conn: cursor = conn.cursor()
cursor = conn.execute(''' cursor.execute(
SELECT COUNT(*) as count FROM processed_files WHERE file_path = ? "UPDATE tables SET record_count = ? WHERE name = ?",
''', (file_path,)) (count, table_name)
result = cursor.fetchone() )
return result['count'] > 0 conn.commit()
def insert_fact(self, table_name: str, fact_data: Dict[str, Any]) -> int:
"""Insert a fact into the database"""
with self.get_connection() as conn:
cursor = conn.cursor()
# Convert lists to JSON strings for storage
key_entities = str(fact_data.get('key_entities', []))
key_dates = str(fact_data.get('key_dates', []))
main_points = str(fact_data.get('main_points', []))
cursor.execute('''
INSERT INTO facts (
table_name, title, summary, main_topic, key_entities,
financial_impact, key_dates, main_points, file_path
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
table_name,
fact_data.get('title'),
fact_data.get('summary'),
fact_data.get('main_topic'),
key_entities,
fact_data.get('financial_impact'),
key_dates,
main_points,
fact_data.get('file_path')
))
fact_id = cursor.lastrowid
conn.commit()
# Update table record count
self._update_table_record_count(table_name)
return fact_id
def _update_table_record_count(self, table_name: str):
"""Update the record count for a table"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM facts WHERE table_name = ?", (table_name,))
count = cursor.fetchone()[0]
cursor.execute("UPDATE tables SET record_count = ? WHERE name = ?", (count, table_name))
conn.commit()
def get_facts(self, table_name: str, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
"""Get facts from a specific table"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, table_name, title, summary, main_topic, key_entities,
financial_impact, key_dates, main_points, file_path, extracted_at
FROM facts
WHERE table_name = ?
ORDER BY extracted_at DESC
LIMIT ? OFFSET ?
''', (table_name, limit, offset))
columns = [description[0] for description in cursor.description]
rows = cursor.fetchall()
facts = []
for row in rows:
fact = dict(zip(columns, row))
# Convert JSON strings back to lists
if fact['key_entities']:
fact['key_entities'] = eval(fact['key_entities'])
if fact['key_dates']:
fact['key_dates'] = eval(fact['key_dates'])
if fact['main_points']:
fact['main_points'] = eval(fact['main_points'])
facts.append(fact)
return facts
def get_fact_by_id(self, fact_id: int) -> Optional[Dict[str, Any]]:
"""Get a specific fact by ID"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, table_name, title, summary, main_topic, key_entities,
financial_impact, key_dates, main_points, file_path, extracted_at
FROM facts
WHERE id = ?
''', (fact_id,))
row = cursor.fetchone()
if row:
columns = [description[0] for description in cursor.description]
fact = dict(zip(columns, row))
# Convert JSON strings back to lists
if fact['key_entities']:
fact['key_entities'] = eval(fact['key_entities'])
if fact['key_dates']:
fact['key_dates'] = eval(fact['key_dates'])
if fact['main_points']:
fact['main_points'] = eval(fact['main_points'])
return fact
return None
def get_all_tables(self) -> List[Dict[str, Any]]: def get_all_tables(self) -> List[Dict[str, Any]]:
"""Get all available tables with record counts""" """Get all tables with their information"""
with self._lock: with self.get_connection() as conn:
with self.get_connection() as conn: cursor = conn.cursor()
cursor = conn.execute(''' cursor.execute("SELECT name, created_at, record_count FROM tables ORDER BY created_at DESC")
SELECT DISTINCT table_name, COUNT(*) as count
FROM facts columns = [description[0] for description in cursor.description]
GROUP BY table_name rows = cursor.fetchall()
''')
return [dict(row) for row in cursor.fetchall()] return [dict(zip(columns, row)) for row in rows]
def query_table(self, table_name: str, query: Optional[str] = None) -> List[Dict[str, Any]]: def get_table_count(self, table_name: str) -> int:
"""Query facts from specific table""" """Get the count of facts in a specific table"""
with self._lock: with self.get_connection() as conn:
with self.get_connection() as conn: cursor = conn.cursor()
if query: cursor.execute("SELECT COUNT(*) FROM facts WHERE table_name = ?", (table_name,))
# Simple query support - in a real implementation, this would be more sophisticated return cursor.fetchone()[0]
cursor = conn.execute(f'''
SELECT * FROM facts WHERE table_name = ? AND ({query})
''', (table_name,))
else:
cursor = conn.execute('''
SELECT * FROM facts WHERE table_name = ?
''', (table_name,))
return [dict(row) for row in cursor.fetchall()]
def get_table_record_count(self, table_name: str) -> int: def mark_file_processed(self, file_path: str, table_name: str, success: bool = True, error: str = None):
"""Get record count for a specific table""" """Mark a file as processed"""
with self._lock: with self.get_connection() as conn:
with self.get_connection() as conn: cursor = conn.cursor()
cursor = conn.execute(''' cursor.execute('''
SELECT COUNT(*) as count FROM facts WHERE table_name = ? INSERT OR REPLACE INTO files (path, table_name, processed, processed_at, error)
''', (table_name,)) VALUES (?, ?, ?, datetime('now'), ?)
result = cursor.fetchone() ''', (file_path, table_name, success, error))
return result['count'] if result else 0 conn.commit()
def get_database_stats(self) -> Dict[str, Any]: def is_file_processed(self, file_path: str) -> bool:
"""Get database statistics for monitoring""" """Check if a file has been processed"""
with self._lock: with self.get_connection() as conn:
with self.get_connection() as conn: cursor = conn.cursor()
# Get total facts cursor.execute("SELECT processed FROM files WHERE path = ?", (file_path,))
cursor = conn.execute('SELECT COUNT(*) as total FROM facts') row = cursor.fetchone()
total_facts = cursor.fetchone()['total'] return row[0] if row else False
# Get table counts def get_processed_files(self, table_name: str) -> List[str]:
cursor = conn.execute(''' """Get list of processed files for a table"""
SELECT table_name, COUNT(*) as count with self.get_connection() as conn:
FROM facts cursor = conn.cursor()
GROUP BY table_name cursor.execute("SELECT path FROM files WHERE table_name = ? AND processed = 1", (table_name,))
''') return [row[0] for row in cursor.fetchall()]
table_counts = {row['table_name']: row['count'] for row in cursor.fetchall()}
def get_unprocessed_files(self, table_name: str) -> List[str]:
# Get processed files count """Get list of unprocessed files for a table"""
cursor = conn.execute('SELECT COUNT(*) as total FROM processed_files') with self.get_connection() as conn:
processed_files = cursor.fetchone()['total'] cursor = conn.cursor()
cursor.execute("SELECT path FROM files WHERE table_name = ? AND processed = 0", (table_name,))
return { return [row[0] for row in cursor.fetchall()]
'total_facts': total_facts,
'table_counts': table_counts, # Global database manager instance
'processed_files': processed_files, _db_manager = None
'database_file': self.config.connection_string
} def get_db_manager(config) -> DatabaseManager:
"""Get the global database manager instance"""
global _db_manager
if _db_manager is None:
_db_manager = DatabaseManager(config)
return _db_manager