@fadhilp/stateql 0.1.2 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -20
- package/dist/src/adapters.d.ts +15 -1
- package/dist/src/adapters.js +631 -181
- package/dist/src/cli.js +16 -3
- package/dist/src/connection.d.ts +10 -0
- package/dist/src/connection.js +51 -0
- package/dist/src/errors.js +6 -2
- package/dist/src/filter.d.ts +14 -0
- package/dist/src/filter.js +256 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/response-data.d.ts +8 -0
- package/dist/src/response-data.js +66 -0
- package/dist/src/sql.js +8 -2
- package/dist/src/sqlite-process.d.ts +1 -0
- package/dist/src/sqlite-process.js +240 -0
- package/dist/src/stateql.d.ts +15 -5
- package/dist/src/stateql.js +189 -397
- package/dist/src/store.d.ts +2 -0
- package/dist/src/store.js +37 -2
- package/dist/src/types.d.ts +56 -6
- package/package.json +5 -2
package/dist/src/adapters.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { createConnection as createMySqlConnection, } from "mysql2";
|
|
3
3
|
import { Client, types as pgTypes } from "pg";
|
|
4
|
-
import {
|
|
4
|
+
import { parseJson, toJsonSafe } from "./util.js";
|
|
5
|
+
export class AdapterExecutionError extends Error {
|
|
6
|
+
reason;
|
|
7
|
+
outcomeUnknown;
|
|
8
|
+
constructor(message, reason, outcomeUnknown) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
this.outcomeUnknown = outcomeUnknown;
|
|
12
|
+
this.name = "AdapterExecutionError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
5
15
|
export class BatchWriteError extends Error {
|
|
6
16
|
outcomeUnknown;
|
|
7
17
|
constructor(message, outcomeUnknown) {
|
|
@@ -10,7 +20,21 @@ export class BatchWriteError extends Error {
|
|
|
10
20
|
this.name = "BatchWriteError";
|
|
11
21
|
}
|
|
12
22
|
}
|
|
13
|
-
export
|
|
23
|
+
export class AdapterWriteError extends Error {
|
|
24
|
+
outcomeUnknown;
|
|
25
|
+
constructor(message, outcomeUnknown) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.outcomeUnknown = outcomeUnknown;
|
|
28
|
+
this.name = "AdapterWriteError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function createAdapterContext(timeoutMs, signal) {
|
|
32
|
+
return {
|
|
33
|
+
deadline: Date.now() + timeoutMs,
|
|
34
|
+
...(signal ? { signal } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function createAdapter(connection, context) {
|
|
14
38
|
const source = connection.secret_env
|
|
15
39
|
? process.env[connection.secret_env]
|
|
16
40
|
: connection.source;
|
|
@@ -18,201 +42,451 @@ export async function createAdapter(connection) {
|
|
|
18
42
|
throw new Error(`Environment variable ${connection.secret_env ?? "(missing)"} is not set.`);
|
|
19
43
|
}
|
|
20
44
|
if (connection.driver === "sqlite") {
|
|
21
|
-
return new SQLiteAdapter(source, Boolean(connection.read_only));
|
|
45
|
+
return new SQLiteAdapter(source, Boolean(connection.read_only), context);
|
|
22
46
|
}
|
|
23
|
-
|
|
47
|
+
if (connection.driver === "postgres") {
|
|
48
|
+
return new PostgresAdapter(source, Boolean(connection.read_only), context);
|
|
49
|
+
}
|
|
50
|
+
return new MySqlAdapter(source, Boolean(connection.read_only), context);
|
|
24
51
|
}
|
|
25
52
|
class SQLiteAdapter {
|
|
26
53
|
source;
|
|
27
54
|
readOnly;
|
|
55
|
+
context;
|
|
28
56
|
confidence = "database_reported";
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
child;
|
|
58
|
+
pending = new Map();
|
|
59
|
+
nextId = 1;
|
|
60
|
+
closed = false;
|
|
61
|
+
constructor(source, readOnly, context) {
|
|
31
62
|
this.source = source;
|
|
32
63
|
this.readOnly = readOnly;
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
this.context = context;
|
|
65
|
+
this.child = fork(new URL("./sqlite-process.js", import.meta.url), [], {
|
|
66
|
+
execArgv: [],
|
|
67
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
68
|
+
serialization: "advanced",
|
|
69
|
+
});
|
|
70
|
+
this.child.on("message", (message) => {
|
|
71
|
+
const call = this.pending.get(message.id);
|
|
72
|
+
if (!call)
|
|
73
|
+
return;
|
|
74
|
+
if (message.error) {
|
|
75
|
+
call.reject(call.batch
|
|
76
|
+
? new BatchWriteError(message.error.message, message.error.outcomeUnknown ?? true)
|
|
77
|
+
: message.error.outcomeUnknown === undefined
|
|
78
|
+
? new Error(message.error.message)
|
|
79
|
+
: new AdapterWriteError(message.error.message, message.error.outcomeUnknown));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
call.resolve(message.result);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
this.child.on("error", (error) => this.failPending(error));
|
|
86
|
+
this.child.on("exit", (code, signal) => {
|
|
87
|
+
if (!this.closed) {
|
|
88
|
+
this.failPending(new Error(`SQLite execution process exited unexpectedly (${signal ?? code ?? "unknown"}).`));
|
|
89
|
+
}
|
|
36
90
|
});
|
|
37
|
-
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
38
91
|
}
|
|
39
92
|
async read(sql, params) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
93
|
+
return this.call("read", [sql, params], false, false);
|
|
94
|
+
}
|
|
95
|
+
async write(sql, params) {
|
|
96
|
+
return this.call("write", [sql, params], true, false);
|
|
97
|
+
}
|
|
98
|
+
async writeBatch(operations, isolation) {
|
|
99
|
+
return this.call("writeBatch", [operations, isolation], true, true);
|
|
100
|
+
}
|
|
101
|
+
async signature() {
|
|
102
|
+
return this.call("signature", [], false, false);
|
|
103
|
+
}
|
|
104
|
+
async inspect(kind, table) {
|
|
105
|
+
return this.call("inspect", [kind, table], false, false);
|
|
106
|
+
}
|
|
107
|
+
async close() {
|
|
108
|
+
if (this.closed)
|
|
109
|
+
return;
|
|
110
|
+
this.closed = true;
|
|
111
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null)
|
|
112
|
+
return;
|
|
113
|
+
await new Promise((resolve) => {
|
|
114
|
+
const done = () => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
resolve();
|
|
117
|
+
};
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
try {
|
|
120
|
+
this.child.kill("SIGKILL");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Process already exited.
|
|
124
|
+
}
|
|
125
|
+
resolve();
|
|
126
|
+
}, 1_000);
|
|
127
|
+
timer.unref();
|
|
128
|
+
this.child.once("exit", done);
|
|
129
|
+
try {
|
|
130
|
+
this.child.disconnect();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
try {
|
|
134
|
+
this.child.kill("SIGKILL");
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Process already exited.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async call(operation, args, outcomeUnknown, batch) {
|
|
143
|
+
throwIfStopped(this.context, false);
|
|
144
|
+
if (this.closed)
|
|
145
|
+
throw new Error("SQLite adapter is closed.");
|
|
146
|
+
const id = this.nextId++;
|
|
147
|
+
const result = new Promise((resolve, reject) => {
|
|
148
|
+
this.pending.set(id, { batch, resolve, reject });
|
|
149
|
+
try {
|
|
150
|
+
this.child.send({
|
|
151
|
+
id,
|
|
152
|
+
source: this.source,
|
|
153
|
+
readOnly: this.readOnly,
|
|
154
|
+
operation,
|
|
155
|
+
args,
|
|
156
|
+
busyTimeoutMs: Math.min(5_000, remainingMilliseconds(this.context)),
|
|
157
|
+
}, (error) => {
|
|
158
|
+
if (error)
|
|
159
|
+
reject(error);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
reject(error);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
return (await withContext(result, this.context, () => this.terminate(), outcomeUnknown));
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
this.pending.delete(id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
terminate() {
|
|
174
|
+
if (this.closed)
|
|
175
|
+
return;
|
|
176
|
+
this.closed = true;
|
|
177
|
+
try {
|
|
178
|
+
this.child.kill("SIGKILL");
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Process already exited.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
failPending(error) {
|
|
185
|
+
for (const call of this.pending.values())
|
|
186
|
+
call.reject(error);
|
|
187
|
+
this.pending.clear();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
class PostgresAdapter {
|
|
191
|
+
readOnly;
|
|
192
|
+
context;
|
|
193
|
+
confidence = "ttl_based";
|
|
194
|
+
client;
|
|
195
|
+
connected = false;
|
|
196
|
+
ending;
|
|
197
|
+
constructor(source, readOnly, context) {
|
|
198
|
+
this.readOnly = readOnly;
|
|
199
|
+
this.context = context;
|
|
200
|
+
const timeout = Math.min(2_147_483_647, remainingMilliseconds(context));
|
|
201
|
+
this.client = new Client({
|
|
202
|
+
connectionString: source,
|
|
203
|
+
connectionTimeoutMillis: timeout,
|
|
204
|
+
statement_timeout: timeout,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async read(sql, params) {
|
|
208
|
+
await this.connect();
|
|
209
|
+
await this.query("BEGIN READ ONLY", [], false);
|
|
210
|
+
try {
|
|
211
|
+
await this.setLocalDeadline();
|
|
212
|
+
const result = await this.query(sql, postgresParams(params), false);
|
|
213
|
+
await this.query("COMMIT", [], false);
|
|
214
|
+
return {
|
|
215
|
+
rows: toJsonSafe(result.rows),
|
|
216
|
+
columns: result.fields.map((field) => ({
|
|
217
|
+
name: field.name,
|
|
218
|
+
type: pgTypes.getTypeParser(field.dataTypeID).name ||
|
|
219
|
+
`oid_${field.dataTypeID}`,
|
|
220
|
+
})),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
await this.rollbackQuietly();
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
49
227
|
}
|
|
50
228
|
async write(sql, params) {
|
|
51
229
|
if (this.readOnly)
|
|
52
230
|
throw new Error("Connection is read-only.");
|
|
53
|
-
|
|
54
|
-
|
|
231
|
+
try {
|
|
232
|
+
await this.connect();
|
|
233
|
+
await this.query("BEGIN", [], false);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error instanceof AdapterExecutionError)
|
|
237
|
+
throw error;
|
|
238
|
+
throw new AdapterWriteError(errorText(error), false);
|
|
239
|
+
}
|
|
240
|
+
let committing = false;
|
|
241
|
+
try {
|
|
242
|
+
await this.setLocalDeadline();
|
|
243
|
+
const result = await this.query(sql, postgresParams(params), true);
|
|
244
|
+
committing = true;
|
|
245
|
+
await this.query("COMMIT", [], true);
|
|
246
|
+
return { affectedRows: result.rowCount ?? 0 };
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const rolledBack = await this.rollbackQuietly();
|
|
250
|
+
if (!committing && rolledBack) {
|
|
251
|
+
if (error instanceof AdapterExecutionError) {
|
|
252
|
+
throw new AdapterExecutionError(error.message, error.reason, false);
|
|
253
|
+
}
|
|
254
|
+
throw new AdapterWriteError(errorText(error), false);
|
|
255
|
+
}
|
|
256
|
+
if (error instanceof AdapterExecutionError)
|
|
257
|
+
throw error;
|
|
258
|
+
throw new AdapterWriteError(errorText(error), true);
|
|
259
|
+
}
|
|
55
260
|
}
|
|
56
261
|
async writeBatch(operations, isolation) {
|
|
57
262
|
if (this.readOnly)
|
|
58
263
|
throw new Error("Connection is read-only.");
|
|
59
|
-
|
|
60
|
-
|
|
264
|
+
await this.connect();
|
|
265
|
+
const level = isolation.toUpperCase();
|
|
266
|
+
if (!POSTGRES_ISOLATION_LEVELS.has(level)) {
|
|
267
|
+
throw new Error(`Unsupported PostgreSQL isolation level "${isolation}".`);
|
|
61
268
|
}
|
|
62
|
-
const results = [];
|
|
63
269
|
try {
|
|
64
|
-
this.
|
|
270
|
+
await this.query(`BEGIN ISOLATION LEVEL ${level}`, [], false);
|
|
65
271
|
}
|
|
66
272
|
catch (error) {
|
|
67
273
|
throw new BatchWriteError(errorText(error), false);
|
|
68
274
|
}
|
|
275
|
+
const results = [];
|
|
69
276
|
try {
|
|
70
277
|
for (const operation of operations) {
|
|
71
|
-
|
|
278
|
+
await this.setLocalDeadline();
|
|
279
|
+
const result = await this.query(operation.sql, postgresParams(parseJson(operation.parameters, [])), true);
|
|
280
|
+
results.push({ affectedRows: result.rowCount ?? 0 });
|
|
72
281
|
}
|
|
73
282
|
}
|
|
74
283
|
catch (error) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw new BatchWriteError(errorText(error), true);
|
|
284
|
+
if (await this.rollbackQuietly()) {
|
|
285
|
+
throw new BatchWriteError(errorText(error), false);
|
|
80
286
|
}
|
|
81
|
-
throw new BatchWriteError(errorText(error),
|
|
287
|
+
throw new BatchWriteError(errorText(error), true);
|
|
82
288
|
}
|
|
83
289
|
try {
|
|
84
|
-
this.
|
|
290
|
+
await this.query("COMMIT", [], true);
|
|
85
291
|
return results;
|
|
86
292
|
}
|
|
87
293
|
catch (error) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new BatchWriteError(errorText(error), false);
|
|
91
|
-
}
|
|
92
|
-
catch (rollbackError) {
|
|
93
|
-
if (rollbackError instanceof BatchWriteError)
|
|
94
|
-
throw rollbackError;
|
|
95
|
-
throw new BatchWriteError(errorText(error), true);
|
|
96
|
-
}
|
|
294
|
+
await this.rollbackQuietly();
|
|
295
|
+
throw new BatchWriteError(errorText(error), true);
|
|
97
296
|
}
|
|
98
297
|
}
|
|
99
298
|
async signature() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const stats = statSync(this.source, { bigint: true });
|
|
103
|
-
const walPath = `${this.source}-wal`;
|
|
104
|
-
const wal = existsSync(walPath)
|
|
105
|
-
? statSync(walPath, { bigint: true })
|
|
106
|
-
: undefined;
|
|
107
|
-
return hash({
|
|
108
|
-
size: stats.size.toString(),
|
|
109
|
-
modified: stats.mtimeNs.toString(),
|
|
110
|
-
walSize: wal?.size.toString() ?? "0",
|
|
111
|
-
walModified: wal?.mtimeNs.toString() ?? "0",
|
|
112
|
-
});
|
|
299
|
+
throwIfStopped(this.context, false);
|
|
300
|
+
return "ttl";
|
|
113
301
|
}
|
|
114
302
|
async inspect(kind, table) {
|
|
303
|
+
await this.connect();
|
|
304
|
+
await this.query("BEGIN READ ONLY", [], false);
|
|
305
|
+
try {
|
|
306
|
+
const result = await this.inspectTransaction(kind, table);
|
|
307
|
+
await this.query("COMMIT", [], false);
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
await this.rollbackQuietly();
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async close() {
|
|
316
|
+
if (!this.ending) {
|
|
317
|
+
this.ending = this.client.end().catch(() => undefined);
|
|
318
|
+
}
|
|
319
|
+
await this.ending;
|
|
320
|
+
}
|
|
321
|
+
async inspectTransaction(kind, table) {
|
|
115
322
|
if (kind === "schema") {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return { schema: "main", tables };
|
|
323
|
+
await this.setLocalDeadline();
|
|
324
|
+
const result = await this.query(`SELECT table_schema AS schema, table_name AS name, table_type AS type
|
|
325
|
+
FROM information_schema.tables
|
|
326
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
327
|
+
ORDER BY table_schema, table_name`, [], false);
|
|
328
|
+
return { tables: result.rows };
|
|
123
329
|
}
|
|
124
330
|
if (!table)
|
|
125
331
|
throw new Error(`Table is required for inspect ${kind}.`);
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
332
|
+
const [schema, name] = table.includes(".")
|
|
333
|
+
? table.split(".", 2)
|
|
334
|
+
: ["public", table];
|
|
335
|
+
await this.setLocalDeadline();
|
|
336
|
+
const columns = await this.query(`SELECT column_name AS name, data_type AS type,
|
|
337
|
+
is_nullable = 'YES' AS nullable
|
|
338
|
+
FROM information_schema.columns
|
|
339
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
340
|
+
ORDER BY ordinal_position`, [schema, name], false);
|
|
341
|
+
if (columns.rows.length === 0) {
|
|
131
342
|
throw new Error(`Table "${table}" was not found.`);
|
|
132
|
-
|
|
133
|
-
.prepare(`PRAGMA index_list(${quoted})`)
|
|
134
|
-
.all();
|
|
135
|
-
const foreignKeys = this.db
|
|
136
|
-
.prepare(`PRAGMA foreign_key_list(${quoted})`)
|
|
137
|
-
.all();
|
|
343
|
+
}
|
|
138
344
|
if (kind === "columns")
|
|
139
|
-
return { table, columns };
|
|
345
|
+
return { table: name, schema, columns: columns.rows };
|
|
346
|
+
await this.setLocalDeadline();
|
|
347
|
+
const indexes = await this.query(`SELECT indexname AS name, indexdef AS definition
|
|
348
|
+
FROM pg_indexes WHERE schemaname = $1 AND tablename = $2
|
|
349
|
+
ORDER BY indexname`, [schema, name], false);
|
|
350
|
+
await this.setLocalDeadline();
|
|
351
|
+
const constraints = await this.query(`SELECT constraint_name AS name, constraint_type AS type
|
|
352
|
+
FROM information_schema.table_constraints
|
|
353
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
354
|
+
ORDER BY constraint_name`, [schema, name], false);
|
|
140
355
|
if (kind === "indexes")
|
|
141
|
-
return { table, indexes };
|
|
356
|
+
return { table: name, schema, indexes: indexes.rows };
|
|
142
357
|
if (kind === "constraints") {
|
|
143
|
-
return {
|
|
144
|
-
table,
|
|
145
|
-
primary_key: columns
|
|
146
|
-
.filter((column) => Number(column.pk) > 0)
|
|
147
|
-
.map((column) => column.name),
|
|
148
|
-
foreign_keys: foreignKeys,
|
|
149
|
-
};
|
|
358
|
+
return { table: name, schema, constraints: constraints.rows };
|
|
150
359
|
}
|
|
151
360
|
if (kind !== "table")
|
|
152
361
|
throw new Error(`Unknown inspection kind "${kind}".`);
|
|
153
362
|
return {
|
|
154
|
-
table,
|
|
155
|
-
schema
|
|
156
|
-
columns: columns.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
nullable: column.notnull === 0 && Number(column.pk) === 0,
|
|
160
|
-
primary_key: Number(column.pk) > 0,
|
|
161
|
-
})),
|
|
162
|
-
indexes: indexes.length,
|
|
163
|
-
foreign_keys: foreignKeys.length,
|
|
363
|
+
table: name,
|
|
364
|
+
schema,
|
|
365
|
+
columns: columns.rows,
|
|
366
|
+
indexes: indexes.rows.length,
|
|
367
|
+
constraints: constraints.rows.length,
|
|
164
368
|
};
|
|
165
369
|
}
|
|
166
|
-
async
|
|
167
|
-
this.
|
|
370
|
+
async connect() {
|
|
371
|
+
if (this.connected)
|
|
372
|
+
return;
|
|
373
|
+
throwIfStopped(this.context, false);
|
|
374
|
+
await withContext(this.client.connect(), this.context, () => this.stop(), false);
|
|
375
|
+
this.connected = true;
|
|
376
|
+
if (this.readOnly) {
|
|
377
|
+
await this.query("SET default_transaction_read_only = on", [], false);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async setLocalDeadline() {
|
|
381
|
+
await this.query(`SET LOCAL statement_timeout = ${remainingMilliseconds(this.context)}`, [], false);
|
|
382
|
+
}
|
|
383
|
+
async query(sql, params, outcomeUnknown) {
|
|
384
|
+
throwIfStopped(this.context, outcomeUnknown);
|
|
385
|
+
try {
|
|
386
|
+
return await withContext(this.client.query(sql, params), this.context, () => this.stop(), outcomeUnknown);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
if (error instanceof AdapterExecutionError)
|
|
390
|
+
throw error;
|
|
391
|
+
if (postgresErrorCode(error) === "57014") {
|
|
392
|
+
throw stoppedError(this.context, outcomeUnknown);
|
|
393
|
+
}
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async rollbackQuietly() {
|
|
398
|
+
if (this.ending)
|
|
399
|
+
return false;
|
|
400
|
+
try {
|
|
401
|
+
await this.query("ROLLBACK", [], false);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
stop() {
|
|
409
|
+
if (!this.ending) {
|
|
410
|
+
this.ending = this.client.end().catch(() => undefined);
|
|
411
|
+
}
|
|
168
412
|
}
|
|
169
413
|
}
|
|
170
|
-
class
|
|
414
|
+
class MySqlAdapter {
|
|
415
|
+
source;
|
|
171
416
|
readOnly;
|
|
417
|
+
context;
|
|
172
418
|
confidence = "ttl_based";
|
|
419
|
+
rawClient;
|
|
173
420
|
client;
|
|
174
421
|
connected = false;
|
|
175
|
-
|
|
422
|
+
closed = false;
|
|
423
|
+
constructor(source, readOnly, context) {
|
|
424
|
+
this.source = source;
|
|
176
425
|
this.readOnly = readOnly;
|
|
177
|
-
this.
|
|
426
|
+
this.context = context;
|
|
178
427
|
}
|
|
179
428
|
async read(sql, params) {
|
|
180
|
-
await this.
|
|
181
|
-
await this.client.query("BEGIN READ ONLY");
|
|
429
|
+
await this.query("START TRANSACTION READ ONLY", [], false, false);
|
|
182
430
|
try {
|
|
183
|
-
const result = await this.
|
|
184
|
-
await this.
|
|
431
|
+
const [result, fields] = await this.query(sql, mysqlParams(params), false, true);
|
|
432
|
+
await this.query("COMMIT", [], false, false);
|
|
185
433
|
return {
|
|
186
|
-
rows: toJsonSafe(result
|
|
187
|
-
columns:
|
|
434
|
+
rows: toJsonSafe(mysqlRows(result)),
|
|
435
|
+
columns: fields.map((field) => ({
|
|
188
436
|
name: field.name,
|
|
189
|
-
type:
|
|
190
|
-
`oid_${field.dataTypeID}`,
|
|
437
|
+
type: mysqlFieldType(field),
|
|
191
438
|
})),
|
|
192
439
|
};
|
|
193
440
|
}
|
|
194
441
|
catch (error) {
|
|
195
|
-
await this.
|
|
442
|
+
await this.rollbackQuietly();
|
|
196
443
|
throw error;
|
|
197
444
|
}
|
|
198
445
|
}
|
|
199
446
|
async write(sql, params) {
|
|
200
447
|
if (this.readOnly)
|
|
201
448
|
throw new Error("Connection is read-only.");
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
449
|
+
let values;
|
|
450
|
+
try {
|
|
451
|
+
values = mysqlParams(params);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
throw new AdapterWriteError(errorText(error), false);
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
await this.query("START TRANSACTION", [], false, false);
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
if (error instanceof AdapterExecutionError)
|
|
461
|
+
throw error;
|
|
462
|
+
throw new AdapterWriteError(errorText(error), false);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const [result] = await this.query(sql, values, true, true);
|
|
466
|
+
await this.query("COMMIT", [], true, false);
|
|
467
|
+
return { affectedRows: mysqlAffectedRows(result) };
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
await this.rollbackQuietly();
|
|
471
|
+
if (error instanceof AdapterExecutionError)
|
|
472
|
+
throw error;
|
|
473
|
+
throw new AdapterWriteError(errorText(error), true);
|
|
474
|
+
}
|
|
205
475
|
}
|
|
206
476
|
async writeBatch(operations, isolation) {
|
|
207
477
|
if (this.readOnly)
|
|
208
478
|
throw new Error("Connection is read-only.");
|
|
209
|
-
|
|
479
|
+
const unsupported = operations.find((operation) => !MYSQL_TRANSACTIONAL_STATEMENTS.has(operation.statement_type));
|
|
480
|
+
if (unsupported) {
|
|
481
|
+
throw new BatchWriteError(`MySQL transactions cannot atomically include ${unsupported.statement_type.toUpperCase()} statements.`, false);
|
|
482
|
+
}
|
|
210
483
|
const level = isolation.toUpperCase();
|
|
211
|
-
if (!
|
|
212
|
-
throw new Error(`Unsupported
|
|
484
|
+
if (!MYSQL_ISOLATION_LEVELS.has(level)) {
|
|
485
|
+
throw new Error(`Unsupported MySQL isolation level "${isolation}".`);
|
|
213
486
|
}
|
|
214
487
|
try {
|
|
215
|
-
await this.
|
|
488
|
+
await this.query(`SET TRANSACTION ISOLATION LEVEL ${level}`, [], false, false);
|
|
489
|
+
await this.query("START TRANSACTION", [], false, false);
|
|
216
490
|
}
|
|
217
491
|
catch (error) {
|
|
218
492
|
throw new BatchWriteError(errorText(error), false);
|
|
@@ -220,127 +494,303 @@ class PostgresAdapter {
|
|
|
220
494
|
const results = [];
|
|
221
495
|
try {
|
|
222
496
|
for (const operation of operations) {
|
|
223
|
-
|
|
497
|
+
const [result] = await this.query(operation.sql, mysqlParams(parseJson(operation.parameters, [])), true, true);
|
|
498
|
+
results.push({ affectedRows: mysqlAffectedRows(result) });
|
|
224
499
|
}
|
|
225
500
|
}
|
|
226
501
|
catch (error) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
throw new BatchWriteError(errorText(error), true);
|
|
502
|
+
if (await this.rollbackQuietly()) {
|
|
503
|
+
throw new BatchWriteError(errorText(error), false);
|
|
232
504
|
}
|
|
233
|
-
throw new BatchWriteError(errorText(error),
|
|
505
|
+
throw new BatchWriteError(errorText(error), true);
|
|
234
506
|
}
|
|
235
507
|
try {
|
|
236
|
-
await this.
|
|
508
|
+
await this.query("COMMIT", [], true, false);
|
|
237
509
|
return results;
|
|
238
510
|
}
|
|
239
511
|
catch (error) {
|
|
240
|
-
|
|
241
|
-
await this.client.query("ROLLBACK");
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
// COMMIT response was lost; rollback cannot establish outcome.
|
|
245
|
-
}
|
|
512
|
+
await this.rollbackQuietly();
|
|
246
513
|
throw new BatchWriteError(errorText(error), true);
|
|
247
514
|
}
|
|
248
515
|
}
|
|
249
516
|
async signature() {
|
|
517
|
+
throwIfStopped(this.context, false);
|
|
250
518
|
return "ttl";
|
|
251
519
|
}
|
|
252
520
|
async inspect(kind, table) {
|
|
253
|
-
await this.
|
|
521
|
+
await this.query("START TRANSACTION READ ONLY", [], false, false);
|
|
522
|
+
try {
|
|
523
|
+
const result = await this.inspectTransaction(kind, table);
|
|
524
|
+
await this.query("COMMIT", [], false, false);
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
await this.rollbackQuietly();
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async close() {
|
|
533
|
+
if (this.closed)
|
|
534
|
+
return;
|
|
535
|
+
this.closed = true;
|
|
536
|
+
this.connected = false;
|
|
537
|
+
if (!this.client)
|
|
538
|
+
return;
|
|
539
|
+
await this.client.end().catch(() => undefined);
|
|
540
|
+
}
|
|
541
|
+
async inspectTransaction(kind, table) {
|
|
254
542
|
if (kind === "schema") {
|
|
255
|
-
const result = await this.
|
|
543
|
+
const [result] = await this.query(`SELECT TABLE_SCHEMA AS schema, TABLE_NAME AS name, TABLE_TYPE AS type
|
|
256
544
|
FROM information_schema.tables
|
|
257
|
-
WHERE
|
|
258
|
-
ORDER BY
|
|
259
|
-
return { tables: result
|
|
545
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
546
|
+
ORDER BY TABLE_NAME`, [], false, false);
|
|
547
|
+
return { tables: mysqlRows(result) };
|
|
260
548
|
}
|
|
261
549
|
if (!table)
|
|
262
550
|
throw new Error(`Table is required for inspect ${kind}.`);
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
551
|
+
const separator = table.indexOf(".");
|
|
552
|
+
const schema = separator === -1
|
|
553
|
+
? await this.databaseName()
|
|
554
|
+
: table.slice(0, separator);
|
|
555
|
+
const name = separator === -1 ? table : table.slice(separator + 1);
|
|
556
|
+
if (!schema || !name)
|
|
557
|
+
throw new Error(`Invalid table name "${table}".`);
|
|
558
|
+
const [columnResult] = await this.query(`SELECT COLUMN_NAME AS name, DATA_TYPE AS type,
|
|
559
|
+
IS_NULLABLE = 'YES' AS nullable
|
|
268
560
|
FROM information_schema.columns
|
|
269
|
-
WHERE
|
|
270
|
-
ORDER BY
|
|
271
|
-
|
|
561
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
562
|
+
ORDER BY ORDINAL_POSITION`, [schema, name], false, true);
|
|
563
|
+
const columns = mysqlRows(columnResult).map((column) => ({
|
|
564
|
+
...column,
|
|
565
|
+
nullable: Boolean(column.nullable),
|
|
566
|
+
}));
|
|
567
|
+
if (columns.length === 0) {
|
|
272
568
|
throw new Error(`Table "${table}" was not found.`);
|
|
273
569
|
}
|
|
274
570
|
if (kind === "columns")
|
|
275
|
-
return { table: name, schema, columns
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
571
|
+
return { table: name, schema, columns };
|
|
572
|
+
const [indexResult] = await this.query(`SELECT INDEX_NAME AS name, NON_UNIQUE AS non_unique,
|
|
573
|
+
INDEX_TYPE AS index_type,
|
|
574
|
+
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ', ') AS columns
|
|
575
|
+
FROM information_schema.statistics
|
|
576
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
577
|
+
GROUP BY INDEX_NAME, NON_UNIQUE, INDEX_TYPE
|
|
578
|
+
ORDER BY INDEX_NAME`, [schema, name], false, true);
|
|
579
|
+
const indexes = mysqlRows(indexResult).map((index) => ({
|
|
580
|
+
name: String(index.name),
|
|
581
|
+
definition: `${Number(index.non_unique) === 0 ? "UNIQUE " : ""}${String(index.index_type)} (${String(index.columns)})`,
|
|
582
|
+
}));
|
|
583
|
+
const [constraintResult] = await this.query(`SELECT CONSTRAINT_NAME AS name, CONSTRAINT_TYPE AS type
|
|
280
584
|
FROM information_schema.table_constraints
|
|
281
|
-
WHERE
|
|
282
|
-
ORDER BY
|
|
585
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
586
|
+
ORDER BY CONSTRAINT_NAME`, [schema, name], false, true);
|
|
587
|
+
const constraints = mysqlRows(constraintResult);
|
|
283
588
|
if (kind === "indexes")
|
|
284
|
-
return { table: name, schema, indexes
|
|
589
|
+
return { table: name, schema, indexes };
|
|
285
590
|
if (kind === "constraints") {
|
|
286
|
-
return { table: name, schema, constraints
|
|
591
|
+
return { table: name, schema, constraints };
|
|
287
592
|
}
|
|
288
593
|
if (kind !== "table")
|
|
289
594
|
throw new Error(`Unknown inspection kind "${kind}".`);
|
|
290
595
|
return {
|
|
291
596
|
table: name,
|
|
292
597
|
schema,
|
|
293
|
-
columns
|
|
294
|
-
indexes: indexes.
|
|
295
|
-
constraints: constraints.
|
|
598
|
+
columns,
|
|
599
|
+
indexes: indexes.length,
|
|
600
|
+
constraints: constraints.length,
|
|
296
601
|
};
|
|
297
602
|
}
|
|
298
|
-
async
|
|
299
|
-
|
|
300
|
-
|
|
603
|
+
async databaseName() {
|
|
604
|
+
const [result] = await this.query("SELECT DATABASE() AS database_name", [], false, false);
|
|
605
|
+
const row = mysqlRows(result)[0];
|
|
606
|
+
if (!row?.database_name)
|
|
607
|
+
throw new Error("MySQL connection has no database selected.");
|
|
608
|
+
return String(row.database_name);
|
|
301
609
|
}
|
|
302
610
|
async connect() {
|
|
303
611
|
if (this.connected)
|
|
304
612
|
return;
|
|
305
|
-
|
|
306
|
-
this.
|
|
307
|
-
|
|
308
|
-
|
|
613
|
+
throwIfStopped(this.context, false);
|
|
614
|
+
if (this.closed)
|
|
615
|
+
throw new Error("MySQL adapter is closed.");
|
|
616
|
+
const rawClient = createMySqlConnection({
|
|
617
|
+
uri: this.source,
|
|
618
|
+
connectTimeout: remainingMilliseconds(this.context),
|
|
619
|
+
multipleStatements: false,
|
|
620
|
+
namedPlaceholders: false,
|
|
621
|
+
supportBigNumbers: true,
|
|
622
|
+
bigNumberStrings: true,
|
|
623
|
+
dateStrings: true,
|
|
624
|
+
});
|
|
625
|
+
this.rawClient = rawClient;
|
|
626
|
+
this.client = rawClient.promise();
|
|
627
|
+
try {
|
|
628
|
+
await withContext(this.client.connect(), this.context, () => this.stop(), false);
|
|
629
|
+
this.connected = true;
|
|
630
|
+
if (this.readOnly) {
|
|
631
|
+
await this.runQuery("SET SESSION TRANSACTION READ ONLY", [], false, false);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
this.stop();
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async query(sql, params, outcomeUnknown, prepared) {
|
|
640
|
+
await this.connect();
|
|
641
|
+
return this.runQuery(sql, params, outcomeUnknown, prepared);
|
|
642
|
+
}
|
|
643
|
+
async runQuery(sql, params, outcomeUnknown, prepared) {
|
|
644
|
+
throwIfStopped(this.context, outcomeUnknown);
|
|
645
|
+
if (!this.client || this.closed)
|
|
646
|
+
throw new Error("MySQL adapter is closed.");
|
|
647
|
+
const result = prepared
|
|
648
|
+
? this.client.execute(sql, params)
|
|
649
|
+
: this.client.query(sql, params);
|
|
650
|
+
return withContext(result, this.context, () => this.stop(), outcomeUnknown);
|
|
651
|
+
}
|
|
652
|
+
async rollbackQuietly() {
|
|
653
|
+
if (this.closed || !this.connected)
|
|
654
|
+
return false;
|
|
655
|
+
try {
|
|
656
|
+
await this.query("ROLLBACK", [], false, false);
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return false;
|
|
309
661
|
}
|
|
310
662
|
}
|
|
663
|
+
stop() {
|
|
664
|
+
if (this.closed)
|
|
665
|
+
return;
|
|
666
|
+
this.closed = true;
|
|
667
|
+
this.connected = false;
|
|
668
|
+
this.rawClient?.destroy();
|
|
669
|
+
}
|
|
311
670
|
}
|
|
312
|
-
const
|
|
671
|
+
const MYSQL_ISOLATION_LEVELS = new Set([
|
|
313
672
|
"SERIALIZABLE",
|
|
314
673
|
"REPEATABLE READ",
|
|
315
674
|
"READ COMMITTED",
|
|
316
675
|
"READ UNCOMMITTED",
|
|
317
676
|
]);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
677
|
+
const MYSQL_TRANSACTIONAL_STATEMENTS = new Set([
|
|
678
|
+
"delete",
|
|
679
|
+
"insert",
|
|
680
|
+
"replace",
|
|
681
|
+
"update",
|
|
682
|
+
]);
|
|
683
|
+
function mysqlParams(params) {
|
|
684
|
+
if (!Array.isArray(params)) {
|
|
685
|
+
throw new Error("MySQL parameters must be a JSON array.");
|
|
686
|
+
}
|
|
687
|
+
return params.map((value) => {
|
|
688
|
+
if (value === undefined ||
|
|
689
|
+
Array.isArray(value) ||
|
|
690
|
+
(typeof value === "object" &&
|
|
691
|
+
value !== null &&
|
|
692
|
+
!(value instanceof Date) &&
|
|
693
|
+
!Buffer.isBuffer(value) &&
|
|
694
|
+
!(value instanceof Uint8Array))) {
|
|
695
|
+
throw new Error("MySQL parameter values must be scalar.");
|
|
696
|
+
}
|
|
697
|
+
return value;
|
|
698
|
+
});
|
|
322
699
|
}
|
|
323
|
-
function
|
|
324
|
-
if (Array.isArray(
|
|
325
|
-
return
|
|
326
|
-
|
|
700
|
+
function mysqlRows(result) {
|
|
701
|
+
if (!Array.isArray(result)) {
|
|
702
|
+
throw new Error("MySQL statement did not return rows.");
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
327
705
|
}
|
|
706
|
+
function mysqlAffectedRows(result) {
|
|
707
|
+
if (Array.isArray(result) || !("affectedRows" in result)) {
|
|
708
|
+
throw new Error("MySQL statement did not return a write result.");
|
|
709
|
+
}
|
|
710
|
+
return Number(result.affectedRows);
|
|
711
|
+
}
|
|
712
|
+
function mysqlFieldType(field) {
|
|
713
|
+
return String(field.typeName ?? field.columnType ?? field.type ?? "unknown").toLowerCase();
|
|
714
|
+
}
|
|
715
|
+
const POSTGRES_ISOLATION_LEVELS = new Set([
|
|
716
|
+
"SERIALIZABLE",
|
|
717
|
+
"REPEATABLE READ",
|
|
718
|
+
"READ COMMITTED",
|
|
719
|
+
"READ UNCOMMITTED",
|
|
720
|
+
]);
|
|
328
721
|
function postgresParams(params) {
|
|
329
722
|
if (Array.isArray(params))
|
|
330
723
|
return params;
|
|
331
724
|
throw new Error("PostgreSQL parameters must be a JSON array.");
|
|
332
725
|
}
|
|
333
|
-
function
|
|
334
|
-
return
|
|
726
|
+
function remainingMilliseconds(context) {
|
|
727
|
+
return Math.max(1, Math.ceil(context.deadline - Date.now()));
|
|
728
|
+
}
|
|
729
|
+
function throwIfStopped(context, outcomeUnknown) {
|
|
730
|
+
if (context.signal?.aborted || context.deadline <= Date.now()) {
|
|
731
|
+
throw stoppedError(context, outcomeUnknown);
|
|
732
|
+
}
|
|
335
733
|
}
|
|
336
|
-
function
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
return "unknown";
|
|
340
|
-
if (Buffer.isBuffer(value))
|
|
341
|
-
return "binary";
|
|
342
|
-
return typeof value;
|
|
734
|
+
function stoppedError(context, outcomeUnknown) {
|
|
735
|
+
const aborted = context.signal?.aborted ?? false;
|
|
736
|
+
return new AdapterExecutionError(aborted ? "Database operation cancelled." : "Database operation timed out.", aborted ? "aborted" : "timeout", outcomeUnknown);
|
|
343
737
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
738
|
+
function withContext(promise, context, onStop, outcomeUnknown) {
|
|
739
|
+
try {
|
|
740
|
+
throwIfStopped(context, outcomeUnknown);
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
try {
|
|
744
|
+
onStop();
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// Preserve deadline/cancellation error.
|
|
748
|
+
}
|
|
749
|
+
return Promise.reject(error);
|
|
750
|
+
}
|
|
751
|
+
return new Promise((resolve, reject) => {
|
|
752
|
+
let settled = false;
|
|
753
|
+
const finish = (action) => {
|
|
754
|
+
if (settled)
|
|
755
|
+
return;
|
|
756
|
+
settled = true;
|
|
757
|
+
clearTimeout(timer);
|
|
758
|
+
context.signal?.removeEventListener("abort", stop);
|
|
759
|
+
action();
|
|
760
|
+
};
|
|
761
|
+
const stop = () => {
|
|
762
|
+
finish(() => {
|
|
763
|
+
try {
|
|
764
|
+
onStop();
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
// Preserve deadline/cancellation error.
|
|
768
|
+
}
|
|
769
|
+
reject(stoppedError(context, outcomeUnknown));
|
|
770
|
+
});
|
|
771
|
+
};
|
|
772
|
+
const stopped = () => Boolean(context.signal?.aborted) || context.deadline <= Date.now();
|
|
773
|
+
const timer = setTimeout(stop, remainingMilliseconds(context));
|
|
774
|
+
context.signal?.addEventListener("abort", stop, { once: true });
|
|
775
|
+
promise.then((value) => {
|
|
776
|
+
if (stopped())
|
|
777
|
+
stop();
|
|
778
|
+
else
|
|
779
|
+
finish(() => resolve(value));
|
|
780
|
+
}, (error) => {
|
|
781
|
+
if (stopped())
|
|
782
|
+
stop();
|
|
783
|
+
else
|
|
784
|
+
finish(() => reject(error));
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function postgresErrorCode(error) {
|
|
789
|
+
if (!error || typeof error !== "object")
|
|
790
|
+
return undefined;
|
|
791
|
+
const code = error.code;
|
|
792
|
+
return typeof code === "string" ? code : undefined;
|
|
793
|
+
}
|
|
794
|
+
function errorText(error) {
|
|
795
|
+
return error instanceof Error ? error.message : String(error);
|
|
346
796
|
}
|