Prisma $queryRawUnsafe — uuid vs. text bez explicitního castu (42804/42883)
import { Aside } from ‘@astrojs/starlight/components’;
Symptom
Sekce “Symptom”Tři na pohled nesouvisející rozbité featury v produkčním SaaS:
- Archiv generovaných XML dokumentů zůstával trvale prázdný, přestože generování vracelo 200.
- AI vytěžení dokumentu (OCR) vracelo 500 „Něco se pokazilo”, přestože samotná AI extrakce proběhla úspěšně.
- Generování PDF padalo na 500 — ale jen u organizací bez vyplněného firemního profilu.
V PM2 lozích se opakovaly Postgres chyby 42804 a 42883:
ERROR: column "generated_by" is of type uuid but expression is of type textERROR: operator does not exist: uuid = textERROR: operator does not exist: text = uuidRoot cause
Sekce “Root cause”Prisma binduje parametry raw dotazů jako text. Postgres je v INSERT ... VALUES kontextu umí implicitně přetypovat podle cílového sloupce, ale v WHERE col = $1 a u typovaných výrazů NE:
-- padá (uuid = text):UPDATE tenant_x.records SET status = 'done' WHERE id = $1
-- padá (sloupec uuid, výraz text):INSERT INTO tenant_x.archive (..., created_by) VALUES (..., $6)Záludný je i opačný směr: tabulka s text/cuid primárním klíčem (typicky Prisma @id @default(cuid())) a dotaz s nadbytečným castem:
-- padá (text = uuid), protože id je cuid TEXT:SELECT * FROM public.organizations WHERE id = $1::uuidSmrtící kombinace: handler chybu z archivačního kroku spolkl (catch { console.error(...) }) a vrátil 200 → feature byla měsíce tiše rozbitá a nikdo si nevšiml.
Fix
Sekce “Fix”Explicitní casty podle skutečného typu sloupce:
-- předUPDATE tenant_x.records SET status = 'done' WHERE id = $1-- poUPDATE tenant_x.records SET status = 'done' WHERE id = $1::uuid
-- před (column "created_by" is of type uuid...)VALUES ($1, $2, $3, $4, $5, $6, $7, $8)-- poVALUES ($1, $2, $3, $4, $5, $6::uuid, $7, $8)
-- před (public tabulka má TEXT cuid PK!)SELECT * FROM public.organizations WHERE id = $1::uuid-- poSELECT * FROM public.organizations WHERE id = $1Jak to odhalit
Sekce “Jak to odhalit”- E2E test, který ověřuje výsledek operace (řádek v archivu, stažené PDF), ne jen HTTP 200 hlavního kroku.
- Grep na rizikový vzor:
grep -rn 'WHERE id = \$[0-9]' src/ | grep RawUnsafea zkontrolovat typ PK každé tabulky. - V multi-tenant setupu pozor: tenant tabulky mívají
uuidPK (gen_random_uuid()), public/Prisma tabulkytextcuid — stejný dotazový vzor potřebuje jiný cast.
Lekce
Sekce “Lekce”- Implicitní koerce v INSERT maskuje problém — projde insert, spadne až UPDATE/WHERE na stejné tabulce.
- Prázdný/log-only catch kolem vedlejších efektů = tiše rozbitá feature; lepší je propagovat chybu nebo aspoň alertovat.
- Po každé změně typů PK projít všechny raw dotazy na tabulku.