From 3394ddcf4cd8363d7a52da73c63bec66facc922d Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 11 Oct 2026 21:57:25 +0400 Subject: [PATCH] =?UTF-8?q?fix(hub):=20=D1=82=D1=80=D0=B0=D0=BD=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B2=20set=5Fnote=20=D0=B8?= =?UTF-8?q?=20rename=5Fnote,=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=82=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D1=80=D0=B0=D0=B2=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rename_note: при old_key == new_key ранний выход (DELETE раньше стирал единственную заметку); DELETE и UPDATE выполняются в транзакции. set_note: чтение, слияние и запись в одной транзакции (соединение общее для потоков). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o --- src/jira_hub/mirror.py | 24 ++++++++++++++---------- tests/test_mirror.py | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/jira_hub/mirror.py b/src/jira_hub/mirror.py index cd97480..a8961ef 100644 --- a/src/jira_hub/mirror.py +++ b/src/jira_hub/mirror.py @@ -95,20 +95,24 @@ def set_note(conn, key, fields: dict, updated_by: str) -> dict: unknown = set(fields) - set(NOTE_FIELDS) if unknown: raise ValueError(f"неизвестные поля заметки: {', '.join(sorted(unknown))}") - cur = get_note(conn, key) or {"key": key} - cur.update(fields) - cur["updated_at"], cur["updated_by"] = db.now(), updated_by - cols = ["key"] + NOTE_FIELDS + ["updated_at", "updated_by"] - conn.execute(f"INSERT INTO issue_note({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))}) " - "ON CONFLICT(key) DO UPDATE SET " + ", ".join(f"{c} = excluded.{c}" for c in cols[1:]), - [cur.get(c) for c in cols]) + with db.transaction(conn): # чтение, слияние и запись — атомарно (соединение общее для потоков) + cur = get_note(conn, key) or {"key": key} + cur.update(fields) + cur["updated_at"], cur["updated_by"] = db.now(), updated_by + cols = ["key"] + NOTE_FIELDS + ["updated_at", "updated_by"] + conn.execute(f"INSERT INTO issue_note({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))}) " + "ON CONFLICT(key) DO UPDATE SET " + ", ".join(f"{c} = excluded.{c}" for c in cols[1:]), + [cur.get(c) for c in cols]) return get_note(conn, key) def rename_note(conn, old_key, new_key) -> None: """После создания задачи: заметка с временного ключа переезжает на настоящий (старая запись под новым ключом, если вдруг была, уступает место).""" - if get_note(conn, old_key) is None: + if old_key == new_key: return - conn.execute("DELETE FROM issue_note WHERE key = ?", (new_key,)) - conn.execute("UPDATE issue_note SET key = ? WHERE key = ?", (new_key, old_key)) + with db.transaction(conn): + if get_note(conn, old_key) is None: + return + conn.execute("DELETE FROM issue_note WHERE key = ?", (new_key,)) + conn.execute("UPDATE issue_note SET key = ? WHERE key = ?", (new_key, old_key)) diff --git a/tests/test_mirror.py b/tests/test_mirror.py index d281b5c..d825f62 100644 --- a/tests/test_mirror.py +++ b/tests/test_mirror.py @@ -69,3 +69,26 @@ def test_list_issues_filters(): assert [i["key"] for i in mirror.list_issues(conn, {"q": "A-2"})] == ["A-2"] assert mirror.list_issues(conn, {"assignee": "ivanov"})[0]["work_kind"] is None assert len(mirror.list_issues(conn, None, limit=1)) == 1 + + +def test_rename_note_same_key_keeps_note(): + conn = db.connect(":memory:") + mirror.apply_state(conn, [issue("A-1")], []) + mirror.set_note(conn, "A-1", {"fact_status": "В работе"}, "x") + mirror.rename_note(conn, "A-1", "A-1") + assert mirror.get_note(conn, "A-1")["fact_status"] == "В работе" + + +def test_rename_note_missing_is_noop(): + conn = db.connect(":memory:") + mirror.rename_note(conn, "new:404", "A-9") # заметки нет — не падает + assert mirror.get_note(conn, "new:404") is None + assert mirror.get_note(conn, "A-9") is None + + +def test_set_note_partial_keeps_other_fields(): + conn = db.connect(":memory:") + mirror.set_note(conn, "A-1", {"work_kind": "разработка", "done_text": "сделано"}, "x") + got = mirror.set_note(conn, "A-1", {"fact_status": "Готово"}, "y") + assert got["work_kind"] == "разработка" and got["done_text"] == "сделано" + assert got["fact_status"] == "Готово" and got["updated_by"] == "y"