@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 +366 -320
- package/dist/connection.d.ts +1 -0
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +5 -3
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +6 -4
- package/dist/protocol/connection-request-writer.js +1 -1
- package/dist/protocol/connection-response-reader.d.ts +9 -1
- package/dist/protocol/connection-response-reader.d.ts.map +1 -1
- package/dist/protocol/connection-response-reader.js +146 -6
- package/dist/protocol/constants.d.ts +48 -0
- package/dist/protocol/constants.d.ts.map +1 -1
- package/dist/protocol/constants.js +47 -0
- package/dist/query.d.ts +2 -1
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +21 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,380 +1,426 @@
|
|
|
1
|
-
## 🚀 Pgtx
|
|
1
|
+
## 🚀 Pgtx
|
|
2
2
|
|
|
3
|
-
[](https://github.com/M2K-5F/pgtx/actions/workflows/tests.yaml)
|
|
4
|
-
[](https://www.npmjs.com/package/@m2k-5f/pgtx)
|
|
3
|
+
[](https://github.com/M2K-5F/pgtx/actions/workflows/tests.yaml)
|
|
4
|
+
[](https://www.npmjs.com/package/@m2k-5f/pgtx)
|
|
5
5
|
|
|
6
|
-
**Blazing-fast PostgreSQL driver
|
|
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
|
-
|
|
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
|
-
##
|
|
15
|
+
## 📦 Installation
|
|
23
16
|
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|------|-----|----------|-------------|
|
|
28
|
-
| **Pgtx** | **1584** | **0.631ms** | **20 pool connections (pipeline multiplexing)** |
|
|
29
|
-
| Native `pg` | 179 | 5.585ms | 20 pool connections |
|
|
24
|
+
---
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
## ✨ Features
|
|
32
27
|
|
|
33
|
-
**
|
|
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
|
-
|
|
40
|
+
## ⚡ Performance
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
### Stress test — 3000 concurrent `SELECT` queries
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
**Environment**
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
56
|
+
### Relative performance
|
|
55
57
|
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
host: 'localhost',
|
|
78
|
-
user: 'postgres',
|
|
79
|
-
password: 'postgres',
|
|
80
|
-
database: 'myapp'
|
|
81
|
-
})
|
|
73
|
+
---
|
|
82
74
|
|
|
83
|
-
|
|
84
|
-
const [user] = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
|
|
75
|
+
## 🔥 Why Pgtx?
|
|
85
76
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
143
|
+
### Pipeline by Default
|
|
122
144
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
```typescript
|
|
182
|
+
// 1. Sending a notification
|
|
183
|
+
await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
|
|
137
184
|
|
|
138
|
-
|
|
185
|
+
// 2. Receiving notifications (Requires a dedicated connection from the pool)
|
|
186
|
+
const conn = await pool.acquire()
|
|
139
187
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
188
|
+
const onEvent = (payload: string) => {
|
|
189
|
+
console.log(`Received payload: ${payload}`)
|
|
190
|
+
}
|
|
143
191
|
|
|
144
|
-
//
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
const users = [
|
|
169
|
-
{ name: 'Alice', email: 'alice@test.com' },
|
|
170
|
-
{ name: 'Bob', email: 'bob@test.com' }
|
|
171
|
-
]
|
|
220
|
+
### Dynamic Updates
|
|
172
221
|
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
const data = { status: 'active', last_login: new Date() }
|
|
231
|
+
### Recursive Fragments
|
|
183
232
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
237
|
+
await pool.query`
|
|
238
|
+
SELECT * FROM users
|
|
239
|
+
WHERE ${filter} AND role_id = (${subquery})
|
|
240
|
+
`
|
|
241
|
+
```
|
|
195
242
|
|
|
196
|
-
|
|
197
|
-
SELECT * FROM users
|
|
198
|
-
WHERE ${filter} AND role_id = (${subquery})
|
|
199
|
-
`
|
|
200
|
-
```
|
|
243
|
+
### Smart Lists
|
|
201
244
|
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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**
|