postgrejs 3.4.0 → 3.6.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 (41) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +29 -4
  3. package/connection/connection.d.ts +37 -0
  4. package/connection/connection.js +85 -7
  5. package/connection/cursor.d.ts +8 -0
  6. package/connection/cursor.js +39 -1
  7. package/connection/intl-connection.d.ts +6 -1
  8. package/connection/intl-connection.js +284 -122
  9. package/connection/pool.d.ts +16 -0
  10. package/connection/pool.js +52 -6
  11. package/connection/portal.d.ts +1 -1
  12. package/connection/portal.js +3 -2
  13. package/connection/prepared-statement.js +8 -4
  14. package/data-type-map.d.ts +11 -0
  15. package/data-type-map.js +20 -2
  16. package/data-types/char-type.js +15 -0
  17. package/data-types/date-type.js +4 -18
  18. package/data-types/float4-type.js +79 -1
  19. package/data-types/json-type.js +7 -12
  20. package/data-types/jsonb-type.js +7 -12
  21. package/data-types/time-type.js +2 -15
  22. package/data-types/timestamp-type.js +8 -29
  23. package/data-types/timestamptz-type.js +7 -31
  24. package/interfaces/batch-result.d.ts +1 -1
  25. package/interfaces/command-result.d.ts +21 -1
  26. package/interfaces/data-mapping-options.d.ts +16 -0
  27. package/interfaces/data-type.d.ts +11 -0
  28. package/interfaces/query-options.d.ts +25 -3
  29. package/package.json +1 -1
  30. package/protocol/frontend.d.ts +7 -0
  31. package/protocol/frontend.js +5 -3
  32. package/protocol/pg-socket.d.ts +2 -2
  33. package/protocol/pg-socket.js +12 -6
  34. package/util/channel-name.d.ts +24 -0
  35. package/util/channel-name.js +44 -0
  36. package/util/get-parsers.d.ts +2 -1
  37. package/util/get-parsers.js +16 -1
  38. package/util/resolve-column-formats.d.ts +33 -0
  39. package/util/resolve-column-formats.js +67 -0
  40. package/util/wrap-row-description.d.ts +2 -1
  41. package/util/wrap-row-description.js +7 -1
@@ -24,7 +24,27 @@ export interface CommandResult {
24
24
  */
25
25
  executeTime?: number;
26
26
  /**
27
- * How many rows affected
27
+ * True when the server stopped at the `fetchCount` limit instead of
28
+ * running the statement to completion: the portal suspended and the rows
29
+ * behind it were never fetched, so `rows` is a prefix rather than the
30
+ * whole result.
31
+ *
32
+ * Only ever set when an explicit `fetchCount` was given - the default
33
+ * asks for every row. Note the server does not look ahead: a statement
34
+ * whose result is exactly `fetchCount` rows long suspends too, so this
35
+ * means "the limit was reached", not "there are definitely more rows".
36
+ *
37
+ * Use a Cursor when the point is to read a large result in batches; a
38
+ * suspended portal on this path is simply discarded by the Sync that
39
+ * follows.
40
+ */
41
+ suspended?: boolean;
42
+ /**
43
+ * How many rows the statement changed, for INSERT/UPDATE/DELETE/MERGE
44
+ * (MERGE reports its own total of inserted, updated and deleted rows,
45
+ * and needs PostgreSQL 15+). Left undefined for every other command,
46
+ * including SELECT - the count those report describes rows returned or
47
+ * moved, not rows affected.
28
48
  */
29
49
  rowsAffected?: number;
30
50
  }
@@ -5,5 +5,21 @@ export interface DataMappingOptions {
5
5
  * @default false
6
6
  */
7
7
  utcDates?: boolean;
8
+ /**
9
+ * OIDs whose columns are to be handed back exactly as the server renders
10
+ * them, instead of being converted to a JavaScript value.
11
+ *
12
+ * This is a wire-level request, not a formatting choice made here: the
13
+ * listed columns are asked for in PostgreSQL's text format and returned
14
+ * unparsed, so the string is the server's own and nothing can drift
15
+ * between the two. Bind's result format codes are positional, though, so
16
+ * the OIDs can only be mapped onto columns once their types are known -
17
+ * a query whose statement is not prepared asks for the whole row as text
18
+ * instead (see IntlConnection.queryOnce()).
19
+ *
20
+ * An array column is selected by its own array OID (`_timestamptz`),
21
+ * never by its element's, and then the whole array literal is the string
22
+ * that comes back.
23
+ */
8
24
  fetchAsString?: OID[];
9
25
  }
@@ -7,6 +7,17 @@ export interface DataType {
7
7
  jsType: string;
8
8
  arraySeparator?: string;
9
9
  isType: (v: any) => boolean;
10
+ /**
11
+ * Whether DataTypeMap.determine() may pick this type for a value, which
12
+ * it does by asking `isType`. Defaults to true.
13
+ *
14
+ * `false` is for a type that can genuinely hold the value - so `isType`
15
+ * answers truthfully for anyone who asks it directly - but that no
16
+ * caller means when they pass a plain JavaScript value. Inference then
17
+ * passes it by and the type is reached only by asking for it, with
18
+ * `new BindParam(oid, value)`.
19
+ */
20
+ inferrable?: boolean;
10
21
  decodeBinary: DecodeBinaryFunction;
11
22
  decodeText: DecodeTextFunction;
12
23
  decodeTextBuffer?: DecodeTextBufferFunction;
@@ -37,7 +37,15 @@ export interface QueryOptions extends DataMappingOptions {
37
37
  */
38
38
  typeMap?: DataTypeMap;
39
39
  /**
40
- * If true, returns Cursor instance instead of rows
40
+ * If true, returns Cursor instance instead of rows.
41
+ *
42
+ * The cursor reads through a portal, and a portal lives only as long as
43
+ * the transaction that created it. Outside an explicit transaction that
44
+ * is the implicit one, which any other statement on the same connection
45
+ * ends - so running another query on that connection while the cursor is
46
+ * open destroys it. Open the cursor inside a transaction, or give it a
47
+ * connection of its own; `Pool.query()` does the latter for you and
48
+ * keeps the connection out of the pool until the cursor closes.
41
49
  */
42
50
  cursor?: boolean;
43
51
  /**
@@ -50,8 +58,22 @@ export interface QueryOptions extends DataMappingOptions {
50
58
  */
51
59
  columnFormat?: DataFormat | DataFormat[];
52
60
  /**
53
- * Specifies how many rows will be fetched. For Cursor, this value specifies how many rows will be fetched in a batch
54
- * @default 100
61
+ * Maximum number of rows to fetch.
62
+ *
63
+ * `query()` fetches every row by default. Giving an explicit limit stops
64
+ * the server at that many rows and leaves the rest unfetched: `rows` is
65
+ * then a prefix and `suspended` is set on the result to say so. There is
66
+ * no way to ask for the remainder afterwards - the portal is discarded
67
+ * by the Sync that closes the call - so use a Cursor when the point is
68
+ * to read a large result a piece at a time.
69
+ *
70
+ * For a Cursor this is the batch size instead: how many rows each fetch
71
+ * asks for, defaulting to 100, with the cursor itself walking the whole
72
+ * result.
73
+ *
74
+ * 0 means unlimited, as it does in the protocol.
75
+ *
76
+ * @default 0 for query(), 100 for a Cursor's batches
55
77
  */
56
78
  fetchCount?: number;
57
79
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.4.0",
4
+ "version": "3.6.0",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
@@ -20,6 +20,13 @@ export declare namespace Frontend {
20
20
  paramTypes?: Maybe<OID>[];
21
21
  params?: any[];
22
22
  queryOptions: QueryOptions;
23
+ /**
24
+ * Result format codes to write, overriding queryOptions.columnFormat.
25
+ * Set by callers that know the statement's column types and have
26
+ * turned fetchAsString's OID list into positional codes - see
27
+ * resolveColumnFormats().
28
+ */
29
+ columnFormat?: Protocol.DataFormat | Protocol.DataFormat[];
23
30
  }
24
31
  interface ParseMessageArgs {
25
32
  statement?: string;
@@ -133,9 +133,11 @@ export class Frontend {
133
133
  io.writeCString(args.portal || '', 'utf8');
134
134
  io.writeCString(args.statement || '', 'utf8');
135
135
  const { params, paramTypes, queryOptions } = args;
136
- const columnFormat = queryOptions.columnFormat != null
137
- ? queryOptions.columnFormat
138
- : DEFAULT_COLUMN_FORMAT;
136
+ const columnFormat = args.columnFormat != null
137
+ ? args.columnFormat
138
+ : queryOptions.columnFormat != null
139
+ ? queryOptions.columnFormat
140
+ : DEFAULT_COLUMN_FORMAT;
139
141
  if (params && params.length) {
140
142
  io.writeInt16BE(params.length);
141
143
  const formatOffset = io.position;
@@ -119,9 +119,9 @@ export declare class PgSocket extends SafeEventEmitter {
119
119
  */
120
120
  sendPipelineMessages(args: {
121
121
  statements: {
122
- parse: Frontend.ParseMessageArgs;
122
+ parse?: Frontend.ParseMessageArgs;
123
123
  bind: Frontend.BindMessageArgs;
124
- describe: Frontend.DescribeMessageArgs;
124
+ describe?: Frontend.DescribeMessageArgs;
125
125
  execute: Frontend.ExecuteMessageArgs;
126
126
  }[];
127
127
  }, cb: CaptureCallback): Promise<any>;
@@ -280,17 +280,23 @@ export class PgSocket extends SafeEventEmitter {
280
280
  */
281
281
  sendPipelineMessages(args, cb) {
282
282
  const l = args.statements.length;
283
- const data = new Array(l * 4 + 1);
283
+ const data = [];
284
284
  let i;
285
285
  let st;
286
286
  for (i = 0; i < l; i++) {
287
287
  st = args.statements[i];
288
- data[i * 4] = this._frontend.getParseMessage(st.parse);
289
- data[i * 4 + 1] = this._frontend.getBindMessage(st.bind);
290
- data[i * 4 + 2] = this._frontend.getDescribeMessage(st.describe);
291
- data[i * 4 + 3] = this._frontend.getExecuteMessage(st.execute);
288
+ // Parse and Describe are both optional: a statement bound to one the
289
+ // connection has already prepared needs neither, since the server
290
+ // still holds the plan and IntlConnection still holds the
291
+ // RowDescription that came with it.
292
+ if (st.parse)
293
+ data.push(this._frontend.getParseMessage(st.parse));
294
+ data.push(this._frontend.getBindMessage(st.bind));
295
+ if (st.describe)
296
+ data.push(this._frontend.getDescribeMessage(st.describe));
297
+ data.push(this._frontend.getExecuteMessage(st.execute));
292
298
  }
293
- data[l * 4] = this._frontend.getSyncMessage();
299
+ data.push(this._frontend.getSyncMessage());
294
300
  return this._sendAndCapture(data, cb, 'sendPipelineMessages', args);
295
301
  }
296
302
  /**
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Checks a LISTEN/NOTIFY channel name and answers with the name the server
3
+ * will actually use for it.
4
+ *
5
+ * The name is written into LISTEN/UNLISTEN as an identifier, and an
6
+ * identifier can never be a parameter - so it goes through
7
+ * escapeIdentifier() at the call site rather than being restricted to a
8
+ * pattern. Any name PostgreSQL accepts then works, one-character and
9
+ * underscore-leading names included, which the old `/^[A-Z]\w+$/i` guard
10
+ * refused.
11
+ *
12
+ * Quoting alone would change what a name means, though, which is what this
13
+ * function is for: `LISTEN Foo` unquoted subscribes to `foo`, and every
14
+ * notification for it comes back as `foo`. Callers were registering their
15
+ * callback under the name they passed and never hearing from it - any
16
+ * channel name that was not already lower case was silently dead. Folding
17
+ * the same way the server would keeps the registration key and the name
18
+ * the server reports the same string, and keeps `NOTIFY Foo` written by
19
+ * hand on the other side reaching a `listen('Foo')` here.
20
+ *
21
+ * A name that could not have been written unquoted (a space, a dash) has
22
+ * no folding question to answer and is passed through as it is.
23
+ */
24
+ export declare function normalizeChannelName(channel: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * PostgreSQL folds an unquoted identifier to lower case and takes a quoted
3
+ * one literally. A LISTEN channel name is an identifier, so this is the
4
+ * shape that would have been folded had it been written unquoted.
5
+ */
6
+ const UNQUOTED_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
7
+ /** NAMEDATALEN - 1. */
8
+ const MAX_CHANNEL_BYTES = 63;
9
+ /**
10
+ * Checks a LISTEN/NOTIFY channel name and answers with the name the server
11
+ * will actually use for it.
12
+ *
13
+ * The name is written into LISTEN/UNLISTEN as an identifier, and an
14
+ * identifier can never be a parameter - so it goes through
15
+ * escapeIdentifier() at the call site rather than being restricted to a
16
+ * pattern. Any name PostgreSQL accepts then works, one-character and
17
+ * underscore-leading names included, which the old `/^[A-Z]\w+$/i` guard
18
+ * refused.
19
+ *
20
+ * Quoting alone would change what a name means, though, which is what this
21
+ * function is for: `LISTEN Foo` unquoted subscribes to `foo`, and every
22
+ * notification for it comes back as `foo`. Callers were registering their
23
+ * callback under the name they passed and never hearing from it - any
24
+ * channel name that was not already lower case was silently dead. Folding
25
+ * the same way the server would keeps the registration key and the name
26
+ * the server reports the same string, and keeps `NOTIFY Foo` written by
27
+ * hand on the other side reaching a `listen('Foo')` here.
28
+ *
29
+ * A name that could not have been written unquoted (a space, a dash) has
30
+ * no folding question to answer and is passed through as it is.
31
+ */
32
+ export function normalizeChannelName(channel) {
33
+ if (typeof channel !== 'string' || !channel.length)
34
+ throw new TypeError('Channel name must be a non-empty string');
35
+ if (channel.indexOf('\0') >= 0)
36
+ throw new TypeError('Channel name cannot contain NUL (\\0) bytes');
37
+ // Refused rather than passed on: the server truncates a longer
38
+ // identifier instead of rejecting it, so LISTEN would subscribe to a
39
+ // name the caller never asked for and every notification would arrive
40
+ // under that one - the same silent mismatch as the case folding above.
41
+ if (Buffer.byteLength(channel, 'utf8') > MAX_CHANNEL_BYTES)
42
+ throw new TypeError(`Channel name cannot be longer than ${MAX_CHANNEL_BYTES} bytes`);
43
+ return UNQUOTED_IDENTIFIER.test(channel) ? channel.toLowerCase() : channel;
44
+ }
@@ -1,4 +1,5 @@
1
1
  import type { DataTypeMap } from '../data-type-map.js';
2
+ import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
2
3
  import type { Protocol } from '../protocol/protocol.js';
3
4
  import type { AnyParseFunction } from '../types.js';
4
- export declare function getParsers(typeMap: DataTypeMap, fields: Protocol.RowDescription[]): AnyParseFunction[];
5
+ export declare function getParsers(typeMap: DataTypeMap, fields: Protocol.RowDescription[], mappingOptions?: DataMappingOptions): AnyParseFunction[];
@@ -3,13 +3,28 @@ import { decodeBinaryArray } from './decode-binaryarray.js';
3
3
  import { parsePostgresArray } from './parse-array.js';
4
4
  const DefaultColumnParser = (data, offset, len) => data.subarray(offset, offset + len);
5
5
  const DefaultTextColumnParser = (data, offset, len) => data.toString('utf8', offset, offset + len);
6
- export function getParsers(typeMap, fields) {
6
+ export function getParsers(typeMap, fields, mappingOptions) {
7
7
  const parsers = new Array(fields.length);
8
+ const asString = mappingOptions?.fetchAsString;
8
9
  const l = fields.length;
9
10
  let f;
10
11
  let i;
11
12
  for (i = 0; i < l; i++) {
12
13
  f = fields[i];
14
+ // fetchAsString asks for the value exactly as the server renders it,
15
+ // and resolveColumnFormats() has already asked the server to send this
16
+ // column as text - so there is nothing left to do but hand the bytes
17
+ // back. An array column only reaches here when its own array OID was
18
+ // listed, and then the whole literal is the string that comes back.
19
+ // The format check is not redundant: a column can still arrive binary
20
+ // when its types were unknown at Bind time (a pipelined one-shot), and
21
+ // then the registered decoder is the only thing that can read it.
22
+ if (asString &&
23
+ f.format === DataFormat.text &&
24
+ asString.includes(f.dataTypeId)) {
25
+ parsers[i] = DefaultTextColumnParser;
26
+ continue;
27
+ }
13
28
  const dataTypeReg = typeMap.get(f.dataTypeId);
14
29
  if (dataTypeReg) {
15
30
  const isArray = !!dataTypeReg.elementsOID;
@@ -0,0 +1,33 @@
1
+ import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
2
+ import { Protocol } from '../protocol/protocol.js';
3
+ import type { Maybe, OID } from '../types.js';
4
+ export interface ColumnFormatOptions extends DataMappingOptions {
5
+ columnFormat?: Protocol.DataFormat | Protocol.DataFormat[];
6
+ }
7
+ /**
8
+ * Turns `fetchAsString`'s OID list into the positional result format codes
9
+ * a Bind message actually carries.
10
+ *
11
+ * The protocol has no way to say "send this OID as text" - Bind's result
12
+ * format codes are per column, by position, and nothing in Parse/Bind is
13
+ * keyed on a type. So the OID list can only be honored once the column
14
+ * types are known, which means after a Describe and before the Bind that
15
+ * asks for them. Callers that already hold a RowDescription (a prepared
16
+ * statement's, or a cached one's) pay nothing for this; callers that do
17
+ * not have to ask for the whole row as text instead - see queryOnce().
18
+ *
19
+ * A column matches on its own `dataTypeId`, so an array column is only
20
+ * ever selected by its own array OID (`_timestamptz`), never by its
21
+ * element's. Listing the element OID leaves array columns alone.
22
+ *
23
+ * Returns `columnFormat` untouched when there is nothing to do, so the
24
+ * common case allocates nothing and keeps sending a single format code.
25
+ */
26
+ export declare function resolveColumnFormats(fields: Maybe<Protocol.RowDescription[]>, options: ColumnFormatOptions): Protocol.DataFormat | Protocol.DataFormat[];
27
+ /**
28
+ * Whether two `fetchAsString` lists select the same columns. Used to tell
29
+ * a cached set of parsers apart from one built for a different list, which
30
+ * `columnFormat` alone cannot do: an explicitly text column and one turned
31
+ * text by fetchAsString carry the same format code but decode differently.
32
+ */
33
+ export declare function fetchAsStringEqual(a: Maybe<OID[]>, b: Maybe<OID[]>): boolean;
@@ -0,0 +1,67 @@
1
+ import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
2
+ import { Protocol } from '../protocol/protocol.js';
3
+ const DataFormat = Protocol.DataFormat;
4
+ /**
5
+ * Turns `fetchAsString`'s OID list into the positional result format codes
6
+ * a Bind message actually carries.
7
+ *
8
+ * The protocol has no way to say "send this OID as text" - Bind's result
9
+ * format codes are per column, by position, and nothing in Parse/Bind is
10
+ * keyed on a type. So the OID list can only be honored once the column
11
+ * types are known, which means after a Describe and before the Bind that
12
+ * asks for them. Callers that already hold a RowDescription (a prepared
13
+ * statement's, or a cached one's) pay nothing for this; callers that do
14
+ * not have to ask for the whole row as text instead - see queryOnce().
15
+ *
16
+ * A column matches on its own `dataTypeId`, so an array column is only
17
+ * ever selected by its own array OID (`_timestamptz`), never by its
18
+ * element's. Listing the element OID leaves array columns alone.
19
+ *
20
+ * Returns `columnFormat` untouched when there is nothing to do, so the
21
+ * common case allocates nothing and keeps sending a single format code.
22
+ */
23
+ export function resolveColumnFormats(fields, options) {
24
+ const base = options.columnFormat != null ? options.columnFormat : DEFAULT_COLUMN_FORMAT;
25
+ const asString = options.fetchAsString;
26
+ if (!fields || !asString || !asString.length)
27
+ return base;
28
+ const l = fields.length;
29
+ const baseIsArray = Array.isArray(base);
30
+ const out = new Array(l);
31
+ let hit = false;
32
+ let i;
33
+ for (i = 0; i < l; i++) {
34
+ if (asString.includes(fields[i].dataTypeId)) {
35
+ // fetchAsString names the value the caller wants verbatim, so it
36
+ // overrides an explicit columnFormat for its own columns - the two
37
+ // cannot both be honored and the OID list is the more specific ask.
38
+ out[i] = DataFormat.text;
39
+ hit = true;
40
+ }
41
+ else
42
+ out[i] = baseIsArray
43
+ ? (base[i] ?? DEFAULT_COLUMN_FORMAT)
44
+ : base;
45
+ }
46
+ return hit ? out : base;
47
+ }
48
+ /**
49
+ * Whether two `fetchAsString` lists select the same columns. Used to tell
50
+ * a cached set of parsers apart from one built for a different list, which
51
+ * `columnFormat` alone cannot do: an explicitly text column and one turned
52
+ * text by fetchAsString carry the same format code but decode differently.
53
+ */
54
+ export function fetchAsStringEqual(a, b) {
55
+ if (a === b)
56
+ return true;
57
+ if (!a || !b)
58
+ return !a?.length && !b?.length;
59
+ const l = a.length;
60
+ if (l !== b.length)
61
+ return false;
62
+ let i;
63
+ for (i = 0; i < l; i++)
64
+ if (a[i] !== b[i])
65
+ return false;
66
+ return true;
67
+ }
@@ -1,5 +1,6 @@
1
1
  import { DataFormat } from '../constants.js';
2
2
  import type { DataTypeMap } from '../data-type-map.js';
3
+ import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
3
4
  import type { FieldInfo } from '../interfaces/field-info.js';
4
5
  import type { Protocol } from '../protocol/protocol.js';
5
- export declare function wrapRowDescription(typeMap: DataTypeMap, fields: Protocol.RowDescription[], columnFormat: DataFormat | DataFormat[]): FieldInfo[];
6
+ export declare function wrapRowDescription(typeMap: DataTypeMap, fields: Protocol.RowDescription[], columnFormat: DataFormat | DataFormat[], mappingOptions?: DataMappingOptions): FieldInfo[];
@@ -9,7 +9,8 @@ const DataTypeOIDByName = {};
9
9
  for (const oid of Object.keys(DataTypeNames)) {
10
10
  DataTypeOIDByName[DataTypeNames[oid]] = Number(oid);
11
11
  }
12
- export function wrapRowDescription(typeMap, fields, columnFormat) {
12
+ export function wrapRowDescription(typeMap, fields, columnFormat, mappingOptions) {
13
+ const asString = mappingOptions?.fetchAsString;
13
14
  return fields.map((f, idx) => {
14
15
  const cf = Array.isArray(columnFormat) ? columnFormat[idx] : columnFormat;
15
16
  const x = {
@@ -35,6 +36,11 @@ export function wrapRowDescription(typeMap, fields, columnFormat) {
35
36
  if (reg) {
36
37
  x.jsType = reg.jsType;
37
38
  }
39
+ // A column the caller asked for as a string is handed back exactly as
40
+ // the server rendered it, whatever the registered type would otherwise
41
+ // have produced - including an array, which comes back as the literal.
42
+ if (asString && asString.includes(x.dataTypeId))
43
+ x.jsType = 'string';
38
44
  return x;
39
45
  });
40
46
  }