fix(opencode): a -wal left without its -shm is read, not ignored (Issue #60, Linux item 5, review)
_ro_connect read every database without both sidecars as immutable, which skips a -wal's committed rows: a recovered DB+WAL with no -shm showed no sessions (planner-reviewer-codex-01). That state - no writer runs without a -shm - is now read from a private DB+WAL copy, where SQLite may rebuild the index, loaded into memory; nothing beside the source is touched. The query matrix gains the walonly state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
463817ee34
commit
7803d47026
2 files changed
+28
-9
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
import os, sys, sqlite3, shutil, subprocess
|
import os, sys, sqlite3, shutil, subprocess, tempfile
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
from typing import Optional, Any, List
|
from typing import Optional, Any, List
|
||||||
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names, db_rows_naming, conversation_in_use
|
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext, scan_names, db_rows_naming, conversation_in_use
|
||||||
@@ -11,12 +11,29 @@ _LEGACY_TABLES = (("message", "session_id"), ("messages", "session_id"), ("part"
|
|||||||
("session", "id"), ("sessions", "id"))
|
("session", "id"), ("sessions", "id"))
|
||||||
|
|
||||||
def _ro_connect(db: str):
|
def _ro_connect(db: str):
|
||||||
"""A read that neither creates nor removes anything beside db (Issue #60):
|
"""A read that neither creates nor removes anything beside db (Issue #60).
|
||||||
the WAL sidecars are used only when both already exist (a live writer
|
Both WAL sidecars (a live writer keeps them): read through them. No -wal:
|
||||||
keeps them); otherwise the file is read as immutable."""
|
nothing to apply, so the file is read as immutable. A -wal without its
|
||||||
|
-shm (left behind - no writer runs without one) still holds committed
|
||||||
|
rows: they are read from a private copy, where SQLite may rebuild the
|
||||||
|
index, loaded into memory."""
|
||||||
path = os.path.abspath(db)
|
path = os.path.abspath(db)
|
||||||
both = os.path.exists(path + "-wal") and os.path.exists(path + "-shm")
|
wal, shm = os.path.exists(path + "-wal"), os.path.exists(path + "-shm")
|
||||||
return sqlite3.connect(f"file:{quote(path)}?mode=ro" + ("" if both else "&immutable=1"), uri=True)
|
if wal and shm:
|
||||||
|
return sqlite3.connect(f"file:{quote(path)}?mode=ro", uri=True)
|
||||||
|
if not wal:
|
||||||
|
return sqlite3.connect(f"file:{quote(path)}?mode=ro&immutable=1", uri=True)
|
||||||
|
mem = sqlite3.connect(":memory:")
|
||||||
|
with tempfile.TemporaryDirectory(prefix="mam-opencode-") as tmp:
|
||||||
|
copy = os.path.join(tmp, "opencode.db")
|
||||||
|
shutil.copyfile(path, copy)
|
||||||
|
shutil.copyfile(path + "-wal", copy + "-wal")
|
||||||
|
src = sqlite3.connect(copy)
|
||||||
|
try:
|
||||||
|
src.backup(mem)
|
||||||
|
finally:
|
||||||
|
src.close()
|
||||||
|
return mem
|
||||||
|
|
||||||
|
|
||||||
# (binary, HOME, XDG_DATA_HOME) -> the db `debug paths` printed for them.
|
# (binary, HOME, XDG_DATA_HOME) -> the db `debug paths` printed for them.
|
||||||
|
|||||||
@@ -135,8 +135,9 @@ def _wal_writer(db: Path, ws: Path) -> sqlite3.Connection:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def wal_home(tmp_path, monkeypatch):
|
def wal_home(tmp_path, monkeypatch):
|
||||||
"""(adapter, ctx, db, ws, make(state)) - make builds the database in one
|
"""(adapter, ctx, db, ws, make(state)) - make builds the database in one
|
||||||
of three states: `clean` (closed: no sidecars), `leftover` (both sidecars
|
of four states: `clean` (closed: no sidecars), `leftover` (both sidecars
|
||||||
left behind by a writer that is gone, the rows only in the -wal),
|
left behind by a writer that is gone, the rows only in the -wal),
|
||||||
|
`walonly` (the same without its -shm: the committed rows still count),
|
||||||
`writer` (a live writer whose rows are only in the -wal)."""
|
`writer` (a live writer whose rows are only in the -wal)."""
|
||||||
home, ws = tmp_path / "home", tmp_path / "ws"
|
home, ws = tmp_path / "home", tmp_path / "ws"
|
||||||
ws.mkdir()
|
ws.mkdir()
|
||||||
@@ -150,11 +151,11 @@ def wal_home(tmp_path, monkeypatch):
|
|||||||
def make(state):
|
def make(state):
|
||||||
if state == "clean":
|
if state == "clean":
|
||||||
_wal_writer(db, ws).close()
|
_wal_writer(db, ws).close()
|
||||||
elif state == "leftover":
|
elif state in ("leftover", "walonly"):
|
||||||
src = tmp_path / "src" / "opencode.db"
|
src = tmp_path / "src" / "opencode.db"
|
||||||
src.parent.mkdir()
|
src.parent.mkdir()
|
||||||
conn = _wal_writer(src, ws)
|
conn = _wal_writer(src, ws)
|
||||||
for suffix in ("", "-wal", "-shm"):
|
for suffix in ("", "-wal", "-shm")[:3 if state == "leftover" else 2]:
|
||||||
shutil.copyfile(str(src) + suffix, str(db) + suffix)
|
shutil.copyfile(str(src) + suffix, str(db) + suffix)
|
||||||
conn.close()
|
conn.close()
|
||||||
else:
|
else:
|
||||||
@@ -170,6 +171,7 @@ def wal_home(tmp_path, monkeypatch):
|
|||||||
@pytest.mark.parametrize("state,listing", [
|
@pytest.mark.parametrize("state,listing", [
|
||||||
("clean", ["opencode.db"]),
|
("clean", ["opencode.db"]),
|
||||||
("leftover", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]),
|
("leftover", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]),
|
||||||
|
("walonly", ["opencode.db", "opencode.db-wal"]),
|
||||||
("writer", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]),
|
("writer", ["opencode.db", "opencode.db-shm", "opencode.db-wal"]),
|
||||||
])
|
])
|
||||||
def test_a_query_reads_right_and_leaves_the_sidecars_alone(wal_home, state, listing, query):
|
def test_a_query_reads_right_and_leaves_the_sidecars_alone(wal_home, state, listing, query):
|
||||||
|
|||||||
Reference in new issue
Block a user