mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
578 lines
19 KiB
Python
578 lines
19 KiB
Python
"""Database Indexing Module.
|
|
|
|
Index creation and optimization using standard library only.
|
|
|
|
Key Features:
|
|
- Index creation and management
|
|
- Index optimization and analysis
|
|
- Query performance monitoring
|
|
- Index usage statistics
|
|
- Covering indexes
|
|
- Standard library only (no external dependencies)
|
|
|
|
Dependencies:
|
|
- sqlite3 (standard library)
|
|
- typing (standard library)
|
|
"""
|
|
|
|
import re
|
|
import sqlite3
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
|
|
class IndexingError(Exception):
|
|
"""Indexing operation error"""
|
|
|
|
|
|
def _validate_identifier(identifier: str) -> str:
|
|
"""Validate SQL identifier to prevent SQL injection.
|
|
|
|
Args:
|
|
identifier: SQL identifier (table/column/index name)
|
|
|
|
Returns:
|
|
The validated identifier
|
|
|
|
Raises:
|
|
IndexingError: If identifier contains invalid characters
|
|
"""
|
|
if not identifier or not isinstance(identifier, str):
|
|
raise IndexingError("Identifier cannot be empty")
|
|
|
|
# Only allow alphanumeric and underscore, must start with letter
|
|
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', identifier):
|
|
raise IndexingError(f"Invalid identifier: {identifier}")
|
|
|
|
return identifier
|
|
|
|
|
|
class DatabaseIndexing:
|
|
"""Handle database indexing operations.
|
|
|
|
Provides comprehensive index management including creation,
|
|
optimization, and performance monitoring.
|
|
"""
|
|
|
|
def __init__(self, connection: sqlite3.Connection):
|
|
"""Initialize indexing manager.
|
|
|
|
Args:
|
|
connection: SQLite database connection
|
|
"""
|
|
self.connection = connection
|
|
|
|
def create_indexes(self) -> None:
|
|
"""Create all recommended indexes.
|
|
|
|
Raises:
|
|
IndexingError: If index creation fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Files table indexes (from schema)
|
|
indexes: List[str] = [
|
|
"CREATE INDEX IF NOT EXISTS idx_files_path ON files(path)",
|
|
"CREATE INDEX IF NOT EXISTS idx_files_size ON files(size)",
|
|
"CREATE INDEX IF NOT EXISTS idx_files_hash ON files(hash)",
|
|
"CREATE INDEX IF NOT EXISTS idx_files_is_duplicate ON files(is_duplicate)",
|
|
"CREATE INDEX IF NOT EXISTS idx_files_duplicate_of ON files(duplicate_of)",
|
|
"CREATE INDEX IF NOT EXISTS idx_files_status ON files(status)",
|
|
"CREATE INDEX IF NOT EXISTS idx_files_modified_time ON files(modified_time)",
|
|
|
|
# Embeddings table indexes
|
|
"CREATE INDEX IF NOT EXISTS idx_embeddings_file_id ON embeddings(file_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_embeddings_model_version ON embeddings(model_version)",
|
|
"CREATE INDEX IF NOT EXISTS idx_embeddings_created_time ON embeddings(created_time)",
|
|
|
|
# File relationships indexes
|
|
"CREATE INDEX IF NOT EXISTS idx_file_relationships_file1_id ON file_relationships(file1_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_file_relationships_file2_id ON file_relationships(file2_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_file_relationships_type ON file_relationships(relationship_type)",
|
|
"CREATE INDEX IF NOT EXISTS idx_file_relationships_similarity ON file_relationships(similarity_score)",
|
|
|
|
# Plugins table indexes
|
|
"CREATE INDEX IF NOT EXISTS idx_tools_name ON tools(name)",
|
|
"CREATE INDEX IF NOT EXISTS idx_tools_type ON tools(type)",
|
|
"CREATE INDEX IF NOT EXISTS idx_tools_status ON tools(status)",
|
|
"CREATE INDEX IF NOT EXISTS idx_tools_enabled ON tools(enabled)",
|
|
|
|
# Composite indexes for common queries
|
|
(
|
|
"CREATE INDEX IF NOT EXISTS idx_files_status_duplicate "
|
|
"ON files(status, is_duplicate)"
|
|
),
|
|
(
|
|
"CREATE INDEX IF NOT EXISTS idx_file_relationships_similarity_type "
|
|
"ON file_relationships(similarity_score, relationship_type)"
|
|
),
|
|
]
|
|
|
|
for index_sql in indexes:
|
|
cursor.execute(index_sql)
|
|
|
|
self.connection.commit()
|
|
|
|
except sqlite3.Error as e:
|
|
self.connection.rollback()
|
|
raise IndexingError(f"Failed to create indexes: {e}") from e
|
|
|
|
def optimize_indexes(self) -> None:
|
|
"""Optimize database indexes using ANALYZE.
|
|
|
|
Updates SQLite query planner statistics for better query performance.
|
|
|
|
Raises:
|
|
IndexingError: If optimization fails
|
|
"""
|
|
try:
|
|
# Run ANALYZE to update index statistics
|
|
self.connection.execute("ANALYZE")
|
|
self.connection.commit()
|
|
|
|
except sqlite3.Error as e:
|
|
raise IndexingError(f"Index optimization failed: {e}") from e
|
|
|
|
def create_index(
|
|
self,
|
|
index_name: str,
|
|
table_name: str,
|
|
columns: List[str],
|
|
unique: bool = False,
|
|
if_not_exists: bool = True
|
|
) -> None:
|
|
"""Create a custom index.
|
|
|
|
Args:
|
|
index_name: Name of the index
|
|
table_name: Name of the table
|
|
columns: List of column names
|
|
unique: Create unique index
|
|
if_not_exists: Use IF NOT EXISTS clause
|
|
|
|
Raises:
|
|
IndexingError: If index creation fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Validate identifiers to prevent SQL injection
|
|
_validate_identifier(index_name)
|
|
_validate_identifier(table_name)
|
|
for col in columns:
|
|
_validate_identifier(col)
|
|
|
|
# Build index SQL
|
|
unique_clause = "UNIQUE " if unique else ""
|
|
exists_clause = "IF NOT EXISTS " if if_not_exists else ""
|
|
columns_str = ", ".join(columns)
|
|
|
|
index_sql = (
|
|
f"CREATE {unique_clause}INDEX {exists_clause}{index_name} "
|
|
f"ON {table_name}({columns_str})"
|
|
)
|
|
|
|
cursor.execute(index_sql)
|
|
self.connection.commit()
|
|
|
|
except sqlite3.Error as e:
|
|
self.connection.rollback()
|
|
raise IndexingError(f"Failed to create index {index_name}: {e}") from e
|
|
except IndexingError:
|
|
raise
|
|
except Exception as e:
|
|
raise IndexingError(f"Failed to create index {index_name}: {e}") from e
|
|
|
|
def drop_index(self, index_name: str, if_exists: bool = True) -> None:
|
|
"""Drop an index.
|
|
|
|
Args:
|
|
index_name: Name of the index to drop
|
|
if_exists: Use IF EXISTS clause
|
|
|
|
Raises:
|
|
IndexingError: If index drop fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Validate identifier to prevent SQL injection
|
|
_validate_identifier(index_name)
|
|
|
|
exists_clause = "IF EXISTS " if if_exists else ""
|
|
drop_sql = f"DROP INDEX {exists_clause}{index_name}"
|
|
|
|
cursor.execute(drop_sql)
|
|
self.connection.commit()
|
|
|
|
except sqlite3.Error as e:
|
|
self.connection.rollback()
|
|
raise IndexingError(f"Failed to drop index {index_name}: {e}") from e
|
|
except IndexingError:
|
|
raise
|
|
except Exception as e:
|
|
raise IndexingError(f"Failed to drop index {index_name}: {e}") from e
|
|
|
|
def get_indexes(self, table_name: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
"""Get list of indexes.
|
|
|
|
Args:
|
|
table_name: Filter by table name (None = all indexes)
|
|
|
|
Returns:
|
|
List of index information dictionaries
|
|
|
|
Raises:
|
|
IndexingError: If getting indexes fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
if table_name:
|
|
cursor.execute(
|
|
"SELECT name, tbl_name, sql FROM sqlite_master "
|
|
"WHERE type='index' AND tbl_name=? AND name NOT LIKE 'sqlite_%'",
|
|
(table_name,)
|
|
)
|
|
else:
|
|
cursor.execute(
|
|
"SELECT name, tbl_name, sql FROM sqlite_master "
|
|
"WHERE type='index' AND name NOT LIKE 'sqlite_%'"
|
|
)
|
|
|
|
indexes: List[Dict[str, Any]] = []
|
|
for row in cursor.fetchall():
|
|
indexes.append({
|
|
'name': row[0],
|
|
'table': row[1],
|
|
'sql': row[2]
|
|
})
|
|
|
|
return indexes
|
|
|
|
except sqlite3.Error as e:
|
|
raise IndexingError(f"Failed to get indexes: {e}") from e
|
|
|
|
def get_index_info(self, index_name: str) -> List[Dict[str, Any]]:
|
|
"""Get detailed information about an index.
|
|
|
|
Args:
|
|
index_name: Name of the index
|
|
|
|
Returns:
|
|
List of column information for the index
|
|
|
|
Raises:
|
|
IndexingError: If getting index info fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Validate identifier to prevent SQL injection
|
|
_validate_identifier(index_name)
|
|
|
|
cursor.execute(f"PRAGMA index_info({index_name})")
|
|
|
|
columns: List[Dict[str, Any]] = []
|
|
for row in cursor.fetchall():
|
|
columns.append({
|
|
'seqno': row[0],
|
|
'cid': row[1],
|
|
'name': row[2]
|
|
})
|
|
|
|
return columns
|
|
|
|
except sqlite3.Error as e:
|
|
raise IndexingError(f"Failed to get index info for {index_name}: {e}") from e
|
|
except IndexingError:
|
|
raise
|
|
except Exception as e:
|
|
raise IndexingError(f"Failed to get index info for {index_name}: {e}") from e
|
|
|
|
def analyze_query(self, query: str) -> List[Dict[str, Any]]:
|
|
"""Analyze query execution plan.
|
|
|
|
Args:
|
|
query: SQL query to analyze
|
|
|
|
Returns:
|
|
List of execution plan steps
|
|
|
|
Raises:
|
|
IndexingError: If query analysis fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(f"EXPLAIN QUERY PLAN {query}")
|
|
|
|
plan: List[Dict[str, Any]] = []
|
|
rows = cursor.fetchall()
|
|
for row in rows:
|
|
plan_item: Dict[str, Any] = {
|
|
'id': row[0],
|
|
'parent': row[1],
|
|
'detail': row[3] if len(row) > 3 else row[2]
|
|
}
|
|
plan.append(plan_item)
|
|
|
|
return plan
|
|
|
|
except sqlite3.Error as e:
|
|
raise IndexingError(f"Query analysis failed: {e}") from e
|
|
|
|
def is_index_used(self, query: str, index_name: str) -> bool:
|
|
"""Check if a query uses a specific index.
|
|
|
|
Args:
|
|
query: SQL query
|
|
index_name: Name of the index
|
|
|
|
Returns:
|
|
True if index is used in query plan
|
|
|
|
Raises:
|
|
IndexingError: If check fails
|
|
"""
|
|
try:
|
|
# Validate identifier to prevent SQL injection
|
|
_validate_identifier(index_name)
|
|
|
|
plan = self.analyze_query(query)
|
|
|
|
for step in plan:
|
|
detail = step.get('detail', '').lower()
|
|
if index_name.lower() in detail:
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
if isinstance(e, IndexingError):
|
|
raise
|
|
raise IndexingError(f"Index usage check failed: {e}") from e
|
|
|
|
def get_table_stats(self, table_name: str) -> Dict[str, Any]:
|
|
"""Get table statistics.
|
|
|
|
Args:
|
|
table_name: Name of the table
|
|
|
|
Returns:
|
|
Dictionary with table statistics
|
|
|
|
Raises:
|
|
IndexingError: If getting stats fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Validate identifier to prevent SQL injection
|
|
_validate_identifier(table_name)
|
|
|
|
# Get row count
|
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
row_count = cursor.fetchone()[0]
|
|
|
|
# Get table size (approximate)
|
|
cursor.execute(
|
|
"SELECT SUM(pgsize) FROM dbstat WHERE name=?",
|
|
(table_name,)
|
|
)
|
|
result = cursor.fetchone()
|
|
table_size = result[0] if result[0] else 0
|
|
|
|
# Get indexes
|
|
cursor.execute(
|
|
"SELECT COUNT(*) FROM sqlite_master "
|
|
"WHERE type='index' AND tbl_name=?",
|
|
(table_name,)
|
|
)
|
|
index_count = cursor.fetchone()[0]
|
|
|
|
return {
|
|
'table_name': table_name,
|
|
'row_count': row_count,
|
|
'table_size_bytes': table_size,
|
|
'index_count': index_count
|
|
}
|
|
|
|
except sqlite3.Error as e:
|
|
raise IndexingError(f"Failed to get table stats for {table_name}: {e}") from e
|
|
except IndexingError:
|
|
raise
|
|
except Exception as e:
|
|
raise IndexingError(f"Failed to get table stats for {table_name}: {e}") from e
|
|
|
|
def reindex(self, index_name: Optional[str] = None) -> None:
|
|
"""Rebuild indexes.
|
|
|
|
Args:
|
|
index_name: Specific index to rebuild (None = all)
|
|
|
|
Raises:
|
|
IndexingError: If reindex fails
|
|
"""
|
|
try:
|
|
if index_name:
|
|
# Validate identifier to prevent SQL injection
|
|
_validate_identifier(index_name)
|
|
self.connection.execute(f"REINDEX {index_name}")
|
|
else:
|
|
self.connection.execute("REINDEX")
|
|
|
|
self.connection.commit()
|
|
|
|
except sqlite3.Error as e:
|
|
self.connection.rollback()
|
|
raise IndexingError(f"Reindex failed: {e}") from e
|
|
except IndexingError:
|
|
raise
|
|
except Exception as e:
|
|
raise IndexingError(f"Reindex failed: {e}") from e
|
|
|
|
def find_missing_indexes(self) -> List[Dict[str, Any]]:
|
|
"""Find tables without indexes (except primary key).
|
|
|
|
Returns:
|
|
List of tables that might benefit from indexes
|
|
|
|
Raises:
|
|
IndexingError: If analysis fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Get all tables
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
)
|
|
tables = [row[0] for row in cursor.fetchall()]
|
|
|
|
suggestions: List[Dict[str, Any]] = []
|
|
|
|
for table in tables:
|
|
# Validate identifier to prevent SQL injection
|
|
_validate_identifier(table)
|
|
|
|
# Get table info
|
|
cursor.execute(f"PRAGMA table_info({table})")
|
|
columns = [row[1] for row in cursor.fetchall()]
|
|
|
|
# Get existing indexes
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=?",
|
|
(table,)
|
|
)
|
|
indexes = [row[0] for row in cursor.fetchall()]
|
|
|
|
# Check if table has non-pk indexes
|
|
if not indexes:
|
|
# Suggest indexes on common column types
|
|
for col in columns:
|
|
if col.lower() in ['id', 'created_at', 'updated_at', 'status', 'type']:
|
|
suggestions.append({
|
|
'table': table,
|
|
'column': col,
|
|
'reason': f'Common query column: {col}'
|
|
})
|
|
|
|
return suggestions
|
|
|
|
except sqlite3.Error as e:
|
|
raise IndexingError(f"Missing index analysis failed: {e}") from e
|
|
except IndexingError:
|
|
raise
|
|
except Exception as e:
|
|
raise IndexingError(f"Missing index analysis failed: {e}") from e
|
|
|
|
def get_index_stats(self) -> Dict[str, Any]:
|
|
"""Get overall index statistics.
|
|
|
|
Returns:
|
|
Dictionary with index statistics
|
|
|
|
Raises:
|
|
IndexingError: If getting stats fails
|
|
"""
|
|
try:
|
|
cursor = self.connection.cursor()
|
|
|
|
# Total indexes
|
|
cursor.execute(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'"
|
|
)
|
|
total_indexes = cursor.fetchone()[0]
|
|
|
|
# Indexes by table
|
|
cursor.execute(
|
|
"SELECT tbl_name, COUNT(*) FROM sqlite_master "
|
|
"WHERE type='index' AND name NOT LIKE 'sqlite_%' GROUP BY tbl_name"
|
|
)
|
|
by_table = {row[0]: row[1] for row in cursor.fetchall()}
|
|
|
|
# Total tables
|
|
cursor.execute(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
)
|
|
total_tables = cursor.fetchone()[0]
|
|
|
|
return {
|
|
'total_indexes': total_indexes,
|
|
'total_tables': total_tables,
|
|
'indexes_by_table': by_table,
|
|
'avg_indexes_per_table': total_indexes / total_tables if total_tables > 0 else 0
|
|
}
|
|
|
|
except sqlite3.Error as e:
|
|
raise IndexingError(f"Failed to get index stats: {e}") from e
|
|
|
|
|
|
# Convenience function for creating covering indexes
|
|
def create_covering_index(
|
|
connection: sqlite3.Connection,
|
|
index_name: str,
|
|
table_name: str,
|
|
where_columns: List[str],
|
|
select_columns: List[str]
|
|
) -> None:
|
|
"""Create a covering index for a specific query pattern.
|
|
|
|
Args:
|
|
connection: Database connection
|
|
index_name: Name for the index
|
|
table_name: Table name
|
|
where_columns: Columns used in WHERE clause
|
|
select_columns: Columns used in SELECT clause
|
|
|
|
Raises:
|
|
IndexingError: If index creation fails
|
|
"""
|
|
try:
|
|
# Validate identifiers to prevent SQL injection
|
|
_validate_identifier(index_name)
|
|
_validate_identifier(table_name)
|
|
for col in where_columns:
|
|
_validate_identifier(col)
|
|
for col in select_columns:
|
|
_validate_identifier(col)
|
|
|
|
# Combine columns (WHERE columns first, then SELECT columns)
|
|
all_columns = where_columns + [c for c in select_columns if c not in where_columns]
|
|
columns_str = ", ".join(all_columns)
|
|
|
|
index_sql = (
|
|
f"CREATE INDEX IF NOT EXISTS {index_name} "
|
|
f"ON {table_name}({columns_str})"
|
|
)
|
|
|
|
connection.execute(index_sql)
|
|
connection.commit()
|
|
|
|
except sqlite3.Error as e:
|
|
connection.rollback()
|
|
raise IndexingError(f"Failed to create covering index: {e}") from e
|
|
except IndexingError:
|
|
raise
|
|
except Exception as e:
|
|
raise IndexingError(f"Failed to create covering index: {e}") from e
|