SQL builder: a `where` comparison on an enum column accepts any string
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:
// 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:
import { defineConfig } from '@prisma/orm-postgres/config';
import { definePrismaConfig } from 'prisma/config';
export default definePrismaConfig({
orm: defineConfig({ contract: './contract.prisma' }),
});tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true
},
"include": ["*.ts"]
}enum.ts:
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:
npm init -y
npm pkg set type=module
npm install @prisma/[email protected] [email protected] [email protected]
npx prisma contract emit
npx tsc --noEmitOutput, 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:StorageTableToScopeTablekeeps onlycodecIdandnullablefor each column. The enum's members are not in the scope that thewherecallback sees.packages/2-sql/4-lanes/sql-builder/src/expression.ts:64-87and94-114:eq,ne,gt,gte,lt,lte,inandnotIntype both operands asCodecExpression<CodecId, N, CT>.packages/2-sql/4-lanes/relational-core/src/expression.ts:57-83:CodecExpressionaccepts a raw value of typeCT[CodecId]['input'], the codec's input type. Forpg/text@1that isstring.packages/2-sql/4-lanes/sql-builder/src/types/table-proxy.ts:37-56and122-130:insertandupdatetype their values fromExtractStorageColumnInputTypes<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.
Source: prisma/prisma