@uniflowed/sql 0.35.0 → 0.37.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/adapters/pg.js +115 -4
- package/adapters/pglite.js +24 -1
- package/adapters/postgres.js +94 -3
- package/index.js +143 -7
- package/package.json +1 -1
package/adapters/pg.js
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
// with the identity for that query alone, so the pool's own configuration is
|
|
12
12
|
// left as the application set it, and generated code still sees the server's
|
|
13
13
|
// text. A transaction checks a client out of the pool for its duration.
|
|
14
|
+
// `:copyfrom` is a `COPY FROM STDIN`: node-postgres runs it when the query
|
|
15
|
+
// object supplies `submit` and `handleCopyInResponse`, which is the protocol
|
|
16
|
+
// hook it already has and does not implement for a string query.
|
|
14
17
|
|
|
15
|
-
import type { Connection, QueryResult, Queryable, Run } from "../index.js";
|
|
18
|
+
import type { Connection, CopyIn, QueryResult, Queryable, Run } from "../index.js";
|
|
16
19
|
import { singleConnection, transactionOn } from "../index.js";
|
|
17
20
|
|
|
18
21
|
type PgResult = { readonly rows: $ReadOnlyArray<mixed>, readonly rowCount: number | null, ... };
|
|
@@ -24,9 +27,32 @@ type PgQuery = {|
|
|
|
24
27
|
readonly types : {| readonly getTypeParser: () => (value: string) => string |},
|
|
25
28
|
|};
|
|
26
29
|
|
|
30
|
+
/** The connection methods a `COPY FROM STDIN` uses. `pg` passes its own. */
|
|
31
|
+
type PgCopyConnection = {
|
|
32
|
+
query(text: string): mixed,
|
|
33
|
+
sendCopyFromChunk(chunk: Uint8Array): mixed,
|
|
34
|
+
endCopyFrom(): mixed,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** A query object `pg` runs when `submit` is a function. */
|
|
38
|
+
type PgCopyRequest = {
|
|
39
|
+
text : string,
|
|
40
|
+
callback: ?(?mixed, ?PgResult) => mixed,
|
|
41
|
+
submit(connection: PgCopyConnection): null,
|
|
42
|
+
handleCopyInResponse(connection: PgCopyConnection): void,
|
|
43
|
+
handleCommandComplete(message: { text?: string, ... }): void,
|
|
44
|
+
handleReadyForQuery(): void,
|
|
45
|
+
handleError(error: mixed): void,
|
|
46
|
+
handleCopyData(): void,
|
|
47
|
+
handleRowDescription(): void,
|
|
48
|
+
handleDataRow(): void,
|
|
49
|
+
handleEmptyQuery(): void,
|
|
50
|
+
handlePortalSuspended(): void,
|
|
51
|
+
};
|
|
52
|
+
|
|
27
53
|
/** A connected `pg.Client` or a client checked out of a pool. */
|
|
28
54
|
export interface PgClient {
|
|
29
|
-
query(config: PgQuery): Promise<PgResult
|
|
55
|
+
query(config: PgQuery | PgCopyRequest): Promise<PgResult> | void;
|
|
30
56
|
}
|
|
31
57
|
|
|
32
58
|
/** A client checked out of a `pg.Pool`. */
|
|
@@ -36,15 +62,99 @@ export interface PgPoolClient extends PgClient {
|
|
|
36
62
|
|
|
37
63
|
/** A `pg.Pool`. */
|
|
38
64
|
export interface PgPool {
|
|
39
|
-
query(config: PgQuery): Promise<PgResult>;
|
|
65
|
+
query(config: PgQuery | PgCopyRequest): Promise<PgResult>;
|
|
40
66
|
connect(): Promise<PgPoolClient>;
|
|
41
67
|
}
|
|
42
68
|
|
|
69
|
+
const COPY_ROWS = /COPY (\d+)/;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One `COPY FROM STDIN` on `runner`.
|
|
73
|
+
*
|
|
74
|
+
* The callback is left unset when `runner` is a pool: the pool sets it, and
|
|
75
|
+
* setting it here would skip the pool's own, which is what releases the
|
|
76
|
+
* client. A client sets it in the wrapper below, because `client.query` only
|
|
77
|
+
* returns a promise when the object does not already have one and nobody
|
|
78
|
+
* passed a callback.
|
|
79
|
+
*/
|
|
80
|
+
function copyRequest(statement: string, payload: string): PgCopyRequest {
|
|
81
|
+
let rowCount = 0;
|
|
82
|
+
let settled = false;
|
|
83
|
+
const request: PgCopyRequest = {
|
|
84
|
+
text : statement,
|
|
85
|
+
callback: undefined,
|
|
86
|
+
submit(connection) {
|
|
87
|
+
connection.query(statement);
|
|
88
|
+
return null;
|
|
89
|
+
},
|
|
90
|
+
handleCopyInResponse(connection) {
|
|
91
|
+
// pg-protocol copies the chunk with `Buffer#copy`. A `Uint8Array` has
|
|
92
|
+
// no `copy`, and the throw leaves the backend waiting for `CopyData`.
|
|
93
|
+
connection.sendCopyFromChunk(Buffer.from(payload, "utf8"));
|
|
94
|
+
connection.endCopyFrom();
|
|
95
|
+
},
|
|
96
|
+
handleCommandComplete(message) {
|
|
97
|
+
const text = typeof message.text === "string" ? message.text : "";
|
|
98
|
+
const match = COPY_ROWS.exec(text);
|
|
99
|
+
if (match?.[1] !== undefined) {
|
|
100
|
+
rowCount = Number(match[1]);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
handleReadyForQuery() {
|
|
104
|
+
if (settled) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
settled = true;
|
|
108
|
+
request.callback?.(null, { rows: [], rowCount });
|
|
109
|
+
},
|
|
110
|
+
handleError(error) {
|
|
111
|
+
if (settled) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
settled = true;
|
|
115
|
+
request.callback?.(error, undefined);
|
|
116
|
+
},
|
|
117
|
+
handleCopyData() {},
|
|
118
|
+
handleRowDescription() {},
|
|
119
|
+
handleDataRow() {},
|
|
120
|
+
handleEmptyQuery() {},
|
|
121
|
+
handlePortalSuspended() {},
|
|
122
|
+
};
|
|
123
|
+
return request;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function copyOn(runner: PgClient): CopyIn {
|
|
127
|
+
return (statement, payload) =>
|
|
128
|
+
new Promise((resolve, reject) => {
|
|
129
|
+
const request = copyRequest(statement, payload);
|
|
130
|
+
request.callback = (error, result) => {
|
|
131
|
+
if (error != null) {
|
|
132
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
resolve(result?.rowCount ?? 0);
|
|
136
|
+
};
|
|
137
|
+
runner.query(request);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function copyOnPool(pool: PgPool): CopyIn {
|
|
142
|
+
return async (statement, payload) => {
|
|
143
|
+
const result = await pool.query(copyRequest(statement, payload));
|
|
144
|
+
return result.rowCount ?? 0;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
43
148
|
const RAW = { getTypeParser: () => (value: string) => value };
|
|
44
149
|
|
|
45
150
|
function runOn(client: PgClient): Run {
|
|
46
151
|
return async (text, params): Promise<QueryResult> => {
|
|
47
152
|
const result = await client.query({ text, values: params, rowMode: "array", types: RAW });
|
|
153
|
+
// A `COPY` request returns void. This call is a normal query, which
|
|
154
|
+
// returns a result; the union is what `query` is declared as.
|
|
155
|
+
if (result == null) {
|
|
156
|
+
throw new Error("pg query returned no result");
|
|
157
|
+
}
|
|
48
158
|
const rows: Array<$ReadOnlyArray<mixed>> = [];
|
|
49
159
|
for (const row of result.rows) {
|
|
50
160
|
if (Array.isArray(row)) {
|
|
@@ -56,7 +166,7 @@ function runOn(client: PgClient): Run {
|
|
|
56
166
|
}
|
|
57
167
|
|
|
58
168
|
function connection(client: PgClient): Connection {
|
|
59
|
-
return { engine: "postgresql", maxParams: 65535, run: runOn(client) };
|
|
169
|
+
return { engine: "postgresql", maxParams: 65535, run: runOn(client), copy: copyOn(client) };
|
|
60
170
|
}
|
|
61
171
|
|
|
62
172
|
/** A [`Queryable`] over one connected `pg.Client`, which it serialises. */
|
|
@@ -70,6 +180,7 @@ export function fromPgPool(pool: PgPool): Queryable {
|
|
|
70
180
|
engine : "postgresql",
|
|
71
181
|
maxParams: 65535,
|
|
72
182
|
query : runOn(pool),
|
|
183
|
+
copy : copyOnPool(pool),
|
|
73
184
|
transaction: async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
|
|
74
185
|
const client = await pool.connect();
|
|
75
186
|
let broken: mixed = undefined;
|
package/adapters/pglite.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// PGlite parses and serialises values itself, by type. This adapter turns both
|
|
11
11
|
// off — an identity parser and serialiser for every type it knows — so rows
|
|
12
12
|
// arrive as the server's text and parameters leave as the codecs wrote them,
|
|
13
|
-
// exactly as with every other PostgreSQL adapter.
|
|
13
|
+
// exactly as with every other PostgreSQL adapter. `:copyfrom` uses PGlite's
|
|
14
|
+
// `/dev/blob` device, which is how its `COPY FROM` reads a payload.
|
|
14
15
|
|
|
15
16
|
import type { Queryable, SqlParam } from "../index.js";
|
|
16
17
|
import { singleConnection } from "../index.js";
|
|
@@ -24,6 +25,7 @@ export interface PGliteDatabase {
|
|
|
24
25
|
readonly rowMode : "array",
|
|
25
26
|
readonly parsers : { readonly [string]: (value: string) => mixed },
|
|
26
27
|
readonly serializers: { readonly [string]: (value: mixed) => mixed },
|
|
28
|
+
readonly blob? : Blob,
|
|
27
29
|
|},
|
|
28
30
|
): Promise<{ readonly rows: $ReadOnlyArray<mixed>, readonly affectedRows?: number, ... }>;
|
|
29
31
|
readonly parsers : { readonly [string]: mixed };
|
|
@@ -52,6 +54,27 @@ export function fromPGlite(database: PGliteDatabase): Queryable {
|
|
|
52
54
|
return singleConnection({
|
|
53
55
|
engine : "postgresql",
|
|
54
56
|
maxParams: 65535,
|
|
57
|
+
copy: async (statement, payload) => {
|
|
58
|
+
// `COPY … FROM STDIN` is the wire form. PGlite reads the payload from
|
|
59
|
+
// the virtual file named here instead of a frontend COPY stream.
|
|
60
|
+
const text = statement.replace(" FROM STDIN", " FROM '/dev/blob'");
|
|
61
|
+
const result = await database.query(text, [], {
|
|
62
|
+
rowMode : "array",
|
|
63
|
+
parsers : {},
|
|
64
|
+
serializers: {},
|
|
65
|
+
blob : new Blob([payload]),
|
|
66
|
+
});
|
|
67
|
+
if (typeof result.affectedRows === "number") {
|
|
68
|
+
return result.affectedRows;
|
|
69
|
+
}
|
|
70
|
+
let rows = 0;
|
|
71
|
+
for (let i = 0; i < payload.length; i += 1) {
|
|
72
|
+
if (payload[i] === "\n") {
|
|
73
|
+
rows += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return rows;
|
|
77
|
+
},
|
|
55
78
|
run: async (text, params) => {
|
|
56
79
|
const size = Object.keys(database.parsers).length + Object.keys(database.serializers).length;
|
|
57
80
|
if (size !== known) {
|
package/adapters/postgres.js
CHANGED
|
@@ -18,14 +18,30 @@
|
|
|
18
18
|
// `bytea`, `x === true ? "t" : "f"` for `bool` — each of which would corrupt a
|
|
19
19
|
// value the codecs already wrote as text. Declaring every parameter as
|
|
20
20
|
// `unknown` (OID 705) leaves the server to infer the type, as an undeclared
|
|
21
|
-
// one would, and leaves postgres.js nothing to serialise.
|
|
21
|
+
// one would, and leaves postgres.js nothing to serialise. `:copyfrom` writes
|
|
22
|
+
// the text-format payload to the stream `unsafe(statement, []).writable()`
|
|
23
|
+
// opens for `COPY FROM STDIN`.
|
|
22
24
|
|
|
23
25
|
import { SqlError } from "../index.js";
|
|
24
|
-
import type { Queryable, SqlParam } from "../index.js";
|
|
26
|
+
import type { CopyIn, Queryable, SqlParam } from "../index.js";
|
|
25
27
|
|
|
26
28
|
type RawRows = Promise<$ReadOnlyArray<mixed> & { readonly count?: number | null, ... }>;
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
/** The Node writable postgres.js resolves `COPY FROM STDIN` to. */
|
|
31
|
+
type CopyWritable = {
|
|
32
|
+
write(chunk: string, callback: (error?: mixed) => mixed): mixed,
|
|
33
|
+
end(callback: (error?: mixed) => mixed): mixed,
|
|
34
|
+
once(event: "error", listener: (error: mixed) => mixed): mixed,
|
|
35
|
+
...
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type Unsafe = {
|
|
39
|
+
raw(): RawRows,
|
|
40
|
+
writable(): Promise<CopyWritable>,
|
|
41
|
+
/** Rejects the query. postgres.js calls it for a server error during `COPY`. */
|
|
42
|
+
reject: (error: mixed) => mixed,
|
|
43
|
+
...
|
|
44
|
+
};
|
|
29
45
|
|
|
30
46
|
/** What postgres.js's `Sql` and its transaction handles have in common. */
|
|
31
47
|
export interface PostgresQueries {
|
|
@@ -56,6 +72,78 @@ function text(value: mixed): mixed {
|
|
|
56
72
|
/** `unknown`: a parameter whose type the server decides. */
|
|
57
73
|
const UNKNOWN = 705;
|
|
58
74
|
|
|
75
|
+
function rowCount(payload: string): number {
|
|
76
|
+
let rows = 0;
|
|
77
|
+
for (let i = 0; i < payload.length; i += 1) {
|
|
78
|
+
if (payload[i] === "\n") {
|
|
79
|
+
rows += 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Write `payload` and wait until the server finishes the `COPY`. */
|
|
86
|
+
function writeCopy(writable: CopyWritable, payload: string): Promise<void> {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
let settled = false;
|
|
89
|
+
const fail = (error: mixed) => {
|
|
90
|
+
if (settled) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
settled = true;
|
|
94
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
95
|
+
};
|
|
96
|
+
const succeed = () => {
|
|
97
|
+
if (settled) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
settled = true;
|
|
101
|
+
resolve();
|
|
102
|
+
};
|
|
103
|
+
writable.once("error", fail);
|
|
104
|
+
writable.write(payload, (error) => {
|
|
105
|
+
if (error != null) {
|
|
106
|
+
fail(error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
writable.end((endError) => {
|
|
110
|
+
if (endError != null) {
|
|
111
|
+
fail(endError);
|
|
112
|
+
} else {
|
|
113
|
+
succeed();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function copyOn(sql: PostgresQueries): CopyIn {
|
|
121
|
+
return async (statement, payload) => {
|
|
122
|
+
const query = sql.unsafe(statement, []);
|
|
123
|
+
// CopyInResponse resolves the query with the writable, and sending
|
|
124
|
+
// CopyDone clears that stream before the server answers. A failure after
|
|
125
|
+
// the data is in (a foreign key) calls `query.reject` and never reaches
|
|
126
|
+
// the writable, so its end callback would wait forever.
|
|
127
|
+
let fail: (error: mixed) => void = () => {};
|
|
128
|
+
const failed = new Promise((resolve, reject) => {
|
|
129
|
+
void resolve;
|
|
130
|
+
fail = reject;
|
|
131
|
+
});
|
|
132
|
+
failed.catch(() => {});
|
|
133
|
+
const rejectQuery = query.reject.bind(query);
|
|
134
|
+
query.reject = (error) => {
|
|
135
|
+
fail(error);
|
|
136
|
+
return rejectQuery(error);
|
|
137
|
+
};
|
|
138
|
+
const writable = await query.writable();
|
|
139
|
+
await Promise.race([writeCopy(writable, payload), failed]);
|
|
140
|
+
// CommandComplete carries `COPY n`, and postgres.js keeps that count on
|
|
141
|
+
// its internal result. A `COPY` without `WHERE` accepts every row or
|
|
142
|
+
// fails, so the number of lines written is the number the server copied.
|
|
143
|
+
return rowCount(payload);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
59
147
|
function run(sql: PostgresQueries): Queryable["query"] {
|
|
60
148
|
return async (statement, params) => {
|
|
61
149
|
const typed = params.map((param) => (param === null ? null : sql.typed(param, UNKNOWN)));
|
|
@@ -87,6 +175,8 @@ async function scoped<T>(tx: PostgresTransaction, body: (tx: Queryable) => Promi
|
|
|
87
175
|
maxParams: 65535,
|
|
88
176
|
query: (statement, params, mode) =>
|
|
89
177
|
open ? query(statement, params, mode) : Promise.reject(new SqlError({ kind: "closed" })),
|
|
178
|
+
copy: (statement, payload) =>
|
|
179
|
+
open ? copyOn(tx)(statement, payload) : Promise.reject(new SqlError({ kind: "closed" })),
|
|
90
180
|
transaction: <U>(inner: (tx: Queryable) => Promise<U>): Promise<U> =>
|
|
91
181
|
open
|
|
92
182
|
? tx.savepoint((savepoint) => scoped(savepoint, inner))
|
|
@@ -105,6 +195,7 @@ export function fromPostgres(sql: PostgresSql): Queryable {
|
|
|
105
195
|
engine : "postgresql",
|
|
106
196
|
maxParams: 65535,
|
|
107
197
|
query : run(sql),
|
|
198
|
+
copy : copyOn(sql),
|
|
108
199
|
transaction: <T>(body: (tx: Queryable) => Promise<T>): Promise<T> =>
|
|
109
200
|
sql.begin((tx) => scoped(tx, body)),
|
|
110
201
|
};
|
package/index.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// type. So an `int8` is a `bigint` under `pg` and under `postgres` alike,
|
|
17
17
|
// rather than whatever each driver's own parser decided.
|
|
18
18
|
// * **The parts of sqlc's semantics a driver does not have**: `sqlc.slice`
|
|
19
|
-
// expansion, `:copyfrom`
|
|
20
|
-
//
|
|
19
|
+
// expansion, `:copyfrom` (PostgreSQL `COPY` when the adapter can send it,
|
|
20
|
+
// otherwise chunked multi-row inserts), `:batch*` in one transaction, and
|
|
21
|
+
// nested transactions as savepoints.
|
|
21
22
|
// * **A synchronous path** for drivers that run a statement on the calling
|
|
22
23
|
// thread (`node:sqlite`, `better-sqlite3`, `bun:sqlite`). Generated code
|
|
23
24
|
// with `sync: true` calls it, so a transaction can stay inside `BEGIN`
|
|
@@ -65,12 +66,24 @@ export type QueryMode = "rows" | "exec";
|
|
|
65
66
|
* `transaction` is absent where the platform has no interactive transactions
|
|
66
67
|
* (Cloudflare D1). Inside a transaction it opens a savepoint.
|
|
67
68
|
*/
|
|
69
|
+
/**
|
|
70
|
+
* Send one `COPY … FROM STDIN` payload and resolve to the number of rows the
|
|
71
|
+
* server accepted.
|
|
72
|
+
*
|
|
73
|
+
* Present on a PostgreSQL adapter that can speak `COPY`. Absent everywhere
|
|
74
|
+
* else, where `:copyfrom` stays a chunked `INSERT`. The payload is PostgreSQL
|
|
75
|
+
* text format: tab-separated fields, one row per line.
|
|
76
|
+
*/
|
|
77
|
+
export type CopyIn = (statement: string, payload: string) => Promise<number>;
|
|
78
|
+
|
|
68
79
|
export type Queryable = {
|
|
69
80
|
readonly engine: Engine,
|
|
70
81
|
/** The most bound parameters one statement may carry; `:copyfrom` chunks under it. */
|
|
71
82
|
readonly maxParams: number,
|
|
72
83
|
query(text: string, params: $ReadOnlyArray<SqlParam>, mode: QueryMode): Promise<QueryResult>,
|
|
73
84
|
readonly transaction?: <T>(body: (tx: Queryable) => Promise<T>) => Promise<T>,
|
|
85
|
+
/** PostgreSQL `COPY`, when this adapter can send one. */
|
|
86
|
+
readonly copy?: CopyIn,
|
|
74
87
|
...
|
|
75
88
|
};
|
|
76
89
|
|
|
@@ -494,6 +507,96 @@ export type CopyPlan = {|
|
|
|
494
507
|
/** The most rows one statement carries when a row has no parameters at all. */
|
|
495
508
|
const COPY_ROWS_WITHOUT_PARAMS = 1000;
|
|
496
509
|
|
|
510
|
+
/**
|
|
511
|
+
* `COPY <table> (<columns>) FROM STDIN` for an `INSERT … VALUES` plan, or
|
|
512
|
+
* `null` when the plan is not that shape.
|
|
513
|
+
*
|
|
514
|
+
* sqlc only accepts `INSERT INTO t (…) VALUES (…)` for `:copyfrom`, and the
|
|
515
|
+
* generator puts the table and the column list in `head`. Anything else keeps
|
|
516
|
+
* the chunked `INSERT`, which is the path every engine can run.
|
|
517
|
+
*/
|
|
518
|
+
function copyStatement(plan: CopyPlan): string | null {
|
|
519
|
+
const prefix = "INSERT INTO ";
|
|
520
|
+
const suffix = " VALUES ";
|
|
521
|
+
if (!plan.head.startsWith(prefix) || !plan.head.endsWith(suffix)) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const target = plan.head.slice(prefix.length, plan.head.length - suffix.length);
|
|
525
|
+
return `COPY ${target} FROM STDIN WITH (FORMAT text)`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** One field of PostgreSQL's text `COPY` format. */
|
|
529
|
+
function copyField(value: SqlParam): string {
|
|
530
|
+
if (value === null) {
|
|
531
|
+
return "\\N";
|
|
532
|
+
}
|
|
533
|
+
return escapeCopyText(copyText(value));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function copyText(value: string | number | bigint | Uint8Array): string {
|
|
537
|
+
if (typeof value === "string") {
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
if (typeof value === "number" || typeof value === "bigint") {
|
|
541
|
+
return String(value);
|
|
542
|
+
}
|
|
543
|
+
let hex = "\\x";
|
|
544
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
545
|
+
hex += value[i].toString(16).padStart(2, "0");
|
|
546
|
+
}
|
|
547
|
+
return hex;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** `\`, newline, carriage return and tab, the four escapes text `COPY` requires. */
|
|
551
|
+
function escapeCopyText(text: string): string {
|
|
552
|
+
let out = "";
|
|
553
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
554
|
+
const character = text[i];
|
|
555
|
+
if (character === "\\") {
|
|
556
|
+
out += "\\\\";
|
|
557
|
+
} else if (character === "\n") {
|
|
558
|
+
out += "\\n";
|
|
559
|
+
} else if (character === "\r") {
|
|
560
|
+
out += "\\r";
|
|
561
|
+
} else if (character === "\t") {
|
|
562
|
+
out += "\\t";
|
|
563
|
+
} else {
|
|
564
|
+
out += character;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Every row of a `:copyfrom` as one text-format payload.
|
|
572
|
+
*
|
|
573
|
+
* A short row throws before anything is sent, the same way the `INSERT` path
|
|
574
|
+
* does. A null field is `\N`. The payload ends each row with a newline, which
|
|
575
|
+
* is the row terminator `COPY` counts.
|
|
576
|
+
*/
|
|
577
|
+
function copyPayload(
|
|
578
|
+
plan: CopyPlan,
|
|
579
|
+
rows: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
|
|
580
|
+
name: string,
|
|
581
|
+
): string {
|
|
582
|
+
const width = plan.refs.length;
|
|
583
|
+
let payload = "";
|
|
584
|
+
for (const row of rows) {
|
|
585
|
+
for (let i = 0; i < width; i += 1) {
|
|
586
|
+
const index = plan.refs[i];
|
|
587
|
+
if (index >= row.length) {
|
|
588
|
+
throw new SqlError({ kind: "row", index, length: row.length }, name);
|
|
589
|
+
}
|
|
590
|
+
if (i > 0) {
|
|
591
|
+
payload += "\t";
|
|
592
|
+
}
|
|
593
|
+
payload += copyField(row[index]);
|
|
594
|
+
}
|
|
595
|
+
payload += "\n";
|
|
596
|
+
}
|
|
597
|
+
return payload;
|
|
598
|
+
}
|
|
599
|
+
|
|
497
600
|
function copyChunk(
|
|
498
601
|
engine: Engine,
|
|
499
602
|
plan : CopyPlan,
|
|
@@ -528,12 +631,15 @@ function copyWidth(db: { readonly maxParams: number, ... }, plan: CopyPlan, name
|
|
|
528
631
|
}
|
|
529
632
|
|
|
530
633
|
/**
|
|
531
|
-
* `:copyfrom` — insert every row
|
|
634
|
+
* `:copyfrom` — insert every row.
|
|
532
635
|
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
636
|
+
* A PostgreSQL adapter that implements `copy` receives one `COPY … FROM STDIN`
|
|
637
|
+
* in text format, which is one statement and so one success or one failure.
|
|
638
|
+
* Every other adapter, and a plan that is not an `INSERT … VALUES`, stays on
|
|
639
|
+
* chunked multi-row `INSERT`s. Those run inside one transaction when the
|
|
640
|
+
* `Queryable` can open one and the rows do not fit in a single statement, so
|
|
641
|
+
* a failure in the third chunk does not leave the first two behind. Resolves
|
|
642
|
+
* to the number of rows inserted.
|
|
537
643
|
*/
|
|
538
644
|
export async function copyFrom(
|
|
539
645
|
db : Queryable,
|
|
@@ -544,6 +650,13 @@ export async function copyFrom(
|
|
|
544
650
|
if (rows.length === 0) {
|
|
545
651
|
return 0;
|
|
546
652
|
}
|
|
653
|
+
const copy = db.copy;
|
|
654
|
+
if (db.engine === "postgresql" && typeof copy === "function") {
|
|
655
|
+
const statement = copyStatement(plan);
|
|
656
|
+
if (statement !== null) {
|
|
657
|
+
return copy(statement, copyPayload(plan, rows, name));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
547
660
|
const perStatement = copyWidth(db, plan, name);
|
|
548
661
|
const insert = async (q: Queryable): Promise<number> => {
|
|
549
662
|
let total = 0;
|
|
@@ -667,6 +780,8 @@ export type Connection = {|
|
|
|
667
780
|
readonly runSync?: RunSync,
|
|
668
781
|
/** The statement that opens a transaction; `BEGIN` unless the adapter knows better. */
|
|
669
782
|
readonly begin?: string,
|
|
783
|
+
/** PostgreSQL `COPY` on this connection, when the driver can send one. */
|
|
784
|
+
readonly copy?: CopyIn,
|
|
670
785
|
|};
|
|
671
786
|
|
|
672
787
|
/**
|
|
@@ -701,10 +816,17 @@ async function scoped<T>(
|
|
|
701
816
|
}
|
|
702
817
|
return connection.run(text, params, mode);
|
|
703
818
|
};
|
|
819
|
+
const copy = connection.copy;
|
|
820
|
+
const guardedCopy: CopyIn | void =
|
|
821
|
+
copy === undefined
|
|
822
|
+
? undefined
|
|
823
|
+
: (statement, payload) =>
|
|
824
|
+
open ? copy(statement, payload) : Promise.reject(new SqlError({ kind: "closed" }));
|
|
704
825
|
const tx: Queryable = {
|
|
705
826
|
engine : connection.engine,
|
|
706
827
|
maxParams: connection.maxParams,
|
|
707
828
|
query : guarded,
|
|
829
|
+
...(guardedCopy === undefined ? null : { copy: guardedCopy }),
|
|
708
830
|
transaction: async <U>(inner: (tx: Queryable) => Promise<U>): Promise<U> => {
|
|
709
831
|
const savepoint = `uf_sp_${depth + 1}`;
|
|
710
832
|
await guarded(`SAVEPOINT ${savepoint}`, [], "exec");
|
|
@@ -910,6 +1032,19 @@ function connect(connection: Connection): Handle {
|
|
|
910
1032
|
}
|
|
911
1033
|
return run(text, params, mode);
|
|
912
1034
|
};
|
|
1035
|
+
const rawCopy = connection.copy;
|
|
1036
|
+
const copy =
|
|
1037
|
+
rawCopy === undefined
|
|
1038
|
+
? undefined
|
|
1039
|
+
: async (statement: string, payload: string): Promise<number> => {
|
|
1040
|
+
if (syncOpen) {
|
|
1041
|
+
unsupported("a COPY while a synchronous transaction is open");
|
|
1042
|
+
}
|
|
1043
|
+
while (gate !== null) {
|
|
1044
|
+
await gate;
|
|
1045
|
+
}
|
|
1046
|
+
return rawCopy(statement, payload);
|
|
1047
|
+
};
|
|
913
1048
|
const transactionSync = <T>(body: (tx: SyncQueryable) => T): T => {
|
|
914
1049
|
if (runSync == null) {
|
|
915
1050
|
unsupported("a synchronous transaction on an asynchronous connection");
|
|
@@ -936,6 +1071,7 @@ function connect(connection: Connection): Handle {
|
|
|
936
1071
|
transaction,
|
|
937
1072
|
querySync,
|
|
938
1073
|
transactionSync,
|
|
1074
|
+
...(copy === undefined ? null : { copy }),
|
|
939
1075
|
};
|
|
940
1076
|
}
|
|
941
1077
|
|
package/package.json
CHANGED