1+ import os
12import time
23from typing import Any , List
34from uuid import uuid4
45
5- from peewee import BooleanField , CharField , DateTimeField , Model , SqliteDatabase
6+ from peewee import BooleanField , CharField , DatabaseError , DateTimeField , Model , SqliteDatabase
67from playhouse .sqlite_ext import JSONField
78
89from eval_protocol .event_bus .logger import logger
910
1011
12+ # SQLite pragmas for hardened concurrency safety
13+ SQLITE_HARDENED_PRAGMAS = {
14+ "journal_mode" : "wal" , # Write-Ahead Logging for concurrent reads/writes
15+ "synchronous" : "normal" , # Balance between safety and performance
16+ "busy_timeout" : 30000 , # 30 second timeout for locked database
17+ "wal_autocheckpoint" : 1000 , # Checkpoint every 1000 pages
18+ "cache_size" : - 64000 , # 64MB cache (negative = KB)
19+ "foreign_keys" : 1 , # Enable foreign key constraints
20+ "temp_store" : "memory" , # Store temp tables in memory
21+ }
22+
23+
24+ class DatabaseCorruptedError (Exception ):
25+ """Raised when the database file is corrupted or not a valid SQLite database."""
26+
27+ def __init__ (self , db_path : str , original_error : Exception ):
28+ self .db_path = db_path
29+ self .original_error = original_error
30+ super ().__init__ (f"Database file is corrupted: { db_path } . Original error: { original_error } " )
31+
32+
33+ def check_and_repair_database (db_path : str , auto_repair : bool = False ) -> bool :
34+ """
35+ Check if a database file is valid and optionally repair it.
36+
37+ Args:
38+ db_path: Path to the database file
39+ auto_repair: If True, automatically delete and recreate corrupted database
40+
41+ Returns:
42+ True if database is valid or was repaired, False otherwise
43+
44+ Raises:
45+ DatabaseCorruptedError: If database is corrupted and auto_repair is False
46+ """
47+ if not os .path .exists (db_path ):
48+ return True # New database, nothing to check
49+
50+ try :
51+ # Try to open the database and run an integrity check
52+ test_db = SqliteDatabase (db_path , pragmas = {"busy_timeout" : 5000 })
53+ test_db .connect ()
54+ cursor = test_db .execute_sql ("PRAGMA integrity_check" )
55+ result = cursor .fetchone ()
56+ test_db .close ()
57+
58+ if result and result [0 ] == "ok" :
59+ return True
60+ else :
61+ logger .warning (f"Database integrity check failed for { db_path } : { result } " )
62+ if auto_repair :
63+ _backup_and_remove_database (db_path )
64+ return True
65+ raise DatabaseCorruptedError (db_path , Exception (f"Integrity check failed: { result } " ))
66+
67+ except DatabaseError as e :
68+ error_str = str (e ).lower ()
69+ if "file is not a database" in error_str or "database disk image is malformed" in error_str :
70+ logger .warning (f"Database file is corrupted: { db_path } " )
71+ if auto_repair :
72+ _backup_and_remove_database (db_path )
73+ return True
74+ raise DatabaseCorruptedError (db_path , e )
75+ raise
76+ except Exception as e :
77+ logger .warning (f"Error checking database { db_path } : { e } " )
78+ if auto_repair :
79+ _backup_and_remove_database (db_path )
80+ return True
81+ raise DatabaseCorruptedError (db_path , e )
82+
83+
84+ def _backup_and_remove_database (db_path : str ) -> None :
85+ """Backup a corrupted database file and remove it."""
86+ backup_path = f"{ db_path } .corrupted.{ int (time .time ())} "
87+ try :
88+ os .rename (db_path , backup_path )
89+ logger .info (f"Backed up corrupted database to: { backup_path } " )
90+ except OSError as e :
91+ logger .warning (f"Failed to backup corrupted database, removing: { e } " )
92+ try :
93+ os .remove (db_path )
94+ except OSError :
95+ pass
96+
97+ # Also try to remove WAL and SHM files if they exist
98+ for suffix in ["-wal" , "-shm" ]:
99+ wal_file = f"{ db_path } { suffix } "
100+ if os .path .exists (wal_file ):
101+ try :
102+ os .remove (wal_file )
103+ except OSError :
104+ pass
105+
106+
11107class SqliteEventBusDatabase :
12108 """SQLite database for cross-process event communication."""
13109
14- def __init__ (self , db_path : str ):
110+ def __init__ (self , db_path : str , auto_repair : bool = True ):
15111 self ._db_path = db_path
16- self ._db = SqliteDatabase (db_path )
112+
113+ # Ensure directory exists
114+ db_dir = os .path .dirname (db_path )
115+ if db_dir :
116+ os .makedirs (db_dir , exist_ok = True )
117+
118+ # Check and optionally repair corrupted database
119+ check_and_repair_database (db_path , auto_repair = auto_repair )
120+
121+ # Initialize database with hardened concurrency settings
122+ self ._db = SqliteDatabase (db_path , pragmas = SQLITE_HARDENED_PRAGMAS )
17123
18124 class BaseModel (Model ):
19125 class Meta :
@@ -29,7 +135,8 @@ class Event(BaseModel): # type: ignore
29135
30136 self ._Event = Event
31137 self ._db .connect ()
32- self ._db .create_tables ([Event ])
138+ # Use safe=True to avoid errors when tables already exist
139+ self ._db .create_tables ([Event ], safe = True )
33140
34141 def publish_event (self , event_type : str , data : Any , process_id : str ) -> None :
35142 """Publish an event to the database."""
0 commit comments