@uniflowed/sql 0.14.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 +80 -0
- package/adapters/bun-sqlite.js +73 -0
- package/adapters/d1.js +71 -0
- package/adapters/mysql2.js +140 -0
- package/adapters/node-sqlite.js +93 -0
- package/adapters/pg.js +88 -0
- package/adapters/pglite.js +72 -0
- package/adapters/postgres.js +111 -0
- package/index.js +562 -0
- package/internal/shared.js +191 -0
- package/internal/statements.js +55 -0
- package/mysql.js +231 -0
- package/package.json +33 -0
- package/postgresql.js +558 -0
- package/sqlite.js +178 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/sql/postgres`: the `postgres` package (postgres.js).
|
|
4
|
+
//
|
|
5
|
+
// import postgres from "postgres";
|
|
6
|
+
// import { fromPostgres } from "@uniflowed/sql/postgres";
|
|
7
|
+
//
|
|
8
|
+
// const db = fromPostgres(postgres(connectionString));
|
|
9
|
+
//
|
|
10
|
+
// Queries go through `sql.unsafe(text, params)` — the text is generated SQL
|
|
11
|
+
// with `$n` placeholders, not a tagged template — and `.raw()`, which returns
|
|
12
|
+
// each value as the bytes the server sent, so postgres.js's own parsers never
|
|
13
|
+
// run. Transactions are postgres.js's `begin` and `savepoint`.
|
|
14
|
+
//
|
|
15
|
+
// Its serialisers are kept out the same way. A prepared statement is
|
|
16
|
+
// described first, and postgres.js then serialises each parameter by the type
|
|
17
|
+
// the server inferred: `JSON.stringify` for `jsonb`, `Buffer.from(x)` for
|
|
18
|
+
// `bytea`, `x === true ? "t" : "f"` for `bool` — each of which would corrupt a
|
|
19
|
+
// value the codecs already wrote as text. Declaring every parameter as
|
|
20
|
+
// `unknown` (OID 705) leaves the server to infer the type, as an undeclared
|
|
21
|
+
// one would, and leaves postgres.js nothing to serialise.
|
|
22
|
+
|
|
23
|
+
import { SqlError } from "../index.js";
|
|
24
|
+
import type { Queryable, SqlParam } from "../index.js";
|
|
25
|
+
|
|
26
|
+
type RawRows = Promise<$ReadOnlyArray<mixed> & { readonly count?: number | null, ... }>;
|
|
27
|
+
|
|
28
|
+
type Unsafe = { raw(): RawRows, ... };
|
|
29
|
+
|
|
30
|
+
/** What postgres.js's `Sql` and its transaction handles have in common. */
|
|
31
|
+
export interface PostgresQueries {
|
|
32
|
+
unsafe(
|
|
33
|
+
text: string,
|
|
34
|
+
params: $ReadOnlyArray<mixed>,
|
|
35
|
+
options?: {| readonly prepare?: boolean |},
|
|
36
|
+
): Unsafe;
|
|
37
|
+
typed(value: SqlParam, oid: number): mixed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A transaction handle, as `begin` and `savepoint` pass one. */
|
|
41
|
+
export interface PostgresTransaction extends PostgresQueries {
|
|
42
|
+
savepoint<T>(body: (tx: PostgresTransaction) => Promise<T>): Promise<T>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A postgres.js `Sql` instance. */
|
|
46
|
+
export interface PostgresSql extends PostgresQueries {
|
|
47
|
+
begin<T>(body: (tx: PostgresTransaction) => Promise<T>): Promise<T>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const decoder = new TextDecoder();
|
|
51
|
+
|
|
52
|
+
function text(value: mixed): mixed {
|
|
53
|
+
return value instanceof Uint8Array ? decoder.decode(value) : value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `unknown`: a parameter whose type the server decides. */
|
|
57
|
+
const UNKNOWN = 705;
|
|
58
|
+
|
|
59
|
+
function run(sql: PostgresQueries): Queryable["query"] {
|
|
60
|
+
return async (statement, params) => {
|
|
61
|
+
const typed = params.map((param) => (param === null ? null : sql.typed(param, UNKNOWN)));
|
|
62
|
+
const result = await sql.unsafe(statement, typed, { prepare: true }).raw();
|
|
63
|
+
const rows: Array<$ReadOnlyArray<mixed>> = [];
|
|
64
|
+
for (const row of result) {
|
|
65
|
+
if (Array.isArray(row)) {
|
|
66
|
+
rows.push(row.map(text));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { rows, rowsAffected: result.count ?? 0, lastInsertId: null };
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run `body` on a `Queryable` over the transaction handle `tx`, which stops
|
|
75
|
+
* working once `body` settles.
|
|
76
|
+
*
|
|
77
|
+
* postgres.js keeps answering on a handle after its `begin` or `savepoint`
|
|
78
|
+
* has ended, and a statement run on it then lands outside the transaction.
|
|
79
|
+
* The other adapters refuse such a statement (`transactionOn` does it for
|
|
80
|
+
* them), so this one does too.
|
|
81
|
+
*/
|
|
82
|
+
async function scoped<T>(tx: PostgresTransaction, body: (tx: Queryable) => Promise<T>): Promise<T> {
|
|
83
|
+
let open = true;
|
|
84
|
+
const query = run(tx);
|
|
85
|
+
const queryable: Queryable = {
|
|
86
|
+
engine: "postgresql",
|
|
87
|
+
maxParams: 65535,
|
|
88
|
+
query: (statement, params, mode) =>
|
|
89
|
+
open ? query(statement, params, mode) : Promise.reject(new SqlError({ kind: "closed" })),
|
|
90
|
+
transaction: <U>(inner: (tx: Queryable) => Promise<U>): Promise<U> =>
|
|
91
|
+
open
|
|
92
|
+
? tx.savepoint((savepoint) => scoped(savepoint, inner))
|
|
93
|
+
: Promise.reject(new SqlError({ kind: "closed" })),
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
return await body(queryable);
|
|
97
|
+
} finally {
|
|
98
|
+
open = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A [`Queryable`] over a postgres.js `Sql`. */
|
|
103
|
+
export function fromPostgres(sql: PostgresSql): Queryable {
|
|
104
|
+
return {
|
|
105
|
+
engine: "postgresql",
|
|
106
|
+
maxParams: 65535,
|
|
107
|
+
query: run(sql),
|
|
108
|
+
transaction: <T>(body: (tx: Queryable) => Promise<T>): Promise<T> =>
|
|
109
|
+
sql.begin((tx) => scoped(tx, body)),
|
|
110
|
+
};
|
|
111
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/sql`: the runtime sqlc's Flow target generates code against.
|
|
4
|
+
//
|
|
5
|
+
// Not a query builder and not an ORM. A BFF that reads its data through SQL it
|
|
6
|
+
// wrote gets three things here, and nothing else:
|
|
7
|
+
//
|
|
8
|
+
// * **One interface over every driver.** [`Queryable`] is the whole contract
|
|
9
|
+
// generated code needs: run this text with these parameters, give the rows
|
|
10
|
+
// back positionally. An adapter in `@uniflowed/sql/<driver>` makes a
|
|
11
|
+
// driver's pool, client or database handle into one.
|
|
12
|
+
// * **Exact values.** An adapter hands rows back in the wire's own
|
|
13
|
+
// representation — PostgreSQL's text format, MySQL's text protocol,
|
|
14
|
+
// SQLite's storage classes with safe integers — and the codecs in
|
|
15
|
+
// `@uniflowed/sql/postgresql`, `/mysql` and `/sqlite` decode them by SQL
|
|
16
|
+
// type. So an `int8` is a `bigint` under `pg` and under `postgres` alike,
|
|
17
|
+
// rather than whatever each driver's own parser decided.
|
|
18
|
+
// * **The parts of sqlc's semantics a driver does not have**: `sqlc.slice`
|
|
19
|
+
// expansion, `:copyfrom` as chunked multi-row inserts, `:batch*` in one
|
|
20
|
+
// transaction, and nested transactions as savepoints.
|
|
21
|
+
//
|
|
22
|
+
// docs/sqlc.md is the design record.
|
|
23
|
+
|
|
24
|
+
/** Which SQL dialect a [`Queryable`] speaks, which decides placeholder syntax. */
|
|
25
|
+
export type Engine = "postgresql" | "mysql" | "sqlite";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A parameter as it goes to an adapter.
|
|
29
|
+
*
|
|
30
|
+
* Already encoded by a codec: PostgreSQL and MySQL parameters are text (or
|
|
31
|
+
* bytes) and the server casts them to the type the statement needs, which is
|
|
32
|
+
* the one representation every driver sends unchanged. SQLite parameters are
|
|
33
|
+
* its storage classes.
|
|
34
|
+
*/
|
|
35
|
+
export type SqlParam = null | string | number | bigint | Uint8Array;
|
|
36
|
+
|
|
37
|
+
/** One row, positionally: a join may return two columns called `id`. */
|
|
38
|
+
export type Row = $ReadOnlyArray<mixed>;
|
|
39
|
+
|
|
40
|
+
/** What a statement produced. */
|
|
41
|
+
export type QueryResult = {|
|
|
42
|
+
readonly rows: $ReadOnlyArray<Row>,
|
|
43
|
+
/** Rows inserted, updated or deleted; 0 for a statement that changes none. */
|
|
44
|
+
readonly rowsAffected: number,
|
|
45
|
+
/** MySQL's `LAST_INSERT_ID()` and SQLite's `last_insert_rowid()`; `null` on PostgreSQL. */
|
|
46
|
+
readonly lastInsertId: bigint | null,
|
|
47
|
+
|};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether a statement is run for its rows or for its effect.
|
|
51
|
+
*
|
|
52
|
+
* SQLite drivers have two entry points (`all` and `run`) and only the second
|
|
53
|
+
* reports `changes` and the last row id; the other engines ignore this.
|
|
54
|
+
*/
|
|
55
|
+
export type QueryMode = "rows" | "exec";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A connection, a pool or an open transaction: anything generated code can
|
|
59
|
+
* run a statement on.
|
|
60
|
+
*
|
|
61
|
+
* `transaction` is absent where the platform has no interactive transactions
|
|
62
|
+
* (Cloudflare D1). Inside a transaction it opens a savepoint.
|
|
63
|
+
*/
|
|
64
|
+
export type Queryable = {
|
|
65
|
+
readonly engine: Engine,
|
|
66
|
+
/** The most bound parameters one statement may carry; `:copyfrom` chunks under it. */
|
|
67
|
+
readonly maxParams: number,
|
|
68
|
+
query(text: string, params: $ReadOnlyArray<SqlParam>, mode: QueryMode): Promise<QueryResult>,
|
|
69
|
+
readonly transaction?: <T>(body: (tx: Queryable) => Promise<T>) => Promise<T>,
|
|
70
|
+
...
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** A JSON document, as `json` and `jsonb` columns decode to. */
|
|
74
|
+
export type JsonValue =
|
|
75
|
+
| null
|
|
76
|
+
| boolean
|
|
77
|
+
| number
|
|
78
|
+
| string
|
|
79
|
+
| $ReadOnlyArray<JsonValue>
|
|
80
|
+
| { readonly [string]: JsonValue };
|
|
81
|
+
|
|
82
|
+
/** What went wrong, as a value. */
|
|
83
|
+
export type SqlFailure =
|
|
84
|
+
| {| readonly kind: "decode", readonly expected: string, readonly value: mixed |}
|
|
85
|
+
| {| readonly kind: "encode", readonly expected: string, readonly value: mixed |}
|
|
86
|
+
| {| readonly kind: "shape", readonly expected: number, readonly actual: number |}
|
|
87
|
+
| {| readonly kind: "closed" |}
|
|
88
|
+
| {| readonly kind: "unsupported", readonly feature: string |};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A value that did not fit its column, a row of the wrong width, or a
|
|
92
|
+
* transaction used after it ended.
|
|
93
|
+
*
|
|
94
|
+
* Never a database error: those are the driver's, and are rethrown untouched
|
|
95
|
+
* so that a caller matching on `code: "23505"` still can.
|
|
96
|
+
*/
|
|
97
|
+
export class SqlError extends Error {
|
|
98
|
+
readonly failure: SqlFailure;
|
|
99
|
+
/** The generated query that was running, when there was one. */
|
|
100
|
+
query: string | null;
|
|
101
|
+
|
|
102
|
+
constructor(failure: SqlFailure, query: string | null = null) {
|
|
103
|
+
super(explain(failure, query));
|
|
104
|
+
this.name = "SqlError";
|
|
105
|
+
this.failure = failure;
|
|
106
|
+
this.query = query;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function show(value: mixed): string {
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
return JSON.stringify(value.length > 64 ? `${value.slice(0, 64)}…` : value);
|
|
113
|
+
}
|
|
114
|
+
if (typeof value === "bigint") {
|
|
115
|
+
return `${value}n`;
|
|
116
|
+
}
|
|
117
|
+
if (value instanceof Uint8Array) {
|
|
118
|
+
return `<${value.length} bytes>`;
|
|
119
|
+
}
|
|
120
|
+
return String(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function explain(failure: SqlFailure, query: string | null): string {
|
|
124
|
+
const where = query === null ? "" : `${query}: `;
|
|
125
|
+
return (
|
|
126
|
+
where +
|
|
127
|
+
match (failure) {
|
|
128
|
+
{kind: "decode", expected: const expected, value: const value} =>
|
|
129
|
+
`expected ${expected} from the database, got ${show(value)}`,
|
|
130
|
+
{kind: "encode", expected: const expected, value: const value} =>
|
|
131
|
+
`expected ${expected} as a parameter, got ${show(value)}`,
|
|
132
|
+
{kind: "shape", expected: const expected, actual: const actual} =>
|
|
133
|
+
`the adapter returned ${actual} columns where the query has ${expected}; ` +
|
|
134
|
+
"regenerate with `uf sqlc generate`, or check that the adapter returns rows as arrays",
|
|
135
|
+
{kind: "closed"} => "the transaction has already committed or rolled back",
|
|
136
|
+
{kind: "unsupported", feature: const feature} =>
|
|
137
|
+
`${feature} is not supported by this adapter`,
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Throw a decode failure. For codecs. */
|
|
143
|
+
export function decodeError(expected: string, value: mixed): empty {
|
|
144
|
+
throw new SqlError({ kind: "decode", expected, value });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Throw an encode failure. For codecs. */
|
|
148
|
+
export function encodeError(expected: string, value: mixed): empty {
|
|
149
|
+
throw new SqlError({ kind: "encode", expected, value });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function attribute(error: mixed, name: string): mixed {
|
|
153
|
+
if (error instanceof SqlError && error.query === null) {
|
|
154
|
+
error.query = name;
|
|
155
|
+
error.message = explain(error.failure, name);
|
|
156
|
+
}
|
|
157
|
+
return error;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function checkWidth(row: Row, width: number): void {
|
|
161
|
+
if (row.length !== width) {
|
|
162
|
+
throw new SqlError({ kind: "shape", expected: width, actual: row.length });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Running a generated query.
|
|
168
|
+
//
|
|
169
|
+
// `name` is the query's name in the SQL file, for error messages; `width` is
|
|
170
|
+
// how many columns the generator saw, which a stale schema or an adapter that
|
|
171
|
+
// returns objects instead of arrays would get wrong.
|
|
172
|
+
|
|
173
|
+
/** `:one` — the first row, or `null`. */
|
|
174
|
+
export async function one<T>(
|
|
175
|
+
db: Queryable,
|
|
176
|
+
name: string,
|
|
177
|
+
text: string,
|
|
178
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
179
|
+
width: number,
|
|
180
|
+
decode: (row: Row) => T,
|
|
181
|
+
): Promise<T | null> {
|
|
182
|
+
try {
|
|
183
|
+
const { rows } = await db.query(text, params, "rows");
|
|
184
|
+
if (rows.length === 0) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const row = rows[0];
|
|
188
|
+
checkWidth(row, width);
|
|
189
|
+
return decode(row);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw attribute(error, name);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `:many` — every row. */
|
|
196
|
+
export async function many<T>(
|
|
197
|
+
db: Queryable,
|
|
198
|
+
name: string,
|
|
199
|
+
text: string,
|
|
200
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
201
|
+
width: number,
|
|
202
|
+
decode: (row: Row) => T,
|
|
203
|
+
): Promise<Array<T>> {
|
|
204
|
+
try {
|
|
205
|
+
const { rows } = await db.query(text, params, "rows");
|
|
206
|
+
const out: Array<T> = new Array(rows.length);
|
|
207
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
208
|
+
const row = rows[i];
|
|
209
|
+
checkWidth(row, width);
|
|
210
|
+
out[i] = decode(row);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw attribute(error, name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function run(
|
|
219
|
+
db: Queryable,
|
|
220
|
+
name: string,
|
|
221
|
+
text: string,
|
|
222
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
223
|
+
): Promise<QueryResult> {
|
|
224
|
+
try {
|
|
225
|
+
return await db.query(text, params, "exec");
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw attribute(error, name);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** `:exec`. */
|
|
232
|
+
export async function exec(
|
|
233
|
+
db: Queryable,
|
|
234
|
+
name: string,
|
|
235
|
+
text: string,
|
|
236
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
await run(db, name, text, params);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** `:execrows` — how many rows the statement changed. */
|
|
242
|
+
export async function execRows(
|
|
243
|
+
db: Queryable,
|
|
244
|
+
name: string,
|
|
245
|
+
text: string,
|
|
246
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
247
|
+
): Promise<number> {
|
|
248
|
+
return (await run(db, name, text, params)).rowsAffected;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** What `:execresult` resolves to. */
|
|
252
|
+
export type ExecResult = {|
|
|
253
|
+
readonly rowsAffected: number,
|
|
254
|
+
readonly lastInsertId: bigint | null,
|
|
255
|
+
|};
|
|
256
|
+
|
|
257
|
+
/** `:execresult` — the affected-row count and, on MySQL and SQLite, the last id. */
|
|
258
|
+
export async function execResult(
|
|
259
|
+
db: Queryable,
|
|
260
|
+
name: string,
|
|
261
|
+
text: string,
|
|
262
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
263
|
+
): Promise<ExecResult> {
|
|
264
|
+
const { rowsAffected, lastInsertId } = await run(db, name, text, params);
|
|
265
|
+
return { rowsAffected, lastInsertId };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* `:execlastid` — the id the statement inserted.
|
|
270
|
+
*
|
|
271
|
+
* PostgreSQL has no such thing; sqlc's answer there is `RETURNING id` with
|
|
272
|
+
* `:one`, and this throws rather than resolve to a made-up zero.
|
|
273
|
+
*/
|
|
274
|
+
export async function execLastId(
|
|
275
|
+
db: Queryable,
|
|
276
|
+
name: string,
|
|
277
|
+
text: string,
|
|
278
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
279
|
+
): Promise<bigint> {
|
|
280
|
+
const { lastInsertId } = await run(db, name, text, params);
|
|
281
|
+
if (lastInsertId === null) {
|
|
282
|
+
throw new SqlError({ kind: "unsupported", feature: ":execlastid on PostgreSQL" }, name);
|
|
283
|
+
}
|
|
284
|
+
return lastInsertId;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// `sqlc.slice` on MySQL and SQLite.
|
|
289
|
+
//
|
|
290
|
+
// PostgreSQL passes a slice as one array parameter. The other two have no
|
|
291
|
+
// arrays, so sqlc writes `IN (/*SLICE:ids*/?)` and leaves the expansion to the
|
|
292
|
+
// generated code. The generator splits the text at those markers, so that
|
|
293
|
+
// nothing here has to find a `?` inside a string literal at run time.
|
|
294
|
+
|
|
295
|
+
/** A `sqlc.slice` argument, already encoded element by element. */
|
|
296
|
+
export class Slice {
|
|
297
|
+
readonly values: $ReadOnlyArray<SqlParam>;
|
|
298
|
+
|
|
299
|
+
constructor(values: $ReadOnlyArray<SqlParam>) {
|
|
300
|
+
this.values = values;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Wrap an encoded list as one `sqlc.slice` argument. */
|
|
305
|
+
export function slice(values: $ReadOnlyArray<SqlParam>): Slice {
|
|
306
|
+
return new Slice(values);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Join `parts` around the slices in `values`, in order.
|
|
311
|
+
*
|
|
312
|
+
* `values` are the statement's parameters in placeholder order; each [`Slice`]
|
|
313
|
+
* sits where a marker was, between `parts[k]` and `parts[k + 1]`. A non-empty
|
|
314
|
+
* slice becomes `?, ?, …`; an empty one becomes `NULL`, so `x IN (NULL)` is
|
|
315
|
+
* false for every row — sqlc's own semantics, and what a list of nothing
|
|
316
|
+
* should match.
|
|
317
|
+
*/
|
|
318
|
+
export function expand(
|
|
319
|
+
parts: $ReadOnlyArray<string>,
|
|
320
|
+
values: $ReadOnlyArray<SqlParam | Slice>,
|
|
321
|
+
): {| readonly text: string, readonly params: $ReadOnlyArray<SqlParam> |} {
|
|
322
|
+
const params: Array<SqlParam> = [];
|
|
323
|
+
let text = parts[0];
|
|
324
|
+
let next = 1;
|
|
325
|
+
for (const value of values) {
|
|
326
|
+
if (value instanceof Slice) {
|
|
327
|
+
text += value.values.length === 0 ? "NULL" : "?, ".repeat(value.values.length - 1) + "?";
|
|
328
|
+
text += parts[next];
|
|
329
|
+
next += 1;
|
|
330
|
+
for (const item of value.values) {
|
|
331
|
+
params.push(item);
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
params.push(value);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return { text, params };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// `:copyfrom`.
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* An `INSERT … VALUES (…)` statement split so it can be repeated per row.
|
|
345
|
+
*
|
|
346
|
+
* `tuple` is the parenthesised group around its placeholders and `refs` says
|
|
347
|
+
* which field of a row each placeholder takes, so `(owner_id, name)` with
|
|
348
|
+
* `VALUES ($1, $2)` is `tuple: ["(", ", ", ")"]`, `refs: [0, 1]`.
|
|
349
|
+
*/
|
|
350
|
+
export type CopyPlan = {|
|
|
351
|
+
readonly head: string,
|
|
352
|
+
readonly tuple: $ReadOnlyArray<string>,
|
|
353
|
+
readonly refs: $ReadOnlyArray<number>,
|
|
354
|
+
readonly tail: string,
|
|
355
|
+
|};
|
|
356
|
+
|
|
357
|
+
/** The most rows one statement carries when a row has no parameters at all. */
|
|
358
|
+
const COPY_ROWS_WITHOUT_PARAMS = 1000;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* `:copyfrom` — insert every row, as few statements as `maxParams` allows.
|
|
362
|
+
*
|
|
363
|
+
* Inside one transaction when the `Queryable` can open one, so a failure in
|
|
364
|
+
* the third chunk does not leave the first two behind: pgx's `CopyFrom` is one
|
|
365
|
+
* `COPY`, and a caller of the generated function should not have to know it is
|
|
366
|
+
* now several statements. Resolves to the number of rows inserted.
|
|
367
|
+
*/
|
|
368
|
+
export async function copyFrom(
|
|
369
|
+
db: Queryable,
|
|
370
|
+
name: string,
|
|
371
|
+
plan: CopyPlan,
|
|
372
|
+
rows: $ReadOnlyArray<$ReadOnlyArray<SqlParam>>,
|
|
373
|
+
): Promise<number> {
|
|
374
|
+
if (rows.length === 0) {
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
const width = plan.refs.length;
|
|
378
|
+
const perStatement =
|
|
379
|
+
width === 0 ? COPY_ROWS_WITHOUT_PARAMS : Math.max(1, Math.floor(db.maxParams / width));
|
|
380
|
+
const insert = async (q: Queryable): Promise<number> => {
|
|
381
|
+
let total = 0;
|
|
382
|
+
for (let start = 0; start < rows.length; start += perStatement) {
|
|
383
|
+
const chunk = rows.slice(start, start + perStatement);
|
|
384
|
+
const params: Array<SqlParam> = [];
|
|
385
|
+
const tuples: Array<string> = [];
|
|
386
|
+
for (const row of chunk) {
|
|
387
|
+
let tuple = plan.tuple[0];
|
|
388
|
+
for (let i = 0; i < width; i += 1) {
|
|
389
|
+
params.push(row[plan.refs[i]]);
|
|
390
|
+
tuple += (q.engine === "postgresql" ? `$${params.length}` : "?") + plan.tuple[i + 1];
|
|
391
|
+
}
|
|
392
|
+
tuples.push(tuple);
|
|
393
|
+
}
|
|
394
|
+
const text = plan.head + tuples.join(", ") + plan.tail;
|
|
395
|
+
total += (await run(q, name, text, params)).rowsAffected;
|
|
396
|
+
}
|
|
397
|
+
return total;
|
|
398
|
+
};
|
|
399
|
+
const transaction = db.transaction;
|
|
400
|
+
if (transaction === undefined || rows.length <= perStatement) {
|
|
401
|
+
return insert(db);
|
|
402
|
+
}
|
|
403
|
+
return transaction(insert);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// `:batchexec`, `:batchmany`, `:batchone`.
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Run `body` once per item, in order, inside one transaction when there is
|
|
411
|
+
* one to open.
|
|
412
|
+
*
|
|
413
|
+
* pgx sends a batch as one implicit transaction; this keeps the atomicity and
|
|
414
|
+
* the order without pgx. Where `transaction` is absent (D1) the items run in
|
|
415
|
+
* order without one, and the docs say so.
|
|
416
|
+
*/
|
|
417
|
+
export async function batch<A, R>(
|
|
418
|
+
db: Queryable,
|
|
419
|
+
items: $ReadOnlyArray<A>,
|
|
420
|
+
body: (q: Queryable, item: A) => Promise<R>,
|
|
421
|
+
): Promise<Array<R>> {
|
|
422
|
+
const each = async (q: Queryable): Promise<Array<R>> => {
|
|
423
|
+
const out: Array<R> = [];
|
|
424
|
+
for (const item of items) {
|
|
425
|
+
out.push(await body(q, item));
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
};
|
|
429
|
+
const transaction = db.transaction;
|
|
430
|
+
if (transaction === undefined || items.length <= 1) {
|
|
431
|
+
return each(db);
|
|
432
|
+
}
|
|
433
|
+
return transaction(each);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// Transactions, for adapters.
|
|
438
|
+
|
|
439
|
+
/** How an adapter runs one statement on one connection. */
|
|
440
|
+
export type Run = (
|
|
441
|
+
text: string,
|
|
442
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
443
|
+
mode: QueryMode,
|
|
444
|
+
) => Promise<QueryResult>;
|
|
445
|
+
|
|
446
|
+
/** What [`transactionOn`] needs from an adapter. */
|
|
447
|
+
export type Connection = {|
|
|
448
|
+
readonly engine: Engine,
|
|
449
|
+
readonly maxParams: number,
|
|
450
|
+
readonly run: Run,
|
|
451
|
+
/** The statement that opens a transaction; `BEGIN` unless the adapter knows better. */
|
|
452
|
+
readonly begin?: string,
|
|
453
|
+
|};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Run `body` in a transaction on `connection`, which must be a single
|
|
457
|
+
* connection nobody else is using until this settles.
|
|
458
|
+
*
|
|
459
|
+
* Commits when `body` resolves and rolls back when it throws, rethrowing what
|
|
460
|
+
* it threw. The [`Queryable`] `body` receives stops working once this
|
|
461
|
+
* settles — a statement run on it afterwards would otherwise land outside the
|
|
462
|
+
* transaction without anyone noticing. Its own `transaction` opens a
|
|
463
|
+
* savepoint.
|
|
464
|
+
*/
|
|
465
|
+
export async function transactionOn<T>(
|
|
466
|
+
connection: Connection,
|
|
467
|
+
body: (tx: Queryable) => Promise<T>,
|
|
468
|
+
): Promise<T> {
|
|
469
|
+
await connection.run(connection.begin ?? "BEGIN", [], "exec");
|
|
470
|
+
return scoped(connection, 0, body, "COMMIT", "ROLLBACK");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function scoped<T>(
|
|
474
|
+
connection: Connection,
|
|
475
|
+
depth: number,
|
|
476
|
+
body: (tx: Queryable) => Promise<T>,
|
|
477
|
+
commit: string,
|
|
478
|
+
rollback: string,
|
|
479
|
+
): Promise<T> {
|
|
480
|
+
let open = true;
|
|
481
|
+
const guarded: Run = (text, params, mode) => {
|
|
482
|
+
if (!open) {
|
|
483
|
+
return Promise.reject(new SqlError({ kind: "closed" }));
|
|
484
|
+
}
|
|
485
|
+
return connection.run(text, params, mode);
|
|
486
|
+
};
|
|
487
|
+
const tx: Queryable = {
|
|
488
|
+
engine: connection.engine,
|
|
489
|
+
maxParams: connection.maxParams,
|
|
490
|
+
query: guarded,
|
|
491
|
+
transaction: async <U>(inner: (tx: Queryable) => Promise<U>): Promise<U> => {
|
|
492
|
+
const savepoint = `uf_sp_${depth + 1}`;
|
|
493
|
+
await guarded(`SAVEPOINT ${savepoint}`, [], "exec");
|
|
494
|
+
return scoped(
|
|
495
|
+
{ ...connection, run: guarded },
|
|
496
|
+
depth + 1,
|
|
497
|
+
inner,
|
|
498
|
+
`RELEASE SAVEPOINT ${savepoint}`,
|
|
499
|
+
`ROLLBACK TO SAVEPOINT ${savepoint}`,
|
|
500
|
+
);
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
let value: T;
|
|
504
|
+
try {
|
|
505
|
+
value = await body(tx);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
open = false;
|
|
508
|
+
try {
|
|
509
|
+
await connection.run(rollback, [], "exec");
|
|
510
|
+
if (depth > 0) {
|
|
511
|
+
// `ROLLBACK TO` leaves the savepoint in place; release it so the
|
|
512
|
+
// enclosing transaction can go on as if the inner one never started.
|
|
513
|
+
await connection.run(commit, [], "exec");
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
// The rollback failed too — usually because the connection is gone,
|
|
517
|
+
// which rolls back on its own. The error worth reporting is the first.
|
|
518
|
+
}
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
open = false;
|
|
522
|
+
await connection.run(commit, [], "exec");
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* A [`Queryable`] over one connection that serves a whole application: a
|
|
528
|
+
* SQLite database, or an embedded PostgreSQL.
|
|
529
|
+
*
|
|
530
|
+
* While a transaction is open every statement run on the returned value waits
|
|
531
|
+
* for it to finish, so a request that is not in the transaction never lands
|
|
532
|
+
* inside it. Statements run on the transaction's own [`Queryable`] do not wait.
|
|
533
|
+
*/
|
|
534
|
+
export function singleConnection(connection: Connection): Queryable {
|
|
535
|
+
let gate: Promise<void> | null = null;
|
|
536
|
+
const query = async (
|
|
537
|
+
text: string,
|
|
538
|
+
params: $ReadOnlyArray<SqlParam>,
|
|
539
|
+
mode: QueryMode,
|
|
540
|
+
): Promise<QueryResult> => {
|
|
541
|
+
while (gate !== null) {
|
|
542
|
+
await gate;
|
|
543
|
+
}
|
|
544
|
+
return connection.run(text, params, mode);
|
|
545
|
+
};
|
|
546
|
+
const transaction = async <T>(body: (tx: Queryable) => Promise<T>): Promise<T> => {
|
|
547
|
+
while (gate !== null) {
|
|
548
|
+
await gate;
|
|
549
|
+
}
|
|
550
|
+
let release: () => void = () => {};
|
|
551
|
+
gate = new Promise((resolve) => {
|
|
552
|
+
release = resolve;
|
|
553
|
+
});
|
|
554
|
+
try {
|
|
555
|
+
return await transactionOn(connection, body);
|
|
556
|
+
} finally {
|
|
557
|
+
gate = null;
|
|
558
|
+
release();
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
return { engine: connection.engine, maxParams: connection.maxParams, query, transaction };
|
|
562
|
+
}
|