Multi-assignee definitions via quest_definition_assignees (D-008)
Per IMPLEMENTATION-ORDER.md 1.3: a quest_definition_assignees table (definition_id, child_id) with a composite primary key and FKs to quest_definitions and children replaces the single child_id column on quest_definitions. - schema.py: definitions DDL loses child_id; new assignees table joins SCHEMA_V1_STATEMENTS; the definitions table body is extracted to QUEST_DEFINITIONS_TABLE_SQL so the rebuild migration cannot drift from the canonical shape. - migrations.py: callable migrations now own their transaction and stamp via stamp_schema_version(database, version), because the rebuild must bracket itself with PRAGMA foreign_keys OFF/ON. New migration 3 creates the assignees table and, when a pre-D-008 quest_definitions still carries child_id, rebuilds the table via SQLite's canonical procedure (create, copy, move assignees, drop, rename), verifies PRAGMA foreign_key_check inside the transaction, and stamps. Migration 2 converted to the new callable contract. - dao_rules.py: QuestDefinitionsDao.create drops the child_id argument and applies assignee_child_ids atomically; set_assignee is replaced by add_assignee (idempotent, active-child validated under the connection lock), remove_assignee and list_assignees; list_by_child reads through the assignees table. - dao_instances.py: upsert validates the child against quest_definition_assignees instead of the definitions column. - tests: schema shape tests for the assignees table (columns, PK, FKs, duplicate/unknown constraints); DAO tests rewritten for the new assignment API; migration tests for the v2 -> v3 rebuild (rows and instances survive, child_id gone, foreign_key_check clean).
This commit is contained in:
+15
-14
@@ -82,7 +82,7 @@ async def _prepare(path) -> tuple:
|
||||
child = await children.create("Ada", _now_stamp())
|
||||
rule = await rules.create("daily", D1)
|
||||
definition = await definitions.create(
|
||||
"Brush teeth", child.id, rule.id, _now_stamp()
|
||||
"Brush teeth", rule.id, _now_stamp(), assignee_child_ids=[child.id]
|
||||
)
|
||||
return (
|
||||
database,
|
||||
@@ -184,17 +184,15 @@ def test_upsert_rejects_unknown_definition_and_child(tmp_path) -> None:
|
||||
with pytest.raises(ValueError, match="definition 999"):
|
||||
await instances.upsert(999, child.id, D1, _now_stamp())
|
||||
# Unknown child: the definition-assignee check fires first and
|
||||
# is the one that matters (an instance must carry its
|
||||
# definition's assignee). To reach the child-existence check
|
||||
# at all, the definition would have to claim child 999 — an
|
||||
# FK-impossible state — so the child check only guards direct
|
||||
# callers with a matching fake assignee; assert the guard
|
||||
# order instead of forcing the FK-impossible state.
|
||||
# is the one that matters (an instance must carry an assigned
|
||||
# child). Under D-008 the assignee check is a membership test
|
||||
# against quest_definition_assignees, so child 999 simply is
|
||||
# not an assignee — the error names the pair.
|
||||
rule = await rules.create("daily", D1)
|
||||
other_definition = await definitions.create(
|
||||
"X", child.id, rule.id, _now_stamp()
|
||||
"X", rule.id, _now_stamp(), assignee_child_ids=[child.id]
|
||||
)
|
||||
with pytest.raises(ValueError, match="not 999"):
|
||||
with pytest.raises(ValueError, match="not assigned to child 999"):
|
||||
await instances.upsert(other_definition.id, 999, D1, _now_stamp())
|
||||
return None
|
||||
|
||||
@@ -329,7 +327,10 @@ def test_upsert_no_past_check_uses_execution_date_not_call_date(
|
||||
child = await children.create("Ada", _now_stamp())
|
||||
rule = await rules.create("daily", D1)
|
||||
definition = await definitions.create(
|
||||
"Brush teeth", child.id, rule.id, _now_stamp()
|
||||
"Brush teeth",
|
||||
rule.id,
|
||||
_now_stamp(),
|
||||
assignee_child_ids=[child.id],
|
||||
)
|
||||
return await _body(
|
||||
database, children, rules, definitions, instances,
|
||||
@@ -406,7 +407,7 @@ def test_list_by_child_and_date_filters(tmp_path) -> None:
|
||||
other = await children.create("Bo", _now_stamp())
|
||||
rule = await rules.create("daily", D1)
|
||||
other_definition = await definitions.create(
|
||||
"Make bed", other.id, rule.id, _now_stamp()
|
||||
"Make bed", rule.id, _now_stamp(), assignee_child_ids=[other.id]
|
||||
)
|
||||
await instances.upsert(definition.id, child.id, D1, _now_stamp())
|
||||
await instances.upsert(definition.id, child.id, D2, _now_stamp())
|
||||
@@ -537,8 +538,8 @@ def test_delete_future_uncompleted_other_definitions_untouched(
|
||||
async def _body(database, children, rules, definitions, instances,
|
||||
events, child, definition):
|
||||
other = await definitions.create(
|
||||
"Make bed", child.id, (await rules.create("daily", D1)).id,
|
||||
_now_stamp(),
|
||||
"Make bed", (await rules.create("daily", D1)).id,
|
||||
_now_stamp(), assignee_child_ids=[child.id],
|
||||
)
|
||||
await instances.upsert(definition.id, child.id, D20, _now_stamp())
|
||||
await instances.upsert(other.id, child.id, D20, _now_stamp())
|
||||
@@ -1090,7 +1091,7 @@ def test_append_concurrent_events_serialize(tmp_path) -> None:
|
||||
child = await children.create("Ada", _now_stamp())
|
||||
rule = await rules.create("daily", D1)
|
||||
definition = await definitions.create(
|
||||
"Brush teeth", child.id, rule.id, _now_stamp()
|
||||
"Brush teeth", rule.id, _now_stamp(), assignee_child_ids=[child.id]
|
||||
)
|
||||
return (database, children, rules, definitions, instances,
|
||||
events, child, definition)
|
||||
|
||||
Reference in New Issue
Block a user