@m2k-5f/pgtx 2.6.0 → 2.6.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,544 +1,278 @@
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 for Node.js.**
6
+ A PostgreSQL driver for Node.js that pipelines everything by default, caches prepared statements without asking, and doesn't ship a single dependency.
7
7
 
8
- Pipeline execution, automatic prepared statements, typed SQL, transactions, and zero dependencies.
9
-
10
- **Up to 25% faster than `Postgres.js` and 15.6× faster than `pg` in concurrent pipeline workloads.**
11
-
12
- ---
13
-
14
- ## 📦 Installation
15
-
16
- ```bash
17
- npm install @m2k-5f/pgtx
18
- # yarn add @m2k-5f/pgtx
19
- # pnpm add @m2k-5f/pgtx
20
- # bun add @m2k-5f/pgtx
21
- ```
22
-
23
- ---
24
-
25
- ## 🚀 Quick Start
26
-
27
- ```typescript
28
- import { sql, Pool } from "@m2k-5f/pgtx";
29
-
30
- const pool = new Pool({
31
- host: 'localhost',
32
- user: 'postgres',
33
- password: 'postgres',
34
- database: 'myapp'
35
- })
36
-
37
- // Type-safe query
38
- const [user] = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
39
-
40
- // Bulk insert
41
- await pool.query`
42
- INSERT INTO users ${sql.insert([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }])}
43
- `
44
-
45
- // Transaction
46
- await pool.begin(async (tx) => {
47
- await tx.query`UPDATE accounts SET balance = balance - 100 WHERE id = ${1}`
48
- await tx.query`UPDATE accounts SET balance = balance + 100 WHERE id = ${2}`
49
- })
50
- ```
51
-
52
- ---
53
-
54
- ## ✨ Features
55
-
56
- - **Pipeline queries** — Automatic query multiplexing over PostgreSQL pipeline protocol
57
- - **Tagged templates** — Natural SQL with type safety
58
- - **Native Web Streams API** — Memory-efficient data streaming via `pool.stream()`
59
- - **Transactions & Savepoints** — Nested transactions with rollback
60
- - **Bulk inserts** — Auto-extract columns from objects
61
- - **Dynamic updates** — Generate SET clauses from objects
62
- - **Recursive fragments** — Compose SQL like Lego
63
- - **Prepared statements** — Automatic prepared statement caching
64
- - **Connection pool** — Auto-management connections with support for pipeline queries via the pool itself.
65
- - **Zero dependencies** — Lightweight and blazing
66
-
67
- ---
68
-
69
- ## ⚡ Performance
70
-
71
- All benchmarks are executed on **GitHub Actions** (Ubuntu, 2 vCPUs) and are fully reproducible. Benchmark sources are included in this repository.
72
-
73
- ### 1. PostgreSQL Pipeline Stress Test
74
-
75
- **3000 concurrent parameterized `SELECT` queries**
76
-
77
- * Connection pool: **10 connections**
78
- * Measured with **mitata**
79
-
80
- | Driver | Avg Time | Relative Speed | Memory (p75) |
81
- | :--------------------- | -----------: | -------------------: | -----------: |
82
- | **Pgtx (Pipeline)** | **24.18 ms** | **Baseline (1.00×)** | **≈2.5 MB** |
83
- | Postgres.js (Pipeline) | 83.36 ms | **3.45× slower** | ≈7.6 MB |
84
- | node-postgres (`pg`) | 377.95 ms | **15.63× slower** | ≈11.4 MB |
85
-
86
- ### 2. Real-World HTTP Throughput
87
-
88
- Simple `node:http` server serving a PostgreSQL-backed endpoint.
89
-
90
- Measured with:
8
+ It exists because `pg` leaves throughput on the table and `postgres.js`, while fast, still isn't fast enough once you actually saturate a connection pool. Pgtx is built around one idea: batch what can be batched, write to the socket once, and get out of the way.
91
9
 
92
10
  ```bash
93
- wrk -t2 -c<N> -d10s http://localhost:3000/users
11
+ npm install @m2k-5f/pgtx
94
12
  ```
95
13
 
96
- | Concurrent Connections | Pgtx | Postgres.js | Speedup |
97
- | ---------------------: | ---------------: | -----------: | --------: |
98
- | 50 | 5,272 req/s | 5,691 req/s | 0.93× |
99
- | 200 | **12,918 req/s** | 6,724 req/s | **1.92×** |
100
- | 1000 | **21,429 req/s** | 8,423 req/s | **2.54×** |
101
- | 10000 | **22,486 req/s** | 12,764 req/s | **1.76×** |
102
-
103
- ### Why is Pgtx fast?
104
-
105
- Pgtx is engineered for **throughput**, not for minimizing the latency of individual queries.
106
-
107
- Instead of optimizing a single request in isolation, Pgtx minimizes per-query overhead under sustained concurrent load by combining:
108
-
109
- * Pipeline query multiplexing
110
- * Synchronous PostgreSQL wire protocol encoding
111
- * Batched socket writes
112
- * Automatic prepared statement caching
113
- * Prepared statement deduplication
114
- * Row description caching
115
- * Binary protocol support
116
- * Zero-dependency implementation
117
-
118
- As concurrency increases, these optimizations significantly reduce protocol overhead, allowing Pgtx to scale more efficiently than traditional PostgreSQL drivers.
119
-
120
- In the `mitata` benchmark, Pgtx also demonstrated approximately **3× lower memory usage** than Postgres.js while processing the same workload, reducing allocation pressure and improving sustained throughput under heavy load.
121
-
122
- > **Blazing** isn't just a tagline — it's backed by reproducible benchmarks.
123
-
124
- ---
125
-
126
- ## 🔥 Why Pgtx?
127
-
128
- | Capability | **Pgtx** | **Postgres.js** | **pg** |
129
- | -------------------------------- | :------: | :-------------: | :----: |
130
- | Pipeline queries | ✅ | ✅ | ❌ |
131
- | Pipeline multiplexing in pool | ✅ | ❌ | ❌ |
132
- | Tagged template SQL | ✅ | ✅ | ❌ |
133
- | Automatic prepared statements | ✅ | ✅ | ✅ |
134
- | Prepared statement deduplication | ✅ | ❌ | ❌ |
135
- | Transactions | ✅ | ✅ | ✅ |
136
- | Savepoints | ✅ | ✅ | ❌ |
137
- | LISTEN / NOTIFY | ✅ | ✅ | ✅ |
138
- | Connection pool | ✅ | ✅ | ✅ |
139
- | Zero dependencies | ✅ | ✅ | ❌ |
140
-
141
- ---
142
-
143
-
144
- ## 📖 Features
145
-
146
- ### 🎯 Typed Error Handling
14
+ ## Thirty seconds
147
15
 
148
- Pgtx queries return `Future<T, PostgresError>` from [fluent-future](https://www.npmjs.com/package/fluent-future) instead of raw `Promise<T>`. This gives you:
16
+ ```typescript
17
+ import { sql, Pool } from "@m2k-5f/pgtx"
149
18
 
150
- - **Typed errors** `PostgresError` with `code`, `severity`, `detail`
151
- - **Declarative recovery** — `.recover()`, `.recoverIf()` instead of try/catch
152
- - **Chain composition** — `.andThen()`, `.orElse()`, `.tap()`, `.tapErr()`
19
+ const pool = new Pool({
20
+ host: 'localhost',
21
+ user: 'postgres',
22
+ password: 'postgres',
23
+ database: 'myapp'
24
+ })
153
25
 
154
- ```typescript
155
- const users = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
156
- .recoverIf(err => err.code === '42P01', []) // undefined_table → []
157
- .recoverIf(err => err.code === '23505', []) // unique_violation → []
158
- .tapErr(err => logger.error(err)) // log remaining errors
159
- ```
26
+ const [user] = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
160
27
 
161
- ---
28
+ // returns rows → query, doesn't → execute
29
+ await pool.execute`
30
+ INSERT INTO users ${sql.insert([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }])}
31
+ `
162
32
 
163
- ### Pipeline by Default
164
-
165
- `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:
166
-
167
- ```typescript
168
- // 5 queries, only 2 network round-trips
169
- const {user, posts, ...data} = await Bind({
170
- user: () => pool.query<User>`...`,
171
- config: () => pool.query<Config>`...`,
172
- announcements: () => pool.query<Announcement>`...`
173
- })
174
- .bind({
175
- posts: ({ user }) => pool.query<Post>`...`,
176
- notifications: ({ user }) => pool.query<Notif>`...`
177
- })
178
- ```
179
- > 🚀 Pgtx automatically groups concurrent queries into pipeline batches, reducing network overhead by up to 5x compared to sequential queries.
180
-
181
-
182
- ---
183
-
184
-
185
- ### High-Performance Data Streaming (`pool.stream`)
186
-
187
- For heavy database lookups (exporting millions of rows, bulk reports, or large analytical dumps), memory accumulation is the ultimate killer of backend stability. Storing rows in a standard JavaScript array causes massive heap pollution and triggers blocking Garbage Collection spikes.
188
-
189
- Pgtx solves this at the protocol level by introducing `pool.stream()`, which bypasses row aggregation entirely and pipes rows transitively directly into a native Web **`ReadableStream`**.
190
-
191
- #### 1. Ultra-Low Memory Row Iteration
192
- You can consume database rows sequentially using standard `for await...of` syntax. Rows are processed and evicted from memory the moment they arrive from the network socket buffer.
193
-
194
- ```typescript
195
- interface HeavyLog { id: number; data: string; timestamp: Date; }
196
-
197
- const logStream = pool.stream<HeavyLog>`
198
- SELECT id, data, timestamp FROM application_logs WHERE level = ${'error'}
199
- `;
200
-
201
- for await (const log of logStream) {
202
- // Each log object is parsed on-the-fly and processed instantly.
203
- // Zero rows are accumulated in the internal driver state!
204
- console.log(`[${log.timestamp.toISOString()}] ${log.data}`);
205
- }
206
- ```
33
+ await pool.begin(async tx => {
34
+ await tx.execute`UPDATE accounts SET balance = balance - 100 WHERE id = ${1}`
35
+ await tx.execute`UPDATE accounts SET balance = balance + 100 WHERE id = ${2}`
36
+ })
37
+ ```
207
38
 
208
- #### 2. Streaming Directly to HTTP Responses (`Bun.serve`)
209
- Since Pgtx implements the standardized Web Streams API, you can bridge your database query directly into an HTTP response body with absolutely zero intermediate buffers.
39
+ That's most of what you need to know to use it. The rest of this document is for when you want to know *why* it's fast, or you need one of the sharper tools.
210
40
 
211
- ```typescript
212
- import { Pool } from "@m2k-5f/pgtx";
41
+ ## Numbers, since it's the first thing everyone asks
213
42
 
214
- const pool = new Pool({ /* ... config ... */ });
43
+ Benchmarks run on GitHub Actions (Ubuntu, 2 vCPUs), reproducible, sources in this repo. Take CI numbers with the usual grain of salt — noisy neighbors and all that — but the gap is wide enough that it holds.
215
44
 
216
- export default {
217
- port: 3000,
218
- async fetch(request) {
219
- const url = new URL(request.url);
45
+ **3000 concurrent parameterized SELECTs, pool of 10, measured with mitata:**
220
46
 
221
- if (url.pathname === "/export/users") {
222
- // Synchronously returns a stream handle even if pool sockets are currently busy
223
- const userStream = pool.stream`SELECT id, email, profile_metadata FROM giant_user_table`;
47
+ | Driver | Avg time | Relative | Memory (p75) |
48
+ |---|---:|---:|---:|
49
+ | Pgtx | **24.18 ms** | 1.00× | ≈4.5 MB |
50
+ | Postgres.js | 83.36 ms | 3.45× slower | ≈7.6 MB |
51
+ | node-postgres (`pg`) | 377.95 ms | 15.63× slower | ≈11.4 MB |
224
52
 
225
- return new Response(userStream, {
226
- headers: {
227
- "Content-Type": "application/json",
228
- "Transfer-Encoding": "chunked",
229
- },
230
- });
231
- }
53
+ **Real HTTP throughput, `node:http` serving a PG-backed route, `wrk`:**
232
54
 
233
- return new Response("Not Found", { status: 404 });
234
- },
235
- };
236
- ```
55
+ | Concurrency | Pgtx | Postgres.js | Speedup |
56
+ |---:|---:|---:|---:|
57
+ | 50 | 5,272 req/s | 5,691 req/s | 0.93× |
58
+ | 200 | **12,918 req/s** | 6,724 req/s | 1.92× |
59
+ | 1000 | **21,429 req/s** | 8,423 req/s | 2.54× |
60
+ | 10000 | **22,486 req/s** | 12,764 req/s | 1.76× |
237
61
 
238
- ---
62
+ At low concurrency Pgtx is roughly a wash with Postgres.js — pipelining has nothing to multiplex yet. The gap opens up as soon as there's real contention, which is the only regime that matters in production.
239
63
 
240
- ### Transactions & Savepoints
64
+ **How:** everything you fire concurrently against the same connection gets folded into one pipelined write — Parse/Bind/Execute for every query in the batch goes out in a single `socket.write()`, and results get demuxed as they come back, in order, without buffering rows you haven't asked for yet. Prepared statements are cached and deduplicated automatically, row descriptions are cached alongside them, and the binary protocol skips text (de)serialization where it can. None of this requires you to change how you write queries.
241
65
 
242
- ```typescript
243
- await pool.begin(async (tx) => {
244
- await tx.query`INSERT INTO orders (user_id) VALUES (${userId})`
245
-
246
- await tx.savepoint('update_stock', async (stx) => {
247
- await stx.query`UPDATE stock SET count = count - 1 WHERE product_id = ${productId}`
248
- if (outOfStock) throw new Error('out of stock') // Only savepoint rolls back
249
- })
250
- .tapErr(console.log) // Error: out of stock
251
- })
252
- ```
66
+ ## The parts worth knowing about
253
67
 
254
- ---
68
+ ### Errors you can pattern-match on
255
69
 
256
- ### Async Notifications (LISTEN / NOTIFY)
70
+ Every call returns a `Future<T[], PostgresError>` from [fluent-future](https://www.npmjs.com/package/fluent-future) instead of a bare `Promise`. `await` still works exactly like you'd expect — but you also get typed errors and a way to handle them without a `try/catch` pyramid:
257
71
 
258
- Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting the multiplexed query pipeline. It offers two distinct ways to subscribe: high-level pool-driven subscriptions and low-level connection pinning.
72
+ ```typescript
73
+ const users = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
74
+ .recoverIf(err => err.code === '42P01', []) // undefined_table → []
75
+ .recoverIf(err => err.code === '23505', []) // unique_violation → []
76
+ .tapErr(err => logger.error(err))
77
+ ```
259
78
 
260
- #### 1. Sending a Notification
261
- Notifications are atomic and can be triggered directly from the `Pool` utilizing any available socket:
262
- ```typescript
263
- await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
264
- ```
79
+ ### Pipelining is automatic, not opt-in
265
80
 
266
- #### 2. High-Level Pool Subscription (Recommended)
267
- You can subscribe directly via the `Pool` instance. Pgtx will automatically borrow a dedicated connection from the pool, issue the `LISTEN` command, and seamlessly manage its lifecycle.
81
+ `Bind`/`.bind` from fluent-future group independent queries into the same pipeline batch for you:
268
82
 
269
- The method returns a lazy, async **unsubscribe function** that cleanly handles `UNLISTEN` and returns the connection to the pool when invoked.
83
+ ```typescript
84
+ // 5 queries, 2 round-trips
85
+ const { user, posts, ...data } = await Bind({
86
+ user: pool.query<User>`...`,
87
+ config: pool.query<Config>`...`,
88
+ announcements: pool.query<Announcement>`...`
89
+ }).bind({
90
+ posts: ({ user }) => pool.query<Post>`...`,
91
+ notifications: ({ user }) => pool.query<Notif>`...`
92
+ })
93
+ ```
270
94
 
271
- ```typescript
272
- const onEvent = (payload: string) => {
273
- console.log(`Received payload: ${payload}`)
274
- }
95
+ Anything you fire off in the same tick without awaiting in between ends up on the wire together.
275
96
 
276
- // Automatically borrows a connection and sets up the listener
277
- const unsubscribe = await pool.listen('user_events', onEvent)
97
+ ### Streaming that doesn't buffer
278
98
 
279
- // When the subscription is no longer needed (e.g., server shutdown):
280
- // It automatically sends UNLISTEN and releases the connection back to the pool!
281
- await unsubscribe()
282
- ```
99
+ `pool.stream()` pipes rows straight from the socket into a `ReadableStream`, no intermediate array, no GC spike from holding a million-row export in memory.
283
100
 
284
- #### 3. Low-Level Connection Subscription (Stateful)
285
- If you need complete control over a specific PostgreSQL backend process, you can acquire an explicit `Connection` instance. This allows you to multiplex multiple callbacks onto a single channel seamlessly.
101
+ ```typescript
102
+ for await (const log of pool.stream<Log>`SELECT * FROM application_logs WHERE level = ${'error'}`) {
103
+ console.log(log.timestamp, log.data)
104
+ }
105
+ ```
286
106
 
287
- ```typescript
288
- const conn = await pool.acquire()
107
+ It's a real Web Streams object, so it drops straight into an HTTP response body:
289
108
 
290
- const onEvent = (payload: string) => {
291
- console.log(`Received payload: ${payload}`)
109
+ ```typescript
110
+ export default {
111
+ async fetch(req) {
112
+ const stream = pool.stream`SELECT id, email FROM giant_user_table`
113
+ return new Response(stream, { headers: { "Content-Type": "application/json" } })
292
114
  }
115
+ }
116
+ ```
293
117
 
294
- // Multiplexes multiple callbacks onto a single LISTEN command seamlessly
295
- await conn.listen('user_events', onEvent)
296
- await conn.listen('user_events', (data) => logToFile(data))
297
-
298
- // Cleans up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
299
- await conn.unlisten('user_events', onEvent)
300
-
301
- // ⚠️ Manual lifecycle management is strictly required for this pattern!
302
- // Do NOT release it back to the pool until you are completely done listening.
303
- this.release(conn)
304
- ```
305
-
306
- > ⚠️ **Architecture Note:** While `pool.notify` is a fire-and-forget atomic command, subscription states (`LISTEN`/`UNLISTEN`) are strictly tied to specific PostgreSQL backend processes. Using the high-level `pool.listen()` is strongly recommended for application code, as it encapsulates socket management into an elegant, leak-proof callback boundary.
307
-
308
- ---
118
+ ### Transactions and savepoints
309
119
 
310
- ### Bulk Inserts
120
+ ```typescript
121
+ await pool.begin(async tx => {
122
+ await tx.execute`INSERT INTO orders (user_id) VALUES (${userId})`
311
123
 
312
- ```typescript
313
- const users = [
314
- { name: 'Alice', email: 'alice@test.com' },
315
- { name: 'Bob', email: 'bob@test.com' }
316
- ]
124
+ await tx.savepoint('reserve_stock', async stx => {
125
+ await stx.execute`UPDATE stock SET count = count - 1 WHERE product_id = ${productId}`
126
+ if (outOfStock) throw new Error('out of stock') // only the savepoint rolls back
127
+ }).tapErr(console.log)
128
+ })
129
+ ```
317
130
 
318
- await pool.query`
319
- INSERT INTO users ${sql.insert(users)}
320
- `
321
- // INSERT INTO users (name, email) VALUES ($1, $2), ($3, $4)
322
- ```
131
+ ### LISTEN / NOTIFY without babysitting a connection
323
132
 
324
- ---
133
+ ```typescript
134
+ await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
325
135
 
326
- ### Dynamic Updates
136
+ const unlisten = await pool.listen('user_events', payload => {
137
+ console.log('got:', payload)
138
+ })
327
139
 
328
- ```typescript
329
- const data = { status: 'active', last_login: new Date() }
140
+ // later
141
+ await unlisten() // sends UNLISTEN, hands the connection back
142
+ ```
330
143
 
331
- await pool.query`
332
- UPDATE users SET ${sql.update(data)} WHERE id = ${userId}
333
- `
334
- // UPDATE users SET status = $1, last_login = $2 WHERE id = $3
335
- ```
144
+ `pool.listen` borrows a dedicated connection and manages its lifecycle for you. If you need to multiplex several callbacks onto one channel on a connection you're pinning yourself, drop down to `conn.listen`/`conn.unlisten` directly — just don't release that connection back to the pool while you're still using it for that.
336
145
 
337
- ---
146
+ ### Building queries without string-gluing
338
147
 
339
- ### Recursive Fragments
148
+ ```typescript
149
+ // bulk insert — columns inferred from the object
150
+ await pool.execute`INSERT INTO users ${sql.insert(users)}`
340
151
 
341
- ```typescript
342
- const filter = sql.fragment`status = ${'active'} AND age > ${21}`
343
- const subquery = sql.fragment`(SELECT id FROM roles WHERE name = ${'admin'})`
152
+ // dynamic SET clause
153
+ await pool.execute`UPDATE users SET ${sql.update({ status: 'active', last_login: new Date() })} WHERE id = ${userId}`
344
154
 
345
- await pool.query`
346
- SELECT * FROM users
347
- WHERE ${filter} AND role_id = (${subquery})
348
- `
349
- ```
155
+ // composable fragments
156
+ const filter = sql.fragment`status = ${'active'} AND age > ${21}`
157
+ await pool.query`SELECT * FROM users WHERE ${filter}`
350
158
 
351
- ---
159
+ // clean WHERE from an object, undefined keys just drop out
160
+ await pool.query`SELECT * FROM users WHERE ${sql.where({ role: 'admin', age: undefined, active: true })}`
352
161
 
353
- ### Smart Lists
162
+ // conditional fragments
163
+ await pool.query`SELECT * FROM posts ${search ? sql.fragment`WHERE title ILIKE ${search}` : sql.empty}`
164
+ ```
354
165
 
355
- ```typescript
356
- const ids = [10, 20, 30]
357
- await pool.query`
358
- SELECT * FROM users WHERE id IN (${sql.array(ids)})
359
- `
360
- // SELECT * FROM users WHERE id IN ($1, $2, $3)
166
+ `undefined` means `DEFAULT` in an insert, means "skip this field" in an update, and throws if you try to hand it to `VALUES` or an array — it's meant to be a decision point, not a silent `NULL`.
361
167
 
362
- const conditions = [
363
- sql.fragment`status = ${'active'}`,
364
- sql.fragment`age > ${18}`
365
- ]
366
- await pool.query`
367
- SELECT * FROM users WHERE ${sql.array(conditions, ' AND ')}
368
- `
369
- ```
370
-
371
- ---
372
-
373
- ### Clean WHERE Clauses
374
-
375
- ```typescript
376
- const filters = { role: 'admin', age: undefined, active: true }
377
- await pool.query`
378
- SELECT * FROM users WHERE ${sql.where(filters)}
379
- `
380
- // SELECT * FROM users WHERE role = $1 AND active = $2
381
- ```
382
-
383
- ---
384
-
385
- ### Conditional Logic
386
-
387
- ```typescript
388
- const search = ""
389
- await pool.query`
390
- SELECT * FROM posts
391
- ${search ? sql.fragment`WHERE title ILIKE ${search}` : sql.empty}
392
- `
393
- ```
394
-
395
- ---
396
-
397
- ## 🛡️ Security
398
-
399
- | Pattern | Protection |
400
- |---------|------------|
401
- | `sql.ident(name)` | Escapes identifiers: `user` → `"user"` |
402
- | `sql.literal(value)` | Escapes string literals |
403
- | Parameter binding | Uses native `$1, $2` placeholders |
404
- | Template tags | Cannot be injected via user input |
405
-
406
- ```typescript
407
- // ✅ Safe - parameterized
408
- await pool.query`SELECT * FROM users WHERE name = ${userInput}`
409
-
410
- // ⚠️ Unsafe - raw interpolation (DON'T DO THIS)
411
- await pool.query(`SELECT * FROM users WHERE name = '${userInput}'`)
412
-
413
- // ✅ Safe - identifiers
414
- await pool.query`SELECT * FROM ${sql.ident(tableName)}`
415
- ```
416
-
417
- ---
418
-
419
- ## 📊 Null & Undefined Handling
420
-
421
- | Value | In INSERT | In UPDATE | In VALUES | In Arrays |
422
- |-------|-----------|-----------|-----------|-----------|
423
- | `null` | `NULL` | `NULL` | `NULL` | `NULL` |
424
- | `undefined` | `DEFAULT` | Skipped | `Error` | `Error` |
425
-
426
-
427
- ```typescript
428
- // undefined becomes DEFAULT
429
- await pool.query`
430
- INSERT INTO users ${sql.insert({
431
- name: 'Alice',
432
- age: undefined, // → DEFAULT
433
- email: null // → NULL
434
- })}
435
- `
436
- // INSERT INTO users (name, age, email) VALUES ($1, DEFAULT, $2)
437
-
438
- // undefined fields are skipped in UPDATE
439
- await pool.query`
440
- UPDATE users SET ${sql.update({
441
- name: 'Bob',
442
- age: undefined // Skipped - age remains unchanged
443
- })} WHERE id = 1
444
- `
445
- // UPDATE users SET name = $1 WHERE id = 1
446
- ```
447
-
448
- ---
449
-
450
- ## 🔧 API Reference
451
-
452
- ### Connection
453
- ```typescript
454
- class Connection {
455
- static new(params: ConnectionParams): Promise<Connection>
456
-
457
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
458
- begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
459
- notify(channelName: string, payload?: string): Future<[], PostgresError>
460
- listen(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
461
- unlisten(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
462
- stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...params: any[]): ReadableStream<T>
463
- get isAlive(): boolean
464
- close(): void
465
- }
168
+ ## Not doing this
466
169
 
467
- interface ConnectionParams {
468
- user: string
469
- password?: string
470
- host: string
471
- port: number
472
- database: string
473
- queryTimeout?: number // default: 30 srconds
474
- logLevel?: 'none' | 'error' | 'notice' | 'query' // default: "error"
475
- }
476
- ```
477
-
478
- ### Pool
479
-
480
- ```typescript
481
- class Pool {
482
- constructor(config: PoolConfig)
483
-
484
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
485
- begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
486
- notify(channelName: string, payload?: string): Future<[], PostgresError>
487
- listen(channel: string, callback: (payload: string) => void): Future<() => Promise<void>, PostgresError>
488
- stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...args: any[]): ReadableStream<T>
489
- withAcquire<T>(fn: (conn: Connection) => Promise<T>): Future<T, Error>
490
- acquire(): Future<Connection, PostgresError>
491
- release(conn: Connection): void
492
- close(): void
493
-
494
- get size(): number
495
- get total(): number
496
- }
170
+ ```typescript
171
+ // don't
172
+ await pool.query(`SELECT * FROM users WHERE name = '${userInput}'`)
497
173
 
498
- interface PoolConfig extends ConnectionParams {
499
- max?: number
500
- }
501
- ```
174
+ // do
175
+ await pool.query`SELECT * FROM users WHERE name = ${userInput}`
176
+ await pool.query`SELECT * FROM ${sql.ident(tableName)}`
177
+ ```
502
178
 
503
- ### Transaction
179
+ Everything that goes through a tagged template is bound as `$1, $2, ...`. There's no code path where a template value becomes raw SQL text — if you need a dynamic identifier or literal, `sql.ident`/`sql.literal` exist precisely so you're never tempted to interpolate by hand.
180
+
181
+ ## API
182
+
183
+ ### `Connection`
184
+
185
+ ```typescript
186
+ class Connection {
187
+ static new(config: ConnectionPartialConfig): Future<Connection, PostgresError>
188
+
189
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
190
+ execute(strings: TemplateStringsArray, ...values: any[]): Future<void, PostgresError>
191
+ stream<T>(strings: TemplateStringsArray, ...values: any[]): ReadableStream<T>
192
+ begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
193
+ notify(channelName: string, payload?: string): Future<void, PostgresError>
194
+ listen(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>
195
+ unlisten(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>
196
+ close(): Future<void, PostgresError>
197
+
198
+ get isOpened(): boolean
199
+ get isClosed(): boolean
200
+ }
201
+
202
+ interface ConnectionPartialConfig {
203
+ user: string
204
+ password?: string
205
+ host: string
206
+ port: number
207
+ database: string
208
+ logLevel?: 'error' | 'notice' | 'query' // default 'error'
209
+ int8toBigint?: boolean // default false
210
+ queryTimeout?: number // default 30000
211
+ syncShedule?: 'beforeMicrotask' | 'afterMicrotask' | 'Immediate' // default 'afterMicrotask'
212
+ }
213
+ ```
504
214
 
505
- ```typescript
506
- class Transaction {
507
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
508
- commit(): Future<[], PostgresError>
509
- rollback(): Future<[], PostgresError>
510
- savepoint<T>(name: string, callback: (tx: Transaction) => Promise<T>): Future<T, Error>
215
+ ### `Pool`
216
+
217
+ ```typescript
218
+ class Pool {
219
+ constructor(config: PoolPartialConfig)
220
+
221
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
222
+ execute(strings: TemplateStringsArray, ...values: any[]): Future<void, PostgresError>
223
+ stream<T>(strings: TemplateStringsArray, ...values: any[]): ReadableStream<T>
224
+ begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
225
+ notify(channelName: string, payload?: string): Future<void, PostgresError>
226
+ listen(channel: string, callback: (payload: string) => void): Future<() => Future<void, PostgresError>, PostgresError>
227
+ withAcquire<T>(fn: (conn: Connection) => Promise<T>): Future<T, unknown>
228
+ acquire(): Future<Connection, PostgresError>
229
+ release(conn: Connection): void
230
+ close(): Future<void, PostgresError>
231
+
232
+ get size(): number
233
+ get total(): number
234
+ }
235
+
236
+ interface PoolPartialConfig extends ConnectionPartialConfig {
237
+ max?: number // default 20
238
+ }
239
+ ```
511
240
 
512
- get isActive(): boolean
513
- }
514
- ```
515
-
516
- ### **sql** helper
517
- ```typescript
518
- const sql: {
519
- ident<T extends string>(identificator: T): IdentifierClause<T>
520
- literal<T extends string>(value: T): LiteralClause<T>
521
- fragment(strings: TemplateStringsArray, ...values: any[]): FragmentClause
522
- insert<T extends Record<string, any>>(...objects: NoInfer<T>[]): InsertClause<T>
523
- update<T extends Record<string, any>>(object: T): UpdateClause<T>
524
- where<T extends Record<string, any>>(whereMap: T): WhereClause<T>
525
- excluded(fields: string[]): ExcludeUpdateClause
526
- array(array: any[], separator?: string): ArrayClause
527
- empty: EmptyClause
528
- }
529
- ```
241
+ ### `Transaction`
530
242
 
531
- ---
243
+ ```typescript
244
+ class Transaction {
245
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
246
+ commit(): Future<void, PostgresError>
247
+ rollback(): Future<void, PostgresError>
248
+ savepoint<T>(name: string, callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
532
249
 
250
+ get isActive(): boolean
251
+ }
252
+ ```
533
253
 
534
- Pgtx is a PostgreSQL driver.
254
+ ### `sql`
255
+
256
+ ```typescript
257
+ const sql: {
258
+ ident<T extends string>(name: T): IdentifierClause<T>
259
+ literal<T extends string>(value: T): LiteralClause<T>
260
+ fragment(strings: TemplateStringsArray, ...values: any[]): FragmentClause
261
+ insert<T extends Record<string, any>>(...objects: T[]): InsertClause<T>
262
+ update<T extends Record<string, any>>(object: T): UpdateClause<T>
263
+ where<T extends Record<string, any>>(map: T): WhereClause<T>
264
+ excluded(fields: string[]): ExcludeUpdateClause
265
+ array(values: any[], separator?: string): ArrayClause
266
+ empty: EmptyClause
267
+ }
268
+ ```
535
269
 
536
- It is not an ORM. It is absolutely Blazing.
270
+ ## What this isn't
537
271
 
538
- ## 📝 License
272
+ Not an ORM. No migrations, no model layer, no query builder that hides SQL from you. You write SQL, Pgtx gets it to Postgres as fast as it can and gets the rows back to you with as little overhead as possible. If you want an ORM on top, Pgtx is a fine thing to put underneath one.
539
273
 
540
- MIT © [M2K-5F](https://github.com/M2K-5F)
274
+ ## License
541
275
 
542
- ---
276
+ MIT © [M2K-5F](https://github.com/M2K-5F)
543
277
 
544
- **Made with ❤️ and a bit of insanity**
278
+ **Made with ❤️ and a bit of insanity**