Reject the semantic inverse of an existing ticket dependency (#51)
addDependency()'s "already exists" check only matched the exact (ticket_id, depends_on_id, dependency_type) tuple. A user could add "A blocks B" from ticket A's page, then separately add "B blocked_by A" from ticket B's page — wouldCreateCycle() correctly found no cycle (both normalize to the same precedence edge), so the insert was allowed, creating two DB rows describing one real relationship (shown twice on ticket B's page: once under Dependencies, once under Dependents). Added an inverse-relationship check before the insert: blocks/ blocked_by are inverses of each other, relates_to is its own inverse (symmetric). duplicates has no defined inverse type in the schema, so both directions remain independently insertable, which is correct — "A duplicates B" and "B duplicates A" are distinct claims. Verified against a local MariaDB instance: the exact repro from the issue (A blocks B, then B blocked_by A) is now rejected, relates_to's symmetric case is rejected in both directions, and duplicates in either direction is unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X
This commit is contained in:
@@ -172,6 +172,27 @@ class DependencyModel
|
||||
}
|
||||
$checkStmt->close();
|
||||
|
||||
// Also check the semantic inverse: "A blocks B" and "B blocked_by A"
|
||||
// describe the same relationship, so adding one from either ticket's
|
||||
// page must be rejected as a duplicate of the other. relates_to is
|
||||
// its own inverse (symmetric); duplicates has no defined inverse type.
|
||||
$inverseTypes = ['blocks' => 'blocked_by', 'blocked_by' => 'blocks', 'relates_to' => 'relates_to'];
|
||||
if (isset($inverseTypes[$type])) {
|
||||
$inverseType = $inverseTypes[$type];
|
||||
$checkInverseSql = "SELECT dependency_id FROM ticket_dependencies
|
||||
WHERE ticket_id = ? AND depends_on_id = ? AND dependency_type = ?";
|
||||
$checkInverseStmt = $this->conn->prepare($checkInverseSql);
|
||||
$checkInverseStmt->bind_param("sss", $dependsOnId, $ticketId, $inverseType);
|
||||
$checkInverseStmt->execute();
|
||||
$inverseResult = $checkInverseStmt->get_result();
|
||||
|
||||
if ($inverseResult->num_rows > 0) {
|
||||
$checkInverseStmt->close();
|
||||
return ['success' => false, 'error' => 'This relationship already exists'];
|
||||
}
|
||||
$checkInverseStmt->close();
|
||||
}
|
||||
|
||||
// Check for circular dependency
|
||||
if ($this->wouldCreateCycle($ticketId, $dependsOnId, $type)) {
|
||||
return ['success' => false, 'error' => 'This would create a circular dependency'];
|
||||
|
||||
Reference in New Issue
Block a user