Context
PR #24 introduced the Edge model with service-level de-duplication (ADR-0005). The de-dup check in connectNodes uses findFirst to test for a duplicate active Edge, then creates it—but under READ COMMITTED isolation, a concurrent double-submit can slip two identical Edges in the window between the check and the write.
Why deferred
A database partial unique index is the only correct fix:
CREATE UNIQUE INDEX ... ON "Edge"(canvasNodeId, sourceId, targetId) WHERE "deletedAt" IS NULL
This allows soft-delete-then-recreate (a plain @@unique would wrongly block it) and closes the race. However, Prisma cannot express partial unique indexes declaratively—it requires switching from db:push to prisma migrate + raw SQL, a workflow change beyond the current slice.
The realistic trigger is narrow (one owner double-submitting the same Edge within milliseconds) and the blast radius (a duplicate active Edge) is recoverable via soft-delete.
Resolution
- Switch to
prisma migrate workflow.
- Add the composite index:
CREATE UNIQUE INDEX "idx_edge_dedup" ON "Edge"(canvasNodeId, sourceId, targetId) WHERE "deletedAt" IS NULL.
- In
connectNodes, catch Prisma P2002 (unique constraint violation) and map it to ConflictError.
- Update ADR-0005's deferral note to point at this issue.
The non-unique composite index from PR #24 (@@index([canvasNodeId, sourceId, targetId, deletedAt])) is the fast-path precursor for the findFirst query.
Context
PR #24 introduced the Edge model with service-level de-duplication (ADR-0005). The de-dup check in
connectNodesusesfindFirstto test for a duplicate active Edge, then creates it—but under READ COMMITTED isolation, a concurrent double-submit can slip two identical Edges in the window between the check and the write.Why deferred
A database partial unique index is the only correct fix:
This allows soft-delete-then-recreate (a plain
@@uniquewould wrongly block it) and closes the race. However, Prisma cannot express partial unique indexes declaratively—it requires switching fromdb:pushtoprisma migrate+ raw SQL, a workflow change beyond the current slice.The realistic trigger is narrow (one owner double-submitting the same Edge within milliseconds) and the blast radius (a duplicate active Edge) is recoverable via soft-delete.
Resolution
prisma migrateworkflow.CREATE UNIQUE INDEX "idx_edge_dedup" ON "Edge"(canvasNodeId, sourceId, targetId) WHERE "deletedAt" IS NULL.connectNodes, catch PrismaP2002(unique constraint violation) and map it toConflictError.The non-unique composite index from PR #24 (
@@index([canvasNodeId, sourceId, targetId, deletedAt])) is the fast-path precursor for thefindFirstquery.