Skip to content

Computed field expression is not parenthesized when inlined, breaking boolean filters on Postgres #2795

Description

@evgenovalov

Description

When a computed field is inlined into a larger SQL expression, the implementation's expression is embedded as-is, without parentheses. If the implementation is a bare binary comparison (e.g. eb('authorId', '=', 1)), a boolean filter on that field compiles to a chained comparison — "authorId" = $2 = $3 — which Postgres rejects as a syntax error.

Filing this separately as suggested in #2789 (comment) — it surfaced on the Postgres CI matrix of that PR, but it is independent of it and reproduces with a plain computed field that doesn't use anything from that PR.

Reproduction

model Post {
    id Int @id @default(autoincrement())
    authorId Int
    isMine Boolean @computed
}
const db = new ZenStackClient(schema, {
    // ...postgres dialect
    computedFields: {
        post: {
            isMine: (eb) => eb('authorId', '=', 1),
        },
    },
});

await db.post.findMany();                         // ok
await db.post.findMany({ where: { isMine: true } }); // syntax error

Actual behavior

error: syntax error at or near "="

The compiled SQL (note the projection is fine, the where is not):

select "Post"."id" as "id", "Post"."authorId" as "authorId", "authorId" = $1 as "isMine"
from "public"."Post"
where "authorId" = $2 = $3

where: { NOT: { isMine: true } } fails the same way.

Per-provider behavior

Same schema, same implementations, findMany over rows id/authorId = 1/1, 2/2, 3/3:

query postgresql sqlite
where: { isMine: true } syntax error [1]
where: { NOT: { isMine: true } } syntax error [2, 3]
where: { isSpecial: true } (impl uses eb.or([...])) [1, 2] [1, 2]

SQLite (and MySQL — both matrices passed in #2789's CI with the unparenthesized version) parse a = $2 = $3 left-associatively as (a = $2) = $3, which happens to be the intended semantics, so the bug is invisible there.

Only implementations whose top-level node is an unparenthesized binary operation are affected. Kysely already parenthesizes eb.or([...]) / eb.and([...]) output, which is why the isSpecial row above works everywhere.

Cause

BaseCrudDialect.fieldRef() (packages/orm/src/client/crud/dialects/base-dialect.ts) returns the computer's expression unchanged:

return computer(this.eb, { modelAlias }, computedArgs);

The boolean filter path then embeds that expression as the left operand of = $n, so the operator precedence of the implementation leaks into the generated query.

Suggested fix

Parenthesize the inlined computed expression in fieldRef() (e.g. this.eb.parens(...)) so the implementation is always a single self-contained operand regardless of what wraps it. Parentheses in the SELECT projection are harmless, so this can be unconditional.

Workaround

Wrap the implementation yourself:

isMine: (eb) => eb.parens(eb('authorId', '=', 1)),

Environment

  • ZenStack v3 (@zenstackhq/orm 3.9.0, dev branch)
  • Verified locally through the e2e test harness against PostgreSQL 17.5 and SQLite (better-sqlite3)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions