fix(sessions): probe sqlite3 CLI for .recover capability, not just PATH presence

Ubuntu CI (and other distro builds) ship a sqlite3 shell compiled without
the sqlite_dbpage virtual table that .recover requires, so PATH presence
alone let the lane attempt and fail with 'no such table: sqlite_dbpage'.
find_sqlite3_cli() now probes .recover on a scratch DB once; the test skip
gate uses the same probe, and the no-CLI guidance names the capability
requirement.
This commit is contained in:
Teknium
2026-08-12 16:59:12 -07:00
parent 6dad74596e
commit 97c06dcfd7
2 changed files with 53 additions and 9 deletions
+48 -8
View File
@@ -27,6 +27,7 @@ import re
import shutil
import sqlite3
import subprocess
import tempfile
from pathlib import Path
from typing import Any, Optional
@@ -55,13 +56,15 @@ _EPOCH_LOW = 1_000_000_000.0 # 2001
_EPOCH_HIGH = 4_000_000_000.0 # 2096
SQLITE3_CLI_GUIDANCE = (
"A last-resort page-level salvage is available when the `sqlite3` "
"command-line shell is installed: its `.recover` command can rebuild "
"rows into lost_and_found tables even when the table schemas are "
"A last-resort page-level salvage is available when a `.recover`-capable "
"`sqlite3` command-line shell is installed: its `.recover` command can "
"rebuild rows into lost_and_found tables even when the table schemas are "
"unreadable (this is a CLI-only feature, not part of Python's sqlite3 "
"module). Install the sqlite3 CLI (e.g. `apt install sqlite3` or "
"`brew install sqlite`) so it is on PATH, then re-run with "
"--allow-partial."
"module, and some distro builds lack it — the shell must include the "
"sqlite_dbpage extension, as the official builds from sqlite.org do). "
"Install such a sqlite3 CLI (e.g. `brew install sqlite` or the "
"precompiled sqlite-tools from sqlite.org) so it is on PATH, then re-run "
"with --allow-partial."
)
@@ -70,9 +73,46 @@ class LostAndFoundError(RuntimeError):
def find_sqlite3_cli() -> Optional[str]:
"""Return the sqlite3 CLI path, or None when page-level salvage is out."""
"""Return a ``.recover``-capable sqlite3 CLI path, or None.
return shutil.which("sqlite3")
PATH presence is not enough: distro builds (e.g. Ubuntu's) can ship a
sqlite3 shell compiled without the ``sqlite_dbpage`` virtual table that
``.recover`` requires — those fail every recovery with
``no such table: sqlite_dbpage``. Probe capability on a scratch DB once
instead of discovering it mid-recovery.
"""
binary = shutil.which("sqlite3")
if binary is None:
return None
return binary if _cli_supports_recover(binary) else None
def _cli_supports_recover(binary: str) -> bool:
"""True when ``binary`` can run ``.recover`` (has sqlite_dbpage)."""
scratch_dir = tempfile.mkdtemp(prefix="hermes-recover-probe-")
scratch = Path(scratch_dir) / "probe.db"
try:
conn = sqlite3.connect(str(scratch))
try:
conn.execute("CREATE TABLE t (x)")
conn.execute("INSERT INTO t VALUES (1)")
conn.commit()
finally:
conn.close()
probe = subprocess.run(
[binary, "-readonly", str(scratch), ".recover"],
capture_output=True,
timeout=30,
)
if probe.returncode != 0:
return False
return b"sqlite_dbpage" not in probe.stderr
except (OSError, subprocess.SubprocessError, sqlite3.Error):
return False
finally:
shutil.rmtree(scratch_dir, ignore_errors=True)
def run_cli_lost_and_found_recover(
@@ -35,7 +35,11 @@ from tests.hermes_cli.test_session_recovery import (
)
HAVE_SQLITE3_CLI = shutil.which("sqlite3") is not None
from hermes_cli.session_lost_and_found import find_sqlite3_cli
# .recover needs a sqlite3 shell built with sqlite_dbpage — PATH presence
# alone is not enough (Ubuntu CI ships a build without it).
HAVE_SQLITE3_CLI = find_sqlite3_cli() is not None
# ── physical corruption helpers ─────────────────────────────────────────────