Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concurrent relation upserts conflict #9751

Closed
dariusj18 opened this issue Oct 13, 2021 · 11 comments
Closed

Concurrent relation upserts conflict #9751

dariusj18 opened this issue Oct 13, 2021 · 11 comments
Assignees
Labels
bug/2-confirmed Bug has been reproduced and confirmed. kind/bug A reported bug. team/client Issue for team Client. tech/engines Issue for tech Engines. topic: query race condition topic: upsert nested upsert
Milestone

Comments

@dariusj18
Copy link

dariusj18 commented Oct 13, 2021

Bug description

Running multiple upserts concurrently and receiving the error:

PrismaClientKnownRequestError3 [PrismaClientKnownRequestError]: 
Invalid `prisma.post.upsert()` invocation:


  Unique constraint failed on the fields: (`name`,`type`,`userId`)
    at cb (C:\PROJECTFOLDER\node_modules\@prisma\client\runtime\index.js:36063:17)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async Promise.all (index 5) {
  code: 'P2002',
  clientVersion: '2.30.2',
  meta: { target: [ 'name', 'type', 'userId' ] }
}

How to reproduce

https://github.com/dariusj18/prisma_playground/tree/prisma_9751

  1. Initialize database
  2. Run npm start

Expected behavior

No errors, upserts happen as expected

Prisma information

datasource db {
  url      = env("DATABASE_URL")
  provider = "postgres"
}

generator client {
  provider = "prisma-client-js"
  // previewFeatures = ["interactiveTransactions", "selectRelationCount", "referentialActions"]
}

model User {
  id       String @id @default(uuid()) @db.Uuid
  username String @unique

  type UserType

  profiles Profile[]
  tags     Tag[]

  publishedPosts Post[]

  @@unique([username, type])
}

enum UserType {
  PUBLISHER
  AUTHOR
  EDITOR
}

model Profile {
  id  String @id @default(uuid()) @db.Uuid
  bio String

  user   User   @relation(fields: [userId], references: [id])
  userId String @db.Uuid
}

model Tag {
  id   String @id @default(uuid()) @db.Uuid
  name String

  type TagType

  user   User   @relation(fields: [userId], references: [id])
  userId String @db.Uuid

  posts PostTag[]

  @@unique([name, type, userId])
}

enum TagType {
  CATEGORY
  POSTTYPE
  LANGUAGE
}

model Post {
  id    String @id @default(uuid()) @db.Uuid
  title String

  user   User   @relation(fields: [userId], references: [id])
  userId String @db.Uuid

  tags PostTag[]
}

model PostTag {
  post   Post   @relation(fields: [postId], references: [id])
  postId String @db.Uuid

  tag   Tag    @relation(fields: [tagId], references: [id])
  tagId String @db.Uuid

  @@id([postId, tagId])
}

Environment & setup

  • OS: Windows
  • Database: PostgreSQL
  • Node.js version: 14.16.0

Prisma Version

prisma                : 2.30.2
@prisma/client        : 2.30.2
Current platform      : windows
Query Engine (Binary) : query-engine b8c35d44de987a9691890b3ddf3e2e7effb9bf20 (at node_modules\@prisma\engines\query-engine-windows.exe)
Migration Engine      : migration-engine-cli b8c35d44de987a9691890b3ddf3e2e7effb9bf20 (at node_modules\@prisma\engines\migration-engine-windows.exe)
Introspection Engine  : introspection-core b8c35d44de987a9691890b3ddf3e2e7effb9bf20 (at node_modules\@prisma\engines\introspection-engine-windows.exe)
Format Binary         : prisma-fmt b8c35d44de987a9691890b3ddf3e2e7effb9bf20 (at node_modules\@prisma\engines\prisma-fmt-windows.exe)
Default Engines Hash  : b8c35d44de987a9691890b3ddf3e2e7effb9bf20
Studio                : 0.422.0
@dariusj18 dariusj18 added the kind/bug A reported bug. label Oct 13, 2021
@dariusj18
Copy link
Author

Debug output

prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query BEGIN
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query INSERT INTO "public"."PostTag" ("postId","tagId") VALUES ($1,$2) RETURNING "public"."PostTag"."postId", "public"."PostTag"."tagId"
prisma:query SELECT "public"."Post"."id", "public"."Post"."title", "public"."Post"."userId" FROM "public"."Post" WHERE "public"."Post"."id" = $1 LIMIT $2 OFFSET $3
prisma:query BEGIN
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query BEGIN
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query INSERT INTO "public"."PostTag" ("postId","tagId") VALUES ($1,$2) RETURNING "public"."PostTag"."postId", "public"."PostTag"."tagId"
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query SELECT "public"."Post"."id", "public"."Post"."title", "public"."Post"."userId" FROM "public"."Post" WHERE "public"."Post"."id" = $1 LIMIT $2 OFFSET $3
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query SELECT "public"."Tag"."id" FROM "public"."Tag" WHERE ("public"."Tag"."name" = $1 AND "public"."Tag"."type" = $2 AND "public"."Tag"."userId" = $3) OFFSET $4
prisma:query COMMIT
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query ROLLBACK
prisma:query BEGIN
prisma:query ROLLBACK
PrismaClientKnownRequestError3 [PrismaClientKnownRequestError]: 
Invalid `prisma.post.upsert()` invocation:


  Unique constraint failed on the fields: (`name`,`type`,`userId`)
    at cb (C:\PROJECTFOLDER\node_modules\@prisma\client\runtime\index.js:36063:17)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at async Promise.all (index 5) {
  code: 'P2002',
  clientVersion: '2.30.2',
  meta: { target: [ 'name', 'type', 'userId' ] }
}
prisma:query ROLLBACK
prisma:query ROLLBACK
prisma:query BEGIN
prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query ROLLBACK
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query BEGIN
prisma:query BEGIN
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query BEGIN
prisma:query INSERT INTO "public"."Tag" ("id","name","type","userId") VALUES ($1,$2,$3,$4) RETURNING "public"."Tag"."id"
prisma:query SELECT "public"."Post"."id" FROM "public"."Post" WHERE "public"."Post"."id" = $1 OFFSET $2
prisma:query INSERT INTO "public"."Post" ("id","title","userId") VALUES ($1,$2,$3) RETURNING "public"."Post"."id"
prisma:query ROLLBACK

@barryhagan
Copy link

Related to #8612.

Concurrency control in prisma is a lie.

@matthewmueller
Copy link
Contributor

Since it's all in one block, you should be able to stick this in a transaction prisma.$transaction([...])

We do have a problem when you're unable to stick the code in a block and the block is concurrent, see #3242 .

@janpio janpio added bug/0-unknown Bug is new, does not have information for reproduction or reproduction could not be confirmed. team/client Issue for team Client. topic: upsert nested upsert labels Nov 19, 2021
@dariusj18
Copy link
Author

So, this must be a fundamental flaw with Prisma and how it works, because I get this error when I run this across multiple requests and the actual upsert is inside a transaction. And the idea that "Hey folks, this is expected behavior" is the response in #3242 is pretty ridiculous.

This was also added as a milestone for 2.19 and seems to have been left behind?

@SevInf
Copy link
Contributor

SevInf commented Aug 31, 2022

@pimeys pimeys self-assigned this Sep 5, 2022
@pimeys pimeys added bug/2-confirmed Bug has been reproduced and confirmed. and removed bug/0-unknown Bug is new, does not have information for reproduction or reproduction could not be confirmed. labels Sep 5, 2022
@pimeys
Copy link
Contributor

pimeys commented Sep 5, 2022

Can still reproduce this with 4.3.1. Logs:

https://gist.github.com/pimeys/f3831809ac388a6c9b6ef8a720d8dce9

@garrensmith
Copy link
Contributor

Prisma Upserts are very powerful and support very nested upserts. This can lead to a Unique constraint error being thrown. We have updated our docs with why this happens and what to do in that situation.
We have also added support for INSERT .. ON CONFLICT .. UPDATE see the docs on when it will be used.

@chientrm
Copy link

chientrm commented Mar 2, 2023

Just wrap the upsert with a $transaction:

[settings] = await prisma.$transaction([
  prisma.settings.upsert({
    where: { sub },
    update: {},
    create: { sub },
  }),
]);

@garhul
Copy link

garhul commented Mar 27, 2023

I'm seeing this issue on transactions as well.

My case scenario is different but not that much, I've got a collection with an unique constraint on a value, but that constraint needs to be case insensitive (this is mongodb btw).
So when I do an upsert I cannot use an insensitive where clause, so I perform a find first and then an upsert, all within a transaction.

The result is duplicates everywhere because prisma doesn't guarantee that other transaction will start execution as my first arriving transaction has started execution, hence data corruption.

I don't believe my case use is so uncommon, what's the recommended course of action to ensure that two commands get executed in an "atomic" way?

@janpio
Copy link
Member

janpio commented Mar 27, 2023

Open a new issue or discussion please instead of commenting on a closed bug report. Thanks.

@chris-activewrite
Copy link

For those who are reporting concurrency issues, it would help if you noted what isolation level you're running in. Obviously that has a big effect on concurrency in general, and that needs to be used when reproducing your problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug/2-confirmed Bug has been reproduced and confirmed. kind/bug A reported bug. team/client Issue for team Client. tech/engines Issue for tech Engines. topic: query race condition topic: upsert nested upsert
Projects
None yet
Development

No branches or pull requests