From c3a7b6eefdf6173ec55f20d2b9962e91530719ae Mon Sep 17 00:00:00 2001 From: Athena Kaminsky Date: Wed, 26 Aug 2026 03:32:15 -0500 Subject: [PATCH] fix(sync): close SQLite handles before restore Use contextlib.closing for dump, comparison, and restore connections so Windows can unlink the live database immediately after the comparison step. --- bin/sync.py | 12 ++++++++---- tests/test_smoke.py | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/bin/sync.py b/bin/sync.py index a3bef49..ab56a4d 100644 --- a/bin/sync.py +++ b/bin/sync.py @@ -18,6 +18,7 @@ bin/db-compare.sh could never work there. A fresh machine clones first (git clone nexus-core), then runs this. """ import argparse +import contextlib import os import re import shutil @@ -162,7 +163,7 @@ def dump_db() -> bool: if not DB.exists(): return False try: - with sqlite3.connect(f"file:{DB}?mode=ro", uri=True) as conn: + with contextlib.closing(sqlite3.connect(f"file:{DB}?mode=ro", uri=True)) as conn: ext = _vec0_extension() if ext: try: @@ -232,9 +233,12 @@ def compare(db: Path = DB, dump: Path = DB_SQL) -> str: if not db.exists(): return "no-live" try: - with sqlite3.connect(f"file:{db}?mode=ro", uri=True) as live_conn: + # closing(), not sqlite3's context manager: that one commits without + # closing, and restore_db() unlinks the DB right after calling this - + # Windows fails that unlink while any handle is still open. + with contextlib.closing(sqlite3.connect(f"file:{db}?mode=ro", uri=True)) as live_conn: live = _state(live_conn) - with sqlite3.connect(":memory:") as dump_conn: + with contextlib.closing(sqlite3.connect(":memory:")) as dump_conn: dump_conn.executescript(dump.read_text(encoding="utf-8")) backup = _state(dump_conn) except (sqlite3.Error, OSError): @@ -287,7 +291,7 @@ def restore_db() -> None: for suffix in ("", "-wal", "-shm"): Path(str(DB) + suffix).unlink(missing_ok=True) try: - with sqlite3.connect(DB) as conn: + with contextlib.closing(sqlite3.connect(DB)) as conn, conn: conn.executescript(DB_SQL.read_text(encoding="utf-8")) print("Memory DB restored (conversations + history + facts).") except sqlite3.Error as exc: diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 46cf2a2..ccacfc9 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -264,13 +264,17 @@ def test_sync_compare_detects_direction(tmp_path): """The guard that stops a stale box from overwriting the other's chats. Both backup and restore refuse to run when this says the wrong thing, so a silent break here loses conversation history.""" + import contextlib import sqlite3 as sq sync = _load_sync() db, dump = tmp_path / "memory.db", tmp_path / "memory.db.sql" def write(rows): db.unlink(missing_ok=True) - with sq.connect(db) as conn: + # closing() then the connection itself: sqlite3's own context manager + # commits but never closes, and Windows refuses to unlink a file that + # still has an open handle. + with contextlib.closing(sq.connect(db)) as conn, conn: # updated_at REAL, matching the production schema in store.py. A TEXT # column here hid a real TypeError for months: the comparison in # _extra() ran str-vs-str in the test and str-vs-float in the field.