@m2k-5f/pgtx 2.4.0 → 2.5.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
@@ -55,6 +55,7 @@
55
55
 
56
56
  - **Pipeline queries** — Automatic query multiplexing over PostgreSQL pipeline protocol
57
57
  - **Tagged templates** — Natural SQL with type safety
58
+ - **Native Web Streams API** — Memory-efficient data streaming via `pool.stream()`
58
59
  - **Transactions & Savepoints** — Nested transactions with rollback
59
60
  - **Bulk inserts** — Auto-extract columns from objects
60
61
  - **Dynamic updates** — Generate SET clauses from objects
@@ -63,42 +64,64 @@
63
64
  - **Connection pool** — Auto-management connections with support for pipeline queries via the pool itself.
64
65
  - **Zero dependencies** — Lightweight and blazing
65
66
 
66
- ---
67
+ ---
67
68
 
68
- ## ⚡ Performance & Benchmarks
69
+ ## ⚡ Performance
69
70
 
70
- ### 1. In-Engine Pipeline Blast (3000 Parallel Queries)
71
- *Measured using `mitata` inside GitHub Actions cloud runners (Ubuntu, 2 vCPUs, 10 DB Connections).*
71
+ All benchmarks are executed on **GitHub Actions** (Ubuntu, 2 vCPUs) and are fully reproducible. Benchmark sources are included in this repository.
72
72
 
73
- | Driver | Avg Time per Iteration | Relative Speed | Memory (p75) |
74
- | :--- | :---: | :---: | :---: |
75
- | **Pgtx (Pipeline)** | **19.21 ms** | **Baseline (3.6×)** | **874.71 KB** |
76
- | Postgres.js (Pipeline) | 70.50 ms | 3.6× Slower | 1.29 MB |
77
- | node-postgres (no pipeline) | 327.07 ms | 17.0× Slower | 4.05 MB |
73
+ ### 1. PostgreSQL Pipeline Stress Test
78
74
 
79
- > **Stability Note:** Pgtx provides an rock-solid flat latency graph (p99 is strictly bounded to `21.76 ms`), while maintaining a 2.5× smaller memory footprint compared to Postgres.js due to zero-allocation binary parsing.
75
+ **3000 concurrent parameterized `SELECT` queries**
80
76
 
81
- ### 2. Real-World HTTP Throughput (`wrk` Stress Test)
82
- *HTTP server baseline using a `node:http` instance on GitHub Actions runner, handling 1,000 concurrent network connections (`wrk -t2 -c1000 -d10s`).*
77
+ * Connection pool: **10 connections**
78
+ * Measured with **mitata**
83
79
 
84
- - **Pgtx:** **~14,500 RPS**
85
- - **Postgres.js:** **~11,500 RPS**
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 |
86
85
 
87
- On high-concurrency bare metal servers, Pgtx effortlessly maintains a **+25% performance lead** over Postgres.js.
86
+ ### 2. Real-World HTTP Throughput
88
87
 
89
- Pgtx achieves high throughput by:
88
+ Simple `node:http` server serving a PostgreSQL-backed endpoint.
90
89
 
91
- * Pipeline query multiplexing
92
- * Synchronous protocol encoding
93
- * Batched socket writes
94
- * Automatic prepared statement caching
95
- * Row description caching
96
- * Zero-dependency implementation
97
- * Binary protocol support
90
+ Measured with:
98
91
 
99
- > Benchmarks source is available in the repository and can be reproduced locally.
92
+ ```bash
93
+ wrk -t2 -c<N> -d10s http://localhost:3000/users
94
+ ```
100
95
 
101
- ---
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
+ ---
102
125
 
103
126
  ## 🔥 Why Pgtx?
104
127
 
@@ -156,6 +179,60 @@
156
179
  > 🚀 Pgtx automatically groups concurrent queries into pipeline batches, reducing network overhead by up to 5x compared to sequential queries.
157
180
 
158
181
 
182
+ ### High-Performance Data Streaming (`pool.stream`)
183
+
184
+ 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.
185
+
186
+ 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`**.
187
+
188
+ #### 1. Ultra-Low Memory Row Iteration
189
+ 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.
190
+
191
+ ```typescript
192
+ interface HeavyLog { id: number; data: string; timestamp: Date; }
193
+
194
+ const logStream = pool.stream<HeavyLog>`
195
+ SELECT id, data, timestamp FROM application_logs WHERE level = ${'error'}
196
+ `;
197
+
198
+ for await (const log of logStream) {
199
+ // Each log object is parsed on-the-fly and processed instantly.
200
+ // Zero rows are accumulated in the internal driver state!
201
+ console.log(`[${log.timestamp.toISOString()}] ${log.data}`);
202
+ }
203
+ ```
204
+
205
+ #### 2. Streaming Directly to HTTP Responses (`Bun.serve`)
206
+ 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.
207
+
208
+ ```typescript
209
+ import { Pool } from "@m2k-5f/pgtx";
210
+
211
+ const pool = new Pool({ /* ... config ... */ });
212
+
213
+ export default {
214
+ port: 3000,
215
+ async fetch(request) {
216
+ const url = new URL(request.url);
217
+
218
+ if (url.pathname === "/export/users") {
219
+ // Synchronously returns a stream handle even if pool sockets are currently busy
220
+ const userStream = pool.stream`SELECT id, email, profile_metadata FROM giant_user_table`;
221
+
222
+ return new Response(userStream, {
223
+ headers: {
224
+ "Content-Type": "application/json",
225
+ "Transfer-Encoding": "chunked",
226
+ },
227
+ });
228
+ }
229
+
230
+ return new Response("Not Found", { status: 404 });
231
+ },
232
+ };
233
+ ```
234
+
235
+
159
236
  ### Transactions & Savepoints
160
237
 
161
238
  ```typescript
@@ -173,13 +250,36 @@
173
250
 
174
251
  ### Async Notifications (LISTEN / NOTIFY)
175
252
 
176
- Pgtx natively handles PostgreSQL `LISTEN/NOTIFY` protocol messages asynchronously without interrupting multiplexed query pipeline.
253
+ 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.
177
254
 
255
+ #### 1. Sending a Notification
256
+ Notifications are atomic and can be triggered directly from the `Pool` utilizing any available socket:
178
257
  ```typescript
179
- // 1. Sending a notification
180
258
  await pool.notify('user_events', JSON.stringify({ id: 42, action: 'signup' }))
259
+ ```
260
+
261
+ #### 2. High-Level Pool Subscription (Recommended)
262
+ 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.
263
+
264
+ The method returns a lazy, async **unsubscribe function** that cleanly handles `UNLISTEN` and returns the connection to the pool when invoked.
265
+
266
+ ```typescript
267
+ const onEvent = (payload: string) => {
268
+ console.log(`Received payload: ${payload}`)
269
+ }
270
+
271
+ // Automatically borrows a connection and sets up the listener
272
+ const unsubscribe = await pool.listen('user_events', onEvent)
273
+
274
+ // When the subscription is no longer needed (e.g., server shutdown):
275
+ // It automatically sends UNLISTEN and releases the connection back to the pool!
276
+ await unsubscribe()
277
+ ```
278
+
279
+ #### 3. Low-Level Connection Subscription (Stateful)
280
+ 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.
181
281
 
182
- // 2. Receiving notifications (Requires a dedicated connection from the pool)
282
+ ```typescript
183
283
  const conn = await pool.acquire()
184
284
 
185
285
  const onEvent = (payload: string) => {
@@ -190,14 +290,15 @@
190
290
  await conn.listen('user_events', onEvent)
191
291
  await conn.listen('user_events', (data) => logToFile(data))
192
292
 
193
- // Clean up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
293
+ // Cleans up callbacks (Sends UNLISTEN only when the channel has zero callbacks left)
194
294
  await conn.unlisten('user_events', onEvent)
195
295
 
196
- // Keep the connection active as long as you need notifications!
197
- // Do NOT release it back to the pool prematurely.
296
+ // ⚠️ Manual lifecycle management is strictly required for this pattern!
297
+ // Do NOT release it back to the pool until you are completely done listening.
298
+ this.release(conn)
198
299
  ```
199
300
 
200
- > ⚠️ **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()`.
301
+ > ⚠️ **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.
201
302
 
202
303
 
203
304
  ### Bulk Inserts
@@ -342,7 +443,7 @@
342
443
  notify(channelName: string, payload?: string): Future<[], PostgresError>
343
444
  listen(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
344
445
  unlisten(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>
345
-
446
+ stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...params: any[]): ReadableStream<T>
346
447
  get isAlive(): boolean
347
448
  close(): void
348
449
  }
@@ -353,7 +454,8 @@
353
454
  host: string
354
455
  port: number
355
456
  database: string
356
- logLevel?: 'none' | 'error' | 'notice' | 'query' // defaul: "error"
457
+ queryTimeout?: number // default: 30 srconds
458
+ logLevel?: 'none' | 'error' | 'notice' | 'query' // default: "error"
357
459
  }
358
460
  ```
359
461
 
@@ -363,10 +465,13 @@
363
465
  class Pool {
364
466
  constructor(config: PoolConfig)
365
467
 
366
- query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], Error>
468
+ query<T>(strings: TemplateStringsArray, ...values: any[]): Future<T[], PostgresError>
367
469
  begin<T>(callback: (tx: Transaction) => Promise<T>): Future<T, Error>
368
470
  notify(channelName: string, payload?: string): Future<[], PostgresError>
369
- acquire(): Future<Connection, Error>
471
+ listen(channel: string, callback: (payload: string) => void): Future<() => Promise<void>, PostgresError>
472
+ stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...args: any[]): ReadableStream<T>
473
+ withAcquire<T>(fn: (conn: Connection) => Promise<T>): Future<T, Error>
474
+ acquire(): Future<Connection, PostgresError>
370
475
  release(conn: Connection): void
371
476
  close(): void
372
477
 
@@ -1,4 +1,4 @@
1
- import { Branded, ColumnDescription } from "./types";
1
+ import { Branded } from "./types";
2
2
  import { Transaction } from "./transaction";
3
3
  import { Future } from 'fluent-future';
4
4
  import { PostgresError } from "./error";
@@ -11,23 +11,7 @@ export type ConnectionParams = {
11
11
  database: string;
12
12
  logLevel?: LogLevel;
13
13
  int8toBigint?: boolean;
14
- };
15
- export type ExecuteQueueUnit = {
16
- rows: (string | null)[][];
17
- resolve: (value: any) => void;
18
- reject: (err: Error) => void;
19
- statementName: StatementName;
20
- };
21
- export type ParsingQueueUnit = {
22
- resolve: (statementName: StatementName) => void;
23
- reject: (error: Error) => void;
24
- text: QueryText;
25
- statementName: StatementName;
26
- };
27
- export type DescribeQueueUnit = {
28
- resolve: (value: ColumnDescription[]) => void;
29
- reject: (error: Error) => void;
30
- statementName: StatementName;
14
+ queryTimeout?: number;
31
15
  };
32
16
  export type StatementName = Branded<string, 'StatementName'>;
33
17
  export type QueryText = Branded<string, 'QueryText'>;
@@ -182,7 +166,35 @@ export declare class Connection {
182
166
  * ```
183
167
  */
184
168
  unlisten(channelName: string, callback: (payload: string) => void): Future<[], PostgresError>;
169
+ /**
170
+ * Executes an SQL query in streaming mode.
171
+ *
172
+ * Data is streamed directly from the PostgreSQL binary network buffer into the Web Streams API
173
+ * (`ReadableStream`), bypassing any intermediate array allocation or row accumulation in the JS heap.
174
+ * This pattern provides a true Zero-Memory Footprint and is ideal for exporting massive tables
175
+ * or piping database payloads directly into HTTP responses (e.g., via `Bun.serve` or fetch `Response`).
176
+ *
177
+ * @template T The expected shape of a single row interface.
178
+ * @param {TemplateStringsArray} templates The SQL string parts from the tagged template literal.
179
+ * @param {...any} args The parameterized query arguments.
180
+ * @returns {ReadableStream<T>} Synchronously returns a native Web ReadableStream instance.
181
+ *
182
+ * @example
183
+ * // Streaming a giant table directly to an HTTP response (Bun.serve)
184
+ * const userStream = conn.stream<User>`SELECT id, name FROM users`;
185
+ * return new Response(userStream, { headers: { 'Content-Type': 'application/json' } });
186
+ *
187
+ * @example
188
+ * // Asynchronously iterating over rows as they arrive from the wire socket
189
+ * const stream = conn.stream<User>`SELECT * FROM orders WHERE status = ${'processed'}`;
190
+ * for await (const row of stream) {
191
+ * console.log(row.id, row.amount); // Row object is eligible for GC immediately after iteration
192
+ * }
193
+ */
194
+ stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...params: any[]): ReadableStream<T>;
195
+ private _streamWithController;
185
196
  private _createQuery;
197
+ private _createStream;
186
198
  private _writeQuery;
187
199
  private _registerFlush;
188
200
  private _flush;
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAK3C,OAAO,EAAQ,MAAM,EAAU,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEvC,KAAK,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAA;AAErD,MAAM,MAAM,gBAAgB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAMD,MAAM,MAAM,gBAAgB,GAAG;IAC3B,IAAI,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,EAAE,CAAA;IACzB,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7B,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC5B,aAAa,EAAE,aAAa,CAAA;CAC/B,CAAA;AAGD,MAAM,MAAM,gBAAgB,GAAG;IAC3B,OAAO,EAAE,CAAC,aAAa,EAAE,aAAa,KAAK,IAAI,CAAA;IAC/C,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B,IAAI,EAAE,SAAS,CAAA;IACf,aAAa,EAAE,aAAa,CAAA;CAC/B,CAAA;AAGD,MAAM,MAAM,iBAAiB,GAAG;IAC5B,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,KAAK,IAAI,CAAA;IAC7C,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B,aAAa,EAAE,aAAa,CAAA;CAC/B,CAAA;AAKD,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAE5D,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;AAEpD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAGxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,OAAO,CAAyB;IAExC,OAAO,CAAC,eAAe,CAAwB;IAE/C,OAAO,CAAC,UAAU,CAAgD;IAClE,OAAO,CAAC,kBAAkB,CAA2B;IACrD,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,eAAe,CAAsC;IAE7D,OAAO,CAAC,mBAAmB,CAAyD;IACpF,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,SAAS,CAAU;IAG3B,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,YAAY;IAKpB,OAAO;IAgBP;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB;IAOnC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IAsBtF;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC;IAmB7D;;;;;;;;;;OAUG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW;IAMhD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;IAc/D;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC;IAmB7F,OAAO,CAAC,YAAY;IAuDpB,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,MAAM;IAsBd,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,qBAAqB;YAWf,UAAU;IAmBxB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,aAAa;IA8IrB;;;OAGG;IACH,IAAI,QAAQ,YAEX;IAGD;;;;;;;;;OASG;IACH,KAAK;CAQR"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAA;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAK3C,OAAO,EAAQ,MAAM,EAAU,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAGvC,KAAK,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAA;AAErD,MAAM,MAAM,gBAAgB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAQD,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAE5D,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;AAEpD,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;AAGxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,OAAO,CAAyB;IAExC,OAAO,CAAC,eAAe,CAAwB;IAE/C,OAAO,CAAC,UAAU,CAAgD;IAClE,OAAO,CAAC,kBAAkB,CAA2B;IACrD,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,eAAe,CAAsC;IAE7D,OAAO,CAAC,mBAAmB,CAAyD;IACpF,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,SAAS,CAAU;IAG3B,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,YAAY;IAKpB,OAAO;IAgBP;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB;IAOnC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IAkBtF;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC;IAmB7D;;;;;;;;;;OAUG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW;IAMhD;;;;;;;;;;OAUG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;IAc/D;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC;IAmB7F;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IA2BvF,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,YAAY;IAuDpB,OAAO,CAAC,aAAa;IA4DrB,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,MAAM;IAsBd,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,qBAAqB;YAWf,UAAU;IAmBxB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;IA2IrB;;;OAGG;IACH,IAAI,QAAQ,YAEX;IAGD;;;;;;;;;OASG;IACH,KAAK;CAQR"}
@@ -5,7 +5,7 @@ import { compileSqlTemplate } from "./utils/template-compiler";
5
5
  import { Transaction } from "./transaction";
6
6
  import { SocketConnector } from "./protocol/socket-connector";
7
7
  import { Queue } from "./queue";
8
- import { Query, QueryState } from "./query";
8
+ import { Query, QueryState, StreamQuery } from "./query";
9
9
  import { sql } from ".";
10
10
  import { Begin, Future, Resolve } from 'fluent-future';
11
11
  import { PostgresError } from "./error";
@@ -62,7 +62,7 @@ export class Connection {
62
62
  this._stmtCounter = 0;
63
63
  this.params = params;
64
64
  this._logLevel = logLevel;
65
- this._socket = new SocketConnector(socket, (type, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
65
+ this._socket = new SocketConnector(socket, (type, _, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
66
66
  this._writer = writer;
67
67
  }
68
68
  /**
@@ -121,11 +121,8 @@ export class Connection {
121
121
  }
122
122
  const query = this._createQuery(text, args);
123
123
  this._writeQuery(query);
124
- return Future.of(query.promise, error => {
125
- if (error instanceof PostgresError)
126
- return error;
127
- return new PostgresError(error.message);
128
- });
124
+ query.startTimeout(this.params.queryTimeout || 30000);
125
+ return query.future;
129
126
  }
130
127
  /**
131
128
  * Starts a managed transaction on this connection.
@@ -222,6 +219,61 @@ export class Connection {
222
219
  }
223
220
  return Resolve([]);
224
221
  }
222
+ /**
223
+ * Executes an SQL query in streaming mode.
224
+ *
225
+ * Data is streamed directly from the PostgreSQL binary network buffer into the Web Streams API
226
+ * (`ReadableStream`), bypassing any intermediate array allocation or row accumulation in the JS heap.
227
+ * This pattern provides a true Zero-Memory Footprint and is ideal for exporting massive tables
228
+ * or piping database payloads directly into HTTP responses (e.g., via `Bun.serve` or fetch `Response`).
229
+ *
230
+ * @template T The expected shape of a single row interface.
231
+ * @param {TemplateStringsArray} templates The SQL string parts from the tagged template literal.
232
+ * @param {...any} args The parameterized query arguments.
233
+ * @returns {ReadableStream<T>} Synchronously returns a native Web ReadableStream instance.
234
+ *
235
+ * @example
236
+ * // Streaming a giant table directly to an HTTP response (Bun.serve)
237
+ * const userStream = conn.stream<User>`SELECT id, name FROM users`;
238
+ * return new Response(userStream, { headers: { 'Content-Type': 'application/json' } });
239
+ *
240
+ * @example
241
+ * // Asynchronously iterating over rows as they arrive from the wire socket
242
+ * const stream = conn.stream<User>`SELECT * FROM orders WHERE status = ${'processed'}`;
243
+ * for await (const row of stream) {
244
+ * console.log(row.id, row.amount); // Row object is eligible for GC immediately after iteration
245
+ * }
246
+ */
247
+ stream(templates, ...params) {
248
+ this._checkOpened();
249
+ this._registerFlush();
250
+ const { text, args } = compileSqlTemplate({ templates, args: params });
251
+ if (this._logLevel === 'query') {
252
+ console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
253
+ }
254
+ let controller;
255
+ const stream = new ReadableStream({
256
+ start: c => {
257
+ controller = c;
258
+ }
259
+ });
260
+ const query = this._createStream(text, args, controller);
261
+ this._writeQuery(query);
262
+ query.startTimeout(this.params.queryTimeout || 30000);
263
+ return stream;
264
+ }
265
+ _streamWithController(templates, params, controller) {
266
+ this._checkOpened();
267
+ this._registerFlush();
268
+ const { text, args } = compileSqlTemplate({ templates, args: params });
269
+ if (this._logLevel === 'query') {
270
+ console.log(`\nQUERY: ${text}\n${args.length !== 0 ? `ARGUMENTS: [${args}]\n` : ""}`);
271
+ }
272
+ const query = this._createStream(text, args, controller);
273
+ this._writeQuery(query);
274
+ query.startTimeout(this.params.queryTimeout || 30000);
275
+ return query;
276
+ }
225
277
  _createQuery(text, args) {
226
278
  if (this._parsed.has(text)) {
227
279
  const statementName = this._parsed.get(text);
@@ -251,6 +303,35 @@ export class Connection {
251
303
  }
252
304
  }
253
305
  }
306
+ _createStream(text, args, controller) {
307
+ if (this._parsed.has(text)) {
308
+ const statementName = this._parsed.get(text);
309
+ if (this._described.has(statementName)) {
310
+ const columns = this._described.get(statementName);
311
+ return new StreamQuery(text, args, QueryState.Executing, statementName, controller, columns);
312
+ }
313
+ else {
314
+ if (this._describingPending.has(statementName)) {
315
+ return new StreamQuery(text, args, QueryState.Executing, statementName, controller);
316
+ }
317
+ else {
318
+ this._describingPending.add(statementName);
319
+ return new StreamQuery(text, args, QueryState.Describing, statementName, controller);
320
+ }
321
+ }
322
+ }
323
+ else {
324
+ if (this._parsingPending.has(text)) {
325
+ const statementName = this._parsingPending.get(text);
326
+ return new StreamQuery(text, args, QueryState.Describing, statementName, controller);
327
+ }
328
+ else {
329
+ const statementName = this._nextStatement();
330
+ this._parsingPending.set(text, statementName);
331
+ return new StreamQuery(text, args, QueryState.Parsing, statementName, controller);
332
+ }
333
+ }
334
+ }
254
335
  _writeQuery(query) {
255
336
  if (query.state === QueryState.Parsing) {
256
337
  this._writer
@@ -270,7 +351,7 @@ export class Connection {
270
351
  .writeBind("", query.statementName, query.args)
271
352
  .writeExecute("");
272
353
  }
273
- this._pipelinesQueue.get().push(query);
354
+ this._pipelinesQueue.last.push(query);
274
355
  }
275
356
  _registerFlush() {
276
357
  if (!this._isFlushing) {
@@ -314,7 +395,7 @@ export class Connection {
314
395
  async _reconnect() {
315
396
  this._resetConnectionState(ErrConnectionReconnecring);
316
397
  const socket = await createAuthorizedSocket(ConnectionRequestWriter.new(), this.params);
317
- const connector = new SocketConnector(socket, (type, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
398
+ const connector = new SocketConnector(socket, (type, _, reader) => this._handlePacket(type, reader), (err) => this._registerReconnect());
318
399
  this._socket = connector;
319
400
  void this._restoreSubscriptions();
320
401
  this._isReconnecting = false;
@@ -328,15 +409,14 @@ export class Connection {
328
409
  return Promise.all(promises);
329
410
  }
330
411
  _getCurrentQuery() {
331
- return this._pipelinesQueue.get().get();
412
+ return this._pipelinesQueue.current.current;
332
413
  }
333
414
  _rejectPipeline(error) {
334
415
  if (this._pipelinesQueue.isFree)
335
416
  return;
336
- const queue = this._pipelinesQueue.get();
417
+ const queue = this._pipelinesQueue.current;
337
418
  while (queue.hasMore) {
338
- queue.get().reject(error);
339
- queue.next();
419
+ queue.shift.reject(error);
340
420
  }
341
421
  }
342
422
  _handlePacket(type, reader) {
@@ -407,7 +487,7 @@ export class Connection {
407
487
  break;
408
488
  }
409
489
  query.state = QueryState.Completed;
410
- this._pipelinesQueue.get().next();
490
+ this._pipelinesQueue.current.next();
411
491
  query.resolve();
412
492
  }
413
493
  break;
@@ -433,10 +513,7 @@ export class Connection {
433
513
  case ResponseTypes.ReadyForQuery:
434
514
  {
435
515
  reader.readReadyForQuery();
436
- const pipeline = this._pipelinesQueue.get();
437
- if (!pipeline.hasMore) {
438
- this._pipelinesQueue.next();
439
- }
516
+ this._pipelinesQueue.next();
440
517
  }
441
518
  break;
442
519
  case ResponseTypes.Notice:
package/dist/pool.d.ts CHANGED
@@ -67,7 +67,26 @@ export declare class Pool {
67
67
  * }
68
68
  * ```
69
69
  */
70
- acquire(): Future<Connection, Error>;
70
+ acquire(): Future<Connection, PostgresError>;
71
+ /**
72
+ * Provides a safe execution context for performing low-level operations
73
+ * directly on a single, dedicated `Connection` instance.
74
+ *
75
+ * Automatically borrows a free socket from the pool, forwards it to the provided callback function,
76
+ * and guarantees that the connection is released back to the pool once the execution completes,
77
+ * even if errors or unexpected exceptions are thrown. Prevents connection descriptor leaks.
78
+ *
79
+ * @template T The return type of the provided callback function.
80
+ * @param {(conn: Connection) => Promise<T>} fn A callback function that operates on the allocated Connection.
81
+ * @returns {Future<T, PostgresError>} A `Future` that resolves with the return value of the callback.
82
+ *
83
+ * @example
84
+ * // Executing low-level engine commands on a single, pinned connection
85
+ * const status = await pool.withAcquire(async (conn) => {
86
+ * return await conn.query`SELECT pg_is_in_recovery()`;
87
+ * });
88
+ */
89
+ withAcquire<T>(fn: (conn: Connection) => Promise<T>): Future<T, Error>;
71
90
  /**
72
91
  * Releases the connection back to the pool.
73
92
  *
@@ -129,7 +148,33 @@ export declare class Pool {
129
148
  * const users = await pool.query<User>`SELECT * FROM users`
130
149
  * ```
131
150
  */
132
- query<T extends Record<string, any>>(templates: TemplateStringsArray, ...args: any[]): Future<T[], Error>;
151
+ query<T extends Record<string, any>>(templates: TemplateStringsArray, ...args: any[]): Future<T[], PostgresError>;
152
+ /**
153
+ * Executes an SQL query in streaming mode.
154
+ *
155
+ * Data is streamed directly from the PostgreSQL binary network buffer into the Web Streams API
156
+ * (`ReadableStream`), bypassing any intermediate array allocation or row accumulation in the JS heap.
157
+ * This pattern provides a true Zero-Memory Footprint and is ideal for exporting massive tables
158
+ * or piping database payloads directly into HTTP responses (e.g., via `Bun.serve` or fetch `Response`).
159
+ *
160
+ * @template T The expected shape of a single row interface.
161
+ * @param {TemplateStringsArray} templates The SQL string parts from the tagged template literal.
162
+ * @param {...any} args The parameterized query arguments.
163
+ * @returns {ReadableStream<T>} Synchronously returns a native Web ReadableStream instance.
164
+ *
165
+ * @example
166
+ * // Streaming a giant table directly to an HTTP response (Bun.serve)
167
+ * const userStream = pool.stream<User>`SELECT id, name FROM users`;
168
+ * return new Response(userStream, { headers: { 'Content-Type': 'application/json' } });
169
+ *
170
+ * @example
171
+ * // Asynchronously iterating over rows as they arrive from the wire socket
172
+ * const stream = pool.stream<User>`SELECT * FROM orders WHERE status = ${'processed'}`;
173
+ * for await (const row of stream) {
174
+ * console.log(row.id, row.amount); // Row object is eligible for GC immediately after iteration
175
+ * }
176
+ */
177
+ stream<T extends Record<string, any>>(templates: TemplateStringsArray, ...args: any[]): ReadableStream<T>;
133
178
  /**
134
179
  * Sends an asynchronous notification to a channel via `pg_notify`.
135
180
  *
@@ -142,6 +187,31 @@ export declare class Pool {
142
187
  * ```
143
188
  */
144
189
  notify(channelName: string, payload?: string): Future<[], PostgresError>;
190
+ /**
191
+ * Asynchronously subscribes to pub/sub events on a specific PostgreSQL channel (LISTEN).
192
+ *
193
+ * This method automatically claims a dedicated connection from the pool, registers the callback
194
+ * to handle incoming asynchronous database notices (`NotificationResponse` packets), and returns
195
+ * a lazy unsubscribe function wrapped in a `Future`.
196
+ *
197
+ * Invoking the returned unsubscribe function will automatically issue the `UNLISTEN` command
198
+ * to the database backend, clean up the memory callback, and safely release the connection back to the pool.
199
+ *
200
+ * @param {string} channel The name of the PostgreSQL notification channel.
201
+ * @param {(payload: string) => void} callback The event handler invoked when a NOTIFY message arrives.
202
+ * @returns {Future<() => Promise<void>, PostgresError>} A `Future` resolving to an async unsubscribe function.
203
+ *
204
+ * @example
205
+ * // Subscribing to database events directly from the Pool
206
+ * const unlisten = await pool.listen('order_created', (payload) => {
207
+ * const order = JSON.parse(payload);
208
+ * console.log(`New order received: ${order.id}`);
209
+ * });
210
+ *
211
+ * // When the subscription is no longer needed (e.g., during teardown or server stop):
212
+ * await unlisten(); // The socket cleanly issues UNLISTEN and returns to the pool of free connections.
213
+ */
214
+ listen(channel: string, callback: (payload: string) => void): Future<() => Promise<void>, PostgresError>;
145
215
  /**
146
216
  * Number of available (idle) connections in the pool.
147
217
  */
@@ -1 +1 @@
1
- {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAG3C,OAAO,EAAE,MAAM,EAAW,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,KAAK,UAAU,GAAG,gBAAgB,GAAG;IACjC,GAAG,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,IAAI;IACb,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,SAAS,CAAQ;IAEzB,OAAO,CAAC,YAAY;gBAIR,MAAM,EAAE,UAAU;IAM9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO;IA0BP;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,IAAI,EAAE,UAAU;IAoBxB;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC;IAW7D;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE;IAiCpF;;;;;;;;;;OAUG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW,GACuB,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC;IAIhG;;OAEG;IACH,IAAI,IAAI,WAEP;IAGD;;;OAGG;IACH,IAAI,KAAK,WAER;IAGD;;;;;;;;;;OAUG;IACH,KAAK;CAeR"}
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAG3C,OAAO,EAAS,MAAM,EAAW,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,KAAK,UAAU,GAAG,gBAAgB,GAAG;IACjC,GAAG,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,IAAI;IACb,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,SAAS,CAAQ;IAEzB,OAAO,CAAC,YAAY;gBAIR,MAAM,EAAE,UAAU;IAM9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO;IA2BP;;;;;;;;;;;;;;;;;OAiBG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC;IAUnD;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,IAAI,EAAE,UAAU;IAiBxB;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC;IAY7D;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE;IAuBpF;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC;IAoCzG;;;;;;;;;;OAUG;IACH,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW,GACuB,MAAM,CAAC,EAAE,EAAE,aAAa,CAAC;IAIhG;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;IAY3D;;OAEG;IACH,IAAI,IAAI,WAEP;IAGD;;;OAGG;IACH,IAAI,KAAK,WAER;IAGD;;;;;;;;;;OAUG;IACH,KAAK;CAWR"}