@m2k-5f/pgtx 2.3.0 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,380 +1,426 @@
1
- ## 🚀 Pgtx
1
+ ## 🚀 Pgtx
2
2
 
3
- [![Tests](https://github.com/M2K-5F/pgtx/actions/workflows/tests.yaml/badge.svg)](https://github.com/M2K-5F/pgtx/actions/workflows/tests.yaml)
4
- [![npm version](https://img.shields.io/npm/v/@m2k-5f/pgtx.svg)](https://www.npmjs.com/package/@m2k-5f/pgtx)
3
+ [![Tests](https://github.com/M2K-5F/pgtx/actions/workflows/tests.yaml/badge.svg)](https://github.com/M2K-5F/pgtx/actions/workflows/tests.yaml)
4
+ [![npm version](https://img.shields.io/npm/v/@m2k-5f/pgtx.svg)](https://www.npmjs.com/package/@m2k-5f/pgtx)
5
5
 
6
- **Blazing-fast PostgreSQL driver** with pipeline support.
7
- Zero deps, pure TypeScript.
6
+ **Blazing-fast PostgreSQL driver for Node.js.**
8
7
 
9
- ---
8
+ Pipeline execution, automatic prepared statements, typed SQL, transactions, and zero dependencies.
10
9
 
11
- ## 📦 Installation
10
+ **Up to 3.4× faster than Postgres.js and 15.6× faster than `pg` in concurrent pipeline workloads.**
12
11
 
13
- ```bash
14
- npm install @m2k-5f/pgtx
15
- # yarn add @m2k-5f/pgtx
16
- # pnpm add @m2k-5f/pgtx
17
- # bun add @m2k-5f/pgtx
18
- ```
19
12
 
20
- ---
13
+ ---
21
14
 
22
- ## Performance
15
+ ## 📦 Installation
23
16
 
24
- **2000 concurrent UPSERT queries, 150 concurrency, connection pool:**
17
+ ```bash
18
+ npm install @m2k-5f/pgtx
19
+ # yarn add @m2k-5f/pgtx
20
+ # pnpm add @m2k-5f/pgtx
21
+ # bun add @m2k-5f/pgtx
22
+ ```
25
23
 
26
- | Tool | RPS | Avg Time | Connections |
27
- |------|-----|----------|-------------|
28
- | **Pgtx** | **1584** | **0.631ms** | **20 pool connections (pipeline multiplexing)** |
29
- | Native `pg` | 179 | 5.585ms | 20 pool connections |
24
+ ---
30
25
 
31
- **Up to 9x faster in concurrent pipeline workloads**
26
+ ## Features
32
27
 
33
- **Pgtx achieves higher throughput by multiplexing concurrent queries over PostgreSQL connections using pipeline execution.**
28
+ - **Pipeline queries** Automatic query multiplexing over PostgreSQL pipeline protocol
29
+ - **Tagged templates** — Natural SQL with type safety
30
+ - **Transactions & Savepoints** — Nested transactions with rollback
31
+ - **Bulk inserts** — Auto-extract columns from objects
32
+ - **Dynamic updates** — Generate SET clauses from objects
33
+ - **Recursive fragments** — Compose SQL like Lego
34
+ - **Prepared statements** — Automatic prepared statement caching
35
+ - **Connection pool** — Auto-management connections with support for pipeline queries via the pool itself.
36
+ - **Zero dependencies** — Lightweight and blazing
34
37
 
38
+ ---
35
39
 
36
- > Benchmark source available in the [repository](https://github.com/M2K-5F/pgtx).
40
+ ## Performance
37
41
 
38
- ---
42
+ ### Stress test — 3000 concurrent `SELECT` queries
39
43
 
40
- ## ✨ Features
44
+ **Environment**
41
45
 
42
- - **Pipeline queries** 9x faster than `pg`
43
- - **Tagged templates** Natural SQL with type safety
44
- - **Transactions & Savepoints** Nested transactions with rollback
45
- - **Bulk inserts** — Auto-extract columns from objects
46
- - **Dynamic updates** — Generate SET clauses from objects
47
- - **Recursive fragments** — Compose SQL like Lego
48
- - **Prepared statements** — Automatic prepared statement caching
49
- - **Connection pool** — Auto-management connections with support for pipeline queries via the pool itself.
50
- - **Zero dependencies** — Lightweight and blazing
46
+ * PostgreSQL connection pool: **10 connections**
47
+ * **3000 concurrent parameterized `SELECT` queries**
48
+ * Same SQL and identical workload for all drivers
51
49
 
52
- ---
50
+ | Driver | Time |
51
+ | ---------------------- | -----------: |
52
+ | 🥇 **Pgtx (Pipeline)** | **24.18 ms** |
53
+ | Postgres.js (Pipeline) | 83.36 ms |
54
+ | node-postgres (`pg`) | 377.95 ms |
53
55
 
54
- ## 🔥 Why Pgtx?
56
+ ### Relative performance
55
57
 
56
- | Feature | Pgtx | pg | TypeORM | Prisma |
57
- |---------|------|----|---------|--------|
58
- | **Pipeline Queries** | ✅ | ❌ | ❌ | ❌ |
59
- | **Tagged Templates** | ✅ | ❌ | ❌ | ❌ |
60
- | **Transactions** | ✅ | ✅ | ✅ | ✅ |
61
- | **Savepoints** | ✅ | ❌ | ✅ | ❌ |
62
- | **Bulk Insert** | ✅ | ❌ | ✅ | ✅ |
63
- | **Prepared Statements** | ✅ | ✅ | ✅ | ✅ |
64
- | **TypeScript** | ✅ | ✅ | ✅ | ✅ |
65
- | **Zero Dependencies** | ✅ | ❌ | ❌ | ❌ |
66
- | **Connection Pool** | ✅ | ✅ | ✅ | ✅ |
58
+ * 🚀 **3.4× faster than Postgres.js**
59
+ * 🚀 **15.6× faster than node-postgres (`pg`)**
67
60
 
68
- ---
61
+ Pgtx achieves high throughput by:
69
62
 
63
+ * Pipeline query multiplexing
64
+ * Synchronous protocol encoding
65
+ * Batched socket writes
66
+ * Automatic prepared statement caching
67
+ * Row description caching
68
+ * Zero-dependency implementation
70
69
 
71
- ## 🚀 Quick Start
70
+ > Benchmark source is available in the repository and can be reproduced locally.
72
71
 
73
- ```typescript
74
- import { sql, Pool } from "@m2k-5f/pgtx";
75
72
 
76
- const pool = new Pool({
77
- host: 'localhost',
78
- user: 'postgres',
79
- password: 'postgres',
80
- database: 'myapp'
81
- })
73
+ ---
82
74
 
83
- // Type-safe query
84
- const [user] = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
75
+ ## 🔥 Why Pgtx?
85
76
 
86
- // Bulk insert
87
- await pool.query`
88
- INSERT INTO users ${sql.insert([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }])}
89
- `
77
+ | Capability | **Pgtx** | **Postgres.js** | **pg** |
78
+ | -------------------------------- | :------: | :-------------: | :----: |
79
+ | Pipeline queries | ✅ | ✅ | ❌ |
80
+ | Pipeline multiplexing in pool | ✅ | ❌ | ❌ |
81
+ | Tagged template SQL | ✅ | ✅ | ❌ |
82
+ | Automatic prepared statements | ✅ | ✅ | ✅ |
83
+ | Prepared statement deduplication | ✅ | ❌ | ❌ |
84
+ | Transactions | ✅ | ✅ | ✅ |
85
+ | Savepoints | ✅ | ✅ | ❌ |
86
+ | LISTEN / NOTIFY | ✅ | ✅ | ✅ |
87
+ | Connection pool | ✅ | ✅ | ✅ |
88
+ | Zero dependencies | ✅ | ✅ | ❌ |
90
89
 
91
- // Transaction
92
- await pool.begin(async (tx) => {
93
- await tx.query`UPDATE accounts SET balance = balance - 100 WHERE id = ${1}`
94
- await tx.query`UPDATE accounts SET balance = balance + 100 WHERE id = ${2}`
95
- })
96
- ```
90
+ ---
97
91
 
98
- ---
92
+ ## 🚀 Quick Start
99
93
 
94
+ ```typescript
95
+ import { sql, Pool } from "@m2k-5f/pgtx";
100
96
 
101
- ## 🎯 Typed Error Handling
97
+ const pool = new Pool({
98
+ host: 'localhost',
99
+ user: 'postgres',
100
+ password: 'postgres',
101
+ database: 'myapp'
102
+ })
103
+
104
+ // Type-safe query
105
+ const [user] = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
106
+
107
+ // Bulk insert
108
+ await pool.query`
109
+ INSERT INTO users ${sql.insert([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }])}
110
+ `
111
+
112
+ // Transaction
113
+ await pool.begin(async (tx) => {
114
+ await tx.query`UPDATE accounts SET balance = balance - 100 WHERE id = ${1}`
115
+ await tx.query`UPDATE accounts SET balance = balance + 100 WHERE id = ${2}`
116
+ })
117
+ ```
118
+
119
+ ---
120
+
121
+
122
+ ## 🎯 Typed Error Handling
123
+
124
+ Pgtx queries return `Future<T, PostgresError>` from [fluent-future](https://www.npmjs.com/package/fluent-future) instead of raw `Promise<T>`. This gives you:
102
125
 
103
- Pgtx queries return `Future<T, PostgresError>` from [fluent-future](https://www.npmjs.com/package/fluent-future) instead of raw `Promise<T>`. This gives you:
126
+ - **Typed errors** `PostgresError` with `code`, `severity`, `detail`
127
+ - **Declarative recovery** — `.recover()`, `.recoverIf()` instead of try/catch
128
+ - **Chain composition** — `.andThen()`, `.orElse()`, `.tap()`, `.tapErr()`
104
129
 
105
- - **Typed errors** — `PostgresError` with `code`, `severity`, `detail`
106
- - **Declarative recovery** `.recover()`, `.recoverIf()` instead of try/catch
107
- - **Chain composition** `.andThen()`, `.orElse()`, `.tap()`, `.tapErr()`
130
+ ```typescript
131
+ const users = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
132
+ .recoverIf(err => err.code === '42P01', []) // undefined_table → []
133
+ .recoverIf(err => err.code === '23505', []) // unique_violation → []
134
+ .tapErr(err => logger.error(err)) // log remaining errors
135
+ ```
108
136
 
109
- ```typescript
110
- const users = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
111
- .recoverIf(err => err.code === '42P01', []) // undefined_table → []
112
- .recoverIf(err => err.code === '23505', []) // unique_violation → []
113
- .tapErr(err => logger.error(err)) // log remaining errors
114
- ```
137
+ ---
115
138
 
116
- ---
117
139
 
140
+ ## 📖 Features
118
141
 
119
- ## 📖 Features
120
142
 
121
- ### Transactions & Savepoints
143
+ ### Pipeline by Default
122
144
 
123
- ```typescript
124
- await pool.begin(async (tx) => {
125
- await tx.query`INSERT INTO orders (user_id) VALUES (${userId})`
126
-
127
- await tx.savepoint('update_stock', async (stx) => {
128
- await stx.query`UPDATE stock SET count = count - 1 WHERE product_id = ${productId}`
129
- if (outOfStock) throw new Error('out of stock') // Only savepoint rolls back
145
+ `Bind` and `.bind` from [fluent-future](https://www.npmjs.com/package/fluent-future) automatically multiplex independent queries over PostgreSQL pipeline protocol — no manual batching required:
146
+
147
+ ```typescript
148
+ // 5 queries, only 2 network round-trips
149
+ const {user, posts, ...data} = await Bind({
150
+ user: () => pool.query<User>`...`,
151
+ config: () => pool.query<Config>`...`,
152
+ announcements: () => pool.query<Announcement>`...`
153
+ })
154
+ .bind({
155
+ posts: ({ user }) => pool.query<Post>`...`,
156
+ notifications: ({ user }) => pool.query<Notif>`...`
157
+ })
158
+ ```
159
+ > 🚀 Pgtx automatically groups concurrent queries into pipeline batches, reducing network overhead by up to 5x compared to sequential queries.
160
+
161
+
162
+ ### Transactions & Savepoints
163
+
164
+ ```typescript
165
+ await pool.begin(async (tx) => {
166
+ await tx.query`INSERT INTO orders (user_id) VALUES (${userId})`
167
+
168
+ await tx.savepoint('update_stock', async (stx) => {
169
+ await stx.query`UPDATE stock SET count = count - 1 WHERE product_id = ${productId}`
170
+ if (outOfStock) throw new Error('out of stock') // Only savepoint rolls back
171
+ })
172
+ .tapErr(console.log) // Error: out of stock
130
173
  })
131
- .tapErr(console.log) // Error: out of stock
132
- })
133
- ```
174
+ ```
175
+
176
+
177
+ ### Async Notifications (LISTEN / NOTIFY)
134
178
 
179
+ Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting multiplexed query pipeline.
135
180
 
136
- ### Async Notifications (LISTEN / NOTIFY)
181
+ ```typescript
182
+ // 1. Sending a notification
183
+ await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
137
184
 
138
- Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting multiplexed query pipeline.
185
+ // 2. Receiving notifications (Requires a dedicated connection from the pool)
186
+ const conn = await pool.acquire()
139
187
 
140
- ```typescript
141
- // 1. Sending a notification
142
- await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
188
+ const onEvent = (payload: string) => {
189
+ console.log(`Received payload: ${payload}`)
190
+ }
143
191
 
144
- // 2. Receiving notifications (Requires a dedicated connection from the pool)
145
- const conn = await pool.acquire()
192
+ // Multiplexes multiple callbacks onto a single LISTEN command seamlessly
193
+ await conn.listen('user_events', onEvent)
194
+ await conn.listen('user_events', (data) => logToFile(data))
146
195
 
147
- const onEvent = (payload: string) => {
148
- console.log(`Received payload: ${payload}`)
149
- }
196
+ // Clean up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
197
+ await conn.unlisten('user_events', onEvent)
150
198
 
151
- // Multiplexes multiple callbacks onto a single LISTEN command seamlessly
152
- await conn.listen('user_events', onEvent)
153
- await conn.listen('user_events', (data) => logToFile(data))
199
+ // Keep the connection active as long as you need notifications!
200
+ // Do NOT release it back to the pool prematurely.
201
+ ```
154
202
 
155
- // Clean up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
156
- await conn.unlisten('user_events', onEvent)
203
+ > ⚠️ **Architecture Note:** While `notify` is atomic and can be triggered directly from the `Pool` on any random socket, `listen` and `unlisten` are stateful commands tied to a specific PostgreSQL backend process. Therefore, subscription methods are **strictly available only on explicit `Connection` instances** fetched via `pool.acquire()`.
157
204
 
158
- // Keep the connection active as long as you need notifications!
159
- // Do NOT release it back to the pool prematurely.
160
- ```
161
205
 
162
- > ⚠️ **Architecture Note:** While `notify` is atomic and can be triggered directly from the `Pool` on any random socket, `listen` and `unlisten` are stateful commands tied to a specific PostgreSQL backend process. Therefore, subscription methods are **strictly available only on explicit `Connection` instances** fetched via `pool.acquire()`.
206
+ ### Bulk Inserts
163
207
 
208
+ ```typescript
209
+ const users = [
210
+ { name: 'Alice', email: 'alice@test.com' },
211
+ { name: 'Bob', email: 'bob@test.com' }
212
+ ]
164
213
 
165
- ### Bulk Inserts
214
+ await pool.query`
215
+ INSERT INTO users ${sql.insert(users)}
216
+ `
217
+ // INSERT INTO users (name, email) VALUES ($1, $2), ($3, $4)
218
+ ```
166
219
 
167
- ```typescript
168
- const users = [
169
- { name: 'Alice', email: 'alice@test.com' },
170
- { name: 'Bob', email: 'bob@test.com' }
171
- ]
220
+ ### Dynamic Updates
172
221
 
173
- await pool.query`
174
- INSERT INTO users ${sql.insert(users)}
175
- `
176
- // INSERT INTO users (name, email) VALUES ($1, $2), ($3, $4)
177
- ```
222
+ ```typescript
223
+ const data = { status: 'active', last_login: new Date() }
178
224
 
179
- ### Dynamic Updates
225
+ await pool.query`
226
+ UPDATE users SET ${sql.update(data)} WHERE id = ${userId}
227
+ `
228
+ // UPDATE users SET status = $1, last_login = $2 WHERE id = $3
229
+ ```
180
230
 
181
- ```typescript
182
- const data = { status: 'active', last_login: new Date() }
231
+ ### Recursive Fragments
183
232
 
184
- await pool.query`
185
- UPDATE users SET ${sql.update(data)} WHERE id = ${userId}
186
- `
187
- // UPDATE users SET status = $1, last_login = $2 WHERE id = $3
188
- ```
233
+ ```typescript
234
+ const filter = sql.fragment`status = ${'active'} AND age > ${21}`
235
+ const subquery = sql.fragment`(SELECT id FROM roles WHERE name = ${'admin'})`
189
236
 
190
- ### Recursive Fragments
191
-
192
- ```typescript
193
- const filter = sql.fragment`status = ${'active'} AND age > ${21}`
194
- const subquery = sql.fragment`(SELECT id FROM roles WHERE name = ${'admin'})`
237
+ await pool.query`
238
+ SELECT * FROM users
239
+ WHERE ${filter} AND role_id = (${subquery})
240
+ `
241
+ ```
195
242
 
196
- await pool.query`
197
- SELECT * FROM users
198
- WHERE ${filter} AND role_id = (${subquery})
199
- `
200
- ```
243
+ ### Smart Lists
201
244
 
202
- ### Smart Lists
245
+ ```typescript
246
+ const ids = [10, 20, 30]
247
+ await pool.query`
248
+ SELECT * FROM users WHERE id IN (${sql.array(ids)})
249
+ `
250
+ // SELECT * FROM users WHERE id IN ($1, $2, $3)
203
251
 
204
- ```typescript
205
- const ids = [10, 20, 30]
206
- await pool.query`
207
- SELECT * FROM users WHERE id IN (${sql.array(ids)})
208
- `
209
- // SELECT * FROM users WHERE id IN ($1, $2, $3)
210
-
211
- const conditions = [
212
- sql.fragment`status = ${'active'}`,
213
- sql.fragment`age > ${18}`
214
- ]
215
- await pool.query`
216
- SELECT * FROM users WHERE ${sql.array(conditions, ' AND ')}
217
- `
218
- ```
219
-
220
- ### Clean WHERE Clauses
221
-
222
- ```typescript
223
- const filters = { role: 'admin', age: undefined, active: true }
224
- await pool.query`
225
- SELECT * FROM users WHERE ${sql.where(filters)}
226
- `
227
- // SELECT * FROM users WHERE role = $1 AND active = $2
228
- ```
229
-
230
- ### Conditional Logic
231
-
232
- ```typescript
233
- const search = ""
234
- await pool.query`
235
- SELECT * FROM posts
236
- ${search ? sql.fragment`WHERE title ILIKE ${search}` : sql.empty}
237
- `
238
- ```
239
-
240
- ---
241
-
242
- ## 🛡️ Security
243
-
244
- | Pattern | Protection |
245
- |---------|------------|
246
- | `sql.ident(name)` | Escapes identifiers: `user` `"user"` |
247
- | `sql.literal(value)` | Escapes string literals |
248
- | Parameter binding | Uses native `$1, $2` placeholders |
249
- | Template tags | Cannot be injected via user input |
250
-
251
- ```typescript
252
- // Safe - parameterized
253
- await pool.query`SELECT * FROM users WHERE name = ${userInput}`
254
-
255
- // ⚠️ Unsafe - raw interpolation (DON'T DO THIS)
256
- await pool.query(`SELECT * FROM users WHERE name = '${userInput}'`)
257
-
258
- // ✅ Safe - identifiers
259
- await pool.query`SELECT * FROM ${sql.ident(tableName)}`
260
- ```
261
-
262
- ---
263
-
264
- ## 📊 Null & Undefined Handling
265
-
266
- | Value | In INSERT | In UPDATE | In VALUES | In Arrays |
267
- |-------|-----------|-----------|-----------|-----------|
268
- | `null` | `NULL` | `NULL` | `NULL` | `NULL` |
269
- | `undefined` | `DEFAULT` | Skipped | `Error` | `Error` |
270
-
271
-
272
- ```typescript
273
- // undefined becomes DEFAULT
274
- await pool.query`
275
- INSERT INTO users ${sql.insert({
276
- name: 'Alice',
277
- age: undefined, // → DEFAULT
278
- email: null // NULL
279
- })}
280
- `
281
- // INSERT INTO users (name, age, email) VALUES ($1, DEFAULT, $2)
282
-
283
- // undefined fields are skipped in UPDATE
284
- await pool.query`
285
- UPDATE users SET ${sql.update({
286
- name: 'Bob',
287
- age: undefined // Skipped - age remains unchanged
288
- })} WHERE id = 1
289
- `
290
- // UPDATE users SET name = $1 WHERE id = 1
291
- ```
292
-
293
- ---
294
-
295
- ## 🔧 API Reference
296
-
297
- ### Connection
298
- ```typescript
299
- class Connection {
300
- static new(params: ConnectionParams): Promise<Connection>
301
-
302
- query<T>(strings: TemplateStringsArray, ...values: any[]): Promise<T[], PostgresError>
303
- begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
304
- notify(channelName: string, payload?: string): Future<[], PostgresError>
305
- listen(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
306
- unlisten(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
307
-
308
- get isAlive(): boolean
309
- close(): void
310
- }
311
-
312
- interface ConnectionParams {
313
- user: string
314
- password?: string
315
- host: string
316
- port: number
317
- database: string
318
- logLevel?: 'none' | 'error' | 'notice' | 'query' // defaul: "error"
319
- }
320
- ```
321
-
322
- ### Pool
323
-
324
- ```typescript
325
- class Pool {
326
- constructor(config: PoolConfig)
327
-
328
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], Error>
329
- begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
330
- notify(channelName: string, payload?: string): Future<[], PostgresError>
331
- acquire(): Future<Connection, Error>
332
- release(conn: Connection): void
333
- close(): void
334
-
335
- get size(): number
336
- get total(): number
337
- }
338
-
339
- interface PoolConfig extends ConnectionParams {
340
- max?: number
341
- }
342
- ```
343
-
344
- ### Transaction
345
-
346
- ```typescript
347
- class Transaction {
348
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
349
- commit(): Future<[], PostgresError>
350
- rollback(): Future<[], PostgresError>
351
- savepoint<T>(name: string, callback: (tx: Transaction) => Promise<T>): Future<T, Error>
352
-
353
- get isActive(): boolean
354
- }
355
- ```
356
-
357
- ### **sql** helper
358
- ```typescript
359
- const sql: {
360
- ident<T extends string>(identificator: T): IdentifierClause<T>
361
- literal<T extends string>(value: T): LiteralClause<T>
362
- fragment(strings: TemplateStringsArray, ...values: any[]): FragmentClause
363
- insert<T extends Record<string, any>>(...objects: NoInfer<T>[]): InsertClause<T>
364
- update<T extends Record<string, any>>(object: T): UpdateClause<T>
365
- where<T extends Record<string, any>>(whereMap: T): WhereClause<T>
366
- excluded(fields: string[]): ExcludeUpdateClause
367
- array(array: any[], separator?: string): ArrayClause
368
- empty: EmptyClause
369
- }
370
- ```
371
-
372
- ---
373
-
374
- ## 📝 License
375
-
376
- MIT © [M2K-5F](https://github.com/M2K-5F)
377
-
378
- ---
379
-
380
- **Made with ❤️ and a bit of insanity**
252
+ const conditions = [
253
+ sql.fragment`status = ${'active'}`,
254
+ sql.fragment`age > ${18}`
255
+ ]
256
+ await pool.query`
257
+ SELECT * FROM users WHERE ${sql.array(conditions, ' AND ')}
258
+ `
259
+ ```
260
+
261
+ ### Clean WHERE Clauses
262
+
263
+ ```typescript
264
+ const filters = { role: 'admin', age: undefined, active: true }
265
+ await pool.query`
266
+ SELECT * FROM users WHERE ${sql.where(filters)}
267
+ `
268
+ // SELECT * FROM users WHERE role = $1 AND active = $2
269
+ ```
270
+
271
+ ### Conditional Logic
272
+
273
+ ```typescript
274
+ const search = ""
275
+ await pool.query`
276
+ SELECT * FROM posts
277
+ ${search ? sql.fragment`WHERE title ILIKE ${search}` : sql.empty}
278
+ `
279
+ ```
280
+
281
+ ---
282
+
283
+ ## 🛡️ Security
284
+
285
+ | Pattern | Protection |
286
+ |---------|------------|
287
+ | `sql.ident(name)` | Escapes identifiers: `user` → `"user"` |
288
+ | `sql.literal(value)` | Escapes string literals |
289
+ | Parameter binding | Uses native `$1, $2` placeholders |
290
+ | Template tags | Cannot be injected via user input |
291
+
292
+ ```typescript
293
+ // ✅ Safe - parameterized
294
+ await pool.query`SELECT * FROM users WHERE name = ${userInput}`
295
+
296
+ // ⚠️ Unsafe - raw interpolation (DON'T DO THIS)
297
+ await pool.query(`SELECT * FROM users WHERE name = '${userInput}'`)
298
+
299
+ // ✅ Safe - identifiers
300
+ await pool.query`SELECT * FROM ${sql.ident(tableName)}`
301
+ ```
302
+
303
+ ---
304
+
305
+ ## 📊 Null & Undefined Handling
306
+
307
+ | Value | In INSERT | In UPDATE | In VALUES | In Arrays |
308
+ |-------|-----------|-----------|-----------|-----------|
309
+ | `null` | `NULL` | `NULL` | `NULL` | `NULL` |
310
+ | `undefined` | `DEFAULT` | Skipped | `Error` | `Error` |
311
+
312
+
313
+ ```typescript
314
+ // undefined becomes DEFAULT
315
+ await pool.query`
316
+ INSERT INTO users ${sql.insert({
317
+ name: 'Alice',
318
+ age: undefined, // → DEFAULT
319
+ email: null // → NULL
320
+ })}
321
+ `
322
+ // INSERT INTO users (name, age, email) VALUES ($1, DEFAULT, $2)
323
+
324
+ // undefined fields are skipped in UPDATE
325
+ await pool.query`
326
+ UPDATE users SET ${sql.update({
327
+ name: 'Bob',
328
+ age: undefined // Skipped - age remains unchanged
329
+ })} WHERE id = 1
330
+ `
331
+ // UPDATE users SET name = $1 WHERE id = 1
332
+ ```
333
+
334
+ ---
335
+
336
+ ## 🔧 API Reference
337
+
338
+ ### Connection
339
+ ```typescript
340
+ class Connection {
341
+ static new(params: ConnectionParams): Promise<Connection>
342
+
343
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
344
+ begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
345
+ notify(channelName: string, payload?: string): Future<[], PostgresError>
346
+ listen(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
347
+ unlisten(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
348
+
349
+ get isAlive(): boolean
350
+ close(): void
351
+ }
352
+
353
+ interface ConnectionParams {
354
+ user: string
355
+ password?: string
356
+ host: string
357
+ port: number
358
+ database: string
359
+ logLevel?: 'none' | 'error' | 'notice' | 'query' // defaul: "error"
360
+ }
361
+ ```
362
+
363
+ ### Pool
364
+
365
+ ```typescript
366
+ class Pool {
367
+ constructor(config: PoolConfig)
368
+
369
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], Error>
370
+ begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
371
+ notify(channelName: string, payload?: string): Future<[], PostgresError>
372
+ acquire(): Future<Connection, Error>
373
+ release(conn: Connection): void
374
+ close(): void
375
+
376
+ get size(): number
377
+ get total(): number
378
+ }
379
+
380
+ interface PoolConfig extends ConnectionParams {
381
+ max?: number
382
+ }
383
+ ```
384
+
385
+ ### Transaction
386
+
387
+ ```typescript
388
+ class Transaction {
389
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
390
+ commit(): Future<[], PostgresError>
391
+ rollback(): Future<[], PostgresError>
392
+ savepoint<T>(name: string, callback: (tx: Transaction) => Promise<T>): Future<T, Error>
393
+
394
+ get isActive(): boolean
395
+ }
396
+ ```
397
+
398
+ ### **sql** helper
399
+ ```typescript
400
+ const sql: {
401
+ ident<T extends string>(identificator: T): IdentifierClause<T>
402
+ literal<T extends string>(value: T): LiteralClause<T>
403
+ fragment(strings: TemplateStringsArray, ...values: any[]): FragmentClause
404
+ insert<T extends Record<string, any>>(...objects: NoInfer<T>[]): InsertClause<T>
405
+ update<T extends Record<string, any>>(object: T): UpdateClause<T>
406
+ where<T extends Record<string, any>>(whereMap: T): WhereClause<T>
407
+ excluded(fields: string[]): ExcludeUpdateClause
408
+ array(array: any[], separator?: string): ArrayClause
409
+ empty: EmptyClause
410
+ }
411
+ ```
412
+
413
+ ---
414
+
415
+
416
+ Pgtx is a PostgreSQL driver.
417
+
418
+ It is not an ORM. It is absolutely Blazing.
419
+
420
+ ## 📝 License
421
+
422
+ MIT © [M2K-5F](https://github.com/M2K-5F)
423
+
424
+ ---
425
+
426
+ **Made with ❤️ and a bit of insanity**