postgrejs 2.23.1 → 3.0.2
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 +171 -1
- package/LICENSE +26 -17
- package/README.md +195 -175
- package/connection/connection.d.ts +169 -16
- package/connection/connection.js +318 -56
- package/connection/copy-stream.d.ts +88 -0
- package/connection/copy-stream.js +244 -0
- package/connection/cursor.js +20 -13
- package/connection/intl-connection.d.ts +113 -5
- package/connection/intl-connection.js +500 -70
- package/connection/large-object.d.ts +65 -0
- package/connection/large-object.js +151 -0
- package/connection/logical-replication.d.ts +121 -0
- package/connection/logical-replication.js +279 -0
- package/connection/pool.d.ts +61 -4
- package/connection/pool.js +177 -29
- package/connection/portal.d.ts +18 -4
- package/connection/portal.js +53 -59
- package/connection/prepared-statement.d.ts +22 -1
- package/connection/prepared-statement.js +129 -109
- package/data-type-map.js +4 -2
- package/data-types/bool-type.js +14 -10
- package/data-types/box-type.js +15 -8
- package/data-types/bytea-type.js +9 -5
- package/data-types/char-type.js +11 -5
- package/data-types/circle-type.js +15 -7
- package/data-types/date-type.js +25 -19
- package/data-types/float4-type.js +10 -3
- package/data-types/float8-type.js +12 -3
- package/data-types/int2-type.js +9 -4
- package/data-types/int2-vector-type.js +9 -4
- package/data-types/int4-type.js +11 -5
- package/data-types/int8-type.js +22 -7
- package/data-types/json-type.js +17 -8
- package/data-types/jsonb-type.js +20 -14
- package/data-types/lseg-type.js +16 -8
- package/data-types/numeric-type.d.ts +8 -0
- package/data-types/numeric-type.js +118 -9
- package/data-types/oid-type.d.ts +0 -1
- package/data-types/oid-type.js +9 -10
- package/data-types/oid-vector-type.d.ts +9 -0
- package/data-types/oid-vector-type.js +51 -0
- package/data-types/point-type.js +14 -6
- package/data-types/time-type.js +20 -13
- package/data-types/timestamp-type.js +32 -25
- package/data-types/timestamptz-type.js +33 -21
- package/data-types/uuid-type.js +21 -11
- package/data-types/varchar-type.js +16 -4
- package/index.d.ts +5 -0
- package/index.js +5 -0
- package/interfaces/command-result.d.ts +2 -1
- package/interfaces/data-type.d.ts +5 -3
- package/interfaces/database-connection-params.d.ts +108 -0
- package/interfaces/query-options.d.ts +23 -0
- package/interfaces/script-execute-options.d.ts +23 -0
- package/interfaces/script-result.d.ts +3 -2
- package/package.json +2 -3
- package/protocol/backend.d.ts +6 -1
- package/protocol/backend.js +70 -45
- package/protocol/buffer-reader.d.ts +0 -2
- package/protocol/buffer-reader.js +3 -10
- package/protocol/cert-signature.d.ts +12 -0
- package/protocol/cert-signature.js +114 -0
- package/protocol/frontend.d.ts +16 -0
- package/protocol/frontend.js +70 -11
- package/protocol/pg-socket.d.ts +220 -9
- package/protocol/pg-socket.js +539 -102
- package/protocol/pgoutput.d.ts +76 -0
- package/protocol/pgoutput.js +155 -0
- package/protocol/protocol.d.ts +3 -4
- package/protocol/protocol.js +1 -1
- package/protocol/sasl.d.ts +11 -1
- package/protocol/sasl.js +36 -5
- package/protocol/smart-buffer.d.ts +0 -2
- package/protocol/smart-buffer.js +11 -16
- package/types.d.ts +5 -3
- package/util/abort-signal.d.ts +34 -0
- package/util/abort-signal.js +76 -0
- package/util/array-calculatedim.js +3 -1
- package/util/connection-config.js +77 -3
- package/util/decode-binaryarray.d.ts +1 -1
- package/util/decode-binaryarray.js +15 -6
- package/util/encode-binaryarray.js +7 -3
- package/util/escape-identifier.d.ts +14 -0
- package/util/escape-identifier.js +32 -0
- package/util/escape-literal.js +3 -0
- package/util/fast-parseint.d.ts +1 -0
- package/util/fast-parseint.js +12 -17
- package/util/format-datetime.d.ts +10 -0
- package/util/format-datetime.js +71 -0
- package/util/get-parsers.js +31 -10
- package/util/parse-array.js +2 -2
- package/util/parse-bytea.d.ts +2 -0
- package/util/parse-bytea.js +83 -0
- package/util/parse-datetime.d.ts +3 -1
- package/util/parse-datetime.js +86 -17
- package/util/parse-row.d.ts +3 -1
- package/util/parse-row.js +60 -5
- package/util/parse-time.js +8 -1
- package/util/sql-tag.d.ts +60 -0
- package/util/sql-tag.js +202 -0
- package/util/starts-transaction.d.ts +38 -0
- package/util/starts-transaction.js +148 -0
- package/util/stringify-arrayliteral.js +13 -1
- package/util/wrap-row-description.js +13 -4
- package/util/convert-row-to-object.d.ts +0 -2
- package/util/convert-row-to-object.js +0 -9
package/util/parse-row.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
|
|
2
|
+
import type { FieldInfo } from '../interfaces/field-info.js';
|
|
2
3
|
import type { AnyParseFunction } from '../types.js';
|
|
3
|
-
export declare function parseRow(parsers: AnyParseFunction[],
|
|
4
|
+
export declare function parseRow(parsers: AnyParseFunction[], data: Buffer, columnCount: number, options: DataMappingOptions): any[];
|
|
5
|
+
export declare function parseObjectRow(parsers: AnyParseFunction[], data: Buffer, columnCount: number, options: DataMappingOptions, fields: FieldInfo[]): object;
|
package/util/parse-row.js
CHANGED
|
@@ -1,8 +1,63 @@
|
|
|
1
|
-
export function parseRow(parsers,
|
|
2
|
-
const
|
|
1
|
+
export function parseRow(parsers, data, columnCount, options) {
|
|
2
|
+
const row = new Array(columnCount);
|
|
3
|
+
let offset = 0;
|
|
4
|
+
let len;
|
|
3
5
|
let i;
|
|
4
|
-
for (i = 0; i <
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
for (i = 0; i < columnCount; i++) {
|
|
7
|
+
// The length of the column value, in bytes (this count does not
|
|
8
|
+
// include itself). Can be zero. As a special case, -1 indicates a
|
|
9
|
+
// NULL column value - no value bytes follow in that case.
|
|
10
|
+
len = data.readInt32BE(offset);
|
|
11
|
+
offset += 4;
|
|
12
|
+
if (len < 0) {
|
|
13
|
+
row[i] = null;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// None of the parsers use `this`, so a direct call avoids .call()'s
|
|
17
|
+
// extra indirection (measured ~5.7x slower than invoking directly).
|
|
18
|
+
row[i] = parsers[i](data, offset, len, options);
|
|
19
|
+
offset += len;
|
|
20
|
+
}
|
|
7
21
|
}
|
|
22
|
+
return row;
|
|
23
|
+
}
|
|
24
|
+
export function parseObjectRow(parsers, data, columnCount, options, fields) {
|
|
25
|
+
const row = {};
|
|
26
|
+
let offset = 0;
|
|
27
|
+
let len;
|
|
28
|
+
let i;
|
|
29
|
+
let value;
|
|
30
|
+
for (i = 0; i < columnCount; i++) {
|
|
31
|
+
// The length of the column value, in bytes (this count does not
|
|
32
|
+
// include itself). Can be zero. As a special case, -1 indicates a
|
|
33
|
+
// NULL column value - no value bytes follow in that case.
|
|
34
|
+
len = data.readInt32BE(offset);
|
|
35
|
+
offset += 4;
|
|
36
|
+
if (len < 0) {
|
|
37
|
+
value = null;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// None of the parsers use `this`, so a direct call avoids .call()'s
|
|
41
|
+
// extra indirection (measured ~5.7x slower than invoking directly).
|
|
42
|
+
value = parsers[i](data, offset, len, options);
|
|
43
|
+
offset += len;
|
|
44
|
+
}
|
|
45
|
+
const name = fields[i].fieldName;
|
|
46
|
+
if (name === '__proto__') {
|
|
47
|
+
// `value`, not row[i] - nothing is ever assigned by numeric index
|
|
48
|
+
// here (row is a plain object keyed by field name), so reading row[i]
|
|
49
|
+
// handed defineProperty an undefined and silently dropped the
|
|
50
|
+
// column's actual value.
|
|
51
|
+
Object.defineProperty(row, name, {
|
|
52
|
+
value,
|
|
53
|
+
writable: true,
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
row[name] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return row;
|
|
8
63
|
}
|
package/util/parse-time.js
CHANGED
|
@@ -14,8 +14,15 @@ export function parseTime(str, parseTimeZone, utc) {
|
|
|
14
14
|
const s = m[i];
|
|
15
15
|
args[i + 2] = fastParseInt(s) || 0;
|
|
16
16
|
}
|
|
17
|
+
// Fractional seconds (m[4]) were never read here at all - pad/truncate to
|
|
18
|
+
// exactly 3 digits, same reasoning as parse-datetime.ts's equivalent fix.
|
|
19
|
+
if (m[4])
|
|
20
|
+
args[6] = fastParseInt((m[4] + '000').slice(0, 3));
|
|
17
21
|
if (parseTimeZone && m[6]) {
|
|
18
|
-
|
|
22
|
+
// m[6] is the sign group itself ('+'/'-') - there is no m[9] in this
|
|
23
|
+
// pattern (only 8 capture groups), so the old `m[9] === '-'` check was
|
|
24
|
+
// always false, silently treating every offset as positive.
|
|
25
|
+
const r = m[6] === '-' ? -1 : 1;
|
|
19
26
|
args[3] -= (fastParseInt(m[7]) || 0) * r;
|
|
20
27
|
args[4] -= (fastParseInt(m[8]) || 0) * r;
|
|
21
28
|
return new Date(Date.UTC(...args));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DataTypeMap } from '../data-type-map.js';
|
|
2
|
+
import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
|
|
3
|
+
/**
|
|
4
|
+
* A statement built by the `sql` tag: its text with the values pulled out.
|
|
5
|
+
*
|
|
6
|
+
* The same object serves both protocols, which is the point of building it
|
|
7
|
+
* rather than a string. query() takes `sql` and `params` and lets the server
|
|
8
|
+
* receive the values out of band, where they can never be read as SQL.
|
|
9
|
+
* execute() has no such option - the Simple Query protocol carries no
|
|
10
|
+
* parameters at all - so it takes stringify(), which writes the values into
|
|
11
|
+
* the statement as properly encoded literals.
|
|
12
|
+
*
|
|
13
|
+
* Those two are not equally strong. Parameters are safe by construction;
|
|
14
|
+
* literals are only as safe as the encoder, which is why stringify() refuses
|
|
15
|
+
* to guess (see below) instead of producing something that looks plausible.
|
|
16
|
+
*/
|
|
17
|
+
export declare class QueryRequest {
|
|
18
|
+
readonly sql: string;
|
|
19
|
+
readonly params: any[];
|
|
20
|
+
constructor(text: string, params: any[]);
|
|
21
|
+
/**
|
|
22
|
+
* The statement with its values written in as literals, for the Simple
|
|
23
|
+
* Query protocol.
|
|
24
|
+
*
|
|
25
|
+
* Every value is encoded by its own data type and given an explicit cast:
|
|
26
|
+
* a parameter carries its OID in the Bind message, but a literal carries
|
|
27
|
+
* nothing, so without the cast the server would infer a type from context
|
|
28
|
+
* and the same statement could mean different things through query() and
|
|
29
|
+
* execute(). A value whose type has no text encoding throws rather than
|
|
30
|
+
* falling back to a generic conversion.
|
|
31
|
+
*/
|
|
32
|
+
stringify(options?: DataMappingOptions & {
|
|
33
|
+
typeMap?: DataTypeMap;
|
|
34
|
+
}): string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds a QueryRequest from a template literal, turning every interpolated
|
|
38
|
+
* value into a bind parameter.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* await connection.query(sql`select * from t where id = any(${[1, 3]})`);
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* A nested QueryRequest is spliced in as a fragment with its parameters
|
|
45
|
+
* renumbered, so a statement can be assembled from pieces:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const filter = city ? sql`where city = ${city}` : sql``;
|
|
49
|
+
* await connection.query(sql`select * from t ${filter} order by id`);
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Interpolated values are never written into the text, so this is not string
|
|
53
|
+
* building with extra steps - there is no way for a value to be read as SQL.
|
|
54
|
+
*/
|
|
55
|
+
export declare function sql(strings: TemplateStringsArray, ...values: any[]): QueryRequest;
|
|
56
|
+
export declare namespace sql {
|
|
57
|
+
var ident: (name: string) => QueryRequest;
|
|
58
|
+
var values: (data: Record<string, any> | Record<string, any>[], columns?: string[]) => QueryRequest;
|
|
59
|
+
var set: (data: Record<string, any>, columns?: string[]) => QueryRequest;
|
|
60
|
+
}
|
package/util/sql-tag.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { BindParam } from '../connection/bind-param.js';
|
|
2
|
+
import { DataTypeMap, GlobalTypeMap } from '../data-type-map.js';
|
|
3
|
+
import { escapeIdentifier } from './escape-identifier.js';
|
|
4
|
+
import { escapeLiteral } from './escape-literal.js';
|
|
5
|
+
/**
|
|
6
|
+
* A statement built by the `sql` tag: its text with the values pulled out.
|
|
7
|
+
*
|
|
8
|
+
* The same object serves both protocols, which is the point of building it
|
|
9
|
+
* rather than a string. query() takes `sql` and `params` and lets the server
|
|
10
|
+
* receive the values out of band, where they can never be read as SQL.
|
|
11
|
+
* execute() has no such option - the Simple Query protocol carries no
|
|
12
|
+
* parameters at all - so it takes stringify(), which writes the values into
|
|
13
|
+
* the statement as properly encoded literals.
|
|
14
|
+
*
|
|
15
|
+
* Those two are not equally strong. Parameters are safe by construction;
|
|
16
|
+
* literals are only as safe as the encoder, which is why stringify() refuses
|
|
17
|
+
* to guess (see below) instead of producing something that looks plausible.
|
|
18
|
+
*/
|
|
19
|
+
export class QueryRequest {
|
|
20
|
+
sql;
|
|
21
|
+
params;
|
|
22
|
+
constructor(text, params) {
|
|
23
|
+
this.sql = text;
|
|
24
|
+
this.params = params;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The statement with its values written in as literals, for the Simple
|
|
28
|
+
* Query protocol.
|
|
29
|
+
*
|
|
30
|
+
* Every value is encoded by its own data type and given an explicit cast:
|
|
31
|
+
* a parameter carries its OID in the Bind message, but a literal carries
|
|
32
|
+
* nothing, so without the cast the server would infer a type from context
|
|
33
|
+
* and the same statement could mean different things through query() and
|
|
34
|
+
* execute(). A value whose type has no text encoding throws rather than
|
|
35
|
+
* falling back to a generic conversion.
|
|
36
|
+
*/
|
|
37
|
+
stringify(options) {
|
|
38
|
+
const typeMap = options?.typeMap || GlobalTypeMap;
|
|
39
|
+
const params = this.params;
|
|
40
|
+
if (!params.length)
|
|
41
|
+
return this.sql;
|
|
42
|
+
// $1..$n, longest first so $10 is not matched as $1 followed by "0".
|
|
43
|
+
return this.sql.replace(/\$(\d+)/g, (match, n) => {
|
|
44
|
+
const i = +n - 1;
|
|
45
|
+
if (i < 0 || i >= params.length)
|
|
46
|
+
return match;
|
|
47
|
+
return encodeLiteral(params[i], typeMap, options);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Builds a QueryRequest from a template literal, turning every interpolated
|
|
53
|
+
* value into a bind parameter.
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* await connection.query(sql`select * from t where id = any(${[1, 3]})`);
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* A nested QueryRequest is spliced in as a fragment with its parameters
|
|
60
|
+
* renumbered, so a statement can be assembled from pieces:
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* const filter = city ? sql`where city = ${city}` : sql``;
|
|
64
|
+
* await connection.query(sql`select * from t ${filter} order by id`);
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* Interpolated values are never written into the text, so this is not string
|
|
68
|
+
* building with extra steps - there is no way for a value to be read as SQL.
|
|
69
|
+
*/
|
|
70
|
+
export function sql(strings, ...values) {
|
|
71
|
+
const params = [];
|
|
72
|
+
let text = '';
|
|
73
|
+
const l = strings.length;
|
|
74
|
+
let i;
|
|
75
|
+
for (i = 0; i < l; i++) {
|
|
76
|
+
text += strings[i];
|
|
77
|
+
if (i >= values.length)
|
|
78
|
+
continue;
|
|
79
|
+
const v = values[i];
|
|
80
|
+
if (v instanceof QueryRequest) {
|
|
81
|
+
// Renumber the fragment's placeholders onto this statement's params.
|
|
82
|
+
const offset = params.length;
|
|
83
|
+
text += v.sql.replace(/\$(\d+)/g, (_m, n) => '$' + (+n + offset));
|
|
84
|
+
params.push(...v.params);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
params.push(v);
|
|
88
|
+
text += '$' + params.length;
|
|
89
|
+
}
|
|
90
|
+
return new QueryRequest(text, params);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Quotes a table, column or schema name for use inside a `sql` template.
|
|
94
|
+
*
|
|
95
|
+
* Values become parameters, but names cannot: `$1` is always a value, so
|
|
96
|
+
* `sql`select ${col} from t`` selects the string, not the column. This
|
|
97
|
+
* writes the name into the statement instead, quoted the way PostgreSQL's
|
|
98
|
+
* own `quote_ident()` does.
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* await connection.query(sql`select ${sql.ident(col)} from ${sql.ident(table)}`);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
sql.ident = function ident(name) {
|
|
105
|
+
return new QueryRequest(escapeIdentifier(name), []);
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Builds the column list and VALUES clause of an INSERT from an object, or
|
|
109
|
+
* from an array of objects for a multi-row insert.
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* await connection.query(sql`insert into users ${sql.values(user)}`);
|
|
113
|
+
* // insert into users ("id","name") values ($1,$2)
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* `columns` restricts and orders what is written. Pass it whenever the
|
|
117
|
+
* object comes from outside - a request body, say. Without it the columns
|
|
118
|
+
* are whatever keys the object happens to carry, so a caller could add one
|
|
119
|
+
* you did not intend to accept. The values are always parameters, so this
|
|
120
|
+
* is not an SQL injection either way; it is the column list that needs
|
|
121
|
+
* deciding by you rather than by the input.
|
|
122
|
+
*/
|
|
123
|
+
sql.values = function values(data, columns) {
|
|
124
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
125
|
+
if (!rows.length)
|
|
126
|
+
throw new TypeError('sql.values() needs at least one row');
|
|
127
|
+
const cols = columns ?? Object.keys(rows[0]);
|
|
128
|
+
if (!cols.length)
|
|
129
|
+
throw new TypeError('sql.values() needs at least one column');
|
|
130
|
+
const params = [];
|
|
131
|
+
const tuples = rows.map(row => {
|
|
132
|
+
const placeholders = cols.map(c => {
|
|
133
|
+
params.push(row[c]);
|
|
134
|
+
return '$' + params.length;
|
|
135
|
+
});
|
|
136
|
+
return `(${placeholders.join(',')})`;
|
|
137
|
+
});
|
|
138
|
+
const names = cols.map(escapeIdentifier).join(',');
|
|
139
|
+
return new QueryRequest(`(${names}) values ${tuples.join(',')}`, params);
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Builds the assignment list of an UPDATE from an object.
|
|
143
|
+
*
|
|
144
|
+
* ```ts
|
|
145
|
+
* await connection.query(
|
|
146
|
+
* sql`update users set ${sql.set({ city })} where id = ${id}`,
|
|
147
|
+
* );
|
|
148
|
+
* // update users set "city" = $1 where id = $2
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* `columns` restricts what is written, for the same reason as in
|
|
152
|
+
* sql.values().
|
|
153
|
+
*/
|
|
154
|
+
sql.set = function set(data, columns) {
|
|
155
|
+
const cols = columns ?? Object.keys(data);
|
|
156
|
+
if (!cols.length)
|
|
157
|
+
throw new TypeError('sql.set() needs at least one column');
|
|
158
|
+
const params = [];
|
|
159
|
+
const assignments = cols.map(c => {
|
|
160
|
+
params.push(data[c]);
|
|
161
|
+
return `${escapeIdentifier(c)} = $${params.length}`;
|
|
162
|
+
});
|
|
163
|
+
return new QueryRequest(assignments.join(', '), params);
|
|
164
|
+
};
|
|
165
|
+
function encodeLiteral(value, typeMap, options) {
|
|
166
|
+
let v = value;
|
|
167
|
+
let oid;
|
|
168
|
+
if (v instanceof BindParam) {
|
|
169
|
+
oid = v.oid;
|
|
170
|
+
v = v.value;
|
|
171
|
+
}
|
|
172
|
+
if (v == null)
|
|
173
|
+
return 'null';
|
|
174
|
+
oid = oid ?? typeMap.determine(v);
|
|
175
|
+
/* c8 ignore start - typeMap.determine() always falls back to the
|
|
176
|
+
"unknown" oid rather than returning null/undefined, so this can't
|
|
177
|
+
currently be reached through any public call path. */
|
|
178
|
+
if (oid == null)
|
|
179
|
+
throw new TypeError(`Cannot write ${describe(v)} into a statement as a literal: no data type matches it. ` +
|
|
180
|
+
`Use query() instead, which sends it as a parameter.`);
|
|
181
|
+
/* c8 ignore stop */
|
|
182
|
+
const dataType = typeMap.get(oid);
|
|
183
|
+
if (!dataType?.encodeText)
|
|
184
|
+
throw new TypeError(`Cannot write ${describe(v)} into a statement as a literal: ` +
|
|
185
|
+
`data type "${dataType?.name ?? oid}" has no text encoding. ` +
|
|
186
|
+
`Use query() instead, which sends it as a parameter.`);
|
|
187
|
+
const cast = dataType.name;
|
|
188
|
+
if (dataType.elementsOID) {
|
|
189
|
+
const elementType = typeMap.get(dataType.elementsOID);
|
|
190
|
+
const arr = (Array.isArray(v) ? v : [v]).map(x => x == null
|
|
191
|
+
? 'null'
|
|
192
|
+
: escapeLiteral(elementType.encodeText(x, options || {})));
|
|
193
|
+
return `ARRAY[${arr.join(',')}]::${cast}`;
|
|
194
|
+
}
|
|
195
|
+
return `${escapeLiteral(dataType.encodeText(v, options || {}))}::${cast}`;
|
|
196
|
+
}
|
|
197
|
+
function describe(v) {
|
|
198
|
+
const t = typeof v;
|
|
199
|
+
if (t === 'object')
|
|
200
|
+
return v.constructor?.name || 'object';
|
|
201
|
+
return t;
|
|
202
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this SQL open a transaction?
|
|
3
|
+
*
|
|
4
|
+
* Pool.query()/Pool.execute() may share one connection between several
|
|
5
|
+
* in-flight queries, which is only safe while none of them opens a
|
|
6
|
+
* transaction: the others are already dispatched on that connection by the
|
|
7
|
+
* time a BEGIN takes effect, so they would silently join it. Knowing
|
|
8
|
+
* BEFORE dispatch is what makes the difference between routing the
|
|
9
|
+
* statement to a connection of its own - where it simply works - and
|
|
10
|
+
* finding out afterwards, when the damage is done and all that is left is
|
|
11
|
+
* to report it.
|
|
12
|
+
*
|
|
13
|
+
* Two passes: a cheap regex rejects the overwhelming majority, and only a
|
|
14
|
+
* hit pays for the accurate scan below, which skips comments and quoted
|
|
15
|
+
* text so `select 'begin'`, `-- begin`, and a `DO $$ BEGIN ... END $$`
|
|
16
|
+
* block (PL/pgSQL, not a transaction) are not mistaken for one. It looks
|
|
17
|
+
* ANYWHERE rather than only at the start, since execute() runs
|
|
18
|
+
* multi-statement scripts where the BEGIN can be the second statement.
|
|
19
|
+
*
|
|
20
|
+
* What it cannot see is a transaction opened inside a called procedure -
|
|
21
|
+
* no reading of the SQL can reveal that. Pool keeps a second, authoritative
|
|
22
|
+
* check on the server's own transaction status for exactly that case.
|
|
23
|
+
*/
|
|
24
|
+
export declare function startsTransaction(sql: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Reports whether `sql` contains a COPY statement.
|
|
27
|
+
*
|
|
28
|
+
* Pooled pipelining has to refuse these outright: a COPY puts the server
|
|
29
|
+
* into copy-in/copy-out mode, where the only messages it will accept are
|
|
30
|
+
* CopyData/CopyDone/CopyFail - the next pipelined query's Query message
|
|
31
|
+
* arriving mid-copy is a protocol error that takes the whole shared
|
|
32
|
+
* connection down with it, not just that one caller.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately blunt: it matches the word anywhere outside comments and
|
|
35
|
+
* quoted text, so a column actually named "copy" costs its query a
|
|
36
|
+
* dedicated connection. That is the harmless direction to be wrong in.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startsCopy(sql: string): boolean;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Cheap first pass. Almost no statement contains either word, so almost
|
|
2
|
+
// every pooled query is decided by this one scan and never reaches the
|
|
3
|
+
// accurate (and more expensive) pass below. It over-matches on purpose -
|
|
4
|
+
// comments, string literals and PL/pgSQL blocks all get through here.
|
|
5
|
+
const MAYBE_TRANSACTION = /\b(?:begin|start)\b/i;
|
|
6
|
+
const MAYBE_COPY = /\bcopy\b/i;
|
|
7
|
+
function isWordChar(code) {
|
|
8
|
+
return ((code >= 48 && code <= 57) || // 0-9
|
|
9
|
+
(code >= 65 && code <= 90) || // A-Z
|
|
10
|
+
(code >= 97 && code <= 122) || // a-z
|
|
11
|
+
code === 95 || // _
|
|
12
|
+
code === 36 // $
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
/** Case-insensitive whole-word match of `word` at `at`. */
|
|
16
|
+
function matchesWord(sql, at, word) {
|
|
17
|
+
if (at + word.length > sql.length)
|
|
18
|
+
return false;
|
|
19
|
+
for (let i = 0; i < word.length; i++) {
|
|
20
|
+
if ((sql.charCodeAt(at + i) | 32) !== word.charCodeAt(i))
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (at > 0 && isWordChar(sql.charCodeAt(at - 1)))
|
|
24
|
+
return false;
|
|
25
|
+
const after = at + word.length;
|
|
26
|
+
return after >= sql.length || !isWordChar(sql.charCodeAt(after));
|
|
27
|
+
}
|
|
28
|
+
/** Skips whitespace, then reports whether `word` follows. */
|
|
29
|
+
function nextWordIs(sql, from, word) {
|
|
30
|
+
let i = from;
|
|
31
|
+
while (i < sql.length && /\s/.test(sql[i]))
|
|
32
|
+
i++;
|
|
33
|
+
return matchesWord(sql, i, word);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Walks `sql` calling `at(i)` at every position that is real code, skipping
|
|
37
|
+
* line and (nestable) block comments, single- and double-quoted text and
|
|
38
|
+
* dollar-quoted bodies. Reports whether `at` ever returned true, stopping
|
|
39
|
+
* at the first one that does.
|
|
40
|
+
*/
|
|
41
|
+
function scanCode(sql, at) {
|
|
42
|
+
const n = sql.length;
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < n) {
|
|
45
|
+
const c = sql[i];
|
|
46
|
+
if (c === '-' && sql[i + 1] === '-') {
|
|
47
|
+
const nl = sql.indexOf('\n', i + 2);
|
|
48
|
+
i = nl === -1 ? n : nl + 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === '/' && sql[i + 1] === '*') {
|
|
52
|
+
// Block comments nest in PostgreSQL, unlike in most SQL dialects.
|
|
53
|
+
let depth = 1;
|
|
54
|
+
i += 2;
|
|
55
|
+
while (i < n && depth > 0) {
|
|
56
|
+
if (sql[i] === '/' && sql[i + 1] === '*') {
|
|
57
|
+
depth++;
|
|
58
|
+
i += 2;
|
|
59
|
+
}
|
|
60
|
+
else if (sql[i] === '*' && sql[i + 1] === '/') {
|
|
61
|
+
depth--;
|
|
62
|
+
i += 2;
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// '...' literal or "..." identifier; a doubled quote escapes itself.
|
|
70
|
+
if (c === "'" || c === '"') {
|
|
71
|
+
i++;
|
|
72
|
+
while (i < n) {
|
|
73
|
+
if (sql[i] === c) {
|
|
74
|
+
if (sql[i + 1] === c)
|
|
75
|
+
i += 2;
|
|
76
|
+
else {
|
|
77
|
+
i++;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// $tag$ ... $tag$ - function bodies live in these, and the BEGIN of a
|
|
87
|
+
// PL/pgSQL block is not a transaction.
|
|
88
|
+
if (c === '$') {
|
|
89
|
+
const tag = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql.slice(i))?.[0];
|
|
90
|
+
if (tag) {
|
|
91
|
+
const end = sql.indexOf(tag, i + tag.length);
|
|
92
|
+
i = end === -1 ? n : end + tag.length;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (at(i))
|
|
97
|
+
return true;
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Does this SQL open a transaction?
|
|
104
|
+
*
|
|
105
|
+
* Pool.query()/Pool.execute() may share one connection between several
|
|
106
|
+
* in-flight queries, which is only safe while none of them opens a
|
|
107
|
+
* transaction: the others are already dispatched on that connection by the
|
|
108
|
+
* time a BEGIN takes effect, so they would silently join it. Knowing
|
|
109
|
+
* BEFORE dispatch is what makes the difference between routing the
|
|
110
|
+
* statement to a connection of its own - where it simply works - and
|
|
111
|
+
* finding out afterwards, when the damage is done and all that is left is
|
|
112
|
+
* to report it.
|
|
113
|
+
*
|
|
114
|
+
* Two passes: a cheap regex rejects the overwhelming majority, and only a
|
|
115
|
+
* hit pays for the accurate scan below, which skips comments and quoted
|
|
116
|
+
* text so `select 'begin'`, `-- begin`, and a `DO $$ BEGIN ... END $$`
|
|
117
|
+
* block (PL/pgSQL, not a transaction) are not mistaken for one. It looks
|
|
118
|
+
* ANYWHERE rather than only at the start, since execute() runs
|
|
119
|
+
* multi-statement scripts where the BEGIN can be the second statement.
|
|
120
|
+
*
|
|
121
|
+
* What it cannot see is a transaction opened inside a called procedure -
|
|
122
|
+
* no reading of the SQL can reveal that. Pool keeps a second, authoritative
|
|
123
|
+
* check on the server's own transaction status for exactly that case.
|
|
124
|
+
*/
|
|
125
|
+
export function startsTransaction(sql) {
|
|
126
|
+
if (!MAYBE_TRANSACTION.test(sql))
|
|
127
|
+
return false;
|
|
128
|
+
return scanCode(sql, i => matchesWord(sql, i, 'begin') ||
|
|
129
|
+
(matchesWord(sql, i, 'start') && nextWordIs(sql, i + 5, 'transaction')));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Reports whether `sql` contains a COPY statement.
|
|
133
|
+
*
|
|
134
|
+
* Pooled pipelining has to refuse these outright: a COPY puts the server
|
|
135
|
+
* into copy-in/copy-out mode, where the only messages it will accept are
|
|
136
|
+
* CopyData/CopyDone/CopyFail - the next pipelined query's Query message
|
|
137
|
+
* arriving mid-copy is a protocol error that takes the whole shared
|
|
138
|
+
* connection down with it, not just that one caller.
|
|
139
|
+
*
|
|
140
|
+
* Deliberately blunt: it matches the word anywhere outside comments and
|
|
141
|
+
* quoted text, so a column actually named "copy" costs its query a
|
|
142
|
+
* dedicated connection. That is the harmless direction to be wrong in.
|
|
143
|
+
*/
|
|
144
|
+
export function startsCopy(sql) {
|
|
145
|
+
if (!MAYBE_COPY.test(sql))
|
|
146
|
+
return false;
|
|
147
|
+
return scanCode(sql, i => matchesWord(sql, i, 'copy'));
|
|
148
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { arrayCalculateDim } from './array-calculatedim.js';
|
|
2
2
|
export function stringifyArrayLiteral(value, options, encode) {
|
|
3
3
|
const dim = arrayCalculateDim(value);
|
|
4
|
+
// `dim` is built once above and never mutated, so its last index is
|
|
5
|
+
// invariant across the whole recursion - not just this loop. Reading
|
|
6
|
+
// dim.length (a property load) and subtracting on every element of every
|
|
7
|
+
// level was the only per-element work here that didn't depend on the
|
|
8
|
+
// element.
|
|
9
|
+
const lastLevel = dim.length - 1;
|
|
4
10
|
const writeDim = (arr, level) => {
|
|
5
11
|
const elemCount = dim[level];
|
|
6
12
|
const out = [];
|
|
13
|
+
const isLeafLevel = level >= lastLevel;
|
|
7
14
|
for (let i = 0; i < elemCount; i++) {
|
|
8
15
|
let x = arr && arr[i];
|
|
9
|
-
if (
|
|
16
|
+
if (!isLeafLevel) {
|
|
10
17
|
if (x != null && !Array.isArray(x))
|
|
11
18
|
x = [x];
|
|
12
19
|
out.push(writeDim(x, level + 1));
|
|
@@ -17,10 +24,15 @@ export function stringifyArrayLiteral(value, options, encode) {
|
|
|
17
24
|
out.push('NULL');
|
|
18
25
|
continue;
|
|
19
26
|
}
|
|
27
|
+
/* c8 ignore start - dim is (re)computed above from a DFS that visits
|
|
28
|
+
every array node in `value` and deepens dim to match, so a value
|
|
29
|
+
can never actually be an array once `level` reaches the leaf -
|
|
30
|
+
if it were, dim would already have gone one level deeper there. */
|
|
20
31
|
if (Array.isArray(x)) {
|
|
21
32
|
out.push(stringifyArrayLiteral(x, options, encode));
|
|
22
33
|
continue;
|
|
23
34
|
}
|
|
35
|
+
/* c8 ignore stop */
|
|
24
36
|
if (encode)
|
|
25
37
|
x = encode(x, options || {});
|
|
26
38
|
out.push(escapeArrayItem('' + x));
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { DataFormat, DataTypeNames } from '../constants.js';
|
|
2
|
+
// DataTypeNames (oid -> name) is a static, never-mutated map, so its
|
|
3
|
+
// reverse (name -> oid) can be precomputed once here instead of rebuilding
|
|
4
|
+
// Object.keys(DataTypeNames) and linearly rescanning it (with no early
|
|
5
|
+
// exit) for every array-typed column in every query. Duplicate names would
|
|
6
|
+
// resolve to the same "last one wins" oid either way, matching the old
|
|
7
|
+
// loop's behavior exactly.
|
|
8
|
+
const DataTypeOIDByName = {};
|
|
9
|
+
for (const oid of Object.keys(DataTypeNames)) {
|
|
10
|
+
DataTypeOIDByName[DataTypeNames[oid]] = Number(oid);
|
|
11
|
+
}
|
|
2
12
|
export function wrapRowDescription(typeMap, fields, columnFormat) {
|
|
3
13
|
return fields.map((f, idx) => {
|
|
4
14
|
const cf = Array.isArray(columnFormat) ? columnFormat[idx] : columnFormat;
|
|
@@ -13,10 +23,9 @@ export function wrapRowDescription(typeMap, fields, columnFormat) {
|
|
|
13
23
|
x.isArray = x.dataTypeName.startsWith('_');
|
|
14
24
|
if (x.isArray) {
|
|
15
25
|
x.elementDataTypeName = x.dataTypeName.substring(1);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
26
|
+
const elementOid = DataTypeOIDByName[x.elementDataTypeName];
|
|
27
|
+
if (elementOid !== undefined)
|
|
28
|
+
x.elementDataTypeId = elementOid;
|
|
20
29
|
}
|
|
21
30
|
if (f.fixedSize && f.fixedSize > 0)
|
|
22
31
|
x.fixedSize = f.fixedSize;
|