fix(tasks): cascade dependency cleanup on delete (#724) - #796
Conversation
tasks.delete() left the deleted id in other tasks' depends_on and pointed users to a delete_cascade() that never existed. A dependent kept a dangling depends_on entry; readiness needs deps.issubset(completed), so it could never become ready — deleting a task silently and permanently stranded its dependents. delete() now strips the id from every dependent's depends_on in the same transaction as the row delete; docstring reference to the nonexistent delete_cascade() removed and the new behavior documented. Closes #724
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 53 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThe ChangesDelete cascade cleanup
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant TasksDelete as tasks.delete()
participant DB
Caller->>TasksDelete: delete(task_id)
TasksDelete->>DB: query all tasks (id, depends_on)
DB-->>TasksDelete: task list
TasksDelete->>DB: update depends_on/updated_at for dependents
TasksDelete->>DB: delete target task row
DB-->>TasksDelete: deletion result
TasksDelete-->>Caller: True/False
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Code Review — fix(tasks): cascade dependency cleanup on delete (#724)This is a clean, well-scoped fix for a real P0 correctness bug. The change is small, the intent is clear, and the tests cover the right cases. A few observations below. Whats good
Concerns and suggestions1. Cascade runs even when the task does not exist (minor correctness issue) The SELECT + UPDATE loop runs unconditionally before the DELETE. If task_id is not found (DELETE returns rowcount == 0 and returns False), the cascade updates have already been committed. In practice this is harmless — tasks depending on a ghost ID were already broken — but it is semantically wrong to mutate state when the function returns False. Consider checking existence first. 2. Full-workspace table scan This fetches every task in the workspace and decodes every depends_on JSON blob in Python. For workspaces with hundreds of tasks this works fine; for larger ones it is wasteful. A LIKE pre-filter would reduce Python-side decoding while remaining correct. Not a blocker for this PR, but worth a TODO comment if not addressed now. 3. The deleted task itself appears in the SELECT Adding AND id != ? to the SELECT is a no-op correctness-wise but communicates intent and avoids a pointless JSON decode on the row about to be deleted. 4. get_dependents() cross-reference get_dependents() (line 725) returns exactly the tasks that have task_id in their depends_on. You cannot reuse it inside delete() because it opens its own connection (breaking the transaction), but a brief comment pointing to it would help the next reader understand the shape of the data. Test gap worth addingThe tests do not assert that cascade-updated tasks have their updated_at bumped. This is the only signal a cache or UI would use to detect a stale read. SummaryThe bug fix is correct and the core approach is sound. Items 1 (pre-existence check) and 3 (exclude self from scan) are small and worth fixing in this PR. Item 2 (scan efficiency) can be a follow-up. The updated_at test is a nice-to-have. Happy to approve once item 1 is addressed — it is the only case where the current code has an observable semantic difference from what the docstring promises. |
|
Code review follow-up — adding code examples missing from the earlier comment due to shell formatting restrictions. Item 1 (blocking): The cascade SELECT+UPDATE runs before the DELETE, so if task_id is absent the function mutates other tasks then returns False. Fix: add a SELECT 1 guard before the cascade loop and return False early if the row is missing. Item 3 (trivial): Add AND id != ? to the cascade SELECT to skip decoding the row that is about to be deleted. Suggested test: Create A and B where B depends on A, capture b.updated_at before the delete, call tasks.delete(A), then assert the refreshed updated_at is greater than before. This verifies the timestamp is actually bumped on cascade-updated rows. Items 2 and 4 from the main review comment are optional or follow-up. Fix is otherwise correct and well-tested. |
|
Thanks — addressed the review:
|
|
Code Review: fix(tasks): cascade dependency cleanup on delete Summary: Clean, targeted P0 fix. The approach is correct and the test coverage is solid. A few observations below. What's good
Minor observations
Verdict No correctness issues. The atomicity model is sound, the fix matches the bug description, and the tests cover the right cases. The SELECT pre-filter optimization is the only concrete improvement I would suggest, and it is strictly optional given the comment already flags it. Ready to merge once CI is green. |
Code Review — fix(tasks): cascade dependency cleanup on delete (#724)Summary: Clean, targeted P0 fix. The approach is correct and the test coverage is solid. A few observations below. What's good
Minor observations1. SELECT scans all workspace tasks unnecessarily The cascade scan fetches every task in the workspace, including those with empty cursor.execute(
"SELECT id, depends_on FROM tasks WHERE workspace_id = ?",
(workspace.id,),
)The comment notes this, but a trivial pre-filter avoids loading empty-list rows: cursor.execute(
"SELECT id, depends_on FROM tasks "
"WHERE workspace_id = ? AND depends_on != '[]' AND depends_on IS NOT NULL",
(workspace.id,),
)Not critical for current workloads, but the existing 2. No test for multi-hop chains (optional) A->B->C (C depends on B, B depends on A): deleting B should strip B from C's deps but leave A unaffected. The current fix handles this correctly by design (it scans all dependents, not just direct ones), but there's no test that makes this explicit. Worth adding if this edge case matters to you. 3. Comment block length The inline comment is 5 lines — thorough, but longer than the code it guards. The key sentence is the first one; the rest is context that's already in the docstring and PR description. Minor style point only. VerdictNo correctness issues. The atomicity model is sound, the fix matches the bug description, and the tests cover the right cases. The SELECT pre-filter optimization is the only concrete improvement I'd suggest, and it's strictly optional given the comment already flags it. Ready to merge once CI is green. |
What & why
tasks.delete()left the deleted id in every other task'sdepends_on, and its docstring pointed users to adelete_cascade()that doesn't exist anywhere. A dependent kept adepends_onentry referencing the now-missing task; readiness requiresdeps.issubset(completed), so that dependent could never become ready — deleting a task silently and permanently stranded its dependents.Fixes #724 [P0.13].
Change
tasks.delete()now strips the deleted id from every dependent'sdepends_onin the same transaction as the row delete (so no dangling reference is ever left), for all callers (CLI, API, agent). The danglingdelete_cascade()docstring reference is removed and the cascade behavior documented.Tests (
tests/core/test_task_dependencies.py::TestDeleteCascade)depends_on(→[], not stranded).35 passedacross the tasks suites.ruff+mypyclean.Demo
Acceptance criteria
depends_on(default behavior, all callers)delete_cascade()docstring reference is resolved (removed)Summary by CodeRabbit