Repository navigation
Policy expressions do not map @map-ed enum values in several comparison shapes #2867
Description
Summary
When an enum has @map-ed member values, the ORM's name mapper translates enum names to database values at SQL execution time by pattern-matching Kysely nodes (column = value, column IN (...), insert/update values, select projections). Several SQL shapes produced by the policy plugin never reach the mapper in a form it recognizes, so policies silently return no rows on SQLite or fail with type errors on PostgreSQL.
Follow-up to #2807, which fixed field in [A, B] and field in Enum for this case.
Schema used for all cases
enum PostStatus {
DRAFT @map('draft')
ACTIVE @map('active')
CANCELLED @map('cancelled')
}
model User {
id Int @id @default(autoincrement())
status PostStatus
posts Post[]
@@allow('all', true)
}
model Post {
id Int @id
status PostStatus
authorId Int
author User @relation(fields: [authorId], references: [id])
@@allow('create', true)
@@allow('read', <rule>)
}Seed: one user with status: 'ACTIVE', three posts with DRAFT, ACTIVE, CANCELLED. Auth: { id: 1, status: 'ACTIVE' }.
Failing cases
1. Column on the right side of a comparison
ACTIVE == statusauth().status == status
Generated SQL is $1 = "Post"."status" with $1 = 'ACTIVE'. The mapper's binary-operation hook only handles a column reference on the left. SQLite returns no rows; PostgreSQL fails with invalid input value for enum "PostStatus": "ACTIVE".
2. Column vs. column through a relation
author.status == status
Generated SQL is (select CASE WHEN "$$t1"."status" = 'draft' THEN 'DRAFT' ... END from "User" as "$$t1" where ...) = "Post"."status". The mapper applies its mapped-back CASE projection inside the correlated subquery, so the left side yields the enum name while the right side is the raw database value. SQLite returns no rows; PostgreSQL fails with operator does not exist: text = "PostStatus".
Note that author.status == ACTIVE passes only because both sides happen to be names.
3. Enum array fields (PostgreSQL)
With statuses PostStatus[] on both User and Post:
ACTIVE in statusesauth().status in statusesstatus in auth().statusesCANCELLED in auth().statuses
These go through the = ANY(...) / array-contains paths which the mapper does not handle, and some also hit PostgreSQL type errors (No function matches the given name and argument types).
Passing cases (for reference)
status == ACTIVE, status != CANCELLED, status == auth().status, status in [DRAFT, ACTIVE], !(status in [...]), status in PostStatus, author.status == ACTIVE, @@deny with status == CANCELLED, all four collection-predicate forms, and status in [...] in create/update/post-update/delete policies.
Discussion
Case 1 is a small addition to the mapper (handle the mirrored shape). Cases 2 and 3 are harder to fix by pattern-matching SQL: the mapper cannot distinguish a subquery used as a result column from one used as a comparison operand.
An alternative worth considering: make the policy transformer own the rule that everything inside policy SQL is in database representation, mapping enum literals and auth() values on the way in and never emitting mapped-back CASE projections in policy subqueries. A dedicated enum-reference expression kind in the generated TS schema (instead of a plain string literal) would make that mapping exact. The mapper would keep its current scope: CRUD arguments and result projections.
Activity