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

Implement exists function #5022

Open
greguintow opened this issue Feb 8, 2020 · 32 comments
Open

Implement exists function #5022

greguintow opened this issue Feb 8, 2020 · 32 comments
Labels
kind/feature A request for a new feature. team/client Issue for team Client. topic: client api

Comments

@greguintow
Copy link

greguintow commented Feb 8, 2020

This function have on prisma-binding, it's very helpful, so I thought would be a good idea to insert on the new prisma client.
This functions returns a boolean, you are able to set as many filters (like where from findMany) as you want, if all pass is going to return true, if one fail it's going to return false.

There are some images to help understand on this issue: prisma/prisma-client-js#469.
image

@maticzav
Copy link

maticzav commented Mar 2, 2020

I agree! exists function is very beneficial for implementing permissions where you want to know whether something exists in the backend with specific criteria.

As a current workaround, you may define a utility function $exists, and chain it with findMany.

/**
 * Determines whether something exists.
 * @param ts
 */
export function $exists<T>(ts: T[]): boolean {
  return ts.length > 0
}

const isHost = ctx.prisma.meal
  .findMany({
    where: {
      id,
      host: { id: user.id },
    },
  })
  .then($exists)

@janpio janpio changed the title Implement exists function on prisma client Implement exists function Mar 15, 2020
@greguintow

This comment has been minimized.

@pantharshit00 pantharshit00 transferred this issue from prisma/prisma-client-js Jan 13, 2021
@pantharshit00 pantharshit00 added kind/feature A request for a new feature. team/client Issue for team Client. labels Jan 13, 2021
@cdesch
Copy link

cdesch commented Feb 25, 2021

Is this in the current roadmap?

@SebastienElet
Copy link

You may also perform a count + Boolean cast like

const isHost = await prisma.meal
  .count({
    where: {
      id,
      host: { id: user.id },
    },
  })
  .then(Boolean)

@Threnklyn
Copy link

for many cases a count is not desired since it is more expensive than an exists.
count needs to iterate over every returned value where exists can stop after the first matched row.
Same with a query of all matching rows even if you only need to know there is any.

Compare https://blog.jooq.org/avoid-using-count-in-sql-when-you-could-use-exists/

@tak1n
Copy link

tak1n commented Dec 8, 2021

To get some inspiration how other ORMs deal with this:

Build.where(:created_at => 7.days.ago..1.day.ago).passed.exists?

# SELECT 1 AS one FROM "builds" WHERE ("builds"."created_at" BETWEEN
# '2017-02-22 21:23:04.066301' AND '2017-02-28 21:23:04.066443') AND
# "builds"."result" = $1 LIMIT 1  [["result", "passed"]]

@Yasir5247
Copy link

is this one happening?

@greguintow
Copy link
Author

cc @nikolasburk @pantharshit00

@nikolasburk
Copy link
Member

Hey @greguintow 👋 I'd also love to see this feature but as of now there doesn't seem to be a timeline for it unfortunately since it's not yet placed on the roadmap. In the meantime, the workaround by @SebastienElet is probably the best way to achieve the same outcome, sorry for the inconvenience here 🙏

@Yasir5247
Copy link

@nikolasburk i have several api's which checks existence of a value. for example for each product i would like to see if the user has liked that product or not. again there are so many other use cases. thank you for the response. and i hope this feature will be implemented soon. :)

@jperelli
Copy link

jperelli commented Feb 9, 2022

Avoid using count as much as possible. It's very, very bad for performance. Don't do it, it's not necessary!

Why would you calculate conditions to count through virtually all records when you only need to stop after first match?

Implementation without count is simple, just take: 1 (a further optimization might be selecting only id, if your table has fields with too much data)

const isHost = await prisma.meal
  .findMany({
    where: {
      id,
      host: { id: user.id },
    },
    select: { id: true }, // this line might not be necessary
    take: 1  // this is the important bit
  })
  .then(r => r.length > 0)

Performance in SQL (postgres and mysql) of SELECT id ... LIMIT 1 is practically equal to SELECT ... EXISTS ... (although EXISTS may not do some operations like join if the optimizer finds that is faster)
Count is slower by several orders of magnitude, in tables with millions of records, count can take seconds to return value.

Edit: Alternative implementation, simpler in js by my coworker @gabymorgi, findFirst returns null if not found. You can use the value in a condition as a truthy value if record exists, or cast to boolean for safety.

const isHost = await prisma.meal
  .findFirst({ // using findFirst instead of findMany will not return an array
    where: {
      id,
      host: { id: user.id },
    },
    select: { id: true }, // this line might not be necessary
  })
  .then(r => Boolean(r)) // optional line. Can use truthiness of one record directly (findfirst returns null if not found)

@neongreen
Copy link

neongreen commented Feb 9, 2022

Looks like that's an argument in favor of adding .exists(). The implementation is simpler but at least I can confess that until now I assumed count was as fast as the take solution, and it wouldn't have occurred to me to reach for take.

@sawirricardo
Copy link

I vote with exists, it's easier for newcomers to understand code's readability.

@anton-johansson
Copy link

I'd love this too. Am I correct to think that this existed in a previous version of Prisma? See here:
https://v1.prisma.io/docs/1.34/prisma-client/features/check-existence-JAVASCRIPT-pyl1/

@ko-lem
Copy link

ko-lem commented Oct 19, 2022

@anton-johansson Yes, looks like it. My google search earlier actually landed me in that page making me confused why I can't find prisma.$exists

@dang312
Copy link

dang312 commented Nov 21, 2022

I'd love this too. Am I correct to think that this existed in a previous version of Prisma? See here: https://v1.prisma.io/docs/1.34/prisma-client/features/check-existence-JAVASCRIPT-pyl1/

Landed here with same thought why the exists "existed" on V1 but not current version.
Anyway, does prisma development team plan to implement this in the near time ?

@serg06
Copy link

serg06 commented Jan 2, 2023

I got it working with Client Extensions:

const prisma_ = new PrismaClient();

export const prisma = prisma_
  .$extends({
    model: {
      user: {
        exists(where: Parameters<typeof prisma_.user.findFirst>[0]['where']) {
          return prisma.user.findFirst({
            where,
            select: {
              id: true
            }
          }).then(Boolean);
        }
      },
      // Other models go here
    }
  });

The biggest downside is that I had to repeat it for every single model.

@Lord-Leonard

This comment was marked as abuse.

3 similar comments
@Onihani

This comment was marked as abuse.

@danielb7390

This comment was marked as abuse.

@TiagoCavalcante

This comment was marked as abuse.

@SevInf
Copy link
Contributor

SevInf commented Feb 10, 2023

@serg06 we are a little bit behind on docs with that, but as of latest Prisma version it is possible to implement that universally with $allModels and a couple of helper types:

const prisma = new PrismaClient().$extends({
    model: {
      $allModels: {
        async exists<T>(this: T, where: Prisma.Args<T, 'findFirst'>['where']): Promise<boolean> {
          const context = Prisma.getExtensionContext(this)
          const result = await (context as any).findFirst({ where })
          return result !== null
        },
      },
    },
  })

@jperelli
Copy link

I think this issue can be closed with that last coment

@janpio
Copy link
Member

janpio commented Feb 10, 2023

For now we won't close any of the issues for use cases that are now possible via Client extensions until those have stabilized, and are packaged up and documented.

@larrybek
Copy link

larrybek commented Feb 23, 2023

Exists implementation has a problem when you use transactions.

ctx.prisma.$transaction(prisma => {
  const { id } = await prisma.user.create({});
  const result = await prisma.user.exists({ id });
  console.log(result);
});
prisma:query BEGIN
prisma:query INSERT INTO "public"."Equipment" ("id","name") VALUES ($1,$2) RETURNING "public"."Equipment"."id"
prisma:query SELECT "public"."Equipment"."id", "public"."Equipment"."name" FROM "public"."Equipment" WHERE "public"."Equipment"."id" = $1 LIMIT $2 OFFSET $3
prisma:query SELECT "public"."Equipment"."id", "public"."Equipment"."name" FROM "public"."Equipment" WHERE "public"."Equipment"."id" = $1 LIMIT $2 OFFSET $3
false
prisma:query COMMIT

It creates a new context vs using the existing one. So you cant select the one that was created above.

@MichalLytek
Copy link

Reproduction:

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

generator client {
  provider        = "prisma-client-js"
  output          = "../prisma/generated/client"
  previewFeatures = ["clientExtensions"]
}

model Equipment {
  id   String  @id @default(cuid())
  name String?
}
import { Prisma, PrismaClient } from "./prisma/generated/client";

async function main() {
  const rootPrisma = new PrismaClient({
    log: [
      {
        emit: "event",
        level: "query",
      },
    ],
  }).$extends({
    model: {
      $allModels: {
        async exists<T>(
          this: T,
          where: Prisma.Args<T, "findFirst">["where"],
        ): Promise<boolean> {
          const context = Prisma.getExtensionContext(this);
          const result = await (context as any).findFirst({ where });
          console.log("exists", { result });
          return result !== null;
        },
      },
    },
  });

  rootPrisma.$on("query", e => {
    console.log("Query: " + e.query);
    console.log("Params: " + e.params);
    console.log("Duration: " + e.duration + "ms");
  });

  await rootPrisma.$connect();

  rootPrisma.$transaction(async prisma => {
    const { id } = await prisma.equipment.create({ data: { name: "test" } });
    const findResult = await prisma.equipment.findFirst({ where: { id } });
    const existsResult = await prisma.equipment.exists({ id });
    console.log("transaction", { findResult, existsResult });
  });
}

main().catch(console.error);
Query: BEGIN
Params: []
Duration: 0ms
Query: INSERT INTO "public"."Equipment" ("id","name") VALUES ($1,$2) RETURNING "public"."Equipment"."id"
Params: ["cleh26nmv0001tpt3bm1rafdy","test"]
Duration: 1ms
Query: SELECT "public"."Equipment"."id", "public"."Equipment"."name" FROM "public"."Equipment" WHERE "public"."Equipment"."id" = $1 LIMIT $2 OFFSET $3
Params: ["cleh26nmv0001tpt3bm1rafdy",1,0]
Duration: 0ms
Query: SELECT "public"."Equipment"."id", "public"."Equipment"."name" FROM "public"."Equipment" WHERE "public"."Equipment"."id" = $1 LIMIT $2 OFFSET $3
Params: ["cleh26nmv0001tpt3bm1rafdy",1,0]
Duration: 0ms
Query: SELECT "public"."Equipment"."id", "public"."Equipment"."name" FROM "public"."Equipment" WHERE "public"."Equipment"."id" = $1 LIMIT $2 OFFSET $3
Params: ["cleh26nmv0001tpt3bm1rafdy",1,0]
Duration: 0ms
exists { result: null }
transaction {
  findResult: { id: 'cleh26nmv0001tpt3bm1rafdy', name: 'test' },
  existsResult: false
}
Query: COMMIT
Params: []
Duration: 42ms

@MatsG23
Copy link

MatsG23 commented Jun 21, 2023

@serg06 we are a little bit behind on docs with that, but as of latest Prisma version it is possible to implement that universally with $allModels and a couple of helper types:

const prisma = new PrismaClient().$extends({
    model: {
      $allModels: {
        async exists<T>(this: T, where: Prisma.Args<T, 'findFirst'>['where']): Promise<boolean> {
          const context = Prisma.getExtensionContext(this)
          const result = await (context as any).findFirst({ where })
          return result !== null
        },
      },
    },
  })

Is there a way of accessing the model itself without type-casting to any?

@jakeleventhal
Copy link

jakeleventhal commented Jun 25, 2023

For now we won't close any of the issues for use cases that are now possible via Client extensions until those have stabilized, and are packaged up and documented.

@janpio this is possible with the client extensions, and those are now "stabilized", but to me it seems like an exists function ought to have first class support without extensions

@MatsG23
Copy link

MatsG23 commented Jun 25, 2023

To get some inspiration how other ORMs deal with this:

Build.where(:created_at => 7.days.ago..1.day.ago).passed.exists?

# SELECT 1 AS one FROM "builds" WHERE ("builds"."created_at" BETWEEN
# '2017-02-22 21:23:04.066301' AND '2017-02-28 21:23:04.066443') AND
# "builds"."result" = $1 LIMIT 1  [["result", "passed"]]

Imo, this is very desirable. Implementations of the exists function using Client Extensions are always limited by the existing Prisma functions. We cannot select "nothing" which adds a removable performance overheap to the custom exists function. Something like the quoted approach would be better because it does not select data from the database.

@MitchellMcKenna
Copy link

I feel like exists() should be built into the ORM by default, and not require creating a ClientExtension to add it as a helper method. It is pretty common in other ORMs:

drizzle: https://orm.drizzle.team/docs/operators#exists
kysely: https://kysely.dev/docs/examples/WHERE/complex-where-clause

@janpio
Copy link
Member

janpio commented Sep 4, 2023

Related: #5046

@marceloverdijk
Copy link

marceloverdijk commented Apr 16, 2024

I'm wondering @nikolasburk , is this now on the roadmap? I can honestly not understand why this feature is missing...

Also note when using the $extends as described here the type is not PrismaClient anymore but something like below and make it more difficult to pass it e.g. in a context object.

const prisma: DynamicClientExtensionThis<Prisma.TypeMap<InternalArgs & {
    result: {};
    model: {
    ..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature A request for a new feature. team/client Issue for team Client. topic: client api
Projects
None yet
Development

No branches or pull requests