@jarenjs/db 0.56.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +412 -56
- package/README.md +600 -57
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +169 -20
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +752 -64
- package/docs/REPLICATION-FORMAT.md +208 -0
- package/package.json +21 -7
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
- package/schemas/jaren-replication-snapshot.schema.json +83 -0
- package/schemas/jaren-replication.draft-07.schema.json +82 -0
- package/schemas/jaren-replication.schema.json +82 -0
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +230 -47
- package/src/cli.js +165 -59
- package/src/cursor.js +417 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- package/src/dialect.js +268 -113
- 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 +244 -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 +430 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/live-join.js +250 -0
- package/src/live-nested.js +120 -0
- package/src/live.js +18 -4
- package/src/logical-rows.js +90 -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/replication-format.js +115 -0
- package/src/replication.js +332 -0
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1567 -273
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1158 -27
- 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 +27 -4
- package/types/wasm.d.ts +14 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The R\*Tree mapping's own DDL — the second physical
|
|
4
|
+
* realization of a `derive: 'bbox'` column set (MODEL-FORMAT §2.1,
|
|
5
|
+
* `physical: 'rtree'`): a virtual table beside the collection and the
|
|
6
|
+
* three row triggers that keep it in sync.
|
|
7
|
+
*
|
|
8
|
+
* It lives beside the dialects rather than in the shared statement
|
|
9
|
+
* builder because every line of it is one engine family's spelling —
|
|
10
|
+
* `CREATE VIRTUAL TABLE ... USING`, and a trigger body between `BEGIN`
|
|
11
|
+
* and `END`. A dialect whose `capabilities.virtualTables` is false
|
|
12
|
+
* composes none of it, and the planner maps a `physical: 'rtree'`
|
|
13
|
+
* column set back onto the B-tree over the four edge columns instead.
|
|
14
|
+
*
|
|
15
|
+
* Parameterized by the spelling spec it belongs to, so the two specs
|
|
16
|
+
* that DO carry an R\*Tree share one implementation and still produce
|
|
17
|
+
* their own quoting, their own module name and their own column order.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The virtual-table DDL group for one spelling spec.
|
|
22
|
+
* @param {{ quoteIdentifier: (s: string) => string,
|
|
23
|
+
* rowIdentity: () => string,
|
|
24
|
+
* rtree: { module: string, columns: readonly string[] } }} spec
|
|
25
|
+
* @returns {Record<string, Function>}
|
|
26
|
+
*/
|
|
27
|
+
export function rtreeDdl(spec) {
|
|
28
|
+
const q = spec.quoteIdentifier;
|
|
29
|
+
return {
|
|
30
|
+
/**
|
|
31
|
+
* The second physical realization of a `derive: 'bbox'` column set
|
|
32
|
+
* (MODEL-FORMAT §2.1, `physical: 'rtree'`): an R\*Tree virtual
|
|
33
|
+
* table beside the collection, keyed by the collection's row id and
|
|
34
|
+
* carrying the four box edges as `(minx, maxx, miny, maxy)`.
|
|
35
|
+
*
|
|
36
|
+
* The coordinates are 32-bit floats rounded OUTWARD, so the stored
|
|
37
|
+
* box is a superset of the row's — no false negatives, which is
|
|
38
|
+
* what an implied conjunct needs, and the reason `$bbox-intersects`
|
|
39
|
+
* stops being exact under this mapping.
|
|
40
|
+
* @param {{ name: string }} shape
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
createVirtualTable({ name }) {
|
|
44
|
+
return `CREATE VIRTUAL TABLE ${q(name)} USING ${spec.rtree.module}(`
|
|
45
|
+
+ `${spec.rtree.columns.map(q).join(', ')})`;
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} name
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
dropVirtualTable(name) {
|
|
52
|
+
return `DROP TABLE ${q(name)}`;
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
dropTrigger(name) {
|
|
59
|
+
return `DROP TRIGGER ${q(name)}`;
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Fill an R\*Tree from the documents already stored — the migration
|
|
63
|
+
* step that turns a `columns` collection into an `rtree` one. The
|
|
64
|
+
* `IS NOT NULL` is §3.2's rule in SQL: a row with no bounded
|
|
65
|
+
* position is ABSENT from the index, not at `[0, 0]`.
|
|
66
|
+
* @param {{ table: string, virtualTable: string,
|
|
67
|
+
* edges: { name: string }[] }} shape
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
fillVirtualTable({ table, virtualTable, edges }) {
|
|
71
|
+
const columns = spec.rtree.columns;
|
|
72
|
+
const sources = [spec.rowIdentity(), ...edges.map((edge) => q(edge.name))];
|
|
73
|
+
return `INSERT INTO ${q(virtualTable)} (${columns.map(q).join(', ')}) `
|
|
74
|
+
+ `SELECT ${sources.join(', ')} FROM ${q(table)} `
|
|
75
|
+
+ `WHERE ${q(edges[0].name)} IS NOT NULL`;
|
|
76
|
+
},
|
|
77
|
+
/**
|
|
78
|
+
* The three triggers that keep an R\*Tree in sync with its
|
|
79
|
+
* collection — insert, update, delete — as DECLARED objects of the
|
|
80
|
+
* collection table.
|
|
81
|
+
*
|
|
82
|
+
* Declared, and not a second write path in JavaScript: a trigger is
|
|
83
|
+
* inside the writing transaction by construction (SQLite cannot
|
|
84
|
+
* separate them), no write path can bypass it (`insert`,
|
|
85
|
+
* `insertAllocated`, `upsert`, a translated patch, the patch
|
|
86
|
+
* fallback, a delete and a migration backfill all fire it), and it
|
|
87
|
+
* belongs to the collection table, so the existing declared-text
|
|
88
|
+
* drift check sees it for free.
|
|
89
|
+
*
|
|
90
|
+
* The body reads the DERIVED COLUMNS through `NEW` rather than
|
|
91
|
+
* restating the box expression, so the columns stay the box's one
|
|
92
|
+
* definition — and that text works unchanged on the stored-column
|
|
93
|
+
* branch, where those columns are ordinary ones.
|
|
94
|
+
*
|
|
95
|
+
* The `IS NOT NULL` guard is load-bearing: an R\*Tree coerces a
|
|
96
|
+
* `NULL` coordinate to `0.0` without complaint, so without it every
|
|
97
|
+
* unbounded document would land on Null Island instead of being
|
|
98
|
+
* absent (MODEL-FORMAT §3.2).
|
|
99
|
+
* @param {{ table: string, virtualTable: string, prefix: string,
|
|
100
|
+
* edges: { name: string }[] }} shape - `edges` are the four
|
|
101
|
+
* derived columns in `(w, e, s, n)` order, which is the order the
|
|
102
|
+
* virtual table's `(minx, maxx, miny, maxy)` carry
|
|
103
|
+
* @returns {{ name: string, sql: string }[]}
|
|
104
|
+
*/
|
|
105
|
+
createSyncTriggers({ table, virtualTable, prefix, edges }) {
|
|
106
|
+
const rid = spec.rowIdentity();
|
|
107
|
+
const target = `${q(virtualTable)} (${spec.rtree.columns.map(q).join(', ')})`;
|
|
108
|
+
const guard = `${q(edges[0].name)} IS NOT NULL`;
|
|
109
|
+
const values = (row) => [`${row}.${rid}`,
|
|
110
|
+
...edges.map((edge) => `${row}.${q(edge.name)}`)].join(', ');
|
|
111
|
+
return [
|
|
112
|
+
{ name: `${prefix}_ai`,
|
|
113
|
+
sql: `CREATE TRIGGER ${q(`${prefix}_ai`)} AFTER INSERT ON ${q(table)} `
|
|
114
|
+
+ `WHEN NEW.${guard} BEGIN `
|
|
115
|
+
+ `INSERT INTO ${target} VALUES (${values('NEW')}); END` },
|
|
116
|
+
// one trigger, not two: the old id leaves and the new box
|
|
117
|
+
// arrives only when it exists, so a document that loses its
|
|
118
|
+
// geometry leaves the index rather than keeping a stale box
|
|
119
|
+
{ name: `${prefix}_au`,
|
|
120
|
+
sql: `CREATE TRIGGER ${q(`${prefix}_au`)} AFTER UPDATE ON ${q(table)} BEGIN `
|
|
121
|
+
+ `DELETE FROM ${q(virtualTable)} WHERE ${q(spec.rtree.columns[0])} = OLD.${rid}; `
|
|
122
|
+
+ `INSERT INTO ${target} SELECT ${values('NEW')} WHERE NEW.${guard}; END` },
|
|
123
|
+
{ name: `${prefix}_ad`,
|
|
124
|
+
sql: `CREATE TRIGGER ${q(`${prefix}_ad`)} AFTER DELETE ON ${q(table)} BEGIN `
|
|
125
|
+
+ `DELETE FROM ${q(virtualTable)} WHERE ${q(spec.rtree.columns[0])} = OLD.${rid}; END` },
|
|
126
|
+
];
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
package/src/dialects/sqlite.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createDialect } from '../dialect.js';
|
|
12
|
+
import { rtreeDdl } from './rtree-ddl.js';
|
|
13
|
+
import { readExpression } from './expression-read.js';
|
|
12
14
|
|
|
13
15
|
/** @param {string} s */
|
|
14
16
|
function quoteIdentifier(s) {
|
|
@@ -63,8 +65,9 @@ function typeFor(schemaType, hint) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
|
-
* A guarded PRAGMA
|
|
67
|
-
* closed word
|
|
68
|
+
* A guarded PRAGMA word: a pragma's name and a keyword value (journal
|
|
69
|
+
* modes and the like) are closed word sets, never interpolated user
|
|
70
|
+
* text.
|
|
68
71
|
* @param {string} word
|
|
69
72
|
* @returns {string}
|
|
70
73
|
*/
|
|
@@ -74,20 +77,209 @@ function pragmaWord(word) {
|
|
|
74
77
|
return String(word);
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/**
|
|
81
|
+
* A guarded PRAGMA value: an integer spelled whole, or a keyword from a
|
|
82
|
+
* closed set. Anything else is refused here, so no unvalidated value
|
|
83
|
+
* can reach the statement text.
|
|
84
|
+
* @param {number | string} value
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function pragmaValue(value) {
|
|
88
|
+
if (typeof value === 'number') {
|
|
89
|
+
if (!Number.isFinite(value)) throw new TypeError(`not a PRAGMA value: ${value}`);
|
|
90
|
+
return String(Math.trunc(value));
|
|
91
|
+
}
|
|
92
|
+
return pragmaWord(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** SQLite's own per-row identity: the implicit `rowid` of every
|
|
96
|
+
* table this store creates, which is INSERTION order. */
|
|
97
|
+
const rowIdentity = () => '"rowid"';
|
|
98
|
+
|
|
99
|
+
/** The R*Tree module and the shape this store gives it: the row id and
|
|
100
|
+
* the four box edges in (minx, maxx, miny, maxy) order, which is the
|
|
101
|
+
* (w, e, s, n) a bbox index covers its columns in. The three shadow
|
|
102
|
+
* tables SQLite creates beside a virtual table are its own storage —
|
|
103
|
+
* deterministic from the name, and dropped with it. */
|
|
104
|
+
const RTREE = Object.freeze({
|
|
105
|
+
module: 'rtree',
|
|
106
|
+
columns: Object.freeze(['id', 'minx', 'maxx', 'miny', 'maxy']),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The schema type a declared column type came from. `INTEGER` carries
|
|
111
|
+
* both `integer` and `boolean` in this mapping and `ANY` carries no
|
|
112
|
+
* type at all, so the inverse is partial by construction — which is
|
|
113
|
+
* what the introspector's loss report exists to say.
|
|
114
|
+
* @param {string} declaredType
|
|
115
|
+
* @returns {string | undefined}
|
|
116
|
+
*/
|
|
117
|
+
function schemaTypeOf(declaredType) {
|
|
118
|
+
switch (String(declaredType).toUpperCase()) {
|
|
119
|
+
case 'TEXT': return 'string';
|
|
120
|
+
case 'INTEGER': return 'integer';
|
|
121
|
+
case 'REAL': return 'number';
|
|
122
|
+
default: return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The member path a generated column's expression reads, recovered
|
|
128
|
+
* from this dialect's own spelling: `jsonb_extract("doc", '<path>')`
|
|
129
|
+
* over a path text this same module wrote. Anything else — a hand-made
|
|
130
|
+
* column, another tool's expression — answers `null`, and the caller
|
|
131
|
+
* reports the column rather than inventing a path for it.
|
|
132
|
+
* @param {string} expression
|
|
133
|
+
* @returns {import('../dialect.js').JsonPathSegment[] | null}
|
|
134
|
+
*/
|
|
135
|
+
function memberPathOf(expression) {
|
|
136
|
+
const match = /^\s*\(*\s*jsonb_extract\s*\(\s*"[^"]*"\s*,\s*'((?:[^']|'')*)'\s*\)\s*\)*\s*$/
|
|
137
|
+
.exec(String(expression));
|
|
138
|
+
if (match === null) return null;
|
|
139
|
+
return parsePathText(match[1].replace(/''/g, "'"));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* `$."a"."b"[0]` back into typed segments — the inverse of
|
|
144
|
+
* {@link jsonPathText}, and only of that: a path shape this module
|
|
145
|
+
* cannot have written answers `null`.
|
|
146
|
+
* @param {string} text
|
|
147
|
+
* @returns {import('../dialect.js').JsonPathSegment[] | null}
|
|
148
|
+
*/
|
|
149
|
+
function parsePathText(text) {
|
|
150
|
+
if (!text.startsWith('$')) return null;
|
|
151
|
+
/** @type {import('../dialect.js').JsonPathSegment[]} */
|
|
152
|
+
const segments = [];
|
|
153
|
+
let i = 1;
|
|
154
|
+
while (i < text.length) {
|
|
155
|
+
if (text[i] === '.') {
|
|
156
|
+
if (text[i + 1] !== '"') return null;
|
|
157
|
+
const end = text.indexOf('"', i + 2);
|
|
158
|
+
if (end < 0) return null;
|
|
159
|
+
segments.push({ name: text.slice(i + 2, end) });
|
|
160
|
+
i = end + 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (text[i] === '[') {
|
|
164
|
+
const end = text.indexOf(']', i + 1);
|
|
165
|
+
if (end < 0) return null;
|
|
166
|
+
const body = text.slice(i + 1, end);
|
|
167
|
+
// `[#-1]` is how this dialect writes a negative index
|
|
168
|
+
const index = body.startsWith('#') ? Number(body.slice(1)) : Number(body);
|
|
169
|
+
if (!Number.isInteger(index)) return null;
|
|
170
|
+
segments.push({ index });
|
|
171
|
+
i = end + 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return segments.length === 0 ? null : segments;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The declared index EXPRESSION a generated column computes, out of the
|
|
181
|
+
* SQL this dialect wrote for it. `byName` maps the engine's function
|
|
182
|
+
* name back to the model's — here that is the `jaren_x_` registration,
|
|
183
|
+
* which the store made from the model's own name.
|
|
184
|
+
* @param {string} expression
|
|
185
|
+
* @param {Record<string, string>} byName
|
|
186
|
+
* @returns {any | null}
|
|
187
|
+
*/
|
|
188
|
+
function expressionOf(expression, byName) {
|
|
189
|
+
return readExpression(expression, {
|
|
190
|
+
memberOf: (text) => {
|
|
191
|
+
const segments = memberPathOf(text);
|
|
192
|
+
return segments === null ? null : { member: pathOf(segments) };
|
|
193
|
+
},
|
|
194
|
+
nameOf: (name) => byName[name] ?? null,
|
|
195
|
+
stringOf: (text) => (/^'(?:[^']|'')*'$/.test(text)
|
|
196
|
+
? text.slice(1, -1).replace(/''/g, "'") : null),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A recovered segment list, in the spelling a model's path takes. */
|
|
201
|
+
function pathOf(segments) {
|
|
202
|
+
let text = '$';
|
|
203
|
+
for (const segment of segments) {
|
|
204
|
+
if ('index' in segment) { text += `[${segment.index}]`; continue; }
|
|
205
|
+
text += /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment.name)
|
|
206
|
+
? `.${segment.name}` : `[${JSON.stringify(segment.name)}]`;
|
|
207
|
+
}
|
|
208
|
+
return text;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The generated columns of one table, out of the CREATE text SQLite
|
|
213
|
+
* stored for it — the only place the expression lives here, since no
|
|
214
|
+
* pragma reports one.
|
|
215
|
+
* @param {any[]} rows - the `introspect.generated` answer
|
|
216
|
+
* @returns {{ name: string, expression: string }[]}
|
|
217
|
+
*/
|
|
218
|
+
function readGenerated(rows) {
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const row of rows) {
|
|
221
|
+
const sql = String(row?.sql ?? '');
|
|
222
|
+
// `"<name>" <type> GENERATED ALWAYS AS (<expression>) VIRTUAL|STORED`
|
|
223
|
+
const pattern = /"((?:[^"]|"")*)"\s+\w+\s+GENERATED\s+ALWAYS\s+AS\s*\(/gi;
|
|
224
|
+
let match = pattern.exec(sql);
|
|
225
|
+
while (match !== null) {
|
|
226
|
+
// the expression runs to the parenthesis that closes the one the
|
|
227
|
+
// match ended on, so a nested call inside it is not the end
|
|
228
|
+
let depth = 1;
|
|
229
|
+
let i = pattern.lastIndex;
|
|
230
|
+
while (i < sql.length && depth > 0) {
|
|
231
|
+
if (sql[i] === "'") {
|
|
232
|
+
i = sql.indexOf("'", i + 1);
|
|
233
|
+
if (i < 0) break;
|
|
234
|
+
}
|
|
235
|
+
else if (sql[i] === '(') depth += 1;
|
|
236
|
+
else if (sql[i] === ')') depth -= 1;
|
|
237
|
+
i += 1;
|
|
238
|
+
}
|
|
239
|
+
if (depth !== 0) break;
|
|
240
|
+
out.push({ name: match[1].replace(/""/g, '"'), expression: sql.slice(pattern.lastIndex, i - 1) });
|
|
241
|
+
pattern.lastIndex = i;
|
|
242
|
+
match = pattern.exec(sql);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
|
|
77
248
|
export const sqliteDialect = createDialect({
|
|
78
249
|
name: 'sqlite',
|
|
79
250
|
capabilities: {
|
|
80
251
|
jsonb: true,
|
|
81
252
|
generatedColumns: true,
|
|
253
|
+
indexableGeneratedColumns: true,
|
|
254
|
+
// `ANY` is a STRICT table's honest answer for a path the schema
|
|
255
|
+
// does not type, and it compares with a bound value of any type
|
|
256
|
+
untypedColumns: true,
|
|
82
257
|
returning: true,
|
|
83
258
|
upsert: true,
|
|
84
259
|
savepoints: true,
|
|
260
|
+
// a SAVEPOINT outside a transaction starts one, which is why a
|
|
261
|
+
// top-level transaction here is one checkpoint rather than a block
|
|
262
|
+
savepointStartsTransaction: true,
|
|
263
|
+
immediateTransactions: true,
|
|
85
264
|
alterTableFull: false,
|
|
265
|
+
virtualTables: true,
|
|
266
|
+
triggers: true,
|
|
267
|
+
pragmas: true,
|
|
268
|
+
declaredSqlText: true,
|
|
269
|
+
// foreign_keys defaults OFF and is set per connection, so this is
|
|
270
|
+
// the one engine in the suite that must verify it took
|
|
271
|
+
foreignKeysAlwaysOn: false,
|
|
272
|
+
// the implicit `rowid` every non-WITHOUT ROWID table carries
|
|
273
|
+
rowIdentity: true,
|
|
86
274
|
// a GROUP BY / ORDER BY term may name a result alias, so a bucket
|
|
87
275
|
// ladder is written once rather than three times
|
|
88
276
|
groupByAlias: true,
|
|
89
277
|
},
|
|
90
278
|
tableSuffix: ' STRICT',
|
|
279
|
+
// a generated column over `jsonb_extract` is cheap to recompute and
|
|
280
|
+
// costs nothing on disk, so it is VIRTUAL; the value is materialised
|
|
281
|
+
// only in the index over it
|
|
282
|
+
generatedStorage: 'VIRTUAL',
|
|
91
283
|
// RFC 3339 text → epoch milliseconds, in SQL: the migration planner
|
|
92
284
|
// populates derived instant columns with it (rounded to the ms;
|
|
93
285
|
// finer precision is the write contract's business, §10.3)
|
|
@@ -142,6 +334,16 @@ export const sqliteDialect = createDialect({
|
|
|
142
334
|
jsonAgg: (exprSql) => `json_group_array(${exprSql})`,
|
|
143
335
|
jsonTypeOf: (columnSql, pathText) =>
|
|
144
336
|
`json_type(${columnSql}, ${stringLiteral(pathText)})`,
|
|
337
|
+
// SQLite keeps the two number types it stores apart, so a JSON number
|
|
338
|
+
// is one of two names here
|
|
339
|
+
numericTypeNames: ['integer', 'real'],
|
|
340
|
+
jsonObject: (pairsSql) => `json_object(${pairsSql})`,
|
|
341
|
+
// `json()` tags the value with SQLite's JSON subtype, which is what
|
|
342
|
+
// makes an enclosing `json_object` embed it rather than quote it
|
|
343
|
+
jsonEmbed: (columnSql) => `json(${columnSql})`,
|
|
344
|
+
// a dynamically typed engine binds an external as itself and compares
|
|
345
|
+
// it with whatever the member holds
|
|
346
|
+
externalEncoding: 'value',
|
|
145
347
|
valueTypeOf: (paramSql) => `typeof(${paramSql})`,
|
|
146
348
|
// the half-open range over the prefix, which an index on the value
|
|
147
349
|
// can seek; `substr(value, 1, n) = p` and `value LIKE 'p%'` both read
|
|
@@ -172,23 +374,34 @@ export const sqliteDialect = createDialect({
|
|
|
172
374
|
groupAggregate: (fn, valueSql) => (valueSql === null
|
|
173
375
|
? 'COUNT(*)'
|
|
174
376
|
: `${{ sum: 'SUM', avg: 'AVG', min: 'MIN', max: 'MAX' }[fn]}(${valueSql})`),
|
|
175
|
-
rowIdentity
|
|
377
|
+
rowIdentity,
|
|
176
378
|
// membership of the row identity in a bound list — the fetch of a
|
|
177
379
|
// k-nearest plan's candidates. `IN` over the rowid is a primary-key
|
|
178
380
|
// lookup per value; a NULL in the list matches no row, which is what
|
|
179
381
|
// lets a caller pad a batch
|
|
180
382
|
identityIn: (identitySql, paramSqls) => `${identitySql} IN (${paramSqls.join(', ')})`,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
383
|
+
rtree: RTREE,
|
|
384
|
+
rtreeDdl: rtreeDdl({ quoteIdentifier, rowIdentity, rtree: RTREE }),
|
|
385
|
+
schemaTypeOf,
|
|
386
|
+
memberPathOf,
|
|
387
|
+
expressionOf,
|
|
388
|
+
readGenerated,
|
|
187
389
|
explainQuery: (sql) => `EXPLAIN QUERY PLAN ${sql}`,
|
|
390
|
+
// SQLite's plan is PROSE, one `detail` column per row
|
|
391
|
+
explainLines: (rows) => rows.map((row) => String(row.detail)),
|
|
392
|
+
// `SCAN <table>` with no index behind it is the full read; a join
|
|
393
|
+
// statement's narrative names the ALIAS the emitter gave the table,
|
|
394
|
+
// which is why an alias shape counts as one too
|
|
395
|
+
isFullScan: (line, tables) =>
|
|
396
|
+
(/^SCAN t\d+\b/.test(line) || tables.some((table) => line.startsWith(`SCAN ${table}`)))
|
|
397
|
+
&& !line.includes('USING INDEX'),
|
|
398
|
+
usesIndex: (line, index) =>
|
|
399
|
+
line.includes(`USING INDEX ${index}`) || line.includes(`USING COVERING INDEX ${index}`),
|
|
188
400
|
excludedRef: (columnSql) => `excluded.${columnSql}`,
|
|
189
401
|
tx: {
|
|
190
402
|
begin: 'BEGIN',
|
|
191
403
|
beginImmediate: 'BEGIN IMMEDIATE',
|
|
404
|
+
deferForeignKeys: 'PRAGMA defer_foreign_keys = ON',
|
|
192
405
|
commit: 'COMMIT',
|
|
193
406
|
rollback: 'ROLLBACK',
|
|
194
407
|
savepoint: (n) => `SAVEPOINT ${quoteIdentifier(n)}`,
|
|
@@ -196,13 +409,25 @@ export const sqliteDialect = createDialect({
|
|
|
196
409
|
rollbackTo: (n) => `ROLLBACK TO SAVEPOINT ${quoteIdentifier(n)}`,
|
|
197
410
|
},
|
|
198
411
|
pragma: {
|
|
199
|
-
|
|
200
|
-
|
|
412
|
+
// the one configuration spelling: the name comes from the store's
|
|
413
|
+
// closed pragma table and the value from its validators, and both
|
|
414
|
+
// are guarded again here
|
|
415
|
+
set: (name, value) => `PRAGMA ${pragmaWord(name)} = ${pragmaValue(value)}`,
|
|
201
416
|
foreignKeys: (on) => `PRAGMA foreign_keys = ${on ? 'ON' : 'OFF'}`,
|
|
202
417
|
foreignKeyCheck: () => 'PRAGMA foreign_key_check',
|
|
418
|
+
// the maintenance operations: a checkpoint mode is a closed word,
|
|
419
|
+
// an integrity-check limit a whole integer
|
|
420
|
+
walCheckpoint: (mode) => `PRAGMA wal_checkpoint(${pragmaWord(mode)})`,
|
|
421
|
+
integrityCheck: (limit) => (limit === undefined
|
|
422
|
+
? 'PRAGMA integrity_check'
|
|
423
|
+
: `PRAGMA integrity_check(${pragmaValue(limit)})`),
|
|
424
|
+
optimize: () => 'PRAGMA optimize',
|
|
203
425
|
},
|
|
204
426
|
introspect: {
|
|
205
427
|
version: () => 'SELECT sqlite_version() AS version',
|
|
428
|
+
// the read-back of one configuration pragma: `PRAGMA name` answers
|
|
429
|
+
// one row whose single column carries the value in effect
|
|
430
|
+
pragma: (name) => `PRAGMA ${pragmaWord(name)}`,
|
|
206
431
|
compileOptions: () =>
|
|
207
432
|
'SELECT compile_options AS name FROM pragma_compile_options',
|
|
208
433
|
tableExists: () =>
|
|
@@ -230,6 +455,14 @@ export const sqliteDialect = createDialect({
|
|
|
230
455
|
'SELECT type, name, sql FROM sqlite_schema '
|
|
231
456
|
+ `WHERE tbl_name = ${stringLiteral(table)} AND sql IS NOT NULL `
|
|
232
457
|
+ 'ORDER BY type, name',
|
|
458
|
+
// every table this store might own: the engine's own are excluded
|
|
459
|
+
// by name, and a VIEW is reported rather than derived
|
|
460
|
+
tables: () =>
|
|
461
|
+
"SELECT name, type FROM sqlite_schema WHERE type IN ('table', 'view') "
|
|
462
|
+
+ "AND name NOT LIKE 'sqlite_%' ORDER BY type, name",
|
|
463
|
+
// the CREATE text is where a generated column's expression lives
|
|
464
|
+
generated: (table) =>
|
|
465
|
+
`SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ${stringLiteral(table)}`,
|
|
233
466
|
// the whole declared schema, for shape-equality comparison after a
|
|
234
467
|
// rebuild: every object that carries SQL text, in a stable order
|
|
235
468
|
schemaDump: () =>
|