IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL41/64-dars23 daqiqa
Mundarija (32)

node-postgres (pg) asoslari: Client, Pool, natija obyekti va turlar

Qisqacha: Node ilovasi Postgres bilan drayver orqali gaplashadi; eng keng tarqalgani — pg (node-postgres). new pg.Pool({ connectionString }) ulanishlar hovuzini yaratadi, pool.query(sql, [qiymatlar]) esa natija obyekti qaytaradi: rows — qatorlar massivi, rowCount — nechta qator. Turlar avtomatik o'giriladi, lekin bigint, count(*) va numeric string bo'lib keladi, date esa mahalliy vaqt zonasidagi Date bo'ladi — bu ikki tuzoqni kanon tajribasi ko'rsatdi.

Bu darsda

  • pg ni o'rnatib, birinchi so'rovni Node'dan yuborasiz.
  • Client va Pool farqini va qachon qaysi biri kerakligini tushuntirasiz.
  • Natija obyektini o'qiysiz: rows, rowCount, command, fields.
  • Postgres turlari JavaScript'ga qanday o'girilishini bilasiz va setTypeParser bilan sozlaysiz.
  • Xato obyektidagi code va constraint bo'yicha ilovada qaror qabul qilasiz.

Oldin bilishingiz kerak: SELECT va WHERE, PostgreSQL o'rnatish va ulanish, PostgreSQL ma'lumot turlari, Express'ga kirish, --watch, --env-file va node --run.

1. Nega bu kerak?

«SQL qadami» bo'limlarida biz vazifalar bazasini psql orqali qurdik: 6 jadval, view, trigger'lar, 48 ta namuna vazifa. psql — inson uchun qulay vosita. Lekin 25-qismdagi vazifalar-api serveri inson emas. U har HTTP so'rovda bazaga o'zi murojaat qilishi kerak: "Azizning vazifalarini ber", "yangi vazifa qo'sh".

Node'ning o'zida Postgres bilan gaplashadigan modul yo'q. Bu ishni drayver (driver) bajaradi — dastur bilan baza orasidagi tarjimon kutubxona. Drayver Postgres'ning tarmoq protokolini biladi: TCP ulanish ochadi, so'rovni baytlarga o'giradi, javobni JavaScript obyektlariga aylantiradi.

O'xshatish: chet eldagi do'konga buyurtma beryapsiz. Siz o'zbekcha yozasiz, tarjimon xatni ularning tiliga o'giradi va javobni sizga o'zbekcha qaytaradi. Siz protokol (ularning tili) bilan ishlamaysiz — faqat "nima kerak" deysiz.

sequenceDiagram
  participant A as Node ilova
  participant D as pg (drayver)
  participant P as Postgres
  A->>D: pool.query(sql, [1])
  D->>P: TCP: so'rov baytlari
  P-->>D: qatorlar (matn ko'rinishida)
  D-->>A: { rows: [...], rowCount }

Diagrammaga qarang: Postgres javobni matn sifatida yuboradi ('48', 't', '2026-10-24'). Uni number, boolean, Date ga aylantirish — drayverning ishi. Darsning eng muhim tuzoqlari aynan shu o'girishda.

Bu qism kanoni (biz o'lchagan muhit): PostgreSQL 18.6 (Docker), Node 24.21.0, pg 8.23.1, Windows 11. Kanon chiqishlari — natijalar/tajriba/t41-pg-asoslari.txt faylidan.

2. O'rnatish va birinchi so'rov

2.1 Tajriba papkasi

Node qadamidagi tajribalar uchun alohida papka oching (vazifalar-api ga hali tegmaymiz — sababi «Node qadami» bo'limida):

bash
mkdir pg-tajriba
cd pg-tajriba
npm init -y
npm i pg@8.23.1

package.json ga "type": "module" qo'shing — ESM va top-level await uchun (ES Modules Node'da). Ulanish manzilini kodga yozmaymiz — .env faylga:

text
DATABASE_URL=postgres://postgres@127.0.0.1:5499/vazifalar

Bu PostgreSQL o'rnatish va ulanish darsidagi kanon konteyneri: lokal, parolsiz (trust). Haqiqiy serverda manzilda parol bo'ladi — shuning uchun u kodda emas, muhit o'zgaruvchisida turadi. Skriptni node --env-file=.env fayl.js bilan ishga tushiramiz (--env-file).

Diqqat: Parolli DATABASE_URL ni hech qachon kodga va git'ga yozmang. .env — .gitignore da, repoda faqat .env.example (parolsiz namuna).

2.2 Client: bitta ulanish

js
import pg from "pg";

const client = new pg.Client({
  connectionString: process.env.DATABASE_URL,
});
await client.connect();               // TCP ulanish ochiladi
const r = await client.query(
  "SELECT id, matn, bajarildi FROM vazifalar " +
    "WHERE egasi_id = $1 ORDER BY id LIMIT 2",
  [1],
);
console.log("rows:", r.rows);
await client.end();                   // ulanish yopiladi

Kanon chiqishi:

text
rows: [
  {
    id: '1',
    matn: "JavaScript kursining 26-qismini boshlash #o'qish",
    bajarildi: false
  },
  {
    id: '2',
    matn: "Postgres'ni Docker'da ko'tarish #o'qish #docker",
    bajarildi: true
  }
]

Uch narsaga qarang:

  1. import pg from "pg" — pg CommonJS paket, shuning uchun default import qilib, pg.Client deb olamiz.
  2. $1 — parametr: qiymat (1) SQL matniga yopishtirilmaydi, alohida massivda ketadi. Nega bu hayot-mamot masalasi — keyingi darsda. Hozircha qoida: foydalanuvchidan kelgan har qiymat — $1, $2, … orqali.
  3. id: '1' — qo'shtirnoqda! id ustuni bigint, pg uni string qilib berdi. bajarildi esa haqiqiy false (boolean). Sababini «Turlar» bo'limida ko'ramiz.

client.end() ni unutsangiz, ochiq TCP ulanish event loop'ni band qiladi va skript tugamaydi — terminal "osilib" qoladi (Event loop fazalari).

2.3 Ulanish xatolari

Server ishlamasa yoki baza nomi xato bo'lsa, connect() xato tashlaydi. Biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, Windows 11):

text
ECONNREFUSED | connect ECONNREFUSED 127.0.0.1:5490
3D000 | database "yoq_baza" does not exist

Birinchisi — Node'ning tarmoq xatosi (ECONNREFUSED — "ulanish rad etildi": bu portda hech kim tinglamayapti; Docker konteyneri to'xtagan bo'lishi mumkin). Ikkinchisi — Postgres'ning o'z javobi: 3D000 — SQLSTATE kodi, "bunday baza yo'q". Postgres xatolarida doim besh belgili kod bor — u «Xato obyekti» bo'limida asosiy vositamiz bo'ladi.

Tekshirib ko'ring: Skript natijani to'g'ri chiqardi, lekin terminalga qaytmayapti. Nima unutilgan?

Javob

await client.end(). Ochiq ulanish (soket) event loop'da faol resurs — u yopilmaguncha Node jarayonni tugatmaydi. Serverda bu muammo emas (server baribir ishlab turadi), lekin skript va testlarda har doim yoping.

3. Natija obyekti

query() faqat qatorlarni emas, butun obyektni qaytaradi. Kanon chiqishidan (SELECT va UPDATE):

text
rowCount: 2 | command: SELECT | fields: id(oid 20), matn(oid 25), bajarildi(oid 16)
UPDATE rowCount: 13 | rows: 0
Xususiyat Nima Misol
rows qatorlar massivi (har biri obyekt) [{ id: '1', … }]
rowCount qaytgan yoki o'zgargan qatorlar soni 2, UPDATE da 13
command bajarilgan buyruq turi SELECT, UPDATE
fields ustunlar: nomi va tur raqami id(oid 20)

UPDATE … WHERE egasi_id = 1 uchun rows bo'sh, rowCount esa 13 — Azizning 13 ta vazifasi yangilandi. Bu juda foydali: API'da "id = 999 ni yangila" so'rovi rowCount: 0 qaytarsa — bunday vazifa yo'q, javob 404. RETURNING bilan (INSERT va RETURNING) esa UPDATE ham rows qaytaradi.

OID (object identifier) — Postgres har turga bergan raqam: 20 — bigint, 25 — text, 16 — boolean, 1082 — date. Drayver turlarni aynan shu raqam bo'yicha o'giradi. Bu raqamlarni yodlash shart emas, lekin 20 va 1082 ni eslab qoling — pastda ular kerak bo'ladi.

3.1 Qatorlar massiv ko'rinishida

Ba'zan obyekt emas, massiv qulay (masalan CSV yozganda). rowMode: "array":

js
const r3 = await pool.query({
  text: "SELECT id, matn FROM vazifalar WHERE id = $1",
  values: [2],
  rowMode: "array",
});
console.log("rowMode array:", r3.rows);

Kanon chiqishi:

text
rowMode array: [ [ 2, "Postgres'ni Docker'da ko'tarish #o'qish #docker" ] ]

Bu yerda id — son 2, string emas. Sababi: kanon skripti bu so'rovdan oldin bigint uchun o'z o'giruvchisini o'rnatgan («Turlar» bo'limiga qarang).

Yana bir shakl — nomlangan so'rov (name): { name: "vazifa-ol", text, values }. Postgres so'rovni birinchi marta tahlil qilib eslab qoladi (prepared statement), shu ulanishda keyingi safar tahlilni takrorlamaydi. Kanon chiqishi: named statement: { matn: 'Mijozga taklifnoma yuborish #ish' }. Odatda kerak emas — pg oddiy parametrli so'rovni ham tez bajaradi; tashqi pooler bilan esa nomlangan so'rov muammo tug'dirishi mumkin (PgBouncer).

4. Pool: ulanishlar hovuzi

4.1 Nega Client serverga yetmaydi

Ulanish ochish qimmat: TCP, autentifikatsiya, Postgres serverida yangi jarayon. Kanon o'lchovi (Connection pooling darsida batafsil): har so'rovga yangi ulanish — 12.7 ms, tayyor ulanishdan — 1.1 ms. Bitta umumiy Client esa boshqa muammo: bir ulanish bir vaqtda faqat bitta so'rov bajaradi, qolganlari navbatda turadi.

Yechim — hovuz (pool): bir nechta ochiq ulanish, so'rov kelganda bo'shini beradi, ish tugagach qaytarib oladi. Xuddi taksi parkiga o'xshaydi: har yo'lovchi uchun yangi mashina sotib olinmaydi — bo'sh turgan mashina yuboriladi, safardan keyin u parkka qaytadi.

flowchart LR
  S1[so'rov 1] --> P{Pool}
  S2[so'rov 2] --> P
  S3[so'rov 3] --> P
  P --> U1[ulanish A]
  P --> U2[ulanish B]
  P -.navbat.-> S3
  U1 --> DB[(Postgres)]
  U2 --> DB

Diagrammada ikkita ulanish bor: uchinchi so'rov bo'sh ulanish chiqquncha navbatda kutadi.

4.2 pool.query — o'zi oladi, o'zi qaytaradi

js
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 5,                 // ko'pi bilan 5 ta ulanish
});
const ishlar = Array.from({ length: 5 }, () =>
  pool.query("SELECT pg_backend_pid() AS pid, pg_sleep(0.05)"),
);
const natijalar = await Promise.all(ishlar);
const pidlar = new Set(natijalar.map((x) => x.rows[0].pid));
console.log("turli ulanishlar:", pidlar.size);
await pool.end();

Kanon chiqishi:

text
5 parallel so'rov, turli ulanishlar: 5 | pool: { total: 5, idle: 5, waiting: 0 }

pg_backend_pid() — so'rovni bajargan server jarayoni raqami. Beshta parallel so'rov beshta turli jarayonda bajarildi: hovuz 5 ta ulanish ochdi. Ish tugagach hammasi bo'sh (idle: 5) — keyingi so'rovlarni kutyapti.

pool.query() ichida uch qadam bor: bo'sh ulanishni oladi → so'rovni bajaradi → ulanishni hovuzga qaytaradi. Siz hech narsa qaytarmaysiz — shuning uchun oddiy so'rovlar uchun eng xavfsiz yo'l shu.

4.3 Qaysi biri qachon

Holat Tanlov
Server, API — ko'p parallel so'rov Pool (ilovada bitta, modul darajasida)
Bir martalik skript, migratsiya Client yoki Pool — farqi kam
Tranzaksiya (bir nechta so'rov bitta ulanishda) pool.connect() → client → release()
LISTEN — doimiy tinglash alohida Client

Uchinchi qator — Tranzaksiya asoslari darsida, to'rtinchisi — LISTEN/NOTIFY darsida. Eng ko'p uchraydigan xato: har HTTP so'rovda new pg.Pool() yaratish. Hovuz bir marta, ilova ishga tushganda yaratiladi va hamma joyda ishlatiladi.

Tekshirib ko'ring: max: 5 li hovuzga bir vaqtda 8 ta pool.query keldi. Nima bo'ladi?

Javob

5 tasi darhol ulanish oladi, 3 tasi navbatda kutadi (waitingCount: 3). Birinchi ulanish bo'shashi bilan navbatdagisi uni oladi. Xato yo'q — faqat kutish. Kutish chegarasi (connectionTimeoutMillis) qo'yilsa, juda uzoq kutgan so'rov xato bilan tugaydi (Connection pooling).

5. Turlar: Postgres → JavaScript

5.1 Kanon jadvali

Kanon bitta so'rovda 16 ta turli turdagi qiymatni oldi va JavaScript'da qaysi turga aylanganini chiqardi:

text
  int          number   1
  bigint       string   "9007199254740993"
  count        string   "48"
  numeric      string   "12.50"
  float8       number   0.1
  bool         boolean  true
  tz           Date     2026-10-24T04:00:00.000Z
  ts           Date     2026-10-24T04:00:00.000Z
  date         Date     2026-10-23T19:00:00.000Z
  interval     object   {"days":1,"hours":2}
  jsonb        object   {"a":1}
  int_massiv   Array    [1,2]
  text_massiv  Array    ["#uy"]
  enum         string   "yuqori"
  uuid         string   "36188893-7917-44ed-91b2-ed6d3a00d863"
  bytea        Buffer   {"type":"Buffer","data":[1,2]}

Ko'pchiligi kutilganidek: int → son, jsonb → obyekt, massiv → Array, timestamptz → Date. Lekin uchta joy alohida e'tibor talab qiladi.

5.2 bigint va count(*) nega string

JavaScript'ning number turi butun sonni faqat 2⁵³ − 1 gacha aniq saqlaydi (Number.MAX_SAFE_INTEGER). Postgres bigint esa 2⁶³ − 1 gacha boradi. Kattaroq sonni number ga aylantirsak — jim buziladi:

js
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number("9007199254740993")); // 9007199254740992
console.log(BigInt("9007199254740993")); // 9007199254740993n

Ikkinchi qatorga qarang: oxirgi raqam 3 emas, 2 bo'ldi. Hech qanday xato yo'q — shunchaki boshqa son. ID'lar uchun bu dahshat: 993-vazifani so'rasangiz, 992-ni olasiz. Shuning uchun pg xavfsiz yo'lni tanlaydi: bigint (OID 20) ni string qiladi. count(*) ham bigint qaytaradi — shuning uchun "48" string.

Bizning vazifalar sxemamizda hamma ID bigint (Primary key tanlovi). Demak har id string keladi.

5.3 numeric nega string

numeric(12,2) — aniq o'nlik son, pul uchun (Pul, miqdor va enum'larni modellash). JavaScript'da aniq o'nlik tur yo'q — number ikkilik kasr, 0.1 + 0.2 bo'lmaydi 0.3. pg aniqlikni yo'qotmaslik uchun "12.50" ni string qoldiradi. Hisoblashni bazada qiling yoki tiyinda butun son bilan ishlang.

5.4 date tuzog'i: bir kun oldin

Eng ayyor tuzoq. '2026-10-24'::date — sana, vaqtsiz. Kanon natijasi:

text
  date         Date     2026-10-23T19:00:00.000Z

24-oktabr 23-oktabr bo'lib ketdi! Sabab: pg sanani JavaScript Date ga Node jarayonining mahalliy vaqt zonasida yarim tun deb o'giradi. Toshkent — UTC+5, demak 24-oktabr 00:00 (Toshkent) = 23-oktabr 19:00 (UTC). toISOString() UTC'da ko'rsatadi — va sana surildi. Serverdagi Node UTC'da, sizniki Toshkentda bo'lsa, bir xil kod turli natija beradi.

Zonasiz timestamp (ts qatori) ham xuddi shunday: 09:00 mahalliy deb olindi → 04:00Z. Faqat timestamptz (zonali) ishonchli — u aniq vaqt nuqtasi. Bizning sxemada hamma vaqt ustunlari timestamptz (SQL'da sana va vaqt zonalari) — shuning uchun bu tuzoq faqat date ga tegishli.

5.5 setTypeParser: o'giruvchini o'zgartirish

pg.types.setTypeParser(oid, funksiya) — shu OID uchun o'z o'giruvchingiz. Funksiya Postgres yuborgan matnni oladi. Kanon tavsiyasi ikkita:

js
import pg from "pg";

// bigint (OID 20) → number, lekin xavfsiz bo'lmasa xato
pg.types.setTypeParser(20, (s) => {
  const n = Number(s);
  if (!Number.isSafeInteger(n)) {
    throw new RangeError(`bigint ${s} Number'ga sig'maydi`);
  }
  return n;
});
// date (OID 1082) → o'girmasdan, "2026-10-24" satri
pg.types.setTypeParser(1082, (s) => s);

Kanon chiqishi (bigint o'giruvchisidan keyin):

text
setTypeParser(20) dan keyin: { id: 1, jami: 13 } number
katta bigint: RangeError bigint 9007199254740993 Number'ga sig'maydi

date o'giruvchisini biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, TZ=Asia/Tashkent):

text
sukut: 2026-10-23T19:00:00.000Z
parser bilan: 2026-10-24

Nega bigint uchun shunchaki Number emas, tekshiruvli funksiya? Chunki jim buzilish ochiq xatodan yomonroq. Bizning ilovada ID 9 kvadrillionga yetmaydi — lekin yetsa, o'giruvchi darhol "baqiradi". BigInt qaytarish ham mumkin, lekin JSON.stringify uni bilmaydi:

js
const vazifa = { id: 1n, matn: "Bozordan meva olish" };
console.log(JSON.stringify(vazifa));

Konsolda:

text
TypeError: Do not know how to serialize a BigInt

"BigInt'ni qanday seriyalashni bilmayman" — API javobi JSON bo'lgani uchun BigInt har javobda qo'shimcha ish talab qiladi. Kanon tanlovi — xavfsiz number.

Diqqat: setTypeParser — global sozlama: butun jarayondagi hamma pg ulanishlariga ta'sir qiladi. Uni ilova boshida, bitta joyda (masalan db.js modulida) chaqiring. Undan ham xavfsizi — o'giruvchini faqat bitta hovuzga berish (pg.TypeOverrides); vazifalar-api aynan shunday qiladi («Node qadami» bo'limi).

Tekshirib ko'ring: SELECT count(*) FROM vazifalar natijasini if (r.rows[0].count > 10) bilan tekshirdingiz. Bu ishlaydimi?

Javob

Tasodifan ishlaydi: "48" > 10 da JavaScript satrni songa aylantiradi. Lekin r.rows[0].count + 1 → "481" (satr ulanishi!). Ishonchli yo'l — SQL'da count(*)::int deb yozish (Postgres integer qaytaradi → number) yoki Number(...) bilan aniq o'girish.

6. Xato obyekti: code va constraint

Bazadagi cheklovlar (CREATE TABLE va cheklovlar) buzilsa, pg xato tashlaydi. Xato obyektida Postgres yuborgan maydonlar bor. Kanon chiqishi:

text
xato: {
  code: '23505',
  constraint: 'vazifalar_egasi_id_matn_key',
  table: 'vazifalar',
  detail: 'Key (egasi_id, lower(matn))=(1, onamga dori olib borish #oila) already exists.'
}
xato: 23514 vazifalar_matn_check
xato: 22P02 invalid input syntax for type integer: "abc"

Birinchisi — Aziz "Onamga dori olib borish #OILA" ni qo'shmoqchi bo'ldi, lekin registrga befarq noyoblik cheklovi bor: "kalit (egasi_id, lower(matn)) allaqachon mavjud". Ikkinchisi — bo'sh matn, CHECK cheklovi. Uchinchisi — "abc" ni songa aylantirib bo'lmadi.

Ilova xato matnini emas, code ni tekshiradi — matn til va versiyaga qarab o'zgaradi, kod esa doimiy. API'da bu HTTP javobga aylanadi (Xato javoblari: Problem Details):

SQLSTATE Ma'nosi HTTP
23505 unique buzildi 409 Conflict
23514 CHECK buzildi 400 / 422
23503 foreign key buzildi 409 yoki 422
22P02 noto'g'ri formatdagi qiymat 400

constraint maydoni aniqroq javob uchun: vazifalar_egasi_id_matn_key → "Sizda bunday vazifa bor". Cheklov nomlarini bir andozada bergan edik (Amaliyot: real loyiha sxemasi) — mana foydasi.

Oldindan bitta eslatma: takror matn tekshiruvi keyinroq o'zgaradi. Deadlock va serialization failure darsida bu cheklov EXCLUDE … DEFERRABLE ga almashadi (sql/014), shunda xato kodi 23P01 bo'ladi, cheklov nomi — vazifalar_egasi_id_matn_excl. API ikkalasini ham 409 ga aylantiradi. Hozircha, holat 44 da — 23505.

Maslahat: Validatsiyani faqat bazaga qoldirmang: 25-qismda Zod bilan kirishda tekshirasiz. Baza — oxirgi himoya chizig'i: Zod o'tkazib yuborgan yoki ikki parallel so'rov tufayli paydo bo'lgan xatoni (masalan takror matn) faqat baza ushlaydi.

7. SSL va xavfsiz ulanish

Lokal konteyner SSL'siz ishlaydi. Bulut Postgres'i (Neon, Supabase, RDS) odatda SSL (TLS) talab qiladi — trafik shifrlangan bo'lishi uchun. pg da ikki yo'l: manzilda ?sslmode=require yoki obyektda ssl: { … }.

Kanon sslmode=require ni lokal serverga sinadi. Ikki narsa chiqdi:

text
(node:22544) Warning: SECURITY WARNING: The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full'.
ssl: The server does not support SSL connections

Ikkinchi qator kutilgan: server SSL'ni qo'llamaydi. Birinchisi — pg 8.23 ning ogohlantirishi (to'liq matni bir necha qator). Unga ko'ra hozir require aslida verify-full (sertifikat va host nomini to'liq tekshirish) deb talqin qilinadi; pg 9 da standart libpq ma'nosiga o'tadi (require — faqat shifrlash, sertifikat tekshirilmaydi). Hozirgi xulqni saqlash uchun ogohlantirish sslmode=verify-full ni aniq yozishni maslahat beradi. Bulut provayderining hujjatida qaysi rejim tavsiya qilingan bo'lsa — shuni aniq yozing.

ssl: { rejectUnauthorized: false } ni internetdagi maslahatlarda ko'p uchratasiz — u sertifikat tekshiruvini o'chiradi, ya'ni o'rtadagi odam hujumiga (net, dns va tls — MITM) eshik ochadi. Faqat lokal tajribada.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
r.rows[0].id === 1 har doim false bigint → string '1' setTypeParser(20) yoki Number(id)
count + 1 → "481" count(*) string count(*)::int
Sana bir kun oldin chiqadi date mahalliy zonada Date setTypeParser(1082, s => s)
Skript tugamaydi client.end() / pool.end() yo'q oxirida yoping
Har so'rovda yangi Pool hovuz qayta yaratilyapti bitta hovuz, modul darajasida
ECONNREFUSED konteyner to'xtagan, port xato docker ps, DATABASE_URL

9. Mashqlar

Mashqlar kurs/mashqlar/26/41-pg-asoslari/ da. Bazaga ulanadigan yechimlar uchun kanon konteyneri (holat 40) ishlab turishi kerak.

1-mashq (oson): bo'shliqlarni to'ldiring

  • pool.query natijasidagi qatorlar massivi — r.
  • UPDATE nechta qatorni o'zgartirganini bildiradi — r.
  • bigint ning OID raqami —
  • count(*) ni son qilib olish uchun SQL'da — count(*)::
  • Unique cheklov buzilganda SQLSTATE —
Yechim
  • rows — har qator obyekt (yoki rowMode: "array" da massiv).
  • rowCount — rows bo'sh bo'lsa ham (UPDATE, DELETE) soni bor.
  • 20 — pg.types.setTypeParser(20, …).
  • int — integer turi number bo'lib keladi (2³¹ gacha — sanash uchun yetarli).
  • 23505 — unique_violation; API'da odatda 409.

2-mashq (o'rta): xavfsiz bigint o'giruvchisi

bigintniOgir(s) funksiyasini yozing: satrni oladi, xavfsiz butun son bo'lsa number qaytaradi, bo'lmasa RangeError tashlaydi. Bazasiz — oddiy JS. "48", "9007199254740991" va "9007199254740993" bilan sinang (Number.isSafeInteger — Matn ↔ son va sonni tekshirish).

Yechim
js
function bigintniOgir(s) {
  const n = Number(s);
  if (!Number.isSafeInteger(n)) {
    throw new RangeError(`bigint ${s} Number'ga sig'maydi`);
  }
  return n;
}

for (const s of ["48", "9007199254740991", "9007199254740993"]) {
  try {
    console.log(s, "→", bigintniOgir(s));
  } catch (e) {
    console.log(s, "→", e.name, e.message);
  }
}

Konsolda:

text
48 → 48
9007199254740991 → 9007199254740991
9007199254740993 → RangeError bigint 9007199254740993 Number'ga sig'maydi

Number("9007199254740993") 9007199254740992 beradi — bu butun son, lekin xavfsiz emas (isSafeInteger false), shuning uchun tekshiruv ushlaydi. Number.isInteger ishlatganda ushlamasdi. Shu funksiyani pg.types.setTypeParser(20, bigintniOgir) ga bersangiz — kanon o'giruvchisi.

3-mashq (qiyin): foydalanuvchi statistikasi

Skript yozing (pg, Pool): har foydalanuvchi uchun ismi, jami vazifalari soni va bajarilganlari soni (o'chirilmaganlar — faol_vazifalar view'i), faqat vazifasi borlar, ism bo'yicha tartiblangan. Natijada sonlar number bo'lsin — setTypeParser siz. Har qatorni Ism: bajarildi/jami ko'rinishida chiqaring. (JOIN va GROUP BY — JOIN'lar, Agregatlar.)

Yechim
js
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});
const { rows } = await pool.query(`
  SELECT f.ism,
         count(*)::int AS jami,
         count(*) FILTER (WHERE v.bajarildi)::int AS bajarilgan
  FROM foydalanuvchilar f
  JOIN faol_vazifalar v ON v.egasi_id = f.id
  GROUP BY f.id, f.ism
  ORDER BY f.ism`);
for (const q of rows) {
  console.log(`${q.ism}: ${q.bajarilgan}/${q.jami}`);
}
console.log("tur:", typeof rows[0].jami);
await pool.end();

Biz ishga tushirdik (holat 40 namuna ma'lumoti, PostgreSQL 17.6, Node 24.21.0):

text
Aziz Karimov: 5/12
Bekzod Nazarov: 2/4
Dilnoza Rahimova: 3/6
Gulnora Saidova: 2/4
Javohir Olimov: 3/6
Madina Yusupova: 4/9
Sardor Tursunov: 3/6
tur: number

count(*)::int — integer turi, pg uni number qiladi. FILTER (WHERE …) — faqat shartga mos qatorlarni sanaydi. Azizda 12 ta: 13-vazifasi Savatda (yumshoq o'chirilgan), view uni ko'rmaydi. Shahnoza (8-foydalanuvchi) yo'q: uning vazifasi yo'q, oddiy JOIN uni tashlab ketdi. Hammani chiqarish kerak bo'lsa — LEFT JOIN va count(v.id).

10. Node qadami: vazifalar-api Postgres'ga ulanadi

Avval tajriba: pg-tajriba papkasida «Turlar» bo'limidagi so'rovni ishga tushiring. date qatori sizda ham bir kun oldin chiqyaptimi? TZ=UTC bilan-chi? Keyin setTypeParser(20) va setTypeParser(1082) ni qo'shib, natija o'zgarganini ko'ring.

Endi loyiha. 25-qismdagi vazifalar-api vazifalarni xotirada yoki JSON faylda saqlardi. Ombor interfeysi (VazifaOmbori) bor edi: server faqat shu interfeysni biladi (qatlamli loyiha tuzilmasi). Shu darsdan boshlab unga uchinchi amalga oshirish qo'shiladi — PgOmbori. Diagrammaga qarang: marshrut va xizmat kodi o'zgarmaydi, faqat ombor tanlovi (SAQLASH) kengayadi.

flowchart LR
  M[marshrut va xizmat] --> I{{VazifaOmbori}}
  I -->|SAQLASH=xotira| X[XotiraOmbori]
  I -->|SAQLASH=fayl| F[FaylOmbori]
  I -->|SAQLASH=postgres| P[PgOmbori]
  P --> H[pg.Pool]
  H --> DB[(Postgres)]

Bu ish to'rt darsga bo'lingan:

Dars vazifalar-api da nima qo'shiladi
shu dars 013 sxema fayli, pg, PgOmbori ning o'qish metodlari
Parametrli so'rovlar filtrlar, saralash oq ro'yxati, qidiruv
Connection pooling .env sozlamalari, hovuz, ishga tushish va yopilish
PgBouncer yakuniy tekshiruv va commit

10.1 Sxema: 013-api-tartib.sql

API'ning uchta talabi sxemada yo'q edi (Amaliyot: real loyiha sxemasi darsida "kerak bo'lsa yangi fayl bilan" degan edik):

  1. tartib — foydalanuvchi vazifalarni sudrab tartiblaydi (20-qism), API esa ro'yxatni shu tartibda qaytaradi.
  2. Vaqtlar millisekundgacha. Postgres vaqtni mikrosekundgacha saqlaydi, JavaScript Date esa millisekundgacha. Farq qolsa, sahifalash kursori (.123Z) bazadagi .123456 dan kichik chiqadi va qator ikkinchi sahifada qayta keladi (Keyset pagination).
  3. Ro'yxat versiyasi — foydalanuvchilar.vazifalar_versiyasi: har yozuvda +1, API ro'yxatning ETag'i uchun ishlatadi.

Kanon faylidan asosiy qatorlar (to'liq faylda trigger va izohlar ham bor):

sql
ALTER TABLE vazifalar ADD COLUMN tartib integer;
-- mavjudlarga: egasi bo'yicha yaratilish tartibida 1, 2, 3 …
UPDATE vazifalar v SET tartib = t.n
FROM (SELECT id, row_number() OVER (
        PARTITION BY egasi_id ORDER BY yaratilgan_vaqt, id) AS n
      FROM vazifalar) t
WHERE t.id = v.id;
ALTER TABLE vazifalar ALTER COLUMN tartib SET NOT NULL;
CREATE INDEX vazifalar_tartib_idx ON vazifalar (egasi_id, tartib, id)
  WHERE ochirilgan_vaqt IS NULL;
ALTER TABLE vazifalar ALTER COLUMN yaratilgan_vaqt
  SET DEFAULT date_trunc('milliseconds', now());
ALTER TABLE foydalanuvchilar
  ADD COLUMN vazifalar_versiyasi bigint NOT NULL DEFAULT 0;

row_number() OVER (PARTITION BY …) — window funksiya: har egasining vazifalarini alohida raqamlaydi. Faylda yana tartibni_qoy trigger'i bor: tartib berilmasa, yangi vazifa egasining ro'yxati oxiriga tushadi. faol_vazifalar ko'rinishi ham qayta yaratiladi — SELECT * ko'rinish yangi ustunni ko'rmaydi (Amaliyot: real loyiha sxemasi dagi topilma).

Qo'llash — avvalgi fayllar kabi:

bash
psql -X -1 -v ON_ERROR_STOP=1 \
  -d postgres://postgres@127.0.0.1:5499/vazifalar \
  -f sql/013-api-tartib.sql

Tekshiruv. Nimaga qarang: tartib ustunida 3 yo'q — 8-vazifa Savatda, ko'rinish uni ko'rsatmaydi.

sql
SELECT id, tartib, matn
FROM faol_vazifalar
WHERE egasi_id = 1
ORDER BY tartib, id
LIMIT 4;
SELECT id, vazifalar_versiyasi FROM foydalanuvchilar
WHERE id IN (1, 2) ORDER BY id;
text
 id | tartib |                       matn
----+--------+--------------------------------------------------
  1 |      1 | JavaScript kursining 26-qismini boshlash #o'qish
  2 |      2 | Postgres'ni Docker'da ko'tarish #o'qish #docker
  9 |      4 | Onamga dori olib borish #oila
 17 |      5 | Portfolio saytiga blog yozish #portfolio
(4 rows)

 id | vazifalar_versiyasi
----+---------------------
  1 |                  13
  2 |                   6
(2 rows)

Biz holat 44 ni o'z PostgreSQL 17.6 klasterimizda noldan qurib ishga tushirdik. Nima bo'ldi? Har vazifa egasi ichida raqam oldi; Azizning ro'yxat versiyasi boshlanishiga 13 — uning vazifalari soni (fayl shunday to'ldiradi).

10.2 pg va PgOmbori ning o'qish qismi

vazifalar-api papkasida:

bash
npm i pg@8.23.1
npm i -D @types/pg@8.23.1

@types/pg — TypeScript turlari (pg o'zi JavaScript'da yozilgan). Yangi fayl src/vazifalar/pg-ombor.ts. Uning boshi — bigint o'giruvchisi va hovuz (kanon kodi, uzun qatorlar bo'lingan):

ts
import pg from "pg";
import type { Pool } from "pg";

/** bigint (oid 20) → number, faqat xavfsiz butun son bo'lsa */
function bigintSon(s: string): number {
  const n = Number(s);
  if (!Number.isSafeInteger(n)) {
    throw new RangeError(`bigint ${s} Number'ga sig'maydi`);
  }
  return n;
}

/** Pool'ning o'z tur o'giruvchilari — global pg.types o'zgarmaydi */
export const TURLAR = new pg.TypeOverrides();
TURLAR.setTypeParser(pg.types.builtins.INT8, bigintSon);

export function pgPool(
  url: string,
  sozlama: pg.PoolConfig = {},
): Pool {
  return new pg.Pool({
    connectionString: url,
    application_name: "vazifalar-api",
    types: TURLAR,
    ...sozlama,
  });
}

Uch narsaga qarang:

  • pg.TypeOverrides — «setTypeParser» bo'limidagi global sozlamaning xavfsiz shakli. O'giruvchi faqat shu hovuzga tegishli (types: TURLAR), boshqa kutubxonalar global pg.types ni o'zgarmagan holda ko'radi. pg.types.builtins.INT8 — o'sha OID 20.
  • bigintSon — 2-mashqdagi funksiya. Endi API javobida id — son.
  • application_name — server tomonida har ulanish shu nom bilan ko'rinadi (pg_stat_activity). Bazada "bu ulanish kimniki?" degan savolga javob.

O'qish metodi ol(id). Nimaga qarang: egasi sharti va "Savatda emas" sharti — har so'rovda.

ts
async ol(id: number): Promise<Vazifa | undefined> {
  const [q] = await this.#bitta<VazifaQatori>(
    `SELECT ${QATOR} FROM vazifalar v
     WHERE v.id = $1 AND v.egasi_id = $2
       AND v.ochirilgan_vaqt IS NULL`,
    [id, this.#egasi],
  );
  return q && vazifa(q);
}

QATOR — ustunlar ro'yxati: v.id, v.matn, v.bajarildi, v.tartib va hokazo. Yordamchi #bitta — pool.query ning o'rami: u xatoni API xatosiga aylantiradi («Xato obyekti» bo'limi). Funksiya vazifa(q) qatorni API shakliga o'giradi. Masalan, yaratilgan_vaqt (Date) → yaratildi (ISO satr). Teglar esa matndan olinadi, 25-qismdagi qoida bilan. egasiId konstruktorda beriladi: auth hali yo'q (28-qism), API bitta foydalanuvchining vazifalari bilan ishlaydi. royxat va soni ham shu faylda — ularning shartlari keyingi darsda.

Sinov skripti bilan ko'rdik (holat 44, PostgreSQL 17.6, Node 24.21.0):

text
ol(9): number 9 [ 'oila' ] 2026-10-03T02:55:00.000Z

id — number (string emas), teglar matndan ajratildi, vaqt — ISO satr.

10.3 Shartnoma testlari

25-qismda xotira va fayl omborlari bitta shartnoma testlar to'plamidan (contract testlar) o'tgan edi (omborShartnomasi). Endi Postgres ombori ham xuddi shu testlardan o'tadi — server uchun uchala ombor bir xil ishlashining isboti. Yangi test/pg-ombor.test.ts:

ts
const URL = process.env["TEST_DATABASE_URL"]
  ?? "postgres://postgres@127.0.0.1:5499/vazifalar_test";

omborShartnomasi("postgres", async () => {
  await tozala(pool); // TRUNCATE … RESTART IDENTITY + 2 foydalanuvchi
  return new PgOmbori(pool, 1);
});

Testlar alohida bazada ishlaydi — vazifalar_test, namuna ma'lumotsiz: har test jadvallarni tozalaydi, haqiqiy bazangizga tegmaydi. Uni bir marta yarating (namuna fayllarini o'tkazib):

bash
createdb -h 127.0.0.1 -p 5499 -U postgres vazifalar_test
for f in sql/0*.sql; do
  case $f in *namuna*) continue;; esac
  psql -X -q -1 -v ON_ERROR_STOP=1 \
    -d postgres://postgres@127.0.0.1:5499/vazifalar_test -f "$f"
done

Baza topilmasa, Postgres testlari yiqilmaydi — o'tkaziladi (SKIP) va sababini yozadi: npm test Docker'siz kompyuterda ham ishlaydi. Kanon chiqishi (yo'q bazaga ulanish, qator qisqartirildi):

text
﹣ PgOmbori # test bazasi yo'q (…): connect ECONNREFUSED

Shartnoma testlaridan tashqari faylda Postgres'ga xos 7 ta test bor (yumshoq o'chirish, trigger teglari, takror matn, injection, kursor…) — ularni keyingi darslarda ko'ramiz. Biz holat 44 testlarini o'z klasterimizda ishga tushirdik (PostgreSQL 17.6, Node 24.21.0):

text
✔ VazifaOmbori shartnomasi: postgres (658.6044ms)
✔ PgOmbori: Postgres'ga xos (1092.6057ms)
ℹ tests 12
ℹ pass 12
ℹ fail 0

12 ta Postgres testi o'tdi; kanonda butun to'plam (25-qismdagi testlar bilan) — 79/79. Commit hali yo'q: holat 44 PgBouncer darsida yakunlanadi.

11. Real ishda

  • Har Node backend Postgres bilan shu yo'l bilan gaplashadi: to'g'ridan pg, yoki uning ustidagi vosita — Prisma, Drizzle, Kysely (27-qismda; ular ham ichida drayver ishlatadi).
  • Tur o'giruvchilari — production'dagi ko'p "sirli" xatolarning manbai: ID taqqoslash, sana surilishi, pul summasi satr bo'lib ulanishi.
  • Xato kodlari — yaxshi API "Internal Server Error" emas, "bunday vazifa bor (409)" deydi. SQLSTATE ro'yxati — Postgres hujjatida, Appendix A.
  • Intervyuda: "Pool va Client farqi?", "Nega bigint string bo'lib keladi?", "Unique buzilishini qanday aniqlaysiz?"

Xulosa

  • Node Postgres bilan drayver orqali gaplashadi; pg — eng keng tarqalgani (kanon: 8.23.1).
  • Pool — serverda bitta, pool.query ulanishni o'zi oladi va qaytaradi; Client — bitta ulanish, qo'lda connect/end.
  • Natija: rows, rowCount, command, fields; UPDATE'da rowCount: 0 → 404.
  • bigint, count(*), numeric — string; date va timestamp — mahalliy zonadagi Date (bir kun surilishi mumkin).
  • setTypeParser(20) — xavfsiz number, setTypeParser(1082, s => s) — sana satr bo'lib qoladi.
  • Xatoni matn bilan emas, code va constraint bilan aniqlang.

Keyingi dars: Parametrli so'rovlar va SQL injection — nega $1 hayot-mamot masalasi: kanon matnni yopishtirib, boshqalarning 48 ta vazifasini va hamma emaillarni o'g'irladi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
node-postgres (pg) asoslari: Client, Pool, natija obyekti va turlar — IlmHamroh