@m2k-5f/pgtx 2.6.11 → 2.7.0

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,528 +1,287 @@
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.
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.
9
9
 
10
- **Up to 25% faster than `Postgres.js` and 15.6× faster than `pg` in concurrent pipeline workloads.**
10
+ ```bash
11
+ npm install @m2k-5f/pgtx
12
+ ```
11
13
 
12
- ---
14
+ ## Thirty seconds
13
15
 
14
- ## 📦 Installation
16
+ ```typescript
17
+ import { sql, Pool } from "@m2k-5f/pgtx"
15
18
 
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
- ```
19
+ const pool = new Pool({
20
+ host: 'localhost',
21
+ user: 'postgres',
22
+ password: 'postgres',
23
+ database: 'myapp'
24
+ })
22
25
 
23
- ---
24
-
25
- ## 🚀 Quick Start
26
+ const [user] = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
26
27
 
27
- ```typescript
28
- import { sql, Pool } from "@m2k-5f/pgtx";
28
+ await pool.execute`
29
+ INSERT INTO users ${sql.insert([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }])}
30
+ `
29
31
 
30
- const pool = new Pool({
31
- host: 'localhost',
32
- user: 'postgres',
33
- password: 'postgres',
34
- database: 'myapp'
35
- })
32
+ await pool.begin(async tx => {
33
+ await tx.execute`UPDATE accounts SET balance = balance - 100 WHERE id = ${1}`
34
+ await tx.execute`UPDATE accounts SET balance = balance + 100 WHERE id = ${2}`
35
+ })
36
+ ```
36
37
 
37
- // Type-safe query
38
- const [user] = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
38
+ 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.
39
39
 
40
- // Bulk insert
41
- await pool.query`
42
- INSERT INTO users ${sql.insert([{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }])}
43
- `
40
+ ## Benchmarks
44
41
 
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
- ```
42
+ 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.
51
43
 
52
- ---
44
+ **8192 concurrent parameterized SELECTs, measured with mitata:**
53
45
 
54
- ## Features
46
+ | Driver | Avg time | Relative | Memory (p75) |
47
+ |---|---:|---:|---:|
48
+ | Pgtx | **69.23 ms** | 1.00× | ≈10.00 MB |
49
+ | Postgres.js | 201.69 ms | 2.91× slower | ≈12.25 MB |
50
+ | node-postgres (`pg`) | 738.97 ms | 10.67× slower | ≈12.74 MB |
55
51
 
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
52
+ **Real HTTP throughput, `node:http` serving a PG-backed route, `wrk`:**
66
53
 
67
- ---
54
+ | Concurrency | Pgtx | Postgres.js | Speedup |
55
+ |---:|---:|---:|---:|
56
+ | 50 | **13,575 req/s** | 5,507 req/s | 2.47× |
57
+ | 200 | **18,431 req/s** | 6,640 req/s | 2.78× |
58
+ | 1000 | **16,992 req/s** | 8,099 req/s | 2.10× |
59
+ | 10000 | **17,700 req/s** | 10,414 req/s | 1.70× |
68
60
 
69
- ## Performance
61
+ (`/users` route — `SELECT` returning 5 rows, single connection pool, 2 wrk threads, 10s runs)
70
62
 
71
- All benchmarks are executed on **GitHub Actions** (Ubuntu, 2 vCPUs) and are fully reproducible. Benchmark sources are included in this repository.
63
+ The gap is widest at low-to-mid concurrency, where per-request overhead dominates; it narrows past ~10k concurrent connections as both drivers start hitting OS/socket limits rather than protocol overhead.
72
64
 
73
- ### 1. PostgreSQL Pipeline Stress Test
65
+ **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.
74
66
 
75
- **3000 concurrent parameterized `SELECT` queries**
67
+ ## The parts worth knowing about
76
68
 
77
- * Connection pool: **10 connections**
78
- * Measured with **mitata**
69
+ ### Errors you can pattern-match on
79
70
 
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 |
71
+ 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:
85
72
 
86
- ### 2. Real-World HTTP Throughput
73
+ ```typescript
74
+ const users = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
75
+ .recoverIf(err => err.code === '42P01', []) // undefined_table → []
76
+ .recoverIf(err => err.code === '23505', []) // unique_violation → []
77
+ .tapErr(err => logger.error(err))
78
+ ```
87
79
 
88
- Simple `node:http` server serving a PostgreSQL-backed endpoint.
80
+ ### Pipelining is automatic, not opt-in
89
81
 
90
- Measured with:
82
+ `Bind`/`.bind` from fluent-future group independent queries into the same pipeline batch for you:
91
83
 
92
- ```bash
93
- wrk -t2 -c<N> -d10s http://localhost:3000/users
94
- ```
84
+ ```typescript
85
+ // 5 queries, 2 round-trips
86
+ const { user, posts, ...data } = await Bind({
87
+ user: pool.query<User>`...`,
88
+ config: pool.query<Config>`...`,
89
+ announcements: pool.query<Announcement>`...`
90
+ }).bind({
91
+ posts: ({ user }) => pool.query<Post>`...`,
92
+ notifications: ({ user }) => pool.query<Notif>`...`
93
+ })
94
+ ```
95
95
 
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×** |
96
+ Anything you fire off in the same tick without awaiting in between ends up on the wire together.
102
97
 
103
- ### Why is Pgtx fast?
98
+ ### Streaming that doesn't buffer
104
99
 
105
- Pgtx is engineered for **throughput**, not for minimizing the latency of individual queries.
100
+ `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.
106
101
 
107
- Instead of optimizing a single request in isolation, Pgtx minimizes per-query overhead under sustained concurrent load by combining:
102
+ ```typescript
103
+ for await (const log of pool.stream<Log>`SELECT * FROM application_logs WHERE level = ${'error'}`) {
104
+ console.log(log.timestamp, log.data)
105
+ }
106
+ ```
108
107
 
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
108
+ It's a real Web Streams object, so it drops straight into an HTTP response body:
117
109
 
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 **2× 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
- ## 📖 Features
127
-
128
- ### 🎯 Typed Error Handling
129
-
130
- Pgtx queries return `Future<T, PostgresError>` from [fluent-future](https://www.npmjs.com/package/fluent-future) instead of raw `Promise<T>`. This gives you:
131
-
132
- - **Typed errors** — `PostgresError` with `code`, `severity`, `detail`
133
- - **Declarative recovery** — `.recover()`, `.recoverIf()` instead of try/catch
134
- - **Chain composition** — `.andThen()`, `.orElse()`, `.tap()`, `.tapErr()`
135
-
136
- ```typescript
137
- const users = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
138
- .recoverIf(err => err.code === '42P01', []) // undefined_table → []
139
- .recoverIf(err => err.code === '23505', []) // unique_violation → []
140
- .tapErr(err => logger.error(err)) // log remaining errors
141
- ```
142
-
143
- ---
144
-
145
- ### Pipeline by Default
146
-
147
- `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:
148
-
149
- ```typescript
150
- // 5 queries, only 2 network round-trips
151
- const {user, posts, ...data} = await Bind({
152
- user: pool.query<User>`...`,
153
- config: pool.query<Config>`...`,
154
- announcements: pool.query<Announcement>`...`
155
- })
156
- .bind({
157
- posts: ({ user }) => pool.query<Post>`...`,
158
- notifications: ({ user }) => pool.query<Notif>`...`
159
- })
160
- ```
161
- > 🚀 Pgtx automatically groups concurrent queries into pipeline batches, reducing network overhead by up to 5x compared to sequential queries.
162
-
163
-
164
- ---
165
-
166
-
167
- ### High-Performance Data Streaming (`pool.stream`)
168
-
169
- 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.
170
-
171
- 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`**.
172
-
173
- #### 1. Ultra-Low Memory Row Iteration
174
- 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.
175
-
176
- ```typescript
177
- interface HeavyLog { id: number; data: string; timestamp: Date; }
178
-
179
- const logStream = pool.stream<HeavyLog>`
180
- SELECT id, data, timestamp FROM application_logs WHERE level = ${'error'}
181
- `;
182
-
183
- for await (const log of logStream) {
184
- // Each log object is parsed on-the-fly and processed instantly.
185
- // Zero rows are accumulated in the internal driver state!
186
- console.log(`[${log.timestamp.toISOString()}] ${log.data}`);
110
+ ```typescript
111
+ export default {
112
+ async fetch(req) {
113
+ const stream = pool.stream`SELECT id, email FROM giant_user_table`
114
+ return new Response(stream, { headers: { "Content-Type": "application/json" } })
187
115
  }
188
- ```
189
-
190
- #### 2. Streaming Directly to HTTP Responses (`Bun.serve`)
191
- 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.
192
-
193
- ```typescript
194
- import { Pool } from "@m2k-5f/pgtx";
116
+ }
117
+ ```
195
118
 
196
- const pool = new Pool({ /* ... config ... */ });
119
+ ### Transactions and savepoints
197
120
 
198
- export default {
199
- port: 3000,
200
- async fetch(request) {
201
- const url = new URL(request.url);
121
+ ```typescript
122
+ await pool.begin(async tx => {
123
+ await tx.execute`INSERT INTO orders (user_id) VALUES (${userId})`
202
124
 
203
- if (url.pathname === "/export/users") {
204
- // Synchronously returns a stream handle even if pool sockets are currently busy
205
- const userStream = pool.stream`SELECT id, email, profile_metadata FROM giant_user_table`;
125
+ await tx.savepoint('reserve_stock', async stx => {
126
+ await stx.execute`UPDATE stock SET count = count - 1 WHERE product_id = ${productId}`
127
+ if (outOfStock) throw new Error('out of stock') // only the savepoint rolls back
128
+ }).tapErr(console.log)
129
+ })
130
+ ```
206
131
 
207
- return new Response(userStream, {
208
- headers: {
209
- "Content-Type": "application/json",
210
- "Transfer-Encoding": "chunked",
211
- },
212
- });
213
- }
132
+ ### LISTEN / NOTIFY without babysitting a connection
214
133
 
215
- return new Response("Not Found", { status: 404 });
216
- },
217
- };
218
- ```
134
+ ```typescript
135
+ await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
219
136
 
220
- ---
137
+ const unlisten = await pool.listen('user_events', payload => {
138
+ console.log('got:', payload)
139
+ })
221
140
 
222
- ### Transactions & Savepoints
141
+ // later
142
+ await unlisten() // sends UNLISTEN, hands the connection back
143
+ ```
144
+
145
+ `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.
223
146
 
224
- ```typescript
225
- await pool.begin(async (tx) => {
226
- await tx.query`INSERT INTO orders (user_id) VALUES (${userId})`
227
-
228
- await tx.savepoint('update_stock', async (stx) => {
229
- await stx.query`UPDATE stock SET count = count - 1 WHERE product_id = ${productId}`
230
- if (outOfStock) throw new Error('out of stock') // Only savepoint rolls back
231
- })
232
- .tapErr(console.log) // Error: out of stock
233
- })
234
- ```
147
+ ### Building queries without string-gluing
235
148
 
236
- ---
149
+ ```typescript
150
+ // bulk insert — columns inferred from the object
151
+ await pool.execute`INSERT INTO users ${sql.insert(users)}`
152
+
153
+ // dynamic SET clause
154
+ await pool.execute`UPDATE users SET ${sql.update({ status: 'active', last_login: new Date() })} WHERE id = ${userId}`
155
+ ```
156
+
157
+ Rule of thumb: `execute` when you don't need rows back, `query` when you do — same rule as the raw driver, `sql.*` doesn't change it.
237
158
 
238
- ### Async Notifications (LISTEN / NOTIFY)
159
+ ```typescript
160
+ // composable fragments
161
+ const filter = sql.fragment`status = ${'active'} AND age > ${21}`
162
+ await pool.query`SELECT * FROM users WHERE ${filter}`
163
+
164
+ // clean WHERE from an object, undefined keys just drop out
165
+ await pool.query`SELECT * FROM users WHERE ${sql.where({ role: 'admin', age: undefined, active: true })}`
166
+
167
+ // conditional fragments
168
+ await pool.query`SELECT * FROM posts ${search ? sql.fragment`WHERE title ILIKE ${search}` : sql.empty}`
169
+ ```
170
+
171
+ `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`.
172
+
173
+ ## Not doing this
174
+
175
+ ```typescript
176
+ // don't
177
+ await pool.query(`SELECT * FROM users WHERE name = '${userInput}'`)
178
+
179
+ // do
180
+ await pool.query`SELECT * FROM users WHERE name = ${userInput}`
181
+ await pool.query`SELECT * FROM ${sql.ident(tableName)}`
182
+ ```
239
183
 
240
- 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.
184
+ 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.
241
185
 
242
- #### 1. Sending a Notification
243
- Notifications are atomic and can be triggered directly from the `Pool` utilizing any available socket:
244
- ```typescript
245
- await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
246
- ```
186
+ ## API
247
187
 
248
- #### 2. High-Level Pool Subscription (Recommended)
249
- 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.
188
+ ### `Connection`
250
189
 
251
- The method returns a lazy, async **unsubscribe function** that cleanly handles `UNLISTEN` and returns the connection to the pool when invoked.
252
-
253
- ```typescript
254
- const onEvent = (payload: string) => {
255
- console.log(`Received payload: ${payload}`)
256
- }
257
-
258
- // Automatically borrows a connection and sets up the listener
259
- const unsubscribe = await pool.listen('user_events', onEvent)
260
-
261
- // When the subscription is no longer needed (e.g., server shutdown):
262
- // It automatically sends UNLISTEN and releases the connection back to the pool!
263
- await unsubscribe()
264
- ```
265
-
266
- #### 3. Low-Level Connection Subscription (Stateful)
267
- 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.
268
-
269
- ```typescript
270
- const conn = await pool.acquire()
271
-
272
- const onEvent = (payload: string) => {
273
- console.log(`Received payload: ${payload}`)
274
- }
190
+ ```typescript
191
+ class Connection {
192
+ static new(config: ConnectionPartialConfig): Future<Connection, PostgresError>
275
193
 
276
- // Multiplexes multiple callbacks onto a single LISTEN command seamlessly
277
- await conn.listen('user_events', onEvent)
278
- await conn.listen('user_events', (data) => logToFile(data))
194
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
195
+ execute(strings: TemplateStringsArray, ...values: any[]): Future<void, PostgresError>
196
+ stream<T>(strings: TemplateStringsArray, ...values: any[]): ReadableStream<T>
197
+ begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
198
+ notify(channelName: string, payload?: string): Future<void, PostgresError>
199
+ listen(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>
200
+ unlisten(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>
201
+ close(): Future<void, PostgresError>
279
202
 
280
- // Cleans up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
281
- await conn.unlisten('user_events', onEvent)
203
+ get isOpened(): boolean
204
+ get isClosed(): boolean
205
+ }
282
206
 
283
- // ⚠️ Manual lifecycle management is strictly required for this pattern!
284
- // Do NOT release it back to the pool until you are completely done listening.
285
- this.release(conn)
286
- ```
207
+ interface ConnectionPartialConfig {
208
+ user: string
209
+ password?: string
210
+ host: string
211
+ port: number
212
+ database: string
213
+ logLevel?: 'error' | 'notice' | 'query' // default 'error'
214
+ int8toBigint?: boolean // default false
215
+ queryTimeout?: number // default 30000
216
+ syncShedule?: 'beforeMicrotask' | 'afterMicrotask' | 'Immediate' // default 'afterMicrotask'
217
+ }
218
+ ```
219
+
220
+ ### `Pool`
221
+
222
+ ```typescript
223
+ class Pool {
224
+ constructor(config: PoolPartialConfig)
225
+
226
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
227
+ execute(strings: TemplateStringsArray, ...values: any[]): Future<void, PostgresError>
228
+ stream<T>(strings: TemplateStringsArray, ...values: any[]): ReadableStream<T>
229
+ begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
230
+ notify(channelName: string, payload?: string): Future<void, PostgresError>
231
+ listen(channel: string, callback: (payload: string) => void): Future<() => Future<void, PostgresError>, PostgresError>
232
+ withAcquire<T>(fn: (conn: Connection) => Promise<T>): Future<T, unknown>
233
+ acquire(): Future<Connection, PostgresError>
234
+ release(conn: Connection): void
235
+ close(): Future<void, PostgresError>
287
236
 
288
- > ⚠️ **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.
237
+ get size(): number
238
+ get total(): number
239
+ }
289
240
 
290
- ---
241
+ interface PoolPartialConfig extends ConnectionPartialConfig {
242
+ max?: number // default 20
243
+ }
244
+ ```
245
+
246
+ ### `Transaction`
247
+
248
+ ```typescript
249
+ class Transaction {
250
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
251
+ commit(): Future<void, PostgresError>
252
+ rollback(): Future<void, PostgresError>
253
+ savepoint<T>(name: string, callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
254
+
255
+ get isActive(): boolean
256
+ }
257
+ ```
258
+
259
+ ### `sql`
260
+
261
+ ```typescript
262
+ const sql: {
263
+ ident<T extends string>(name: T): IdentifierClause<T>
264
+ literal<T extends string>(value: T): LiteralClause<T>
265
+ fragment(strings: TemplateStringsArray, ...values: any[]): FragmentClause
266
+ insert<T extends Record<string, any>>(...objects: T[]): InsertClause<T>
267
+ update<T extends Record<string, any>>(object: T): UpdateClause<T>
268
+ where<T extends Record<string, any>>(map: T): WhereClause<T>
269
+ excluded(fields: string[]): ExcludeUpdateClause
270
+ array(values: any[], separator?: string): ArrayClause
271
+ empty: EmptyClause
272
+ }
273
+ ```
274
+
275
+ ## What this isn't
276
+
277
+ 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.
278
+
279
+ ## License
280
+
281
+ MIT © [M2K-5F](https://github.com/M2K-5F)
291
282
 
292
- ### Bulk Inserts
293
-
294
- ```typescript
295
- const users = [
296
- { name: 'Alice', email: 'alice@test.com' },
297
- { name: 'Bob', email: 'bob@test.com' }
298
- ]
299
-
300
- await pool.query`
301
- INSERT INTO users ${sql.insert(users)}
302
- `
303
- // INSERT INTO users (name, email) VALUES ($1, $2), ($3, $4)
304
- ```
305
-
306
- ---
307
-
308
- ### Dynamic Updates
309
-
310
- ```typescript
311
- const data = { status: 'active', last_login: new Date() }
312
-
313
- await pool.query`
314
- UPDATE users SET ${sql.update(data)} WHERE id = ${userId}
315
- `
316
- // UPDATE users SET status = $1, last_login = $2 WHERE id = $3
317
- ```
318
-
319
- ---
320
-
321
- ### Recursive Fragments
322
-
323
- ```typescript
324
- const filter = sql.fragment`status = ${'active'} AND age > ${21}`
325
- const subquery = sql.fragment`(SELECT id FROM roles WHERE name = ${'admin'})`
326
-
327
- await pool.query`
328
- SELECT * FROM users
329
- WHERE ${filter} AND role_id = (${subquery})
330
- `
331
- ```
332
-
333
- ---
334
-
335
- ### Smart Lists
336
-
337
- ```typescript
338
- const ids = [10, 20, 30]
339
- await pool.query`
340
- SELECT * FROM users WHERE id IN (${sql.array(ids)})
341
- `
342
- // SELECT * FROM users WHERE id IN ($1, $2, $3)
343
-
344
- const conditions = [
345
- sql.fragment`status = ${'active'}`,
346
- sql.fragment`age > ${18}`
347
- ]
348
- await pool.query`
349
- SELECT * FROM users WHERE ${sql.array(conditions, ' AND ')}
350
- `
351
- ```
352
-
353
- ---
354
-
355
- ### Clean WHERE Clauses
356
-
357
- ```typescript
358
- const filters = { role: 'admin', age: undefined, active: true }
359
- await pool.query`
360
- SELECT * FROM users WHERE ${sql.where(filters)}
361
- `
362
- // SELECT * FROM users WHERE role = $1 AND active = $2
363
- ```
364
-
365
- ---
366
-
367
- ### Conditional Logic
368
-
369
- ```typescript
370
- const search = ""
371
- await pool.query`
372
- SELECT * FROM posts
373
- ${search ? sql.fragment`WHERE title ILIKE ${search}` : sql.empty}
374
- `
375
- ```
376
-
377
- ---
378
-
379
- ## 🛡️ Security
380
-
381
- | Pattern | Protection |
382
- |---------|------------|
383
- | `sql.ident(name)` | Escapes identifiers: `user` → `"user"` |
384
- | `sql.literal(value)` | Escapes string literals |
385
- | Parameter binding | Uses native `$1, $2` placeholders |
386
- | Template tags | Cannot be injected via user input |
387
-
388
- ```typescript
389
- // ✅ Safe - parameterized
390
- await pool.query`SELECT * FROM users WHERE name = ${userInput}`
391
-
392
- // ⚠️ Unsafe - raw interpolation (DON'T DO THIS)
393
- await pool.query(`SELECT * FROM users WHERE name = '${userInput}'`)
394
-
395
- // ✅ Safe - identifiers
396
- await pool.query`SELECT * FROM ${sql.ident(tableName)}`
397
- ```
398
-
399
- ---
400
-
401
- ## 📊 Null & Undefined Handling
402
-
403
- | Value | In INSERT | In UPDATE | In VALUES | In Arrays |
404
- |-------|-----------|-----------|-----------|-----------|
405
- | `null` | `NULL` | `NULL` | `NULL` | `NULL` |
406
- | `undefined` | `DEFAULT` | Skipped | `Error` | `Error` |
407
-
408
-
409
- ```typescript
410
- // undefined becomes DEFAULT
411
- await pool.query`
412
- INSERT INTO users ${sql.insert({
413
- name: 'Alice',
414
- age: undefined, // → DEFAULT
415
- email: null // → NULL
416
- })}
417
- `
418
- // INSERT INTO users (name, age, email) VALUES ($1, DEFAULT, $2)
419
-
420
- // undefined fields are skipped in UPDATE
421
- await pool.query`
422
- UPDATE users SET ${sql.update({
423
- name: 'Bob',
424
- age: undefined // Skipped - age remains unchanged
425
- })} WHERE id = 1
426
- `
427
- // UPDATE users SET name = $1 WHERE id = 1
428
- ```
429
-
430
- ---
431
-
432
- ## 🔧 API Reference
433
-
434
- ### Connection
435
- ```typescript
436
- class Connection {
437
- static new(params: ConnectionPartialParams): Promise<Connection>
438
-
439
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
440
- begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
441
- notify(channelName: string, payload?: string): Future<void, PostgresError>
442
- listen(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>
443
- unlisten(channelName: string, callback: (payload: string) => void): Future<void, PostgresError>
444
- stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...params: any[]): ReadableStream<T>
445
- get isAlive(): boolean
446
- close(): void
447
- }
448
-
449
- interface ConnectionPartialParams {
450
- user: string
451
- password?: string
452
- host: string
453
- port: number
454
- database: string
455
- logLevel?: LogLevel, // default "error"
456
- int8toBigint?: boolean, // default false
457
- queryTimeout?: number // default 30000 (30 seconds)
458
- syncShedule?: "Tick" | "Immediate" // default "Immediate"
459
- }
460
- ```
461
-
462
- ### Pool
463
-
464
- ```typescript
465
- class Pool {
466
- constructor(config: PoolPartialConfig)
467
-
468
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
469
- begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
470
- notify(channelName: string, payload?: string): Future<void, PostgresError>
471
- listen(channel: string, callback: (payload: string) => void): Future<() => Future<void, PostgresError>, PostgresError>
472
- stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...args: any[]): ReadableStream<T>
473
- withAcquire<T>(fn: (conn: Connection) => Promise<T>): Future<T, unknown>
474
- acquire(): Future<Connection, PostgresError>
475
- release(conn: Connection): void
476
- close(): void
477
-
478
- get size(): number
479
- get total(): number
480
- }
481
-
482
- interface PoolPartialConfig extends ConnectionPartialConfig {
483
- max?: number
484
- }
485
- ```
486
-
487
- ### Transaction
488
-
489
- ```typescript
490
- class Transaction {
491
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
492
- commit(): Future<void, PostgresError>
493
- rollback(): Future<void, PostgresError>
494
- savepoint<T>(name: string, callback: (tx: Transaction) => Promise<T>): Future<T, unknown>
495
-
496
- get isActive(): boolean
497
- }
498
- ```
499
-
500
- ### **sql** helper
501
- ```typescript
502
- const sql: {
503
- ident<T extends string>(identificator: T): IdentifierClause<T>
504
- literal<T extends string>(value: T): LiteralClause<T>
505
- fragment(strings: TemplateStringsArray, ...values: any[]): FragmentClause
506
- insert<T extends Record<string, any>>(...objects: NoInfer<T>[]): InsertClause<T>
507
- update<T extends Record<string, any>>(object: T): UpdateClause<T>
508
- where<T extends Record<string, any>>(whereMap: T): WhereClause<T>
509
- excluded(fields: string[]): ExcludeUpdateClause
510
- array(array: any[], separator?: string): ArrayClause
511
- empty: EmptyClause
512
- }
513
- ```
514
-
515
- ---
516
-
517
-
518
- Pgtx is a PostgreSQL driver.
519
-
520
- It is not an ORM. It is absolutely Blazing.
521
-
522
- ## 📝 License
523
-
524
- MIT © [M2K-5F](https://github.com/M2K-5F)
283
+ ---
525
284
 
526
- ---
285
+ **Made with ❤️ and a bit of insanity**
527
286
 
528
- **Made with ❤️ and a bit of insanity**
287
+ *Manufactured under license by the **Blazing Corporation**. Side effects may include throughput.*