Skip to content

Policy expressions do not map @map-ed enum values in several comparison shapes #2867

Description

@ymc9

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

  • 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 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pluginZenStack plugin relatedruntimeZenStack runtime related

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions