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:
+148
-60
@@ -266,7 +266,7 @@ def test_rule_delete_unreferenced_succeeds(tmp_path) -> None:
|
||||
def test_rule_delete_referenced_is_rejected(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
await definitions.create("Brush teeth", child.id, rule.id, NOW)
|
||||
await definitions.create("Brush teeth", rule.id, NOW)
|
||||
assert await rules.delete_if_unreferenced(rule.id) is False
|
||||
fetched = await rules.get(rule.id)
|
||||
assert fetched is not None, (
|
||||
@@ -288,7 +288,7 @@ def test_rule_delete_after_last_reference_removed_succeeds(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
definition = await definitions.create(
|
||||
"Brush teeth", child.id, rule.id, NOW
|
||||
"Brush teeth", rule.id, NOW
|
||||
)
|
||||
assert await rules.delete_if_unreferenced(rule.id) is False
|
||||
await definitions.set_active(definition.id, False)
|
||||
@@ -335,12 +335,12 @@ def test_rule_delete_concurrent_with_definition_create_is_safe(
|
||||
import custom_components.nestquest.dao_rules as dao_rules
|
||||
|
||||
original_validate = (
|
||||
dao_rules.QuestDefinitionsDao._validate_assignable
|
||||
dao_rules.QuestDefinitionsDao._validate_rule_exists
|
||||
)
|
||||
release = asyncio.Event()
|
||||
started = asyncio.Event()
|
||||
|
||||
async def _pausing_validate(self, child_id, rule_id):
|
||||
async def _pausing_validate(self, rule_id):
|
||||
# Run the real validation FIRST (this is the
|
||||
# time-of-check), THEN pause: the delete task gets
|
||||
# scheduled while the create sits between its
|
||||
@@ -348,19 +348,17 @@ def test_rule_delete_concurrent_with_definition_create_is_safe(
|
||||
# transaction must keep the delete queued until the
|
||||
# insert commits, so the create must win the race
|
||||
# and the delete must be rejected as referenced.
|
||||
await original_validate(self, child_id, rule_id)
|
||||
await original_validate(self, rule_id)
|
||||
if rule_id == rule_a.id and not started.is_set():
|
||||
started.set()
|
||||
await release.wait()
|
||||
|
||||
dao_rules.QuestDefinitionsDao._validate_assignable = (
|
||||
dao_rules.QuestDefinitionsDao._validate_rule_exists = (
|
||||
_pausing_validate
|
||||
)
|
||||
try:
|
||||
create_task = asyncio.ensure_future(
|
||||
definitions.create(
|
||||
"Brush teeth", child.id, rule_a.id, NOW
|
||||
)
|
||||
definitions.create("Brush teeth", rule_a.id, NOW)
|
||||
)
|
||||
await started.wait()
|
||||
delete_task = asyncio.ensure_future(
|
||||
@@ -382,7 +380,7 @@ def test_rule_delete_concurrent_with_definition_create_is_safe(
|
||||
|
||||
async def _create():
|
||||
return await definitions.create(
|
||||
"Brush teeth", child.id, rule_a.id, NOW
|
||||
"Brush teeth", rule_a.id, NOW
|
||||
)
|
||||
|
||||
delete_task = asyncio.ensure_future(_delete())
|
||||
@@ -434,20 +432,24 @@ def test_definition_create_returns_typed_record(tmp_path) -> None:
|
||||
rule = await rules.create("weekly", "2026-09-14", weekday_set="0,2")
|
||||
definition = await definitions.create(
|
||||
"Brush teeth",
|
||||
child.id,
|
||||
rule.id,
|
||||
NOW,
|
||||
description="Morning and night",
|
||||
icon="mdi:tooth",
|
||||
due_time="08:00",
|
||||
assignee_child_ids=[child.id],
|
||||
)
|
||||
assert isinstance(definition, QuestDefinitionRecord)
|
||||
assert definition.title == "Brush teeth"
|
||||
assert definition.child_id == child.id
|
||||
assert definition.schedule_rule_id == rule.id
|
||||
assert definition.due_time == "08:00"
|
||||
assert definition.is_active is True
|
||||
assert definition.created_at == NOW
|
||||
assert not hasattr(definition, "child_id"), (
|
||||
"D-008: assignment lives in quest_definition_assignees"
|
||||
)
|
||||
assignees = await definitions.list_assignees(definition.id)
|
||||
assert [c.id for c in assignees] == [child.id]
|
||||
return definition
|
||||
|
||||
_with_db(tmp_path, "definition-create.db")(_body)
|
||||
@@ -460,26 +462,30 @@ def test_definition_get_missing_returns_none(tmp_path) -> None:
|
||||
assert _with_db(tmp_path, "definition-get-missing.db")(_body) is None
|
||||
|
||||
|
||||
def test_definition_create_unknown_child_raises_value_error(
|
||||
def test_definition_create_unknown_assignee_raises_value_error(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
await definitions.create("X", 999, rule.id, NOW)
|
||||
await definitions.create(
|
||||
"X", rule.id, NOW, assignee_child_ids=[999]
|
||||
)
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "definition-bad-child.db")(_body)
|
||||
|
||||
|
||||
def test_definition_create_inactive_child_raises_value_error(
|
||||
def test_definition_create_inactive_assignee_raises_value_error(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
await children.set_active(child.id, False)
|
||||
with pytest.raises(ValueError, match="inactive"):
|
||||
await definitions.create("X", child.id, rule.id, NOW)
|
||||
await definitions.create(
|
||||
"X", rule.id, NOW, assignee_child_ids=[child.id]
|
||||
)
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "definition-inactive-child.db")(_body)
|
||||
@@ -490,7 +496,7 @@ def test_definition_create_unknown_rule_raises_value_error(
|
||||
) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
await definitions.create("X", child.id, 999, NOW)
|
||||
await definitions.create("X", 999, NOW)
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "definition-bad-rule.db")(_body)
|
||||
@@ -505,9 +511,11 @@ def test_definition_list_by_child_filters_and_orders(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
other_child = await children.create("Bo", NOW)
|
||||
await definitions.create("A", child.id, rule.id, NOW)
|
||||
await definitions.create("B", child.id, rule.id, NOW)
|
||||
await definitions.create("C", other_child.id, rule.id, NOW)
|
||||
await definitions.create("A", rule.id, NOW, assignee_child_ids=[child.id])
|
||||
await definitions.create("B", rule.id, NOW, assignee_child_ids=[child.id])
|
||||
await definitions.create(
|
||||
"C", rule.id, NOW, assignee_child_ids=[other_child.id]
|
||||
)
|
||||
mine = await definitions.list_by_child(child.id)
|
||||
assert [d.title for d in mine] == ["B", "A"]
|
||||
return mine
|
||||
@@ -527,8 +535,8 @@ def test_definition_list_by_child_missing_child_returns_empty(
|
||||
def test_definition_list_active_excludes_inactive(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
first = await definitions.create("A", child.id, rule.id, NOW)
|
||||
await definitions.create("B", child.id, rule.id, NOW)
|
||||
first = await definitions.create("A", rule.id, NOW)
|
||||
await definitions.create("B", rule.id, NOW)
|
||||
await definitions.set_active(first.id, False)
|
||||
active = await definitions.list_active()
|
||||
assert [d.title for d in active] == ["B"]
|
||||
@@ -547,7 +555,7 @@ def test_definition_update_fields_round_trip(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
definition = await definitions.create(
|
||||
"A", child.id, rule.id, NOW, due_time="08:00"
|
||||
"A", rule.id, NOW, due_time="08:00"
|
||||
)
|
||||
assert await definitions.update(
|
||||
definition.id,
|
||||
@@ -572,9 +580,11 @@ def test_definition_update_cannot_change_assignee_or_active(
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
other = await children.create("Bo", NOW)
|
||||
definition = await definitions.create("A", child.id, rule.id, NOW)
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[child.id]
|
||||
)
|
||||
# title= / child_id= / is_active= are NOT update() parameters;
|
||||
# this asserts the API shape: reassignment and activation have
|
||||
# this asserts the API shape: assignment and activation have
|
||||
# their own explicit methods.
|
||||
import inspect
|
||||
|
||||
@@ -590,7 +600,10 @@ def test_definition_update_cannot_change_assignee_or_active(
|
||||
definition.id, title="A2"
|
||||
) == 1
|
||||
fetched = await definitions.get(definition.id)
|
||||
assert fetched.child_id == child.id
|
||||
assert [c.id for c in await definitions.list_assignees(definition.id)] == [
|
||||
child.id
|
||||
]
|
||||
assert other.id not in [c.id for c in await definitions.list_assignees(definition.id)]
|
||||
assert fetched.is_active is True
|
||||
return fetched
|
||||
|
||||
@@ -600,7 +613,7 @@ def test_definition_update_cannot_change_assignee_or_active(
|
||||
def test_definition_update_no_fields_returns_zero(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
definition = await definitions.create("A", child.id, rule.id, NOW)
|
||||
definition = await definitions.create("A", rule.id, NOW)
|
||||
return await definitions.update(definition.id)
|
||||
|
||||
assert _with_db(tmp_path, "definition-update-empty.db")(_body) == 0
|
||||
@@ -612,7 +625,6 @@ def test_definition_update_can_clear_optional_fields(tmp_path) -> None:
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
definition = await definitions.create(
|
||||
"A",
|
||||
child.id,
|
||||
rule.id,
|
||||
NOW,
|
||||
description="d",
|
||||
@@ -646,7 +658,7 @@ def test_definition_update_missing_returns_zero(tmp_path) -> None:
|
||||
def test_definition_set_active_round_trip(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
definition = await definitions.create("A", child.id, rule.id, NOW)
|
||||
definition = await definitions.create("A", rule.id, NOW)
|
||||
assert await definitions.set_active(definition.id, False) == 1
|
||||
fetched = await definitions.get(definition.id)
|
||||
assert fetched.is_active is False
|
||||
@@ -683,76 +695,152 @@ def test_definition_no_delete_method_exists(tmp_path) -> None:
|
||||
_with_db(tmp_path, "definition-no-delete.db")(_body)
|
||||
|
||||
|
||||
def test_definition_set_assignee_round_trip(tmp_path) -> None:
|
||||
def test_definition_add_assignee_round_trip(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
other = await children.create("Bo", NOW)
|
||||
definition = await definitions.create("A", child.id, rule.id, NOW)
|
||||
assert await definitions.set_assignee(definition.id, other.id) == 1
|
||||
fetched = await definitions.get(definition.id)
|
||||
assert fetched.child_id == other.id
|
||||
return fetched
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[child.id]
|
||||
)
|
||||
await definitions.add_assignee(definition.id, other.id)
|
||||
assignees = await definitions.list_assignees(definition.id)
|
||||
assert sorted(c.id for c in assignees) == sorted([child.id, other.id])
|
||||
# Idempotent: re-adding an existing assignee stays a no-op.
|
||||
await definitions.add_assignee(definition.id, other.id)
|
||||
assignees = await definitions.list_assignees(definition.id)
|
||||
assert sorted(c.id for c in assignees) == sorted([child.id, other.id])
|
||||
return assignees
|
||||
|
||||
_with_db(tmp_path, "definition-set-assignee.db")(_body)
|
||||
_with_db(tmp_path, "definition-add-assignee.db")(_body)
|
||||
|
||||
|
||||
def test_definition_set_assignee_inactive_child_raises(tmp_path) -> None:
|
||||
def test_definition_add_assignee_inactive_child_raises(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
other = await children.create("Bo", NOW)
|
||||
definition = await definitions.create("A", child.id, rule.id, NOW)
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[child.id]
|
||||
)
|
||||
await children.set_active(other.id, False)
|
||||
with pytest.raises(ValueError, match="inactive"):
|
||||
await definitions.set_assignee(definition.id, other.id)
|
||||
fetched = await definitions.get(definition.id)
|
||||
assert fetched.child_id == child.id, (
|
||||
"failed reassignment must leave the current assignee intact"
|
||||
await definitions.add_assignee(definition.id, other.id)
|
||||
assignees = await definitions.list_assignees(definition.id)
|
||||
assert [c.id for c in assignees] == [child.id], (
|
||||
"failed assignment must leave the current assignees intact"
|
||||
)
|
||||
return fetched
|
||||
return assignees
|
||||
|
||||
_with_db(tmp_path, "definition-assignee-inactive.db")(_body)
|
||||
|
||||
|
||||
def test_definition_set_assignee_unknown_child_raises(tmp_path) -> None:
|
||||
def test_definition_add_assignee_unknown_child_raises(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
definition = await definitions.create("A", child.id, rule.id, NOW)
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[child.id]
|
||||
)
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
await definitions.set_assignee(definition.id, 999)
|
||||
await definitions.add_assignee(definition.id, 999)
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "definition-assignee-unknown.db")(_body)
|
||||
|
||||
|
||||
def test_definition_set_assignee_missing_definition_returns_zero(
|
||||
def test_definition_add_assignee_missing_definition_raises(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
with pytest.raises(ValueError, match="does not exist"):
|
||||
await definitions.add_assignee(999, child.id)
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "definition-assignee-missing.db")(_body)
|
||||
|
||||
|
||||
def test_definition_remove_assignee_round_trip(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
other = await children.create("Bo", NOW)
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[child.id, other.id]
|
||||
)
|
||||
assert await definitions.remove_assignee(definition.id, other.id) is True
|
||||
assignees = await definitions.list_assignees(definition.id)
|
||||
assert [c.id for c in assignees] == [child.id]
|
||||
# Removing a non-assignee reports False.
|
||||
assert await definitions.remove_assignee(definition.id, other.id) is False
|
||||
return assignees
|
||||
|
||||
_with_db(tmp_path, "definition-remove-assignee.db")(_body)
|
||||
|
||||
|
||||
def test_definition_list_assignees_orders_by_child_sort_order(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
other = await children.create("Bo", NOW)
|
||||
return await definitions.set_assignee(999, other.id)
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
second = await children.create("Bo", NOW)
|
||||
third = await children.create("Cleo", NOW)
|
||||
await children.reorder([third.id, child.id, second.id])
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[second.id, child.id, third.id]
|
||||
)
|
||||
assignees = await definitions.list_assignees(definition.id)
|
||||
assert [c.id for c in assignees] == [third.id, child.id, second.id]
|
||||
return assignees
|
||||
|
||||
assert _with_db(tmp_path, "definition-assignee-missing.db")(_body) == 0
|
||||
_with_db(tmp_path, "definition-assignees-order.db")(_body)
|
||||
|
||||
|
||||
def test_definition_set_assignee_leaves_history_rows_alone(
|
||||
def test_definition_unassigned_has_no_assignees(tmp_path) -> None:
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
definition = await definitions.create("A", rule.id, NOW)
|
||||
assert await definitions.list_assignees(definition.id) == []
|
||||
return None
|
||||
|
||||
_with_db(tmp_path, "definition-no-assignees.db")(_body)
|
||||
|
||||
|
||||
def test_definition_shared_by_multiple_children_via_list_by_child(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
"""Reassignment writes the definition row only: any instances that
|
||||
already exist keep their generation-time child_id (Feature 06
|
||||
"""D-008: one definition covering several children appears for
|
||||
every assignee through list_by_child, once each."""
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
second = await children.create("Bo", NOW)
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[child.id, second.id]
|
||||
)
|
||||
for assignee in (child, second):
|
||||
mine = await definitions.list_by_child(assignee.id)
|
||||
assert [d.id for d in mine] == [definition.id]
|
||||
return definition
|
||||
|
||||
_with_db(tmp_path, "definition-shared.db")(_body)
|
||||
|
||||
|
||||
def test_definition_assignment_leaves_history_rows_alone(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
"""Assignment edits write the assignees table only: any instances
|
||||
that already exist keep their generation-time child_id (Feature 06
|
||||
guardrail 'changes future instances only').
|
||||
"""
|
||||
async def _body(database, rules, definitions, children, child):
|
||||
rule = await rules.create("daily", "2026-09-14")
|
||||
other = await children.create("Bo", NOW)
|
||||
definition = await definitions.create("A", child.id, rule.id, NOW)
|
||||
definition = await definitions.create(
|
||||
"A", rule.id, NOW, assignee_child_ids=[child.id]
|
||||
)
|
||||
# Simulate an already-generated instance (materialization owns
|
||||
# this table, but its row shape proves reassignment isolation).
|
||||
# this table, but its row shape proves assignment isolation).
|
||||
await database.execute(
|
||||
"INSERT INTO quest_instances (definition_id, child_id, "
|
||||
"due_date, generated_at) VALUES (?, ?, ?, ?)",
|
||||
(definition.id, child.id, "2026-09-15", NOW),
|
||||
)
|
||||
await definitions.set_assignee(definition.id, other.id)
|
||||
await definitions.add_assignee(definition.id, other.id)
|
||||
await definitions.remove_assignee(definition.id, child.id)
|
||||
row = await database.fetch_one(
|
||||
"SELECT child_id FROM quest_instances WHERE definition_id = ?",
|
||||
(definition.id,),
|
||||
@@ -801,7 +889,7 @@ def test_rules_and_definitions_sql_lives_only_in_dao_module() -> None:
|
||||
}
|
||||
sql_pattern = re.compile(
|
||||
r"(FROM|INTO|UPDATE|DELETE\s+FROM|JOIN)\s+[`'\"]*(\[)?"
|
||||
r"(schedule_rules|quest_definitions)\b",
|
||||
r"(schedule_rules|quest_definitions|quest_definition_assignees)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
offenders: list[str] = []
|
||||
@@ -813,7 +901,7 @@ def test_rules_and_definitions_sql_lives_only_in_dao_module() -> None:
|
||||
if sql_pattern.search(py.read_text()):
|
||||
offenders.append(relative)
|
||||
assert offenders == [], (
|
||||
f"SQL touching schedule_rules/quest_definitions leaked into: "
|
||||
f"SQL touching schedule_rules/quest_definitions/assignees leaked: "
|
||||
f"{offenders}"
|
||||
)
|
||||
|
||||
@@ -853,7 +941,7 @@ def test_dao_instances_test_file_uses_dao_not_raw_rules_sql() -> None:
|
||||
)
|
||||
sql_pattern = re.compile(
|
||||
r"(SELECT\s[^\"']*?FROM|INSERT\s+INTO|UPDATE|DELETE\s+FROM|"
|
||||
r"FROM|JOIN)\s+[`'\"]*(\[)?(schedule_rules|quest_definitions)\b",
|
||||
r"FROM|JOIN)\s+[`'\"]*(\[)?(schedule_rules|quest_definitions|quest_definition_assignees)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
assert sql_pattern.search(remaining) is None, (
|
||||
@@ -898,7 +986,7 @@ def test_dao_rules_test_file_uses_dao_not_raw_table_sql() -> None:
|
||||
)
|
||||
sql_pattern = re.compile(
|
||||
r"(SELECT\s[^\"']*?FROM|INSERT\s+INTO|UPDATE|DELETE\s+FROM|"
|
||||
r"FROM|JOIN)\s+[`'\"]*(\[)?(schedule_rules|quest_definitions)\b",
|
||||
r"FROM|JOIN)\s+[`'\"]*(\[)?(schedule_rules|quest_definitions|quest_definition_assignees)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
assert sql_pattern.search(remaining) is None, (
|
||||
|
||||
Reference in New Issue
Block a user