@uniflowed/sql 0.19.0 → 0.21.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 +29 -24
- package/adapters/bun-sqlite.js +29 -24
- package/adapters/node-sqlite.js +30 -25
- package/index.js +438 -50
- package/package.json +1 -1
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// const db = fromBetterSqlite3(new Database("app.db"));
|
|
9
9
|
//
|
|
10
|
+
// The handle has `query` and `querySync`. `querySync` and `transactionSync`
|
|
11
|
+
// run on this thread, which is what sqlc's `sync: true` output calls.
|
|
12
|
+
//
|
|
10
13
|
// Tested with its prebuilt native addon in `tests/sqlc/external-adapters.mjs`,
|
|
11
14
|
// including exact integers, nulls, transactions and savepoints.
|
|
12
15
|
|
|
13
|
-
import type { Queryable, SqlParam } from "../index.js";
|
|
14
|
-
import { SqlError,
|
|
16
|
+
import type { Queryable, RunSync, SqlParam, SyncQueryable } from "../index.js";
|
|
17
|
+
import { SqlError, singleConnectionSync } from "../index.js";
|
|
15
18
|
import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../internal/statements.js";
|
|
16
19
|
|
|
17
20
|
/** The part of better-sqlite3's `Statement` this uses. */
|
|
@@ -38,11 +41,11 @@ export type BetterSqlite3Options = {|
|
|
|
38
41
|
readonly maxParams?: number,
|
|
39
42
|
|};
|
|
40
43
|
|
|
41
|
-
/** A [`Queryable`] over one better-sqlite3 `Database`. */
|
|
44
|
+
/** A [`Queryable`] and a [`SyncQueryable`] over one better-sqlite3 `Database`. */
|
|
42
45
|
export function fromBetterSqlite3(
|
|
43
46
|
database: BetterSqlite3Database,
|
|
44
47
|
options: BetterSqlite3Options = {},
|
|
45
|
-
): Queryable {
|
|
48
|
+
): Queryable & SyncQueryable {
|
|
46
49
|
const statement = statementCache((text) => {
|
|
47
50
|
const prepared = database.prepare(text);
|
|
48
51
|
prepared.safeIntegers(true);
|
|
@@ -53,28 +56,30 @@ export function fromBetterSqlite3(
|
|
|
53
56
|
}
|
|
54
57
|
return prepared;
|
|
55
58
|
}, options.statementCache ?? DEFAULT_STATEMENT_CACHE);
|
|
56
|
-
|
|
59
|
+
const runSync: RunSync = (text, params, mode) => {
|
|
60
|
+
const prepared = statement(text);
|
|
61
|
+
if (mode === "rows" && prepared.reader) {
|
|
62
|
+
const rows: Array<$ReadOnlyArray<mixed>> = [];
|
|
63
|
+
for (const row of prepared.all(...params)) {
|
|
64
|
+
if (!Array.isArray(row)) {
|
|
65
|
+
throw new SqlError({ kind: "unsupported", feature: "better-sqlite3 rows as objects" });
|
|
66
|
+
}
|
|
67
|
+
rows.push(row);
|
|
68
|
+
}
|
|
69
|
+
return { rows, rowsAffected: 0, lastInsertId: null };
|
|
70
|
+
}
|
|
71
|
+
const result = prepared.run(...params);
|
|
72
|
+
return {
|
|
73
|
+
rows: [],
|
|
74
|
+
rowsAffected: count(result.changes),
|
|
75
|
+
lastInsertId: rowId(result.lastInsertRowid),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
return singleConnectionSync({
|
|
57
79
|
engine: "sqlite",
|
|
58
80
|
maxParams: options.maxParams ?? 32766,
|
|
59
81
|
begin: options.begin,
|
|
60
|
-
run: async (text, params, mode) =>
|
|
61
|
-
|
|
62
|
-
if (mode === "rows" && prepared.reader) {
|
|
63
|
-
const rows: Array<$ReadOnlyArray<mixed>> = [];
|
|
64
|
-
for (const row of prepared.all(...params)) {
|
|
65
|
-
if (!Array.isArray(row)) {
|
|
66
|
-
throw new SqlError({ kind: "unsupported", feature: "better-sqlite3 rows as objects" });
|
|
67
|
-
}
|
|
68
|
-
rows.push(row);
|
|
69
|
-
}
|
|
70
|
-
return { rows, rowsAffected: 0, lastInsertId: null };
|
|
71
|
-
}
|
|
72
|
-
const result = prepared.run(...params);
|
|
73
|
-
return {
|
|
74
|
-
rows: [],
|
|
75
|
-
rowsAffected: count(result.changes),
|
|
76
|
-
lastInsertId: rowId(result.lastInsertRowid),
|
|
77
|
-
};
|
|
78
|
-
},
|
|
82
|
+
run: async (text, params, mode) => runSync(text, params, mode),
|
|
83
|
+
runSync,
|
|
79
84
|
});
|
|
80
85
|
}
|
package/adapters/bun-sqlite.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// const db = fromBunSqlite(new Database("app.db"));
|
|
9
9
|
//
|
|
10
|
+
// The handle has `query` and `querySync`. `querySync` and `transactionSync`
|
|
11
|
+
// run on this thread, which is what sqlc's `sync: true` output calls.
|
|
12
|
+
//
|
|
10
13
|
// Safe integers are turned on per statement, so the `Database` does not have
|
|
11
14
|
// to be opened with `safeIntegers: true` — though it does no harm.
|
|
12
15
|
|
|
13
|
-
import type { Queryable, SqlParam } from "../index.js";
|
|
14
|
-
import { SqlError,
|
|
16
|
+
import type { Queryable, RunSync, SqlParam, SyncQueryable } from "../index.js";
|
|
17
|
+
import { SqlError, singleConnectionSync } from "../index.js";
|
|
15
18
|
import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../internal/statements.js";
|
|
16
19
|
|
|
17
20
|
/** The part of `bun:sqlite`'s `Statement` this uses. */
|
|
@@ -36,38 +39,40 @@ export type BunSqliteOptions = {|
|
|
|
36
39
|
readonly maxParams?: number,
|
|
37
40
|
|};
|
|
38
41
|
|
|
39
|
-
/** A [`Queryable`] over one `bun:sqlite` `Database`. */
|
|
42
|
+
/** A [`Queryable`] and a [`SyncQueryable`] over one `bun:sqlite` `Database`. */
|
|
40
43
|
export function fromBunSqlite(
|
|
41
44
|
database: BunSqliteDatabase,
|
|
42
45
|
options: BunSqliteOptions = {},
|
|
43
|
-
): Queryable {
|
|
46
|
+
): Queryable & SyncQueryable {
|
|
44
47
|
const statement = statementCache((text) => {
|
|
45
48
|
const prepared = database.prepare(text);
|
|
46
49
|
prepared.safeIntegers(true);
|
|
47
50
|
return prepared;
|
|
48
51
|
}, options.statementCache ?? DEFAULT_STATEMENT_CACHE);
|
|
49
|
-
|
|
52
|
+
const runSync: RunSync = (text, params, mode) => {
|
|
53
|
+
const prepared = statement(text);
|
|
54
|
+
if (mode === "rows") {
|
|
55
|
+
const rows: Array<$ReadOnlyArray<mixed>> = [];
|
|
56
|
+
for (const row of prepared.values(...params)) {
|
|
57
|
+
if (!Array.isArray(row)) {
|
|
58
|
+
throw new SqlError({ kind: "unsupported", feature: "bun:sqlite rows as objects" });
|
|
59
|
+
}
|
|
60
|
+
rows.push(row);
|
|
61
|
+
}
|
|
62
|
+
return { rows, rowsAffected: 0, lastInsertId: null };
|
|
63
|
+
}
|
|
64
|
+
const result = prepared.run(...params);
|
|
65
|
+
return {
|
|
66
|
+
rows: [],
|
|
67
|
+
rowsAffected: count(result.changes),
|
|
68
|
+
lastInsertId: rowId(result.lastInsertRowid),
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
return singleConnectionSync({
|
|
50
72
|
engine: "sqlite",
|
|
51
73
|
maxParams: options.maxParams ?? 32766,
|
|
52
74
|
begin: options.begin,
|
|
53
|
-
run: async (text, params, mode) =>
|
|
54
|
-
|
|
55
|
-
if (mode === "rows") {
|
|
56
|
-
const rows: Array<$ReadOnlyArray<mixed>> = [];
|
|
57
|
-
for (const row of prepared.values(...params)) {
|
|
58
|
-
if (!Array.isArray(row)) {
|
|
59
|
-
throw new SqlError({ kind: "unsupported", feature: "bun:sqlite rows as objects" });
|
|
60
|
-
}
|
|
61
|
-
rows.push(row);
|
|
62
|
-
}
|
|
63
|
-
return { rows, rowsAffected: 0, lastInsertId: null };
|
|
64
|
-
}
|
|
65
|
-
const result = prepared.run(...params);
|
|
66
|
-
return {
|
|
67
|
-
rows: [],
|
|
68
|
-
rowsAffected: count(result.changes),
|
|
69
|
-
lastInsertId: rowId(result.lastInsertRowid),
|
|
70
|
-
};
|
|
71
|
-
},
|
|
75
|
+
run: async (text, params, mode) => runSync(text, params, mode),
|
|
76
|
+
runSync,
|
|
72
77
|
});
|
|
73
78
|
}
|
package/adapters/node-sqlite.js
CHANGED
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
//
|
|
8
8
|
// const db = fromNodeSqlite(new DatabaseSync("app.db"));
|
|
9
9
|
//
|
|
10
|
+
// The handle has `query` and `querySync`. `querySync` and `transactionSync`
|
|
11
|
+
// run on this thread, which is what sqlc's `sync: true` output calls.
|
|
12
|
+
//
|
|
10
13
|
// Needs `StatementSync#setReturnArrays`, which Node added in 24.0 and 22.16.
|
|
11
14
|
// An older Node gets an error that says so, rather than rows keyed by column
|
|
12
15
|
// name — which would silently merge two columns that share one.
|
|
13
16
|
|
|
14
|
-
import type { Queryable, SqlParam } from "../index.js";
|
|
15
|
-
import { SqlError,
|
|
17
|
+
import type { Queryable, RunSync, SqlParam, SyncQueryable } from "../index.js";
|
|
18
|
+
import { SqlError, singleConnectionSync } from "../index.js";
|
|
16
19
|
import { DEFAULT_STATEMENT_CACHE, count, rowId, statementCache } from "../internal/statements.js";
|
|
17
20
|
|
|
18
21
|
/** The part of `node:sqlite`'s `StatementSync` this uses. */
|
|
@@ -56,38 +59,40 @@ function prepared(database: NodeSqliteDatabase, text: string): NodeSqliteStateme
|
|
|
56
59
|
return statement;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
/** A [`Queryable`] over one `DatabaseSync`. */
|
|
62
|
+
/** A [`Queryable`] and a [`SyncQueryable`] over one `DatabaseSync`. */
|
|
60
63
|
export function fromNodeSqlite(
|
|
61
64
|
database: NodeSqliteDatabase,
|
|
62
65
|
options: NodeSqliteOptions = {},
|
|
63
|
-
): Queryable {
|
|
66
|
+
): Queryable & SyncQueryable {
|
|
64
67
|
const statement = statementCache(
|
|
65
68
|
(text) => prepared(database, text),
|
|
66
69
|
options.statementCache ?? DEFAULT_STATEMENT_CACHE,
|
|
67
70
|
);
|
|
68
|
-
|
|
71
|
+
const runSync: RunSync = (text, params, mode) => {
|
|
72
|
+
const prepared = statement(text);
|
|
73
|
+
if (mode === "rows") {
|
|
74
|
+
const rows = prepared.all(...params);
|
|
75
|
+
const arrays: Array<$ReadOnlyArray<mixed>> = [];
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
if (!Array.isArray(row)) {
|
|
78
|
+
throw new SqlError({ kind: "unsupported", feature: "node:sqlite rows as objects" });
|
|
79
|
+
}
|
|
80
|
+
arrays.push(row);
|
|
81
|
+
}
|
|
82
|
+
return { rows: arrays, rowsAffected: 0, lastInsertId: null };
|
|
83
|
+
}
|
|
84
|
+
const result = prepared.run(...params);
|
|
85
|
+
return {
|
|
86
|
+
rows: [],
|
|
87
|
+
rowsAffected: count(result.changes),
|
|
88
|
+
lastInsertId: rowId(result.lastInsertRowid),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
return singleConnectionSync({
|
|
69
92
|
engine: "sqlite",
|
|
70
93
|
maxParams: options.maxParams ?? 32766,
|
|
71
94
|
begin: options.begin,
|
|
72
|
-
run: async (text, params, mode) =>
|
|
73
|
-
|
|
74
|
-
if (mode === "rows") {
|
|
75
|
-
const rows = prepared.all(...params);
|
|
76
|
-
const arrays: Array<$ReadOnlyArray<mixed>> = [];
|
|
77
|
-
for (const row of rows) {
|
|
78
|
-
if (!Array.isArray(row)) {
|
|
79
|
-
throw new SqlError({ kind: "unsupported", feature: "node:sqlite rows as objects" });
|
|
80
|
-
}
|
|
81
|
-
arrays.push(row);
|
|
82
|
-
}
|
|
83
|
-
return { rows: arrays, rowsAffected: 0, lastInsertId: null };
|
|
84
|
-
}
|
|
85
|
-
const result = prepared.run(...params);
|
|
86
|
-
return {
|
|
87
|
-
rows: [],
|
|
88
|
-
rowsAffected: count(result.changes),
|
|
89
|
-
lastInsertId: rowId(result.lastInsertRowid),
|
|
90
|
-
};
|
|
91
|
-
},
|
|
95
|
+
run: async (text, params, mode) => runSync(text, params, mode),
|
|
96
|
+
runSync,
|
|
92
97
|
});
|
|
93
98
|
}
|
package/index.js
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
// * **The parts of sqlc's semantics a driver does not have**: `sqlc.slice`
|
|
19
19
|
// expansion, `:copyfrom` as chunked multi-row inserts, `:batch*` in one
|
|
20
20
|
// transaction, and nested transactions as savepoints.
|
|
21
|
+
// * **A synchronous path** for drivers that run a statement on the calling
|
|
22
|
+
// thread (`node:sqlite`, `better-sqlite3`, `bun:sqlite`). Generated code
|
|
23
|
+
// with `sync: true` calls it, so a transaction can stay inside `BEGIN`
|
|
24
|
+
// through `COMMIT` without yielding.
|
|
21
25
|
//
|
|
22
26
|
// docs/sqlc.md is the design record.
|
|
23
27
|
|
|
@@ -70,6 +74,30 @@ export type Queryable = {
|
|
|
70
74
|
...
|
|
71
75
|
};
|
|
72
76
|
|
|
77
|
+
/**
|
|
78
|
+
* A connection whose statements run on the calling thread and return before
|
|
79
|
+
* the call does.
|
|
80
|
+
*
|
|
81
|
+
* This is the contract `sync: true` generated code needs. `node:sqlite`,
|
|
82
|
+
* `better-sqlite3` and `bun:sqlite` implement it on the same object as
|
|
83
|
+
* [`Queryable`]. Drivers that are asynchronous all the way down (D1, `pg`,
|
|
84
|
+
* `postgres`, `mysql2`) do not have it, and a synchronous function does not
|
|
85
|
+
* typecheck against them.
|
|
86
|
+
*
|
|
87
|
+
* `transactionSync` does not yield between `BEGIN` and `COMMIT`. Nested
|
|
88
|
+
* `transactionSync` opens a savepoint. Calling the outer handle while a
|
|
89
|
+
* transaction is open throws: an asynchronous transaction cannot be waited
|
|
90
|
+
* out on this thread, and reaching back to the outer handle from inside the
|
|
91
|
+
* body would deadlock the same way `await db.query` does inside `transaction`.
|
|
92
|
+
*/
|
|
93
|
+
export type SyncQueryable = {
|
|
94
|
+
readonly engine: Engine,
|
|
95
|
+
readonly maxParams: number,
|
|
96
|
+
querySync(text: string, params: $ReadOnlyArray<SqlParam>, mode: QueryMode): QueryResult,
|
|
97
|
+
readonly transactionSync?: <T>(body: (tx: SyncQueryable) => T) => T,
|
|
98
|
+
...
|
|
99
|
+
};
|
|
100
|
+
|
|
73
101
|
/** A JSON document, as `json` and `jsonb` columns decode to. */
|
|
74
102
|
export type JsonValue =
|
|
75
103
|
| null
|
|
@@ -169,6 +197,29 @@ function checkWidth(row: Row, width: number): void {
|
|
|
169
197
|
}
|
|
170
198
|
}
|
|
171
199
|
|
|
200
|
+
function unsupported(feature: string): empty {
|
|
201
|
+
throw new SqlError({ kind: "unsupported", feature });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function firstRow<T>(rows: $ReadOnlyArray<Row>, width: number, decode: (row: Row) => T): T | null {
|
|
205
|
+
if (rows.length === 0) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const row = rows[0];
|
|
209
|
+
checkWidth(row, width);
|
|
210
|
+
return decode(row);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function allRows<T>(rows: $ReadOnlyArray<Row>, width: number, decode: (row: Row) => T): Array<T> {
|
|
214
|
+
const out: Array<T> = new Array(rows.length);
|
|
215
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
216
|
+
const row = rows[i];
|
|
217
|
+
checkWidth(row, width);
|
|
218
|
+
out[i] = decode(row);
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
172
223
|
// ---------------------------------------------------------------------------
|
|
173
224
|
// Running a generated query.
|
|
174
225
|
//
|
|
@@ -187,12 +238,24 @@ export async function one<T>(
|
|
|
187
238
|
): Promise<T | null> {
|
|
188
239
|
try {
|
|
189
240
|
const { rows } = await db.query(text, params, "rows");
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
241
|
+
return firstRow(rows, width, decode);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw attribute(error, name);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** `:one` on a [`SyncQueryable`]. */
|
|
248
|
+
export function oneSync<T>(
|
|
249
|
+
db: SyncQueryable,
|
|
250
|
+
name: string,
|
|
251
|
+
text: string,
|
|
252
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
253
|
+
width: number,
|
|
254
|
+
decode: (row: Row) => T,
|
|
255
|
+
): T | null {
|
|
256
|
+
try {
|
|
257
|
+
const { rows } = db.querySync(text, params, "rows");
|
|
258
|
+
return firstRow(rows, width, decode);
|
|
196
259
|
} catch (error) {
|
|
197
260
|
throw attribute(error, name);
|
|
198
261
|
}
|
|
@@ -209,13 +272,24 @@ export async function many<T>(
|
|
|
209
272
|
): Promise<Array<T>> {
|
|
210
273
|
try {
|
|
211
274
|
const { rows } = await db.query(text, params, "rows");
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
275
|
+
return allRows(rows, width, decode);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw attribute(error, name);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** `:many` on a [`SyncQueryable`]. */
|
|
282
|
+
export function manySync<T>(
|
|
283
|
+
db: SyncQueryable,
|
|
284
|
+
name: string,
|
|
285
|
+
text: string,
|
|
286
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
287
|
+
width: number,
|
|
288
|
+
decode: (row: Row) => T,
|
|
289
|
+
): Array<T> {
|
|
290
|
+
try {
|
|
291
|
+
const { rows } = db.querySync(text, params, "rows");
|
|
292
|
+
return allRows(rows, width, decode);
|
|
219
293
|
} catch (error) {
|
|
220
294
|
throw attribute(error, name);
|
|
221
295
|
}
|
|
@@ -234,6 +308,19 @@ async function run(
|
|
|
234
308
|
}
|
|
235
309
|
}
|
|
236
310
|
|
|
311
|
+
function executed(
|
|
312
|
+
db: SyncQueryable,
|
|
313
|
+
name: string,
|
|
314
|
+
text: string,
|
|
315
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
316
|
+
): QueryResult {
|
|
317
|
+
try {
|
|
318
|
+
return db.querySync(text, params, "exec");
|
|
319
|
+
} catch (error) {
|
|
320
|
+
throw attribute(error, name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
237
324
|
/** `:exec`. */
|
|
238
325
|
export async function exec(
|
|
239
326
|
db: Queryable,
|
|
@@ -244,6 +331,16 @@ export async function exec(
|
|
|
244
331
|
await run(db, name, text, params);
|
|
245
332
|
}
|
|
246
333
|
|
|
334
|
+
/** `:exec` on a [`SyncQueryable`]. */
|
|
335
|
+
export function execSync(
|
|
336
|
+
db: SyncQueryable,
|
|
337
|
+
name: string,
|
|
338
|
+
text: string,
|
|
339
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
340
|
+
): void {
|
|
341
|
+
executed(db, name, text, params);
|
|
342
|
+
}
|
|
343
|
+
|
|
247
344
|
/** `:execrows` — how many rows the statement changed. */
|
|
248
345
|
export async function execRows(
|
|
249
346
|
db: Queryable,
|
|
@@ -254,6 +351,16 @@ export async function execRows(
|
|
|
254
351
|
return (await run(db, name, text, params)).rowsAffected;
|
|
255
352
|
}
|
|
256
353
|
|
|
354
|
+
/** `:execrows` on a [`SyncQueryable`]. */
|
|
355
|
+
export function execRowsSync(
|
|
356
|
+
db: SyncQueryable,
|
|
357
|
+
name: string,
|
|
358
|
+
text: string,
|
|
359
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
360
|
+
): number {
|
|
361
|
+
return executed(db, name, text, params).rowsAffected;
|
|
362
|
+
}
|
|
363
|
+
|
|
257
364
|
/** What `:execresult` resolves to. */
|
|
258
365
|
export type ExecResult = {|
|
|
259
366
|
readonly rowsAffected: number,
|
|
@@ -271,6 +378,24 @@ export async function execResult(
|
|
|
271
378
|
return { rowsAffected, lastInsertId };
|
|
272
379
|
}
|
|
273
380
|
|
|
381
|
+
/** `:execresult` on a [`SyncQueryable`]. */
|
|
382
|
+
export function execResultSync(
|
|
383
|
+
db: SyncQueryable,
|
|
384
|
+
name: string,
|
|
385
|
+
text: string,
|
|
386
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
387
|
+
): ExecResult {
|
|
388
|
+
const { rowsAffected, lastInsertId } = executed(db, name, text, params);
|
|
389
|
+
return { rowsAffected, lastInsertId };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function insertedId(result: QueryResult, name: string): bigint {
|
|
393
|
+
if (result.lastInsertId === null) {
|
|
394
|
+
throw new SqlError({ kind: "unsupported", feature: ":execlastid on PostgreSQL" }, name);
|
|
395
|
+
}
|
|
396
|
+
return result.lastInsertId;
|
|
397
|
+
}
|
|
398
|
+
|
|
274
399
|
/**
|
|
275
400
|
* `:execlastid` — the id the statement inserted.
|
|
276
401
|
*
|
|
@@ -283,11 +408,17 @@ export async function execLastId(
|
|
|
283
408
|
text: string,
|
|
284
409
|
params: $ReadOnlyArray<SqlParam>,
|
|
285
410
|
): Promise<bigint> {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
411
|
+
return insertedId(await run(db, name, text, params), name);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** `:execlastid` on a [`SyncQueryable`]. */
|
|
415
|
+
export function execLastIdSync(
|
|
416
|
+
db: SyncQueryable,
|
|
417
|
+
name: string,
|
|
418
|
+
text: string,
|
|
419
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
420
|
+
): bigint {
|
|
421
|
+
return insertedId(executed(db, name, text, params), name);
|
|
291
422
|
}
|
|
292
423
|
|
|
293
424
|
// ---------------------------------------------------------------------------
|
|
@@ -363,6 +494,39 @@ export type CopyPlan = {|
|
|
|
363
494
|
/** The most rows one statement carries when a row has no parameters at all. */
|
|
364
495
|
const COPY_ROWS_WITHOUT_PARAMS = 1000;
|
|
365
496
|
|
|
497
|
+
function copyChunk(
|
|
498
|
+
engine: Engine,
|
|
499
|
+
plan: CopyPlan,
|
|
500
|
+
chunk: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
|
|
501
|
+
name: string,
|
|
502
|
+
): {| readonly text: string, readonly params: $ReadOnlyArray<SqlParam> |} {
|
|
503
|
+
const width = plan.refs.length;
|
|
504
|
+
const params: Array<SqlParam> = [];
|
|
505
|
+
const tuples: Array<string> = [];
|
|
506
|
+
for (const row of chunk) {
|
|
507
|
+
let tuple = plan.tuple[0];
|
|
508
|
+
for (let i = 0; i < width; i += 1) {
|
|
509
|
+
const index = plan.refs[i];
|
|
510
|
+
if (index >= row.length) {
|
|
511
|
+
throw new SqlError({ kind: "row", index, length: row.length }, name);
|
|
512
|
+
}
|
|
513
|
+
params.push(row[index]);
|
|
514
|
+
tuple += (engine === "postgresql" ? `$${params.length}` : "?") + plan.tuple[i + 1];
|
|
515
|
+
}
|
|
516
|
+
tuples.push(tuple);
|
|
517
|
+
}
|
|
518
|
+
return { text: plan.head + tuples.join(", ") + plan.tail, params };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function copyWidth(db: { readonly maxParams: number, ... }, plan: CopyPlan, name: string): number {
|
|
522
|
+
const width = plan.refs.length;
|
|
523
|
+
// One row is the smallest statement. Wider than `maxParams`, it cannot be sent.
|
|
524
|
+
if (width > db.maxParams) {
|
|
525
|
+
throw new SqlError({ kind: "params", width, max: db.maxParams }, name);
|
|
526
|
+
}
|
|
527
|
+
return width === 0 ? COPY_ROWS_WITHOUT_PARAMS : Math.max(1, Math.floor(db.maxParams / width));
|
|
528
|
+
}
|
|
529
|
+
|
|
366
530
|
/**
|
|
367
531
|
* `:copyfrom` — insert every row, as few statements as `maxParams` allows.
|
|
368
532
|
*
|
|
@@ -380,33 +544,12 @@ export async function copyFrom(
|
|
|
380
544
|
if (rows.length === 0) {
|
|
381
545
|
return 0;
|
|
382
546
|
}
|
|
383
|
-
const
|
|
384
|
-
// One row is the smallest statement. Wider than `maxParams`, it cannot be sent.
|
|
385
|
-
if (width > db.maxParams) {
|
|
386
|
-
throw new SqlError({ kind: "params", width, max: db.maxParams }, name);
|
|
387
|
-
}
|
|
388
|
-
const perStatement =
|
|
389
|
-
width === 0 ? COPY_ROWS_WITHOUT_PARAMS : Math.max(1, Math.floor(db.maxParams / width));
|
|
547
|
+
const perStatement = copyWidth(db, plan, name);
|
|
390
548
|
const insert = async (q: Queryable): Promise<number> => {
|
|
391
549
|
let total = 0;
|
|
392
550
|
for (let start = 0; start < rows.length; start += perStatement) {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const tuples: Array<string> = [];
|
|
396
|
-
for (const row of chunk) {
|
|
397
|
-
let tuple = plan.tuple[0];
|
|
398
|
-
for (let i = 0; i < width; i += 1) {
|
|
399
|
-
const index = plan.refs[i];
|
|
400
|
-
if (index >= row.length) {
|
|
401
|
-
throw new SqlError({ kind: "row", index, length: row.length }, name);
|
|
402
|
-
}
|
|
403
|
-
params.push(row[index]);
|
|
404
|
-
tuple += (q.engine === "postgresql" ? `$${params.length}` : "?") + plan.tuple[i + 1];
|
|
405
|
-
}
|
|
406
|
-
tuples.push(tuple);
|
|
407
|
-
}
|
|
408
|
-
const text = plan.head + tuples.join(", ") + plan.tail;
|
|
409
|
-
total += (await run(q, name, text, params)).rowsAffected;
|
|
551
|
+
const statement = copyChunk(q.engine, plan, rows.slice(start, start + perStatement), name);
|
|
552
|
+
total += (await run(q, name, statement.text, statement.params)).rowsAffected;
|
|
410
553
|
}
|
|
411
554
|
return total;
|
|
412
555
|
};
|
|
@@ -417,6 +560,32 @@ export async function copyFrom(
|
|
|
417
560
|
return transaction(insert);
|
|
418
561
|
}
|
|
419
562
|
|
|
563
|
+
/** `:copyfrom` on a [`SyncQueryable`]. */
|
|
564
|
+
export function copyFromSync(
|
|
565
|
+
db: SyncQueryable,
|
|
566
|
+
name: string,
|
|
567
|
+
plan: CopyPlan,
|
|
568
|
+
rows: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
|
|
569
|
+
): number {
|
|
570
|
+
if (rows.length === 0) {
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
const perStatement = copyWidth(db, plan, name);
|
|
574
|
+
const insert = (q: SyncQueryable): number => {
|
|
575
|
+
let total = 0;
|
|
576
|
+
for (let start = 0; start < rows.length; start += perStatement) {
|
|
577
|
+
const statement = copyChunk(q.engine, plan, rows.slice(start, start + perStatement), name);
|
|
578
|
+
total += executed(q, name, statement.text, statement.params).rowsAffected;
|
|
579
|
+
}
|
|
580
|
+
return total;
|
|
581
|
+
};
|
|
582
|
+
const transaction = db.transactionSync;
|
|
583
|
+
if (transaction === undefined || rows.length <= perStatement) {
|
|
584
|
+
return insert(db);
|
|
585
|
+
}
|
|
586
|
+
return transaction(insert);
|
|
587
|
+
}
|
|
588
|
+
|
|
420
589
|
// ---------------------------------------------------------------------------
|
|
421
590
|
// `:batchexec`, `:batchmany`, `:batchone`.
|
|
422
591
|
|
|
@@ -447,6 +616,31 @@ export async function batch<A, R>(
|
|
|
447
616
|
return transaction(each);
|
|
448
617
|
}
|
|
449
618
|
|
|
619
|
+
/**
|
|
620
|
+
* [`batch`] on a [`SyncQueryable`].
|
|
621
|
+
*
|
|
622
|
+
* The body returns its value directly. Where `transactionSync` is absent the
|
|
623
|
+
* items still run in order, on this thread.
|
|
624
|
+
*/
|
|
625
|
+
export function batchSync<A, R>(
|
|
626
|
+
db: SyncQueryable,
|
|
627
|
+
items: $ReadOnlyArray<A>,
|
|
628
|
+
body: (q: SyncQueryable, item: A) => R,
|
|
629
|
+
): Array<R> {
|
|
630
|
+
const each = (q: SyncQueryable): Array<R> => {
|
|
631
|
+
const out: Array<R> = [];
|
|
632
|
+
for (const item of items) {
|
|
633
|
+
out.push(body(q, item));
|
|
634
|
+
}
|
|
635
|
+
return out;
|
|
636
|
+
};
|
|
637
|
+
const transaction = db.transactionSync;
|
|
638
|
+
if (transaction === undefined || items.length <= 1) {
|
|
639
|
+
return each(db);
|
|
640
|
+
}
|
|
641
|
+
return transaction(each);
|
|
642
|
+
}
|
|
643
|
+
|
|
450
644
|
// ---------------------------------------------------------------------------
|
|
451
645
|
// Transactions, for adapters.
|
|
452
646
|
|
|
@@ -457,11 +651,20 @@ export type Run = (
|
|
|
457
651
|
mode: QueryMode,
|
|
458
652
|
) => Promise<QueryResult>;
|
|
459
653
|
|
|
654
|
+
/** [`Run`] for a driver that finishes the statement before returning. */
|
|
655
|
+
export type RunSync = (
|
|
656
|
+
text: string,
|
|
657
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
658
|
+
mode: QueryMode,
|
|
659
|
+
) => QueryResult;
|
|
660
|
+
|
|
460
661
|
/** What [`transactionOn`] needs from an adapter. */
|
|
461
662
|
export type Connection = {|
|
|
462
663
|
readonly engine: Engine,
|
|
463
664
|
readonly maxParams: number,
|
|
464
665
|
readonly run: Run,
|
|
666
|
+
/** Present when the driver can also run a statement on the calling thread. */
|
|
667
|
+
readonly runSync?: RunSync,
|
|
465
668
|
/** The statement that opens a transaction; `BEGIN` unless the adapter knows better. */
|
|
466
669
|
readonly begin?: string,
|
|
467
670
|
|};
|
|
@@ -532,32 +735,150 @@ async function scoped<T>(
|
|
|
532
735
|
}
|
|
533
736
|
throw error;
|
|
534
737
|
}
|
|
738
|
+
// A failed COMMIT leaves the transaction open. Roll it back before the
|
|
739
|
+
// error leaves, or the next statement runs inside a transaction this
|
|
740
|
+
// handle has already forgotten. A nested RELEASE fails the same way;
|
|
741
|
+
// `ROLLBACK TO` then `RELEASE` is the body-error path.
|
|
535
742
|
open = false;
|
|
536
|
-
|
|
743
|
+
try {
|
|
744
|
+
await connection.run(commit, [], "exec");
|
|
745
|
+
} catch (error) {
|
|
746
|
+
try {
|
|
747
|
+
await connection.run(rollback, [], "exec");
|
|
748
|
+
if (depth > 0) {
|
|
749
|
+
await connection.run(commit, [], "exec");
|
|
750
|
+
}
|
|
751
|
+
} catch {
|
|
752
|
+
// The rollback failed too — usually because the connection is gone.
|
|
753
|
+
// The error worth reporting is the commit's.
|
|
754
|
+
}
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
537
757
|
return value;
|
|
538
758
|
}
|
|
539
759
|
|
|
540
760
|
/**
|
|
541
|
-
*
|
|
542
|
-
* SQLite database, or an embedded PostgreSQL.
|
|
761
|
+
* [`transactionOn`] for a driver with [`RunSync`].
|
|
543
762
|
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
763
|
+
* The body runs to completion before this returns. `ROLLBACK` runs when it
|
|
764
|
+
* throws, and the original error is rethrown; a failed rollback is swallowed
|
|
765
|
+
* so it cannot hide that error. The [`SyncQueryable`] the body receives
|
|
766
|
+
* refuses statements after this returns.
|
|
547
767
|
*/
|
|
548
|
-
export function
|
|
768
|
+
export function transactionOnSync<T>(connection: Connection, body: (tx: SyncQueryable) => T): T {
|
|
769
|
+
const runSync =
|
|
770
|
+
connection.runSync ?? unsupported("a synchronous transaction on an asynchronous connection");
|
|
771
|
+
runSync(connection.begin ?? "BEGIN", [], "exec");
|
|
772
|
+
return scopedSync(runSync, connection, 0, body, "COMMIT", "ROLLBACK");
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function scopedSync<T>(
|
|
776
|
+
runSync: RunSync,
|
|
777
|
+
connection: Connection,
|
|
778
|
+
depth: number,
|
|
779
|
+
body: (tx: SyncQueryable) => T,
|
|
780
|
+
commit: string,
|
|
781
|
+
rollback: string,
|
|
782
|
+
): T {
|
|
783
|
+
let open = true;
|
|
784
|
+
const guarded: RunSync = (text, params, mode) => {
|
|
785
|
+
if (!open) {
|
|
786
|
+
throw new SqlError({ kind: "closed" });
|
|
787
|
+
}
|
|
788
|
+
return runSync(text, params, mode);
|
|
789
|
+
};
|
|
790
|
+
const tx: SyncQueryable = {
|
|
791
|
+
engine: connection.engine,
|
|
792
|
+
maxParams: connection.maxParams,
|
|
793
|
+
querySync: guarded,
|
|
794
|
+
transactionSync: <U>(inner: (tx: SyncQueryable) => U): U => {
|
|
795
|
+
const savepoint = `uf_sp_${depth + 1}`;
|
|
796
|
+
guarded(`SAVEPOINT ${savepoint}`, [], "exec");
|
|
797
|
+
return scopedSync(
|
|
798
|
+
guarded,
|
|
799
|
+
connection,
|
|
800
|
+
depth + 1,
|
|
801
|
+
inner,
|
|
802
|
+
`RELEASE SAVEPOINT ${savepoint}`,
|
|
803
|
+
`ROLLBACK TO SAVEPOINT ${savepoint}`,
|
|
804
|
+
);
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
let value: T;
|
|
808
|
+
try {
|
|
809
|
+
value = body(tx);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
open = false;
|
|
812
|
+
try {
|
|
813
|
+
runSync(rollback, [], "exec");
|
|
814
|
+
if (depth > 0) {
|
|
815
|
+
// `ROLLBACK TO` leaves the savepoint in place; release it so the
|
|
816
|
+
// enclosing transaction can go on as if the inner one never started.
|
|
817
|
+
runSync(commit, [], "exec");
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
// The rollback failed too — usually because the connection is gone,
|
|
821
|
+
// which rolls back on its own. The error worth reporting is the first.
|
|
822
|
+
}
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
// Same as `scoped`: a failed COMMIT or RELEASE must not clear the
|
|
826
|
+
// transaction while the database still holds it.
|
|
827
|
+
open = false;
|
|
828
|
+
try {
|
|
829
|
+
runSync(commit, [], "exec");
|
|
830
|
+
} catch (error) {
|
|
831
|
+
try {
|
|
832
|
+
runSync(rollback, [], "exec");
|
|
833
|
+
if (depth > 0) {
|
|
834
|
+
runSync(commit, [], "exec");
|
|
835
|
+
}
|
|
836
|
+
} catch {
|
|
837
|
+
// The rollback failed too. The error worth reporting is the commit's.
|
|
838
|
+
}
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
return value;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
type Handle = Queryable & SyncQueryable;
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* One connection serving a whole application: a SQLite database, or an
|
|
848
|
+
* embedded PostgreSQL.
|
|
849
|
+
*
|
|
850
|
+
* While an asynchronous transaction is open, every asynchronous statement on
|
|
851
|
+
* the returned value waits for it to finish, so a request that is not in the
|
|
852
|
+
* transaction never lands inside it. Statements on the transaction's own
|
|
853
|
+
* handle do not wait. A synchronous call during that wait throws, because
|
|
854
|
+
* this thread cannot block until the promise settles.
|
|
855
|
+
*
|
|
856
|
+
* A synchronous transaction does not yield, so nothing else runs until it
|
|
857
|
+
* returns. A call on this outer handle from inside that body throws: waiting
|
|
858
|
+
* for the body to finish is the body itself.
|
|
859
|
+
*/
|
|
860
|
+
function connect(connection: Connection): Handle {
|
|
549
861
|
let gate: Promise<void> | null = null;
|
|
862
|
+
let syncOpen = false;
|
|
863
|
+
const runSync = connection.runSync;
|
|
864
|
+
|
|
550
865
|
const query = async (
|
|
551
866
|
text: string,
|
|
552
867
|
params: $ReadOnlyArray<SqlParam>,
|
|
553
868
|
mode: QueryMode,
|
|
554
869
|
): Promise<QueryResult> => {
|
|
870
|
+
if (syncOpen) {
|
|
871
|
+
unsupported("an asynchronous query while a synchronous transaction is open");
|
|
872
|
+
}
|
|
555
873
|
while (gate !== null) {
|
|
556
874
|
await gate;
|
|
557
875
|
}
|
|
558
876
|
return connection.run(text, params, mode);
|
|
559
877
|
};
|
|
560
878
|
const transaction = async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
|
|
879
|
+
if (syncOpen) {
|
|
880
|
+
unsupported("an asynchronous transaction while a synchronous transaction is open");
|
|
881
|
+
}
|
|
561
882
|
while (gate !== null) {
|
|
562
883
|
await gate;
|
|
563
884
|
}
|
|
@@ -572,5 +893,72 @@ export function singleConnection(connection: Connection): Queryable {
|
|
|
572
893
|
release();
|
|
573
894
|
}
|
|
574
895
|
};
|
|
575
|
-
|
|
896
|
+
const querySync = (
|
|
897
|
+
text: string,
|
|
898
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
899
|
+
mode: QueryMode,
|
|
900
|
+
): QueryResult => {
|
|
901
|
+
const run =
|
|
902
|
+
connection.runSync ?? unsupported("a synchronous query on an asynchronous connection");
|
|
903
|
+
if (gate !== null) {
|
|
904
|
+
unsupported("a synchronous query while an asynchronous transaction is open");
|
|
905
|
+
}
|
|
906
|
+
if (syncOpen) {
|
|
907
|
+
unsupported(
|
|
908
|
+
"a synchronous query on the outer connection while a synchronous transaction is open",
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
return run(text, params, mode);
|
|
912
|
+
};
|
|
913
|
+
const transactionSync = <T>(body: (tx: SyncQueryable) => T): T => {
|
|
914
|
+
if (runSync == null) {
|
|
915
|
+
unsupported("a synchronous transaction on an asynchronous connection");
|
|
916
|
+
}
|
|
917
|
+
if (gate !== null) {
|
|
918
|
+
unsupported("a synchronous transaction while an asynchronous transaction is open");
|
|
919
|
+
}
|
|
920
|
+
if (syncOpen) {
|
|
921
|
+
unsupported(
|
|
922
|
+
"a synchronous transaction on the outer connection while a synchronous transaction is open",
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
syncOpen = true;
|
|
926
|
+
try {
|
|
927
|
+
return transactionOnSync(connection, body);
|
|
928
|
+
} finally {
|
|
929
|
+
syncOpen = false;
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
return {
|
|
933
|
+
engine: connection.engine,
|
|
934
|
+
maxParams: connection.maxParams,
|
|
935
|
+
query,
|
|
936
|
+
transaction,
|
|
937
|
+
querySync,
|
|
938
|
+
transactionSync,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* A [`Queryable`] over one connection.
|
|
944
|
+
*
|
|
945
|
+
* The returned object also has `querySync` and `transactionSync` when
|
|
946
|
+
* `connection.runSync` is set. [`singleConnectionSync`] is that fact in the
|
|
947
|
+
* type, for the SQLite adapters.
|
|
948
|
+
*/
|
|
949
|
+
export function singleConnection(connection: Connection): Queryable {
|
|
950
|
+
return connect(connection);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* [`singleConnection`] typed as both [`Queryable`] and [`SyncQueryable`].
|
|
955
|
+
*
|
|
956
|
+
* Requires `connection.runSync`. Without it this throws, rather than handing
|
|
957
|
+
* back a handle whose synchronous methods fail on first use.
|
|
958
|
+
*/
|
|
959
|
+
export function singleConnectionSync(connection: Connection): Queryable & SyncQueryable {
|
|
960
|
+
if (connection.runSync == null) {
|
|
961
|
+
unsupported("a synchronous connection");
|
|
962
|
+
}
|
|
963
|
+
return connect(connection);
|
|
576
964
|
}
|
package/package.json
CHANGED