#30492·Prisma

SQL builder: a `where` comparison on an enum column accepts any string

Author: wmadden-electricCreated Sep 29, 2026Updated Sep 29, 2026
Labelskind/bug

What happened

In an application we are building on Prisma 8, we compared an enum column with a string in the typed SQL builder's where. The string was not a member of the enum, and it type-checked. The same value in update({...}) or insert([...]) is a type error, and so is the same value in the ORM client's where. So a misspelt enum value in a builder where is caught nowhere: the query runs and matches no rows.

Reproduction

Versions: @prisma/orm-postgres 8.0.0-rc.12, prisma 8.0.0-rc.17, TypeScript 6.0.3, Node 24.13.0. The same errors, and the same missing errors, appear with @prisma/orm-postgres 8.0.0-rc.13-dev.2. The cause below is read from main at d136137.

contract.prisma:

prisma
// use prisma-8

enum actor_kind {
  @@type("pg/text@1")
  human = "human"
  agent = "agent"
}

model Contact {
  id   String @id
  name String

  @@map("contact")
}

model Actor {
  id        String     @id
  kind      actor_kind
  contact   Contact?   @relation(fields: [contactId], references: [id])
  contactId String?

  @@map("actor")
}

prisma.config.ts:

typescript
import { defineConfig } from '@prisma/orm-postgres/config';
import { definePrismaConfig } from 'prisma/config';

export default definePrismaConfig({
  orm: defineConfig({ contract: './contract.prisma' }),
});

tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Preserve",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["*.ts"]
}

enum.ts:

typescript
import postgres from '@prisma/orm-postgres/runtime';
import type { Contract } from './contract.d.ts';
import contractJson from './contract.json' with { type: 'json' };

const { sql, orm } = postgres<Contract>({ contractJson });

// Accepted: no error.
sql.public.actor.select('id').where((f, fns) => fns.eq(f.kind, 'humna'));
sql.public.actor.select('id').where((f, fns) => fns.in(f.kind, ['humna', 'robot']));

// Rejected: TS2769 and TS2820.
sql.public.actor.update({ kind: 'humna' });
sql.public.actor.insert([{ id: 'a1', kind: 'humna' }]);
orm.public.Actor.where({ kind: 'humna' });

Commands:

bash
npm init -y
npm pkg set type=module
npm install @prisma/[email protected] [email protected] [email protected]
npx prisma contract emit
npx tsc --noEmit

Output, with the overload details trimmed:

enum.ts(12,27): error TS2769: No overload matches this call.
    Type '"humna"' is not assignable to type '"human" | "agent"'. Did you mean '"human"'?
enum.ts(13,38): error TS2820: Type '"humna"' is not assignable to type '"human" | "agent" | undefined'. Did you mean '"human"'?
enum.ts(14,26): error TS2769: No overload matches this call.
    Type '"humna"' is not assignable to type '"human" | "agent" | Expression<{ codecId: "pg/text@1"; nullable: boolean; many?: never; }> | null | undefined'. Did you mean '"human"'?

Lines 8 and 9 have no error. fns.ne behaves like fns.eq.

What was expected

A raw value compared with an enum column in eq, ne, in or notIn is checked against the enum's members, as it is in update, insert and the ORM client's where. fns.eq(f.kind, 'humna') fails with the same error as update({ kind: 'humna' }).

Cause

  • packages/2-sql/4-lanes/sql-builder/src/scope.ts:48-53: StorageTableToScopeTable keeps only codecId and nullable for each column. The enum's members are not in the scope that the where callback sees.
  • packages/2-sql/4-lanes/sql-builder/src/expression.ts:64-87 and 94-114: eq, ne, gt, gte, lt, lte, in and notIn type both operands as CodecExpression<CodecId, N, CT>.
  • packages/2-sql/4-lanes/relational-core/src/expression.ts:57-83: CodecExpression accepts a raw value of type CT[CodecId]['input'], the codec's input type. For pg/text@1 that is string.
  • packages/2-sql/4-lanes/sql-builder/src/types/table-proxy.ts:37-56 and 122-130: insert and update type their values from ExtractStorageColumnInputTypes<C>, which is per column, so they get the enum's union.

The builder already knows the members in other places. The select result type is SqlQueryPlan<{ id: string; kind: NonNullable<"human" | "agent"> }>, through resolvedColumnOutputTypes (table-proxy.ts:27-35 and 92). Only the comparison functions fall back to the codec's type.

Suggested fix

Carry the column's resolved input type on each scope field, for example as a type-only property beside codecId in StorageTableToScopeTable, filled from ExtractStorageColumnInputTypes. In the comparison functions, infer the field from whichever operand is a column, and type a raw value on the other side as that field's input type. Fall back to the codec's input type when neither operand carries one, such as a computed expression. Keep a way to match a value that the enum no longer lists, because a migration that renames a member has to find the rows with the old value; a cast or fns.raw is enough for that.

Why it matters

A misspelt or removed enum value in a builder where matches no rows, so a read returns nothing and an update or delete changes nothing, with no error at any stage. The builder's own insert and update and the ORM client's where already reject such a value, so this is the one place where the enum is not checked.