Fix task/project deletion broken by lazy='raise' on cascade relationships

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) <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-17 07:51:26 +08:00
parent bb4212d17f
commit 03d0742dc4
3 changed files with 8 additions and 2 deletions

View File

@ -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")

View File

@ -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",
)

View File

@ -201,6 +201,9 @@ export default function ProjectDetail() {
toast.success('Task deleted');
setSelectedTaskId(null);
},
onError: () => {
toast.error('Failed to delete task');
},
});
const deleteProjectMutation = useMutation({