postgrejs 3.7.0 → 3.8.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/CHANGELOG.md +16 -0
- package/connection/connection.js +13 -1
- package/connection/intl-connection.js +27 -5
- package/connection/pool.js +6 -0
- package/connection/prepared-statement.d.ts +11 -0
- package/connection/prepared-statement.js +17 -3
- package/data-types/classes/numeric.d.ts +8 -4
- package/data-types/classes/numeric.js +8 -4
- package/data-types/int2-vector-type.js +4 -1
- package/data-types/oid-vector-type.js +4 -1
- package/data-types/timestamptz-type.js +9 -0
- package/package.json +1 -1
- package/protocol/frontend.js +17 -0
- package/util/decode-binaryarray.js +8 -1
- package/util/encode-binaryarray.d.ts +12 -1
- package/util/encode-binaryarray.js +13 -2
- package/util/format-datetime.d.ts +18 -0
- package/util/format-datetime.js +33 -0
- package/util/sql-tag.js +15 -0
- package/util/stringify-for-sql.js +7 -0
- package/util/unspecified-param.d.ts +32 -0
- package/util/unspecified-param.js +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
+
### [v3.8.0](https://github.com/panates/postgrejs/compare/v3.7.0...v3.8.0) -
|
|
4
|
+
|
|
5
|
+
#### 🚀 New Features
|
|
6
|
+
|
|
7
|
+
- feat: deliver server notices to the connection @Eray Hanoğlu
|
|
8
|
+
|
|
9
|
+
#### 🪲 Fixes
|
|
10
|
+
|
|
11
|
+
- fix: send a string parameter unspecified, so the column decides @Eray Hanoğlu
|
|
12
|
+
- fix: send a Date parameter unspecified, so the column decides @Eray Hanoğlu
|
|
13
|
+
- fix: write a lower bound of 1 for an ordinary binary array @Eray Hanoğlu
|
|
14
|
+
|
|
15
|
+
#### 🧪 Changes to Test Assests
|
|
16
|
+
|
|
17
|
+
- test: skip the types PostgreSQL 12 does not have @Eray Hanoğlu
|
|
18
|
+
|
|
3
19
|
### [v3.7.0](https://github.com/panates/postgrejs/compare/v3.6.1...v3.7.0) - 20 September 2026
|
|
4
20
|
|
|
5
21
|
#### Breaking Changes :warning:
|
package/connection/connection.js
CHANGED
|
@@ -5,6 +5,7 @@ import { withAbortSignal } from '../util/abort-signal.js';
|
|
|
5
5
|
import { normalizeChannelName } from '../util/channel-name.js';
|
|
6
6
|
import { escapeIdentifier } from '../util/escape-identifier.js';
|
|
7
7
|
import { QueryRequest } from '../util/sql-tag.js';
|
|
8
|
+
import { isUnspecifiedParam } from '../util/unspecified-param.js';
|
|
8
9
|
import { BindParam } from './bind-param.js';
|
|
9
10
|
import { IntlConnection } from './intl-connection.js';
|
|
10
11
|
import { LargeObject, LargeObjectMode } from './large-object.js';
|
|
@@ -631,7 +632,18 @@ export class Connection extends SafeEventEmitter {
|
|
|
631
632
|
}
|
|
632
633
|
async _query(sql, options) {
|
|
633
634
|
const typeMap = options?.typeMap || GlobalTypeMap;
|
|
634
|
-
const paramTypes = options?.params?.map(prm => prm instanceof BindParam
|
|
635
|
+
const paramTypes = options?.params?.map(prm => prm instanceof BindParam
|
|
636
|
+
? prm.oid
|
|
637
|
+
: // A Date and a string go out with no declared type, so the
|
|
638
|
+
// server resolves each from where it lands - neither can say
|
|
639
|
+
// what it is, and naming a type for them is what made a `Date`
|
|
640
|
+
// move by the client's offset and a string unusable anywhere a
|
|
641
|
+
// `varchar` is not what the context wanted. See
|
|
642
|
+
// `isUnspecifiedParam()` for what that costs. The text they go
|
|
643
|
+
// out as is written in getBindMessage().
|
|
644
|
+
isUnspecifiedParam(prm)
|
|
645
|
+
? 0
|
|
646
|
+
: typeMap.determine(prm));
|
|
635
647
|
const effectiveAutoCommit = options?.autoCommit != null
|
|
636
648
|
? options.autoCommit
|
|
637
649
|
: this._intlCon.config.autoCommit;
|
|
@@ -124,6 +124,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
124
124
|
// ordinary shutdown from the backend being terminated under us.
|
|
125
125
|
this.socket.on('close', (reason) => this.emit('close', reason));
|
|
126
126
|
this.socket.on('notification', payload => this.emit('notification', payload));
|
|
127
|
+
// Covers a notice that arrives while no statement is running. One
|
|
128
|
+
// raised *by* a statement is dispatched to that statement's own
|
|
129
|
+
// message loop instead, which is why each of those forwards it too.
|
|
130
|
+
this.socket.on('notice', payload => this.emit('notice', payload));
|
|
127
131
|
this.socket.on('connecting', () => this.emit('connecting'));
|
|
128
132
|
this._onErrorSavePoint = 'SP_' + Math.round(Math.random() * 100000000);
|
|
129
133
|
}
|
|
@@ -561,6 +565,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
561
565
|
new Error('COPY TO STDOUT is not supported by execute() - use copyTo() instead');
|
|
562
566
|
break;
|
|
563
567
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
568
|
+
// Not an error: handed over, and the loop carries on.
|
|
569
|
+
this.emit('notice', msg);
|
|
570
|
+
break;
|
|
564
571
|
case Protocol.BackendMessageCode.EmptyQueryResponse:
|
|
565
572
|
break;
|
|
566
573
|
case Protocol.BackendMessageCode.RowDescription:
|
|
@@ -825,10 +832,13 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
825
832
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
826
833
|
}, (code, msg, done) => {
|
|
827
834
|
switch (code) {
|
|
835
|
+
case Protocol.BackendMessageCode.NoticeResponse:
|
|
836
|
+
// Not an error: handed over, and the loop carries on.
|
|
837
|
+
this.emit('notice', msg);
|
|
838
|
+
break;
|
|
828
839
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
829
840
|
case Protocol.BackendMessageCode.BindComplete:
|
|
830
841
|
case Protocol.BackendMessageCode.NoData:
|
|
831
|
-
case Protocol.BackendMessageCode.NoticeResponse:
|
|
832
842
|
break;
|
|
833
843
|
// The server stopped at the fetchCount limit rather than
|
|
834
844
|
// running out of rows. The Sync that closes this call
|
|
@@ -910,10 +920,13 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
910
920
|
describe: { type: 'S', name: statementName },
|
|
911
921
|
}, (code, msg, done) => {
|
|
912
922
|
switch (code) {
|
|
923
|
+
case Protocol.BackendMessageCode.NoticeResponse:
|
|
924
|
+
// Not an error: handed over, and the loop carries on.
|
|
925
|
+
this.emit('notice', msg);
|
|
926
|
+
break;
|
|
913
927
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
914
928
|
case Protocol.BackendMessageCode.ParameterDescription:
|
|
915
929
|
case Protocol.BackendMessageCode.NoData:
|
|
916
|
-
case Protocol.BackendMessageCode.NoticeResponse:
|
|
917
930
|
break;
|
|
918
931
|
case Protocol.BackendMessageCode.RowDescription:
|
|
919
932
|
fields = msg.fields;
|
|
@@ -1091,9 +1104,12 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1091
1104
|
await this.socket
|
|
1092
1105
|
.sendPipelineMessages({ statements }, (code, msg, done) => {
|
|
1093
1106
|
switch (code) {
|
|
1107
|
+
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1108
|
+
// Not an error: handed over, and the loop carries on.
|
|
1109
|
+
this.emit('notice', msg);
|
|
1110
|
+
break;
|
|
1094
1111
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
1095
1112
|
case Protocol.BackendMessageCode.BindComplete:
|
|
1096
|
-
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1097
1113
|
break;
|
|
1098
1114
|
case Protocol.BackendMessageCode.NoData:
|
|
1099
1115
|
pendingFields = undefined;
|
|
@@ -1234,8 +1250,11 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1234
1250
|
execute: { fetchCount: 0 },
|
|
1235
1251
|
}, (code, msg, done) => {
|
|
1236
1252
|
switch (code) {
|
|
1237
|
-
case Protocol.BackendMessageCode.BindComplete:
|
|
1238
1253
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1254
|
+
// Not an error: handed over, and the loop carries on.
|
|
1255
|
+
this.emit('notice', msg);
|
|
1256
|
+
break;
|
|
1257
|
+
case Protocol.BackendMessageCode.BindComplete:
|
|
1239
1258
|
break;
|
|
1240
1259
|
case Protocol.BackendMessageCode.DataRow:
|
|
1241
1260
|
(pendingRows || (pendingRows = [])).push(msg);
|
|
@@ -1348,9 +1367,12 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1348
1367
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
1349
1368
|
}, (code, msg, done) => {
|
|
1350
1369
|
switch (code) {
|
|
1370
|
+
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1371
|
+
// Not an error: handed over, and the loop carries on.
|
|
1372
|
+
this.emit('notice', msg);
|
|
1373
|
+
break;
|
|
1351
1374
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
1352
1375
|
case Protocol.BackendMessageCode.BindComplete:
|
|
1353
|
-
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1354
1376
|
break;
|
|
1355
1377
|
// See queryOnce() for what a suspended portal means here.
|
|
1356
1378
|
case Protocol.BackendMessageCode.PortalSuspended:
|
package/connection/pool.js
CHANGED
|
@@ -157,6 +157,12 @@ export class Pool extends SafeEventEmitter {
|
|
|
157
157
|
connection.on('execute', (...args) => this.emit('execute', ...args));
|
|
158
158
|
if (this.listenerCount('query'))
|
|
159
159
|
connection.on('query', (...args) => this.emit('query', ...args));
|
|
160
|
+
// Forwarded with the connection that raised it, the way 'destroy'
|
|
161
|
+
// names its own subject: a caller using pool.query() never sees the
|
|
162
|
+
// Connection, so without this a notice raised by their statement has
|
|
163
|
+
// nowhere to go at all.
|
|
164
|
+
if (this.listenerCount('notice'))
|
|
165
|
+
connection.on('notice', msg => this.emit('notice', msg, connection));
|
|
160
166
|
return connection;
|
|
161
167
|
}
|
|
162
168
|
/**
|
|
@@ -68,6 +68,17 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
|
|
|
68
68
|
* takes, minus `fetchCount` and `cursor`, which a batch cannot honour.
|
|
69
69
|
*/
|
|
70
70
|
executeBatch(paramSets: any[][], options?: QueryOptions): Promise<BatchResult>;
|
|
71
|
+
/**
|
|
72
|
+
* A notice reaches both this statement and the connection it ran on.
|
|
73
|
+
*
|
|
74
|
+
* These are the three loops the statement keeps for its own close and
|
|
75
|
+
* cancel traffic; an `execute()` runs through the connection's loop
|
|
76
|
+
* instead and is forwarded there. Without this second hop a notice
|
|
77
|
+
* arriving on one of these three would be the only kind a caller
|
|
78
|
+
* holding a `Connection` never saw. Hard to provoke - `DEALLOCATE`
|
|
79
|
+
* does not normally raise one - which is why there is no test for it.
|
|
80
|
+
*/
|
|
81
|
+
protected _emitNotice(msg: any): void;
|
|
71
82
|
protected _executeBatchWithTransaction(paramSets: any[][], options: QueryOptions): Promise<BatchResult>;
|
|
72
83
|
protected _executeWithTransaction(options?: QueryOptions): Promise<QueryResult>;
|
|
73
84
|
/**
|
|
@@ -114,6 +114,20 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
114
114
|
return withAbortSignal(options.signal, () => intlCon.cancel(), () => this._executeBatchWithTransaction(paramSets, options));
|
|
115
115
|
return this._executeBatchWithTransaction(paramSets, options);
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A notice reaches both this statement and the connection it ran on.
|
|
119
|
+
*
|
|
120
|
+
* These are the three loops the statement keeps for its own close and
|
|
121
|
+
* cancel traffic; an `execute()` runs through the connection's loop
|
|
122
|
+
* instead and is forwarded there. Without this second hop a notice
|
|
123
|
+
* arriving on one of these three would be the only kind a caller
|
|
124
|
+
* holding a `Connection` never saw. Hard to provoke - `DEALLOCATE`
|
|
125
|
+
* does not normally raise one - which is why there is no test for it.
|
|
126
|
+
*/
|
|
127
|
+
_emitNotice(msg) {
|
|
128
|
+
this.emit('notice', msg);
|
|
129
|
+
getIntlConnection(this.connection).emit('notice', msg);
|
|
130
|
+
}
|
|
117
131
|
_executeBatchWithTransaction(paramSets, options) {
|
|
118
132
|
const intlCon = getIntlConnection(this.connection);
|
|
119
133
|
return this._withTransaction(options, () => intlCon.executeBatchReused(this.name, this._fields, this.paramTypes, paramSets, options));
|
|
@@ -268,7 +282,7 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
268
282
|
}, (code, msg, done) => {
|
|
269
283
|
switch (code) {
|
|
270
284
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
271
|
-
this.
|
|
285
|
+
this._emitNotice(msg);
|
|
272
286
|
break;
|
|
273
287
|
case Protocol.BackendMessageCode.CloseComplete:
|
|
274
288
|
// Arrives twice (once per Close) - neither call is terminal.
|
|
@@ -299,7 +313,7 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
299
313
|
const closePromise = socket.sendCloseMessage({ type: 'S', name: this.name }, (code, msg, done) => {
|
|
300
314
|
switch (code) {
|
|
301
315
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
302
|
-
this.
|
|
316
|
+
this._emitNotice(msg);
|
|
303
317
|
break;
|
|
304
318
|
case Protocol.BackendMessageCode.CloseComplete:
|
|
305
319
|
done();
|
|
@@ -314,7 +328,7 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
314
328
|
const syncPromise = socket.sendSyncMessage((code, msg, done) => {
|
|
315
329
|
switch (code) {
|
|
316
330
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
317
|
-
this.
|
|
331
|
+
this._emitNotice(msg);
|
|
318
332
|
break;
|
|
319
333
|
case Protocol.BackendMessageCode.ReadyForQuery:
|
|
320
334
|
intoCon.transactionStatus = msg.status;
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
* to a plain number; this class is what the rest become, and it carries
|
|
10
10
|
* the digits exactly as the server wrote them.
|
|
11
11
|
*
|
|
12
|
-
* It is a class rather than a bare string so that
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* It is a class rather than a bare string so that the value keeps
|
|
13
|
+
* saying what it is. A bare string parameter does reach a numeric
|
|
14
|
+
* column - it goes out with no declared type and the server resolves it
|
|
15
|
+
* there (see `util/unspecified-param.ts`) - but only where there is a
|
|
16
|
+
* column to resolve it from: `determine()` answers `varchar` for it, an
|
|
17
|
+
* inlined literal is written with no cast, and a decoded one is
|
|
18
|
+
* indistinguishable from text. A `Numeric` carries the type with the
|
|
19
|
+
* digits.
|
|
16
20
|
*
|
|
17
21
|
* There is deliberately no `valueOf()`. Arithmetic on one of these has
|
|
18
22
|
* to be an explicit decision - `Symbol.toPrimitive` hands back the exact
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
* to a plain number; this class is what the rest become, and it carries
|
|
10
10
|
* the digits exactly as the server wrote them.
|
|
11
11
|
*
|
|
12
|
-
* It is a class rather than a bare string so that
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* It is a class rather than a bare string so that the value keeps
|
|
13
|
+
* saying what it is. A bare string parameter does reach a numeric
|
|
14
|
+
* column - it goes out with no declared type and the server resolves it
|
|
15
|
+
* there (see `util/unspecified-param.ts`) - but only where there is a
|
|
16
|
+
* column to resolve it from: `determine()` answers `varchar` for it, an
|
|
17
|
+
* inlined literal is written with no cast, and a decoded one is
|
|
18
|
+
* indistinguishable from text. A `Numeric` carries the type with the
|
|
19
|
+
* digits.
|
|
16
20
|
*
|
|
17
21
|
* There is deliberately no `valueOf()`. Arithmetic on one of these has
|
|
18
22
|
* to be an explicit decision - `Symbol.toPrimitive` hands back the exact
|
|
@@ -10,7 +10,10 @@ export const Int2VectorType = {
|
|
|
10
10
|
encodeBinary(buf, v) {
|
|
11
11
|
encodeBinaryArray(buf, v, DataTypeOIDs.int2, {}, (io, x) => {
|
|
12
12
|
io.writeInt16BE(x);
|
|
13
|
-
}
|
|
13
|
+
},
|
|
14
|
+
// A vector is genuinely 0-based, unlike every ordinary array -
|
|
15
|
+
// `array_lower(pg_index.indkey, 1)` is 0 on the server.
|
|
16
|
+
undefined, 0);
|
|
14
17
|
},
|
|
15
18
|
encodeCalculateDim(v) {
|
|
16
19
|
return [v.length];
|
|
@@ -16,7 +16,10 @@ export const OidVectorType = {
|
|
|
16
16
|
encodeBinary(buf, v) {
|
|
17
17
|
encodeBinaryArray(buf, v, DataTypeOIDs.oid, {}, (io, x) => {
|
|
18
18
|
io.writeUInt32BE(x);
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
|
+
// A vector is genuinely 0-based, unlike every ordinary array -
|
|
21
|
+
// `array_lower(pg_index.indkey, 1)` is 0 on the server.
|
|
22
|
+
undefined, 0);
|
|
20
23
|
},
|
|
21
24
|
encodeCalculateDim(v) {
|
|
22
25
|
return [v.length];
|
|
@@ -67,6 +67,15 @@ export const TimestamptzType = {
|
|
|
67
67
|
return d;
|
|
68
68
|
return TimestamptzType.decodeText(buf.toString('latin1', offset, offset + len), options);
|
|
69
69
|
},
|
|
70
|
+
/**
|
|
71
|
+
* Unreachable through `determine()`: TimestampType, TimeType and
|
|
72
|
+
* DateType are all consulted before this one and every Date is claimed
|
|
73
|
+
* by one of them. Nothing depends on it - a Date parameter goes out
|
|
74
|
+
* unspecified and the column decides what it means (see
|
|
75
|
+
* format-datetime.ts's formatDateParam) - so this stays as the
|
|
76
|
+
* truthful answer to a direct call rather than being narrowed to
|
|
77
|
+
* describe a walk it never wins.
|
|
78
|
+
*/
|
|
70
79
|
isType(v) {
|
|
71
80
|
return v instanceof Date;
|
|
72
81
|
},
|
package/package.json
CHANGED
package/protocol/frontend.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
|
|
2
2
|
import { DataTypeMap } from '../data-type-map.js';
|
|
3
3
|
import { encodeBinaryArray } from '../util/encode-binaryarray.js';
|
|
4
|
+
import { formatDateParam } from '../util/format-datetime.js';
|
|
4
5
|
import { stringifyArrayLiteral } from '../util/stringify-arrayliteral.js';
|
|
6
|
+
import { arrayLeaf } from '../util/unspecified-param.js';
|
|
5
7
|
import { Protocol } from './protocol.js';
|
|
6
8
|
import { SmartBuffer } from './smart-buffer.js';
|
|
7
9
|
const DataFormat = Protocol.DataFormat;
|
|
@@ -186,6 +188,21 @@ export class Frontend {
|
|
|
186
188
|
io.writeLString(v, 'utf8');
|
|
187
189
|
}
|
|
188
190
|
}
|
|
191
|
+
else if (v instanceof Date) {
|
|
192
|
+
// No declared type, so the server resolves it from the column
|
|
193
|
+
// - which is the whole point, and why the text has to carry
|
|
194
|
+
// the zone. `String(v)` is a JavaScript date string the server
|
|
195
|
+
// cannot parse at all, which is what a prepared statement with
|
|
196
|
+
// no paramTypes used to send.
|
|
197
|
+
io.writeLString(formatDateParam(v, queryOptions), 'utf8');
|
|
198
|
+
}
|
|
199
|
+
else if (Array.isArray(v)) {
|
|
200
|
+
// Also undeclared, so the server reads an array literal and
|
|
201
|
+
// takes the element type from the column. `'' + v` would hand
|
|
202
|
+
// it `a,b`, which is not one - the elements have to be quoted
|
|
203
|
+
// and the braces written, and a nested array needs its own.
|
|
204
|
+
io.writeLString(stringifyArrayLiteral(v, queryOptions, arrayLeaf(v) instanceof Date ? formatDateParam : undefined), 'utf8');
|
|
205
|
+
}
|
|
189
206
|
else if (Buffer.isBuffer(v)) {
|
|
190
207
|
// Set param format to binary
|
|
191
208
|
io.buffer.writeInt16BE(Protocol.DataFormat.binary, formatOffset + i * 2);
|
|
@@ -38,7 +38,14 @@ export function decodeBinaryArray(buf, offset, decoder, options = {}) {
|
|
|
38
38
|
};
|
|
39
39
|
for (let d = 0; d < ndims; d++) {
|
|
40
40
|
dims[d] = io.readInt32BE();
|
|
41
|
-
|
|
41
|
+
// The lower bound is read and dropped, deliberately. A server-side
|
|
42
|
+
// array really can have a non-default one, and a JavaScript array is
|
|
43
|
+
// always 0-based, so carrying it would mean deciding what a decoded
|
|
44
|
+
// array is supposed to represent - a wider change than the encoder's.
|
|
45
|
+
// Note that this is also what hid the encoder writing 0: a value
|
|
46
|
+
// written and read back through this client looked right, and only
|
|
47
|
+
// SQL against the stored value saw the difference.
|
|
48
|
+
io.readInt32BE();
|
|
42
49
|
}
|
|
43
50
|
return readDim(0);
|
|
44
51
|
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
|
|
2
2
|
import type { SmartBuffer } from '../protocol/smart-buffer.js';
|
|
3
3
|
import type { EncodeBinaryFunction, EncodeCalculateDimFunction, OID } from '../types.js';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Writes an array in the binary wire format.
|
|
6
|
+
*
|
|
7
|
+
* `lowerBound` is the index the first element answers to in SQL, and it
|
|
8
|
+
* is 1 for every ordinary array - `array_lower('{1,2}'::int4[], 1)` is
|
|
9
|
+
* 1, and a value written with 0 comes back subscripted from 0, so
|
|
10
|
+
* `arr[1]` is its *second* element and `::text` renders the
|
|
11
|
+
* explicit-bounds form `[0:2]={…}`. `oidvector` and `int2vector` are the
|
|
12
|
+
* exception and really are 0-based - confirmed against the catalog,
|
|
13
|
+
* `array_lower(pg_index.indkey, 1)` is 0 - so they pass 0 themselves.
|
|
14
|
+
*/
|
|
15
|
+
export declare function encodeBinaryArray(io: SmartBuffer, value: any[], itemOid: OID, options: DataMappingOptions, encode: EncodeBinaryFunction, encodeCalculateDimFn?: EncodeCalculateDimFunction, lowerBound?: number): void;
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
2
|
import { arrayCalculateDim } from './array-calculatedim.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Writes an array in the binary wire format.
|
|
5
|
+
*
|
|
6
|
+
* `lowerBound` is the index the first element answers to in SQL, and it
|
|
7
|
+
* is 1 for every ordinary array - `array_lower('{1,2}'::int4[], 1)` is
|
|
8
|
+
* 1, and a value written with 0 comes back subscripted from 0, so
|
|
9
|
+
* `arr[1]` is its *second* element and `::text` renders the
|
|
10
|
+
* explicit-bounds form `[0:2]={…}`. `oidvector` and `int2vector` are the
|
|
11
|
+
* exception and really are 0-based - confirmed against the catalog,
|
|
12
|
+
* `array_lower(pg_index.indkey, 1)` is 0 - so they pass 0 themselves.
|
|
13
|
+
*/
|
|
14
|
+
export function encodeBinaryArray(io, value, itemOid, options, encode, encodeCalculateDimFn, lowerBound = 1) {
|
|
4
15
|
encodeCalculateDimFn = encodeCalculateDimFn || arrayCalculateDim;
|
|
5
16
|
itemOid = itemOid || DataTypeOIDs.varchar;
|
|
6
17
|
const dim = encodeCalculateDimFn(value);
|
|
@@ -12,7 +23,7 @@ export function encodeBinaryArray(io, value, itemOid, options, encode, encodeCal
|
|
|
12
23
|
let d;
|
|
13
24
|
for (d = 0; d < ndims; d++) {
|
|
14
25
|
io.writeInt32BE(dim[d]); // Number of items in dimension
|
|
15
|
-
io.writeInt32BE(
|
|
26
|
+
io.writeInt32BE(lowerBound); // Index the first element answers to
|
|
16
27
|
}
|
|
17
28
|
let hasNull = false;
|
|
18
29
|
let pos;
|
|
@@ -8,3 +8,21 @@ export declare function formatTimestamp(v: Date | number | string, options?: Dat
|
|
|
8
8
|
* ignores `utcDates`, which only governs the zone-less types.
|
|
9
9
|
*/
|
|
10
10
|
export declare function formatTimestamptz(v: Date | number | string): string;
|
|
11
|
+
/**
|
|
12
|
+
* A `Date` as a parameter, rendered the way `pg` renders one: the wall
|
|
13
|
+
* clock in the process's own zone, followed by that zone's offset.
|
|
14
|
+
*
|
|
15
|
+
* It is the only rendering that is right for both column kinds at once,
|
|
16
|
+
* which is why the parameter path uses it instead of declaring a type.
|
|
17
|
+
* A `timestamptz` column honours the offset and stores the instant; a
|
|
18
|
+
* `timestamp` column discards it and keeps the wall clock - so the
|
|
19
|
+
* server decides, from the column, what one JavaScript `Date` could
|
|
20
|
+
* never say on its own. Declaring `timestamp` instead moved the instant
|
|
21
|
+
* by the client's offset, and declaring `timestamptz` moved the wall
|
|
22
|
+
* clock by it; measured against `pg` in three session zones, this
|
|
23
|
+
* matches in all six.
|
|
24
|
+
*
|
|
25
|
+
* Under `utcDates` the UTC components go out with a zero offset, which
|
|
26
|
+
* is the same rule the zone-less encoders follow.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatDateParam(v: Date | number | string, options?: DataMappingOptions): string;
|
package/util/format-datetime.js
CHANGED
|
@@ -69,3 +69,36 @@ export function formatTimestamptz(v) {
|
|
|
69
69
|
return d;
|
|
70
70
|
return d.toISOString().replace('T', ' ').replace('Z', '+00');
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* A `Date` as a parameter, rendered the way `pg` renders one: the wall
|
|
74
|
+
* clock in the process's own zone, followed by that zone's offset.
|
|
75
|
+
*
|
|
76
|
+
* It is the only rendering that is right for both column kinds at once,
|
|
77
|
+
* which is why the parameter path uses it instead of declaring a type.
|
|
78
|
+
* A `timestamptz` column honours the offset and stores the instant; a
|
|
79
|
+
* `timestamp` column discards it and keeps the wall clock - so the
|
|
80
|
+
* server decides, from the column, what one JavaScript `Date` could
|
|
81
|
+
* never say on its own. Declaring `timestamp` instead moved the instant
|
|
82
|
+
* by the client's offset, and declaring `timestamptz` moved the wall
|
|
83
|
+
* clock by it; measured against `pg` in three session zones, this
|
|
84
|
+
* matches in all six.
|
|
85
|
+
*
|
|
86
|
+
* Under `utcDates` the UTC components go out with a zero offset, which
|
|
87
|
+
* is the same rule the zone-less encoders follow.
|
|
88
|
+
*/
|
|
89
|
+
export function formatDateParam(v, options) {
|
|
90
|
+
const d = coerce(v);
|
|
91
|
+
if (typeof d === 'string')
|
|
92
|
+
return d;
|
|
93
|
+
const p = parts(d, options);
|
|
94
|
+
const clock = `${pad(p.year, 4)}-${pad(p.month)}-${pad(p.day)}` +
|
|
95
|
+
` ${pad(p.hour)}:${pad(p.minute)}:${pad(p.second)}.${pad(p.ms, 3)}`;
|
|
96
|
+
if (options?.utcDates)
|
|
97
|
+
return clock + '+00:00';
|
|
98
|
+
// getTimezoneOffset() counts minutes west of UTC, the opposite way
|
|
99
|
+
// round from how an offset is written.
|
|
100
|
+
const offset = -d.getTimezoneOffset();
|
|
101
|
+
const sign = offset < 0 ? '-' : '+';
|
|
102
|
+
const abs = Math.abs(offset);
|
|
103
|
+
return `${clock}${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
104
|
+
}
|
package/util/sql-tag.js
CHANGED
|
@@ -2,6 +2,9 @@ import { BindParam } from '../connection/bind-param.js';
|
|
|
2
2
|
import { DataTypeMap, GlobalTypeMap } from '../data-type-map.js';
|
|
3
3
|
import { escapeIdentifier } from './escape-identifier.js';
|
|
4
4
|
import { escapeLiteral } from './escape-literal.js';
|
|
5
|
+
import { formatDateParam } from './format-datetime.js';
|
|
6
|
+
import { stringifyArrayLiteral } from './stringify-arrayliteral.js';
|
|
7
|
+
import { arrayLeaf, isUnspecifiedParam } from './unspecified-param.js';
|
|
5
8
|
/**
|
|
6
9
|
* A statement built by the `sql` tag: its text with the values pulled out.
|
|
7
10
|
*
|
|
@@ -171,6 +174,18 @@ function encodeLiteral(value, typeMap, options) {
|
|
|
171
174
|
}
|
|
172
175
|
if (v == null)
|
|
173
176
|
return 'null';
|
|
177
|
+
// A Date and a string go in bare, exactly as they go out as
|
|
178
|
+
// parameters with no declared type (see unspecified-param.ts): an
|
|
179
|
+
// unadorned literal is `unknown` and takes the type of wherever it
|
|
180
|
+
// lands. The cast this path adds everywhere else is what keeps
|
|
181
|
+
// query() and execute() meaning the same thing, and for these two it
|
|
182
|
+
// is what would break that - `'{"a":1}'::varchar` is refused by a
|
|
183
|
+
// json column that takes the parameter without complaint.
|
|
184
|
+
if (oid == null && isUnspecifiedParam(v)) {
|
|
185
|
+
if (!Array.isArray(v))
|
|
186
|
+
return escapeLiteral(v instanceof Date ? formatDateParam(v, options) : '' + v);
|
|
187
|
+
return escapeLiteral(stringifyArrayLiteral(v, options, arrayLeaf(v) instanceof Date ? formatDateParam : undefined));
|
|
188
|
+
}
|
|
174
189
|
oid = oid ?? typeMap.determine(v);
|
|
175
190
|
/* c8 ignore start - typeMap.determine() always falls back to the
|
|
176
191
|
"unknown" oid rather than returning null/undefined, so this can't
|
|
@@ -2,6 +2,7 @@ import { DataTypeOIDs } from '../constants.js';
|
|
|
2
2
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
3
3
|
import { UuidType } from '../data-types/uuid-type.js';
|
|
4
4
|
import { escapeLiteral } from './escape-literal.js';
|
|
5
|
+
import { formatDateParam } from './format-datetime.js';
|
|
5
6
|
export function stringifyArrayForSQL(v, options, encode) {
|
|
6
7
|
const arr = v.map(x => stringifyValueForSQL(x, options, encode));
|
|
7
8
|
return 'ARRAY[' + arr.join(',') + ']';
|
|
@@ -51,6 +52,12 @@ export function stringifyValueForSQL(v, options, encode) {
|
|
|
51
52
|
return v.toString();
|
|
52
53
|
if (typeof v === 'string' && UuidType.isType(v))
|
|
53
54
|
return escapeLiteral('' + v) + '::uuid';
|
|
55
|
+
// A Date is written bare, with its offset and no cast, so that it
|
|
56
|
+
// means the same inlined as it does bound - the server resolves it
|
|
57
|
+
// from the column either way. See format-datetime.ts's
|
|
58
|
+
// formatDateParam().
|
|
59
|
+
if (v instanceof Date)
|
|
60
|
+
return escapeLiteral(formatDateParam(v, options));
|
|
54
61
|
if (typeof v === 'object')
|
|
55
62
|
return stringifyObjectForSQL(v, options);
|
|
56
63
|
// A string is deliberately written bare. determine() would answer
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first value inside `v` that is not itself an array - what an array
|
|
3
|
+
* parameter's element type would be read from. A scalar answers itself.
|
|
4
|
+
*/
|
|
5
|
+
export declare function arrayLeaf(v: any): any;
|
|
6
|
+
/**
|
|
7
|
+
* Whether a parameter goes out with no declared type (OID 0), as text,
|
|
8
|
+
* for the server to resolve from wherever it lands.
|
|
9
|
+
*
|
|
10
|
+
* Two kinds of value cannot say what they are:
|
|
11
|
+
*
|
|
12
|
+
* - A `Date` is an instant or a wall clock depending on the column, and
|
|
13
|
+
* declaring either one moves the other. See `format-datetime.ts`.
|
|
14
|
+
* - A string is whichever of PostgreSQL's text-input types the context
|
|
15
|
+
* calls for. `determine()` answers `varchar`, and declaring that stops
|
|
16
|
+
* the server inferring: `insert into t(j) values($1)` into a `json`
|
|
17
|
+
* column, `id = $1` against a `uuid`, `coalesce($1, 1)` and an enum
|
|
18
|
+
* column all fail with "expression is of type character varying".
|
|
19
|
+
* Unspecified, every one of them works.
|
|
20
|
+
*
|
|
21
|
+
* This is what `pg` sends for both, and the divergence from it is what
|
|
22
|
+
* the shapes above were failing on. It is not free: a parameter with no
|
|
23
|
+
* context to be resolved from - `$1 is null`, `array_agg($1)`,
|
|
24
|
+
* `concat($1, 1)` - now raises "could not determine data type", exactly
|
|
25
|
+
* as it does under `pg`. A cast (`$1::text`) or a named type
|
|
26
|
+
* (`new BindParam(DataTypeOIDs.varchar, v)`) says which type is meant.
|
|
27
|
+
*
|
|
28
|
+
* Numbers, booleans and Buffers keep their declared types: they are
|
|
29
|
+
* already right nearly everywhere, and sending them as text would give
|
|
30
|
+
* up the binary encoding for no correctness gain.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isUnspecifiedParam(v: any): boolean;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first value inside `v` that is not itself an array - what an array
|
|
3
|
+
* parameter's element type would be read from. A scalar answers itself.
|
|
4
|
+
*/
|
|
5
|
+
export function arrayLeaf(v) {
|
|
6
|
+
let x = v;
|
|
7
|
+
while (Array.isArray(x))
|
|
8
|
+
x = x[0];
|
|
9
|
+
return x;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether a parameter goes out with no declared type (OID 0), as text,
|
|
13
|
+
* for the server to resolve from wherever it lands.
|
|
14
|
+
*
|
|
15
|
+
* Two kinds of value cannot say what they are:
|
|
16
|
+
*
|
|
17
|
+
* - A `Date` is an instant or a wall clock depending on the column, and
|
|
18
|
+
* declaring either one moves the other. See `format-datetime.ts`.
|
|
19
|
+
* - A string is whichever of PostgreSQL's text-input types the context
|
|
20
|
+
* calls for. `determine()` answers `varchar`, and declaring that stops
|
|
21
|
+
* the server inferring: `insert into t(j) values($1)` into a `json`
|
|
22
|
+
* column, `id = $1` against a `uuid`, `coalesce($1, 1)` and an enum
|
|
23
|
+
* column all fail with "expression is of type character varying".
|
|
24
|
+
* Unspecified, every one of them works.
|
|
25
|
+
*
|
|
26
|
+
* This is what `pg` sends for both, and the divergence from it is what
|
|
27
|
+
* the shapes above were failing on. It is not free: a parameter with no
|
|
28
|
+
* context to be resolved from - `$1 is null`, `array_agg($1)`,
|
|
29
|
+
* `concat($1, 1)` - now raises "could not determine data type", exactly
|
|
30
|
+
* as it does under `pg`. A cast (`$1::text`) or a named type
|
|
31
|
+
* (`new BindParam(DataTypeOIDs.varchar, v)`) says which type is meant.
|
|
32
|
+
*
|
|
33
|
+
* Numbers, booleans and Buffers keep their declared types: they are
|
|
34
|
+
* already right nearly everywhere, and sending them as text would give
|
|
35
|
+
* up the binary encoding for no correctness gain.
|
|
36
|
+
*/
|
|
37
|
+
export function isUnspecifiedParam(v) {
|
|
38
|
+
const x = arrayLeaf(v);
|
|
39
|
+
return typeof x === 'string' || x instanceof Date;
|
|
40
|
+
}
|