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 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:
@@ -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 ? prm.oid : typeMap.determine(prm));
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:
@@ -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.emit('notice', msg);
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.emit('notice', msg);
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.emit('notice', msg);
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 it can be told apart:
13
- * `determine()` types a bare string as `varchar`, which the server then
14
- * refuses to put in a numeric column, so a string could be read but not
15
- * written back. A `Numeric` round-trips.
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 it can be told apart:
13
- * `determine()` types a bare string as `varchar`, which the server then
14
- * refuses to put in a numeric column, so a string could be read but not
15
- * written back. A `Numeric` round-trips.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.7.0",
4
+ "version": "3.8.0",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
@@ -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
- io.readInt32BE(); // LBound
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
- export declare function encodeBinaryArray(io: SmartBuffer, value: any[], itemOid: OID, options: DataMappingOptions, encode: EncodeBinaryFunction, encodeCalculateDimFn?: EncodeCalculateDimFunction): void;
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
- export function encodeBinaryArray(io, value, itemOid, options, encode, encodeCalculateDimFn) {
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(0); // LBound always 0.
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;
@@ -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
+ }