@uniflowed/sql 0.34.0 → 0.36.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/better-sqlite3.js +8 -8
- package/adapters/bun-sqlite.js +8 -8
- package/adapters/d1.js +2 -2
- package/adapters/mysql2.js +13 -13
- package/adapters/node-sqlite.js +7 -7
- package/adapters/pg.js +120 -9
- package/adapters/pglite.js +29 -6
- package/adapters/postgres.js +99 -8
- package/index.js +233 -97
- package/internal/shared.js +4 -4
- package/internal/statements.js +1 -1
- package/mysql.js +10 -10
- package/package.json +1 -1
- package/postgresql.js +14 -14
- package/sqlite.js +13 -13
|
@@ -22,7 +22,7 @@ export interface BetterSqlite3Statement {
|
|
|
22
22
|
readonly reader: boolean;
|
|
23
23
|
all(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
|
|
24
24
|
run(...params: $ReadOnlyArray<SqlParam>): {
|
|
25
|
-
readonly changes: number,
|
|
25
|
+
readonly changes : number,
|
|
26
26
|
readonly lastInsertRowid: number | bigint,
|
|
27
27
|
...
|
|
28
28
|
};
|
|
@@ -36,15 +36,15 @@ export interface BetterSqlite3Database {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export type BetterSqlite3Options = {|
|
|
39
|
-
readonly begin
|
|
39
|
+
readonly begin? : string,
|
|
40
40
|
readonly statementCache?: number,
|
|
41
|
-
readonly maxParams
|
|
41
|
+
readonly maxParams? : number,
|
|
42
42
|
|};
|
|
43
43
|
|
|
44
44
|
/** A [`Queryable`] and a [`SyncQueryable`] over one better-sqlite3 `Database`. */
|
|
45
45
|
export function fromBetterSqlite3(
|
|
46
46
|
database: BetterSqlite3Database,
|
|
47
|
-
options: BetterSqlite3Options = {},
|
|
47
|
+
options : BetterSqlite3Options = {},
|
|
48
48
|
): Queryable & SyncQueryable {
|
|
49
49
|
const statement = statementCache((text) => {
|
|
50
50
|
const prepared = database.prepare(text);
|
|
@@ -70,16 +70,16 @@ export function fromBetterSqlite3(
|
|
|
70
70
|
}
|
|
71
71
|
const result = prepared.run(...params);
|
|
72
72
|
return {
|
|
73
|
-
rows: [],
|
|
73
|
+
rows : [],
|
|
74
74
|
rowsAffected: count(result.changes),
|
|
75
75
|
lastInsertId: rowId(result.lastInsertRowid),
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
78
|
return singleConnectionSync({
|
|
79
|
-
engine: "sqlite",
|
|
79
|
+
engine : "sqlite",
|
|
80
80
|
maxParams: options.maxParams ?? 32766,
|
|
81
|
-
begin: options.begin,
|
|
82
|
-
run: async (text, params, mode) => runSync(text, params, mode),
|
|
81
|
+
begin : options.begin,
|
|
82
|
+
run : async (text, params, mode) => runSync(text, params, mode),
|
|
83
83
|
runSync,
|
|
84
84
|
});
|
|
85
85
|
}
|
package/adapters/bun-sqlite.js
CHANGED
|
@@ -21,7 +21,7 @@ import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../intern
|
|
|
21
21
|
export interface BunSqliteStatement {
|
|
22
22
|
values(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
|
|
23
23
|
run(...params: $ReadOnlyArray<SqlParam>): {
|
|
24
|
-
readonly changes: number | bigint,
|
|
24
|
+
readonly changes : number | bigint,
|
|
25
25
|
readonly lastInsertRowid: number | bigint,
|
|
26
26
|
...
|
|
27
27
|
};
|
|
@@ -34,15 +34,15 @@ export interface BunSqliteDatabase {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export type BunSqliteOptions = {|
|
|
37
|
-
readonly begin
|
|
37
|
+
readonly begin? : string,
|
|
38
38
|
readonly statementCache?: number,
|
|
39
|
-
readonly maxParams
|
|
39
|
+
readonly maxParams? : number,
|
|
40
40
|
|};
|
|
41
41
|
|
|
42
42
|
/** A [`Queryable`] and a [`SyncQueryable`] over one `bun:sqlite` `Database`. */
|
|
43
43
|
export function fromBunSqlite(
|
|
44
44
|
database: BunSqliteDatabase,
|
|
45
|
-
options: BunSqliteOptions = {},
|
|
45
|
+
options : BunSqliteOptions = {},
|
|
46
46
|
): Queryable & SyncQueryable {
|
|
47
47
|
const statement = statementCache((text) => {
|
|
48
48
|
const prepared = database.prepare(text);
|
|
@@ -63,16 +63,16 @@ export function fromBunSqlite(
|
|
|
63
63
|
}
|
|
64
64
|
const result = prepared.run(...params);
|
|
65
65
|
return {
|
|
66
|
-
rows: [],
|
|
66
|
+
rows : [],
|
|
67
67
|
rowsAffected: count(result.changes),
|
|
68
68
|
lastInsertId: rowId(result.lastInsertRowid),
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
71
|
return singleConnectionSync({
|
|
72
|
-
engine: "sqlite",
|
|
72
|
+
engine : "sqlite",
|
|
73
73
|
maxParams: options.maxParams ?? 32766,
|
|
74
|
-
begin: options.begin,
|
|
75
|
-
run: async (text, params, mode) => runSync(text, params, mode),
|
|
74
|
+
begin : options.begin,
|
|
75
|
+
run : async (text, params, mode) => runSync(text, params, mode),
|
|
76
76
|
runSync,
|
|
77
77
|
});
|
|
78
78
|
}
|
package/adapters/d1.js
CHANGED
|
@@ -47,7 +47,7 @@ function toD1(param: SqlParam): mixed {
|
|
|
47
47
|
/** A [`Queryable`] over a D1 binding. */
|
|
48
48
|
export function fromD1(database: D1Database): Queryable {
|
|
49
49
|
return {
|
|
50
|
-
engine: "sqlite",
|
|
50
|
+
engine : "sqlite",
|
|
51
51
|
maxParams: 100,
|
|
52
52
|
query: async (text, params, mode) => {
|
|
53
53
|
const statement = database.prepare(text).bind(...params.map(toD1));
|
|
@@ -63,7 +63,7 @@ export function fromD1(database: D1Database): Queryable {
|
|
|
63
63
|
}
|
|
64
64
|
const { meta } = await statement.run();
|
|
65
65
|
return {
|
|
66
|
-
rows: [],
|
|
66
|
+
rows : [],
|
|
67
67
|
rowsAffected: count(meta.changes ?? 0),
|
|
68
68
|
lastInsertId: rowId(meta.last_row_id),
|
|
69
69
|
};
|
package/adapters/mysql2.js
CHANGED
|
@@ -34,13 +34,13 @@ export type Mysql2Field = {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
type Mysql2Options = {|
|
|
37
|
-
readonly sql: string,
|
|
38
|
-
readonly values: $ReadOnlyArray<mixed>,
|
|
39
|
-
readonly rowsAsArray: true,
|
|
37
|
+
readonly sql : string,
|
|
38
|
+
readonly values : $ReadOnlyArray<mixed>,
|
|
39
|
+
readonly rowsAsArray : true,
|
|
40
40
|
readonly supportBigNumbers: true,
|
|
41
|
-
readonly bigNumberStrings: true,
|
|
42
|
-
readonly dateStrings: true,
|
|
43
|
-
readonly typeCast: (field: Mysql2Field, next: () => mixed) => mixed,
|
|
41
|
+
readonly bigNumberStrings : true,
|
|
42
|
+
readonly dateStrings : true,
|
|
43
|
+
readonly typeCast : (field: Mysql2Field, next: () => mixed) => mixed,
|
|
44
44
|
|};
|
|
45
45
|
|
|
46
46
|
type Mysql2Result =
|
|
@@ -93,12 +93,12 @@ function lastId(value: number | string): bigint | null {
|
|
|
93
93
|
function runOn(connection: Mysql2Connection): Run {
|
|
94
94
|
return async (text, params, mode): Promise<QueryResult> => {
|
|
95
95
|
const options: Mysql2Options = {
|
|
96
|
-
sql: text,
|
|
97
|
-
values: params,
|
|
98
|
-
rowsAsArray: true,
|
|
96
|
+
sql : text,
|
|
97
|
+
values : params,
|
|
98
|
+
rowsAsArray : true,
|
|
99
99
|
supportBigNumbers: true,
|
|
100
|
-
bigNumberStrings: true,
|
|
101
|
-
dateStrings: true,
|
|
100
|
+
bigNumberStrings : true,
|
|
101
|
+
dateStrings : true,
|
|
102
102
|
typeCast,
|
|
103
103
|
};
|
|
104
104
|
const [result] =
|
|
@@ -125,9 +125,9 @@ function connection(client: Mysql2Connection): Connection {
|
|
|
125
125
|
/** A [`Queryable`] over a `mysql2/promise` pool. */
|
|
126
126
|
export function fromMysql2Pool(pool: Mysql2Pool): Queryable {
|
|
127
127
|
return {
|
|
128
|
-
engine: "mysql",
|
|
128
|
+
engine : "mysql",
|
|
129
129
|
maxParams: 65535,
|
|
130
|
-
query: runOn(pool),
|
|
130
|
+
query : runOn(pool),
|
|
131
131
|
transaction: async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
|
|
132
132
|
const client = await pool.getConnection();
|
|
133
133
|
try {
|
package/adapters/node-sqlite.js
CHANGED
|
@@ -22,7 +22,7 @@ import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../intern
|
|
|
22
22
|
export interface NodeSqliteStatement {
|
|
23
23
|
all(...params: $ReadOnlyArray<SqlParam>): $ReadOnlyArray<mixed>;
|
|
24
24
|
run(...params: $ReadOnlyArray<SqlParam>): {
|
|
25
|
-
readonly changes: number | bigint,
|
|
25
|
+
readonly changes : number | bigint,
|
|
26
26
|
readonly lastInsertRowid: number | bigint,
|
|
27
27
|
...
|
|
28
28
|
};
|
|
@@ -51,7 +51,7 @@ function prepared(database: NodeSqliteDatabase, text: string): NodeSqliteStateme
|
|
|
51
51
|
// failing on the call below.
|
|
52
52
|
if (!("setReturnArrays" in statement)) {
|
|
53
53
|
throw new SqlError({
|
|
54
|
-
kind: "unsupported",
|
|
54
|
+
kind : "unsupported",
|
|
55
55
|
feature: "node:sqlite without StatementSync#setReturnArrays (Node 24.0 or 22.16 and newer)",
|
|
56
56
|
});
|
|
57
57
|
}
|
|
@@ -62,7 +62,7 @@ function prepared(database: NodeSqliteDatabase, text: string): NodeSqliteStateme
|
|
|
62
62
|
/** A [`Queryable`] and a [`SyncQueryable`] over one `DatabaseSync`. */
|
|
63
63
|
export function fromNodeSqlite(
|
|
64
64
|
database: NodeSqliteDatabase,
|
|
65
|
-
options: NodeSqliteOptions = {},
|
|
65
|
+
options : NodeSqliteOptions = {},
|
|
66
66
|
): Queryable & SyncQueryable {
|
|
67
67
|
const statement = statementCache(
|
|
68
68
|
(text) => prepared(database, text),
|
|
@@ -83,16 +83,16 @@ export function fromNodeSqlite(
|
|
|
83
83
|
}
|
|
84
84
|
const result = prepared.run(...params);
|
|
85
85
|
return {
|
|
86
|
-
rows: [],
|
|
86
|
+
rows : [],
|
|
87
87
|
rowsAffected: count(result.changes),
|
|
88
88
|
lastInsertId: rowId(result.lastInsertRowid),
|
|
89
89
|
};
|
|
90
90
|
};
|
|
91
91
|
return singleConnectionSync({
|
|
92
|
-
engine: "sqlite",
|
|
92
|
+
engine : "sqlite",
|
|
93
93
|
maxParams: options.maxParams ?? 32766,
|
|
94
|
-
begin: options.begin,
|
|
95
|
-
run: async (text, params, mode) => runSync(text, params, mode),
|
|
94
|
+
begin : options.begin,
|
|
95
|
+
run : async (text, params, mode) => runSync(text, params, mode),
|
|
96
96
|
runSync,
|
|
97
97
|
});
|
|
98
98
|
}
|
package/adapters/pg.js
CHANGED
|
@@ -11,22 +11,48 @@
|
|
|
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, ... };
|
|
19
22
|
|
|
20
23
|
type PgQuery = {|
|
|
21
|
-
readonly text: string,
|
|
22
|
-
readonly values: $ReadOnlyArray<mixed>,
|
|
24
|
+
readonly text : string,
|
|
25
|
+
readonly values : $ReadOnlyArray<mixed>,
|
|
23
26
|
readonly rowMode: "array",
|
|
24
|
-
readonly types: {| readonly getTypeParser: () => (value: string) => string |},
|
|
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. */
|
|
@@ -67,9 +177,10 @@ export function fromPgClient(client: PgClient): Queryable {
|
|
|
67
177
|
/** A [`Queryable`] over a `pg.Pool`. */
|
|
68
178
|
export function fromPgPool(pool: PgPool): Queryable {
|
|
69
179
|
return {
|
|
70
|
-
engine: "postgresql",
|
|
180
|
+
engine : "postgresql",
|
|
71
181
|
maxParams: 65535,
|
|
72
|
-
query: runOn(pool),
|
|
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";
|
|
@@ -18,15 +19,16 @@ import { singleConnection } from "../index.js";
|
|
|
18
19
|
/** The part of a `PGlite` instance this uses. */
|
|
19
20
|
export interface PGliteDatabase {
|
|
20
21
|
query(
|
|
21
|
-
text: string,
|
|
22
|
+
text : string,
|
|
22
23
|
params: $ReadOnlyArray<SqlParam>,
|
|
23
24
|
options: {|
|
|
24
|
-
readonly rowMode: "array",
|
|
25
|
-
readonly parsers: { readonly [string]: (value: string) => mixed },
|
|
25
|
+
readonly rowMode : "array",
|
|
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
|
-
readonly parsers: { readonly [string]: mixed };
|
|
31
|
+
readonly parsers : { readonly [string]: mixed };
|
|
30
32
|
readonly serializers: { readonly [string]: mixed };
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -50,8 +52,29 @@ export function fromPGlite(database: PGliteDatabase): Queryable {
|
|
|
50
52
|
let parsers: { readonly [string]: (value: mixed) => mixed } = {};
|
|
51
53
|
let serializers: { readonly [string]: (value: mixed) => mixed } = {};
|
|
52
54
|
return singleConnection({
|
|
53
|
-
engine: "postgresql",
|
|
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,20 +18,36 @@
|
|
|
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 {
|
|
32
48
|
unsafe(
|
|
33
|
-
text: string,
|
|
34
|
-
params: $ReadOnlyArray<mixed>,
|
|
49
|
+
text : string,
|
|
50
|
+
params : $ReadOnlyArray<mixed>,
|
|
35
51
|
options?: {| readonly prepare?: boolean |},
|
|
36
52
|
): Unsafe;
|
|
37
53
|
typed(value: SqlParam, oid: number): mixed;
|
|
@@ -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)));
|
|
@@ -83,10 +171,12 @@ async function scoped<T>(tx: PostgresTransaction, body: (tx: Queryable) => Promi
|
|
|
83
171
|
let open = true;
|
|
84
172
|
const query = run(tx);
|
|
85
173
|
const queryable: Queryable = {
|
|
86
|
-
engine: "postgresql",
|
|
174
|
+
engine : "postgresql",
|
|
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))
|
|
@@ -102,9 +192,10 @@ async function scoped<T>(tx: PostgresTransaction, body: (tx: Queryable) => Promi
|
|
|
102
192
|
/** A [`Queryable`] over a postgres.js `Sql`. */
|
|
103
193
|
export function fromPostgres(sql: PostgresSql): Queryable {
|
|
104
194
|
return {
|
|
105
|
-
engine: "postgresql",
|
|
195
|
+
engine : "postgresql",
|
|
106
196
|
maxParams: 65535,
|
|
107
|
-
query: run(sql),
|
|
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
|
};
|