@jarenjs/db 0.73.0 → 0.83.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/ARCHITECTURE.md +70 -7
- package/README.md +69 -6
- package/docs/HOSTS.md +17 -0
- package/docs/JOBS-FORMAT.md +26 -0
- package/docs/LIVE-FORMAT.md +52 -13
- package/docs/MIGRATION-FORMAT.md +34 -0
- package/docs/MODEL-FORMAT.md +163 -15
- package/docs/NATIVE-PLANS.md +111 -0
- package/docs/REPLICATION-FORMAT.md +19 -13
- package/docs/SEARCH.md +55 -0
- package/package.json +8 -4
- package/schemas/jaren-migration.draft-07.schema.json +54 -5
- package/schemas/jaren-migration.schema.json +49 -0
- package/schemas/jaren-model.authoring.schema.json +360 -0
- package/schemas/jaren-model.draft-07.schema.json +128 -0
- package/schemas/jaren-model.schema.json +128 -0
- package/src/algebra.js +26 -4
- package/src/backup.js +12 -7
- package/src/cursor.js +27 -4
- package/src/dag-job.js +2 -1
- package/src/ddl.js +13 -0
- package/src/derive.js +14 -3
- package/src/dialect.js +12 -0
- package/src/dialects/check-read.js +151 -0
- package/src/dialects/invariant-sql.js +117 -0
- package/src/dialects/postgres.js +28 -4
- package/src/dialects/sqlite.js +23 -3
- package/src/driver.js +1 -0
- package/src/drivers/bun.js +22 -4
- package/src/emit.js +133 -25
- package/src/entity.js +98 -41
- package/src/errors.js +8 -0
- package/src/graph.js +8 -1
- package/src/index.js +3 -0
- package/src/introspect.js +81 -12
- package/src/invariants.js +45 -0
- package/src/jobs.js +39 -6
- package/src/live-nested.js +27 -10
- package/src/live.js +51 -136
- package/src/migrate.js +136 -22
- package/src/model.js +12 -0
- package/src/mutation.js +165 -0
- package/src/physical.js +147 -0
- package/src/plan.js +275 -64
- package/src/query.js +175 -78
- package/src/search.js +144 -0
- package/src/sql.js +60 -0
- package/src/store.js +49 -13
- package/src/tracker.js +63 -39
- package/src/window.js +1 -0
- package/types/index.d.ts +59 -4
- package/types/search.d.ts +20 -0
- package/types/typed.d.ts +1 -0
package/src/backup.js
CHANGED
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
* platform reports progress (`rate` pages per step): the platform
|
|
21
21
|
* itself takes no signal, so the signal is checked in the progress
|
|
22
22
|
* callback and a throw there is what stops the pump. There is no
|
|
23
|
-
* progress event
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* guaranteed terminal progress event — the platform's completion signal
|
|
24
|
+
* is the resolved copy, which answers the page total. Cancellation is
|
|
25
|
+
* checked again before publication, including after a terminal event.
|
|
26
26
|
*
|
|
27
27
|
* This module imports no runtime builtin: the copy, the rename and the
|
|
28
|
-
* removal are the driver's primitives (`connection.backup`),
|
|
29
|
-
*
|
|
28
|
+
* removal are the driver's primitives (`connection.backup`), supplied by
|
|
29
|
+
* Node online backup and Bun serialized snapshots.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { DbRuntimeError, wrapDriverError } from './errors.js';
|
|
@@ -139,7 +139,8 @@ export function createBackup({ connection, readOnly, gated, checkpoint, random,
|
|
|
139
139
|
};
|
|
140
140
|
let copying;
|
|
141
141
|
try {
|
|
142
|
-
|
|
142
|
+
const copy = () => files.copy(tmpPath, rate === undefined ? { progress } : { rate, progress });
|
|
143
|
+
copying = files.snapshot === true ? gated(copy, 'a snapshot backup') : copy();
|
|
143
144
|
}
|
|
144
145
|
catch (error) {
|
|
145
146
|
return discard(aborted ?? failed(error));
|
|
@@ -153,7 +154,11 @@ export function createBackup({ connection, readOnly, gated, checkpoint, random,
|
|
|
153
154
|
attempt(() => files.rename(tmpPath, targetPath), failed),
|
|
154
155
|
() => Object.freeze({ path: targetPath, pages: Number(pages), checkpoint: checkpointed ?? null }));
|
|
155
156
|
return toPromise(copying)
|
|
156
|
-
.then((pages) =>
|
|
157
|
+
.then((pages) => {
|
|
158
|
+
if (signal?.aborted === true) { aborted = cancelled(signal); throw aborted; }
|
|
159
|
+
callable(options, 'the temporary file was removed and the target path was not written');
|
|
160
|
+
return toPromise(publish(pages));
|
|
161
|
+
})
|
|
157
162
|
.catch((error) => discard(aborted ?? failed(error)));
|
|
158
163
|
});
|
|
159
164
|
},
|
package/src/cursor.js
CHANGED
|
@@ -345,6 +345,12 @@ export function admitSyncCursor(cursor, admit) {
|
|
|
345
345
|
/** The page size a page takes when none is given. */
|
|
346
346
|
export const PAGE_LIMIT_DEFAULT = 100;
|
|
347
347
|
|
|
348
|
+
/** Classify the cursor's byte, row and work refusals at structural adapter boundaries.
|
|
349
|
+
* @param {any} error @returns {boolean} */
|
|
350
|
+
export function isCursorBudgetError(error) {
|
|
351
|
+
return ['JD2073', 'JD2074', 'JD2076'].includes(error?.code);
|
|
352
|
+
}
|
|
353
|
+
|
|
348
354
|
/**
|
|
349
355
|
* Drain a cursor into ONE page: at most `limit` items, at most `maxBytes`
|
|
350
356
|
* serialised bytes (`null` for no byte bound), stopping at an item
|
|
@@ -357,9 +363,13 @@ export const PAGE_LIMIT_DEFAULT = 100;
|
|
|
357
363
|
* An item that does not fit beside earlier ones ends the page before
|
|
358
364
|
* it: `hasMore` is true and the continuation is the last delivered
|
|
359
365
|
* item's, so the next page starts at the item that did not fit.
|
|
360
|
-
* `hasMore` is otherwise decided by one peek past `limit`.
|
|
366
|
+
* `hasMore` is otherwise decided by one peek past `limit`. With
|
|
367
|
+
* `lookahead: false`, a full page reports `hasMore: null` and `work`
|
|
368
|
+
* records each consumed root and its serialized payload bytes, including
|
|
369
|
+
* the root that stopped the page at its byte boundary. Failures preserve
|
|
370
|
+
* those counters on the error; pages using the default keep their shape.
|
|
361
371
|
* @param {any} cursor - a `QueryCursor`
|
|
362
|
-
* @param {{ limit: number, maxBytes: number | null, after?: any,
|
|
372
|
+
* @param {{ limit: number, maxBytes: number | null, after?: any, lookahead?: boolean,
|
|
363
373
|
* sizeOf: (item: any) => number, continuationOf: (item: any) => any }} options
|
|
364
374
|
* @returns {any} value-or-promise, matching the cursor
|
|
365
375
|
*/
|
|
@@ -368,10 +378,13 @@ export function drainPage(cursor, options) {
|
|
|
368
378
|
const after = options.after ?? null;
|
|
369
379
|
const items = [];
|
|
370
380
|
let bytes = 0;
|
|
381
|
+
const measured = options.lookahead === false;
|
|
382
|
+
const work = { rows: 0, bytes: 0 };
|
|
371
383
|
let last = after;
|
|
372
384
|
let hasMore = false;
|
|
373
385
|
const finish = () => chain(cursor.return(), () => ({
|
|
374
386
|
items, continuation: items.length > 0 ? last : (hasMore ? after : null), hasMore,
|
|
387
|
+
...(measured ? { work: { ...work } } : {}),
|
|
375
388
|
}));
|
|
376
389
|
const consume = (pulled) => {
|
|
377
390
|
if (items.length >= limit) {
|
|
@@ -380,7 +393,8 @@ export function drainPage(cursor, options) {
|
|
|
380
393
|
}
|
|
381
394
|
if (pulled.done === true) return finish();
|
|
382
395
|
const item = pulled.value;
|
|
383
|
-
const size = maxBytes === null ? 0 : sizeOf(item);
|
|
396
|
+
const size = maxBytes === null && !measured ? 0 : sizeOf(item);
|
|
397
|
+
if (measured) { work.rows++; work.bytes += size; }
|
|
384
398
|
if (maxBytes !== null && bytes + size > maxBytes) {
|
|
385
399
|
if (items.length === 0) {
|
|
386
400
|
return chain(cursor.return(), () => {
|
|
@@ -393,6 +407,10 @@ export function drainPage(cursor, options) {
|
|
|
393
407
|
items.push(item);
|
|
394
408
|
bytes += size;
|
|
395
409
|
last = continuationOf(item);
|
|
410
|
+
if (options.lookahead === false && items.length === limit) {
|
|
411
|
+
hasMore = null;
|
|
412
|
+
return finish();
|
|
413
|
+
}
|
|
396
414
|
return null;
|
|
397
415
|
};
|
|
398
416
|
const step = () => {
|
|
@@ -403,7 +421,12 @@ export function drainPage(cursor, options) {
|
|
|
403
421
|
if (result !== null) return result;
|
|
404
422
|
}
|
|
405
423
|
};
|
|
406
|
-
return settling(step, () => cursor.return())
|
|
424
|
+
return settling(step, () => cursor.return(), (error) => {
|
|
425
|
+
// A refused boundary row was still consumed. Preserve that evidence
|
|
426
|
+
// through the owning error so a bounded caller cannot report zero work.
|
|
427
|
+
if (measured && error !== null && typeof error === 'object') error.work = { ...work };
|
|
428
|
+
return error;
|
|
429
|
+
});
|
|
407
430
|
}
|
|
408
431
|
|
|
409
432
|
/** The shared refusal for an indivisible item, including a replicated transaction.
|
package/src/dag-job.js
CHANGED
|
@@ -52,7 +52,7 @@ const fingerprint = (value) => hashContent(canonicalizeJson(value ?? null));
|
|
|
52
52
|
* concurrency?: number, pollInterval?: number, leaseMs?: number,
|
|
53
53
|
* owner?: string, renew?: boolean, onOutcome?: (event: any) => void,
|
|
54
54
|
* backoffBase?: number, backoffCap?: number,
|
|
55
|
-
* stopGraceMs?: number }} options
|
|
55
|
+
* stopGraceMs?: number, effectSafety?: (job: any, context: any) => any }} options
|
|
56
56
|
* @returns {{ start: () => any, stop: (options?: any) => Promise<any>, stats: () => any }}
|
|
57
57
|
*/
|
|
58
58
|
export function createDagJobRunner(store, options) {
|
|
@@ -215,6 +215,7 @@ export function createDagJobRunner(store, options) {
|
|
|
215
215
|
owner: options.owner,
|
|
216
216
|
renew: options.renew,
|
|
217
217
|
onOutcome: options.onOutcome,
|
|
218
|
+
effectSafety: options.effectSafety,
|
|
218
219
|
backoffBase: options.backoffBase,
|
|
219
220
|
backoffCap: options.backoffCap,
|
|
220
221
|
// the runner's declared default for `stop()` with no override; an
|
package/src/ddl.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* silently indexing the wrong thing.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { explainMapping } from './model.js';
|
|
17
18
|
import { analyzeQuery } from '@jarenjs/json/query';
|
|
18
19
|
import { DbCompileError } from './errors.js';
|
|
19
20
|
import { chain } from './driver.js';
|
|
@@ -902,6 +903,9 @@ export function verifyShape(connection, plan, collection, docPath) {
|
|
|
902
903
|
* columnNames: Set<string> }}
|
|
903
904
|
*/
|
|
904
905
|
export function planEntity(name, entityMapping, entities, dialect) {
|
|
906
|
+
if (entityMapping.document === false) return { table: entityMapping.table,
|
|
907
|
+
physical: { ...entityMapping, triggers: dialect.invariantTriggers?.(entityMapping, entities, dialect) ?? [] }, createSql: [], expected: { columns: [], indexes: [] },
|
|
908
|
+
columnNames: new Set(entityMapping.columns.map((c) => c.physical)) };
|
|
905
909
|
const storageType = (storage) => dialect.typeFor(storage, 'generated');
|
|
906
910
|
const keyType = (entityName) => {
|
|
907
911
|
const target = entities.entities[entityName];
|
|
@@ -1037,3 +1041,12 @@ export function planJoinTable(tableName, join, entities, dialect) {
|
|
|
1037
1041
|
},
|
|
1038
1042
|
};
|
|
1039
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
/** Plan explicit database-enforced persistence rules; never applies them.
|
|
1046
|
+
* @param {any} model @param {{ dialect: any }} options @returns {any[]} */
|
|
1047
|
+
export function planInvariants(model, options) {
|
|
1048
|
+
const mapping = explainMapping(model);
|
|
1049
|
+
const dialect = options.dialect;
|
|
1050
|
+
if (typeof dialect.invariantTriggers !== 'function') throw new DbCompileError('JD0005', 'this dialect cannot lower database invariants');
|
|
1051
|
+
return Object.values(mapping.entities).flatMap((entity) => dialect.invariantTriggers(entity, mapping, dialect));
|
|
1052
|
+
}
|
package/src/derive.js
CHANGED
|
@@ -383,12 +383,23 @@ export function cellNeighbourhood(cell) {
|
|
|
383
383
|
* external's bounding box, computed at bind time because a GeoJSON
|
|
384
384
|
* object is not a value any database can bind. `null` when the value
|
|
385
385
|
* has no box, which is what tells the caller to divert.
|
|
386
|
-
* @param {
|
|
387
|
-
* @param {any} value - the bound external
|
|
386
|
+
* @param {any} derived
|
|
387
|
+
* @param {any} value - the bound external for bboxAxis; all externals for circleAxis
|
|
388
388
|
* @returns {number | null}
|
|
389
389
|
*/
|
|
390
390
|
export function derivedSlotValue(derived, value) {
|
|
391
|
-
|
|
391
|
+
let box;
|
|
392
|
+
if (derived.kind === 'bboxAxis') box = bboxOf(value);
|
|
393
|
+
else if (derived.kind === 'circleAxis') {
|
|
394
|
+
const read = (input) => 'external' in input ? value[input.external] : input.literal;
|
|
395
|
+
const at = probePosition(read(derived.centre));
|
|
396
|
+
const radius = read(derived.radius);
|
|
397
|
+
if (at === null || typeof radius !== 'number' || !Number.isFinite(radius) || radius < 0)
|
|
398
|
+
return null;
|
|
399
|
+
box = probeCircleBox(at, radius);
|
|
400
|
+
if (box !== null && (box[0] < -180 || box[2] > 180)) return null;
|
|
401
|
+
}
|
|
402
|
+
else throw new TypeError(`unknown derived parameter kind '${derived.kind}'`);
|
|
392
403
|
return box === null ? null : box[BBOX_AT[derived.axis]];
|
|
393
404
|
}
|
|
394
405
|
|
package/src/dialect.js
CHANGED
|
@@ -185,6 +185,12 @@ function normalizeCapabilities(declared) {
|
|
|
185
185
|
* memberPathOf?: (expression: string) => (JsonPathSegment[] | null),
|
|
186
186
|
* expressionOf?: (expression: string, byName: Record<string, string>) => (any | null),
|
|
187
187
|
* readGenerated?: (rows: any[]) => { name: string, expression: string }[],
|
|
188
|
+
* physicalRead?: (codec: string, sql: string) => string,
|
|
189
|
+
* mutationRowGuard?: (count: string, limit: number) => string,
|
|
190
|
+
* codepoint?: (value: string) => string,
|
|
191
|
+
* physicalTypeMatches?: (codec: string, type: string) => boolean,
|
|
192
|
+
* invariantTriggers?: (mapping: any, all: any, dialect: any) => any[],
|
|
193
|
+
* readChecks?: (rows: any[]) => { name: string, column?: string, values?: any[] }[],
|
|
188
194
|
* introspect: { version: () => string, compileOptions: () => string,
|
|
189
195
|
* pragma: (name: string) => string,
|
|
190
196
|
* tableExists: () => string, columns: (table: string) => string,
|
|
@@ -640,6 +646,12 @@ export function createDialect(spec) {
|
|
|
640
646
|
* one that can read it.
|
|
641
647
|
*/
|
|
642
648
|
readGenerated: spec.readGenerated,
|
|
649
|
+
readChecks: spec.readChecks,
|
|
650
|
+
physicalRead: spec.physicalRead,
|
|
651
|
+
mutationRowGuard: spec.mutationRowGuard,
|
|
652
|
+
codepoint: spec.codepoint,
|
|
653
|
+
physicalTypeMatches: spec.physicalTypeMatches,
|
|
654
|
+
invariantTriggers: spec.invariantTriggers,
|
|
643
655
|
explainQuery: spec.explainQuery,
|
|
644
656
|
/** The plan narrative, one line per row the engine answered. */
|
|
645
657
|
explainLines: spec.explainLines,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** Recover only a complete scalar enum CHECK; unfamiliar SQL remains a loss. */
|
|
3
|
+
|
|
4
|
+
/** Tokenize catalog SQL without treating quoted text or comments as syntax.
|
|
5
|
+
* @param {string} sql @returns {{ kind: string, value: string }[]} */
|
|
6
|
+
export function sqlTokens(sql) {
|
|
7
|
+
const tokens = [];
|
|
8
|
+
for (let i = 0; i < sql.length;) {
|
|
9
|
+
const c = sql[i];
|
|
10
|
+
if (/\s/.test(c)) { i++; continue; }
|
|
11
|
+
if (sql.startsWith('--', i)) {
|
|
12
|
+
const end = sql.indexOf('\n', i + 2); i = end < 0 ? sql.length : end + 1; continue;
|
|
13
|
+
}
|
|
14
|
+
if (sql.startsWith('/*', i)) {
|
|
15
|
+
const end = sql.indexOf('*/', i + 2);
|
|
16
|
+
if (end < 0) return [];
|
|
17
|
+
i = end + 2; continue;
|
|
18
|
+
}
|
|
19
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
20
|
+
let value = '';
|
|
21
|
+
let closed = false;
|
|
22
|
+
for (i++; i < sql.length; i++) {
|
|
23
|
+
if (sql[i] !== c) { value += sql[i]; continue; }
|
|
24
|
+
if (sql[i + 1] === c) { value += c; i++; continue; }
|
|
25
|
+
i++; closed = true; break;
|
|
26
|
+
}
|
|
27
|
+
if (!closed) return [];
|
|
28
|
+
tokens.push({ kind: c === "'" ? 'string' : 'identifier', value });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const number = /^(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?/.exec(sql.slice(i));
|
|
32
|
+
if (number) { tokens.push({ kind: 'number', value: number[0] }); i += number[0].length; continue; }
|
|
33
|
+
const word = /^[A-Za-z_][A-Za-z_0-9$]*/.exec(sql.slice(i));
|
|
34
|
+
if (word) { tokens.push({ kind: 'word', value: word[0] }); i += word[0].length; continue; }
|
|
35
|
+
const value = sql.startsWith('::', i) ? '::' : c;
|
|
36
|
+
tokens.push({ kind: 'symbol', value }); i += value.length;
|
|
37
|
+
}
|
|
38
|
+
return tokens;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {any[]} tokens @returns {{ column: string, values: any[] } | null} */
|
|
42
|
+
function enumOf(tokens) {
|
|
43
|
+
let at = 0;
|
|
44
|
+
const take = (value) => {
|
|
45
|
+
const token = tokens[at];
|
|
46
|
+
if (token && token.kind !== 'string' && token.kind !== 'identifier'
|
|
47
|
+
&& token.value.toUpperCase() === value) { at++; return true; }
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
const scalar = () => {
|
|
51
|
+
if (take('(')) {
|
|
52
|
+
const value = scalar();
|
|
53
|
+
return value === undefined || !take(')') ? undefined : cast(value);
|
|
54
|
+
}
|
|
55
|
+
const negative = take('-');
|
|
56
|
+
const token = tokens[at++];
|
|
57
|
+
if (!token) return undefined;
|
|
58
|
+
let value;
|
|
59
|
+
if (token.kind === 'number') value = Number(`${negative ? '-' : ''}${token.value}`);
|
|
60
|
+
else if (negative) return undefined;
|
|
61
|
+
else if (token.kind === 'string') value = token.value;
|
|
62
|
+
else if (token.kind === 'word' && /^(true|false)$/i.test(token.value)) value = token.value.toLowerCase() === 'true';
|
|
63
|
+
else return undefined;
|
|
64
|
+
if (typeof value === 'number' && !Number.isFinite(value)) return undefined;
|
|
65
|
+
return cast(value);
|
|
66
|
+
};
|
|
67
|
+
const cast = (value) => {
|
|
68
|
+
if (!take('::')) return value;
|
|
69
|
+
// Only exact scalar casts emitted by the catalog are understood.
|
|
70
|
+
// Rounding casts could change the enum's members.
|
|
71
|
+
const type = tokens[at++];
|
|
72
|
+
if (type?.kind !== 'word') return undefined;
|
|
73
|
+
const name = type.value.toLowerCase();
|
|
74
|
+
if (typeof value === 'string' && name === 'text') return value;
|
|
75
|
+
if (typeof value === 'boolean' && name === 'boolean') return value;
|
|
76
|
+
// PostgreSQL renders negative constants as quoted numeric casts,
|
|
77
|
+
// e.g. ('-1'::integer)::numeric. Decode only finite numeric syntax.
|
|
78
|
+
if (typeof value === 'string' && ['numeric', 'integer', 'bigint', 'smallint'].includes(name)
|
|
79
|
+
&& /^-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/.test(value)) {
|
|
80
|
+
const number = Number(value);
|
|
81
|
+
if (!Number.isFinite(number) || (['integer', 'bigint', 'smallint'].includes(name)
|
|
82
|
+
&& !Number.isSafeInteger(number))) return undefined;
|
|
83
|
+
return number;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'number' && name === 'numeric') return value;
|
|
86
|
+
if (typeof value === 'number' && ['integer', 'bigint', 'smallint'].includes(name)
|
|
87
|
+
&& Number.isSafeInteger(value)) return value;
|
|
88
|
+
if (typeof value === 'number' && name === 'double' && take('PRECISION')) return value;
|
|
89
|
+
return undefined;
|
|
90
|
+
};
|
|
91
|
+
const expression = () => {
|
|
92
|
+
if (take('(')) {
|
|
93
|
+
const result = expression();
|
|
94
|
+
return result === null || !take(')') ? null : result;
|
|
95
|
+
}
|
|
96
|
+
const column = tokens[at++];
|
|
97
|
+
if (!column || !['identifier', 'word'].includes(column.kind)) return null;
|
|
98
|
+
const any = take('=');
|
|
99
|
+
// A singleton IN list is normalized to scalar equality by PostgreSQL.
|
|
100
|
+
if (any && !take('ANY')) {
|
|
101
|
+
const value = scalar();
|
|
102
|
+
return value === undefined ? null : { column: column.value, values: [value] };
|
|
103
|
+
}
|
|
104
|
+
if (any ? !(take('(') && take('ARRAY') && take('[')) : !(take('IN') && take('('))) return null;
|
|
105
|
+
const values = [];
|
|
106
|
+
do {
|
|
107
|
+
const value = scalar();
|
|
108
|
+
if (value === undefined) return null;
|
|
109
|
+
if (!values.includes(value)) values.push(value);
|
|
110
|
+
} while (take(','));
|
|
111
|
+
if (any ? !(take(']') && take(')')) : !take(')')) return null;
|
|
112
|
+
return { column: column.value, values };
|
|
113
|
+
};
|
|
114
|
+
const result = expression();
|
|
115
|
+
return at === tokens.length ? result : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** SQLite retains the CREATE text, including inline and table CHECKs.
|
|
119
|
+
* @param {any[]} rows @returns {any[]} neutral constraints */
|
|
120
|
+
export function sqliteChecks(rows) {
|
|
121
|
+
const checks = [];
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
const tokens = sqlTokens(String(row.sql ?? ''));
|
|
124
|
+
// A column's inherited collation changes IN equality even when the
|
|
125
|
+
// CHECK itself names no collation. Refuse conservatively per table.
|
|
126
|
+
const collated = tokens.some((token, i) => token.kind === 'word'
|
|
127
|
+
&& token.value.toUpperCase() === 'COLLATE'
|
|
128
|
+
&& tokens[i + 1]?.value.toUpperCase() !== 'BINARY');
|
|
129
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
130
|
+
if (tokens[i].kind !== 'word' || tokens[i].value.toUpperCase() !== 'CHECK'
|
|
131
|
+
|| tokens[i + 1]?.value !== '(') continue;
|
|
132
|
+
const start = i + 2;
|
|
133
|
+
let depth = 1;
|
|
134
|
+
for (i = start; i < tokens.length; i++) {
|
|
135
|
+
if (tokens[i].kind !== 'symbol') continue;
|
|
136
|
+
if (tokens[i].value === '(') depth++;
|
|
137
|
+
if (tokens[i].value === ')' && --depth === 0) break;
|
|
138
|
+
}
|
|
139
|
+
checks.push({ name: `check_${checks.length + 1}`,
|
|
140
|
+
...(collated ? null : enumOf(tokens.slice(start, i))) });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return checks;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** PostgreSQL exposes each CHECK expression separately.
|
|
147
|
+
* @param {any[]} rows @returns {any[]} neutral constraints */
|
|
148
|
+
export function postgresChecks(rows) {
|
|
149
|
+
return rows.map((row) => ({ name: String(row.name),
|
|
150
|
+
...(row.unsafe_collation ? null : enumOf(sqlTokens(String(row.expression ?? '')))) }));
|
|
151
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** SQLite trigger lowering for bounded query predicates over old/new columns. */
|
|
3
|
+
import { DbCompileError } from '../errors.js';
|
|
4
|
+
|
|
5
|
+
/** @param {any} mapping @param {any} all @param {any} dialect @returns {any[]} */
|
|
6
|
+
export function sqliteInvariantTriggers(mapping, all, dialect) {
|
|
7
|
+
const q = dialect.quoteIdentifier;
|
|
8
|
+
const sl = dialect.stringLiteral;
|
|
9
|
+
const fail = (why) => { throw new DbCompileError('JD0005', `database invariant: ${why}`); };
|
|
10
|
+
const rules = mapping.invariants ?? [];
|
|
11
|
+
const columns = mapping.columns;
|
|
12
|
+
let domains = new Set();
|
|
13
|
+
const scalar = (value, op) => {
|
|
14
|
+
if (typeof value === 'string' && value.startsWith('$.')) {
|
|
15
|
+
if (value === '$.op') return sl(op);
|
|
16
|
+
const match = /^\$\.(old|new)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
|
|
17
|
+
if (!match) return fail(`unsupported record path '${value}'`);
|
|
18
|
+
const column = columns.find((c) => c.name === match[2]);
|
|
19
|
+
if (!column || column.null === 'absent' || !['text', 'integer', 'number', 'boolean', 'date', 'datetime'].includes(column.codec)) return fail(`'${match[2]}' needs a scalar column codec`);
|
|
20
|
+
if ((op === 'insert' && match[1] === 'old') || (op === 'delete' && match[1] === 'new')) return fail('a property of an unavailable record is absent, not SQL NULL');
|
|
21
|
+
const ref = `${match[1].toUpperCase()}.${q(column.physical)}`;
|
|
22
|
+
const type = `typeof(${ref})`;
|
|
23
|
+
let valid = ['integer', 'number', 'boolean'].includes(column.codec)
|
|
24
|
+
? column.codec === 'number' ? `${type} IN ('integer', 'real') AND ${ref} BETWEEN -9007199254740991 AND 9007199254740991`
|
|
25
|
+
: `${type} = 'integer' AND ${column.codec === 'boolean' ? `${ref} IN (0, 1)` : `${ref} BETWEEN -9007199254740991 AND 9007199254740991`}`
|
|
26
|
+
: `${type} = 'text'`;
|
|
27
|
+
if (column.null === 'null') valid = `${ref} IS NULL OR (${valid})`;
|
|
28
|
+
domains.add(`(${valid})`);
|
|
29
|
+
return `(${ref} COLLATE BINARY)`;
|
|
30
|
+
}
|
|
31
|
+
if (value === null) return 'NULL';
|
|
32
|
+
if (typeof value === 'string') return sl(value);
|
|
33
|
+
if (typeof value === 'boolean') return value ? '1' : '0';
|
|
34
|
+
if (typeof value === 'number' && Number.isFinite(value) && (!Number.isInteger(value) || Number.isSafeInteger(value))) return String(value);
|
|
35
|
+
if (value && typeof value === 'object' && Object.keys(value).length === 1 && Object.hasOwn(value, '$const')) {
|
|
36
|
+
const literal = value.$const;
|
|
37
|
+
return typeof literal === 'string' ? sl(literal) : scalar(literal, op);
|
|
38
|
+
}
|
|
39
|
+
return fail('only scalar literals and old/new property paths can be lowered');
|
|
40
|
+
};
|
|
41
|
+
const typeOf = (value) => {
|
|
42
|
+
if (typeof value === 'string' && value.startsWith('$.')) {
|
|
43
|
+
if (value === '$.op') return 'string';
|
|
44
|
+
const name = value.slice(value.indexOf('.', 2) + 1);
|
|
45
|
+
const column = columns.find((c) => c.name === name);
|
|
46
|
+
return column?.codec === 'boolean' ? 'boolean'
|
|
47
|
+
: ['integer', 'number'].includes(column?.codec) ? 'number' : 'string';
|
|
48
|
+
}
|
|
49
|
+
if (value && typeof value === 'object' && Object.hasOwn(value, '$const')) return value.$const === null ? 'null' : typeof value.$const;
|
|
50
|
+
return value === null ? 'null' : typeof value;
|
|
51
|
+
};
|
|
52
|
+
const predicate = (node, op) => {
|
|
53
|
+
if (typeof node === 'boolean') return node ? '1' : '0';
|
|
54
|
+
if (!node || typeof node !== 'object' || Array.isArray(node) || Object.keys(node).length !== 1) return fail('predicate must be a bounded query expression');
|
|
55
|
+
const [key, args] = Object.entries(node)[0];
|
|
56
|
+
if (key === '$and' || key === '$or') {
|
|
57
|
+
if (!Array.isArray(args) || !args.length) return fail('logical predicates require operands');
|
|
58
|
+
return `(${args.map((a) => predicate(a, op)).join(key === '$and' ? ' AND ' : ' OR ')})`;
|
|
59
|
+
}
|
|
60
|
+
if (key === '$not') return `(NOT (${predicate(args, op)}))`;
|
|
61
|
+
const operators = { $eq: 'IS', $ne: 'IS NOT', $lt: '<', $le: '<=', $gt: '>', $ge: '>=' };
|
|
62
|
+
if (!operators[key] || !Array.isArray(args) || args.length !== 2) return fail(`unsupported predicate '${key}'`);
|
|
63
|
+
const left = scalar(args[0], op), right = scalar(args[1], op);
|
|
64
|
+
const lt = typeOf(args[0]), rt = typeOf(args[1]);
|
|
65
|
+
if (lt !== rt && lt !== 'null' && rt !== 'null') return key === '$ne' ? '1' : '0';
|
|
66
|
+
const compare = `(${left} ${operators[key]} ${right})`;
|
|
67
|
+
return key === '$eq' || key === '$ne' ? compare : `COALESCE(${compare}, 0)`;
|
|
68
|
+
};
|
|
69
|
+
const changed = columns.filter((c) => c.name !== mapping.version && !c.generated)
|
|
70
|
+
.map((c) => `OLD.${q(c.physical)} IS NOT NEW.${q(c.physical)}`).join(' OR ') || '0';
|
|
71
|
+
const out = [];
|
|
72
|
+
const groups = new Map();
|
|
73
|
+
const add = (op, stage, rule, when, body) => {
|
|
74
|
+
const key = `${op}_${stage}`;
|
|
75
|
+
if (!groups.has(key)) groups.set(key, { op, stage, rules: [], bodies: [] });
|
|
76
|
+
const group = groups.get(key);
|
|
77
|
+
group.rules.push(rule); group.bodies.push(`${body}${when ? ` WHERE ${when}` : ''};`);
|
|
78
|
+
};
|
|
79
|
+
for (const rule of rules) {
|
|
80
|
+
if (rule.enforcement !== 'database') continue;
|
|
81
|
+
if (mapping.document !== false || mapping.kind === 'view') fail('database rules require a writable explicit column layout');
|
|
82
|
+
for (const op of rule.on) {
|
|
83
|
+
domains = new Set();
|
|
84
|
+
const assertion = predicate(rule.assert, op);
|
|
85
|
+
if (rule.audit !== undefined) {
|
|
86
|
+
const target = all.entities[rule.audit.entity];
|
|
87
|
+
if (!target || target.document !== false || target.kind === 'view') fail('audit target must be a mapped application table');
|
|
88
|
+
if (target === mapping || (target.invariants ?? []).some((r) => r.audit)) fail('recursive or chained audit effects are refused');
|
|
89
|
+
const names = [], values = [];
|
|
90
|
+
for (const [member, expression] of Object.entries(rule.audit.values)) {
|
|
91
|
+
const column = target.columns.find((c) => c.name === member);
|
|
92
|
+
if (!column || column.generated || !['text', 'integer', 'number', 'boolean'].includes(column.codec)) fail(`audit member '${member}' needs a writable scalar codec`);
|
|
93
|
+
const expected = column.codec === 'text' ? 'string' : column.codec === 'boolean' ? 'boolean' : 'number';
|
|
94
|
+
const actual = typeOf(expression);
|
|
95
|
+
if (actual !== expected && actual !== 'null') fail(`audit member '${member}' needs a matching scalar type`);
|
|
96
|
+
const value = scalar(expression, op);
|
|
97
|
+
if (column.null === 'reject') domains.add(`(${value} IS NOT NULL)`);
|
|
98
|
+
names.push(q(column.physical)); values.push(value);
|
|
99
|
+
}
|
|
100
|
+
if (!names.length) fail('audit values cannot be empty');
|
|
101
|
+
add(op, 'AFTER', rule.name, op === 'update' ? `(${changed})` : '',
|
|
102
|
+
`INSERT INTO ${q(target.table)} (${names.join(', ')}) SELECT ${values.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
const when = `${op === 'update' ? `(${changed}) AND ` : ''}(${[...domains, assertion].join(' AND ')}) IS NOT TRUE`;
|
|
105
|
+
add(op, 'BEFORE', rule.name, when, `SELECT RAISE(ABORT, ${sl(`jaren invariant:${rule.name}`)})`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const op of ['insert', 'update', 'delete']) {
|
|
109
|
+
const checks = groups.get(`${op}_BEFORE`);
|
|
110
|
+
if (!checks) continue;
|
|
111
|
+
const bodies = [...checks.bodies, ...(groups.get(`${op}_AFTER`)?.bodies ?? [])];
|
|
112
|
+
const name = `_jaren_rule_${mapping.table.length}_${mapping.table}_${op}`;
|
|
113
|
+
out.push({ type: 'trigger', name, owner: mapping.table, rule: checks.rules.join(','),
|
|
114
|
+
sql: `CREATE TRIGGER ${q(name)} AFTER ${op.toUpperCase()} ON ${q(mapping.table)} BEGIN ${bodies.join(' ')} END` });
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
package/src/dialects/postgres.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
import { createDialect } from '../dialect.js';
|
|
41
41
|
import { readExpression } from './expression-read.js';
|
|
42
|
+
import { postgresChecks } from './check-read.js';
|
|
42
43
|
|
|
43
44
|
/** PostgreSQL truncates an identifier past this many BYTES, silently
|
|
44
45
|
* and with only a notice — so two long generated names would collide
|
|
@@ -519,6 +520,7 @@ export function postgresDialect(options = undefined) {
|
|
|
519
520
|
memberPathOf,
|
|
520
521
|
expressionOf,
|
|
521
522
|
// the catalog already answers one row per generated column
|
|
523
|
+
readChecks: postgresChecks,
|
|
522
524
|
readGenerated: (rows) => rows.map((row) => ({
|
|
523
525
|
name: String(row.name), expression: String(row.expression ?? ''),
|
|
524
526
|
})),
|
|
@@ -561,8 +563,11 @@ export function postgresDialect(options = undefined) {
|
|
|
561
563
|
// fact the shape check reads beside the name and the type
|
|
562
564
|
columns: (table) =>
|
|
563
565
|
'SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS type, '
|
|
564
|
-
+ "CASE WHEN a.attgenerated <> '' THEN 1 ELSE 0 END AS hidden "
|
|
566
|
+
+ "CASE WHEN a.attgenerated <> '' THEN 1 ELSE 0 END AS hidden, "
|
|
567
|
+
+ 'CASE WHEN a.attnotnull THEN 1 ELSE 0 END AS not_null, '
|
|
568
|
+
+ 'pg_get_expr(d.adbin, d.adrelid) AS default_value '
|
|
565
569
|
+ 'FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid '
|
|
570
|
+
+ 'LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum '
|
|
566
571
|
+ 'JOIN pg_namespace n ON n.oid = c.relnamespace '
|
|
567
572
|
+ `WHERE c.relname = ${stringLiteral(table)} AND ${inNamespace} `
|
|
568
573
|
+ 'AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum',
|
|
@@ -571,7 +576,8 @@ export function postgresDialect(options = undefined) {
|
|
|
571
576
|
// key's is the engine's own
|
|
572
577
|
indexes: (table) =>
|
|
573
578
|
'SELECT ci.relname AS name, CASE WHEN i.indisunique THEN 1 ELSE 0 END AS uniq, '
|
|
574
|
-
+ "CASE WHEN i.indisprimary THEN 'pk' ELSE 'c' END AS origin "
|
|
579
|
+
+ "CASE WHEN i.indisprimary THEN 'pk' ELSE 'c' END AS origin, "
|
|
580
|
+
+ 'CASE WHEN i.indpred IS NULL THEN 0 ELSE 1 END AS partial '
|
|
575
581
|
+ 'FROM pg_index i JOIN pg_class ci ON ci.oid = i.indexrelid '
|
|
576
582
|
+ 'JOIN pg_class ct ON ct.oid = i.indrelid '
|
|
577
583
|
+ 'JOIN pg_namespace n ON n.oid = ct.relnamespace '
|
|
@@ -584,7 +590,7 @@ export function postgresDialect(options = undefined) {
|
|
|
584
590
|
+ 'JOIN pg_class ci ON ci.oid = i.indexrelid '
|
|
585
591
|
+ 'JOIN pg_namespace n ON n.oid = ci.relnamespace '
|
|
586
592
|
+ 'CROSS JOIN LATERAL unnest(i.indkey::int2[]) WITH ORDINALITY AS k(attnum, ord) '
|
|
587
|
-
+ 'JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum '
|
|
593
|
+
+ 'LEFT JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.attnum '
|
|
588
594
|
+ `WHERE ci.relname = ${stringLiteral(index)} AND ${inNamespace} ORDER BY k.ord`,
|
|
589
595
|
// every table this store might own; a view is reported rather
|
|
590
596
|
// than derived, and the engine's own schemas are never in scope
|
|
@@ -594,6 +600,15 @@ export function postgresDialect(options = undefined) {
|
|
|
594
600
|
+ 'JOIN pg_namespace n ON n.oid = c.relnamespace '
|
|
595
601
|
+ `WHERE c.relkind IN ('r', 'p', 'v', 'm') AND ${inNamespace} `
|
|
596
602
|
+ 'ORDER BY type, c.relname',
|
|
603
|
+
objects: () =>
|
|
604
|
+
"SELECT 'trigger' AS type, t.tgname AS name, c.relname AS owner, "
|
|
605
|
+
+ 'pg_get_triggerdef(t.oid) AS sql FROM pg_trigger t '
|
|
606
|
+
+ 'JOIN pg_class c ON c.oid = t.tgrelid JOIN pg_namespace n ON n.oid = c.relnamespace '
|
|
607
|
+
+ `WHERE NOT t.tgisinternal AND ${inNamespace} UNION ALL `
|
|
608
|
+
+ "SELECT 'index', ci.relname, c.relname, pg_get_indexdef(i.indexrelid) "
|
|
609
|
+
+ 'FROM pg_index i JOIN pg_class c ON c.oid = i.indrelid '
|
|
610
|
+
+ 'JOIN pg_class ci ON ci.oid = i.indexrelid JOIN pg_namespace n ON n.oid = c.relnamespace '
|
|
611
|
+
+ `WHERE ${inNamespace} ORDER BY type, name`,
|
|
597
612
|
generated: (table) =>
|
|
598
613
|
'SELECT a.attname AS name, pg_get_expr(d.adbin, d.adrelid) AS expression '
|
|
599
614
|
+ 'FROM pg_attrdef d '
|
|
@@ -602,13 +617,22 @@ export function postgresDialect(options = undefined) {
|
|
|
602
617
|
+ 'JOIN pg_namespace n ON n.oid = c.relnamespace '
|
|
603
618
|
+ `WHERE c.relname = ${stringLiteral(table)} AND ${inNamespace} `
|
|
604
619
|
+ "AND a.attgenerated <> '' ORDER BY a.attnum",
|
|
620
|
+
checks: (table) =>
|
|
621
|
+
'SELECT c.conname AS name, pg_get_expr(c.conbin, c.conrelid) AS expression, '
|
|
622
|
+
+ 'EXISTS (SELECT 1 FROM pg_attribute ca JOIN pg_collation co ON co.oid = ca.attcollation '
|
|
623
|
+
+ 'WHERE ca.attrelid = c.conrelid AND ca.attnum = ANY(c.conkey) '
|
|
624
|
+
+ 'AND NOT co.collisdeterministic) AS unsafe_collation '
|
|
625
|
+
+ 'FROM pg_constraint c JOIN pg_class ct ON ct.oid = c.conrelid '
|
|
626
|
+
+ 'JOIN pg_namespace n ON n.oid = ct.relnamespace '
|
|
627
|
+
+ `WHERE c.contype = 'c' AND ct.relname = ${stringLiteral(table)} AND ${inNamespace} `
|
|
628
|
+
+ 'ORDER BY c.conname',
|
|
605
629
|
foreignKeyList: (table) => {
|
|
606
630
|
const action = (column) => `CASE ${column} `
|
|
607
631
|
+ "WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT' WHEN 'c' THEN 'CASCADE' "
|
|
608
632
|
+ "WHEN 'n' THEN 'SET NULL' WHEN 'd' THEN 'SET DEFAULT' ELSE 'NO ACTION' END";
|
|
609
633
|
return 'SELECT tt.relname AS target, sa.attname AS source_column, '
|
|
610
634
|
+ `ta.attname AS target_column, ${action('c.confdeltype')} AS on_delete, `
|
|
611
|
-
+ `${action('c.confupdtype')} AS on_update, k.ord - 1 AS seq `
|
|
635
|
+
+ `${action('c.confupdtype')} AS on_update, c.conname AS id, k.ord - 1 AS seq `
|
|
612
636
|
+ 'FROM pg_constraint c JOIN pg_class ct ON ct.oid = c.conrelid '
|
|
613
637
|
+ 'JOIN pg_namespace n ON n.oid = ct.relnamespace '
|
|
614
638
|
+ 'JOIN pg_class tt ON tt.oid = c.confrelid '
|
package/src/dialects/sqlite.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { createDialect } from '../dialect.js';
|
|
12
12
|
import { rtreeDdl } from './rtree-ddl.js';
|
|
13
13
|
import { readExpression } from './expression-read.js';
|
|
14
|
+
import { sqliteInvariantTriggers } from './invariant-sql.js';
|
|
15
|
+
import { sqliteChecks } from './check-read.js';
|
|
14
16
|
|
|
15
17
|
/** @param {string} s */
|
|
16
18
|
function quoteIdentifier(s) {
|
|
@@ -427,6 +429,17 @@ export const sqliteDialect = createDialect({
|
|
|
427
429
|
: `PRAGMA integrity_check(${pragmaValue(limit)})`),
|
|
428
430
|
optimize: () => 'PRAGMA optimize',
|
|
429
431
|
},
|
|
432
|
+
invariantTriggers: sqliteInvariantTriggers,
|
|
433
|
+
// The invalid path is evaluated only on overflow and carries a distinct
|
|
434
|
+
// marker through SQLite's error boundary, before any source row is written.
|
|
435
|
+
codepoint: (value) => `(${value} COLLATE BINARY)`,
|
|
436
|
+
mutationRowGuard: (count, limit) => `CASE WHEN ${count} > ${limit} `
|
|
437
|
+
+ "THEN json_extract('{}', 'jaren-mutation-row-bound') ELSE 1 END",
|
|
438
|
+
physicalRead: (codec, sql) => codec === 'bigint' ? `CAST(${sql} AS TEXT)`
|
|
439
|
+
: codec === 'blob-hex' ? `CASE WHEN ${sql} IS NULL THEN NULL ELSE hex(${sql}) END` : sql,
|
|
440
|
+
physicalTypeMatches: (codec, type) => (codec === 'blob-hex' ? /BLOB/
|
|
441
|
+
: ['integer', 'bigint', 'boolean', 'epoch-ms'].includes(codec) ? /INT/
|
|
442
|
+
: codec === 'number' ? /REAL|FLOA|DOUB/ : /TEXT|CHAR|CLOB/).test(type),
|
|
430
443
|
introspect: {
|
|
431
444
|
version: () => 'SELECT sqlite_version() AS version',
|
|
432
445
|
// the read-back of one configuration pragma: `PRAGMA name` answers
|
|
@@ -437,16 +450,17 @@ export const sqliteDialect = createDialect({
|
|
|
437
450
|
tableExists: () =>
|
|
438
451
|
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?",
|
|
439
452
|
columns: (table) =>
|
|
440
|
-
`SELECT name, type, hidden
|
|
453
|
+
`SELECT name, type, hidden, pk, "notnull" AS not_null, dflt_value AS default_value `
|
|
454
|
+
+ `FROM pragma_table_xinfo(${stringLiteral(table)}) ORDER BY cid`,
|
|
441
455
|
indexes: (table) =>
|
|
442
|
-
`SELECT name, "unique" AS uniq, origin FROM pragma_index_list(${stringLiteral(table)})`,
|
|
456
|
+
`SELECT name, "unique" AS uniq, origin, partial FROM pragma_index_list(${stringLiteral(table)})`,
|
|
443
457
|
indexColumns: (index) =>
|
|
444
458
|
`SELECT name FROM pragma_index_info(${stringLiteral(index)})`,
|
|
445
459
|
foreignKeysOn: () => 'SELECT foreign_keys AS enabled FROM pragma_foreign_keys',
|
|
446
460
|
dataVersion: () => 'SELECT data_version AS v FROM pragma_data_version',
|
|
447
461
|
foreignKeyList: (table) =>
|
|
448
462
|
`SELECT "table" AS target, "from" AS source_column, "to" AS target_column, `
|
|
449
|
-
+ `on_delete, on_update, seq FROM pragma_foreign_key_list(${stringLiteral(table)}) `
|
|
463
|
+
+ `on_delete, on_update, id, seq, match FROM pragma_foreign_key_list(${stringLiteral(table)}) `
|
|
450
464
|
+ 'ORDER BY id, seq',
|
|
451
465
|
// Every schema object one table owns, with the CREATE text SQLite
|
|
452
466
|
// stored verbatim. That text is where the physical facts no pragma
|
|
@@ -467,10 +481,16 @@ export const sqliteDialect = createDialect({
|
|
|
467
481
|
// the CREATE text is where a generated column's expression lives
|
|
468
482
|
generated: (table) =>
|
|
469
483
|
`SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ${stringLiteral(table)}`,
|
|
484
|
+
checks: (table) =>
|
|
485
|
+
`SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ${stringLiteral(table)}`,
|
|
470
486
|
// the whole declared schema, for shape-equality comparison after a
|
|
471
487
|
// rebuild: every object that carries SQL text, in a stable order
|
|
472
488
|
schemaDump: () =>
|
|
473
489
|
"SELECT type, name, tbl_name AS owner, sql FROM sqlite_schema "
|
|
474
490
|
+ "WHERE sql IS NOT NULL ORDER BY type, name",
|
|
491
|
+
objects: () =>
|
|
492
|
+
'SELECT type, name, tbl_name AS owner, sql FROM sqlite_schema '
|
|
493
|
+
+ "WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name",
|
|
475
494
|
},
|
|
495
|
+
readChecks: sqliteChecks,
|
|
476
496
|
});
|
package/src/driver.js
CHANGED
|
@@ -822,6 +822,7 @@ export function finishConnection(raw, dialect, synchronous, capabilities, queueT
|
|
|
822
822
|
// closed exactly as a statement is
|
|
823
823
|
backup: capabilities.backup === true
|
|
824
824
|
? Object.freeze({
|
|
825
|
+
snapshot: raw.backup.snapshot === true,
|
|
825
826
|
copy: (path, options) => { requireOpen(); return raw.backup.copy(path, options); },
|
|
826
827
|
rename: (from, to) => { requireOpen(); return raw.backup.rename(from, to); },
|
|
827
828
|
remove: (path) => { requireOpen(); return raw.backup.remove(path); },
|