postgrejs 3.7.0 → 3.9.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +50 -0
  3. package/connection/connection.d.ts +14 -2
  4. package/connection/connection.js +70 -10
  5. package/connection/intl-connection.d.ts +146 -5
  6. package/connection/intl-connection.js +485 -35
  7. package/connection/pool.d.ts +30 -6
  8. package/connection/pool.js +28 -3
  9. package/connection/prepared-statement.d.ts +11 -0
  10. package/connection/prepared-statement.js +39 -5
  11. package/data-type-map.d.ts +16 -0
  12. package/data-type-map.js +56 -12
  13. package/data-types/classes/numeric.d.ts +8 -4
  14. package/data-types/classes/numeric.js +8 -4
  15. package/data-types/date-type.js +2 -2
  16. package/data-types/int2-vector-type.js +4 -1
  17. package/data-types/money-type.d.ts +38 -0
  18. package/data-types/money-type.js +203 -0
  19. package/data-types/numeric-type.d.ts +2 -0
  20. package/data-types/numeric-type.js +1 -1
  21. package/data-types/oid-vector-type.js +4 -1
  22. package/data-types/timestamp-type.js +2 -2
  23. package/data-types/timestamptz-type.js +11 -2
  24. package/index.d.ts +1 -0
  25. package/index.js +1 -0
  26. package/interfaces/data-mapping-options.d.ts +27 -0
  27. package/interfaces/database-connection-params.d.ts +32 -5
  28. package/interfaces/query-options.d.ts +32 -0
  29. package/interfaces/script-execute-options.d.ts +32 -0
  30. package/interfaces/transaction-options.d.ts +27 -0
  31. package/interfaces/transaction-options.js +1 -0
  32. package/package.json +1 -1
  33. package/protocol/database-error.d.ts +15 -0
  34. package/protocol/database-error.js +19 -0
  35. package/protocol/frontend.js +17 -0
  36. package/protocol/pg-socket.js +7 -10
  37. package/util/array-leaf.d.ts +10 -0
  38. package/util/array-leaf.js +22 -0
  39. package/util/date-style.d.ts +47 -0
  40. package/util/date-style.js +123 -0
  41. package/util/decode-binaryarray.js +8 -1
  42. package/util/encode-binaryarray.d.ts +12 -1
  43. package/util/encode-binaryarray.js +13 -2
  44. package/util/format-datetime.d.ts +18 -0
  45. package/util/format-datetime.js +33 -0
  46. package/util/parse-datetime.d.ts +4 -3
  47. package/util/parse-datetime.js +34 -9
  48. package/util/resolve-column-formats.js +4 -0
  49. package/util/row-decoder.d.ts +24 -4
  50. package/util/row-decoder.js +25 -10
  51. package/util/sql-tag.js +16 -0
  52. package/util/stringify-for-sql.js +7 -0
  53. package/util/transaction-command.d.ts +7 -0
  54. package/util/transaction-command.js +32 -0
  55. package/util/unspecified-param.d.ts +27 -0
  56. package/util/unspecified-param.js +31 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The session's `DateStyle`, when it is not the ISO default.
3
+ *
4
+ * PostgreSQL renders a date four ways, and the client cannot tell which
5
+ * one it is looking at from the string alone - `05.03.2024` is the fifth
6
+ * of March in every style that prints it, but `05/03/2024` is the fifth
7
+ * of March under `SQL, DMY` and the third of May under `SQL, MDY`. The
8
+ * server reports `DateStyle` in its startup parameters and again on
9
+ * every `SET`, so the convention is known rather than guessed.
10
+ *
11
+ * Measured against PostgreSQL 18.4, one value in every combination:
12
+ *
13
+ * ```
14
+ * ISO, any 2024-11-25 01:02:03.456 2024-03-05
15
+ * SQL, MDY 11/25/2024 01:02:03.456 UTC 03/05/2024
16
+ * SQL, DMY 25/11/2024 01:02:03.456 UTC 05/03/2024
17
+ * Postgres, MDY Mon Nov 25 01:02:03.456 2024 03-05-2024
18
+ * Postgres, DMY Mon 25 Nov 01:02:03.456 2024 05-03-2024
19
+ * German, any 25.11.2024 01:02:03.456 UTC 05.03.2024
20
+ * ```
21
+ *
22
+ * Two things in that table are not what the setting's name suggests:
23
+ * German ignores the field order entirely, and `YMD` prints as `MDY`
24
+ * for every style. Both are read off the measurement rather than off
25
+ * the documentation.
26
+ */
27
+ export interface PgDateStyle {
28
+ style: 'SQL' | 'Postgres' | 'German';
29
+ /** Whether the numeric day comes before the month. */
30
+ dayFirst: boolean;
31
+ }
32
+ /**
33
+ * The `DateStyle` the server reported, or undefined for ISO - which is
34
+ * the default, is what every existing parser here expects, and is the
35
+ * one shape that needs no extra work.
36
+ */
37
+ export declare function parseDateStyleSetting(value: string | undefined): PgDateStyle | undefined;
38
+ /**
39
+ * Reads a date or timestamp the server rendered in `style`.
40
+ *
41
+ * Returns undefined when the string is not that style's shape - a `BC`
42
+ * year, an `infinity`, or a zone abbreviation nobody can resolve - so
43
+ * the caller decides what to do with it. What it must never do is guess:
44
+ * handing `05.03.2024` to `new Date()` is how the fifth of March became
45
+ * the third of May, silently, for every day of the month up to twelve.
46
+ */
47
+ export declare function parseStyledDateTime(str: string, style: PgDateStyle, utc?: boolean): Date | undefined;
@@ -0,0 +1,123 @@
1
+ const MONTHS = {
2
+ jan: 0,
3
+ feb: 1,
4
+ mar: 2,
5
+ apr: 3,
6
+ may: 4,
7
+ jun: 5,
8
+ jul: 6,
9
+ aug: 7,
10
+ sep: 8,
11
+ oct: 9,
12
+ nov: 10,
13
+ dec: 11,
14
+ };
15
+ /**
16
+ * `Postgres` style: a weekday, then the month and day in the configured
17
+ * order, the time, the year, and for timestamptz a zone.
18
+ */
19
+ const POSTGRES_PATTERN = /^[A-Za-z]{3} (?:([A-Za-z]{3}) (\d{1,2})|(\d{1,2}) ([A-Za-z]{3})) (\d{2}):(\d{2}):(\d{2})(?:\.(\d+))? (\d{1,6})(?: (.+))?$/;
20
+ /** `SQL` and `German`: three numbers, a time, and for timestamptz a zone. */
21
+ const NUMERIC_PATTERN = /^(\d{1,2})([./])(\d{1,2})\2(\d{1,6})(?: (\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?)?(?: (.+))?$/;
22
+ /** `Postgres` style writes a date on its own with dashes. */
23
+ const POSTGRES_DATE_PATTERN = /^(\d{1,2})-(\d{1,2})-(\d{1,6})$/;
24
+ /**
25
+ * The `DateStyle` the server reported, or undefined for ISO - which is
26
+ * the default, is what every existing parser here expects, and is the
27
+ * one shape that needs no extra work.
28
+ */
29
+ export function parseDateStyleSetting(value) {
30
+ if (!value)
31
+ return undefined;
32
+ const comma = value.indexOf(',');
33
+ const style = (comma < 0 ? value : value.substring(0, comma)).trim();
34
+ if (style === 'ISO')
35
+ return undefined;
36
+ if (style !== 'SQL' && style !== 'Postgres' && style !== 'German')
37
+ return undefined;
38
+ const order = comma < 0 ? '' : value.substring(comma + 1).trim();
39
+ // German prints day-first whatever the order says, and YMD prints as
40
+ // MDY - both measured, neither obvious.
41
+ return { style, dayFirst: style === 'German' || order === 'DMY' };
42
+ }
43
+ /**
44
+ * The offset a rendered zone stands for, in minutes, or undefined when
45
+ * it cannot be known.
46
+ *
47
+ * Outside ISO the server writes the zone's *abbreviation* where it has
48
+ * one - `EST`, `IST` - and those do not identify an offset: IST is India
49
+ * at +05:30, Israel at +02:00 and Ireland at +01:00. A numeric offset
50
+ * and UTC/GMT are exact, and anything else is refused rather than
51
+ * guessed at.
52
+ */
53
+ function zoneOffsetMinutes(zone) {
54
+ if (zone === 'UTC' || zone === 'GMT' || zone === 'Z')
55
+ return 0;
56
+ const m = /^([+-])(\d{1,2})(?::?(\d{2}))?$/.exec(zone);
57
+ if (!m)
58
+ return undefined;
59
+ const sign = m[1] === '-' ? -1 : 1;
60
+ return sign * (+m[2] * 60 + (m[3] ? +m[3] : 0));
61
+ }
62
+ function build(y, mon, d, h, mi, s, ms, offsetMinutes, utc) {
63
+ if (offsetMinutes !== undefined)
64
+ return new Date(Date.UTC(y, mon, d, h, mi - offsetMinutes, s, ms));
65
+ if (utc)
66
+ return new Date(Date.UTC(y, mon, d, h, mi, s, ms));
67
+ return new Date(y, mon, d, h, mi, s, ms);
68
+ }
69
+ function fraction(f) {
70
+ return f ? +(f + '000').slice(0, 3) : 0;
71
+ }
72
+ /**
73
+ * Reads a date or timestamp the server rendered in `style`.
74
+ *
75
+ * Returns undefined when the string is not that style's shape - a `BC`
76
+ * year, an `infinity`, or a zone abbreviation nobody can resolve - so
77
+ * the caller decides what to do with it. What it must never do is guess:
78
+ * handing `05.03.2024` to `new Date()` is how the fifth of March became
79
+ * the third of May, silently, for every day of the month up to twelve.
80
+ */
81
+ export function parseStyledDateTime(str, style, utc) {
82
+ // Every style writes it the same way, and it is not a date.
83
+ if (str.endsWith(' BC'))
84
+ return undefined;
85
+ if (style.style === 'Postgres') {
86
+ const m = POSTGRES_PATTERN.exec(str);
87
+ if (m) {
88
+ const mon = MONTHS[(m[1] || m[4]).toLowerCase()];
89
+ if (mon === undefined)
90
+ return undefined;
91
+ const day = +(m[2] || m[3]);
92
+ let offset;
93
+ if (m[10]) {
94
+ offset = zoneOffsetMinutes(m[10]);
95
+ if (offset === undefined)
96
+ return undefined;
97
+ }
98
+ return build(+m[9], mon, day, +m[5], +m[6], +m[7], fraction(m[8]), offset, utc);
99
+ }
100
+ const d = POSTGRES_DATE_PATTERN.exec(str);
101
+ if (!d)
102
+ return undefined;
103
+ const day = +(style.dayFirst ? d[1] : d[2]);
104
+ const mon = +(style.dayFirst ? d[2] : d[1]) - 1;
105
+ return build(+d[3], mon, day, 0, 0, 0, 0, undefined, utc);
106
+ }
107
+ const m = NUMERIC_PATTERN.exec(str);
108
+ if (!m)
109
+ return undefined;
110
+ // German writes dots and SQL writes slashes; a string using the other
111
+ // one is not this session's rendering and is not read as if it were.
112
+ if (m[2] !== (style.style === 'German' ? '.' : '/'))
113
+ return undefined;
114
+ const day = +(style.dayFirst ? m[1] : m[3]);
115
+ const mon = +(style.dayFirst ? m[3] : m[1]) - 1;
116
+ let offset;
117
+ if (m[9]) {
118
+ offset = zoneOffsetMinutes(m[9]);
119
+ if (offset === undefined)
120
+ return undefined;
121
+ }
122
+ return build(+m[4], mon, day, m[5] ? +m[5] : 0, m[6] ? +m[6] : 0, m[7] ? +m[7] : 0, fraction(m[8]), offset, utc);
123
+ }
@@ -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
+ }
@@ -1,3 +1,4 @@
1
- export declare function parseDate(str: string, utc?: boolean): Date | number;
2
- export declare function parseDateTime(str: string, utc?: boolean): Date | number;
3
- export declare function parseDateTimeTz(str: string, utc?: boolean): Date | number;
1
+ import { type PgDateStyle } from './date-style.js';
2
+ export declare function parseDate(str: string, utc?: boolean, dateStyle?: PgDateStyle): Date | number;
3
+ export declare function parseDateTime(str: string, utc?: boolean, dateStyle?: PgDateStyle): Date | number;
4
+ export declare function parseDateTimeTz(str: string, utc?: boolean, dateStyle?: PgDateStyle): Date | number;
@@ -1,4 +1,5 @@
1
1
  // noinspection RegExpUnnecessaryNonCapturingGroup
2
+ import { parseStyledDateTime } from './date-style.js';
2
3
  import { fastParseInt } from './fast-parseint.js';
3
4
  import { parsePgTimestampString } from './parse-pg-timestamp.js';
4
5
  // noinspection RegExpUnnecessaryNonCapturingGroup
@@ -21,7 +22,12 @@ function fillTimeArgs(m, args) {
21
22
  }
22
23
  }
23
24
  }
24
- export function parseDate(str, utc) {
25
+ export function parseDate(str, utc, dateStyle) {
26
+ if (dateStyle) {
27
+ const styled = parseStyledDateTime(str, dateStyle, utc);
28
+ if (styled)
29
+ return styled;
30
+ }
25
31
  const m = str.match(TIMESTAMP_PATTERN);
26
32
  if (!m)
27
33
  return noMatch(str);
@@ -40,7 +46,12 @@ export function parseDate(str, utc) {
40
46
  return new Date(Date.UTC(...args));
41
47
  return new Date(...args);
42
48
  }
43
- export function parseDateTime(str, utc) {
49
+ export function parseDateTime(str, utc, dateStyle) {
50
+ if (dateStyle) {
51
+ const styled = parseStyledDateTime(str, dateStyle, utc);
52
+ if (styled)
53
+ return styled;
54
+ }
44
55
  if (!utc) {
45
56
  if (str === 'infinity')
46
57
  return Infinity;
@@ -49,9 +60,14 @@ export function parseDateTime(str, utc) {
49
60
  const fast = parsePgTimestampString(str);
50
61
  if (fast)
51
62
  return fast;
52
- const d = new Date(str);
53
- if (!isNaN(d.getTime()))
54
- return d;
63
+ // Only reached under the ISO DateStyle: `new Date()` reads
64
+ // `05.03.2024` as the third of May, and a session that renders dates
65
+ // that way has said so above.
66
+ if (!dateStyle) {
67
+ const d = new Date(str);
68
+ if (!isNaN(d.getTime()))
69
+ return d;
70
+ }
55
71
  }
56
72
  const m = str.match(TIMESTAMP_PATTERN);
57
73
  if (!m)
@@ -72,7 +88,12 @@ export function parseDateTime(str, utc) {
72
88
  return new Date(Date.UTC(...args));
73
89
  return new Date(...args);
74
90
  }
75
- export function parseDateTimeTz(str, utc) {
91
+ export function parseDateTimeTz(str, utc, dateStyle) {
92
+ if (dateStyle) {
93
+ const styled = parseStyledDateTime(str, dateStyle, utc);
94
+ if (styled)
95
+ return styled;
96
+ }
76
97
  if (str === 'infinity')
77
98
  return Infinity;
78
99
  if (str === '-infinity')
@@ -80,9 +101,13 @@ export function parseDateTimeTz(str, utc) {
80
101
  const fast = parsePgTimestampString(str);
81
102
  if (fast)
82
103
  return fast;
83
- const native = new Date(str);
84
- if (!isNaN(native.getTime()))
85
- return native;
104
+ // See parseDateTime(): the engine's guess is only trusted where the
105
+ // session renders ISO.
106
+ if (!dateStyle) {
107
+ const native = new Date(str);
108
+ if (!isNaN(native.getTime()))
109
+ return native;
110
+ }
86
111
  const m = str.match(TIMESTAMP_PATTERN);
87
112
  if (!m)
88
113
  return noMatch(str);
@@ -42,6 +42,10 @@ export function resolveColumnFormats(fields, options, typeMap) {
42
42
  const named = !!asString && asString.length > 0;
43
43
  if (!fields || (!named && !unknownAsString))
44
44
  return base;
45
+ // Both options do one thing: ask for a column as text. A row that is
46
+ // already all text has nothing left for them to ask.
47
+ if (base === DataFormat.text)
48
+ return base;
45
49
  const l = fields.length;
46
50
  const baseIsArray = Array.isArray(base);
47
51
  const out = new Array(l);
@@ -17,12 +17,28 @@ import type { AnyParseFunction } from '../types.js';
17
17
  * every other row's bytes in it) alive until released.
18
18
  */
19
19
  export declare abstract class RowDecoder {
20
+ /**
21
+ * What `QueryResult.rowType`/`Cursor.rowType` reports for rows this
22
+ * decoder produced - a promise to the caller about their shape, so it
23
+ * is the decoder's to make. `'custom'` here is the honest answer for
24
+ * a decoder nobody else knows anything about; the two built-ins name
25
+ * their own shape, and a subclass of one that still produces that
26
+ * shape inherits the right answer without doing anything.
27
+ *
28
+ * Set it when a subclass changes the shape - a decoder that hands
29
+ * back a lazy view rather than a plain array is not an `'array'`, and
30
+ * a caller that indexes it because the result said so would be
31
+ * reading the wrong thing.
32
+ */
33
+ readonly rowType: 'array' | 'object' | 'custom';
20
34
  abstract decode(parsers: AnyParseFunction[], data: Buffer, columnCount: number, options: DataMappingOptions, fields: FieldInfo[]): any;
21
35
  }
22
36
  export declare class ArrayRowDecoder extends RowDecoder {
37
+ readonly rowType: "array";
23
38
  decode(parsers: AnyParseFunction[], data: Buffer, columnCount: number, options: DataMappingOptions): any[];
24
39
  }
25
40
  export declare class ObjectRowDecoder extends RowDecoder {
41
+ readonly rowType: "object";
26
42
  decode(parsers: AnyParseFunction[], data: Buffer, columnCount: number, options: DataMappingOptions, fields: FieldInfo[]): object;
27
43
  }
28
44
  export declare const DEFAULT_ARRAY_ROW_DECODER: ArrayRowDecoder;
@@ -40,10 +56,14 @@ interface RowDecoderOptions {
40
56
  */
41
57
  export declare function resolveRowDecoder(options: RowDecoderOptions): RowDecoder;
42
58
  /**
43
- * The `rowType` string surfaced on `QueryResult`/`Cursor.rowType`. `'custom'`
44
- * when the caller supplied their own RowDecoder subclass - there's no way to
45
- * know what shape it returns - otherwise the same `'array' | 'object'` this
46
- * option has always reported.
59
+ * The `rowType` string surfaced on `QueryResult`/`Cursor.rowType`.
60
+ *
61
+ * Asked of the decoder rather than worked out from its class: an
62
+ * `instanceof` test answers `'array'` for anything derived from
63
+ * `ArrayRowDecoder`, including a subclass that overrode `decode()` to
64
+ * return something else entirely - which is the only reason to subclass
65
+ * it. The built-ins declare their own shape, so the answers are
66
+ * unchanged, and a subclass that changes the shape can now say so.
47
67
  */
48
68
  export declare function resolveRowType(options: RowDecoderOptions): 'array' | 'object' | 'custom';
49
69
  export {};
@@ -15,13 +15,29 @@ import { parseObjectRow, parseRow } from './parse-row.js';
15
15
  * every other row's bytes in it) alive until released.
16
16
  */
17
17
  export class RowDecoder {
18
+ /**
19
+ * What `QueryResult.rowType`/`Cursor.rowType` reports for rows this
20
+ * decoder produced - a promise to the caller about their shape, so it
21
+ * is the decoder's to make. `'custom'` here is the honest answer for
22
+ * a decoder nobody else knows anything about; the two built-ins name
23
+ * their own shape, and a subclass of one that still produces that
24
+ * shape inherits the right answer without doing anything.
25
+ *
26
+ * Set it when a subclass changes the shape - a decoder that hands
27
+ * back a lazy view rather than a plain array is not an `'array'`, and
28
+ * a caller that indexes it because the result said so would be
29
+ * reading the wrong thing.
30
+ */
31
+ rowType = 'custom';
18
32
  }
19
33
  export class ArrayRowDecoder extends RowDecoder {
34
+ rowType = 'array';
20
35
  decode(parsers, data, columnCount, options) {
21
36
  return parseRow(parsers, data, columnCount, options);
22
37
  }
23
38
  }
24
39
  export class ObjectRowDecoder extends RowDecoder {
40
+ rowType = 'object';
25
41
  decode(parsers, data, columnCount, options, fields) {
26
42
  return parseObjectRow(parsers, data, columnCount, options, fields);
27
43
  }
@@ -48,16 +64,15 @@ export function resolveRowDecoder(options) {
48
64
  : DEFAULT_ARRAY_ROW_DECODER;
49
65
  }
50
66
  /**
51
- * The `rowType` string surfaced on `QueryResult`/`Cursor.rowType`. `'custom'`
52
- * when the caller supplied their own RowDecoder subclass - there's no way to
53
- * know what shape it returns - otherwise the same `'array' | 'object'` this
54
- * option has always reported.
67
+ * The `rowType` string surfaced on `QueryResult`/`Cursor.rowType`.
68
+ *
69
+ * Asked of the decoder rather than worked out from its class: an
70
+ * `instanceof` test answers `'array'` for anything derived from
71
+ * `ArrayRowDecoder`, including a subclass that overrode `decode()` to
72
+ * return something else entirely - which is the only reason to subclass
73
+ * it. The built-ins declare their own shape, so the answers are
74
+ * unchanged, and a subclass that changes the shape can now say so.
55
75
  */
56
76
  export function resolveRowType(options) {
57
- const decoder = resolveRowDecoder(options);
58
- if (decoder instanceof ObjectRowDecoder)
59
- return 'object';
60
- if (decoder instanceof ArrayRowDecoder)
61
- return 'array';
62
- return 'custom';
77
+ return resolveRowDecoder(options).rowType;
63
78
  }
package/util/sql-tag.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import { BindParam } from '../connection/bind-param.js';
2
2
  import { DataTypeMap, GlobalTypeMap } from '../data-type-map.js';
3
+ import { arrayLeaf } from './array-leaf.js';
3
4
  import { escapeIdentifier } from './escape-identifier.js';
4
5
  import { escapeLiteral } from './escape-literal.js';
6
+ import { formatDateParam } from './format-datetime.js';
7
+ import { stringifyArrayLiteral } from './stringify-arrayliteral.js';
8
+ import { isUnspecifiedParam } from './unspecified-param.js';
5
9
  /**
6
10
  * A statement built by the `sql` tag: its text with the values pulled out.
7
11
  *
@@ -171,6 +175,18 @@ function encodeLiteral(value, typeMap, options) {
171
175
  }
172
176
  if (v == null)
173
177
  return 'null';
178
+ // A Date and a string go in bare, exactly as they go out as
179
+ // parameters with no declared type (see unspecified-param.ts): an
180
+ // unadorned literal is `unknown` and takes the type of wherever it
181
+ // lands. The cast this path adds everywhere else is what keeps
182
+ // query() and execute() meaning the same thing, and for these two it
183
+ // is what would break that - `'{"a":1}'::varchar` is refused by a
184
+ // json column that takes the parameter without complaint.
185
+ if (oid == null && isUnspecifiedParam(v)) {
186
+ if (!Array.isArray(v))
187
+ return escapeLiteral(v instanceof Date ? formatDateParam(v, options) : '' + v);
188
+ return escapeLiteral(stringifyArrayLiteral(v, options, arrayLeaf(v) instanceof Date ? formatDateParam : undefined));
189
+ }
174
190
  oid = oid ?? typeMap.determine(v);
175
191
  /* c8 ignore start - typeMap.determine() always falls back to the
176
192
  "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,7 @@
1
+ /** Whether `sql` opens or closes a transaction. */
2
+ export declare function isTransactionCommand(sql: string): boolean;
3
+ /**
4
+ * Whether `sql` must not run inside a savepoint - it is a transaction
5
+ * command, or it configures the transaction.
6
+ */
7
+ export declare function refusesSavepoint(sql: string): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Statements that open or close a transaction, and so manage their own
3
+ * boundaries: an implicit BEGIN or COMMIT around one would be wrong, and
4
+ * wrapping one in a savepoint - COMMIT, say - is nonsense.
5
+ */
6
+ const TRANSACTION_COMMAND_PATTERN = /^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i;
7
+ /**
8
+ * `SET TRANSACTION`, which configures the transaction it runs in rather
9
+ * than opening or closing one.
10
+ *
11
+ * It needs an answer of its own because the two are not the same
12
+ * question. PostgreSQL refuses two of its forms inside a subtransaction
13
+ * - `SET TRANSACTION ISOLATION LEVEL` and `SET TRANSACTION [NOT]
14
+ * DEFERRABLE`, both 25001 - so the savepoint `rollbackOnError` puts
15
+ * around every statement in a transaction made them impossible to run
16
+ * at all. But the implicit BEGIN that `autoCommit: false` adds is
17
+ * exactly what they need, so answering "transaction command" for them
18
+ * and suppressing both would trade one failure for another.
19
+ */
20
+ const CONFIGURES_TRANSACTION_PATTERN = /^\bSET\s+TRANSACTION\b/i;
21
+ /** Whether `sql` opens or closes a transaction. */
22
+ export function isTransactionCommand(sql) {
23
+ return TRANSACTION_COMMAND_PATTERN.test(sql);
24
+ }
25
+ /**
26
+ * Whether `sql` must not run inside a savepoint - it is a transaction
27
+ * command, or it configures the transaction.
28
+ */
29
+ export function refusesSavepoint(sql) {
30
+ return (TRANSACTION_COMMAND_PATTERN.test(sql) ||
31
+ CONFIGURES_TRANSACTION_PATTERN.test(sql));
32
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Whether a parameter goes out with no declared type (OID 0), as text,
3
+ * for the server to resolve from wherever it lands.
4
+ *
5
+ * Two kinds of value cannot say what they are:
6
+ *
7
+ * - A `Date` is an instant or a wall clock depending on the column, and
8
+ * declaring either one moves the other. See `format-datetime.ts`.
9
+ * - A string is whichever of PostgreSQL's text-input types the context
10
+ * calls for. `determine()` answers `varchar`, and declaring that stops
11
+ * the server inferring: `insert into t(j) values($1)` into a `json`
12
+ * column, `id = $1` against a `uuid`, `coalesce($1, 1)` and an enum
13
+ * column all fail with "expression is of type character varying".
14
+ * Unspecified, every one of them works.
15
+ *
16
+ * This is what `pg` sends for both, and the divergence from it is what
17
+ * the shapes above were failing on. It is not free: a parameter with no
18
+ * context to be resolved from - `$1 is null`, `array_agg($1)`,
19
+ * `concat($1, 1)` - now raises "could not determine data type", exactly
20
+ * as it does under `pg`. A cast (`$1::text`) or a named type
21
+ * (`new BindParam(DataTypeOIDs.varchar, v)`) says which type is meant.
22
+ *
23
+ * Numbers, booleans and Buffers keep their declared types: they are
24
+ * already right nearly everywhere, and sending them as text would give
25
+ * up the binary encoding for no correctness gain.
26
+ */
27
+ export declare function isUnspecifiedParam(v: any): boolean;
@@ -0,0 +1,31 @@
1
+ import { arrayLeaf } from './array-leaf.js';
2
+ /**
3
+ * Whether a parameter goes out with no declared type (OID 0), as text,
4
+ * for the server to resolve from wherever it lands.
5
+ *
6
+ * Two kinds of value cannot say what they are:
7
+ *
8
+ * - A `Date` is an instant or a wall clock depending on the column, and
9
+ * declaring either one moves the other. See `format-datetime.ts`.
10
+ * - A string is whichever of PostgreSQL's text-input types the context
11
+ * calls for. `determine()` answers `varchar`, and declaring that stops
12
+ * the server inferring: `insert into t(j) values($1)` into a `json`
13
+ * column, `id = $1` against a `uuid`, `coalesce($1, 1)` and an enum
14
+ * column all fail with "expression is of type character varying".
15
+ * Unspecified, every one of them works.
16
+ *
17
+ * This is what `pg` sends for both, and the divergence from it is what
18
+ * the shapes above were failing on. It is not free: a parameter with no
19
+ * context to be resolved from - `$1 is null`, `array_agg($1)`,
20
+ * `concat($1, 1)` - now raises "could not determine data type", exactly
21
+ * as it does under `pg`. A cast (`$1::text`) or a named type
22
+ * (`new BindParam(DataTypeOIDs.varchar, v)`) says which type is meant.
23
+ *
24
+ * Numbers, booleans and Buffers keep their declared types: they are
25
+ * already right nearly everywhere, and sending them as text would give
26
+ * up the binary encoding for no correctness gain.
27
+ */
28
+ export function isUnspecifiedParam(v) {
29
+ const x = arrayLeaf(v);
30
+ return typeof x === 'string' || x instanceof Date;
31
+ }