@m2k-5f/pgtx 2.2.0 → 2.3.1

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.
Files changed (40) hide show
  1. package/README.md +372 -307
  2. package/dist/clauses/abstract.clause.js +1 -5
  3. package/dist/clauses/array.clause.js +3 -7
  4. package/dist/clauses/empty.clause.js +3 -7
  5. package/dist/clauses/exclude.clause.js +2 -6
  6. package/dist/clauses/fragment.clause.js +4 -8
  7. package/dist/clauses/iden.caluse.js +2 -6
  8. package/dist/clauses/index.js +9 -25
  9. package/dist/clauses/insert.clause.js +2 -6
  10. package/dist/clauses/literal.clause.js +2 -6
  11. package/dist/clauses/update.clause.js +2 -6
  12. package/dist/clauses/where.clause.js +2 -6
  13. package/dist/connection.d.ts +11 -7
  14. package/dist/connection.d.ts.map +1 -1
  15. package/dist/connection.js +108 -111
  16. package/dist/error.d.ts +1 -1
  17. package/dist/error.d.ts.map +1 -1
  18. package/dist/error.js +2 -6
  19. package/dist/index.js +24 -37
  20. package/dist/pool.d.ts +9 -7
  21. package/dist/pool.d.ts.map +1 -1
  22. package/dist/pool.js +29 -42
  23. package/dist/protocol/connection-request-writer.js +13 -18
  24. package/dist/protocol/connection-response-reader.js +4 -9
  25. package/dist/protocol/constants.js +4 -7
  26. package/dist/protocol/socket-authorization.d.ts +5 -1
  27. package/dist/protocol/socket-authorization.d.ts.map +1 -1
  28. package/dist/protocol/socket-authorization.js +38 -34
  29. package/dist/protocol/socket-connector.js +3 -7
  30. package/dist/query.js +2 -6
  31. package/dist/queue.js +1 -5
  32. package/dist/security/md5.js +4 -8
  33. package/dist/security/sasl.js +7 -11
  34. package/dist/transaction.d.ts +7 -5
  35. package/dist/transaction.d.ts.map +1 -1
  36. package/dist/transaction.js +18 -26
  37. package/dist/types.js +0 -2
  38. package/dist/utils/template-compiler.js +3 -6
  39. package/dist/utils/value-parser.js +2 -7
  40. package/package.json +9 -2
package/README.md CHANGED
@@ -1,361 +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
 
100
- ## 📖 Features
94
+ ```typescript
95
+ import { sql, Pool } from "@m2k-5f/pgtx";
101
96
 
102
- ### Transactions & Savepoints
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
+ `
103
111
 
104
- ```typescript
105
- await pool.begin(async (tx) => {
106
- await tx.query`INSERT INTO orders (user_id) VALUES (${userId})`
107
-
108
- // err: Error | null
109
- const err = await tx.savepoint('update_stock', async (stx) => {
110
- await stx.query`UPDATE stock SET count = count - 1 WHERE product_id = ${productId}`
111
- if (outOfStock) throw new Error() // Only savepoint rolls back
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}`
112
116
  })
113
- })
114
- ```
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:
125
+
126
+ - **Typed errors** — `PostgresError` with `code`, `severity`, `detail`
127
+ - **Declarative recovery** — `.recover()`, `.recoverIf()` instead of try/catch
128
+ - **Chain composition** — `.andThen()`, `.orElse()`, `.tap()`, `.tapErr()`
129
+
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
+ ```
136
+
137
+ ---
138
+
139
+
140
+ ## 📖 Features
141
+
142
+
143
+ ### Pipeline by Default
144
+
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
173
+ })
174
+ ```
175
+
115
176
 
177
+ ### Async Notifications (LISTEN / NOTIFY)
116
178
 
117
- ### Async Notifications (LISTEN / NOTIFY)
179
+ Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting multiplexed query pipeline.
118
180
 
119
- Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting multiplexed query pipeline.
181
+ ```typescript
182
+ // 1. Sending a notification
183
+ await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
120
184
 
121
- ```typescript
122
- // 1. Sending a notification
123
- await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
185
+ // 2. Receiving notifications (Requires a dedicated connection from the pool)
186
+ const conn = await pool.acquire()
124
187
 
125
- // 2. Receiving notifications (Requires a dedicated connection from the pool)
126
- const conn = await pool.acquire()
188
+ const onEvent = (payload: string) => {
189
+ console.log(`Received payload: ${payload}`)
190
+ }
127
191
 
128
- const onEvent = (payload: string) => {
129
- console.log(`Received payload: ${payload}`)
130
- }
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))
131
195
 
132
- // Multiplexes multiple callbacks onto a single LISTEN command seamlessly
133
- await conn.listen('user_events', onEvent)
134
- await conn.listen('user_events', (data) => logToFile(data))
196
+ // Clean up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
197
+ await conn.unlisten('user_events', onEvent)
135
198
 
136
- // Clean up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
137
- await conn.unlisten('user_events', onEvent)
199
+ // Keep the connection active as long as you need notifications!
200
+ // Do NOT release it back to the pool prematurely.
201
+ ```
138
202
 
139
- // Keep the connection active as long as you need notifications!
140
- // Do NOT release it back to the pool prematurely.
141
- ```
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()`.
142
204
 
143
- > ⚠️ **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()`.
144
205
 
206
+ ### Bulk Inserts
145
207
 
146
- ### Bulk Inserts
208
+ ```typescript
209
+ const users = [
210
+ { name: 'Alice', email: 'alice@test.com' },
211
+ { name: 'Bob', email: 'bob@test.com' }
212
+ ]
147
213
 
148
- ```typescript
149
- const users = [
150
- { name: 'Alice', email: 'alice@test.com' },
151
- { name: 'Bob', email: 'bob@test.com' }
152
- ]
214
+ await pool.query`
215
+ INSERT INTO users ${sql.insert(users)}
216
+ `
217
+ // INSERT INTO users (name, email) VALUES ($1, $2), ($3, $4)
218
+ ```
153
219
 
154
- await pool.query`
155
- INSERT INTO users ${sql.insert(users)}
156
- `
157
- // INSERT INTO users (name, email) VALUES ($1, $2), ($3, $4)
158
- ```
220
+ ### Dynamic Updates
159
221
 
160
- ### Dynamic Updates
222
+ ```typescript
223
+ const data = { status: 'active', last_login: new Date() }
161
224
 
162
- ```typescript
163
- const data = { status: 'active', last_login: new Date() }
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
+ ```
164
230
 
165
- await pool.query`
166
- UPDATE users SET ${sql.update(data)} WHERE id = ${userId}
167
- `
168
- // UPDATE users SET status = $1, last_login = $2 WHERE id = $3
169
- ```
231
+ ### Recursive Fragments
170
232
 
171
- ### Recursive Fragments
172
-
173
- ```typescript
174
- const filter = sql.fragment`status = ${'active'} AND age > ${21}`
175
- const subquery = sql.fragment`(SELECT id FROM roles WHERE name = ${'admin'})`
233
+ ```typescript
234
+ const filter = sql.fragment`status = ${'active'} AND age > ${21}`
235
+ const subquery = sql.fragment`(SELECT id FROM roles WHERE name = ${'admin'})`
176
236
 
177
- await pool.query`
178
- SELECT * FROM users
179
- WHERE ${filter} AND role_id = (${subquery})
180
- `
181
- ```
237
+ await pool.query`
238
+ SELECT * FROM users
239
+ WHERE ${filter} AND role_id = (${subquery})
240
+ `
241
+ ```
182
242
 
183
- ### Smart Lists
243
+ ### Smart Lists
184
244
 
185
- ```typescript
186
- const ids = [10, 20, 30]
187
- await pool.query`
188
- SELECT * FROM users WHERE id IN (${sql.array(ids)})
189
- `
190
- // SELECT * FROM users WHERE id IN ($1, $2, $3)
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)
191
251
 
192
- const conditions = [
193
- sql.fragment`status = ${'active'}`,
194
- sql.fragment`age > ${18}`
195
- ]
196
- await pool.query`
197
- SELECT * FROM users WHERE ${sql.array(conditions, ' AND ')}
198
- `
199
- ```
200
-
201
- ### Clean WHERE Clauses
202
-
203
- ```typescript
204
- const filters = { role: 'admin', age: undefined, active: true }
205
- await pool.query`
206
- SELECT * FROM users WHERE ${sql.where(filters)}
207
- `
208
- // SELECT * FROM users WHERE role = $1 AND active = $2
209
- ```
210
-
211
- ### Conditional Logic
212
-
213
- ```typescript
214
- const search = ""
215
- await pool.query`
216
- SELECT * FROM posts
217
- ${search ? sql.fragment`WHERE title ILIKE ${search}` : sql.empty}
218
- `
219
- ```
220
-
221
- ---
222
-
223
- ## 🛡️ Security
224
-
225
- | Pattern | Protection |
226
- |---------|------------|
227
- | `sql.ident(name)` | Escapes identifiers: `user` → `"user"` |
228
- | `sql.literal(value)` | Escapes string literals |
229
- | Parameter binding | Uses native `$1, $2` placeholders |
230
- | Template tags | Cannot be injected via user input |
231
-
232
- ```typescript
233
- // ✅ Safe - parameterized
234
- await pool.query`SELECT * FROM users WHERE name = ${userInput}`
235
-
236
- // ⚠️ Unsafe - raw interpolation (DON'T DO THIS)
237
- await pool.query(`SELECT * FROM users WHERE name = '${userInput}'`)
238
-
239
- // ✅ Safe - identifiers
240
- await pool.query`SELECT * FROM ${sql.ident(tableName)}`
241
- ```
242
-
243
- ---
244
-
245
- ## 📊 Null & Undefined Handling
246
-
247
- | Value | In INSERT | In UPDATE | In VALUES | In Arrays |
248
- |-------|-----------|-----------|-----------|-----------|
249
- | `null` | `NULL` | `NULL` | `NULL` | `NULL` |
250
- | `undefined` | `DEFAULT` | Skipped | `Error` | `Error` |
251
-
252
-
253
- ```typescript
254
- // undefined becomes DEFAULT
255
- await pool.query`
256
- INSERT INTO users ${sql.insert({
257
- name: 'Alice',
258
- age: undefined, // → DEFAULT
259
- email: null // → NULL
260
- })}
261
- `
262
- // INSERT INTO users (name, age, email) VALUES ($1, DEFAULT, $2)
263
-
264
- // undefined fields are skipped in UPDATE
265
- await pool.query`
266
- UPDATE users SET ${sql.update({
267
- name: 'Bob',
268
- age: undefined // Skipped - age remains unchanged
269
- })} WHERE id = 1
270
- `
271
- // UPDATE users SET name = $1 WHERE id = 1
272
- ```
273
-
274
- ---
275
-
276
- ## 🔧 API Reference
277
-
278
- ### Connection
279
- ```typescript
280
- class Connection {
281
- static new(params: ConnectionParams): Promise<Connection>
282
-
283
- query<T>(strings: TemplateStringsArray, ...values: any[]): Promise<T[]>
284
- begin<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>
285
- notify(channelName: string, payload?: string): Promise<[]>
286
- listen(channelName: string, callback: (payload: string) => void): Promise<[]>
287
- unlisten(channelName: string, callback: (payload: string) => void): Promise<[]>
288
-
289
- get isAlive(): boolean
290
- close(): void
291
- }
292
-
293
- interface ConnectionParams {
294
- user: string
295
- password?: string
296
- host: string
297
- port: number
298
- database: string
299
- logLevel?: 'none' | 'error' | 'notice' | 'query' // defaul: "error"
300
- }
301
- ```
302
-
303
- ### Pool
304
-
305
- ```typescript
306
- class Pool {
307
- constructor(config: PoolConfig)
308
-
309
- query<T>(strings: TemplateStringsArray, ...values: any[]): Promise<T[]>
310
- begin<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>
311
- notify(channelName: string, payload?: string): Promise<[]>
312
- acquire(): Promise<Connection>
313
- release(conn: Connection): void
314
- close(): Promise<void>
315
-
316
- get size(): number
317
- get total(): number
318
- }
319
-
320
- interface PoolConfig extends ConnectionParams {
321
- max?: number
322
- }
323
- ```
324
-
325
- ### Transaction
326
-
327
- ```typescript
328
- class Transaction {
329
- query<T>(strings: TemplateStringsArray, ...values: any[]): Promise<T[]>
330
- commit(): Promise<void>
331
- rollback(): Promise<void>
332
- savepoint<T>(name: string, callback: (tx: Transaction) => Promise<T>): Promise<T>
333
-
334
- get isActive(): boolean
335
- }
336
- ```
337
-
338
- ### **sql** helper
339
- ```typescript
340
- const sql: {
341
- ident<T extends string>(identificator: T): IdentifierClause<T>
342
- literal<T extends string>(value: T): LiteralClause<T>
343
- fragment(strings: TemplateStringsArray, ...values: any[]): FragmentClause
344
- insert<T extends Record<string, any>>(...objects: NoInfer<T>[]): InsertClause<T>
345
- update<T extends Record<string, any>>(object: T): UpdateClause<T>
346
- where<T extends Record<string, any>>(whereMap: T): WhereClause<T>
347
- excluded(fields: string[]): ExcludeUpdateClause
348
- array(array: any[], separator?: string): ArrayClause
349
- empty: EmptyClause
350
- }
351
- ```
352
-
353
- ---
354
-
355
- ## 📝 License
356
-
357
- MIT © [M2K-5F](https://github.com/M2K-5F)
358
-
359
- ---
360
-
361
- **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**