postgrejs 3.5.0 → 3.6.1
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 +40 -1
- package/README.md +22 -4
- package/connection/connection.js +13 -7
- package/connection/cursor.js +22 -1
- package/connection/intl-connection.d.ts +1 -1
- package/connection/intl-connection.js +110 -41
- package/connection/pool.d.ts +22 -0
- package/connection/pool.js +73 -9
- package/connection/portal.d.ts +1 -1
- package/connection/portal.js +3 -2
- package/connection/prepared-statement.js +8 -4
- package/data-type-map.d.ts +11 -0
- package/data-type-map.js +20 -2
- package/data-types/char-type.js +15 -0
- package/data-types/date-type.js +4 -18
- package/data-types/float4-type.js +79 -1
- package/data-types/json-type.js +7 -12
- package/data-types/jsonb-type.js +7 -12
- package/data-types/time-type.js +2 -15
- package/data-types/timestamp-type.js +8 -29
- package/data-types/timestamptz-type.js +7 -31
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/interfaces/batch-result.d.ts +1 -1
- package/interfaces/command-result.d.ts +21 -1
- package/interfaces/data-mapping-options.d.ts +16 -0
- package/interfaces/data-type.d.ts +11 -0
- package/interfaces/query-options.d.ts +25 -3
- package/package.json +1 -1
- package/protocol/connection-lost-error.d.ts +40 -0
- package/protocol/connection-lost-error.js +54 -0
- package/protocol/frontend.d.ts +7 -0
- package/protocol/frontend.js +5 -3
- package/protocol/pg-socket.d.ts +7 -0
- package/protocol/pg-socket.js +26 -2
- package/util/channel-name.d.ts +24 -0
- package/util/channel-name.js +44 -0
- package/util/get-parsers.d.ts +2 -1
- package/util/get-parsers.js +16 -1
- package/util/resolve-column-formats.d.ts +33 -0
- package/util/resolve-column-formats.js +67 -0
- package/util/wrap-row-description.d.ts +2 -1
- package/util/wrap-row-description.js +7 -1
|
@@ -34,14 +34,13 @@ export const TimestamptzType = {
|
|
|
34
34
|
buf.writeInt32BE(hi);
|
|
35
35
|
buf.writeUInt32BE(lo);
|
|
36
36
|
},
|
|
37
|
-
decodeBinary(v, offset = 0
|
|
38
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.timestamptz);
|
|
37
|
+
decodeBinary(v, offset = 0) {
|
|
39
38
|
const hi = v.readInt32BE(offset);
|
|
40
39
|
const lo = v.readUInt32BE(offset + 4);
|
|
41
40
|
if (lo === 0xffffffff && hi === 0x7fffffff)
|
|
42
|
-
return
|
|
41
|
+
return Infinity;
|
|
43
42
|
if (lo === 0x00000000 && hi === -0x80000000)
|
|
44
|
-
return
|
|
43
|
+
return -Infinity;
|
|
45
44
|
// Shift from 2000 to 1970. A timestamptz is an absolute instant, so
|
|
46
45
|
// this is already the value - there is nothing to reinterpret against
|
|
47
46
|
// the local zone the way `timestamp` (which carries no zone of its
|
|
@@ -51,23 +50,10 @@ export const TimestamptzType = {
|
|
|
51
50
|
// ambiguous read as wall-clock, so the rebuild silently picked the
|
|
52
51
|
// other one and moved the value an hour. The text path never did that,
|
|
53
52
|
// so the same row decoded binary and text disagreed.
|
|
54
|
-
|
|
55
|
-
return fetchAsString ? dateToTimestamptzString(d) : d;
|
|
53
|
+
return new Date((lo + hi * timeMul) / 1000 + timeShift);
|
|
56
54
|
},
|
|
57
55
|
decodeText(v, options) {
|
|
58
|
-
|
|
59
|
-
if (options.fetchAsString?.includes(DataTypeOIDs.timestamptz)) {
|
|
60
|
-
if (d instanceof Date)
|
|
61
|
-
return dateToTimestamptzString(d);
|
|
62
|
-
if (d === Infinity)
|
|
63
|
-
return 'infinity';
|
|
64
|
-
if (d === -Infinity)
|
|
65
|
-
return '-infinity';
|
|
66
|
-
// parseDateTimeTz() only ever returns a Date, Infinity or -Infinity,
|
|
67
|
-
// never anything else, so this is unreachable.
|
|
68
|
-
return '';
|
|
69
|
-
}
|
|
70
|
-
return d;
|
|
56
|
+
return parseDateTimeTz(v, options.utcDates);
|
|
71
57
|
},
|
|
72
58
|
// Reads PostgreSQL's own timestamp shape straight from the wire bytes.
|
|
73
59
|
// Building the string first and handing it to decodeText() costs about
|
|
@@ -75,26 +61,16 @@ export const TimestamptzType = {
|
|
|
75
61
|
// scanned a character at a time. Anything not in that exact shape
|
|
76
62
|
// (infinity, a BC suffix, an LMT-style offset) has no Date to give back
|
|
77
63
|
// and takes the original path, which still needs the string.
|
|
78
|
-
//
|
|
79
|
-
// Note this type's decodeText always parses first and only branches on
|
|
80
|
-
// fetchAsString afterward (unlike date/time/timestamp, which early-return
|
|
81
|
-
// the raw string), so the same order is kept here.
|
|
82
64
|
decodeTextBuffer(buf, offset, len, options) {
|
|
83
65
|
const d = parsePgTimestampBuffer(buf, offset, offset + len);
|
|
84
|
-
if (d !== undefined)
|
|
85
|
-
return
|
|
86
|
-
? dateToTimestamptzString(d)
|
|
87
|
-
: d;
|
|
88
|
-
}
|
|
66
|
+
if (d !== undefined)
|
|
67
|
+
return d;
|
|
89
68
|
return TimestamptzType.decodeText(buf.toString('latin1', offset, offset + len), options);
|
|
90
69
|
},
|
|
91
70
|
isType(v) {
|
|
92
71
|
return v instanceof Date;
|
|
93
72
|
},
|
|
94
73
|
};
|
|
95
|
-
function dateToTimestamptzString(d) {
|
|
96
|
-
return d.toISOString().replace('T', ' ');
|
|
97
|
-
}
|
|
98
74
|
export const ArrayTimestamptzType = {
|
|
99
75
|
...TimestamptzType,
|
|
100
76
|
name: '_timestamptz',
|
package/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from './interfaces/query-result.js';
|
|
|
23
23
|
export * from './interfaces/script-execute-options.js';
|
|
24
24
|
export * from './interfaces/script-result.js';
|
|
25
25
|
export * from './interfaces/statement-prepare-options.js';
|
|
26
|
+
export * from './protocol/connection-lost-error.js';
|
|
26
27
|
export * from './protocol/database-error.js';
|
|
27
28
|
export * from './protocol/smart-buffer.js';
|
|
28
29
|
export * from './types.js';
|
package/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export * from './interfaces/query-result.js';
|
|
|
23
23
|
export * from './interfaces/script-execute-options.js';
|
|
24
24
|
export * from './interfaces/script-result.js';
|
|
25
25
|
export * from './interfaces/statement-prepare-options.js';
|
|
26
|
+
export * from './protocol/connection-lost-error.js';
|
|
26
27
|
export * from './protocol/database-error.js';
|
|
27
28
|
export * from './protocol/smart-buffer.js';
|
|
28
29
|
export * from './types.js';
|
|
@@ -8,7 +8,7 @@ import type { FieldInfo } from './field-info.js';
|
|
|
8
8
|
export interface BatchCommandResult {
|
|
9
9
|
/** The command tag PostgreSQL reported for this set (INSERT/UPDATE/...). */
|
|
10
10
|
command?: string;
|
|
11
|
-
/** Rows affected, for INSERT/UPDATE/DELETE - as in QueryResult. */
|
|
11
|
+
/** Rows affected, for INSERT/UPDATE/DELETE/MERGE - as in QueryResult. */
|
|
12
12
|
rowsAffected?: number;
|
|
13
13
|
/**
|
|
14
14
|
* Rows this set returned, decoded the same way `execute()` decodes them
|
|
@@ -24,7 +24,27 @@ export interface CommandResult {
|
|
|
24
24
|
*/
|
|
25
25
|
executeTime?: number;
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connection whose socket closed without anyone asking it to - the
|
|
3
|
+
* backend terminated by an administrator, a failover, a network fault.
|
|
4
|
+
*
|
|
5
|
+
* Reported on `Connection`'s `'close'` event as its reason, and for a
|
|
6
|
+
* pooled connection on `Pool`'s `'destroy'` (as the second argument) and
|
|
7
|
+
* `'error'`. `Pool` emits `'error'` for this and for a connection that
|
|
8
|
+
* could not be created in the first place, and those want different
|
|
9
|
+
* handling - this class is how a listener tells them apart:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* pool.on('error', err => {
|
|
13
|
+
* if (err.code === '08006') log.warn('pooled connection lost', err.processID);
|
|
14
|
+
* else log.error('could not open a connection', err);
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* `code` is there so that branching does not have to mean `instanceof`,
|
|
19
|
+
* which fails across duplicated copies of the package and is awkward from
|
|
20
|
+
* JavaScript. It reads the same way `DatabaseError.code` does - a
|
|
21
|
+
* SQLSTATE - and `08006 connection_failure` is the one this is. The server
|
|
22
|
+
* never sends it: it cannot report that its own connection went away, so
|
|
23
|
+
* there is no ambiguity about where a code of this value came from.
|
|
24
|
+
*
|
|
25
|
+
* The message deliberately says nothing else: it matches what other
|
|
26
|
+
* drivers report for the same event, so code that branches on it keeps
|
|
27
|
+
* working. What happened to *this* connection is on the object instead -
|
|
28
|
+
* `processID` names the backend, and `cause` carries the socket error
|
|
29
|
+
* when there was one (a clean FIN leaves none).
|
|
30
|
+
*/
|
|
31
|
+
export declare class ConnectionLostError extends Error {
|
|
32
|
+
/**
|
|
33
|
+
* SQLSTATE 08006, `connection_failure`. Synthesized here rather than
|
|
34
|
+
* received - see the class doc.
|
|
35
|
+
*/
|
|
36
|
+
readonly code: string;
|
|
37
|
+
/** The backend's process id, as it was before the connection reset. */
|
|
38
|
+
readonly processID?: number;
|
|
39
|
+
constructor(processID?: number, cause?: unknown);
|
|
40
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connection whose socket closed without anyone asking it to - the
|
|
3
|
+
* backend terminated by an administrator, a failover, a network fault.
|
|
4
|
+
*
|
|
5
|
+
* Reported on `Connection`'s `'close'` event as its reason, and for a
|
|
6
|
+
* pooled connection on `Pool`'s `'destroy'` (as the second argument) and
|
|
7
|
+
* `'error'`. `Pool` emits `'error'` for this and for a connection that
|
|
8
|
+
* could not be created in the first place, and those want different
|
|
9
|
+
* handling - this class is how a listener tells them apart:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* pool.on('error', err => {
|
|
13
|
+
* if (err.code === '08006') log.warn('pooled connection lost', err.processID);
|
|
14
|
+
* else log.error('could not open a connection', err);
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* `code` is there so that branching does not have to mean `instanceof`,
|
|
19
|
+
* which fails across duplicated copies of the package and is awkward from
|
|
20
|
+
* JavaScript. It reads the same way `DatabaseError.code` does - a
|
|
21
|
+
* SQLSTATE - and `08006 connection_failure` is the one this is. The server
|
|
22
|
+
* never sends it: it cannot report that its own connection went away, so
|
|
23
|
+
* there is no ambiguity about where a code of this value came from.
|
|
24
|
+
*
|
|
25
|
+
* The message deliberately says nothing else: it matches what other
|
|
26
|
+
* drivers report for the same event, so code that branches on it keeps
|
|
27
|
+
* working. What happened to *this* connection is on the object instead -
|
|
28
|
+
* `processID` names the backend, and `cause` carries the socket error
|
|
29
|
+
* when there was one (a clean FIN leaves none).
|
|
30
|
+
*/
|
|
31
|
+
export class ConnectionLostError extends Error {
|
|
32
|
+
/**
|
|
33
|
+
* SQLSTATE 08006, `connection_failure`. Synthesized here rather than
|
|
34
|
+
* received - see the class doc.
|
|
35
|
+
*/
|
|
36
|
+
code = '08006';
|
|
37
|
+
/** The backend's process id, as it was before the connection reset. */
|
|
38
|
+
processID;
|
|
39
|
+
constructor(processID, cause) {
|
|
40
|
+
super('Connection terminated unexpectedly');
|
|
41
|
+
this.name = 'ConnectionLostError';
|
|
42
|
+
this.processID = processID;
|
|
43
|
+
// Assigned rather than passed to the constructor, as abortError()
|
|
44
|
+
// does - same reason, the option is newer than this package's floor.
|
|
45
|
+
if (cause !== undefined) {
|
|
46
|
+
try {
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* c8 ignore next */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/protocol/frontend.d.ts
CHANGED
|
@@ -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;
|
package/protocol/frontend.js
CHANGED
|
@@ -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 =
|
|
137
|
-
?
|
|
138
|
-
:
|
|
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;
|
package/protocol/pg-socket.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface SocketError extends Error {
|
|
|
13
13
|
export declare class PgSocket extends SafeEventEmitter {
|
|
14
14
|
options: ConnectionConfiguration;
|
|
15
15
|
private _state;
|
|
16
|
+
/**
|
|
17
|
+
* The socket-level error that is about to end this connection, kept so
|
|
18
|
+
* the 'close' that follows can say what happened instead of arriving
|
|
19
|
+
* bare. Cleared by _reset().
|
|
20
|
+
*/
|
|
21
|
+
private _closeReason?;
|
|
16
22
|
private _socket?;
|
|
17
23
|
private _backend;
|
|
18
24
|
private _frontend;
|
|
@@ -282,6 +288,7 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
282
288
|
/** Accepts this server, or drops it and moves to the next candidate. */
|
|
283
289
|
protected _applySessionAttrs(wanted: string, standby: boolean, readOnly: boolean): void;
|
|
284
290
|
protected _handleClose(): void;
|
|
291
|
+
private _buildCloseReason;
|
|
285
292
|
protected _handleError(err: unknown): void;
|
|
286
293
|
/** Rejects and clears every still-open capture, e.g. on close/error. */
|
|
287
294
|
private _failPendingCaptures;
|
package/protocol/pg-socket.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ConnectionState } from '../constants.js';
|
|
|
8
8
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
9
9
|
import { Backend } from './backend.js';
|
|
10
10
|
import { signatureHashOfCertificate } from './cert-signature.js';
|
|
11
|
+
import { ConnectionLostError } from './connection-lost-error.js';
|
|
11
12
|
import { DatabaseError } from './database-error.js';
|
|
12
13
|
import { Frontend } from './frontend.js';
|
|
13
14
|
import { Protocol } from './protocol.js';
|
|
@@ -17,6 +18,12 @@ const COMMAND_RESULT_PATTERN = /^([^\d]+)(?: (\d+)(?: (\d+))?)?$/;
|
|
|
17
18
|
export class PgSocket extends SafeEventEmitter {
|
|
18
19
|
options;
|
|
19
20
|
_state = ConnectionState.CLOSED;
|
|
21
|
+
/**
|
|
22
|
+
* The socket-level error that is about to end this connection, kept so
|
|
23
|
+
* the 'close' that follows can say what happened instead of arriving
|
|
24
|
+
* bare. Cleared by _reset().
|
|
25
|
+
*/
|
|
26
|
+
_closeReason;
|
|
20
27
|
_socket;
|
|
21
28
|
_backend = new Backend();
|
|
22
29
|
_frontend;
|
|
@@ -532,6 +539,7 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
532
539
|
this._socket.removeAllListeners('close');
|
|
533
540
|
}
|
|
534
541
|
_reset() {
|
|
542
|
+
this._closeReason = undefined;
|
|
535
543
|
this._backend.reset();
|
|
536
544
|
this._sessionParameters = {};
|
|
537
545
|
this._processID = undefined;
|
|
@@ -627,14 +635,30 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
627
635
|
this._connectToHost();
|
|
628
636
|
}
|
|
629
637
|
_handleClose() {
|
|
638
|
+
// close() is the only thing that sets CLOSING, so anything else here
|
|
639
|
+
// is the far end going away on its own - an admin terminating the
|
|
640
|
+
// backend, a failover, a network fault. Built before _reset() clears
|
|
641
|
+
// the process id, which is the one detail that makes the report
|
|
642
|
+
// actionable, and carrying the socket error as its cause when there
|
|
643
|
+
// was one (a bare FIN leaves none).
|
|
644
|
+
const reason = this._state === ConnectionState.CLOSING
|
|
645
|
+
? undefined
|
|
646
|
+
: this._buildCloseReason();
|
|
630
647
|
this._failPendingCaptures(new Error('Connection closed'));
|
|
631
648
|
this._reset();
|
|
632
649
|
this._socket = undefined;
|
|
633
650
|
this._state = ConnectionState.CLOSED;
|
|
634
|
-
this.emit('close');
|
|
651
|
+
this.emit('close', reason);
|
|
652
|
+
}
|
|
653
|
+
_buildCloseReason() {
|
|
654
|
+
return new ConnectionLostError(this._processID, this._closeReason);
|
|
635
655
|
}
|
|
636
656
|
_handleError(err) {
|
|
637
|
-
|
|
657
|
+
// Kept for the 'close' that follows: the socket reports the cause
|
|
658
|
+
// here and the fact of closing separately, and only the pair together
|
|
659
|
+
// says what actually happened.
|
|
660
|
+
this._closeReason = err instanceof Error ? err : new Error(String(err));
|
|
661
|
+
this._failPendingCaptures(this._closeReason);
|
|
638
662
|
if (this._state !== ConnectionState.READY) {
|
|
639
663
|
this._socket?.end();
|
|
640
664
|
}
|
|
@@ -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
|
+
}
|
package/util/get-parsers.d.ts
CHANGED
|
@@ -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[];
|
package/util/get-parsers.js
CHANGED
|
@@ -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[];
|