@m2k-5f/pgtx 2.7.3 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,37 +50,48 @@ Benchmarks run on GitHub Actions (Ubuntu, 2 vCPUs), reproducible, sources in thi
50
50
 
51
51
  Bun.sql is Bun's own built-in driver, written in native code and generally treated as the speed baseline in that ecosystem. Pgtx stays ahead of it at every concurrency level tested — the gap doesn't come from JS-vs-native, it comes from the protocol implementation.
52
52
 
53
- **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.
53
+ **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. None of this requires you to change how you write queries.
54
+
54
55
 
55
56
  ## The parts worth knowing about
56
57
 
57
58
 
58
- ### Every query is its own commit boundary
59
+ ### Error handling
60
+
61
+ Every query returns a `Future<T[], PostgresError>` instead of a bare `Promise`. `await` works as usual, while `Future` also provides typed error handling and recovery without a `try/catch` pyramid.
59
62
 
60
- Fire off several queries in the same batch and one of them fails — the others aren't affected. Each query gets its own Sync, which means its own implicit transaction: an error in query B doesn't undo query A, even if A already returned and B is still in flight on the same pipelined write.
63
+ Queries in the same pipeline are isolated from each other. Each query gets its own `Sync`, so an error in one query does not affect other queries that were sent in the same batch.
61
64
 
62
65
  ```typescript
63
66
  const [a, b] = await Promise.allSettled([
64
67
  pool.execute`UPDATE accounts SET balance = balance + 100 WHERE id = ${1}`,
65
68
  pool.execute`INSERT INTO accounts (id) VALUES (${1})` // duplicate key, fails
66
69
  ])
67
- // a: fulfilled, the balance update is committed regardless of b's outcome
68
- ```
69
70
 
70
- This isolation is per-statement, not free-standing atomicity across statements if you need several queries to succeed or fail together, that's what `begin()` and `savepoint()` are for. Outside a transaction, every query stands on its own.
71
-
72
-
73
- ### Errors you can pattern-match on
71
+ // a: fulfilledthe balance update is committed
72
+ // b: rejected — the duplicate key error only affects this query
73
+ ```
74
74
 
75
- 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:
75
+ Errors can be matched and recovered directly on the `Future`:
76
76
 
77
77
  ```typescript
78
- const users = await pool.query<User>`SELECT * FROM users WHERE id = ${1}`
79
- .recoverIf(err => err.code === '42P01', []) // undefined_table → []
80
- .recoverIf(err => err.code === '23505', []) // unique_violation → []
78
+ const users = await pool.query<User>`
79
+ SELECT * FROM users WHERE id = ${1}
80
+ `
81
+ .recoverIf(err => err.code === '42P01', []) // undefined_table → []
82
+ .recoverIf(err => err.code === '23505', []) // unique_violation → []
81
83
  .tapErr(err => logger.error(err))
82
84
  ```
83
85
 
86
+ This isolation is per statement, not atomicity across multiple statements. If several queries must succeed or fail together, use `begin()` and `savepoint()`.
87
+
88
+
89
+ ### PostgreSQL Type Support
90
+
91
+ The driver provides **100% support for PostgreSQL data types using the full binary protocol**. All supported types are encoded and decoded directly in PostgreSQL's binary wire format, without falling back to text-based parsing.
92
+
93
+ For a complete list of supported PostgreSQL types, their JavaScript input/output types, and string formats, see the [PostgreSQL Data Types](./DATATYPES.md) reference.
94
+
84
95
 
85
96
  ### Transactions and savepoints
86
97
 
@@ -26,9 +26,11 @@ export declare class Connection {
26
26
  private _listeningCallbacks;
27
27
  private _stmtCounter;
28
28
  private _nextStatement;
29
+ private _logError;
30
+ private _logQuery;
31
+ private _logNotice;
29
32
  private _registerShedule;
30
33
  private _shedule;
31
- private _parseQuery;
32
34
  private _registerQuery;
33
35
  private constructor();
34
36
  /**
@@ -86,6 +88,7 @@ export declare class Connection {
86
88
  get isOpened(): boolean;
87
89
  /** Whether the connection is closed or closing. */
88
90
  get isClosed(): boolean;
91
+ private _tryFinishClosing;
89
92
  /**
90
93
  * Closes the connection, awaiting for all pending queries. Not usable afterward.
91
94
  */
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiC,uBAAuB,EAAuC,GAAG,EAAiB,MAAM,SAAS,CAAA;AACzI,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAK3C,OAAO,EAAS,MAAM,EAAM,MAAM,eAAe,CAAA;AACjD,OAAO,EAAkD,aAAa,EAAE,MAAM,SAAS,CAAA;AAevF;;;;;;;;;GASG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IAEzC,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,MAAM,CAA6B;IAE3C,OAAO,CAAC,QAAQ,CAAsD;IACtE,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,aAAa,CAA2C;IAEhE,OAAO,CAAC,OAAO,CAAiB;IAEhC,OAAO,CAAC,OAAO,CAAuC;IACtD,OAAO,CAAC,QAAQ,CAAuC;IAEvD,OAAO,CAAC,mBAAmB,CAAyD;IACpF,OAAO,CAAC,YAAY,CAAI;IAExB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,cAAc;IAYtB,OAAO;IAYP;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,uBAAuB;IAkB1C;;;;;;OAMG;IACH,KAAK,CAAC,CAAC,SAAS,GAAG,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IAatE,OAAO,CAAC,aAAa;IA4CrB;;;;;OAKG;IACH,OAAO,CAAC,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IAazD,OAAO,CAAC,eAAe;IA2CvB;;;;;;OAMG;IACH,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IA2BvE,OAAO,CAAC,cAAc;IA8CtB;;;;;;;OAOG;IACH,KAAK,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC;IAoB7D,2EAA2E;IAC3E,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW;IAOhD,sFAAsF;IACtF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;IAe/D,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC;IAoB/F,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,KAAK,aAAa,GAExB;IAGD,OAAO,CAAC,aAAa;IAgIrB,kDAAkD;IAClD,IAAI,QAAQ,YAEX;IAGD,mDAAmD;IACnD,IAAI,QAAQ,YAEX;IAGD;;OAEG;IACH,KAAK;CAwBR"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiC,uBAAuB,EAAuC,GAAG,EAAiB,MAAM,SAAS,CAAA;AACzI,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAK3C,OAAO,EAAS,MAAM,EAAM,MAAM,eAAe,CAAA;AACjD,OAAO,EAAkD,aAAa,EAAE,MAAM,SAAS,CAAA;AAgBvF;;;;;;;;;GASG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IAEzC,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,MAAM,CAA6B;IAE3C,OAAO,CAAC,QAAQ,CAAsD;IACtE,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,aAAa,CAA2C;IAEhE,OAAO,CAAC,OAAO,CAAiB;IAEhC,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,QAAQ,CAA6D;IAE7E,OAAO,CAAC,mBAAmB,CAAyD;IACpF,OAAO,CAAC,YAAY,CAAI;IAExB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,cAAc;IA6BtB,OAAO;IAYP;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,uBAAuB;IAkB1C;;;;;;OAMG;IACH,KAAK,CAAC,CAAC,SAAS,GAAG,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IAatE,OAAO,CAAC,aAAa;IAmDrB;;;;;OAKG;IACH,OAAO,CAAC,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IAazD,OAAO,CAAC,eAAe;IAmDvB;;;;;;OAMG;IACH,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,SAAS,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE;IA2BvE,OAAO,CAAC,cAAc;IAwDtB;;;;;;;OAOG;IACH,KAAK,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC;IAoB7D,2EAA2E;IAC3E,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,MAAW;IAOhD,sFAAsF;IACtF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI;IAe/D,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC;IAoB/F,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,KAAK,aAAa,GAExB;IAGD,OAAO,CAAC,aAAa;IA4GrB,kDAAkD;IAClD,IAAI,QAAQ,YAEX;IAGD,mDAAmD;IACnD,IAAI,QAAQ,YAEX;IAGD,OAAO,CAAC,iBAAiB;IAczB;;OAEG;IACH,KAAK;CAoBR"}
@@ -3,7 +3,7 @@ import { compileSqlTemplate } from "./utils";
3
3
  import { Transaction } from "./transaction";
4
4
  import { SocketConnector } from "./protocol/socket-connector";
5
5
  import { Queue } from "./queue";
6
- import { CollectQuery, StreamQuery, ExecuteQuery } from "./query";
6
+ import { CollectQuery, StreamQuery, ExecuteQuery, ParseQuery } from "./query";
7
7
  import { sql } from ".";
8
8
  import { Begin, Future, Ok } from 'fluent-future';
9
9
  import { ErrConnectionClosed, ErrConnectionReconnecting } from "./error";
@@ -29,6 +29,28 @@ export class Connection {
29
29
  _nextStatement() {
30
30
  return `s-${this._stmtCounter++}`;
31
31
  }
32
+ _logError(error) {
33
+ if (this.config.logLevel === 'error' || this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
34
+ console.log(`\n\x1b[31m┌─ ERROR ────────────────────────────────────────\x1b[0m\n`
35
+ + `\x1b[31m│\x1b[0m ${error}\n`
36
+ + `\x1b[31m└────────────────────────────────────────────────\x1b[0m\n`);
37
+ }
38
+ }
39
+ _logQuery(text, args) {
40
+ if (this.config.logLevel === 'query') {
41
+ console.log(`\n\x1b[36m┌─ QUERY ─────────────────────────────────────────\x1b[0m\n`
42
+ + `\x1b[36m│\x1b[0m ${text}\n`
43
+ + `${args.length !== 0 ? `\x1b[36m│\x1b[0m \x1b[90mArguments:\x1b[0m [${args}]\n` : ''}`
44
+ + `\x1b[36m└────────────────────────────────────────────────\x1b[0m`);
45
+ }
46
+ }
47
+ _logNotice(notice) {
48
+ if (this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
49
+ console.log(`\n\x1b[33m┌─ NOTICE ───────────────────────────────────────\x1b[0m\n`
50
+ + `\x1b[33m│\x1b[0m ${notice}\n`
51
+ + `\x1b[33m└────────────────────────────────────────────────\x1b[0m\n`);
52
+ }
53
+ }
32
54
  _registerShedule() {
33
55
  if (!this._sheduled) {
34
56
  this._sheduled = true;
@@ -39,23 +61,29 @@ export class Connection {
39
61
  _shedule() {
40
62
  if (this._reconnecting)
41
63
  return;
42
- this._socket.write(this._writer);
64
+ this._writer.hasMore && this._socket.write(this._writer);
43
65
  this._writer.clear();
44
66
  this._sheduled = false;
45
67
  }
46
- _parseQuery(query) {
47
- this._registerShedule();
48
- this._writer
49
- .writeParse(query.statement, query.text)
50
- .writeDescribe(DescribeType.Statement, query.statement);
51
- }
52
68
  _registerQuery(query) {
53
69
  this._registerShedule();
54
- this._queue.push(query);
70
+ if (query instanceof ParseQuery) {
71
+ this._writer
72
+ .writeParse(query.meta.statement, query.text)
73
+ .writeDescribe(DescribeType.Statement, query.meta.statement)
74
+ .writeSync();
75
+ this._queue.push(query);
76
+ return null;
77
+ }
78
+ const err = this._writer
79
+ .writeBind("", query.meta, query.args);
80
+ if (err)
81
+ return err;
55
82
  this._writer
56
- .writeBind("", query.statement, query.args, this._parsed[query.text]?.parameters)
57
83
  .writeExecute("")
58
84
  .writeSync();
85
+ this._queue.push(query);
86
+ return null;
59
87
  }
60
88
  constructor(socket, config) {
61
89
  this._writer = ConnectionRequestBuffer.new(65536);
@@ -64,8 +92,8 @@ export class Connection {
64
92
  this._closing = null;
65
93
  this._closed = false;
66
94
  this._reconnecting = null;
67
- this._parsed = {};
68
- this._parsing = {};
95
+ this._parsed = new Map();
96
+ this._parsing = new Map();
69
97
  this._listeningCallbacks = new Map();
70
98
  this._stmtCounter = 0;
71
99
  this.config = config;
@@ -106,29 +134,32 @@ export class Connection {
106
134
  return this._performQuery(text, args, resolvers);
107
135
  }
108
136
  _performQuery(text, args, resolvers) {
109
- if (this.config.logLevel === 'query') {
110
- console.log(`\n\x1b[36m┌─ QUERY ─────────────────────────────────────────\x1b[0m\n`
111
- + `\x1b[36m│\x1b[0m ${text}\n`
112
- + `${args.length !== 0 ? `\x1b[36m│\x1b[0m \x1b[90mArguments:\x1b[0m [${args}]\n` : ''}`
113
- + `\x1b[36m└────────────────────────────────────────────────\x1b[0m`);
114
- }
115
- const parsed = this._parsed[text];
137
+ this._logQuery(text, args);
138
+ const parsed = this._parsed.get(text);
116
139
  if (parsed) {
117
- const query = new CollectQuery(parsed.statement, text, args, parsed.columns, this.config.queryTimeout, resolvers);
118
- this._registerQuery(query);
140
+ const query = new CollectQuery(parsed, text, args, parsed.columns, resolvers, this.config.queryTimeout);
141
+ const err = this._registerQuery(query);
142
+ if (err) {
143
+ query.error(err);
144
+ this._logError(err);
145
+ }
119
146
  return query.resolvers.future;
120
147
  }
121
- const parsing = this._parsing[text];
122
- if (parsing) {
123
- const query = new CollectQuery(parsing, text, args, null, this.config.queryTimeout, resolvers);
124
- this._registerQuery(query);
125
- return query.resolvers.future;
148
+ if (!this._parsing.has(text)) {
149
+ const parseQuery = new ParseQuery({ statement: this._nextStatement(), columns: EMPTY_ARRAY, parameters: EMPTY_ARRAY }, text, Future.withResolvers(), this.config.queryTimeout);
150
+ this._registerQuery(parseQuery);
151
+ this._parsing.set(text, parseQuery.resolvers.future);
126
152
  }
127
- const query = new CollectQuery(this._nextStatement(), text, args, null, this.config.queryTimeout, resolvers);
128
- this._parsing[text] = query.statement;
129
- this._parseQuery(query);
130
- this._registerQuery(query);
131
- return query.resolvers.future;
153
+ const parsing = this._parsing.get(text);
154
+ return parsing.andThen(meta => {
155
+ const query = new CollectQuery(meta, text, args, meta.columns, resolvers, this.config.queryTimeout);
156
+ const err = this._registerQuery(query);
157
+ if (err) {
158
+ query.error(err);
159
+ this._logError(err);
160
+ }
161
+ return query.resolvers.future;
162
+ });
132
163
  }
133
164
  /**
134
165
  * Like {@link query}, but for statements that don't return rows (INSERT/UPDATE/DDL/etc).
@@ -146,29 +177,32 @@ export class Connection {
146
177
  return this._performExecute(text, args, resolvers);
147
178
  }
148
179
  _performExecute(text, args, resolvers) {
149
- if (this.config.logLevel === 'query') {
150
- console.log(`\n\x1b[35m┌─ EXECUTE ──────────────────────────────────────\x1b[0m\n`
151
- + `\x1b[35m│\x1b[0m ${text}\n`
152
- + `${args.length !== 0 ? `\x1b[35m│\x1b[0m \x1b[90mArguments:\x1b[0m [${args}]\n` : ''}`
153
- + `\x1b[35m└────────────────────────────────────────────────\x1b[0m`);
154
- }
155
- const parsed = this._parsed[text];
180
+ this._logQuery(text, args);
181
+ const parsed = this._parsed.get(text);
156
182
  if (parsed) {
157
- const query = new ExecuteQuery(parsed.statement, text, args, this.config.queryTimeout, resolvers);
158
- this._registerQuery(query);
183
+ const query = new ExecuteQuery(parsed, text, args, resolvers, this.config.queryTimeout);
184
+ const err = this._registerQuery(query);
185
+ if (err) {
186
+ query.error(err);
187
+ this._logError(err);
188
+ }
159
189
  return query.resolvers.future;
160
190
  }
161
- const parsing = this._parsing[text];
162
- if (parsing) {
163
- const query = new ExecuteQuery(parsing, text, args, this.config.queryTimeout, resolvers);
164
- this._registerQuery(query);
165
- return query.resolvers.future;
191
+ if (!this._parsing.has(text)) {
192
+ const parseQuery = new ParseQuery({ statement: this._nextStatement(), columns: EMPTY_ARRAY, parameters: EMPTY_ARRAY }, text, Future.withResolvers(), this.config.queryTimeout);
193
+ this._registerQuery(parseQuery);
194
+ this._parsing.set(text, parseQuery.resolvers.future);
166
195
  }
167
- const query = new ExecuteQuery(this._nextStatement(), text, args, this.config.queryTimeout, resolvers);
168
- this._parsing[text] = query.statement;
169
- this._parseQuery(query);
170
- this._registerQuery(query);
171
- return query.resolvers.future;
196
+ const parsing = this._parsing.get(text);
197
+ return parsing.andThen(meta => {
198
+ const query = new ExecuteQuery(meta, text, args, resolvers, this.config.queryTimeout);
199
+ const err = this._registerQuery(query);
200
+ if (err) {
201
+ query.error(err);
202
+ this._logError(err);
203
+ }
204
+ return query.resolvers.future;
205
+ });
172
206
  }
173
207
  /**
174
208
  * Streams query results as a `ReadableStream`, without buffering rows in memory.
@@ -197,28 +231,36 @@ export class Connection {
197
231
  return stream;
198
232
  }
199
233
  _performStream(text, args, controller) {
200
- if (this.config.logLevel === 'query') {
201
- console.log(`\n\x1b[34m┌─ STREAM ───────────────────────────────────────\x1b[0m\n`
202
- + `\x1b[34m│\x1b[0m ${text}\n`
203
- + `${args.length !== 0 ? `\x1b[34m│\x1b[0m \x1b[90mArguments:\x1b[0m [${args}]\n` : ''}`
204
- + `\x1b[34m└────────────────────────────────────────────────\x1b[0m`);
205
- }
206
- const parsed = this._parsed[text];
234
+ this._logQuery(text, args);
235
+ const parsed = this._parsed.get(text);
207
236
  if (parsed) {
208
- const query = new StreamQuery(parsed.statement, text, args, controller, parsed.columns, this.config.queryTimeout);
209
- this._registerQuery(query);
237
+ const query = new StreamQuery(parsed, text, args, controller, parsed.columns, this.config.queryTimeout);
238
+ const err = this._registerQuery(query);
239
+ if (err) {
240
+ controller.error(err);
241
+ this._logError(err);
242
+ }
210
243
  return;
211
244
  }
212
- const parsing = this._parsing[text];
213
- if (parsing) {
214
- const query = new StreamQuery(parsing, text, args, controller, null, this.config.queryTimeout);
215
- this._registerQuery(query);
216
- return;
245
+ if (!this._parsing.has(text)) {
246
+ const parseQuery = new ParseQuery({ statement: this._nextStatement(), columns: EMPTY_ARRAY, parameters: EMPTY_ARRAY }, text, Future.withResolvers(), this.config.queryTimeout);
247
+ this._registerQuery(parseQuery);
248
+ this._parsing.set(text, parseQuery.resolvers.future);
217
249
  }
218
- const query = new StreamQuery(this._nextStatement(), text, args, controller, null, this.config.queryTimeout);
219
- this._parsing[text] = query.statement;
220
- this._parseQuery(query);
221
- this._registerQuery(query);
250
+ const parsing = this._parsing.get(text);
251
+ parsing
252
+ .tap(meta => {
253
+ const query = new StreamQuery(meta, text, args, controller, null, this.config.queryTimeout);
254
+ const err = this._registerQuery(query);
255
+ if (err) {
256
+ controller.error(err);
257
+ this._logError(err);
258
+ }
259
+ })
260
+ .tapErr(err => {
261
+ controller.error(err);
262
+ })
263
+ .recover();
222
264
  }
223
265
  /**
224
266
  * Runs `txCallback` inside `BEGIN`/`COMMIT`, rolling back on error.
@@ -294,8 +336,8 @@ export class Connection {
294
336
  }
295
337
  _performReconnect() {
296
338
  this._socket.destroy();
297
- this._parsed = {};
298
- this._parsing = {};
339
+ this._parsed.clear();
340
+ this._parsing.clear();
299
341
  this._sheduled = false;
300
342
  while (this._queue.hasMore) {
301
343
  this._queue.shift.error(ErrConnectionReconnecting);
@@ -324,37 +366,31 @@ export class Connection {
324
366
  _handlePacket(type, length, reader) {
325
367
  switch (type) {
326
368
  case ResponseTypes.ParseComplete:
327
- case ResponseTypes.NoData:
328
- case ResponseTypes.CloseComplete: break;
329
369
  case ResponseTypes.BindComplete:
370
+ case ResponseTypes.CloseComplete: break;
371
+ case ResponseTypes.ParameterDescription:
330
372
  {
331
373
  const query = this._currentQuery;
332
- if (query instanceof ExecuteQuery) {
333
- break;
334
- }
335
- if (!query.columns) {
336
- query.columns = this._parsed[query.text].columns;
337
- }
374
+ const parameters = reader.readParameterDescription();
375
+ query.meta.parameters = parameters;
338
376
  }
339
377
  break;
340
- case ResponseTypes.ParameterDescription:
378
+ case ResponseTypes.RowDescription:
341
379
  {
342
380
  const query = this._currentQuery;
343
- const description = reader.readParameterDescription();
344
- const meta = {
345
- statement: query.statement,
346
- parameters: description,
347
- columns: EMPTY_ARRAY
348
- };
349
- delete this._parsing[query.text];
350
- this._parsed[query.text] = meta;
381
+ const columns = reader.readRowDescription();
382
+ query.meta.columns = columns;
383
+ this._parsing.delete(query.text);
384
+ this._parsed.set(query.text, query.meta);
385
+ query.complete();
351
386
  }
352
387
  break;
353
- case ResponseTypes.RowDescription:
388
+ case ResponseTypes.NoData:
354
389
  {
355
390
  const query = this._currentQuery;
356
- const columns = reader.readRowDescription();
357
- this._parsed[query.text].columns = columns;
391
+ this._parsing.delete(query.text);
392
+ this._parsed.set(query.text, query.meta);
393
+ query.complete();
358
394
  }
359
395
  break;
360
396
  case ResponseTypes.DataRow:
@@ -364,7 +400,7 @@ export class Connection {
364
400
  reader.skipBytes(length);
365
401
  break;
366
402
  }
367
- query.push(reader.readDataRow(query.columns, this.config.int8toBigint));
403
+ query.push(reader.readDataRow(query.meta.columns, this.config.int8toBigint));
368
404
  }
369
405
  break;
370
406
  case ResponseTypes.ComandComplete:
@@ -377,15 +413,10 @@ export class Connection {
377
413
  case ResponseTypes.ErrorResponse:
378
414
  {
379
415
  const error = reader.readErrorResponse();
380
- if (this.config.logLevel === 'error' || this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
381
- console.log(`\n\x1b[31m┌─ ERROR ────────────────────────────────────────\x1b[0m\n`
382
- + `\x1b[31m│\x1b[0m ${error}\n`
383
- + `\x1b[31m└────────────────────────────────────────────────\x1b[0m\n`);
384
- }
416
+ this._logError(error);
385
417
  const query = this._currentQuery;
386
- if (error.isParseError) {
387
- delete this._parsing[query.text];
388
- }
418
+ if (query instanceof ParseQuery)
419
+ this._parsing.delete(query.text);
389
420
  query.error(error);
390
421
  }
391
422
  break;
@@ -393,17 +424,13 @@ export class Connection {
393
424
  {
394
425
  reader.skipBytes(length);
395
426
  this._queue.next();
396
- this._closing && this._queue.isFree && this._closing.resolve();
427
+ this._tryFinishClosing();
397
428
  }
398
429
  break;
399
430
  case ResponseTypes.Notice:
400
431
  {
401
- const message = reader.readErrorResponse();
402
- if (this.config.logLevel === 'notice' || this.config.logLevel === 'query') {
403
- console.log(`\n\x1b[33m┌─ NOTICE ───────────────────────────────────────\x1b[0m\n`
404
- + `\x1b[33m│\x1b[0m ${message}\n`
405
- + `\x1b[33m└────────────────────────────────────────────────\x1b[0m\n`);
406
- }
432
+ const notice = reader.readErrorResponse();
433
+ this._logNotice(notice);
407
434
  }
408
435
  break;
409
436
  case ResponseTypes.NotificationResponse:
@@ -430,6 +457,19 @@ export class Connection {
430
457
  get isClosed() {
431
458
  return this._closed || !!this._closing;
432
459
  }
460
+ _tryFinishClosing() {
461
+ if (!this._closing)
462
+ return;
463
+ if (this._queue.hasMore)
464
+ return;
465
+ setTimeout(() => {
466
+ if (!this._closing)
467
+ return;
468
+ if (!this._queue.hasMore) {
469
+ this._closing.resolve();
470
+ }
471
+ }, 0);
472
+ }
433
473
  /**
434
474
  * Closes the connection, awaiting for all pending queries. Not usable afterward.
435
475
  */
@@ -439,11 +479,6 @@ export class Connection {
439
479
  if (this._closing) {
440
480
  return this._closing.future;
441
481
  }
442
- if (this._queue.isFree) {
443
- this._closed = true;
444
- this._socket.destroy();
445
- return Future.resolve();
446
- }
447
482
  const closing = Future.withResolvers();
448
483
  this._closing = closing;
449
484
  closing.future.tap(() => {
@@ -451,6 +486,7 @@ export class Connection {
451
486
  this._closed = true;
452
487
  this._socket.destroy();
453
488
  });
489
+ this._tryFinishClosing();
454
490
  return closing.future;
455
491
  }
456
492
  }
package/dist/error.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DataTypeOid } from "./protocol/constants";
1
2
  export declare class PostgresError extends Error {
2
3
  message: string;
3
4
  code: string;
@@ -30,4 +31,5 @@ export declare const ErrSSLDenied: PostgresError;
30
31
  export declare const ErrDatabaseNotFound: PostgresError;
31
32
  export declare const ErrUntrustedCertificate: PostgresError;
32
33
  export declare const ErrCertificateFileNotFound: PostgresError;
34
+ export declare function createBindTypeError(index: number, expectedType: DataTypeOid, actualValue: unknown): PostgresError;
33
35
  //# sourceMappingURL=error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAc,SAAQ,KAAK;IAEhB,OAAO,EAAE,MAAM;IACxB,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,MAAM;IAChB,KAAK,EAAE,MAAM;IACb,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,MAAM;IAChB,UAAU,EAAE,MAAM;gBART,OAAO,EAAE,MAAM,EACxB,IAAI,GAAE,MAAqB,EAC3B,MAAM,GAAE,MAAW,EACnB,QAAQ,GAAE,MAAW,EACrB,KAAK,GAAE,MAAW,EAClB,IAAI,GAAE,MAAW,EACjB,QAAQ,GAAE,MAAW,EACrB,QAAQ,GAAE,MAAW,EACrB,UAAU,GAAE,MAAW;IAMlC,IAAI,YAAY,IAAI,OAAO,CAK1B;IAED,IAAI,WAAW,IAAI,OAAO,CAKzB;IAED,IAAI,uBAAuB,IAAI,OAAO,CAErC;IAED,IAAI,8BAA8B,IAAI,OAAO,CAE5C;IAED,IAAI,qBAAqB,IAAI,OAAO,CAEnC;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,mBAAmB,IAAI,OAAO,CAEjC;CACJ;AAGD,eAAO,MAAM,gBAAgB,eAAmF,CAAA;AAChH,eAAO,MAAM,mBAAmB,eAAqE,CAAA;AACrG,eAAO,MAAM,yBAAyB,eAAiD,CAAA;AAEvF,eAAO,MAAM,eAAe,eAA8C,CAAA;AAE1E,eAAO,MAAM,aAAa,eAAkE,CAAA;AAE5F,eAAO,MAAM,oBAAoB,eAA6E,CAAA;AAE9G,eAAO,MAAM,mBAAmB,eAA8E,CAAA;AAC9G,eAAO,MAAM,yBAAyB,eAA2F,CAAA;AACjI,eAAO,MAAM,YAAY,eAA4D,CAAA;AACrF,eAAO,MAAM,mBAAmB,eAAwD,CAAA;AACxF,eAAO,MAAM,uBAAuB,eAA4E,CAAA;AAChH,eAAO,MAAM,0BAA0B,eAAkF,CAAA"}
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,sBAAsB,CAAA;AAGhE,qBAAa,aAAc,SAAQ,KAAK;IAEhB,OAAO,EAAE,MAAM;IACxB,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,MAAM;IACd,QAAQ,EAAE,MAAM;IAChB,KAAK,EAAE,MAAM;IACb,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,MAAM;IAChB,UAAU,EAAE,MAAM;gBART,OAAO,EAAE,MAAM,EACxB,IAAI,GAAE,MAAqB,EAC3B,MAAM,GAAE,MAAW,EACnB,QAAQ,GAAE,MAAW,EACrB,KAAK,GAAE,MAAW,EAClB,IAAI,GAAE,MAAW,EACjB,QAAQ,GAAE,MAAW,EACrB,QAAQ,GAAE,MAAW,EACrB,UAAU,GAAE,MAAW;IAMlC,IAAI,YAAY,IAAI,OAAO,CAK1B;IAED,IAAI,WAAW,IAAI,OAAO,CAKzB;IAED,IAAI,uBAAuB,IAAI,OAAO,CAErC;IAED,IAAI,8BAA8B,IAAI,OAAO,CAE5C;IAED,IAAI,qBAAqB,IAAI,OAAO,CAEnC;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,mBAAmB,IAAI,OAAO,CAEjC;CACJ;AAGD,eAAO,MAAM,gBAAgB,eAAmF,CAAA;AAChH,eAAO,MAAM,mBAAmB,eAAqE,CAAA;AACrG,eAAO,MAAM,yBAAyB,eAAiD,CAAA;AAEvF,eAAO,MAAM,eAAe,eAA8C,CAAA;AAE1E,eAAO,MAAM,aAAa,eAAkE,CAAA;AAE5F,eAAO,MAAM,oBAAoB,eAA6E,CAAA;AAE9G,eAAO,MAAM,mBAAmB,eAA8E,CAAA;AAC9G,eAAO,MAAM,yBAAyB,eAA2F,CAAA;AACjI,eAAO,MAAM,YAAY,eAA4D,CAAA;AACrF,eAAO,MAAM,mBAAmB,eAAwD,CAAA;AACxF,eAAO,MAAM,uBAAuB,eAA4E,CAAA;AAChH,eAAO,MAAM,0BAA0B,eAAkF,CAAA;AAgCzH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,GAAG,aAAa,CAqBjH"}
package/dist/error.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { handlers } from "./protocol/types";
1
2
  export class PostgresError extends Error {
2
3
  constructor(message, code = 'undeclared', detail = '', severity = '', where = '', hint = '', position = '', dataType = '', constraint = '') {
3
4
  super(message);
@@ -51,3 +52,43 @@ export const ErrSSLDenied = new PostgresError("SSL is required but server denied
51
52
  export const ErrDatabaseNotFound = new PostgresError('Database with that dsn not found');
52
53
  export const ErrUntrustedCertificate = new PostgresError("Database SSL certificate is untrusted or self-signed");
53
54
  export const ErrCertificateFileNotFound = new PostgresError("The SSL certificate file specified in caPath was not found");
55
+ function describeValue(value) {
56
+ if (value === null)
57
+ return 'null';
58
+ if (value === undefined)
59
+ return 'undefined';
60
+ if (typeof value === 'bigint')
61
+ return `${value}n`;
62
+ if (typeof value === 'string') {
63
+ return value.length > 60 ? `${JSON.stringify(value.slice(0, 60))}…` : JSON.stringify(value);
64
+ }
65
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
66
+ return `Uint8Array(length=${value.length})`;
67
+ }
68
+ if (value instanceof Date) {
69
+ return `Date(${isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString()})`;
70
+ }
71
+ if (Array.isArray(value)) {
72
+ const preview = value.slice(0, 5).map(describeValue).join(', ');
73
+ return `[${preview}${value.length > 5 ? ', …' : ''}] (length=${value.length})`;
74
+ }
75
+ if (typeof value === 'object') {
76
+ try {
77
+ const json = JSON.stringify(value);
78
+ return json.length > 120 ? `${json.slice(0, 120)}…` : json;
79
+ }
80
+ catch {
81
+ return Object.prototype.toString.call(value);
82
+ }
83
+ }
84
+ return String(value);
85
+ }
86
+ export function createBindTypeError(index, expectedType, actualValue) {
87
+ const position = index + 1;
88
+ const info = handlers[expectedType];
89
+ const expectedPgType = info?.pgType ?? `oid ${expectedType}`;
90
+ const expectedJsShape = info?.jsShape ?? '(тип не описан в справочнике)';
91
+ const message = `Bind error: Parameter $${position} type mismatch. ` +
92
+ `Expected PostgreSQL type "${expectedPgType}" (${expectedJsShape}), received: ${describeValue(actualValue)}.`;
93
+ return new PostgresError(message, '22000', `Parameter $${position} failed client-side binary validation for pg type "${expectedPgType}".`, 'ERROR', 'writeBind() inside driver', `Pass a value matching ${expectedJsShape} for $${position}.`, '', expectedPgType);
94
+ }