refactor(hermes_cli): pack exploded signatures/calls in session recovery modules (AST-identical)
This commit is contained in:
+39
-145
@@ -81,9 +81,7 @@ def _resolved_output_path(path: Path) -> Path:
|
||||
|
||||
|
||||
def _validate_paths(
|
||||
source_path: Path,
|
||||
output_path: Optional[Path] = None,
|
||||
work_dir: Optional[Path] = None,
|
||||
source_path: Path, output_path: Optional[Path] = None, work_dir: Optional[Path] = None,
|
||||
) -> tuple[Path, Optional[Path], Path]:
|
||||
source = source_path.expanduser().resolve(strict=True)
|
||||
if not source.is_file():
|
||||
@@ -248,10 +246,7 @@ def _immediate_transaction(conn: sqlite3.Connection) -> Iterator[None]:
|
||||
|
||||
|
||||
def _compatible_columns(
|
||||
source: sqlite3.Connection,
|
||||
destination: sqlite3.Connection,
|
||||
table: str,
|
||||
result: dict[str, Any],
|
||||
source: sqlite3.Connection, destination: sqlite3.Connection, table: str, result: dict[str, Any],
|
||||
) -> Optional[list[str]]:
|
||||
"""Columns shared by source and destination; sets a terminal status and returns None otherwise."""
|
||||
source_columns = _table_columns(source, table)
|
||||
@@ -273,17 +268,9 @@ def _quoted_columns(columns: list[str]) -> tuple[str, str]:
|
||||
|
||||
|
||||
def _copy_rows(
|
||||
source: sqlite3.Connection,
|
||||
destination: sqlite3.Connection,
|
||||
select_sql: str,
|
||||
params: tuple[Any, ...],
|
||||
insert_sql: str,
|
||||
*,
|
||||
table: str,
|
||||
chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback],
|
||||
expected_rows: Optional[int],
|
||||
result: dict[str, Any],
|
||||
source: sqlite3.Connection, destination: sqlite3.Connection, select_sql: str, params: tuple[Any, ...],
|
||||
insert_sql: str, *, table: str, chunk_size: int, progress_cb: Optional[ProgressCallback],
|
||||
expected_rows: Optional[int], result: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Chunked straight copy; fills ``status``/``error`` on ``result``."""
|
||||
try:
|
||||
@@ -351,8 +338,7 @@ def _inspect_connection(conn: sqlite3.Connection) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _snapshot_and_inspect(
|
||||
source: Path,
|
||||
work_root: Path,
|
||||
source: Path, work_root: Path,
|
||||
) -> tuple[tempfile.TemporaryDirectory[str], Path, dict[str, Any]]:
|
||||
before = _source_fingerprint(source)
|
||||
temp_dir = tempfile.TemporaryDirectory(prefix="hermes-session-recovery-", dir=str(work_root))
|
||||
@@ -425,14 +411,8 @@ def _fresh_destination(output: Path, *, topic_tables: bool = False) -> sqlite3.C
|
||||
|
||||
|
||||
def _copy_table(
|
||||
source: sqlite3.Connection,
|
||||
destination: sqlite3.Connection,
|
||||
table: str,
|
||||
*,
|
||||
salvage: bool,
|
||||
chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback],
|
||||
source_rows: Optional[int],
|
||||
source: sqlite3.Connection, destination: sqlite3.Connection, table: str, *, salvage: bool, chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback], source_rows: Optional[int],
|
||||
) -> dict[str, Any]:
|
||||
"""Copy one canonical table: straight chunked copy, or rowid-range salvage when ``salvage``."""
|
||||
copy_kwargs = dict(chunk_size=chunk_size, progress_cb=progress_cb, source_rows=source_rows)
|
||||
@@ -446,16 +426,9 @@ def _copy_table(
|
||||
return result
|
||||
quoted, placeholders = _quoted_columns(columns)
|
||||
return _copy_rows(
|
||||
source,
|
||||
destination,
|
||||
f'SELECT {quoted} FROM "{table}"',
|
||||
(),
|
||||
f'INSERT INTO "{table}" ({quoted}) VALUES ({placeholders})',
|
||||
table=table,
|
||||
chunk_size=chunk_size,
|
||||
progress_cb=progress_cb,
|
||||
expected_rows=source_rows,
|
||||
result=result,
|
||||
source, destination, f'SELECT {quoted} FROM "{table}"', (),
|
||||
f'INSERT INTO "{table}" ({quoted}) VALUES ({placeholders})', table=table, chunk_size=chunk_size,
|
||||
progress_cb=progress_cb, expected_rows=source_rows, result=result,
|
||||
)
|
||||
|
||||
|
||||
@@ -544,18 +517,9 @@ class _RowidRangeSalvage:
|
||||
"""Bisecting rowid-range copy for one table; counters live on the shared ``result`` dict."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source: sqlite3.Connection,
|
||||
destination: sqlite3.Connection,
|
||||
table: str,
|
||||
columns: list[str],
|
||||
*,
|
||||
chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback],
|
||||
source_rows: Optional[int],
|
||||
insert_prefix: str,
|
||||
row_filter: Optional[Callable[[tuple[Any, ...], tuple[str, ...]], bool]],
|
||||
result: dict[str, Any],
|
||||
self, source: sqlite3.Connection, destination: sqlite3.Connection, table: str, columns: list[str], *,
|
||||
chunk_size: int, progress_cb: Optional[ProgressCallback], source_rows: Optional[int], insert_prefix: str,
|
||||
row_filter: Optional[Callable[[tuple[Any, ...], tuple[str, ...]], bool]], result: dict[str, Any],
|
||||
) -> None:
|
||||
self.source, self.destination, self.table = source, destination, table
|
||||
self.chunk_size, self.progress_cb, self.source_rows = chunk_size, progress_cb, source_rows
|
||||
@@ -647,14 +611,8 @@ class _RowidRangeSalvage:
|
||||
|
||||
|
||||
def _copy_table_salvage(
|
||||
source: sqlite3.Connection,
|
||||
destination: sqlite3.Connection,
|
||||
table: str,
|
||||
*,
|
||||
chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback],
|
||||
source_rows: Optional[int],
|
||||
insert_prefix: str = "INSERT",
|
||||
source: sqlite3.Connection, destination: sqlite3.Connection, table: str, *, chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback], source_rows: Optional[int], insert_prefix: str = "INSERT",
|
||||
row_filter: Optional[Callable[[tuple[Any, ...], tuple[str, ...]], bool]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Best-effort rowid-range copy that continues past damaged source pages."""
|
||||
@@ -680,16 +638,8 @@ def _copy_table_salvage(
|
||||
return result
|
||||
|
||||
salvage = _RowidRangeSalvage(
|
||||
source,
|
||||
destination,
|
||||
table,
|
||||
columns,
|
||||
chunk_size=chunk_size,
|
||||
progress_cb=progress_cb,
|
||||
source_rows=source_rows,
|
||||
insert_prefix=insert_prefix,
|
||||
row_filter=row_filter,
|
||||
result=result,
|
||||
source, destination, table, columns, chunk_size=chunk_size, progress_cb=progress_cb, source_rows=source_rows,
|
||||
insert_prefix=insert_prefix, row_filter=row_filter, result=result,
|
||||
)
|
||||
salvage.copy_range(int(bounds["low"]), int(bounds["high"]))
|
||||
skipped_ranges = result["skipped_rowid_ranges"]
|
||||
@@ -721,11 +671,7 @@ def _state_meta_result(source_rows: Optional[int], **extra: Any) -> dict[str, An
|
||||
|
||||
|
||||
def _state_meta_precheck(
|
||||
source: sqlite3.Connection,
|
||||
destination: sqlite3.Connection,
|
||||
source_rows: Optional[int],
|
||||
*,
|
||||
salvage: bool,
|
||||
source: sqlite3.Connection, destination: sqlite3.Connection, source_rows: Optional[int], *, salvage: bool,
|
||||
) -> Optional[dict[str, Any]]:
|
||||
"""Terminal ``state_meta`` result when the key/value schema is unusable, else ``None``.
|
||||
|
||||
@@ -755,13 +701,8 @@ def _state_meta_precheck(
|
||||
|
||||
|
||||
def _copy_state_meta(
|
||||
source: sqlite3.Connection,
|
||||
destination: sqlite3.Connection,
|
||||
*,
|
||||
salvage: bool,
|
||||
chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback],
|
||||
source_rows: Optional[int],
|
||||
source: sqlite3.Connection, destination: sqlite3.Connection, *, salvage: bool, chunk_size: int,
|
||||
progress_cb: Optional[ProgressCallback], source_rows: Optional[int],
|
||||
) -> dict[str, Any]:
|
||||
"""Copy user metadata rows; derived FTS/topic keys (``_GENERATED_META_KEYS``) are regenerated, not copied."""
|
||||
problem = _state_meta_precheck(source, destination, source_rows, salvage=salvage)
|
||||
@@ -773,14 +714,8 @@ def _copy_state_meta(
|
||||
return str(row[columns.index("key")]) not in _GENERATED_META_KEYS
|
||||
|
||||
result = _copy_table_salvage(
|
||||
source,
|
||||
destination,
|
||||
"state_meta",
|
||||
chunk_size=chunk_size,
|
||||
progress_cb=progress_cb,
|
||||
source_rows=source_rows,
|
||||
insert_prefix="INSERT OR REPLACE",
|
||||
row_filter=keep_user_meta,
|
||||
source, destination, "state_meta", chunk_size=chunk_size, progress_cb=progress_cb,
|
||||
source_rows=source_rows, insert_prefix="INSERT OR REPLACE", row_filter=keep_user_meta,
|
||||
)
|
||||
result["source_meta_rows"] = result.pop("source_rows")
|
||||
result["excluded_keys"] = sorted(_GENERATED_META_KEYS)
|
||||
@@ -797,16 +732,9 @@ def _copy_state_meta(
|
||||
pass # the copy loop below will return the concrete read error
|
||||
|
||||
return _copy_rows(
|
||||
source,
|
||||
destination,
|
||||
f"SELECT key, value FROM state_meta WHERE key NOT IN ({placeholders})",
|
||||
params,
|
||||
"INSERT OR REPLACE INTO state_meta(key, value) VALUES (?, ?)",
|
||||
table="state_meta",
|
||||
chunk_size=chunk_size,
|
||||
progress_cb=progress_cb,
|
||||
expected_rows=filtered_source_rows,
|
||||
result=_state_meta_result(source_rows),
|
||||
source, destination, f"SELECT key, value FROM state_meta WHERE key NOT IN ({placeholders})", params,
|
||||
"INSERT OR REPLACE INTO state_meta(key, value) VALUES (?, ?)", table="state_meta", chunk_size=chunk_size,
|
||||
progress_cb=progress_cb, expected_rows=filtered_source_rows, result=_state_meta_result(source_rows),
|
||||
)
|
||||
|
||||
|
||||
@@ -967,13 +895,8 @@ def _verify_fts_indexes(conn: sqlite3.Connection, verification: dict[str, Any])
|
||||
|
||||
|
||||
def _verify_row_counts(
|
||||
conn: sqlite3.Connection,
|
||||
verification: dict[str, Any],
|
||||
*,
|
||||
expected_counts: dict[str, Optional[int]],
|
||||
copy_report: dict[str, dict[str, Any]],
|
||||
allow_partial: bool,
|
||||
orphan_cleanup: Optional[dict[str, Any]],
|
||||
conn: sqlite3.Connection, verification: dict[str, Any], *, expected_counts: dict[str, Optional[int]],
|
||||
copy_report: dict[str, dict[str, Any]], allow_partial: bool, orphan_cleanup: Optional[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Compare recovered counts and copy statuses against the source; classify shortfalls as loss."""
|
||||
|
||||
@@ -1039,12 +962,8 @@ def _verify_row_counts(
|
||||
|
||||
|
||||
def _verify_recovered_database(
|
||||
output: Path,
|
||||
*,
|
||||
expected_counts: dict[str, Optional[int]],
|
||||
copy_report: dict[str, dict[str, Any]],
|
||||
allow_partial: bool = False,
|
||||
orphan_cleanup: Optional[dict[str, Any]] = None,
|
||||
output: Path, *, expected_counts: dict[str, Optional[int]], copy_report: dict[str, dict[str, Any]],
|
||||
allow_partial: bool = False, orphan_cleanup: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
verification: dict[str, Any] = {"errors": [], "warnings": [], "loss_detected": False}
|
||||
|
||||
@@ -1057,11 +976,7 @@ def _verify_recovered_database(
|
||||
try:
|
||||
_verify_structure(conn, verification)
|
||||
_verify_row_counts(
|
||||
conn,
|
||||
verification,
|
||||
expected_counts=expected_counts,
|
||||
copy_report=copy_report,
|
||||
allow_partial=allow_partial,
|
||||
conn, verification, expected_counts=expected_counts, copy_report=copy_report, allow_partial=allow_partial,
|
||||
orphan_cleanup=orphan_cleanup,
|
||||
)
|
||||
_verify_fts_indexes(conn, verification)
|
||||
@@ -1103,14 +1018,8 @@ def _finalize_derived_metadata(destination: sqlite3.Connection) -> dict[str, Any
|
||||
|
||||
|
||||
def _recover_via_lost_and_found(
|
||||
*,
|
||||
source: Path,
|
||||
snapshot_source: Path,
|
||||
snapshot_dir: Path,
|
||||
output: Path,
|
||||
inspection: dict[str, Any],
|
||||
disk_space: dict[str, Any],
|
||||
missing_required: list[str],
|
||||
*, source: Path, snapshot_source: Path, snapshot_dir: Path, output: Path, inspection: dict[str, Any],
|
||||
disk_space: dict[str, Any], missing_required: list[str],
|
||||
) -> dict[str, Any]:
|
||||
"""Best-effort page-level salvage when table schemas are unreadable: the sqlite3 CLI's ``.recover``
|
||||
(shell-only, not in Python's ``sqlite3``) rebuilds rows into a scratch lost_and_found database which
|
||||
@@ -1164,10 +1073,7 @@ def _recover_via_lost_and_found(
|
||||
"messages_removed": 0, "total_removed_or_relinked": 0,
|
||||
}
|
||||
verification = _verify_recovered_database(
|
||||
output,
|
||||
expected_counts={"sessions": None, "messages": None},
|
||||
copy_report=copy_report,
|
||||
allow_partial=True,
|
||||
output, expected_counts={"sessions": None, "messages": None}, copy_report=copy_report, allow_partial=True,
|
||||
orphan_cleanup=orphan_cleanup,
|
||||
)
|
||||
verification["loss_detected"] = True
|
||||
@@ -1187,14 +1093,8 @@ def _recover_via_lost_and_found(
|
||||
|
||||
|
||||
def _recovery_report(
|
||||
source: Path,
|
||||
output: Path,
|
||||
inspection: dict[str, Any],
|
||||
disk_space: dict[str, Any],
|
||||
verification: dict[str, Any],
|
||||
*,
|
||||
on_source_change: str,
|
||||
**fields: Any,
|
||||
source: Path, output: Path, inspection: dict[str, Any], disk_space: dict[str, Any], verification: dict[str, Any],
|
||||
*, on_source_change: str, **fields: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""The ``recover`` report: shared header, mode-specific ``fields``, then the verdict. A source bundle
|
||||
that changed during recovery is a verification error and also clears ``verification[on_source_change]``.
|
||||
@@ -1225,13 +1125,8 @@ def _recovery_report(
|
||||
|
||||
|
||||
def recover_session_database(
|
||||
source_path: Path,
|
||||
output_path: Path,
|
||||
*,
|
||||
work_dir: Optional[Path] = None,
|
||||
chunk_size: int = 1_000,
|
||||
progress_cb: Optional[ProgressCallback] = None,
|
||||
allow_partial: bool = False,
|
||||
source_path: Path, output_path: Path, *, work_dir: Optional[Path] = None, chunk_size: int = 1_000,
|
||||
progress_cb: Optional[ProgressCallback] = None, allow_partial: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Recover canonical rows into a separate current-schema database. The source and its sidecars are
|
||||
copied before SQLite opens anything; ``output_path`` must not exist and is never swapped into place."""
|
||||
@@ -1266,8 +1161,7 @@ def recover_session_database(
|
||||
destination_conn: Optional[sqlite3.Connection] = None
|
||||
try:
|
||||
destination_conn = _fresh_destination(
|
||||
output,
|
||||
topic_tables=any(inspection["tables"][table].get("available") for table in _TOPIC_TABLES),
|
||||
output, topic_tables=any(inspection["tables"][table].get("available") for table in _TOPIC_TABLES),
|
||||
)
|
||||
|
||||
copy_report: dict[str, dict[str, Any]] = {}
|
||||
|
||||
Reference in New Issue
Block a user