merge: T109 schema 0014 — memories.event_id column
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
-- 0014_phase45_schema.sql — Phase 4.5 Wave 2 schema bump (T109).
|
||||||
|
--
|
||||||
|
-- Two schema concerns are bundled into this migration:
|
||||||
|
--
|
||||||
|
-- 1. ``embeddings.memory_id`` FK should ideally carry ``ON DELETE CASCADE``
|
||||||
|
-- (T88 review nit). DEFERRED to Phase 5: ``embeddings`` rows are only ever
|
||||||
|
-- deleted when the parent ``memories`` row is deleted, and ``memories``
|
||||||
|
-- rows are never deleted today (memory hide is a soft flag; the surgical
|
||||||
|
-- ``deindex_event`` path operates on ``event_log`` and does NOT cascade
|
||||||
|
-- to projection rows). The CASCADE constraint therefore can't fire under
|
||||||
|
-- current usage — adding the SQLite table-rebuild dance (rename, recreate,
|
||||||
|
-- copy, drop, reindex) for a defensive constraint is unwarranted bloat
|
||||||
|
-- in a polish wave. Revisit during the broader Phase 5 migration cleanup
|
||||||
|
-- when other table reshapes make the rebuild worthwhile.
|
||||||
|
--
|
||||||
|
-- 2. Add ``memories.event_id`` (NULLABLE INTEGER, references ``event_log.id``)
|
||||||
|
-- so cross-chat search results can deep-link back to the originating
|
||||||
|
-- turn (foundation for T111). The column is nullable so historical
|
||||||
|
-- memory rows projected before 0014 ran continue to round-trip cleanly;
|
||||||
|
-- new rows are populated by the ``memory_written`` projector handler
|
||||||
|
-- from the projecting event's id. This is a pure additive change — no
|
||||||
|
-- backfill is performed. Older rows simply read NULL until/unless a
|
||||||
|
-- later migration backfills them; T111 surfaces are coded to accept
|
||||||
|
-- NULL gracefully (no deep-link rendered).
|
||||||
|
ALTER TABLE memories ADD COLUMN event_id INTEGER REFERENCES event_log(id);
|
||||||
@@ -13,13 +13,18 @@ def _row_to_dict(conn: Connection, row: tuple) -> dict:
|
|||||||
|
|
||||||
@on("memory_written")
|
@on("memory_written")
|
||||||
def _apply_memory_written(conn: Connection, e: Event) -> None:
|
def _apply_memory_written(conn: Connection, e: Event) -> None:
|
||||||
|
# T109 (schema 0014): persist the projecting event's id on the memory
|
||||||
|
# row so cross-chat search results can deep-link back to the
|
||||||
|
# originating turn (T111). Older memory rows projected before 0014
|
||||||
|
# ran read NULL here — the column is nullable for that reason.
|
||||||
p = e.payload
|
p = e.payload
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO memories ("
|
"INSERT INTO memories ("
|
||||||
"owner_id, chat_id, scene_id, pov_summary, "
|
"owner_id, chat_id, scene_id, pov_summary, "
|
||||||
"witness_you, witness_host, witness_guest, "
|
"witness_you, witness_host, witness_guest, "
|
||||||
"chat_clock_at, source, reliability, significance, pinned, auto_pinned"
|
"chat_clock_at, source, reliability, significance, pinned, auto_pinned, "
|
||||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
"event_id"
|
||||||
|
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
(
|
(
|
||||||
p["owner_id"],
|
p["owner_id"],
|
||||||
p["chat_id"],
|
p["chat_id"],
|
||||||
@@ -34,6 +39,7 @@ def _apply_memory_written(conn: Connection, e: Event) -> None:
|
|||||||
int(p.get("significance", 1)),
|
int(p.get("significance", 1)),
|
||||||
int(p.get("pinned", 0)),
|
int(p.get("pinned", 0)),
|
||||||
int(p.get("auto_pinned", 0)),
|
int(p.get("auto_pinned", 0)),
|
||||||
|
e.id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -586,3 +586,59 @@ def test_record_turn_memory_enqueues_embedding_job(tmp_path):
|
|||||||
assert {job.memory_id for job in captured} == expected_ids
|
assert {job.memory_id for job in captured} == expected_ids
|
||||||
for job in captured:
|
for job in captured:
|
||||||
assert job.text == "Both bots witness this beat."
|
assert job.text == "Both bots witness this beat."
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# T109: memories.event_id deep-link column populated by the projector.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_written_populates_event_id(tmp_path):
|
||||||
|
"""Schema 0014 added ``memories.event_id`` referencing ``event_log.id``.
|
||||||
|
|
||||||
|
The ``memory_written`` projector handler must populate the column with
|
||||||
|
the projecting event's id so T111 can deep-link cross-chat search hits
|
||||||
|
back to the originating turn.
|
||||||
|
"""
|
||||||
|
db = tmp_path / "t.db"
|
||||||
|
apply_migrations(db)
|
||||||
|
_seed_minimal(db)
|
||||||
|
with open_db(db) as conn:
|
||||||
|
result = record_turn_memory_for_present(
|
||||||
|
conn,
|
||||||
|
chat_id="chat_bot_a",
|
||||||
|
host_bot_id="bot_a",
|
||||||
|
guest_bot_id=None,
|
||||||
|
narrative_text="BotA shrugs.",
|
||||||
|
)
|
||||||
|
eid, mid = result["bot_a"]
|
||||||
|
assert eid > 0 and mid is not None
|
||||||
|
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT event_id FROM memories WHERE id = ?", (mid,)
|
||||||
|
).fetchone()
|
||||||
|
assert row is not None
|
||||||
|
assert row[0] == eid
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_event_id_column_is_nullable_for_backfill(tmp_path):
|
||||||
|
"""Backward compat: the ``event_id`` column is nullable so historical
|
||||||
|
memory rows projected before 0014 ran (or rows synthesised by tests
|
||||||
|
that bypass the projector) don't break the schema. A direct INSERT
|
||||||
|
omitting the column must succeed and read back NULL."""
|
||||||
|
db = tmp_path / "t.db"
|
||||||
|
apply_migrations(db)
|
||||||
|
_seed_minimal(db)
|
||||||
|
with open_db(db) as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO memories ("
|
||||||
|
"owner_id, chat_id, pov_summary, "
|
||||||
|
"witness_you, witness_host, witness_guest"
|
||||||
|
") VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
("bot_a", "chat_bot_a", "legacy row", 1, 1, 0),
|
||||||
|
)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT event_id FROM memories WHERE pov_summary = 'legacy row'"
|
||||||
|
).fetchone()
|
||||||
|
assert row is not None
|
||||||
|
assert row[0] is None
|
||||||
|
|||||||
+2
-2
@@ -324,11 +324,11 @@ def test_get_scene_returns_none_for_missing(tmp_path):
|
|||||||
assert active_scene(conn, "chat_missing") is None
|
assert active_scene(conn, "chat_missing") is None
|
||||||
|
|
||||||
|
|
||||||
def test_schema_version_after_migration_is_13(tmp_path):
|
def test_schema_version_after_migration_is_14(tmp_path):
|
||||||
db = tmp_path / "t.db"
|
db = tmp_path / "t.db"
|
||||||
apply_migrations(db)
|
apply_migrations(db)
|
||||||
with open_db(db) as conn:
|
with open_db(db) as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT value FROM meta WHERE key = 'schema_version'"
|
"SELECT value FROM meta WHERE key = 'schema_version'"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
assert int(row[0]) == 13
|
assert int(row[0]) == 14
|
||||||
|
|||||||
Reference in New Issue
Block a user