From 03d0742dc467c98e2cbdc72767059dce6d547064 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Tue, 17 Mar 2026 07:51:26 +0800 Subject: [PATCH] Fix task/project deletion broken by lazy='raise' on cascade relationships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding lazy='raise' to relationships with cascade='all, delete-orphan' broke db.delete() — SQLAlchemy tried to lazy-load related objects for Python-side cascade but lazy='raise' blocked it with MissingGreenlet. Fix: Add passive_deletes=True to subtasks, comments, assignments, tasks, and members relationships. This tells SQLAlchemy to defer cascade to PostgreSQL's ondelete=CASCADE FK constraint instead of loading objects in Python. Both the FK and ORM cascade are now aligned. Also added onError handler to deleteTaskMutation so failures are visible via toast instead of failing silently. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/models/project.py | 4 ++-- backend/app/models/project_task.py | 3 +++ frontend/src/components/projects/ProjectDetail.tsx | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 57e5acc..8fbc169 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -23,6 +23,6 @@ class Project(Base): updated_at: Mapped[datetime] = mapped_column(default=func.now(), onupdate=func.now()) # Relationships — lazy="raise" to prevent N+1 (mirrors CalendarMember pattern) - tasks: Mapped[List["ProjectTask"]] = relationship(back_populates="project", cascade="all, delete-orphan", lazy="raise") + tasks: Mapped[List["ProjectTask"]] = relationship(back_populates="project", cascade="all, delete-orphan", passive_deletes=True, lazy="raise") todos: Mapped[List["Todo"]] = relationship(back_populates="project", lazy="raise") - members: Mapped[List["ProjectMember"]] = relationship(back_populates="project", cascade="all, delete-orphan", lazy="raise") + members: Mapped[List["ProjectMember"]] = relationship(back_populates="project", cascade="all, delete-orphan", passive_deletes=True, lazy="raise") diff --git a/backend/app/models/project_task.py b/backend/app/models/project_task.py index ad25d27..cb950e0 100644 --- a/backend/app/models/project_task.py +++ b/backend/app/models/project_task.py @@ -36,15 +36,18 @@ class ProjectTask(Base): subtasks: Mapped[List["ProjectTask"]] = sa_relationship( back_populates="parent_task", cascade="all, delete-orphan", + passive_deletes=True, lazy="raise", ) comments: Mapped[List["TaskComment"]] = sa_relationship( back_populates="task", cascade="all, delete-orphan", + passive_deletes=True, lazy="raise", ) assignments: Mapped[List["ProjectTaskAssignment"]] = sa_relationship( back_populates="task", cascade="all, delete-orphan", + passive_deletes=True, lazy="raise", ) diff --git a/frontend/src/components/projects/ProjectDetail.tsx b/frontend/src/components/projects/ProjectDetail.tsx index 6c9dcb3..89d7022 100644 --- a/frontend/src/components/projects/ProjectDetail.tsx +++ b/frontend/src/components/projects/ProjectDetail.tsx @@ -201,6 +201,9 @@ export default function ProjectDetail() { toast.success('Task deleted'); setSelectedTaskId(null); }, + onError: () => { + toast.error('Failed to delete task'); + }, }); const deleteProjectMutation = useMutation({