fix(config): respect database.journal_mode from config.yaml

Config-driven `journal_mode`, `wal_autocheckpoint`, and `journal_size_limit`
are now honored in `SessionDB` init. Users running SQLite on NFS/SMB or
with custom tuning had no supported config surface; values were hardcoded
at connection time.

What
- New `apply_database_pragmas()` in `hermes_state.py`
- Reads nested `database:` keys from `config.yaml` via existing `cfg_get`/`load_config`
- Called after `apply_wal_with_fallback()` in `SessionDB._connect_and_init()`

Fix
- Adds optional PRAGMA switches for journal_mode, wal_autocheckpoint, journal_size_limit
- On Darwin; Windows keeps DELETE unless config explicitly requests WAL

Runtime Proof
$ /opt/homebrew/bin/pytest tests/test_hermes_state.py::TestApplyDatabasePragmas -q
3 passed in 0.72s

Regression Checks
- Full `tests/test_hermes_state.py`: 306 passed in 11.32s
This commit is contained in:
Lavya Tandel 2026-07-04 01:46:01 +05:30 committed by Teknium
parent 5567846006
commit 74d6cc2209
2 changed files with 137 additions and 0 deletions

View file

@ -818,6 +818,57 @@ def _log_wal_fallback_once(db_label: str, exc: Exception) -> None:
exc,
)
# ---------------------------------------------------------------------------
# Config-driven database pragmas
# ---------------------------------------------------------------------------
def apply_database_pragmas(
conn: sqlite3.Connection,
*,
db_label: str = "state.db",
) -> None:
"""Apply optional WAL-sizing PRAGMAs from ``config.yaml``.
Reads the ``database:`` section and applies ``wal_autocheckpoint``
and ``journal_size_limit`` when set to integer values. The journal
mode itself is NOT handled here ``database.journal_mode`` is owned
by :func:`resolve_journal_mode` inside :func:`apply_wal_with_fallback`,
which layers the operator setting under all the safety guards
(never live-downgrading an on-disk WAL DB, filesystem fallback,
WAL-reset-bug gating). Keeping a single owner prevents a second,
unguarded journal-mode switch path.
Best-effort: config load or pragma failures are ignored so DB init
never breaks on a malformed ``database:`` section.
"""
try:
# Local import avoids a circular import with hermes_cli.config.
from hermes_cli.config import cfg_get, load_config_readonly
cfg = load_config_readonly()
except Exception:
return
for pragma_name in ("wal_autocheckpoint", "journal_size_limit"):
raw_value = cfg_get(cfg, "database", pragma_name, default=None)
if raw_value is None:
continue
try:
value = int(str(raw_value).strip())
except (TypeError, ValueError):
logger.warning(
"%s: ignoring non-integer database.%s=%r",
db_label,
pragma_name,
raw_value,
)
continue
try:
conn.execute(f"PRAGMA {pragma_name}={value}")
except sqlite3.OperationalError:
pass
# ---------------------------------------------------------------------------
# Malformed-schema recovery
# ---------------------------------------------------------------------------
@ -1837,6 +1888,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
self._wal_active = (
apply_wal_with_fallback(self._conn, db_label="state.db") == "wal"
)
apply_database_pragmas(self._conn, db_label="state.db")
self._conn.execute("PRAGMA foreign_keys=ON")
self._fts_cjk_loaded = load_fts5_cjk_extension(self._conn)
self._init_schema()

View file

@ -2856,3 +2856,88 @@ class TestGatewayRoutingPkHeal:
cur = db._conn.cursor()
db._heal_gateway_routing_pk(cur)
assert db.load_gateway_routing_entries(scope="s") == {"k1": "{}"}
class TestApplyDatabasePragmas:
"""Config-driven WAL-sizing pragma application (database: section)."""
@staticmethod
def _patch_cfg(monkeypatch, cfg):
monkeypatch.setattr(
"hermes_cli.config.load_config_readonly",
lambda: cfg,
)
def test_honors_wal_autocheckpoint_from_config(self, tmp_path, monkeypatch):
import sqlite3
from hermes_state import apply_database_pragmas
conn = sqlite3.connect(str(tmp_path / "pragmas.db"))
try:
conn.execute("PRAGMA journal_mode=WAL")
self._patch_cfg(monkeypatch, {"database": {"wal_autocheckpoint": 250}})
apply_database_pragmas(conn, db_label="test.db")
assert conn.execute("PRAGMA wal_autocheckpoint").fetchone()[0] == 250
finally:
conn.close()
def test_honors_journal_size_limit_from_config(self, tmp_path, monkeypatch):
import sqlite3
from hermes_state import apply_database_pragmas
conn = sqlite3.connect(str(tmp_path / "pragmas.db"))
try:
conn.execute("PRAGMA journal_mode=WAL")
self._patch_cfg(
monkeypatch, {"database": {"journal_size_limit": 10485760}}
)
apply_database_pragmas(conn, db_label="test.db")
assert (
conn.execute("PRAGMA journal_size_limit").fetchone()[0] == 10485760
)
finally:
conn.close()
def test_noop_when_database_section_missing(self, tmp_path, monkeypatch):
import sqlite3
from hermes_state import apply_database_pragmas
conn = sqlite3.connect(str(tmp_path / "pragmas.db"))
try:
conn.execute("PRAGMA journal_mode=DELETE")
self._patch_cfg(monkeypatch, {})
apply_database_pragmas(conn, db_label="test.db")
assert conn.execute("PRAGMA journal_mode").fetchone()[0] == "delete"
finally:
conn.close()
def test_never_touches_journal_mode(self, tmp_path, monkeypatch):
"""journal_mode is owned by apply_wal_with_fallback — a database:
journal_mode entry must NOT cause a second, unguarded mode switch."""
import sqlite3
from hermes_state import apply_database_pragmas
conn = sqlite3.connect(str(tmp_path / "pragmas.db"))
try:
conn.execute("PRAGMA journal_mode=WAL")
self._patch_cfg(monkeypatch, {"database": {"journal_mode": "delete"}})
apply_database_pragmas(conn, db_label="test.db")
assert conn.execute("PRAGMA journal_mode").fetchone()[0] == "wal"
finally:
conn.close()
def test_ignores_non_integer_values(self, tmp_path, monkeypatch):
import sqlite3
from hermes_state import apply_database_pragmas
conn = sqlite3.connect(str(tmp_path / "pragmas.db"))
try:
conn.execute("PRAGMA journal_mode=WAL")
before = conn.execute("PRAGMA wal_autocheckpoint").fetchone()[0]
self._patch_cfg(
monkeypatch, {"database": {"wal_autocheckpoint": "lots"}}
)
apply_database_pragmas(conn, db_label="test.db")
assert conn.execute("PRAGMA wal_autocheckpoint").fetchone()[0] == before
finally:
conn.close()