From 4f6544ab1aff7055a04142cda9e0bb13dd5e6040 Mon Sep 17 00:00:00 2001 From: busya Date: Sat, 16 May 2026 09:33:15 +0300 Subject: [PATCH] =?UTF-8?q?fix(translate):=20exclude=20key=20columns=20fro?= =?UTF-8?q?m=20cache=20hash=20=E2=80=94=20only=20text=20+=20context=20fiel?= =?UTF-8?q?ds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _compute_source_hash now accepts context_keys parameter - source_data is filtered to only include context-relevant fields (not row_id, primary keys) - If no context_columns configured, hash is based on source_text + dict_hash + config_hash - This ensures identical text in different rows produces a cache hit --- backend/src/models/translate.py | 2 +- backend/src/plugins/translate/executor.py | 26 +++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/backend/src/models/translate.py b/backend/src/models/translate.py index c71e4c18..4ffcccf8 100644 --- a/backend/src/models/translate.py +++ b/backend/src/models/translate.py @@ -133,7 +133,7 @@ class TranslationRecord(Base): token_count_output = Column(Integer, nullable=True) translation_duration_ms = Column(Integer, nullable=True) source_hash = Column(String, nullable=True, - comment="SHA256(source_text+source_data+dict_snapshot_hash+config_hash) for cache dedup") + comment="SHA256(source_text+context+dict_snapshot_hash+config_hash) for cache dedup — excludes key/identifier columns") created_at = Column(DateTime, default=lambda: datetime.now(UTC)) languages = relationship("TranslationLanguage", back_populates="record") diff --git a/backend/src/plugins/translate/executor.py b/backend/src/plugins/translate/executor.py index a4e4d86d..8a9b59fd 100644 --- a/backend/src/plugins/translate/executor.py +++ b/backend/src/plugins/translate/executor.py @@ -127,18 +127,33 @@ def _enforce_dictionary( # #region _compute_source_hash [TYPE Function] # @BRIEF Compute deterministic cache key for a source row. -# SHA256 of (source_text + source_data + dict_config_ctx) so that changing -# the text, context, dictionary snapshot, or job config produces a new key. +# SHA256 of (source_text + context_fields + dict_snapshot_hash + config_hash). +# Key columns (row_id, primary keys) are EXCLUDED — only the text content +# and meaningful context affect the hash. This ensures identical text with +# different row identifiers still produces a cache hit. def _compute_source_hash( source_text: str, source_data: dict | None, dict_snapshot_hash: str | None, config_hash: str | None, + context_keys: list[str] | None = None, ) -> str: - """Deterministic cache key for a translation source row.""" + """Deterministic cache key for a translation source row. + + Only includes source_text and context-relevant fields from source_data. + Key/identifier columns are excluded so identical text in different rows hits cache. + """ + # Extract only context-relevant fields from source_data (not keys/identifiers) + context_data: dict[str, str] = {} + if source_data and context_keys: + for key in context_keys: + val = source_data.get(key) + if val is not None: + context_data[key] = str(val) + payload = json.dumps({ "text": source_text, - "data": source_data, + "ctx": context_data, # only context columns, no key/row_id "dict_hash": dict_snapshot_hash or "", "config_hash": config_hash or "", }, sort_keys=True, ensure_ascii=False) @@ -719,9 +734,12 @@ class TranslationExecutor: if not source_text: continue source_data = row.get("source_data") + # Build context_keys from job config (exclude key columns from hash) + ctx_keys = list(job.context_columns or []) source_hash = _compute_source_hash( source_text, source_data, dict_snapshot_hash, config_hash, + context_keys=ctx_keys, ) row["_source_hash"] = source_hash cached = _check_translation_cache(self.db, source_hash)