postgrejs 3.5.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.
- package/CHANGELOG.md +26 -1
- package/README.md +17 -3
- 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 +106 -39
- package/connection/pool.js +28 -6
- 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/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/frontend.d.ts +7 -0
- package/protocol/frontend.js +5 -3
- 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
|
@@ -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[];
|
|
@@ -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
|
}
|