IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis26/65-dars18 daqiqa
Mundarija (29)

Prisma raw SQL: $queryRaw, Prisma.sql, $executeRaw va TypedSQL

Qisqacha: ORM yetmagan joyda Prisma SQL'ni o'zingiz yozishga ruxsat beradi: db.$queryRaw`SELECT … WHERE id = ${id}` — bu tagged template, ${} ichidagi qiymat matnga yopishmaydi, $1 parametr bo'ladi. Bo'laklarni yig'ish — Prisma.sql, ro'yxat — Prisma.join, ustun nomi — faqat oq ro'yxatdan Prisma.raw. $queryRawUnsafe ga matn ulasangiz — injection: biz sinadik, 1 foydalanuvchi o'rniga 48 qator. TypedSQL esa .sql fayldan tipli funksiya yasaydi.

Bu darsda

  • $queryRaw va $executeRaw bilan parametrli SQL yozasiz.
  • Prisma.sql, Prisma.join, Prisma.raw bilan dinamik so'rovni xavfsiz yig'asiz.
  • Raw natijadagi turlarni (bigint, Decimal, Date) to'g'ri qayta ishlaysiz.
  • $queryRawUnsafe qachon xavfli ekanini isbotlaysiz.
  • TypedSQL bilan SQL faylidan tipli funksiya yasaysiz.
  • PrismaOmbori ni tugatib, holat 20 ni commit qilasiz.

Oldin bilishingiz kerak: Prisma: nested write va tranzaksiyalar, Parametrli so'rovlar va SQL injection, Tagged template literals, UPSERT va MERGE.

1. Nega bu kerak?

Oldingi darslarda Prisma ba'zi narsalarni qila olmasligini ko'rdik:

  • FOR UPDATE qulfi yo'q (tranzaksiyalar darsi).
  • orderBy da collation tanlab bo'lmaydi (filtrlash darsi).
  • groupBy relation orqali (teglar → vazifalar) guruhlay olmaydi.
  • updateMany har qatorga har xil qiymat bera olmaydi.

Bular uchun Prisma eshik qoldirgan — raw SQL. Eshik ikki xil: biri xavfsiz (tagged template), biri xavfli (Unsafe). Qaysi biri qachon — shu dars.

O'xshatish: avtomatik kofe mashinasi 10 xil ichimlik qiladi. 11-chisi kerak bo'lsa, qo'lda tayyorlaysiz — lekin mashinaning o'lchov idishi va filtridan foydalanasiz. Qo'lingizda bo'lsa ham, qoidasi o'sha.

Chiqishlarni biz ishga tushirdik: PostgreSQL 17.6, holat 20 namunasi, Prisma 7.10.1, Node 24.21.0.

2. $queryRaw: tagged template

2.1 Qiymat — parametr

ts
const teg = "#o'qish";
const egasi = 1;
await db.$queryRaw<{ id: bigint; matn: string }[]>`
  SELECT id, matn FROM vazifalar
  WHERE egasi_id = ${egasi} AND strpos(matn, ${teg}) > 0
  ORDER BY id LIMIT 3`;
text
[{"id":"1n","matn":"JavaScript kursining 26-qismini boshlash #o'qish"},{"id":"2n","matn":"Postgres'ni Docker'da ko'tarish #o'qish #docker"},{"id":"34n","matn":"SQL JOIN mashqlarini yechish #o'qish"}]
SQL:  SELECT id, matn FROM vazifalar WHERE egasi_id = $1 AND strpos(matn, $2) > 0 ORDER BY id LIMIT 3

${egasi} va ${teg} SQL matniga tushmadi — ular $1, $2 bo'ldi va alohida yuborildi. Bu oddiy template literal emas: $queryRaw — tag funksiyasi, u matn bo'laklari va qiymatlarni alohida oladi (Tagged template literals). Teg ichidagi ' ham xavfsiz.

<{ id: bigint; matn: string }[]> — natija turini siz aytasiz. Prisma uni tekshirmaydi: xato tur yozsangiz, TypeScript ishonadi. Bu raw SQL'ning narxi (TypedSQL buni hal qiladi).

2.2 Natija turlari

Raw natijada Postgres turi JavaScript turiga Prisma qoidasi bilan aylanadi:

ts
const [t] = await db.$queryRaw<Record<string, unknown>[]>`
  SELECT id, count(*) OVER () AS jami,
         count(*) OVER ()::int AS jami_int,
         round(avg(izohlar_soni) OVER (), 2) AS ortacha,
         yaratilgan_vaqt, jsonb_build_object('a', 1) AS j
  FROM vazifalar WHERE egasi_id = 1 ORDER BY id LIMIT 1`;
text
id: bigint "1n"
jami: bigint "13n"
jami_int: number 13
ortacha: Decimal 0.46
yaratilgan_vaqt: Date "2026-10-01T03:30:00.000Z"
j: object {"a":1}

E'tibor bering: count(*) Postgres'da bigint — natija ham bigint (13n). JSON.stringify uni seriyalay olmaydi (Prisma Client CRUD). Eng oddiy davosi — SQL'da ::int. numeric — Prisma.Decimal (aniq kasr), timestamptz — Date, jsonb — obyekt.

Diqqat: SELECT pg_sleep(0.1) ni $queryRaw bilan bajarsak, xato chiqdi: Failed to deserialize column of type 'void'. Natija turi Prisma bilmaydigan (void) bo'lsa — uni tanlamang yoki ::text ga aylantiring. Natija kerak bo'lmasa — $executeRaw.

2.3 Nom — parametr emas

ts
const ustun = "matn";
await db.$queryRaw`SELECT id FROM vazifalar
  ORDER BY ${ustun} LIMIT 2`;
await db.$queryRaw`SELECT count(*) FROM ${"vazifalar"}`;
text
xato yo'q, lekin: [{"id":"1n"},{"id":"2n"}]
PrismaClientKnownRequestError P2010 (asl 42601): Raw query failed. Code: `42601`. Message: `syntax error at or near "$1"`

26-qismdagining aynan o'zi (SQL injection): ORDER BY $1 — doimiy qiymat bo'yicha "saralash" (umuman saralamaydi), jadval nomi o'rnida $1 — sintaksis xatosi. Raw SQL xatolari Prisma'da P2010 kodi bilan keladi.

Tekshirib ko'ring: Nega birinchi so'rov xato bermadi?

Javob

ORDER BY $1 — Postgres uchun to'g'ri SQL: "har qatorda bir xil 'matn' satri bo'yicha saralash". Hamma qatorda qiymat bir xil — tartib aniqlanmagan. Xato yo'q, natija noto'g'ri — eng yomon xato turi.

3. Bo'laklardan yig'ish

3.1 Prisma.sql va Prisma.join

Dinamik WHERE — 26-qismdagi Parametrlar klassining Prisma varianti. Har bo'lak — Prisma.sql (u ham tagged template), ularni Prisma.join birlashtiradi:

ts
const shartlar = [
  Prisma.sql`egasi_id = ${2}`,
  Prisma.sql`bajarildi = ${false}`,
];
const idlar = [14n, 16n, 32n, 999n];
await db.$queryRaw`
  SELECT id FROM vazifalar
  WHERE ${Prisma.join(shartlar, " AND ")}
    AND id IN (${Prisma.join(idlar)})
  ORDER BY id`;
text
[{"id":"14n"}]
SQL:  SELECT id FROM vazifalar WHERE egasi_id = $1 AND bajarildi = $2 AND id IN ($3,$4,$5,$6) ORDER BY id

Parametrlar raqami o'zi to'g'ri qo'yildi: ichki bo'lakdagi ${2} ham $1 bo'ldi. Prisma.join(idlar) — vergul bilan, har biri parametr. Bo'sh massiv bersangiz — IN () sintaksis xatosi; bunday holatni oldindan tekshiring yoki = ANY(${idlar}::bigint[]) ishlating (massiv bitta parametr bo'ladi — kanon shunday qiladi).

Prisma.sql obyekti bajarilmasdan oldin nimaga aylanishini ko'rsa bo'ladi:

ts
const s = Prisma.sql`SELECT id FROM vazifalar
  WHERE egasi_id = ${3} AND matn ILIKE ${"%bozor%"}`;
console.log(s.text);
console.log(s.values);
text
SELECT id FROM vazifalar WHERE egasi_id = $1 AND matn ILIKE $2
[ 3, '%bozor%' ]

(s.sql esa ? belgilari bilan — MySQL uslubi; Postgres uchun s.text.) Bu — 26-qismdagi { text, values } shaklining o'zi.

3.2 Prisma.raw — faqat oq ro'yxatdan

Ustun nomi yoki ASC/DESC kerak bo'lsa — Prisma.raw(matn). U matnni yopishtiradi — parametr emas. Shuning uchun faqat o'zimiz yozgan qiymat:

ts
const SARALASH = {
  yangi: "yaratilgan_vaqt DESC, id DESC",
  alifbo: 'matn COLLATE "C", id',
} as const;
if (!Object.hasOwn(SARALASH, kalit)) {
  throw new Error("noma'lum saralash");
}
const tartib = Prisma.raw(SARALASH[kalit as keyof typeof SARALASH]);
await db.$queryRaw`SELECT id FROM vazifalar
  WHERE egasi_id = ${2} ORDER BY ${tartib} LIMIT 3`;
text
[{"id":"16n"},{"id":"26n"},{"id":"29n"}]
SQL: SELECT id FROM vazifalar WHERE egasi_id = $1 ORDER BY matn COLLATE "C", id LIMIT 3

COLLATE "C" — collation bo'yicha saralash, Prisma orderBy bera olmaydigan narsa. Object.hasOwn — "constructor" kabi prototip nomlaridan himoya.

flowchart LR
  Q["Qiymat (id, matn, sana)"] --> P["${qiymat} → $1 parametr"]
  L["Ro'yxat"] --> J["Prisma.join → $1,$2,$3"]
  N["Nom (ustun, ASC/DESC)"] --> W{"Oq ro'yxatda?"}
  W -- ha --> R["Prisma.raw"]
  W -- yo'q --> X["xato"]
  U["Foydalanuvchi matni"] -.-> Z["$queryRawUnsafe — HECH QACHON ulanmaydi"]

Diagrammaga qarang: foydalanuvchidan kelgan narsa faqat yuqori uch yo'ldan o'tadi. Pastdagi punktir yo'l — injection.

4. $executeRaw — o'zgartirish, son qaytaradi

ts
const n = await db.$executeRaw`
  UPDATE vazifalar SET ustuvorlik = 'past'
  WHERE egasi_id = ${2} AND bajarildi`;
console.log(n);
text
3
SQL: UPDATE vazifalar SET ustuvorlik = 'past' WHERE egasi_id = $1 AND bajarildi

$executeRaw — ta'sirlangan qatorlar soni. $queryRaw — qatorlar. SELECT … FOR UPDATE qulfi uchun ham $executeRaw qulay (natija kerak emas, faqat qulf) — kanondagi #yoz shunday.

5. $queryRawUnsafe — matn sifatida SQL

Unsafe metodlar SQL'ni oddiy satr sifatida oladi. Ular kerak bo'ladigan yagona holat — SQL matni oldindan butunlay ma'lum emas (masalan, fayldan o'qilgan migratsiya). Kanon va biz bir xil tajribani qildik:

ts
const yomon = "1 OR 1=1"; // URL'dan kelgan "id"
// ❌ matn ulangan
await db.$queryRawUnsafe(
  `SELECT id FROM vazifalar WHERE egasi_id = ${yomon}`,
);
// ✅ Unsafe, lekin qiymat $1 bilan
await db.$queryRawUnsafe(
  "SELECT id FROM vazifalar WHERE egasi_id = $1", yomon,
);
text
ulangan: 48 qator
$1 bilan: PrismaClientKnownRequestError P2010 (asl 22P02): Raw query failed. Code: `22P02`. Message: `invalid input syntax for type bigint: "1 OR 1=1"`
SQL: SELECT id FROM vazifalar WHERE egasi_id = 1 OR 1=1

Birinchisida backtick — oddiy template literal ($queryRawUnsafe( dan keyin qavs bor — bu tag emas). Natija: egasi_id = 1 OR 1=1 — hamma 48 vazifa, 8 foydalanuvchiniki. Ikkinchisida qiymat parametr: Postgres "1 OR 1=1" ni son sifatida o'qishga urindi va rad etdi.

Farq bitta qavsda: $queryRaw`…${x}…` — xavfsiz; $queryRawUnsafe(`…${x}…`) — xavfli. Kod review'da Unsafe so'zi ko'rinsa — har doim to'xtab tekshiriladi.

Tekshirib ko'ring: db.$queryRaw`SELECT … ORDER BY ${Prisma.raw(req.query.sort)}` xavflimi?

Javob

Ha, $queryRawUnsafe darajasida xavfli. Prisma.raw — matnni parametr qilmasdan yopishtiradi; sort ga id; DROP TABLE izohlar -- yozilsa, u SQL kodi bo'ladi. Prisma.raw faqat oq ro'yxatdagi o'zimiz yozgan qiymat bilan.

6. TypedSQL: .sql fayldan tipli funksiya

6.1 Muammo va g'oya

$queryRaw<T> da T ni qo'lda yozamiz — SQL o'zgarsa, tur eskirib qoladi. TypedSQL — SQL'ni alohida .sql faylga yozasiz, prisma generate --sql uni haqiqiy bazaga ulanib tahlil qiladi va parametr hamda natija turlari bilan TypeScript funksiya yasaydi.

Prisma 7.10 da TypedSQL hali preview (sinov bosqichidagi) imkoniyat — sxemada yoqiladi:

ts
generator client {
  provider        = "prisma-client"
  previewFeatures = ["partialIndexes", "typedSql"]
  // … qolgani o'zgarmaydi
}

(Bu schema.prisma bo'lagi; blok ts — chunki saytda prisma rangi yo'q.)

6.2 SQL fayl va generatsiya

prisma/sql/tegStatistikasi.sql — kanondagi teglar() so'rovi, parametr izohi bilan:

sql
-- @param {BigInt} $1:egasiId
SELECT substr(t.nom, 2) AS teg, count(*)::int AS soni
FROM teglar t
JOIN vazifa_teglari vt ON vt.teg_id = t.id
JOIN vazifalar v ON v.id = vt.vazifa_id
WHERE t.egasi_id = $1 AND v.ochirilgan_vaqt IS NULL
GROUP BY t.nom
ORDER BY soni DESC, substr(t.nom, 2) COLLATE "C"

-- @param {BigInt} $1:egasiId — $1 ning nomi va turi. Generatsiya (bazaga ulanadi — DATABASE_URL kerak):

bash
DATABASE_URL=postgres://postgres@127.0.0.1:5432/vazifalar \
  npx prisma generate --sql

Prisma yasagan fayldagi tur:

ts
export namespace tegStatistikasi {
  export type Parameters = [egasiId: number | bigint]
  export type Result = {
    teg: string | null
    soni: number | null
  }
}

6.3 Ishlatish

ts
import { tegStatistikasi }
  from "./generated/prisma/sql/tegStatistikasi.ts";

const r = await db.$queryRawTyped(tegStatistikasi(3n));
text
[{"teg":"ish","soni":2},{"teg":"mashina","soni":2},{"teg":"sport","soni":1},{"teg":"to'lov","soni":1}]
SQL: SELECT substr(t.nom, 2) AS teg, count(*)::int AS soni FROM teglar t JOIN … WHERE t.egasi_id = $1 AND … COLLATE "C"

Endi xato ishlatish kompilyatsiyada ushlanadi. Biz tsc bilan tekshirdik:

ts
const r = await db.$queryRawTyped(tegStatistikasi("3"));
const s: number = r[0].soni;
text
TS2345: Argument of type 'string' is not assignable to parameter of type 'number | bigint'.
TS2322: Type 'number | null' is not assignable to type 'number'.
TS2532: Object is possibly 'undefined'.

Ikkinchi xatoga qarang: soni — number | null. Postgres ifoda natijasi (count(*)::int) NULL bo'lishi mumkinmi — bilmaydi, shuning uchun TypedSQL ehtiyotkorlik bilan null qo'shadi. Kodda ?? 0 yoki tekshiruv kerak.

TypedSQL'ning cheklovlari (Prisma 7 hujjati bo'yicha, 2026-10-27 holatiga): preview; generatsiya uchun haqiqiy baza kerak (CI'da ham); dinamik SQL (o'zgaruvchan WHERE, Prisma.raw) — yo'q, fayl qat'iy. Kanon TypedSQL'ni sinamagan; yuqoridagi chiqishlar bizniki. Loyihaga hozircha kiritmadik: preview imkoniyat va CI'ga baza talabi — 33–34-qismda deploy qurilganda qayta ko'riladi.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Do not know how to serialize a BigInt count(*), bigint ustun SQL'da ::int yoki Number()
P2010 syntax error at or near "$1" nom parametr qilindi oq ro'yxat + Prisma.raw
Failed to deserialize column of type 'void' pg_sleep kabi natija $executeRaw yoki ::text
IN () sintaksis xatosi bo'sh Prisma.join oldindan tekshirish yoki = ANY
48 qator 1 o'rniga Unsafe ga matn ulangan tagged $queryRaw
tur noto'g'ri, xato yo'q $queryRaw<T> da T taxmin TypedSQL yoki tekshiruv (Zod)

8. Mashqlar

Mashqlar kurs/mashqlar/27/26-prisma-raw/ da. 2-mashq yechimi Node 24 da tekshirilgan (bazasiz); 3-mashqni biz bazada ishga tushirdik.

1-mashq (oson): xavfsizmi?

  • db.$queryRaw`SELECT * FROM vazifalar WHERE id = ${id}` —
  • db.$queryRawUnsafe(`SELECT * FROM vazifalar WHERE id = ${id}`) —
  • db.$queryRaw`… ORDER BY ${Prisma.raw(SARALASH[k])}` (k hasOwn bilan tekshirilgan) —
  • db.$queryRaw`… IN (${Prisma.join(idlar)})` —
Yechim
  • xavfsiz — tag; id parametr.
  • xavfli — oddiy template literal, matn ulanadi.
  • xavfsiz — yopishtirilgan matn bizning oq ro'yxatdan.
  • xavfsiz — har element parametr (bo'sh massivni alohida tekshiring).

2-mashq (o'rta): o'z sql tagingiz

Prisma.sql qanday ishlashini tushunish uchun kichik sql tag funksiyasini yozing: u { text, values } qaytarsin; har ${} — $1, $2…; agar qiymat ham shu tag natijasi bo'lsa (ichki bo'lak), uning matni joylanadi va qiymatlari davom ettirilgan raqamlar bilan qo'shiladi.

Yechim
js
const BOLAK = Symbol("sql");

function sql(qismlar, ...qiymatlar) {
  const bolaklar = [];
  const values = [];
  qismlar.forEach((q, i) => {
    bolaklar.push({ matn: q });
    if (i < qiymatlar.length) bolaklar.push({ qiymat: qiymatlar[i] });
  });
  return { [BOLAK]: true, bolaklar, values };
}

function yasash(s, values = []) {
  let text = "";
  for (const b of s.bolaklar) {
    if ("matn" in b) text += b.matn;
    else if (b.qiymat?.[BOLAK]) text += yasash(b.qiymat, values).text;
    else {
      values.push(b.qiymat);
      text += `$${values.length}`;
    }
  }
  return { text, values };
}

const shart = sql`egasi_id = ${2} AND bajarildi = ${false}`;
const hujum = "' OR 1=1 --";
const q = yasash(
  sql`SELECT id FROM vazifalar WHERE ${shart} AND matn = ${hujum}`,
);
console.log(q.text);
console.log(q.values);

Konsolda:

text
SELECT id FROM vazifalar WHERE egasi_id = $1 AND bajarildi = $2 AND matn = $3
[ 2, false, "' OR 1=1 --" ]

Tag funksiya matn bo'laklarini (qismlar) va qiymatlarni alohida oladi — shuning uchun qiymat hech qachon kodga aylanmaydi. Ichki bo'lak (shart) raqamlashni buzmadi: $1, $2, keyin tashqi $3. Prisma'ning Prisma.sql i shu g'oyaning to'liq (va sinalgan) varianti.

3-mashq (qiyin): TypedSQL bilan statistika

prisma/sql/ustuvorlikStatistikasi.sql yozing: berilgan egasining o'chirilmagan vazifalari ustuvorlik bo'yicha — jami va bajarilgan (count(*) FILTER (WHERE …)). Generatsiya qiling va Aziz (1) uchun chiqaring.

Yechim
sql
-- @param {BigInt} $1:egasiId
SELECT ustuvorlik::text AS ustuvorlik,
       count(*)::int AS jami,
       count(*) FILTER (WHERE bajarildi)::int AS bajarilgan
FROM vazifalar
WHERE egasi_id = $1 AND ochirilgan_vaqt IS NULL
GROUP BY ustuvorlik
ORDER BY ustuvorlik
ts
import { ustuvorlikStatistikasi }
  from "./generated/prisma/sql/ustuvorlikStatistikasi.ts";

const qatorlar = await db.$queryRawTyped(ustuvorlikStatistikasi(1n));
for (const q of qatorlar) {
  console.log(q.ustuvorlik, q.jami, q.bajarilgan);
}

Biz ishga tushirdik (PostgreSQL 17.6, Prisma 7.10.1):

text
orta 8 4
past 1 0
yuqori 3 1

8 + 1 + 3 = 12 — Azizning 13 vazifasidan biri Savatda. Bir nozik joy: ORDER BY ustuvorlik bu yerda ustuvorlik::text AS ustuvorlik taxallusiga ishora qiladi — alifbo tartibi (orta, past, yuqori). Enum tartibi (past, orta, yuqori) kerak bo'lsa, ORDER BY vazifalar.ustuvorlik yozing. Generatsiya qilingan turda uchala maydon | null — GROUP BY natijasini Postgres "NULL bo'lmaydi" deb kafolatlamaydi.

9. Loyiha qadami: PrismaOmbori yakuni va holat 20 commit

Oldingi uch darsda PrismaOmbori ning klienti, ol, royxat, soni, yarat, yangila, ochir metodlari yozildi. Prisma Client yetmagan uch joy raw SQL bilan tugaydi.

9.1 teglar() — uch jadval bo'yicha guruhlash

ts
async teglar(): Promise<TegSoni[]> {
  // groupBy relation orqali guruhlay olmaydi — raw SQL
  return this.#db.$queryRaw<TegSoni[]>`
    SELECT substr(t.nom, 2) AS teg, count(*)::int AS soni
    FROM teglar t
    JOIN vazifa_teglari vt ON vt.teg_id = t.id
    JOIN vazifalar v ON v.id = vt.vazifa_id
    WHERE t.egasi_id = ${this.#egasi} AND v.ochirilgan_vaqt IS NULL
    GROUP BY t.nom
    ORDER BY soni DESC, substr(t.nom, 2) COLLATE "C"`;
}

count(*)::int — bigint emas, number kelsin. Kanon o'lchovida bu metod pg'nikidan biroz tez chiqdi: 5.49 ms (pg) / 5.00 ms (prisma) — SQL bir xil.

9.2 Matn bo'yicha saralash — COLLATE "C"

Ro'yxat ?sort=matn bilan so'ralganda: mos id'larni Prisma filtri bilan, tartibni raw SQL bilan olamiz. Saralash ifodasi — oq ro'yxatdan:

ts
const MAYDON_SQL = {
  id: "id", tartib: "tartib", matn: 'matn COLLATE "C"',
  bajarildi: "bajarildi", yaratildi: "yaratilgan_vaqt",
  yangilandi: "yangilangan_vaqt",
} as const;

/** Ustun ifodasi — faqat oq ro'yxatdan */
function ustunSql(maydon: Saralash["maydon"]): string {
  if (!Object.hasOwn(MAYDON_SQL, maydon)) {
    throw new Error(`saralash maydoni oq ro'yxatda yo'q: ${maydon}`);
  }
  return MAYDON_SQL[maydon];
}

royxat ichidagi raw tarmoq:

ts
const mos = await this.#db.vazifa.findMany({
  where: { AND: asos }, select: { id: true },
});
const tartib = Prisma.raw(k.map((s) =>
  `${ustunSql(s.maydon)} ${s.kamayish ? "DESC" : "ASC"}`).join(", "));
qatorlar = await this.#db.$queryRaw<VazifaQatori[]>`
  SELECT id, matn, bajarildi, tartib, versiya,
    yaratilgan_vaqt AS "yaratilganVaqt",
    yangilangan_vaqt AS "yangilanganVaqt"
  FROM vazifalar
  WHERE id = ANY(${mos.map((m) => m.id)}::bigint[]) AND ${kursor}
  ORDER BY ${tartib} LIMIT ${olish} OFFSET ${otkaz}`;

Uch narsa: = ANY(${massiv}::bigint[]) — ro'yxat bitta parametr (bo'sh bo'lsa ham xato yo'q); AS "yaratilganVaqt" — natija maydon nomlari Prisma qatori bilan bir xil bo'lsin; ${kursor} — keysetSql yasagan Prisma.sql bo'lagi (ichida ham qiymatlar parametr, ::bigint/::text turlari bilan). Kanon o'lchovida aynan shu yo'l eng qimmat: matn saralash pg 5.42 ms, prisma 12.07 ms — ikki so'rov.

9.3 almashtirHammasi — unnest

PUT /v1/vazifalar butun ro'yxatni almashtiradi: har vazifaga har xil matn va tartib. Prisma updateMany hammaga bir xil qiymat beradi, createMany esa OVERRIDING SYSTEM VALUE (o'z id'ini berish) bilmaydi. Kanon 26-qismdagi unnest naqshini (Ommaviy ma'lumot: COPY, batch va cursor) Prisma tranzaksiyasi ichida ishlatadi:

ts
await tx.$executeRaw`
  UPDATE vazifalar v SET matn = u.matn, bajarildi = u.bajarildi,
    bajarilgan_vaqt = CASE WHEN u.bajarildi
      THEN coalesce(v.bajarilgan_vaqt, now()) END,
    tartib = u.tartib, ochirilgan_vaqt = NULL
  FROM unnest(${u.id}::bigint[], ${u.matn}::text[],
    ${u.bajarildi}::boolean[], ${u.tartib}::int[])
    AS u(id, matn, bajarildi, tartib)
  WHERE v.id = u.id AND v.egasi_id = ${this.#egasi}`;

To'rt massiv — to'rt parametr, qatorlar soni qancha bo'lmasin. Shundan keyin yangi qatorlar xuddi shunday INSERT … OVERRIDING SYSTEM VALUE SELECT … FROM unnest(…) bilan, oxirida setval bilan sequence tuzatiladi. Kanon o'lchovi: 1 000 qatorli PUT — pg 168 ms, prisma 210 ms.

9.4 Testlar va tekshiruv

test/prisma-ombor.test.ts — 25-qismdagi shartnoma testlari (omborShartnomasi("prisma", …)) va PgOmbori ning Postgres'ga xos testlari, endi Prisma ombori uchun. Baza — alohida vazifalar_test_prisma: node --test fayllarni parallel jarayonlarda ishga tushiradi va ikkala test fayli bitta bazani TRUNCATE qilsa, bir-birining ma'lumotini o'chiradi (kanon shunday 2 ta yiqilgan testni topgan — Test DB strategiyalari darsida batafsil).

Injection testidan bir parcha:

ts
const r = (qidiruv: string) => a.royxat({
  filtr: { qidiruv }, saralash: [], limit: 10,
  sahifa: { tur: "kursor" },
});
assert.deepEqual((await r("%")).vazifalar.map((v) => v.id), [1]);
assert.deepEqual((await r("_")).vazifalar.map((v) => v.id), []);
assert.deepEqual(
  (await r("' OR 1=1 --")).vazifalar.map((v) => v.id), [],
);

% — faqat matnida haqiqatan % bor vazifa (Azizniki 100% #ish) — filtrlash darsidagi ekranlash shu testda tug'ilgan.

Biz holat 20 ni o'z klasterimizda (PostgreSQL 17.6, ikkala test bazasi noldan) tekshirdik:

bash
npm run tip && npm run lint
B=postgres://postgres@127.0.0.1:5432
TEST_DATABASE_URL=$B/vazifalar_test \
TEST_PRISMA_DATABASE_URL=$B/vazifalar_test_prisma \
  npm test

Chiqish uzun (Prisma qismi). Nimaga qarang: har ✔ — bitta talab, oxirgi uch qator — jami natija:

text
▶ VazifaOmbori shartnomasi: prisma
  ✔ yarat: id 1 dan, teglar matndan, versiya 1, tartib oxiriga, sanalar ISO UTC; ol — nusxa (3478.3565ms)
  ✔ yangila: versiya +1, shart mos kelmasa konflikt, yo'q id — topilmadi (726.4868ms)
  ✔ ochir: shart, topilmadi, to'plam versiyasi har yozuvda oshadi (344.4134ms)
  ✔ royxat: filtr (bajarildi, teglar AND, qidiruv), saralash, offset, kursor, jami; soni; teglar (833.5219ms)
  ✔ almashtirHammasi: massiv tartibi = tartib (sukut saralash), id saqlanadi/beriladi, eski o'chadi (552.0879ms)
▶ PrismaOmbori: Postgres'ga xos (PgOmbori testlari bilan bir xil)
  ✔ takror matn — 409 (23P01 adapter xatosidan); o'chirilgandan keyin mumkin; id sequence'ni yeydi (1335.6363ms)
  ✔ almashtirHammasi atomik: takror matn — 409, eski ro'yxat va versiya o'zgarmaydi; matnlarni almashtirish (014) (655.5209ms)
  ✔ egasi ajratilgan va injection: qidiruv '%' / "' OR 1=1 --" — oddiy matn; saralash maydoni — oq ro'yxat (1304.8333ms)
  ✔ matn bo'yicha kursor — COLLATE "C" (JS tartibi): katta harf kichikdan oldin, takrorsiz (308.8715ms)
  ✔ raqobat: 30 parallel yarat — tartib takrorlanmaydi; bitta versiya bilan 10 PATCH — 1 ok (1107.6805ms)
✔ omborYarat(prisma): SAQLASH=prisma — fail-fast va ishlaydi (2951.787ms)
ℹ tests 99
ℹ pass 99
ℹ fail 0

Nima bo'ldi? tip va lint — 0 xato, testlar 99/99: shartnoma testlari ikkala omborga bir xil talab qo'ydi va Prisma ombori hammasidan o'tdi. Kanon (PostgreSQL 18.6) ham 99/99 oldi, uch marta ketma-ket; jonli serverda SAQLASH=postgres va SAQLASH=prisma ikkalasi 9 ta curl so'roviga bir xil HTTP kodlar qaytardi.

9.5 Commit

bash
git add src/vazifalar/prisma-ombor.ts src/ishga-tushir.ts \
  src/config.ts prisma/ prisma.config.ts \
  test/prisma-ombor.test.ts package.json package-lock.json \
  .gitignore eslint.config.js tsconfig.json
M='feat: Prisma 7 ombori (SAQLASH=prisma) — prisma-client '
M+='generatori, adapter-pg, shartnoma testlari'
git commit -m "$M"

src/generated/ git'ga kirmaydi (.gitignore): uni postinstall: prisma generate har npm ci da qayta yasaydi.

10. Real ishda

  • Deyarli har Prisma loyihasida bir nechta raw SQL bor: hisobotlar, FOR UPDATE, full-text search, ommaviy yangilash. Ularni bitta faylda (ombor) saqlash va testlash odat.
  • Kod review qoidasi: Unsafe va Prisma.raw — har biri izoh bilan, nega xavfsiz ekani yozilgan bo'lsin.
  • Intervyuda: "$queryRaw va $queryRawUnsafe farqi?", "Tagged template injection'dan qanday himoya qiladi?", "ORM bilan raw SQL'ni qachon ishlatasiz?"

Xulosa

  • $queryRaw`…${x}…` — x parametr bo'ladi; $executeRaw — o'zgartirish, son qaytaradi; xatolar — P2010.
  • Bo'laklar — Prisma.sql, ro'yxat — Prisma.join yoki = ANY(…::bigint[]), nom — oq ro'yxat + Prisma.raw.
  • $queryRawUnsafe ga matn ulash — injection: 1 o'rniga 48 qator.
  • Raw natijada count(*) — bigint, numeric — Decimal; ::int bilan oldini oling.
  • TypedSQL (preview): .sql fayl → tipli funksiya; natija maydonlari ko'pincha | null.
  • Holat 20 tayyor: PrismaOmbori 99/99 testdan o'tdi va commit qilindi.

Keyingi dars: Prisma Client extensions — $extends bilan hisoblangan maydon, model metodi, soft delete va tenant filtri — va ularning teshiklari.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Prisma raw SQL: $queryRaw, Prisma.sql, $executeRaw va TypedSQL — IlmHamroh