Skip to content

Commit 300d024

Browse files
samuelho-devclaude
andauthored
fix: only treat @db.Uuid columns as UUIDs — drop field-name inference (#37)
isUuidField inferred UUID from field-name patterns (/^id$/, /_id$/, /_uuid$/, /^uuid$/) for any String column. UUID is a column type, not a naming convention: external text identifiers ending in _id (Stripe acct_/cus_/txn_…, slugs, provider refs) were wrongly given Schema.isUUID() and Die'd at decode. Prisma records real uuid columns via @db.Uuid (bare String → text), so the native-type/@db.Uuid checks already capture every genuine UUID. Removed the name-pattern tier; updated tests + docs. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5a85dcf commit 300d024

5 files changed

Lines changed: 73 additions & 27 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
"prisma-effect-kysely": minor
3+
---
4+
5+
fix: only treat `@db.Uuid` columns as UUIDs — drop field-name inference
6+
7+
`isUuidField` previously had a third detection tier that inferred UUID from the
8+
field *name* (`/^id$/`, `/_id$/`, `/^.*_uuid$/`, `/^uuid$/`) for any `String`
9+
column. UUID is a column *type*, not a naming convention, so this was a
10+
false-positive generator: every external-system identifier stored as text — most
11+
notably Stripe IDs (`acct_…`, `cus_…`, `sub_…`, `price_…`, `txn_…`, `ch_…`,
12+
`evt_…`), plus slugs and provider/session references — ends in `_id` without
13+
being a UUID. The generator emitted `Schema.String.check(Schema.isUUID())` for
14+
those columns, and decoding real data threw `Die`/`ParseError`
15+
("Expected a UUID, got \"acct_\"") at runtime.
16+
17+
Prisma always records a genuine `uuid` column via the `@db.Uuid` native type (a
18+
bare `String` maps to `text`), so the native-type and `@db.Uuid`-documentation
19+
checks already capture 100% of real UUID columns. The name-pattern tier only
20+
ever contradicted that authoritative information, so it has been removed.
21+
22+
`isUuidField` now returns true only when:
23+
24+
1. `field.nativeType[0] === 'Uuid'` (a `@db.Uuid` column), or
25+
2. the field documentation includes `@db.Uuid`.
26+
27+
**Migration:** columns that are genuinely UUID-typed are unaffected (they carry
28+
`@db.Uuid`). Columns that were relying on name inference to get UUID validation
29+
lose it — which is the fix, since they were text. To keep an explicit UUID check
30+
on a non-`@db.Uuid` column, add `/// @db.Uuid` to the field, or override its
31+
schema with `/// @customType(...)`.

CLAUDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,15 @@ The `DB`-interface entry for a join table uses `Schema.Codec.Encoded<typeof Join
9191

9292
## UUID detection
9393

94-
`isUuidField()` in `src/prisma/type.ts`, priority order:
94+
`isUuidField()` in `src/prisma/type.ts` — authoritative DMMF type only:
9595

9696
1. `field.nativeType[0] === 'Uuid'` (from `@db.Uuid`)
9797
2. `field.documentation` includes `@db.Uuid`
98-
3. Name regex: `id`, `*_id`, `*_uuid`, `uuid`
98+
99+
No name-regex tier. UUID is a column type, not a naming convention; inferring it
100+
from `*_id`/`*_uuid` names false-positived on text identifiers (e.g. Stripe
101+
`acct_…`/`cus_…`) and crashed at decode. Use `/// @db.Uuid` or `@customType(...)`
102+
to mark non-native UUID columns.
99103

100104
## Type mappings
101105

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,18 @@ Arrays → `Schema.Array(t)`. Nullable → `Schema.NullOr(t)`.
121121

122122
## UUID Detection
123123

124-
Priority order:
124+
A column is treated as a UUID only when Prisma's type information says so:
125125

126126
1. Native type: `@db.Uuid`
127-
2. Documentation: `@db.Uuid` in field comment
128-
3. Field name pattern: `id`, `*_id`, `*_uuid`, `uuid`
127+
2. Documentation: `@db.Uuid` in the field comment (`/// @db.Uuid`)
128+
129+
UUID is a column type, not a naming convention — a bare `String` maps to `text`,
130+
so `@db.Uuid` always captures genuine UUID columns. Field-name inference
131+
(`*_id`, `*_uuid`, …) is intentionally NOT used: external identifiers such as
132+
Stripe IDs (`acct_…`, `cus_…`) are text but end in `_id`, and inferring UUID
133+
from the name produced false `Schema.isUUID()` checks that crashed at decode
134+
time. Mark a non-`@db.Uuid` column as a UUID explicitly via `/// @db.Uuid`, or
135+
override its schema entirely with `@customType(...)`.
129136

130137
## Custom Type Overrides
131138

src/__tests__/prisma-parsing.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { mapFieldToEffectType } from '../effect/type';
2626
*/
2727

2828
describe('Prisma Parsing & Domain Logic', () => {
29-
describe('UUID Detection (3-Tier Strategy)', () => {
29+
describe('UUID Detection (authoritative @db.Uuid only)', () => {
3030
it('should detect UUID via @db.Uuid native type (priority 1)', () => {
3131
const field = createMockField({
3232
name: 'id',
@@ -45,16 +45,20 @@ describe('Prisma Parsing & Domain Logic', () => {
4545
expect(PrismaType.isUuidField(field)).toBe(true);
4646
});
4747

48-
it('should detect UUID via field name patterns (priority 3)', () => {
49-
const uuidFields = [
48+
it('should NOT infer UUID from field-name patterns', () => {
49+
// Names ending in `_id`/`_uuid` (and bare `id`/`uuid`) are NOT proof of a
50+
// UUID column — external identifiers like Stripe IDs (`acct_…`, `cus_…`)
51+
// are text. Only an explicit @db.Uuid marks a UUID; name guessing is gone.
52+
const nameOnlyFields = [
5053
createMockField({ name: 'id', isId: true }),
5154
createMockField({ name: 'user_id' }),
55+
createMockField({ name: 'stripe_customer_id' }),
5256
createMockField({ name: 'external_uuid' }),
5357
createMockField({ name: 'uuid' }),
5458
];
5559

56-
for (const field of uuidFields) {
57-
expect(PrismaType.isUuidField(field)).toBe(true);
60+
for (const field of nameOnlyFields) {
61+
expect(PrismaType.isUuidField(field)).toBe(false);
5862
}
5963
});
6064

src/prisma/type.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
import type { DMMF } from '@prisma/generator-helper';
22

33
/**
4-
* Check if a field is a UUID using native DMMF type information
5-
* 3-tier detection: native type � documentation � field name patterns
4+
* Check if a field is a UUID, based solely on the authoritative DMMF type info.
5+
*
6+
* UUID is a *column type*, not a naming convention. Prisma always records a
7+
* `uuid` column as the `@db.Uuid` native type (a bare `String` maps to `text`),
8+
* so the native-type/`@db.Uuid` checks capture every genuine UUID column.
9+
*
10+
* A previous third tier inferred UUID from field-name patterns (`id`, `*_id`,
11+
* `*_uuid`, `uuid`). That was a false-positive generator: any external-system
12+
* identifier stored as text — Stripe IDs (`acct_…`, `cus_…`, `txn_…`), slugs,
13+
* provider/session references — ends in `_id` without being a UUID, yet got
14+
* `Schema.isUUID()` applied and then died at decode time on real data. The DMMF
15+
* already knows the real type, so the name guess only ever contradicted ground
16+
* truth. It has been removed; use `/// @db.Uuid` (or a real `@db.Uuid` column)
17+
* to mark UUID columns explicitly.
618
*/
719
export function isUuidField(field: DMMF.Field) {
8-
// 1. Check native type (most reliable)
20+
// Native type — the authoritative signal for a `uuid` column.
921
if (field.nativeType?.[0] === 'Uuid') {
1022
return true;
1123
}
1224

13-
// 2. Check documentation for @db.Uuid
25+
// `@db.Uuid` recorded in the field's documentation/attributes.
1426
if (field.documentation?.includes('@db.Uuid')) {
1527
return true;
1628
}
1729

18-
// 3. Fallback: Field name patterns (only for String type)
19-
if (field.type !== 'String') {
20-
return false;
21-
}
22-
23-
const uuidFieldPatterns = [
24-
/^id$/, // Primary ID fields
25-
/_id$/, // Foreign key ID fields
26-
/^.*_uuid$/, // uuid suffix
27-
/^uuid$/, // Direct uuid fields
28-
] as const;
29-
30-
return uuidFieldPatterns.some((pattern) => pattern.test(field.name));
30+
return false;
3131
}
3232

3333
/**

0 commit comments

Comments
 (0)