@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.
- package/README.md +372 -307
- package/dist/clauses/abstract.clause.js +1 -5
- package/dist/clauses/array.clause.js +3 -7
- package/dist/clauses/empty.clause.js +3 -7
- package/dist/clauses/exclude.clause.js +2 -6
- package/dist/clauses/fragment.clause.js +4 -8
- package/dist/clauses/iden.caluse.js +2 -6
- package/dist/clauses/index.js +9 -25
- package/dist/clauses/insert.clause.js +2 -6
- package/dist/clauses/literal.clause.js +2 -6
- package/dist/clauses/update.clause.js +2 -6
- package/dist/clauses/where.clause.js +2 -6
- package/dist/connection.d.ts +11 -7
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +108 -111
- package/dist/error.d.ts +1 -1
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +2 -6
- package/dist/index.js +24 -37
- package/dist/pool.d.ts +9 -7
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +29 -42
- package/dist/protocol/connection-request-writer.js +13 -18
- package/dist/protocol/connection-response-reader.js +4 -9
- package/dist/protocol/constants.js +4 -7
- package/dist/protocol/socket-authorization.d.ts +5 -1
- package/dist/protocol/socket-authorization.d.ts.map +1 -1
- package/dist/protocol/socket-authorization.js +38 -34
- package/dist/protocol/socket-connector.js +3 -7
- package/dist/query.js +2 -6
- package/dist/queue.js +1 -5
- package/dist/security/md5.js +4 -8
- package/dist/security/sasl.js +7 -11
- package/dist/transaction.d.ts +7 -5
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +18 -26
- package/dist/types.js +0 -2
- package/dist/utils/template-compiler.js +3 -6
- package/dist/utils/value-parser.js +2 -7
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -1,361 +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
|
|
|
100
|
-
|
|
94
|
+
```typescript
|
|
95
|
+
import { sql, Pool } from "@m2k-5f/pgtx";
|
|
101
96
|
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
await pool.begin(async (tx) => {
|
|
106
|
-
|
|
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
|
-
|
|
179
|
+
Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting multiplexed query pipeline.
|
|
118
180
|
|
|
119
|
-
|
|
181
|
+
```typescript
|
|
182
|
+
// 1. Sending a notification
|
|
183
|
+
await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
|
|
120
184
|
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
188
|
+
const onEvent = (payload: string) => {
|
|
189
|
+
console.log(`Received payload: ${payload}`)
|
|
190
|
+
}
|
|
127
191
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
//
|
|
133
|
-
await conn.
|
|
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
|
-
//
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
+
```typescript
|
|
209
|
+
const users = [
|
|
210
|
+
{ name: 'Alice', email: 'alice@test.com' },
|
|
211
|
+
{ name: 'Bob', email: 'bob@test.com' }
|
|
212
|
+
]
|
|
147
213
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
```typescript
|
|
223
|
+
const data = { status: 'active', last_login: new Date() }
|
|
161
224
|
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
]
|
|
196
|
-
await pool.query`
|
|
197
|
-
|
|
198
|
-
`
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Clean WHERE Clauses
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
const filters = { role: 'admin', age: undefined, active: true }
|
|
205
|
-
await pool.query`
|
|
206
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
interface ConnectionParams {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
### Pool
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
class Pool {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
interface PoolConfig extends ConnectionParams {
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### Transaction
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
class Transaction {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### **sql** helper
|
|
339
|
-
```typescript
|
|
340
|
-
const sql: {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
---
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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**
|