Skip to content

PostgreSQL: policy check fails with "operator does not exist: text = uuid" when auth() field has @db.Uuid #2394

@niehaus1301

Description

@niehaus1301

Description

When using PostgreSQL with UUID primary keys (@db.Uuid) and access policies that reference auth().userId, the policy-check SQL fails at runtime with:

operator does not exist: text = uuid

Reproduction

ZModel schema

model User {
  userId String @id @db.Uuid @default(uuid())
  @@auth()
}

model Client {
  clientId     String     @id @db.Uuid @default(uuid())
  dealershipId String     @db.Uuid
  Dealership   Dealership @relation(fields: [dealershipId], references: [dealershipId])

  @@allow('all', Dealership.UserAccess?[userId == auth().userId])
}

model DealershipUserAccess {
  dealershipId String @db.Uuid
  userId       String @db.Uuid
  // ...relations
}

What happens

  1. auth().userId is resolved as a JavaScript string value (e.g., "550e8400-e29b-41d4-a716-446655440000")
  2. The valueMemberAccess method in expression-transformer.ts calls transformValue(value, 'String') — because the ZModel type is String
  3. transformValue passes the string through to the PostgreSQL dialect's transformInput, which is a no-op for strings
  4. Kysely creates a parameterized query binding the value as $1 with PostgreSQL type text
  5. The generated SQL compares this text parameter against a uuid column: "DealershipUserAccess"."userId" = $1
  6. PostgreSQL raises operator does not exist: text = uuid because there is no implicit cast from text to uuid

Root cause

In packages/plugins/policy/src/expression-transformer.ts, the transformValue method receives only the BuiltinType (e.g., 'String'). It does not have access to the field's native database type attribute (@db.Uuid), which is available in the FieldDef.attributes array as { name: "@db.Uuid" }.

The PostgreSQL dialect's transformInput method (in packages/orm/src/client/crud/dialects/postgresql.ts) has no handling for UUID values — it falls through to the default pass-through for strings.

Expected behavior

When a String field has @db.Uuid (or other native type attributes), the generated SQL should cast the parameter value appropriately. For example:

-- Current (broken):
"DealershipUserAccess"."userId" = $1   -- $1 is text, column is uuid → ERROR

-- Expected (working):
"DealershipUserAccess"."userId" = $1::uuid   -- explicit cast

Potential fix approaches

  1. Extend transformValue / transformInput to accept native type info: Pass the @db.Uuid attribute information from FieldDef.attributes through to the value transformation, and have the PostgreSQL dialect wrap the value in a ::uuid cast using sql.raw or eb.cast().

  2. Handle it at the binary comparison level: In _binary or transformAuthBinary, when one side is a column reference to a @db.Uuid field and the other is a value, wrap the value with a cast.

Current workaround

Users can work around this by adding an implicit cast in a Prisma migration:

CREATE CAST (text AS uuid) WITH INOUT AS IMPLICIT;

However, this is a global database-level change that may have unintended side effects.

Environment

  • ZenStack: v3.3.3
  • PostgreSQL: 16
  • Prisma: 6.x
  • Node.js: 22

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions