feat: 0014 schema — embeddings FK CASCADE (deferred or applied) + memories.event_id column (T109)
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")
|
||||
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
|
||||
conn.execute(
|
||||
"INSERT INTO memories ("
|
||||
"owner_id, chat_id, scene_id, pov_summary, "
|
||||
"witness_you, witness_host, witness_guest, "
|
||||
"chat_clock_at, source, reliability, significance, pinned, auto_pinned"
|
||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"chat_clock_at, source, reliability, significance, pinned, auto_pinned, "
|
||||
"event_id"
|
||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
p["owner_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("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
|
||||
for job in captured:
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
apply_migrations(db)
|
||||
with open_db(db) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT value FROM meta WHERE key = 'schema_version'"
|
||||
).fetchone()
|
||||
assert int(row[0]) == 13
|
||||
assert int(row[0]) == 14
|
||||
|
||||
Reference in New Issue
Block a user