598 lines
22 KiB
Python
598 lines
22 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Database Analysis Script
|
||
|
|
|
||
|
|
This script analyzes a SQLite database file for corruption and integrity issues.
|
||
|
|
It performs various checks including SQLite's built-in integrity check,
|
||
|
|
table structure validation, and attempts to identify specific errors.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python analyze_db.py <path_to_database_file>
|
||
|
|
|
||
|
|
Example:
|
||
|
|
python analyze_db.py /path/to/malformed.db
|
||
|
|
"""
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import os
|
||
|
|
import sqlite3
|
||
|
|
import sys
|
||
|
|
import logging
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
# Set up logging
|
||
|
|
logging.basicConfig(
|
||
|
|
level=logging.INFO,
|
||
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||
|
|
handlers=[
|
||
|
|
logging.StreamHandler(),
|
||
|
|
logging.FileHandler(f"db_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
|
||
|
|
]
|
||
|
|
)
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
def check_file_exists(db_path):
|
||
|
|
"""Check if the database file exists."""
|
||
|
|
if not os.path.isfile(db_path):
|
||
|
|
logger.error(f"Database file not found: {db_path}")
|
||
|
|
return False
|
||
|
|
return True
|
||
|
|
|
||
|
|
def connect_to_database(db_path):
|
||
|
|
"""Attempt to connect to the database."""
|
||
|
|
try:
|
||
|
|
conn = sqlite3.connect(db_path)
|
||
|
|
logger.info(f"Successfully connected to database: {db_path}")
|
||
|
|
return conn
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Failed to connect to database: {e}")
|
||
|
|
return None
|
||
|
|
|
||
|
|
def run_integrity_check(conn):
|
||
|
|
"""Run SQLite's built-in integrity check."""
|
||
|
|
try:
|
||
|
|
logger.info("Running SQLite integrity check...")
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("PRAGMA integrity_check;")
|
||
|
|
result = cursor.fetchall()
|
||
|
|
|
||
|
|
if len(result) == 1 and result[0][0] == 'ok':
|
||
|
|
logger.info("Integrity check passed: No corruption detected by SQLite")
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
logger.error("Integrity check failed. Issues found:")
|
||
|
|
for row in result:
|
||
|
|
logger.error(f" - {row[0]}")
|
||
|
|
return False
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error during integrity check: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def check_foreign_keys(conn):
|
||
|
|
"""Check for foreign key constraint violations."""
|
||
|
|
try:
|
||
|
|
logger.info("Checking foreign key constraints...")
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("PRAGMA foreign_key_check;")
|
||
|
|
result = cursor.fetchall()
|
||
|
|
|
||
|
|
if not result:
|
||
|
|
logger.info("Foreign key check passed: No violations found")
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
logger.error("Foreign key violations found:")
|
||
|
|
for row in result:
|
||
|
|
logger.error(f" - Table: {row[0]}, Row ID: {row[1]}, Parent: {row[2]}, Foreign Key: {row[3]}")
|
||
|
|
return False
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error during foreign key check: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def check_database_structure(conn):
|
||
|
|
"""Check the structure of the database (tables, columns, etc.)."""
|
||
|
|
try:
|
||
|
|
logger.info("Analyzing database structure...")
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
# Get list of tables
|
||
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||
|
|
tables = cursor.fetchall()
|
||
|
|
|
||
|
|
if not tables:
|
||
|
|
logger.warning("No tables found in the database")
|
||
|
|
return False
|
||
|
|
|
||
|
|
logger.info(f"Found {len(tables)} tables:")
|
||
|
|
for table in tables:
|
||
|
|
table_name = table[0]
|
||
|
|
logger.info(f" - {table_name}")
|
||
|
|
|
||
|
|
# Get table info
|
||
|
|
try:
|
||
|
|
cursor.execute(f"PRAGMA table_info({table_name});")
|
||
|
|
columns = cursor.fetchall()
|
||
|
|
logger.info(f" Columns: {len(columns)}")
|
||
|
|
|
||
|
|
# Try to count rows
|
||
|
|
try:
|
||
|
|
cursor.execute(f"SELECT COUNT(*) FROM {table_name};")
|
||
|
|
row_count = cursor.fetchone()[0]
|
||
|
|
logger.info(f" Rows: {row_count}")
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f" Error counting rows in {table_name}: {e}")
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f" Error getting structure for {table_name}: {e}")
|
||
|
|
|
||
|
|
return True
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error analyzing database structure: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def check_journal_mode(conn):
|
||
|
|
"""Check the journal mode of the database."""
|
||
|
|
try:
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("PRAGMA journal_mode;")
|
||
|
|
mode = cursor.fetchone()[0]
|
||
|
|
logger.info(f"Journal mode: {mode}")
|
||
|
|
|
||
|
|
if mode.lower() == 'wal':
|
||
|
|
# Check for WAL and SHM files
|
||
|
|
db_path = conn.execute("PRAGMA database_list;").fetchone()[2]
|
||
|
|
wal_path = f"{db_path}-wal"
|
||
|
|
shm_path = f"{db_path}-shm"
|
||
|
|
|
||
|
|
if os.path.exists(wal_path):
|
||
|
|
logger.info(f"WAL file exists: {wal_path}")
|
||
|
|
wal_size = os.path.getsize(wal_path)
|
||
|
|
logger.info(f"WAL file size: {wal_size} bytes")
|
||
|
|
if wal_size > 1000000: # 1MB
|
||
|
|
logger.warning("WAL file is large, which might indicate uncommitted transactions")
|
||
|
|
else:
|
||
|
|
logger.info("No WAL file found")
|
||
|
|
|
||
|
|
if os.path.exists(shm_path):
|
||
|
|
logger.info(f"SHM file exists: {shm_path}")
|
||
|
|
else:
|
||
|
|
logger.info("No SHM file found")
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error checking journal mode: {e}")
|
||
|
|
|
||
|
|
def check_for_corruption(conn):
|
||
|
|
"""Perform additional checks for common corruption issues."""
|
||
|
|
try:
|
||
|
|
logger.info("Checking for common corruption issues...")
|
||
|
|
|
||
|
|
# Check for malformed indexes
|
||
|
|
cursor = conn.cursor()
|
||
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='index';")
|
||
|
|
indexes = cursor.fetchall()
|
||
|
|
|
||
|
|
for idx in indexes:
|
||
|
|
index_name = idx[0]
|
||
|
|
try:
|
||
|
|
cursor.execute(f"PRAGMA index_info({index_name});")
|
||
|
|
cursor.fetchall()
|
||
|
|
logger.info(f"Index {index_name} appears to be valid")
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Index {index_name} may be corrupted: {e}")
|
||
|
|
|
||
|
|
# Check for database page size and free pages
|
||
|
|
cursor.execute("PRAGMA page_size;")
|
||
|
|
page_size = cursor.fetchone()[0]
|
||
|
|
logger.info(f"Page size: {page_size} bytes")
|
||
|
|
|
||
|
|
cursor.execute("PRAGMA page_count;")
|
||
|
|
page_count = cursor.fetchone()[0]
|
||
|
|
logger.info(f"Page count: {page_count}")
|
||
|
|
|
||
|
|
cursor.execute("PRAGMA freelist_count;")
|
||
|
|
freelist_count = cursor.fetchone()[0]
|
||
|
|
logger.info(f"Free pages: {freelist_count}")
|
||
|
|
|
||
|
|
# Check for auto_vacuum mode
|
||
|
|
cursor.execute("PRAGMA auto_vacuum;")
|
||
|
|
auto_vacuum = cursor.fetchone()[0]
|
||
|
|
logger.info(f"Auto vacuum mode: {auto_vacuum}")
|
||
|
|
|
||
|
|
return True
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error during corruption check: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def attempt_recovery(db_path, advanced=False):
|
||
|
|
"""
|
||
|
|
Attempt recovery operations on the database.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
db_path: Path to the database file
|
||
|
|
advanced: Whether to use advanced recovery techniques
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if recovery was successful, False otherwise
|
||
|
|
"""
|
||
|
|
logger.info("Attempting database recovery operations...")
|
||
|
|
|
||
|
|
# Create a backup of the original file
|
||
|
|
backup_path = f"{db_path}.backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||
|
|
try:
|
||
|
|
import shutil
|
||
|
|
shutil.copy2(db_path, backup_path)
|
||
|
|
logger.info(f"Created backup at: {backup_path}")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Failed to create backup: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# List to track recovery attempts and their results
|
||
|
|
recovery_results = []
|
||
|
|
|
||
|
|
# Method 1: Standard recovery - create new DB and copy data
|
||
|
|
recovery_results.append(standard_recovery(db_path))
|
||
|
|
|
||
|
|
# If advanced recovery is requested, try additional methods
|
||
|
|
if advanced:
|
||
|
|
# Method 2: Try to repair with SQLite's dump and restore
|
||
|
|
recovery_results.append(dump_and_restore_recovery(db_path))
|
||
|
|
|
||
|
|
# Method 3: Try to recover with VACUUM
|
||
|
|
recovery_results.append(vacuum_recovery(db_path))
|
||
|
|
|
||
|
|
# Method 4: Try to recover individual tables
|
||
|
|
recovery_results.append(table_by_table_recovery(db_path))
|
||
|
|
|
||
|
|
# Check if any recovery method was successful
|
||
|
|
if any(recovery_results):
|
||
|
|
logger.info("At least one recovery method was successful.")
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
logger.error("All recovery methods failed.")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def standard_recovery(db_path):
|
||
|
|
"""Standard recovery method - create new DB and copy data."""
|
||
|
|
try:
|
||
|
|
recovery_path = f"{db_path}.recovered"
|
||
|
|
logger.info(f"Method 1: Standard recovery - attempting to recover data to: {recovery_path}")
|
||
|
|
|
||
|
|
# Connect to the original database in read-only mode if possible
|
||
|
|
try:
|
||
|
|
src_conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||
|
|
except sqlite3.Error:
|
||
|
|
logger.warning("Could not open database in read-only mode, trying normal mode")
|
||
|
|
try:
|
||
|
|
src_conn = sqlite3.connect(db_path)
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Could not open source database: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Create a new database for recovery
|
||
|
|
try:
|
||
|
|
if os.path.exists(recovery_path):
|
||
|
|
os.remove(recovery_path)
|
||
|
|
|
||
|
|
dst_conn = sqlite3.connect(recovery_path)
|
||
|
|
|
||
|
|
# Get schema from the original database
|
||
|
|
cursor = src_conn.cursor()
|
||
|
|
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
|
||
|
|
tables = cursor.fetchall()
|
||
|
|
|
||
|
|
# Create tables in the new database
|
||
|
|
dst_cursor = dst_conn.cursor()
|
||
|
|
for table in tables:
|
||
|
|
if table[0]: # Check if SQL statement is not None
|
||
|
|
try:
|
||
|
|
dst_cursor.execute(table[0])
|
||
|
|
logger.info(f"Created table using: {table[0][:50]}...")
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error creating table: {e}")
|
||
|
|
|
||
|
|
# Copy data for each table
|
||
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
|
||
|
|
table_names = cursor.fetchall()
|
||
|
|
|
||
|
|
for table in table_names:
|
||
|
|
table_name = table[0]
|
||
|
|
try:
|
||
|
|
cursor.execute(f"SELECT * FROM {table_name};")
|
||
|
|
rows = cursor.fetchall()
|
||
|
|
|
||
|
|
if rows:
|
||
|
|
# Get column count for this table
|
||
|
|
cursor.execute(f"PRAGMA table_info({table_name});")
|
||
|
|
columns = cursor.fetchall()
|
||
|
|
placeholders = ','.join(['?' for _ in range(len(columns))])
|
||
|
|
|
||
|
|
# Insert data into the new database
|
||
|
|
dst_conn.executemany(
|
||
|
|
f"INSERT OR IGNORE INTO {table_name} VALUES ({placeholders});",
|
||
|
|
rows
|
||
|
|
)
|
||
|
|
dst_conn.commit()
|
||
|
|
logger.info(f"Copied {len(rows)} rows from table {table_name}")
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error copying data from table {table_name}: {e}")
|
||
|
|
|
||
|
|
# Create indexes
|
||
|
|
cursor.execute("SELECT sql FROM sqlite_master WHERE type='index' AND sql IS NOT NULL;")
|
||
|
|
indexes = cursor.fetchall()
|
||
|
|
|
||
|
|
for idx in indexes:
|
||
|
|
if idx[0]:
|
||
|
|
try:
|
||
|
|
dst_cursor.execute(idx[0])
|
||
|
|
logger.info(f"Created index using: {idx[0][:50]}...")
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error creating index: {e}")
|
||
|
|
|
||
|
|
dst_conn.commit()
|
||
|
|
src_conn.close()
|
||
|
|
dst_conn.close()
|
||
|
|
|
||
|
|
logger.info(f"Standard recovery completed. New database created at: {recovery_path}")
|
||
|
|
return True
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Standard recovery failed: {e}")
|
||
|
|
return False
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Unexpected error during standard recovery: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def dump_and_restore_recovery(db_path):
|
||
|
|
"""Recovery using SQLite's dump and restore functionality."""
|
||
|
|
try:
|
||
|
|
import subprocess
|
||
|
|
import tempfile
|
||
|
|
|
||
|
|
recovery_path = f"{db_path}.dump_recovered"
|
||
|
|
logger.info(f"Method 2: Dump and restore recovery - attempting to recover to: {recovery_path}")
|
||
|
|
|
||
|
|
# Create a temporary SQL file for the dump
|
||
|
|
with tempfile.NamedTemporaryFile(suffix='.sql', delete=False) as temp_file:
|
||
|
|
dump_file = temp_file.name
|
||
|
|
|
||
|
|
# Try to dump the database using the sqlite3 command-line tool
|
||
|
|
try:
|
||
|
|
subprocess.run(
|
||
|
|
['sqlite3', db_path, '.dump'],
|
||
|
|
stdout=open(dump_file, 'w'),
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
check=True,
|
||
|
|
text=True
|
||
|
|
)
|
||
|
|
logger.info(f"Database dumped to {dump_file}")
|
||
|
|
except (subprocess.SubprocessError, OSError) as e:
|
||
|
|
logger.error(f"Failed to dump database: {e}")
|
||
|
|
if os.path.exists(dump_file):
|
||
|
|
os.remove(dump_file)
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Create a new database from the dump
|
||
|
|
if os.path.exists(recovery_path):
|
||
|
|
os.remove(recovery_path)
|
||
|
|
|
||
|
|
try:
|
||
|
|
subprocess.run(
|
||
|
|
['sqlite3', recovery_path, '.read ' + dump_file],
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
check=True,
|
||
|
|
text=True
|
||
|
|
)
|
||
|
|
logger.info(f"Created new database from dump at {recovery_path}")
|
||
|
|
|
||
|
|
# Clean up the temporary dump file
|
||
|
|
if os.path.exists(dump_file):
|
||
|
|
os.remove(dump_file)
|
||
|
|
|
||
|
|
return True
|
||
|
|
except (subprocess.SubprocessError, OSError) as e:
|
||
|
|
logger.error(f"Failed to restore database from dump: {e}")
|
||
|
|
if os.path.exists(dump_file):
|
||
|
|
os.remove(dump_file)
|
||
|
|
return False
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Unexpected error during dump and restore recovery: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def vacuum_recovery(db_path):
|
||
|
|
"""Recovery using SQLite's VACUUM command."""
|
||
|
|
try:
|
||
|
|
recovery_path = f"{db_path}.vacuum_recovered"
|
||
|
|
logger.info(f"Method 3: VACUUM recovery - attempting to recover to: {recovery_path}")
|
||
|
|
|
||
|
|
# Copy the original database to the recovery path
|
||
|
|
import shutil
|
||
|
|
shutil.copy2(db_path, recovery_path)
|
||
|
|
|
||
|
|
# Try to run VACUUM on the copied database
|
||
|
|
try:
|
||
|
|
conn = sqlite3.connect(recovery_path)
|
||
|
|
conn.execute("PRAGMA integrity_check;") # This might fail if the DB is corrupted
|
||
|
|
conn.execute("VACUUM;")
|
||
|
|
conn.close()
|
||
|
|
logger.info(f"VACUUM completed successfully on {recovery_path}")
|
||
|
|
return True
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"VACUUM recovery failed: {e}")
|
||
|
|
return False
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Unexpected error during VACUUM recovery: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def table_by_table_recovery(db_path):
|
||
|
|
"""Recovery by extracting each table individually."""
|
||
|
|
try:
|
||
|
|
recovery_path = f"{db_path}.table_recovered"
|
||
|
|
logger.info(f"Method 4: Table-by-table recovery - attempting to recover to: {recovery_path}")
|
||
|
|
|
||
|
|
# Connect to the original database
|
||
|
|
try:
|
||
|
|
src_conn = sqlite3.connect(db_path)
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Could not open source database: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Create a new database for recovery
|
||
|
|
if os.path.exists(recovery_path):
|
||
|
|
os.remove(recovery_path)
|
||
|
|
|
||
|
|
dst_conn = sqlite3.connect(recovery_path)
|
||
|
|
|
||
|
|
# Get list of tables
|
||
|
|
try:
|
||
|
|
cursor = src_conn.cursor()
|
||
|
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
|
||
|
|
tables = cursor.fetchall()
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Could not get table list: {e}")
|
||
|
|
src_conn.close()
|
||
|
|
dst_conn.close()
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Process each table individually
|
||
|
|
success = False
|
||
|
|
for table in tables:
|
||
|
|
table_name = table[0]
|
||
|
|
try:
|
||
|
|
# Get table schema
|
||
|
|
cursor.execute(f"SELECT sql FROM sqlite_master WHERE type='table' AND name='{table_name}';")
|
||
|
|
table_sql = cursor.fetchone()[0]
|
||
|
|
|
||
|
|
# Create table in destination database
|
||
|
|
dst_conn.execute(table_sql)
|
||
|
|
|
||
|
|
# Try to copy data row by row
|
||
|
|
cursor.execute(f"SELECT * FROM {table_name};")
|
||
|
|
columns = [description[0] for description in cursor.description]
|
||
|
|
column_count = len(columns)
|
||
|
|
|
||
|
|
# Prepare insert statement with column names
|
||
|
|
column_names = ', '.join(columns)
|
||
|
|
placeholders = ', '.join(['?' for _ in range(column_count)])
|
||
|
|
insert_sql = f"INSERT INTO {table_name} ({column_names}) VALUES ({placeholders})"
|
||
|
|
|
||
|
|
# Copy data in batches to handle large tables
|
||
|
|
batch_size = 1000
|
||
|
|
rows = cursor.fetchmany(batch_size)
|
||
|
|
total_rows = 0
|
||
|
|
|
||
|
|
while rows:
|
||
|
|
dst_conn.executemany(insert_sql, rows)
|
||
|
|
dst_conn.commit()
|
||
|
|
total_rows += len(rows)
|
||
|
|
rows = cursor.fetchmany(batch_size)
|
||
|
|
|
||
|
|
logger.info(f"Recovered table {table_name} with {total_rows} rows")
|
||
|
|
success = True
|
||
|
|
except sqlite3.Error as e:
|
||
|
|
logger.error(f"Error recovering table {table_name}: {e}")
|
||
|
|
# Continue with next table
|
||
|
|
|
||
|
|
# Close connections
|
||
|
|
src_conn.close()
|
||
|
|
dst_conn.close()
|
||
|
|
|
||
|
|
if success:
|
||
|
|
logger.info(f"Table-by-table recovery completed with at least one table recovered at {recovery_path}")
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
logger.error("Table-by-table recovery failed to recover any tables")
|
||
|
|
return False
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Unexpected error during table-by-table recovery: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
def analyze_database(db_path, auto_recovery=False, advanced_recovery=False):
|
||
|
|
"""
|
||
|
|
Main function to analyze the database.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
db_path: Path to the database file
|
||
|
|
auto_recovery: Whether to automatically attempt recovery if issues are found
|
||
|
|
advanced_recovery: Whether to use advanced recovery techniques
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if analysis/recovery was successful, False otherwise
|
||
|
|
"""
|
||
|
|
logger.info(f"Starting analysis of database: {db_path}")
|
||
|
|
|
||
|
|
if not check_file_exists(db_path):
|
||
|
|
return False
|
||
|
|
|
||
|
|
conn = connect_to_database(db_path)
|
||
|
|
if not conn:
|
||
|
|
logger.error("Could not connect to the database. It may be severely corrupted.")
|
||
|
|
if auto_recovery:
|
||
|
|
logger.info("Auto-recovery mode enabled. Attempting recovery...")
|
||
|
|
return attempt_recovery(db_path, advanced=advanced_recovery)
|
||
|
|
else:
|
||
|
|
response = input("Would you like to attempt recovery? (y/n): ")
|
||
|
|
if response.lower() == 'y':
|
||
|
|
return attempt_recovery(db_path, advanced=advanced_recovery)
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Run various checks
|
||
|
|
integrity_ok = run_integrity_check(conn)
|
||
|
|
foreign_keys_ok = check_foreign_keys(conn)
|
||
|
|
structure_ok = check_database_structure(conn)
|
||
|
|
check_journal_mode(conn)
|
||
|
|
corruption_check_ok = check_for_corruption(conn)
|
||
|
|
|
||
|
|
# Summarize findings
|
||
|
|
logger.info("\n=== Analysis Summary ===")
|
||
|
|
logger.info(f"Integrity check: {'PASSED' if integrity_ok else 'FAILED'}")
|
||
|
|
logger.info(f"Foreign key check: {'PASSED' if foreign_keys_ok else 'FAILED'}")
|
||
|
|
logger.info(f"Structure check: {'PASSED' if structure_ok else 'FAILED'}")
|
||
|
|
logger.info(f"Corruption check: {'PASSED' if corruption_check_ok else 'FAILED'}")
|
||
|
|
|
||
|
|
# Overall assessment
|
||
|
|
if integrity_ok and foreign_keys_ok and structure_ok and corruption_check_ok:
|
||
|
|
logger.info("\nOVERALL ASSESSMENT: The database appears to be in good condition.")
|
||
|
|
return True
|
||
|
|
else:
|
||
|
|
logger.warning("\nOVERALL ASSESSMENT: Issues were detected in the database.")
|
||
|
|
|
||
|
|
# Attempt recovery if auto_recovery is enabled or user confirms
|
||
|
|
if auto_recovery:
|
||
|
|
logger.info("Auto-recovery mode enabled. Attempting recovery...")
|
||
|
|
return attempt_recovery(db_path, advanced=advanced_recovery)
|
||
|
|
else:
|
||
|
|
response = input("Would you like to attempt recovery? (y/n): ")
|
||
|
|
if response.lower() == 'y':
|
||
|
|
return attempt_recovery(db_path, advanced=advanced_recovery)
|
||
|
|
|
||
|
|
return False
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Parse command line arguments and run the analysis."""
|
||
|
|
parser = argparse.ArgumentParser(description='Analyze a SQLite database for corruption and integrity issues.')
|
||
|
|
parser.add_argument('db_path', help='Path to the SQLite database file (.db)')
|
||
|
|
parser.add_argument('--recovery', action='store_true',
|
||
|
|
help='Automatically attempt recovery if issues are found')
|
||
|
|
parser.add_argument('--advanced', action='store_true',
|
||
|
|
help='Use advanced recovery techniques (multiple methods)')
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
if not args.db_path.endswith('.db'):
|
||
|
|
logger.warning(f"The specified file ({args.db_path}) does not have a .db extension. It may not be a SQLite database.")
|
||
|
|
if not args.recovery: # Only prompt if not in auto-recovery mode
|
||
|
|
response = input("Continue anyway? (y/n): ")
|
||
|
|
if response.lower() != 'y':
|
||
|
|
return
|
||
|
|
else:
|
||
|
|
logger.info("Auto-recovery mode enabled. Continuing despite non-standard extension.")
|
||
|
|
|
||
|
|
# Run analysis with specified recovery options
|
||
|
|
result = analyze_database(args.db_path, auto_recovery=args.recovery, advanced_recovery=args.advanced)
|
||
|
|
|
||
|
|
# Provide a clear summary of the result
|
||
|
|
if result:
|
||
|
|
logger.info("Database analysis/recovery completed successfully.")
|
||
|
|
else:
|
||
|
|
logger.error("Database analysis/recovery failed.")
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|