@jarenjs/db 0.56.0 → 0.66.1
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/ARCHITECTURE.md +393 -56
- package/README.md +585 -53
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +122 -14
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +744 -64
- package/package.json +21 -7
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +218 -45
- package/src/cli.js +165 -59
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +243 -11
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +448 -61
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +17 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit.js +339 -48
- package/src/entity.js +20 -22
- package/src/errors.js +422 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +46 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/maintenance.js +175 -0
- package/src/migrate.js +248 -181
- package/src/model.js +68 -0
- package/src/plan.js +1119 -138
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1634 -323
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1505 -264
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1097 -25
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +69 -2
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +25 -3
- package/types/wasm.d.ts +14 -0
package/src/errors.js
CHANGED
|
@@ -13,34 +13,28 @@
|
|
|
13
13
|
|
|
14
14
|
import { CodedError } from '@jarenjs/core/errors';
|
|
15
15
|
|
|
16
|
+
/** The driver's own settlement envelope; arbitrary caller aggregates stay opaque. */
|
|
17
|
+
export class TransactionFailure extends AggregateError {
|
|
18
|
+
/** @param {any} error @param {any} cleanupError */
|
|
19
|
+
constructor(error, cleanupError) {
|
|
20
|
+
super([error, cleanupError], 'the transaction failed, and rolling it back failed too');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* The runtime code table (the `CSV_CODES` shape): one entry per code
|
|
18
26
|
* this package can raise, proven in sync with MODEL-FORMAT.md §7's
|
|
19
27
|
* normative table by a test.
|
|
20
28
|
*/
|
|
21
|
-
/**
|
|
22
|
-
* Whether a database error is the unique-key collision of `table.column`
|
|
23
|
-
* — the one failure every insert path reports as `JD2001` rather than
|
|
24
|
-
* the generic `JD2005`. One detector for the collection cores, the
|
|
25
|
-
* entity cores and the unit of work, so no path wraps it differently.
|
|
26
|
-
* @param {any} error
|
|
27
|
-
* @param {string} table
|
|
28
|
-
* @param {string} column
|
|
29
|
-
* @returns {boolean}
|
|
30
|
-
*/
|
|
31
|
-
export function isDuplicateKeyError(error, table, column) {
|
|
32
|
-
if (error?.errcode === 1555) return true;
|
|
33
|
-
return typeof error?.message === 'string'
|
|
34
|
-
&& error.message.includes('UNIQUE constraint failed')
|
|
35
|
-
&& error.message.includes(`${table}.${column}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
export const DB_CODES = Object.freeze({
|
|
39
30
|
JD0001: 'the SQLite library is below the supported floor',
|
|
40
|
-
JD0002: 'the
|
|
31
|
+
JD0002: 'the existing database disagrees with the declared model, or the open failed in the driver',
|
|
41
32
|
JD0003: 'the driver binding is unavailable on this runtime',
|
|
42
33
|
JD0004: 'a declared index cannot be mapped to a column',
|
|
43
34
|
JD0005: 'the model document is invalid',
|
|
35
|
+
JD0006: 'an open option named a pragma this store does not configure',
|
|
36
|
+
JD0007: 'the pragma cannot be applied on this driver or store',
|
|
37
|
+
JD0008: 'a pragma did not take: the read-back disagrees with the request',
|
|
44
38
|
JD0010: 'strict mode refused a residual',
|
|
45
39
|
JD0011: 'the profile refused the document',
|
|
46
40
|
JD0012: 'work waited too long for the open transaction to settle',
|
|
@@ -48,6 +42,10 @@ export const DB_CODES = Object.freeze({
|
|
|
48
42
|
JD0031: 'relation declarations contradict each other',
|
|
49
43
|
JD0032: 'the include specification is invalid',
|
|
50
44
|
JD0033: 'an entity query names no entity array',
|
|
45
|
+
JD0034: 'a tracked cursor needs a bare entity return',
|
|
46
|
+
JD0035: 'the continuation does not belong to this ordering',
|
|
47
|
+
JD0036: 'a snapshot page needs an immutable ordering',
|
|
48
|
+
JD0037: 'strictStreaming refused a plan that buffers',
|
|
51
49
|
JD0040: 'the save spans a relation cycle',
|
|
52
50
|
JD0050: 'live queries require change capture',
|
|
53
51
|
JD0051: 'the demanded live mode is unavailable',
|
|
@@ -57,6 +55,7 @@ export const DB_CODES = Object.freeze({
|
|
|
57
55
|
JD0021: 'the migration is missing a required data transform',
|
|
58
56
|
JD0022: 'an applied migration disagrees with the history record',
|
|
59
57
|
JD0023: 'a migration step failed',
|
|
58
|
+
JD0024: 'a document source or target could not be read or written',
|
|
60
59
|
JD2001: 'insert found the key already present',
|
|
61
60
|
JD2002: 'a usable key could not be resolved for the write',
|
|
62
61
|
JD2003: 'the write failed schema validation',
|
|
@@ -71,6 +70,37 @@ export const DB_CODES = Object.freeze({
|
|
|
71
70
|
JD2061: 'another context owns the database',
|
|
72
71
|
JD2062: 'the store closed with job handlers still in flight',
|
|
73
72
|
JD2063: 'the store is closed',
|
|
73
|
+
JD2064: 'the call was aborted while it waited for the open transaction',
|
|
74
|
+
JD2065: 'the job is not leased — it is unknown, or already settled',
|
|
75
|
+
JD2066: 'the lease was superseded by a newer claim or renewal',
|
|
76
|
+
JD2067: 'the lease expired before the call',
|
|
77
|
+
JD2068: 'a settling call needs the lease the claim returned',
|
|
78
|
+
JD2069: 'a resumed run does not match the workflow or input it was checkpointed under',
|
|
79
|
+
JD2070: 'the transaction handle does not belong to the live scope',
|
|
80
|
+
JD2071: 'the savepoint label is blank, duplicate or unknown',
|
|
81
|
+
JD2072: 'the call was aborted before its next row',
|
|
82
|
+
JD2073: 'an include exceeded its per-root bound',
|
|
83
|
+
JD2074: 'an item exceeds the page byte bound',
|
|
84
|
+
JD2075: 'the deadline passed before the next unit of work',
|
|
85
|
+
JD2076: 'an item exceeds the profile byte bound',
|
|
86
|
+
JD2077: 'the maintenance operation is unavailable on this store',
|
|
87
|
+
JD2078: 'the maintenance operation failed',
|
|
88
|
+
JD2079: 'the backup was cancelled',
|
|
89
|
+
JD2080: 'the migration was cancelled between steps',
|
|
90
|
+
JD2081: 'the maintenance operation was cancelled',
|
|
91
|
+
JD2082: 'the database or its disk is full',
|
|
92
|
+
JD2083: 'the database is read-only',
|
|
93
|
+
JD2084: 'a disk I/O error',
|
|
94
|
+
JD2085: 'the database file is corrupt or not a database',
|
|
95
|
+
JD2086: 'a seek anchor came back with a type the plan did not declare',
|
|
96
|
+
JD2087: 'the connection to the database was lost',
|
|
97
|
+
JD2088: 'the transaction was aborted by an earlier failure in it',
|
|
98
|
+
JD2089: 'the statement was cancelled by the server',
|
|
99
|
+
JD2090: 'the worker connection generation was lost',
|
|
100
|
+
JD2091: 'the worker admission queue is full',
|
|
101
|
+
JD2092: 'a worker transport bound was exceeded',
|
|
102
|
+
JD2093: 'the worker protocol frame is invalid',
|
|
103
|
+
JD2094: 'the durable snapshot failed and the connection is invalid',
|
|
74
104
|
});
|
|
75
105
|
|
|
76
106
|
/**
|
|
@@ -82,7 +112,10 @@ export const DB_CODES = Object.freeze({
|
|
|
82
112
|
* supported floor; the reason names the version found
|
|
83
113
|
* - `JD0002` — a declared collection already exists in the database
|
|
84
114
|
* with a different shape; nothing was altered — changing shape is
|
|
85
|
-
* the migration story, a later capability
|
|
115
|
+
* the migration story, a later capability. Also a driver failure
|
|
116
|
+
* anywhere in the open sequence (an unopenable path, a corrupt or
|
|
117
|
+
* locked file): the error carries `class`/`retryable` from the
|
|
118
|
+
* classifier and the driver's error as `cause`
|
|
86
119
|
* - `JD0003` — the runtime builtin behind a driver could not be
|
|
87
120
|
* loaded here (Node cannot resolve `bun:`; Bun ships no
|
|
88
121
|
* `node:sqlite`), or an injected handle is missing
|
|
@@ -93,6 +126,15 @@ export const DB_CODES = Object.freeze({
|
|
|
93
126
|
* names the expression or the member and `docPath` points at it
|
|
94
127
|
* - `JD0005` — the model document is invalid; `docPath` points at
|
|
95
128
|
* the offending member
|
|
129
|
+
* - `JD0006` — an `openStore` option named a pragma outside the closed
|
|
130
|
+
* configurable set (`foreign_keys`, `page_size`, a snake-case
|
|
131
|
+
* spelling of a member); the reason names the option and the set
|
|
132
|
+
* - `JD0007` — a requested pragma cannot be applied here: the driver's
|
|
133
|
+
* binding does not declare it, or the store kind refuses it (a
|
|
134
|
+
* journal-mode write on a read-only store)
|
|
135
|
+
* - `JD0008` — a requested pragma did not take: the value read back
|
|
136
|
+
* from the connection after the open sequence disagrees with the
|
|
137
|
+
* request; the reason carries both, and the store did not open
|
|
96
138
|
* - `JD0010` — `strict: true` and part of the query would have run
|
|
97
139
|
* outside the database; the reason names the forcing construct
|
|
98
140
|
* - `JD0011` — the active profile refused the document before any
|
|
@@ -177,7 +219,349 @@ export class DbCompileError extends CodedError {
|
|
|
177
219
|
* - `JD2063` — a call after `close()`: every entry point of a closed
|
|
178
220
|
* store refuses by name rather than leaking the driver's own error,
|
|
179
221
|
* and a second `close()` is a no-op on every driver
|
|
222
|
+
* - `JD2077` — a maintenance operation (`checkpoint`, `integrityCheck`,
|
|
223
|
+
* `foreignKeyCheck`, `optimize`, `backupTo`) is unavailable here:
|
|
224
|
+
* the driver's binding does not declare it, or the store is read-only
|
|
225
|
+
* and the operation writes; `capabilities.maintenance` says which
|
|
226
|
+
* - `JD2078` — a maintenance operation failed in the driver; the
|
|
227
|
+
* original error is the `cause`. Corruption an integrity check finds
|
|
228
|
+
* is its RESULT, never this error
|
|
229
|
+
* - `JD2079` — a backup was cancelled through its signal between
|
|
230
|
+
* pages: the temporary file was removed and the target path was not
|
|
231
|
+
* written; the signal's reason is the `cause`
|
|
232
|
+
* - `JD2080` — a migration was cancelled through its signal between
|
|
233
|
+
* migrations, steps or batches: the migration in flight rolled back
|
|
234
|
+
* whole, the completed ones stand, a rerun resumes from the recorded
|
|
235
|
+
* position
|
|
236
|
+
* - `JD2081` — a maintenance operation was cancelled through its signal
|
|
237
|
+
* before its statement ran
|
|
238
|
+
* - `JD2086` — a seek anchor came back from the database with a type
|
|
239
|
+
* the plan did not declare: the plan reads an anchor from a column
|
|
240
|
+
* of declared type and binds it through a TYPED slot, so a value of
|
|
241
|
+
* another type is a defect below the store, refused before the
|
|
242
|
+
* statement it would have bound runs
|
|
243
|
+
* - `JD2082`/`JD2083`/`JD2084`/`JD2085` — the driver reported a full
|
|
244
|
+
* disk, a read-only database, an I/O error, a corrupt file: one
|
|
245
|
+
* classifier (`classifyDriverError`) assigns them, whichever path met
|
|
246
|
+
* the failure, and every classified error carries `class`,
|
|
247
|
+
* `retryable` and the driver's error as `cause`; a busy or locked
|
|
248
|
+
* database stays `JD2005` with `class: 'busy'` and `retryable: true`
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The ONE classification of a driver failure. Every path that
|
|
253
|
+
* wraps a driver error — the collection and entity writes, the job
|
|
254
|
+
* queue, the query path, the maintenance operations, the backup, the
|
|
255
|
+
* open sequence — consults this table and nothing else, so a locked
|
|
256
|
+
* database, a full disk, a read-only file, an I/O error and a corrupt
|
|
257
|
+
* file each arrive under one code with one stable `class` and one
|
|
258
|
+
* `retryable` verdict, whichever path met them.
|
|
259
|
+
*
|
|
260
|
+
* The engine's primary result code is the low byte of the extended
|
|
261
|
+
* one the bindings expose (`errcode` on node:sqlite, `errno` on
|
|
262
|
+
* bun:sqlite, `resultCode` on the wasm build's `SQLite3Error`); the
|
|
263
|
+
* message is consulted for the two facts a code alone does not carry —
|
|
264
|
+
* an int64 overflow is a generic `SQLITE_ERROR` (1) with the words
|
|
265
|
+
* `integer overflow`, and a duplicate key is a UNIQUE constraint whose
|
|
266
|
+
* message names the table and column.
|
|
267
|
+
*
|
|
268
|
+
* The engine is discriminated by the EVIDENCE the error carries, not by
|
|
269
|
+
* a table threaded down from the caller: a SQLite binding attaches a
|
|
270
|
+
* numeric result code, a PostgreSQL one attaches a five-character
|
|
271
|
+
* SQLSTATE. Both land in the same closed set of classes and the same
|
|
272
|
+
* `retryable` verdict, so a caller that already handles a busy SQLite
|
|
273
|
+
* database handles a deadlocked PostgreSQL transaction with no new
|
|
274
|
+
* branch. Neither table ever reads a connection string, so nothing a
|
|
275
|
+
* URL carried can reach a message.
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
/** SQLite primary result codes, by name, as the table reads them. */
|
|
279
|
+
const RESULT = Object.freeze({
|
|
280
|
+
ERROR: 1, BUSY: 5, LOCKED: 6, READONLY: 8, IOERR: 10, CORRUPT: 11, FULL: 13,
|
|
281
|
+
CANTOPEN: 14, CONSTRAINT: 19, NOTADB: 26,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The classes, in the order they are tried; `code` is the runtime code
|
|
286
|
+
* a wrapped error carries (`null` for the overflow row, which the query
|
|
287
|
+
* path answers by re-running the document in the engine rather than by
|
|
288
|
+
* raising) and `reason` the sentence the wrapped error leads with.
|
|
180
289
|
*/
|
|
290
|
+
const CLASSES = Object.freeze([
|
|
291
|
+
Object.freeze({ class: 'busy', primaries: [RESULT.BUSY, RESULT.LOCKED], code: 'JD2005',
|
|
292
|
+
retryable: true, reason: 'the database is busy or locked' }),
|
|
293
|
+
Object.freeze({ class: 'full', primaries: [RESULT.FULL], code: 'JD2082',
|
|
294
|
+
retryable: false, reason: 'the database or its disk is full' }),
|
|
295
|
+
Object.freeze({ class: 'readonly', primaries: [RESULT.READONLY], code: 'JD2083',
|
|
296
|
+
retryable: false, reason: 'the database is read-only' }),
|
|
297
|
+
Object.freeze({ class: 'io', primaries: [RESULT.IOERR], code: 'JD2084',
|
|
298
|
+
retryable: false, reason: 'a disk I/O error' }),
|
|
299
|
+
Object.freeze({ class: 'corrupt', primaries: [RESULT.CORRUPT, RESULT.NOTADB], code: 'JD2085',
|
|
300
|
+
retryable: false, reason: 'the database file is corrupt or not a database' }),
|
|
301
|
+
Object.freeze({ class: 'cantopen', primaries: [RESULT.CANTOPEN], code: 'JD2005',
|
|
302
|
+
retryable: false, reason: 'the database file could not be opened' }),
|
|
303
|
+
Object.freeze({ class: 'constraint', primaries: [RESULT.CONSTRAINT], code: 'JD2005',
|
|
304
|
+
retryable: false, reason: 'the database rejected the operation' }),
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
const FALLBACK = Object.freeze({ class: 'error', code: 'JD2005', retryable: false,
|
|
308
|
+
reason: 'the database rejected the operation' });
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* PostgreSQL SQLSTATEs, into the SAME classes. Exact codes first, then
|
|
312
|
+
* the two-character class for everything else in a family — which is
|
|
313
|
+
* how a server-side condition nobody enumerated still lands somewhere
|
|
314
|
+
* honest rather than in the fallback.
|
|
315
|
+
*
|
|
316
|
+
* `duplicate` is decided by the calling path, exactly as it is on
|
|
317
|
+
* SQLite: a `unique_violation` is only the KEY's collision when the
|
|
318
|
+
* constraint the server names is the key's own.
|
|
319
|
+
*/
|
|
320
|
+
const SQLSTATE = Object.freeze({
|
|
321
|
+
// 23 — integrity constraint violation
|
|
322
|
+
'23505': { class: 'constraint', code: 'JD2005', retryable: false,
|
|
323
|
+
reason: 'the database rejected the operation' },
|
|
324
|
+
// 40 — transaction rollback: both are the caller's to retry
|
|
325
|
+
'40001': { class: 'busy', code: 'JD2005', retryable: true,
|
|
326
|
+
reason: 'the transaction could not be serialized' },
|
|
327
|
+
'40P01': { class: 'busy', code: 'JD2005', retryable: true,
|
|
328
|
+
reason: 'the transaction deadlocked' },
|
|
329
|
+
// 55 — object not in prerequisite state
|
|
330
|
+
'55P03': { class: 'busy', code: 'JD2005', retryable: true,
|
|
331
|
+
reason: 'the database is busy or locked' },
|
|
332
|
+
'55006': { class: 'busy', code: 'JD2005', retryable: true,
|
|
333
|
+
reason: 'the database is busy or locked' },
|
|
334
|
+
// 57 — operator intervention
|
|
335
|
+
'57014': { class: 'cancelled', code: 'JD2089', retryable: false,
|
|
336
|
+
reason: 'the statement was cancelled by the server' },
|
|
337
|
+
'57P01': { class: 'connection', code: 'JD2087', retryable: true,
|
|
338
|
+
reason: 'the connection to the database was lost' },
|
|
339
|
+
'57P02': { class: 'connection', code: 'JD2087', retryable: true,
|
|
340
|
+
reason: 'the connection to the database was lost' },
|
|
341
|
+
'57P03': { class: 'connection', code: 'JD2087', retryable: true,
|
|
342
|
+
reason: 'the connection to the database was lost' },
|
|
343
|
+
// 53 — insufficient resources
|
|
344
|
+
'53100': { class: 'full', code: 'JD2082', retryable: false,
|
|
345
|
+
reason: 'the database or its disk is full' },
|
|
346
|
+
'53300': { class: 'busy', code: 'JD2005', retryable: true,
|
|
347
|
+
reason: 'the database is busy or locked' },
|
|
348
|
+
// 58 — system error
|
|
349
|
+
'58030': { class: 'io', code: 'JD2084', retryable: false, reason: 'a disk I/O error' },
|
|
350
|
+
// 25 — invalid transaction state
|
|
351
|
+
'25P02': { class: 'aborted', code: 'JD2088', retryable: false,
|
|
352
|
+
reason: 'the transaction was aborted by an earlier failure in it' },
|
|
353
|
+
'25006': { class: 'readonly', code: 'JD2083', retryable: false,
|
|
354
|
+
reason: 'the database is read-only' },
|
|
355
|
+
// 3D/3F — the catalog or schema the connection named does not exist
|
|
356
|
+
'3D000': { class: 'cantopen', code: 'JD2005', retryable: false,
|
|
357
|
+
reason: 'the database could not be opened' },
|
|
358
|
+
'3F000': { class: 'cantopen', code: 'JD2005', retryable: false,
|
|
359
|
+
reason: 'the database could not be opened' },
|
|
360
|
+
// 42501 — insufficient privilege reads as read-only: the operation is
|
|
361
|
+
// refused for want of write rights, which is what the class means
|
|
362
|
+
'42501': { class: 'readonly', code: 'JD2083', retryable: false,
|
|
363
|
+
reason: 'the database is read-only' },
|
|
364
|
+
// XX — internal error
|
|
365
|
+
'XX001': { class: 'corrupt', code: 'JD2085', retryable: false,
|
|
366
|
+
reason: 'the database file is corrupt or not a database' },
|
|
367
|
+
'XX002': { class: 'corrupt', code: 'JD2085', retryable: false,
|
|
368
|
+
reason: 'the database file is corrupt or not a database' },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/** The family fallbacks, by SQLSTATE class (the first two characters). */
|
|
372
|
+
const SQLSTATE_FAMILY = Object.freeze({
|
|
373
|
+
'23': { class: 'constraint', code: 'JD2005', retryable: false,
|
|
374
|
+
reason: 'the database rejected the operation' },
|
|
375
|
+
'40': { class: 'busy', code: 'JD2005', retryable: true,
|
|
376
|
+
reason: 'the transaction could not be completed' },
|
|
377
|
+
'08': { class: 'connection', code: 'JD2087', retryable: true,
|
|
378
|
+
reason: 'the connection to the database was lost' },
|
|
379
|
+
'53': { class: 'full', code: 'JD2082', retryable: false,
|
|
380
|
+
reason: 'the database or its disk is full' },
|
|
381
|
+
'55': { class: 'busy', code: 'JD2005', retryable: true,
|
|
382
|
+
reason: 'the database is busy or locked' },
|
|
383
|
+
'57': { class: 'connection', code: 'JD2087', retryable: true,
|
|
384
|
+
reason: 'the connection to the database was lost' },
|
|
385
|
+
'58': { class: 'io', code: 'JD2084', retryable: false, reason: 'a disk I/O error' },
|
|
386
|
+
'XX': { class: 'corrupt', code: 'JD2085', retryable: false,
|
|
387
|
+
reason: 'the database file is corrupt or not a database' },
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
/** A five-character SQLSTATE, or `null` for an error that carries none. */
|
|
391
|
+
const SQLSTATE_SHAPE = /^[0-9A-Z]{5}$/;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {any} error
|
|
395
|
+
* @returns {string | null}
|
|
396
|
+
*/
|
|
397
|
+
function sqlStateOf(error) {
|
|
398
|
+
if (error === null || typeof error !== 'object') return null;
|
|
399
|
+
const code = error.code;
|
|
400
|
+
return typeof code === 'string' && SQLSTATE_SHAPE.test(code) ? code : null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The numeric result code a binding attached, or `null` for an error
|
|
405
|
+
* that is not a driver's.
|
|
406
|
+
* @param {any} error
|
|
407
|
+
* @returns {number | null}
|
|
408
|
+
*/
|
|
409
|
+
function resultCodeOf(error) {
|
|
410
|
+
if (error === null || typeof error !== 'object') return null;
|
|
411
|
+
for (const member of ['errcode', 'errno', 'resultCode']) {
|
|
412
|
+
const value = error[member];
|
|
413
|
+
if (typeof value === 'number' && Number.isInteger(value)) return value;
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Whether an error is a SQLite driver's own: it carries a numeric
|
|
420
|
+
* result code, or the shape node:sqlite gives its errors.
|
|
421
|
+
* @param {any} error
|
|
422
|
+
* @returns {boolean}
|
|
423
|
+
*/
|
|
424
|
+
export function isDriverError(error) {
|
|
425
|
+
return resultCodeOf(error) !== null
|
|
426
|
+
|| sqlStateOf(error) !== null
|
|
427
|
+
|| error?.code === 'ERR_SQLITE_ERROR'
|
|
428
|
+
|| error?.name === 'SQLiteError' || error?.name === 'SQLite3Error';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Classify a driver failure.
|
|
433
|
+
* @param {any} error - the driver's error
|
|
434
|
+
* @param {{ table: string, column: string }} [unique] - the unique key
|
|
435
|
+
* a write was inserting under, so its collision classifies as
|
|
436
|
+
* `duplicate` (the `JD2001` every insert path reports)
|
|
437
|
+
* @returns {{ class: string, code: string | null, retryable: boolean, reason: string }}
|
|
438
|
+
*/
|
|
439
|
+
export function classifyDriverError(error, unique = undefined) {
|
|
440
|
+
const state = sqlStateOf(error);
|
|
441
|
+
if (state !== null) return classifySqlState(state, error, unique);
|
|
442
|
+
const extended = resultCodeOf(error);
|
|
443
|
+
const primary = extended === null ? null : extended & 0xff;
|
|
444
|
+
const message = typeof error?.message === 'string' ? error.message : '';
|
|
445
|
+
// the key's own collision: a PRIMARY KEY conflict (1555), or a UNIQUE
|
|
446
|
+
// conflict whose message names the key column — a unique INDEX over
|
|
447
|
+
// another column is a constraint failure, not a duplicate key
|
|
448
|
+
if (unique !== undefined && (extended === 1555
|
|
449
|
+
|| (message.includes('UNIQUE constraint failed') && message.includes(`${unique.table}.${unique.column}`)))) {
|
|
450
|
+
return { class: 'duplicate', code: 'JD2001', retryable: false, reason: 'the key is already present' };
|
|
451
|
+
}
|
|
452
|
+
if (primary === RESULT.ERROR && message.includes('integer overflow')) {
|
|
453
|
+
return { class: 'overflow', code: null, retryable: false,
|
|
454
|
+
reason: 'an integer aggregate overflowed int64' };
|
|
455
|
+
}
|
|
456
|
+
// a locked database reported by a binding that attaches no code
|
|
457
|
+
if (primary === null && /database is locked|database table is locked/.test(message)) {
|
|
458
|
+
return { ...CLASSES[0] };
|
|
459
|
+
}
|
|
460
|
+
for (const row of CLASSES) {
|
|
461
|
+
if (primary !== null && row.primaries.includes(primary))
|
|
462
|
+
return { class: row.class, code: row.code, retryable: row.retryable, reason: row.reason };
|
|
463
|
+
}
|
|
464
|
+
return { ...FALLBACK };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* The PostgreSQL half: a SQLSTATE into the same closed classes. The
|
|
469
|
+
* server names the constraint it rejected against, so the key's own
|
|
470
|
+
* collision is decided by NAME rather than by parsing a message —
|
|
471
|
+
* `<table>_pkey` is the primary key a collection's key column carries,
|
|
472
|
+
* and a unique INDEX over another column stays a constraint failure.
|
|
473
|
+
* @param {string} state
|
|
474
|
+
* @param {any} error
|
|
475
|
+
* @param {{ table: string, column: string }} [unique]
|
|
476
|
+
* @returns {{ class: string, code: string | null, retryable: boolean, reason: string }}
|
|
477
|
+
*/
|
|
478
|
+
function classifySqlState(state, error, unique) {
|
|
479
|
+
if (state === '23505' && unique !== undefined) {
|
|
480
|
+
const constraint = typeof error?.constraint === 'string' ? error.constraint : '';
|
|
481
|
+
if (constraint === `${unique.table}_pkey` || constraint === `${unique.table}_${unique.column}_key`) {
|
|
482
|
+
return { class: 'duplicate', code: 'JD2001', retryable: false,
|
|
483
|
+
reason: 'the key is already present' };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// an integer that will not fit the column: SQLite reports it as a
|
|
487
|
+
// generic error naming the overflow, PostgreSQL as numeric_value_out_of_range
|
|
488
|
+
if (state === '22003') {
|
|
489
|
+
return { class: 'overflow', code: null, retryable: false,
|
|
490
|
+
reason: 'an integer aggregate overflowed int64' };
|
|
491
|
+
}
|
|
492
|
+
const exact = SQLSTATE[state];
|
|
493
|
+
if (exact !== undefined) return { ...exact };
|
|
494
|
+
const family = SQLSTATE_FAMILY[state.slice(0, 2)];
|
|
495
|
+
if (family !== undefined) return { ...family };
|
|
496
|
+
return { ...FALLBACK };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Wrap a driver failure as the coded runtime error its class calls
|
|
501
|
+
* for, with `class` and `retryable` as own members and the original as
|
|
502
|
+
* `cause`. Anything that is not a driver's own error — a coded refusal
|
|
503
|
+
* of this package or of the query engine (a `JQ` error thrown inside a
|
|
504
|
+
* pushed user function), an API-misuse `TypeError` — is the error, and
|
|
505
|
+
* passes through untouched.
|
|
506
|
+
* @param {any} error
|
|
507
|
+
* @param {{ docPath?: string, collection?: string, key?: string | number,
|
|
508
|
+
* unique?: { table: string, column: string },
|
|
509
|
+
* duplicateReason?: string, code?: string, reason?: string,
|
|
510
|
+
* always?: boolean }} details - `duplicateReason` is the `JD2001`
|
|
511
|
+
* sentence the calling path composes (it names the key); `code` and
|
|
512
|
+
* `reason` let a lifecycle that owns its failure code (a maintenance
|
|
513
|
+
* operation's `JD2078`) keep its code and leading sentence while the
|
|
514
|
+
* class and the verdict still come from the one table; `always`
|
|
515
|
+
* wraps even a failure that is not a driver's (a closed handle's
|
|
516
|
+
* state error, a file-system refusal) under that lifecycle code, with
|
|
517
|
+
* `class: 'error'` — a lifecycle that promises a coded failure keeps
|
|
518
|
+
* the promise for every uncoded error it meets
|
|
519
|
+
* @returns {Error}
|
|
520
|
+
*/
|
|
521
|
+
export function wrapDriverError(error, details = {}) {
|
|
522
|
+
if (error instanceof TransactionFailure) {
|
|
523
|
+
let first = error;
|
|
524
|
+
const seen = new Set();
|
|
525
|
+
while (first instanceof TransactionFailure) {
|
|
526
|
+
if (seen.has(first)) return error;
|
|
527
|
+
seen.add(first);
|
|
528
|
+
first = first.errors[0];
|
|
529
|
+
}
|
|
530
|
+
const primary = wrapDriverError(first, details);
|
|
531
|
+
if (typeof primary?.code === 'string' && /^J[A-Z]\d{4}$/.test(primary.code)) {
|
|
532
|
+
// Keep the original and the cleanup failure together, while callers
|
|
533
|
+
// continue to branch on the original classified failure and its cause.
|
|
534
|
+
for (const member of ['code', 'reason', 'class', 'retryable', 'docPath', 'collection', 'key', 'cause']) {
|
|
535
|
+
if (Object.hasOwn(primary, member)) error[member] = primary[member];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return error;
|
|
539
|
+
}
|
|
540
|
+
if (typeof error?.code === 'string' && /^J[A-Z]\d{4}$/.test(error.code)) return error;
|
|
541
|
+
if (!isDriverError(error)) {
|
|
542
|
+
if (details.always !== true) return error;
|
|
543
|
+
const generic = new DbRuntimeError(details.code ?? 'JD2005',
|
|
544
|
+
`${details.reason ?? FALLBACK.reason}: ${error?.message ?? String(error)}`, { cause: error });
|
|
545
|
+
generic.class = 'error';
|
|
546
|
+
generic.retryable = false;
|
|
547
|
+
return generic;
|
|
548
|
+
}
|
|
549
|
+
const classified = classifyDriverError(error, details.unique);
|
|
550
|
+
const message = error?.message ?? String(error);
|
|
551
|
+
/** @type {any} */
|
|
552
|
+
const own = { cause: error };
|
|
553
|
+
if (details.docPath !== undefined) own.docPath = details.docPath;
|
|
554
|
+
if (details.collection !== undefined) own.collection = details.collection;
|
|
555
|
+
if (details.key !== undefined) own.key = details.key;
|
|
556
|
+
const wrapped = classified.class === 'duplicate'
|
|
557
|
+
? new DbRuntimeError('JD2001', details.duplicateReason ?? classified.reason, own)
|
|
558
|
+
: new DbRuntimeError(details.code ?? classified.code ?? 'JD2005',
|
|
559
|
+
`${details.reason ?? classified.reason}: ${message}`, own);
|
|
560
|
+
wrapped.class = classified.class;
|
|
561
|
+
wrapped.retryable = classified.retryable;
|
|
562
|
+
return wrapped;
|
|
563
|
+
}
|
|
564
|
+
|
|
181
565
|
export class DbRuntimeError extends CodedError {
|
|
182
566
|
/**
|
|
183
567
|
* @param {string} code
|
|
@@ -201,3 +585,22 @@ export class DbRuntimeError extends CodedError {
|
|
|
201
585
|
if (details?.errors !== undefined) this.errors = details.errors;
|
|
202
586
|
}
|
|
203
587
|
}
|
|
588
|
+
|
|
589
|
+
/** Cloneable driver errors keep the native classification fields.
|
|
590
|
+
* @param {any} error @returns {any}
|
|
591
|
+
*/
|
|
592
|
+
export function cloneDriverError(error) {
|
|
593
|
+
const out = { message: String(error?.message ?? error), name: String(error?.name ?? 'Error') };
|
|
594
|
+
for (const key of ['code', 'errcode', 'errstr', 'errno', 'resultCode', 'class', 'retryable', 'generation', 'depth']) {
|
|
595
|
+
const value = error?.[key];
|
|
596
|
+
if (['string', 'number', 'boolean'].includes(typeof value)) out[key] = value;
|
|
597
|
+
}
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Convert a returned SQLite C result into the one native error vocabulary.
|
|
602
|
+
* @param {number} rc @param {string} operation
|
|
603
|
+
*/
|
|
604
|
+
export function sqliteResultError(rc, operation) {
|
|
605
|
+
return Object.assign(new Error(`${operation} failed (${rc})`), { resultCode: rc });
|
|
606
|
+
}
|