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 == status
auth().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
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 statuses
auth().status in statuses
status in auth().statuses
CANCELLED 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.
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]andfield in Enumfor this case.Schema used for all cases
Seed: one user with
status: 'ACTIVE', three posts withDRAFT,ACTIVE,CANCELLED. Auth:{ id: 1, status: 'ACTIVE' }.Failing cases
1. Column on the right side of a comparison
ACTIVE == statusauth().status == statusGenerated 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 withinvalid input value for enum "PostStatus": "ACTIVE".2. Column vs. column through a relation
author.status == statusGenerated SQL is
(select CASE WHEN "$$t1"."status" = 'draft' THEN 'DRAFT' ... END from "User" as "$$t1" where ...) = "Post"."status". The mapper applies its mapped-backCASEprojection 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 withoperator does not exist: text = "PostStatus".Note that
author.status == ACTIVEpasses only because both sides happen to be names.3. Enum array fields (PostgreSQL)
With
statuses PostStatus[]on bothUserandPost:ACTIVE in statusesauth().status in statusesstatus in auth().statusesCANCELLED in auth().statusesThese 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,@@denywithstatus == CANCELLED, all four collection-predicate forms, andstatus 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-backCASEprojections 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.