refactor(state): consolidate SQL LIKE escaping onto one shared helper

Follow-up to #79722, which introduced _escape_like in hermes_state.py for
the prune/archive filter fix. The same three-replace escape chain existed
as five more inline copies in hermes_state.py and two in
hermes_state_search.py (which must not import hermes_state — cycle).

Move the helper to hermes_state_common.escape_like (the module that exists
for exactly this) and route every copy through it:

- hermes_state.py: session-ID prefix resolution, find_session_by_title,
  get_next_title_in_lineage, the _like_pattern closure in list projection,
  and the kanban cwd retag
- hermes_state_search.py: the two LIKE-fallback token escapes

hermes_state re-imports it as _escape_like for back-compat. No behavior
change: every site produces byte-identical SQL patterns.
This commit is contained in:
kshitij
2026-08-06 04:23:53 +05:30
committed by kshitij
parent 55e70f570e
commit 52a5fc0048
3 changed files with 21 additions and 26 deletions
+6 -24
View File
@@ -56,6 +56,7 @@ from hermes_state_common import ( # noqa: F401 (re-exported for back-compat)
_shape_preview,
_sql_session_last_active,
_sql_session_last_active_by_id,
escape_like as _escape_like,
DEFERRED_INDEX_SQL,
FTS_CJK_STALE_KEY,
FTS_SQL,
@@ -176,17 +177,6 @@ def _delegate_from_json(col: str = "model_config") -> str:
_MODEL_CONFIG_ROW_MISSING = object()
def _escape_like(text: str) -> str:
"""Escape SQL LIKE wildcards so an operator-supplied filter matches
literally. Pair with ``ESCAPE '\\'`` in the clause.
``%`` and ``_`` are wildcards to LIKE, and ``_`` in particular is common
in the values these filters run against (branch names, session titles).
A filter documented as a substring match must not silently widen.
"""
return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
def _cwd_prefix_clause(cwd_prefix: str) -> Tuple[str, List[str]]:
prefix = cwd_prefix.rstrip("/\\") or cwd_prefix
# ``_`` and ``%`` are LIKE wildcards but ordinary characters in a path
@@ -5286,12 +5276,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
if exact:
return exact["id"]
escaped = (
session_id_or_prefix
.replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
)
escaped = _escape_like(session_id_or_prefix)
with self._lock:
cursor = self._conn.execute(
"SELECT id FROM sessions WHERE id LIKE ? ESCAPE '\\' ORDER BY started_at DESC LIMIT 2",
@@ -5680,7 +5665,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
# Also search for numbered variants: "title #2", "title #3", etc.
# Escape SQL LIKE wildcards (%, _) in the title to prevent false matches
escaped = title.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
escaped = _escape_like(title)
with self._read_ctx() as conn:
cursor = conn.execute(
"SELECT id, title, started_at FROM sessions "
@@ -5711,7 +5696,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
# Find all existing numbered variants
# Escape SQL LIKE wildcards (%, _) in the base to prevent false matches
escaped = base.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
escaped = _escape_like(base)
with self._lock:
cursor = self._conn.execute(
"SELECT title FROM sessions WHERE title = ? OR title LIKE ? ESCAPE '\\'",
@@ -5964,10 +5949,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
filter_clauses: List[str] = []
def _like_pattern(needle: str) -> str:
escaped = (
needle.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
)
return f"%{escaped}%"
return f"%{_escape_like(needle)}%"
if id_needle:
# Admit a surfaced row if its own id or any id in its forward
@@ -8856,7 +8838,7 @@ class SessionDB(SessionSearchMixin, SessionSchemaMixin, SessionPortabilityMixin)
cursor = conn.execute(
"UPDATE sessions SET source = 'kanban' "
"WHERE source = 'cli' AND (cwd = ? OR cwd LIKE ? ESCAPE '\\')",
(prefix, prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "/%"),
(prefix, _escape_like(prefix) + "/%"),
)
# Read rowcount before set_meta reuses this cursor for its INSERT,
# which would otherwise overwrite it with the meta write's count.
+12
View File
@@ -34,6 +34,18 @@ _PREVIEW_SCAFFOLD_WINDOW = 400
_PREVIEW_MAX_CHARS = 60
def escape_like(text: str) -> str:
"""Escape SQL LIKE wildcards so operator/session-derived text matches
literally. Pair with ``ESCAPE '\\'`` in the clause.
``%`` and ``_`` are wildcards to LIKE, and ``_`` in particular is common
in the values these patterns run against (branch names, session titles,
filesystem paths). A match documented as substring/prefix must not
silently widen.
"""
return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
_PREVIEW_CONTENT_SQL = "REPLACE(REPLACE(m.content, X'0A', ' '), X'0D', ' ')"
+3 -2
View File
@@ -25,6 +25,7 @@ from hermes_state_common import (
MAX_FTS5_QUERY_CHARS,
SCHEMA_VERSION,
_FTS_CJK_TRIGGERS,
escape_like as _escape_like,
)
# Moved methods logged under the "hermes_state" logger before the split;
@@ -1730,7 +1731,7 @@ class SessionSearchMixin:
token_clauses = []
like_params: list = []
for tok in non_op_tokens:
esc = tok.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
esc = _escape_like(tok)
token_clauses.append(
"(m.content LIKE ? ESCAPE '\\' OR m.tool_name LIKE ? ESCAPE '\\' OR m.tool_calls LIKE ? ESCAPE '\\')"
)
@@ -1986,7 +1987,7 @@ class SessionSearchMixin:
where = ["m.id > ? AND m.id <= ?"]
params: list = [progress, high_water]
for term in terms:
esc = term.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
esc = _escape_like(term)
where.append(
"(m.content LIKE ? ESCAPE '\\' OR m.tool_name LIKE ? ESCAPE '\\' "
"OR m.tool_calls LIKE ? ESCAPE '\\')"