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
auth().userId is resolved as a JavaScript string value (e.g., "550e8400-e29b-41d4-a716-446655440000")
- The
valueMemberAccess method in expression-transformer.ts calls transformValue(value, 'String') — because the ZModel type is String
transformValue passes the string through to the PostgreSQL dialect's transformInput, which is a no-op for strings
- Kysely creates a parameterized query binding the value as
$1 with PostgreSQL type text
- The generated SQL compares this
text parameter against a uuid column: "DealershipUserAccess"."userId" = $1
- 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
-
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().
-
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
Description
When using PostgreSQL with UUID primary keys (
@db.Uuid) and access policies that referenceauth().userId, the policy-check SQL fails at runtime with:Reproduction
ZModel schema
What happens
auth().userIdis resolved as a JavaScript string value (e.g.,"550e8400-e29b-41d4-a716-446655440000")valueMemberAccessmethod inexpression-transformer.tscallstransformValue(value, 'String')— because the ZModel type isStringtransformValuepasses the string through to the PostgreSQL dialect'stransformInput, which is a no-op for strings$1with PostgreSQL typetexttextparameter against auuidcolumn:"DealershipUserAccess"."userId" = $1operator does not exist: text = uuidbecause there is no implicit cast fromtexttouuidRoot cause
In
packages/plugins/policy/src/expression-transformer.ts, thetransformValuemethod receives only theBuiltinType(e.g.,'String'). It does not have access to the field's native database type attribute (@db.Uuid), which is available in theFieldDef.attributesarray as{ name: "@db.Uuid" }.The PostgreSQL dialect's
transformInputmethod (inpackages/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
Stringfield has@db.Uuid(or other native type attributes), the generated SQL should cast the parameter value appropriately. For example:Potential fix approaches
Extend
transformValue/transformInputto accept native type info: Pass the@db.Uuidattribute information fromFieldDef.attributesthrough to the value transformation, and have the PostgreSQL dialect wrap the value in a::uuidcast usingsql.raworeb.cast().Handle it at the binary comparison level: In
_binaryortransformAuthBinary, when one side is a column reference to a@db.Uuidfield 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:
However, this is a global database-level change that may have unintended side effects.
Environment