postgrejs 3.9.0 → 3.10.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 +22 -1
- package/connection/connection.js +6 -2
- package/connection/intl-connection.d.ts +18 -1
- package/connection/intl-connection.js +104 -11
- package/data-types/circle-type.js +5 -2
- package/data-types/classes/geometric.d.ts +49 -7
- package/data-types/classes/geometric.js +59 -11
- package/data-types/classes/interval.d.ts +40 -16
- package/data-types/classes/interval.js +68 -40
- package/data-types/classes/numeric.d.ts +6 -0
- package/data-types/classes/numeric.js +8 -0
- package/data-types/classes/range.d.ts +6 -1
- package/data-types/classes/range.js +6 -1
- package/data-types/interval-type.js +1 -1
- package/data-types/money-type.js +11 -2
- package/data-types/numeric-type.js +21 -10
- package/data-types/temporal-types.d.ts +19 -0
- package/data-types/temporal-types.js +520 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/interfaces/data-mapping-options.d.ts +99 -4
- package/package.json +1 -1
- package/util/connection-config.js +11 -1
- package/util/decimal-as-string.d.ts +11 -0
- package/util/decimal-as-string.js +30 -0
- package/util/fetch-as-string.d.ts +33 -0
- package/util/fetch-as-string.js +122 -0
- package/util/get-parsers.js +18 -1
- package/util/resolve-column-formats.d.ts +5 -11
- package/util/resolve-column-formats.js +24 -24
- package/util/stringify-for-sql.js +6 -5
- package/util/temporal.d.ts +47 -0
- package/util/temporal.js +61 -0
- package/util/unspecified-param.d.ts +18 -3
- package/util/unspecified-param.js +21 -4
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { DataTypeNames, DataTypeOIDs } from '../constants.js';
|
|
2
|
+
import { DataTypeMap, GlobalTypeMap } from '../data-type-map.js';
|
|
3
|
+
import { readBigInt64BE } from '../util/bigint-methods.js';
|
|
4
|
+
import { getTemporal, systemTimeZone, } from '../util/temporal.js';
|
|
5
|
+
import { DateType } from './date-type.js';
|
|
6
|
+
import { IntervalType, parseIntervalText } from './interval-type.js';
|
|
7
|
+
import { createMultiRangeType, createRangeType } from './range-type.js';
|
|
8
|
+
import { TimeType } from './time-type.js';
|
|
9
|
+
import { TimestampType } from './timestamp-type.js';
|
|
10
|
+
import { TimestamptzType } from './timestamptz-type.js';
|
|
11
|
+
/**
|
|
12
|
+
* PostgreSQL's date/time types decoded into `Temporal` values instead of
|
|
13
|
+
* `Date`.
|
|
14
|
+
*
|
|
15
|
+
* Three things `Date` cannot do and these can: carry the microseconds
|
|
16
|
+
* PostgreSQL actually stores (a `Date` is milliseconds, so the last three
|
|
17
|
+
* digits of every timestamp were being dropped), say that a `timestamp`
|
|
18
|
+
* has no zone at all rather than guessing one from `utcDates`, and keep
|
|
19
|
+
* a `date` a date instead of a midnight in some zone.
|
|
20
|
+
*
|
|
21
|
+
* Decoding only is opinionated: every type here still *accepts* as a
|
|
22
|
+
* parameter everything it accepted before, so turning this on does not
|
|
23
|
+
* invalidate the `Date`s and strings a caller already passes.
|
|
24
|
+
*/
|
|
25
|
+
const EPOCH_2000_NS = 946684800000000000n;
|
|
26
|
+
const TS_INFINITY = 0x7fffffffffffffffn;
|
|
27
|
+
const TS_MINUS_INFINITY = -0x8000000000000000n;
|
|
28
|
+
const DATE_INFINITY = 0x7fffffff;
|
|
29
|
+
const DATE_MINUS_INFINITY = -0x80000000;
|
|
30
|
+
const MICROS_PER_DAY = 86400000000n;
|
|
31
|
+
const MAX_SAFE_MICROS = BigInt(Number.MAX_SAFE_INTEGER);
|
|
32
|
+
/** The five types a caller may ask for by OID. */
|
|
33
|
+
export const TemporalCapableOIDs = Object.freeze([
|
|
34
|
+
DataTypeOIDs.date,
|
|
35
|
+
DataTypeOIDs.time,
|
|
36
|
+
DataTypeOIDs.timestamp,
|
|
37
|
+
DataTypeOIDs.timestamptz,
|
|
38
|
+
DataTypeOIDs.interval,
|
|
39
|
+
]);
|
|
40
|
+
function readMicros(v, offset) {
|
|
41
|
+
return typeof v.readBigInt64BE === 'function'
|
|
42
|
+
? v.readBigInt64BE(offset)
|
|
43
|
+
: readBigInt64BE(v, offset);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* PostgreSQL writes years before the common era as `0044-03-15 BC`;
|
|
47
|
+
* Temporal counts them astronomically, where that year is -43. Rewrites
|
|
48
|
+
* the one into the other - `-000043-03-15` - so the era is not simply
|
|
49
|
+
* refused, and the text path answers what the binary path already does.
|
|
50
|
+
*/
|
|
51
|
+
function withoutEra(v) {
|
|
52
|
+
const body = v.slice(0, -3);
|
|
53
|
+
const dash = body.indexOf('-');
|
|
54
|
+
const year = 1 - +body.substring(0, dash);
|
|
55
|
+
return ((year < 0 ? '-' : '+') +
|
|
56
|
+
String(Math.abs(year)).padStart(6, '0') +
|
|
57
|
+
body.substring(dash));
|
|
58
|
+
}
|
|
59
|
+
function zoneOf(options) {
|
|
60
|
+
return options.timeZone || systemTimeZone();
|
|
61
|
+
}
|
|
62
|
+
/** Guards the one thing PostgreSQL can store and Temporal cannot hold. */
|
|
63
|
+
function requireMicrosecondPrecision(ns) {
|
|
64
|
+
if (ns % 1000n)
|
|
65
|
+
throw new TypeError('PostgreSQL stores microseconds, and this value carries ' +
|
|
66
|
+
'nanoseconds that would be dropped. Round it first: ' +
|
|
67
|
+
"value.round({ smallestUnit: 'microsecond' }).");
|
|
68
|
+
return ns / 1000n;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A `Temporal.Duration` out of the three quantities PostgreSQL keeps
|
|
72
|
+
* apart, or an error when they cannot make one.
|
|
73
|
+
*
|
|
74
|
+
* `interval '1 mon -3 days'` is an ordinary PostgreSQL value and has no
|
|
75
|
+
* Duration: Temporal refuses a duration whose fields disagree in sign,
|
|
76
|
+
* and balancing months against days needs a date to count from, which a
|
|
77
|
+
* bare interval does not have.
|
|
78
|
+
*/
|
|
79
|
+
function toDuration(T, months, days, micros) {
|
|
80
|
+
const signs = [
|
|
81
|
+
Math.sign(months),
|
|
82
|
+
Math.sign(days),
|
|
83
|
+
micros > 0n ? 1 : micros < 0n ? -1 : 0,
|
|
84
|
+
];
|
|
85
|
+
let sign = 0;
|
|
86
|
+
let i;
|
|
87
|
+
for (i = 0; i < 3; i++) {
|
|
88
|
+
if (!signs[i])
|
|
89
|
+
continue;
|
|
90
|
+
if (!sign)
|
|
91
|
+
sign = signs[i];
|
|
92
|
+
else if (signs[i] !== sign)
|
|
93
|
+
throw new TypeError(`The interval ${months} mons ${days} days ${micros} us mixes ` +
|
|
94
|
+
'signs, and Temporal.Duration allows only one sign for all of ' +
|
|
95
|
+
'its fields. Leave `interval` out of `temporalTypes` for a ' +
|
|
96
|
+
'column that holds values like this one.');
|
|
97
|
+
}
|
|
98
|
+
if (micros > MAX_SAFE_MICROS || micros < -MAX_SAFE_MICROS)
|
|
99
|
+
throw new RangeError(`An interval of ${micros} microseconds is past what a ` +
|
|
100
|
+
'Temporal.Duration field can hold.');
|
|
101
|
+
// The time part is spread over hours/minutes/seconds rather than left
|
|
102
|
+
// as one microsecond count: a Duration never balances itself, so
|
|
103
|
+
// `interval '3 hours'` would answer 0 hours and 10800 seconds. Days
|
|
104
|
+
// and months are the two PostgreSQL keeps apart on purpose and are
|
|
105
|
+
// left exactly as they came.
|
|
106
|
+
const abs = micros < 0n ? -micros : micros;
|
|
107
|
+
const s = micros < 0n ? -1 : 1;
|
|
108
|
+
return T.Duration.from({
|
|
109
|
+
months,
|
|
110
|
+
days,
|
|
111
|
+
hours: Number(abs / 3600000000n) * s,
|
|
112
|
+
minutes: Number((abs / 60000000n) % 60n) * s,
|
|
113
|
+
seconds: Number((abs / 1000000n) % 60n) * s,
|
|
114
|
+
microseconds: Number(abs % 1000000n) * s,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The `Date` an existing decoder produced, as the Temporal value this
|
|
119
|
+
* type answers with. Only for the shapes the fast paths below hand back:
|
|
120
|
+
* `infinity` stays the number it already was, and an unreadable date -
|
|
121
|
+
* a zone abbreviation under a non-ISO DateStyle, which names no offset -
|
|
122
|
+
* is an error rather than an `Invalid Date` nobody can use.
|
|
123
|
+
*/
|
|
124
|
+
function checkDate(v, str) {
|
|
125
|
+
if (typeof v === 'number')
|
|
126
|
+
return v;
|
|
127
|
+
if (Number.isNaN(v.getTime()))
|
|
128
|
+
throw new Error(`"${str}" could not be read as a date - see DataMappingOptions.dateStyle.`);
|
|
129
|
+
return v;
|
|
130
|
+
}
|
|
131
|
+
function temporalTimestamptzType(T) {
|
|
132
|
+
const decodeText = (v, options) => {
|
|
133
|
+
// ISO is the default DateStyle and is Instant's own shape once the
|
|
134
|
+
// space between date and time is allowed for, which it is.
|
|
135
|
+
if (!options.dateStyle) {
|
|
136
|
+
const c = v.charCodeAt(0);
|
|
137
|
+
if (c >= 0x30 && c <= 0x39)
|
|
138
|
+
return T.Instant.from(v.endsWith(' BC') ? withoutEra(v) : v).toZonedDateTimeISO(zoneOf(options));
|
|
139
|
+
}
|
|
140
|
+
const d = checkDate(TimestamptzType.decodeText(v, options), v);
|
|
141
|
+
// A styled rendering carries milliseconds at best, so nothing is
|
|
142
|
+
// lost by going through the Date the existing parser built.
|
|
143
|
+
return typeof d === 'number'
|
|
144
|
+
? d
|
|
145
|
+
: T.Instant.fromEpochMilliseconds(d.getTime()).toZonedDateTimeISO(zoneOf(options));
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
name: 'timestamptz',
|
|
149
|
+
oid: DataTypeOIDs.timestamptz,
|
|
150
|
+
jsType: 'Temporal.ZonedDateTime',
|
|
151
|
+
decodeBinary(v, offset = 0, _len, options) {
|
|
152
|
+
const micros = readMicros(v, offset);
|
|
153
|
+
if (micros === TS_INFINITY)
|
|
154
|
+
return Infinity;
|
|
155
|
+
if (micros === TS_MINUS_INFINITY)
|
|
156
|
+
return -Infinity;
|
|
157
|
+
return T.Instant.fromEpochNanoseconds(EPOCH_2000_NS + micros * 1000n).toZonedDateTimeISO(zoneOf(options));
|
|
158
|
+
},
|
|
159
|
+
decodeText,
|
|
160
|
+
decodeTextBuffer(buf, offset, len, options) {
|
|
161
|
+
return decodeText(buf.toString('latin1', offset, offset + len), options);
|
|
162
|
+
},
|
|
163
|
+
encodeBinary(buf, v, options) {
|
|
164
|
+
if (v instanceof T.ZonedDateTime || v instanceof T.Instant) {
|
|
165
|
+
buf.writeBigInt64BE(requireMicrosecondPrecision(v.epochNanoseconds - EPOCH_2000_NS));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
TimestamptzType.encodeBinary(buf, v, options);
|
|
169
|
+
},
|
|
170
|
+
encodeText(v, options) {
|
|
171
|
+
// Without the [zone] suffix, which PostgreSQL does not read - the
|
|
172
|
+
// offset in front of it is what carries the instant anyway.
|
|
173
|
+
if (v instanceof T.ZonedDateTime)
|
|
174
|
+
return v.toString({ timeZoneName: 'never' });
|
|
175
|
+
if (v instanceof T.Instant)
|
|
176
|
+
return v.toString();
|
|
177
|
+
return TimestamptzType.encodeText(v, options);
|
|
178
|
+
},
|
|
179
|
+
isType(v) {
|
|
180
|
+
return (v instanceof T.ZonedDateTime ||
|
|
181
|
+
v instanceof T.Instant ||
|
|
182
|
+
TimestamptzType.isType(v));
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function temporalTimestampType(T) {
|
|
187
|
+
const fromDate = (d, options) => options.utcDates
|
|
188
|
+
? T.PlainDateTime.from({
|
|
189
|
+
year: d.getUTCFullYear(),
|
|
190
|
+
month: d.getUTCMonth() + 1,
|
|
191
|
+
day: d.getUTCDate(),
|
|
192
|
+
hour: d.getUTCHours(),
|
|
193
|
+
minute: d.getUTCMinutes(),
|
|
194
|
+
second: d.getUTCSeconds(),
|
|
195
|
+
millisecond: d.getUTCMilliseconds(),
|
|
196
|
+
})
|
|
197
|
+
: T.PlainDateTime.from({
|
|
198
|
+
year: d.getFullYear(),
|
|
199
|
+
month: d.getMonth() + 1,
|
|
200
|
+
day: d.getDate(),
|
|
201
|
+
hour: d.getHours(),
|
|
202
|
+
minute: d.getMinutes(),
|
|
203
|
+
second: d.getSeconds(),
|
|
204
|
+
millisecond: d.getMilliseconds(),
|
|
205
|
+
});
|
|
206
|
+
const decodeText = (v, options) => {
|
|
207
|
+
if (!options.dateStyle) {
|
|
208
|
+
const c = v.charCodeAt(0);
|
|
209
|
+
if (c >= 0x30 && c <= 0x39)
|
|
210
|
+
return T.PlainDateTime.from(v.endsWith(' BC') ? withoutEra(v) : v);
|
|
211
|
+
}
|
|
212
|
+
const d = checkDate(TimestampType.decodeText(v, options), v);
|
|
213
|
+
return typeof d === 'number' ? d : fromDate(d, options);
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
name: 'timestamp',
|
|
217
|
+
oid: DataTypeOIDs.timestamp,
|
|
218
|
+
jsType: 'Temporal.PlainDateTime',
|
|
219
|
+
decodeBinary(v, offset = 0) {
|
|
220
|
+
const micros = readMicros(v, offset);
|
|
221
|
+
if (micros === TS_INFINITY)
|
|
222
|
+
return Infinity;
|
|
223
|
+
if (micros === TS_MINUS_INFINITY)
|
|
224
|
+
return -Infinity;
|
|
225
|
+
// A timestamp carries no zone, so it is read in UTC and the zone
|
|
226
|
+
// dropped again - `utcDates` has nothing left to decide here.
|
|
227
|
+
return T.Instant.fromEpochNanoseconds(EPOCH_2000_NS + micros * 1000n)
|
|
228
|
+
.toZonedDateTimeISO('UTC')
|
|
229
|
+
.toPlainDateTime();
|
|
230
|
+
},
|
|
231
|
+
decodeText,
|
|
232
|
+
decodeTextBuffer(buf, offset, len, options) {
|
|
233
|
+
return decodeText(buf.toString('latin1', offset, offset + len), options);
|
|
234
|
+
},
|
|
235
|
+
encodeBinary(buf, v, options) {
|
|
236
|
+
if (v instanceof T.PlainDateTime) {
|
|
237
|
+
buf.writeBigInt64BE(requireMicrosecondPrecision(v.toZonedDateTime('UTC').epochNanoseconds - EPOCH_2000_NS));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
TimestampType.encodeBinary(buf, v, options);
|
|
241
|
+
},
|
|
242
|
+
encodeText(v, options) {
|
|
243
|
+
if (v instanceof T.PlainDateTime)
|
|
244
|
+
return v.toString();
|
|
245
|
+
return TimestampType.encodeText(v, options);
|
|
246
|
+
},
|
|
247
|
+
isType(v) {
|
|
248
|
+
return v instanceof T.PlainDateTime || TimestampType.isType(v);
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function temporalDateType(T) {
|
|
253
|
+
const EPOCH_2000 = T.PlainDate.from('2000-01-01');
|
|
254
|
+
const decodeText = (v, options) => {
|
|
255
|
+
if (!options.dateStyle) {
|
|
256
|
+
const c = v.charCodeAt(0);
|
|
257
|
+
if (c >= 0x30 && c <= 0x39)
|
|
258
|
+
return T.PlainDate.from(v.endsWith(' BC') ? withoutEra(v) : v);
|
|
259
|
+
}
|
|
260
|
+
const d = checkDate(DateType.decodeText(v, options), v);
|
|
261
|
+
if (typeof d === 'number')
|
|
262
|
+
return d;
|
|
263
|
+
return options.utcDates
|
|
264
|
+
? T.PlainDate.from({
|
|
265
|
+
year: d.getUTCFullYear(),
|
|
266
|
+
month: d.getUTCMonth() + 1,
|
|
267
|
+
day: d.getUTCDate(),
|
|
268
|
+
})
|
|
269
|
+
: T.PlainDate.from({
|
|
270
|
+
year: d.getFullYear(),
|
|
271
|
+
month: d.getMonth() + 1,
|
|
272
|
+
day: d.getDate(),
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
return {
|
|
276
|
+
name: 'date',
|
|
277
|
+
oid: DataTypeOIDs.date,
|
|
278
|
+
jsType: 'Temporal.PlainDate',
|
|
279
|
+
decodeBinary(v, offset = 0) {
|
|
280
|
+
const days = v.readInt32BE(offset);
|
|
281
|
+
if (days === DATE_INFINITY)
|
|
282
|
+
return Infinity;
|
|
283
|
+
if (days === DATE_MINUS_INFINITY)
|
|
284
|
+
return -Infinity;
|
|
285
|
+
return EPOCH_2000.add({ days });
|
|
286
|
+
},
|
|
287
|
+
decodeText,
|
|
288
|
+
decodeTextBuffer(buf, offset, len, options) {
|
|
289
|
+
return decodeText(buf.toString('latin1', offset, offset + len), options);
|
|
290
|
+
},
|
|
291
|
+
encodeBinary(buf, v, options) {
|
|
292
|
+
if (v instanceof T.PlainDate) {
|
|
293
|
+
buf.writeInt32BE(EPOCH_2000.until(v, { largestUnit: 'day' }).days);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
DateType.encodeBinary(buf, v, options);
|
|
297
|
+
},
|
|
298
|
+
encodeText(v, options) {
|
|
299
|
+
if (v instanceof T.PlainDate)
|
|
300
|
+
return v.toString();
|
|
301
|
+
return DateType.encodeText(v, options);
|
|
302
|
+
},
|
|
303
|
+
isType(v) {
|
|
304
|
+
return v instanceof T.PlainDate || DateType.isType(v);
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function temporalTimeType(T) {
|
|
309
|
+
const MIDNIGHT = T.PlainTime.from('00:00:00');
|
|
310
|
+
// `time` is rendered the same way under every DateStyle, so there is
|
|
311
|
+
// no styled shape to fall back to here.
|
|
312
|
+
const decodeText = (v) => T.PlainTime.from(v);
|
|
313
|
+
return {
|
|
314
|
+
name: 'time',
|
|
315
|
+
oid: DataTypeOIDs.time,
|
|
316
|
+
jsType: 'Temporal.PlainTime',
|
|
317
|
+
decodeBinary(v, offset = 0) {
|
|
318
|
+
const micros = readMicros(v, offset);
|
|
319
|
+
// PostgreSQL's `time` runs to 24:00:00 inclusive; a PlainTime stops
|
|
320
|
+
// one nanosecond short of it, and adding a day's worth of
|
|
321
|
+
// microseconds to midnight would silently wrap back to 00:00:00.
|
|
322
|
+
if (micros >= MICROS_PER_DAY)
|
|
323
|
+
throw new RangeError("PostgreSQL's time '24:00:00' has no Temporal.PlainTime - it is " +
|
|
324
|
+
'the end of the day, and a PlainTime only reaches 23:59:59.999999999.');
|
|
325
|
+
return MIDNIGHT.add({ microseconds: Number(micros) });
|
|
326
|
+
},
|
|
327
|
+
decodeText,
|
|
328
|
+
decodeTextBuffer(buf, offset, len) {
|
|
329
|
+
return decodeText(buf.toString('latin1', offset, offset + len));
|
|
330
|
+
},
|
|
331
|
+
encodeBinary(buf, v, options) {
|
|
332
|
+
if (v instanceof T.PlainTime) {
|
|
333
|
+
const d = MIDNIGHT.until(v, { largestUnit: 'microsecond' });
|
|
334
|
+
if (d.nanoseconds)
|
|
335
|
+
requireMicrosecondPrecision(BigInt(d.nanoseconds));
|
|
336
|
+
buf.writeBigInt64BE(BigInt(d.microseconds));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
TimeType.encodeBinary(buf, v, options);
|
|
340
|
+
},
|
|
341
|
+
encodeText(v, options) {
|
|
342
|
+
if (v instanceof T.PlainTime)
|
|
343
|
+
return v.toString();
|
|
344
|
+
return TimeType.encodeText(v, options);
|
|
345
|
+
},
|
|
346
|
+
isType(v) {
|
|
347
|
+
return v instanceof T.PlainTime || TimeType.isType(v);
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function temporalIntervalType(T) {
|
|
352
|
+
return {
|
|
353
|
+
name: 'interval',
|
|
354
|
+
oid: DataTypeOIDs.interval,
|
|
355
|
+
jsType: 'Temporal.Duration',
|
|
356
|
+
decodeBinary(v, offset = 0) {
|
|
357
|
+
return toDuration(T, v.readInt32BE(offset + 12), v.readInt32BE(offset + 8), readMicros(v, offset));
|
|
358
|
+
},
|
|
359
|
+
decodeText(v) {
|
|
360
|
+
const iv = parseIntervalText(v);
|
|
361
|
+
return toDuration(T, iv.totalMonths, iv.days ?? 0, iv.totalMicroseconds);
|
|
362
|
+
},
|
|
363
|
+
encodeBinary(buf, v, options) {
|
|
364
|
+
if (!(v instanceof T.Duration)) {
|
|
365
|
+
IntervalType.encodeBinary(buf, v, options);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
buf.writeBigInt64BE(durationMicros(v));
|
|
369
|
+
buf.writeInt32BE(v.days + v.weeks * 7);
|
|
370
|
+
buf.writeInt32BE(v.years * 12 + v.months);
|
|
371
|
+
},
|
|
372
|
+
encodeText(v, options) {
|
|
373
|
+
// A Duration prints ISO 8601, which PostgreSQL reads whatever its
|
|
374
|
+
// IntervalStyle is set to.
|
|
375
|
+
if (v instanceof T.Duration)
|
|
376
|
+
return v.toString();
|
|
377
|
+
return IntervalType.encodeText(v, options);
|
|
378
|
+
},
|
|
379
|
+
isType(v) {
|
|
380
|
+
return v instanceof T.Duration || IntervalType.isType(v);
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** A Duration's time part, in the microseconds the wire carries. */
|
|
385
|
+
function durationMicros(d) {
|
|
386
|
+
if (d.nanoseconds % 1000)
|
|
387
|
+
requireMicrosecondPrecision(BigInt(d.nanoseconds));
|
|
388
|
+
return (BigInt(d.hours) * 3600000000n +
|
|
389
|
+
BigInt(d.minutes) * 60000000n +
|
|
390
|
+
BigInt(d.seconds) * 1000000n +
|
|
391
|
+
BigInt(d.milliseconds) * 1000n +
|
|
392
|
+
BigInt(d.microseconds) +
|
|
393
|
+
BigInt(d.nanoseconds / 1000));
|
|
394
|
+
}
|
|
395
|
+
const FAMILIES = {
|
|
396
|
+
[DataTypeOIDs.date]: {
|
|
397
|
+
build: temporalDateType,
|
|
398
|
+
arrayOid: DataTypeOIDs._date,
|
|
399
|
+
range: {
|
|
400
|
+
name: 'daterange',
|
|
401
|
+
oid: DataTypeOIDs.daterange,
|
|
402
|
+
arrayOid: DataTypeOIDs._daterange,
|
|
403
|
+
multiOid: DataTypeOIDs.datemultirange,
|
|
404
|
+
multiArrayOid: DataTypeOIDs._datemultirange,
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
[DataTypeOIDs.time]: {
|
|
408
|
+
build: temporalTimeType,
|
|
409
|
+
arrayOid: DataTypeOIDs._time,
|
|
410
|
+
},
|
|
411
|
+
[DataTypeOIDs.timestamp]: {
|
|
412
|
+
build: temporalTimestampType,
|
|
413
|
+
arrayOid: DataTypeOIDs._timestamp,
|
|
414
|
+
range: {
|
|
415
|
+
name: 'tsrange',
|
|
416
|
+
oid: DataTypeOIDs.tsrange,
|
|
417
|
+
arrayOid: DataTypeOIDs._tsrange,
|
|
418
|
+
multiOid: DataTypeOIDs.tsmultirange,
|
|
419
|
+
multiArrayOid: DataTypeOIDs._tsmultirange,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
[DataTypeOIDs.timestamptz]: {
|
|
423
|
+
build: temporalTimestamptzType,
|
|
424
|
+
arrayOid: DataTypeOIDs._timestamptz,
|
|
425
|
+
range: {
|
|
426
|
+
name: 'tstzrange',
|
|
427
|
+
oid: DataTypeOIDs.tstzrange,
|
|
428
|
+
arrayOid: DataTypeOIDs._tstzrange,
|
|
429
|
+
multiOid: DataTypeOIDs.tstzmultirange,
|
|
430
|
+
multiArrayOid: DataTypeOIDs._tstzmultirange,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
[DataTypeOIDs.interval]: {
|
|
434
|
+
build: temporalIntervalType,
|
|
435
|
+
arrayOid: DataTypeOIDs._interval,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
/** Every registration one selected OID stands for. */
|
|
439
|
+
export function temporalTypesFor(oid, T) {
|
|
440
|
+
const spec = FAMILIES[oid];
|
|
441
|
+
const scalar = spec.build(T);
|
|
442
|
+
const out = [
|
|
443
|
+
scalar,
|
|
444
|
+
{
|
|
445
|
+
...scalar,
|
|
446
|
+
name: '_' + scalar.name,
|
|
447
|
+
oid: spec.arrayOid,
|
|
448
|
+
elementsOID: scalar.oid,
|
|
449
|
+
},
|
|
450
|
+
];
|
|
451
|
+
const r = spec.range;
|
|
452
|
+
if (r) {
|
|
453
|
+
const multiName = r.name.replace('range', 'multirange');
|
|
454
|
+
const range = createRangeType(r.name, r.oid, scalar);
|
|
455
|
+
const multi = createMultiRangeType(multiName, r.multiOid, range);
|
|
456
|
+
out.push(range, { ...range, name: '_' + r.name, oid: r.arrayOid, elementsOID: r.oid }, multi, {
|
|
457
|
+
...multi,
|
|
458
|
+
name: '_' + multiName,
|
|
459
|
+
oid: r.multiArrayOid,
|
|
460
|
+
elementsOID: r.multiOid,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
function normalizeSelection(selection) {
|
|
466
|
+
if (selection === true)
|
|
467
|
+
return TemporalCapableOIDs.slice();
|
|
468
|
+
if (selection === false)
|
|
469
|
+
return [];
|
|
470
|
+
if (!Array.isArray(selection))
|
|
471
|
+
throw new TypeError('`temporalTypes` is either a boolean or an array of OIDs, not ' +
|
|
472
|
+
typeof selection);
|
|
473
|
+
const out = [];
|
|
474
|
+
const l = selection.length;
|
|
475
|
+
let i;
|
|
476
|
+
let oid;
|
|
477
|
+
for (i = 0; i < l; i++) {
|
|
478
|
+
oid = selection[i];
|
|
479
|
+
if (!TemporalCapableOIDs.includes(oid))
|
|
480
|
+
throw new TypeError('`temporalTypes` accepts only date (1082), time (1083), timestamp ' +
|
|
481
|
+
'(1114), timestamptz (1184) and interval (1186). Received ' +
|
|
482
|
+
`${oid}${DataTypeNames[oid] ? ' (' + DataTypeNames[oid] + ')' : ''}.`);
|
|
483
|
+
if (!out.includes(oid))
|
|
484
|
+
out.push(oid);
|
|
485
|
+
}
|
|
486
|
+
return out.sort((a, b) => a - b);
|
|
487
|
+
}
|
|
488
|
+
const _derived = new WeakMap();
|
|
489
|
+
/**
|
|
490
|
+
* A type map that decodes the selected date/time types into `Temporal`
|
|
491
|
+
* values, built on top of `base` - which keeps whatever else the caller
|
|
492
|
+
* registered there.
|
|
493
|
+
*
|
|
494
|
+
* The same selection on the same base always gives back the same map.
|
|
495
|
+
* That is not only to save the copy: a prepared statement is reused only
|
|
496
|
+
* while the map it was parsed with is the same object, so handing out a
|
|
497
|
+
* fresh map per statement would quietly empty the statement cache.
|
|
498
|
+
*/
|
|
499
|
+
export function temporalTypeMap(selection = true, base = GlobalTypeMap) {
|
|
500
|
+
const oids = normalizeSelection(selection);
|
|
501
|
+
if (!oids.length)
|
|
502
|
+
return base;
|
|
503
|
+
const key = oids.join(',');
|
|
504
|
+
let forBase = _derived.get(base);
|
|
505
|
+
if (!forBase) {
|
|
506
|
+
forBase = new Map();
|
|
507
|
+
_derived.set(base, forBase);
|
|
508
|
+
}
|
|
509
|
+
let map = forBase.get(key);
|
|
510
|
+
if (!map) {
|
|
511
|
+
const T = getTemporal();
|
|
512
|
+
map = new DataTypeMap(base);
|
|
513
|
+
const l = oids.length;
|
|
514
|
+
let i;
|
|
515
|
+
for (i = 0; i < l; i++)
|
|
516
|
+
map.register(temporalTypesFor(oids[i], T));
|
|
517
|
+
forBase.set(key, map);
|
|
518
|
+
}
|
|
519
|
+
return map;
|
|
520
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './data-types/classes/geometric.js';
|
|
|
12
12
|
export * from './data-types/classes/interval.js';
|
|
13
13
|
export * from './data-types/classes/numeric.js';
|
|
14
14
|
export * from './data-types/classes/range.js';
|
|
15
|
+
export * from './data-types/temporal-types.js';
|
|
15
16
|
export * from './interfaces/batch-result.js';
|
|
16
17
|
export * from './interfaces/command-result.js';
|
|
17
18
|
export * from './interfaces/copy-from-rows-options.js';
|
|
@@ -33,6 +34,7 @@ export * from './protocol/database-error.js';
|
|
|
33
34
|
export * from './protocol/smart-buffer.js';
|
|
34
35
|
export * from './types.js';
|
|
35
36
|
export * from './util/connection-config.js';
|
|
37
|
+
export * from './util/decimal-as-string.js';
|
|
36
38
|
export * from './util/escape-identifier.js';
|
|
37
39
|
export * from './util/escape-literal.js';
|
|
38
40
|
export * from './util/parse-datetime.js';
|
package/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from './data-types/classes/geometric.js';
|
|
|
12
12
|
export * from './data-types/classes/interval.js';
|
|
13
13
|
export * from './data-types/classes/numeric.js';
|
|
14
14
|
export * from './data-types/classes/range.js';
|
|
15
|
+
export * from './data-types/temporal-types.js';
|
|
15
16
|
export * from './interfaces/batch-result.js';
|
|
16
17
|
export * from './interfaces/command-result.js';
|
|
17
18
|
export * from './interfaces/copy-from-rows-options.js';
|
|
@@ -33,6 +34,7 @@ export * from './protocol/database-error.js';
|
|
|
33
34
|
export * from './protocol/smart-buffer.js';
|
|
34
35
|
export * from './types.js';
|
|
35
36
|
export * from './util/connection-config.js';
|
|
37
|
+
export * from './util/decimal-as-string.js';
|
|
36
38
|
export * from './util/escape-identifier.js';
|
|
37
39
|
export * from './util/escape-literal.js';
|
|
38
40
|
export * from './util/parse-datetime.js';
|
|
@@ -7,7 +7,99 @@ export interface MoneyFormat {
|
|
|
7
7
|
/** The decimal separator the server prints, `.` or `,`. */
|
|
8
8
|
decimalSeparator: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* One entry of `fetchAsString`: an OID on its own, or an OID with what
|
|
12
|
+
* it should reach.
|
|
13
|
+
*/
|
|
14
|
+
export type FetchAsStringItem = OID | FetchAsStringSelector;
|
|
15
|
+
/** An OID named by `fetchAsString`, with what it reaches. */
|
|
16
|
+
export interface FetchAsStringSelector {
|
|
17
|
+
oid: OID;
|
|
18
|
+
/**
|
|
19
|
+
* Whether naming a scalar type also asks for columns of *arrays* of
|
|
20
|
+
* it - which is what naming it on its own does, so this defaults to
|
|
21
|
+
* `true`.
|
|
22
|
+
*
|
|
23
|
+
* `false` is for reproducing something that is inconsistent on the
|
|
24
|
+
* other side: `pg` hands back a scalar `numeric` as a string and a
|
|
25
|
+
* `numeric[]` as numbers, and a `numeric[]` column already decodes
|
|
26
|
+
* that way here, so only the scalar wants asking for as text.
|
|
27
|
+
*/
|
|
28
|
+
arrays?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Reserved. An element OID does not reach range columns today - a
|
|
31
|
+
* `numrange` column is selected by naming `numrange` - so `false` is
|
|
32
|
+
* the only value this can be given, and `true` is refused rather than
|
|
33
|
+
* ignored.
|
|
34
|
+
*/
|
|
35
|
+
ranges?: boolean;
|
|
36
|
+
}
|
|
10
37
|
export interface DataMappingOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Decode `numeric` and `money` into the exact decimal string they
|
|
40
|
+
* carry, instead of a number.
|
|
41
|
+
*
|
|
42
|
+
* `true` takes both; an array takes only the ones it names, and may
|
|
43
|
+
* name only those two (`DecimalAsStringOIDs`). Their array, range and
|
|
44
|
+
* multirange types follow, since those decode through the element.
|
|
45
|
+
*
|
|
46
|
+
* The string is the value and nothing else: an optional leading `-`,
|
|
47
|
+
* the digits, and - for money - a decimal point with exactly the scale
|
|
48
|
+
* the server reported. No currency symbol, no thousands separator, no
|
|
49
|
+
* locale. That is what tells it apart from
|
|
50
|
+
* `fetchAsString: [DataTypeOIDs.money]`, which asks the *server* for
|
|
51
|
+
* its own rendering and gets `-$1,234.50` back, because `lc_monetary`
|
|
52
|
+
* is what writes a money value out.
|
|
53
|
+
*
|
|
54
|
+
* There is no conversion behind this: both types build the exact
|
|
55
|
+
* decimal while decoding and then decide whether a double can carry it
|
|
56
|
+
* (a `Numeric` when it cannot). This hands back that same string, so
|
|
57
|
+
* nothing is rounded, re-parsed or formatted a second time - which is
|
|
58
|
+
* the thing a caller doing it themselves cannot avoid, and cannot get
|
|
59
|
+
* right for money without knowing the scale.
|
|
60
|
+
*
|
|
61
|
+
* `numeric`'s non-finite values keep the spelling PostgreSQL writes:
|
|
62
|
+
* `'NaN'`, `'Infinity'`, `'-Infinity'`.
|
|
63
|
+
*
|
|
64
|
+
* Off by default.
|
|
65
|
+
*/
|
|
66
|
+
decimalAsString?: boolean | OID[];
|
|
67
|
+
/**
|
|
68
|
+
* Decode PostgreSQL's date/time types into `Temporal` values instead
|
|
69
|
+
* of `Date`.
|
|
70
|
+
*
|
|
71
|
+
* `true` takes all five - `date`, `time`, `timestamp`, `timestamptz`
|
|
72
|
+
* and `interval`; an array takes only the ones it names, and may name
|
|
73
|
+
* only those five (`DataTypeOIDs.timestamptz`, and so on - see
|
|
74
|
+
* `TemporalCapableOIDs`). Each one selected brings its own array and
|
|
75
|
+
* range types with it, because those carry a copy of the element's
|
|
76
|
+
* decoders rather than following it.
|
|
77
|
+
*
|
|
78
|
+
* This is decoding only: every type still accepts as a *parameter*
|
|
79
|
+
* everything it accepted before, so the `Date`s and strings already
|
|
80
|
+
* being passed keep working.
|
|
81
|
+
*
|
|
82
|
+
* What it buys: the microseconds PostgreSQL stores and a `Date` cannot
|
|
83
|
+
* hold, a `timestamp` that says it has no zone instead of guessing one
|
|
84
|
+
* from `utcDates`, and a `date` that stays a date. What it costs: no
|
|
85
|
+
* runtime ships `Temporal` yet, so an application turning this on
|
|
86
|
+
* installs a polyfill (`temporal-polyfill`) and imports it before
|
|
87
|
+
* connecting; building these values is also dearer than `new Date`.
|
|
88
|
+
*
|
|
89
|
+
* Off by default.
|
|
90
|
+
*/
|
|
91
|
+
temporalTypes?: boolean | OID[];
|
|
92
|
+
/**
|
|
93
|
+
* The time zone a `timestamptz` is given when it is decoded as a
|
|
94
|
+
* `Temporal.ZonedDateTime` - see `temporalTypes`.
|
|
95
|
+
*
|
|
96
|
+
* A connection fills this in from the `TimeZone` the server reports,
|
|
97
|
+
* so a caller normally never sets it; set it to print those values in
|
|
98
|
+
* a zone of your own choosing. It decides only what wall clock the
|
|
99
|
+
* value shows: a timestamptz is an absolute instant and is the same
|
|
100
|
+
* moment in every zone.
|
|
101
|
+
*/
|
|
102
|
+
timeZone?: string;
|
|
11
103
|
/**
|
|
12
104
|
* How the server renders dates, when it is not rendering them in ISO.
|
|
13
105
|
*
|
|
@@ -44,11 +136,14 @@ export interface DataMappingOptions {
|
|
|
44
136
|
* a query whose statement is not prepared asks for the whole row as text
|
|
45
137
|
* instead (see IntlConnection.queryOnce()).
|
|
46
138
|
*
|
|
47
|
-
* An array column
|
|
48
|
-
*
|
|
49
|
-
*
|
|
139
|
+
* An array column can be named either way, and the two ask for
|
|
140
|
+
* different things. Its own array OID (`_timestamptz`) asks for the
|
|
141
|
+
* whole literal, one string. Its *element's* OID (`timestamptz`) asks
|
|
142
|
+
* for the elements: an array, of the strings the server wrote, with
|
|
143
|
+
* the nulls still null - the same thing naming that OID does for a
|
|
144
|
+
* column of one.
|
|
50
145
|
*/
|
|
51
|
-
fetchAsString?:
|
|
146
|
+
fetchAsString?: FetchAsStringItem[];
|
|
52
147
|
/**
|
|
53
148
|
* Ask the server for text on any column this client has no way to
|
|
54
149
|
* decode, instead of taking bytes it cannot read.
|