Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit 692ec87

Browse files
a-dealclaude
andcommitted
Supplement and medication logging: add SQLite tables + dual-write
Closes the consistency gap where log_supplements and log_medication only wrote to CSV. Now dual-writes to SQLite (supplement_log, medication_log tables) matching the pattern used by meals, weight, BP. 9 new tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7ddd55a commit 692ec87

3 files changed

Lines changed: 227 additions & 0 deletions

File tree

engine/gateway/db.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,31 @@ def close_db():
316316
updated_at TEXT NOT NULL
317317
);
318318
319+
CREATE TABLE IF NOT EXISTS supplement_log (
320+
id TEXT PRIMARY KEY,
321+
person_id TEXT NOT NULL REFERENCES person(id),
322+
date TEXT NOT NULL,
323+
name TEXT NOT NULL,
324+
dose TEXT,
325+
stack TEXT,
326+
source TEXT,
327+
created_at TEXT NOT NULL,
328+
updated_at TEXT NOT NULL
329+
);
330+
331+
CREATE TABLE IF NOT EXISTS medication_log (
332+
id TEXT PRIMARY KEY,
333+
person_id TEXT NOT NULL REFERENCES person(id),
334+
date TEXT NOT NULL,
335+
name TEXT NOT NULL,
336+
dose TEXT NOT NULL,
337+
route TEXT,
338+
notes TEXT,
339+
source TEXT,
340+
created_at TEXT NOT NULL,
341+
updated_at TEXT NOT NULL
342+
);
343+
319344
CREATE TABLE IF NOT EXISTS wearable_token (
320345
id TEXT PRIMARY KEY,
321346
person_id TEXT REFERENCES person(id),

mcp_server/tools.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,23 @@ def _log_supplements(stack: str | None = None, supplements: list[str] | None = N
759759
})
760760
write_csv(path, rows, fieldnames=fieldnames)
761761

762+
# SQLite write
763+
import uuid as _uuid
764+
person_id = _resolve_person_id(user_id)
765+
if person_id:
766+
from engine.gateway.db import get_db, init_db
767+
init_db()
768+
db = get_db()
769+
now = datetime.now().isoformat()
770+
for item in items:
771+
rid = str(_uuid.uuid5(_uuid.NAMESPACE_URL, f"{person_id}:supplement:{date}:{item['name']}"))
772+
db.execute(
773+
"INSERT OR IGNORE INTO supplement_log (id, person_id, date, name, dose, stack, source, created_at, updated_at) "
774+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
775+
(rid, person_id, date, item["name"], item.get("dose", ""), stack or "individual", "mcp", now, now),
776+
)
777+
db.commit()
778+
762779
logged_names = [i["name"] for i in items]
763780
return {"logged": True, "date": date, "count": len(items), "supplements": logged_names}
764781

@@ -1292,6 +1309,23 @@ def _log_medication(
12921309
"source": "mcp",
12931310
})
12941311
write_csv(path, rows, fieldnames=fieldnames)
1312+
1313+
# SQLite write
1314+
import uuid as _uuid
1315+
person_id = _resolve_person_id(user_id)
1316+
if person_id:
1317+
from engine.gateway.db import get_db, init_db
1318+
init_db()
1319+
db = get_db()
1320+
now = datetime.now().isoformat()
1321+
rid = str(_uuid.uuid5(_uuid.NAMESPACE_URL, f"{person_id}:medication:{date}:{name}"))
1322+
db.execute(
1323+
"INSERT OR IGNORE INTO medication_log (id, person_id, date, name, dose, route, notes, source, created_at, updated_at) "
1324+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1325+
(rid, person_id, date, name, dose, route or "", notes or "", "mcp", now, now),
1326+
)
1327+
db.commit()
1328+
12951329
return {"logged": True, "date": date, "name": name, "dose": dose, "route": route or ""}
12961330

12971331

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""Tests for supplement and medication SQLite logging.
2+
3+
Covers: SQLite table creation, dual-write from MCP tools, query via SQL.
4+
"""
5+
6+
import uuid
7+
from datetime import datetime, timezone
8+
9+
import pytest
10+
11+
from engine.gateway.db import get_db, init_db
12+
13+
14+
@pytest.fixture
15+
def db(tmp_path, monkeypatch):
16+
"""Fresh SQLite database with schema applied."""
17+
monkeypatch.setattr("mcp_server.tools.PROJECT_ROOT", tmp_path)
18+
(tmp_path / "data").mkdir(exist_ok=True)
19+
(tmp_path / "data" / "users" / "andrew").mkdir(parents=True, exist_ok=True)
20+
db_path = tmp_path / "data" / "kasane.db"
21+
init_db(str(db_path))
22+
conn = get_db(str(db_path))
23+
24+
# Insert a person for testing
25+
now = datetime.now(timezone.utc).isoformat()
26+
conn.execute(
27+
"INSERT INTO person (id, name, health_engine_user_id, created_at, updated_at) "
28+
"VALUES (?, ?, ?, ?, ?)",
29+
("p1", "Andrew", "andrew", now, now),
30+
)
31+
conn.commit()
32+
return conn
33+
34+
35+
class TestSupplementTable:
36+
def test_supplement_log_table_exists(self, db):
37+
"""Schema should create supplement_log table."""
38+
row = db.execute(
39+
"SELECT name FROM sqlite_master WHERE type='table' AND name='supplement_log'"
40+
).fetchone()
41+
assert row is not None, "supplement_log table missing from schema"
42+
43+
def test_insert_supplement(self, db):
44+
"""Can insert a supplement log entry."""
45+
now = datetime.now(timezone.utc).isoformat()
46+
rid = str(uuid.uuid4())
47+
db.execute(
48+
"INSERT INTO supplement_log (id, person_id, date, name, dose, stack, source, created_at, updated_at) "
49+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
50+
(rid, "p1", "2026-04-04", "vitamin_d", "5000 IU", "morning", "mcp", now, now),
51+
)
52+
db.commit()
53+
54+
row = db.execute("SELECT * FROM supplement_log WHERE id = ?", (rid,)).fetchone()
55+
assert row is not None
56+
assert row["name"] == "vitamin_d"
57+
assert row["dose"] == "5000 IU"
58+
assert row["stack"] == "morning"
59+
60+
def test_query_supplements_by_date(self, db):
61+
"""Can query supplements for a specific date."""
62+
now = datetime.now(timezone.utc).isoformat()
63+
for name, dose in [("vitamin_d", "5000 IU"), ("fish_oil", "2 capsules")]:
64+
db.execute(
65+
"INSERT INTO supplement_log (id, person_id, date, name, dose, stack, source, created_at, updated_at) "
66+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
67+
(str(uuid.uuid4()), "p1", "2026-04-04", name, dose, "morning", "mcp", now, now),
68+
)
69+
db.commit()
70+
71+
rows = db.execute(
72+
"SELECT name, dose FROM supplement_log WHERE person_id = ? AND date = ?",
73+
("p1", "2026-04-04"),
74+
).fetchall()
75+
assert len(rows) == 2
76+
names = {r["name"] for r in rows}
77+
assert names == {"vitamin_d", "fish_oil"}
78+
79+
80+
class TestMedicationTable:
81+
def test_medication_log_table_exists(self, db):
82+
"""Schema should create medication_log table."""
83+
row = db.execute(
84+
"SELECT name FROM sqlite_master WHERE type='table' AND name='medication_log'"
85+
).fetchone()
86+
assert row is not None, "medication_log table missing from schema"
87+
88+
def test_insert_medication(self, db):
89+
"""Can insert a medication log entry."""
90+
now = datetime.now(timezone.utc).isoformat()
91+
rid = str(uuid.uuid4())
92+
db.execute(
93+
"INSERT INTO medication_log (id, person_id, date, name, dose, route, notes, source, created_at, updated_at) "
94+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
95+
(rid, "p1", "2026-04-04", "tirzepatide", "2.5mg", "subcutaneous", "injection site: abdomen", "mcp", now, now),
96+
)
97+
db.commit()
98+
99+
row = db.execute("SELECT * FROM medication_log WHERE id = ?", (rid,)).fetchone()
100+
assert row is not None
101+
assert row["name"] == "tirzepatide"
102+
assert row["dose"] == "2.5mg"
103+
assert row["route"] == "subcutaneous"
104+
105+
def test_query_medications_by_person(self, db):
106+
"""Can query medication history for a person."""
107+
now = datetime.now(timezone.utc).isoformat()
108+
for date in ["2026-04-01", "2026-04-02", "2026-04-03"]:
109+
db.execute(
110+
"INSERT INTO medication_log (id, person_id, date, name, dose, route, notes, source, created_at, updated_at) "
111+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
112+
(str(uuid.uuid4()), "p1", date, "tirzepatide", "2.5mg", "subcutaneous", "", "mcp", now, now),
113+
)
114+
db.commit()
115+
116+
rows = db.execute(
117+
"SELECT date FROM medication_log WHERE person_id = ? ORDER BY date",
118+
("p1",),
119+
).fetchall()
120+
assert len(rows) == 3
121+
122+
123+
class TestToolDualWrite:
124+
"""Verify the MCP tools write to both CSV and SQLite."""
125+
126+
def test_log_supplements_writes_sqlite(self, db, tmp_path, monkeypatch):
127+
"""log_supplements should write to SQLite when person exists."""
128+
monkeypatch.setattr("mcp_server.tools.PROJECT_ROOT", tmp_path)
129+
130+
from mcp_server.tools import _log_supplements
131+
result = _log_supplements(stack="morning", user_id="andrew")
132+
assert result["logged"] is True
133+
134+
rows = db.execute(
135+
"SELECT name, dose, stack FROM supplement_log WHERE person_id = 'p1'"
136+
).fetchall()
137+
assert len(rows) > 0
138+
names = {r["name"] for r in rows}
139+
assert "vitamin_d" in names
140+
141+
def test_log_medication_writes_sqlite(self, db, tmp_path, monkeypatch):
142+
"""log_medication should write to SQLite when person exists."""
143+
monkeypatch.setattr("mcp_server.tools.PROJECT_ROOT", tmp_path)
144+
145+
from mcp_server.tools import _log_medication
146+
result = _log_medication(name="tirzepatide", dose="2.5mg", route="subcutaneous", user_id="andrew")
147+
assert result["logged"] is True
148+
149+
row = db.execute(
150+
"SELECT name, dose, route FROM medication_log WHERE person_id = 'p1'"
151+
).fetchone()
152+
assert row is not None
153+
assert row["name"] == "tirzepatide"
154+
assert row["dose"] == "2.5mg"
155+
assert row["route"] == "subcutaneous"
156+
157+
def test_log_supplements_no_person_still_works(self, db, tmp_path, monkeypatch):
158+
"""log_supplements should still work (CSV only) when no person record exists."""
159+
monkeypatch.setattr("mcp_server.tools.PROJECT_ROOT", tmp_path)
160+
(tmp_path / "data" / "users" / "unknown").mkdir(parents=True, exist_ok=True)
161+
162+
from mcp_server.tools import _log_supplements
163+
result = _log_supplements(stack="morning", user_id="unknown")
164+
assert result["logged"] is True
165+
166+
# No SQLite row since person doesn't exist
167+
rows = db.execute("SELECT * FROM supplement_log").fetchall()
168+
assert len(rows) == 0

0 commit comments

Comments
 (0)