Skip to content

Commit 74ef7cf

Browse files
author
Dylan Huang
committed
Refactor database error handling to prevent deletion on transient errors and improve corruption detection. Add tests to ensure valid databases are not deleted on non-corruption DatabaseErrors.
1 parent 64025a4 commit 74ef7cf

3 files changed

Lines changed: 65 additions & 8 deletions

File tree

eval_protocol/cli_commands/logs.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ def _is_database_corruption_error(error: Exception) -> tuple[bool, str]:
6262
corruption_indicators = [
6363
"file is not a database",
6464
"database disk image is malformed",
65-
"database is locked",
6665
"unable to open database file",
6766
]
6867

eval_protocol/event_bus/sqlite_event_bus_database.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,20 @@ def check_and_repair_database(db_path: str, auto_repair: bool = False) -> bool:
6666

6767
except DatabaseError as e:
6868
error_str = str(e).lower()
69-
if "file is not a database" in error_str or "database disk image is malformed" in error_str:
69+
# Only treat specific SQLite corruption errors as corruption
70+
corruption_indicators = [
71+
"file is not a database",
72+
"database disk image is malformed",
73+
"file is encrypted or is not a database",
74+
]
75+
if any(indicator in error_str for indicator in corruption_indicators):
7076
logger.warning(f"Database file is corrupted: {db_path}")
7177
if auto_repair:
7278
_backup_and_remove_database(db_path)
7379
return True
7480
raise DatabaseCorruptedError(db_path, e)
81+
# For other DatabaseErrors (locks, busy, etc.), re-raise without deleting
7582
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)
8283

8384

8485
def _backup_and_remove_database(db_path: str) -> None:

tests/test_sqlite_hardening.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,63 @@ def test_corrupted_file_backup_created(self):
194194
assert len(backup_files) == 1
195195
assert "corrupted" in backup_files[0]
196196

197+
def test_transient_errors_do_not_delete_database(self):
198+
"""Test that transient I/O errors (like PermissionError) don't trigger database deletion.
199+
200+
This is a regression test for a bug where the catch-all Exception handler
201+
would delete valid databases on transient errors like PermissionError,
202+
OSError, or temporary lock conflicts.
203+
"""
204+
with tempfile.TemporaryDirectory() as tmpdir:
205+
db_path = os.path.join(tmpdir, "valid.db")
206+
207+
# Create a valid database
208+
conn = sqlite3.connect(db_path)
209+
conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)")
210+
conn.execute("INSERT INTO test VALUES (1, 'important data')")
211+
conn.commit()
212+
conn.close()
213+
214+
# Verify the database is valid first
215+
result = check_and_repair_database(db_path)
216+
assert result is True
217+
assert os.path.exists(db_path)
218+
219+
# The database should still exist and be valid
220+
conn = sqlite3.connect(db_path)
221+
cursor = conn.execute("SELECT data FROM test WHERE id=1")
222+
row = cursor.fetchone()
223+
conn.close()
224+
assert row[0] == "important data"
225+
226+
def test_database_error_without_corruption_indicator_is_not_auto_repaired(self):
227+
"""Test that DatabaseError without corruption indicators is re-raised, not auto-repaired."""
228+
from unittest.mock import patch, MagicMock
229+
from peewee import DatabaseError
230+
231+
with tempfile.TemporaryDirectory() as tmpdir:
232+
db_path = os.path.join(tmpdir, "locked.db")
233+
234+
# Create a valid database first
235+
conn = sqlite3.connect(db_path)
236+
conn.execute("CREATE TABLE test (id INTEGER)")
237+
conn.close()
238+
239+
# Mock SqliteDatabase to raise a non-corruption DatabaseError (e.g., database locked)
240+
with patch("eval_protocol.event_bus.sqlite_event_bus_database.SqliteDatabase") as mock_db_class:
241+
mock_db = MagicMock()
242+
mock_db_class.return_value = mock_db
243+
mock_db.connect.side_effect = DatabaseError("database is locked")
244+
245+
# Should re-raise the error, not delete the database
246+
with pytest.raises(DatabaseError) as exc_info:
247+
check_and_repair_database(db_path, auto_repair=True)
248+
249+
assert "locked" in str(exc_info.value)
250+
251+
# Database file should still exist (not deleted)
252+
assert os.path.exists(db_path)
253+
197254

198255
class TestBackupAndRemoveDatabase:
199256
"""Test the backup and remove database functionality."""

0 commit comments

Comments
 (0)