diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 4d97b685e..5d71fe78f 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -1689,9 +1689,10 @@ export abstract class BaseCrudDialect { // every query issued through the ORM builds the dialect from a client, and a dialect // built from a standalone schema/options pair never inlines computed fields invariant(this.client, `computed field "${field}" of model "${model}" needs a client to be evaluated`); - // `computedArgs` is the query-time args object for a parameterized computed - // field (undefined otherwise); forwarded as the implementation's 3rd argument. - return computer(this.eb, { modelAlias, client: this.client }, computedArgs); + // `computedArgs` is the query-time args of a parameterized computed field (undefined + // otherwise), forwarded as the implementation's 3rd argument. The result is parenthesized + // as it gets embedded into larger expressions: `where: { isMine: true }` → `() = $n`. + return this.eb.parens(computer(this.eb, { modelAlias, client: this.client }, computedArgs)); } } diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index 2be62ad94..01f74c662 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -1002,9 +1002,8 @@ model Post { { computedFields: { Post: { - // parenthesized: the expression gets embedded into larger ones - // (e.g. `where: { isMine: true }` wraps it with `= true`), and an - // unparenthesized chained comparison is a syntax error on postgres + // the dialect parenthesizes an inlined implementation itself; the explicit + // `eb.parens` here guards that an already-parenthesized one isn't wrapped twice isMine: (eb: any, { client }: any) => eb.parens(eb('authorId', '=', client.$auth?.id ?? -1)), }, }, @@ -1026,4 +1025,46 @@ model Post { // the original client is unaffected await expect(db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ isMine: false }); }); + + it('contains the precedence of an inlined computed field expression', async () => { + const db = await createTestClient( + ` +model Post { + id Int @id @default(autoincrement()) + authorId Int + isMine Boolean @computed + isSpecial Boolean @computed +} +`, + { + computedFields: { + Post: { + // top-level node is a binary operation, which is embedded into + // ` = $n` when the field is used as a boolean filter + isMine: (eb: any) => eb('authorId', '=', 1), + // top-level node is a logical combinator + isSpecial: (eb: any) => eb.or([eb('authorId', '=', 1), eb('id', '=', 2)]), + }, + }, + } as any, + ); + + await db.post.create({ data: { id: 1, authorId: 1 } }); + await db.post.create({ data: { id: 2, authorId: 2 } }); + await db.post.create({ data: { id: 3, authorId: 3 } }); + + const findIds = async (where: any) => + (await db.post.findMany({ where, orderBy: { id: 'asc' } })).map((r: any) => r.id); + + expect(await findIds({ isMine: true })).toEqual([1]); + expect(await findIds({ isMine: false })).toEqual([2, 3]); + expect(await findIds({ NOT: { isMine: true } })).toEqual([2, 3]); + expect(await findIds({ isSpecial: true })).toEqual([1, 2]); + + // reading the fields is unaffected + await expect(db.post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ + isMine: true, + isSpecial: true, + }); + }); });