@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.
Files changed (79) hide show
  1. package/ARCHITECTURE.md +412 -56
  2. package/README.md +600 -57
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +293 -45
  5. package/docs/LIVE-FORMAT.md +169 -20
  6. package/docs/MIGRATION-FORMAT.md +142 -17
  7. package/docs/MODEL-FORMAT.md +752 -64
  8. package/docs/REPLICATION-FORMAT.md +208 -0
  9. package/package.json +21 -7
  10. package/schemas/jaren-model.draft-07.schema.json +224 -162
  11. package/schemas/jaren-model.schema.json +224 -162
  12. package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
  13. package/schemas/jaren-replication-snapshot.schema.json +83 -0
  14. package/schemas/jaren-replication.draft-07.schema.json +82 -0
  15. package/schemas/jaren-replication.schema.json +82 -0
  16. package/src/algebra.js +227 -9
  17. package/src/backup.js +161 -0
  18. package/src/cancellation.js +48 -0
  19. package/src/capture.js +230 -47
  20. package/src/cli.js +165 -59
  21. package/src/cursor.js +417 -0
  22. package/src/dag-job.js +154 -21
  23. package/src/ddl.js +102 -8
  24. package/src/dialect.js +268 -113
  25. package/src/dialects/expression-read.js +158 -0
  26. package/src/dialects/postgres.js +618 -0
  27. package/src/dialects/rtree-ddl.js +129 -0
  28. package/src/dialects/sqlite.js +244 -11
  29. package/src/document-files.js +311 -0
  30. package/src/document-steps.js +422 -0
  31. package/src/documents.js +335 -0
  32. package/src/driver.js +448 -61
  33. package/src/drivers/bun.js +37 -1
  34. package/src/drivers/indexeddb-snapshot.js +149 -0
  35. package/src/drivers/node-pool.js +11 -0
  36. package/src/drivers/node-worker-endpoint.js +105 -0
  37. package/src/drivers/node-worker.js +204 -0
  38. package/src/drivers/node.js +41 -7
  39. package/src/drivers/postgres.js +331 -0
  40. package/src/drivers/wasm-oo1.js +97 -0
  41. package/src/drivers/wasm-session.js +67 -0
  42. package/src/drivers/wasm.js +17 -83
  43. package/src/drivers/worker-pool.js +183 -0
  44. package/src/drivers/worker-protocol.js +79 -0
  45. package/src/drivers/worker-queue.js +60 -0
  46. package/src/emit.js +339 -48
  47. package/src/entity.js +20 -22
  48. package/src/errors.js +430 -19
  49. package/src/expression.js +284 -0
  50. package/src/graph.js +64 -8
  51. package/src/index.js +48 -17
  52. package/src/introspect.js +583 -0
  53. package/src/jobs.js +843 -107
  54. package/src/json-bytes.js +58 -0
  55. package/src/live-join.js +250 -0
  56. package/src/live-nested.js +120 -0
  57. package/src/live.js +18 -4
  58. package/src/logical-rows.js +90 -0
  59. package/src/maintenance.js +175 -0
  60. package/src/migrate.js +248 -181
  61. package/src/model.js +68 -0
  62. package/src/plan.js +1119 -138
  63. package/src/pragmas.js +314 -0
  64. package/src/profile.js +151 -3
  65. package/src/query.js +1634 -323
  66. package/src/replication-format.js +115 -0
  67. package/src/replication.js +332 -0
  68. package/src/residual.js +17 -0
  69. package/src/series.js +12 -4
  70. package/src/store.js +1567 -273
  71. package/src/tracker.js +203 -29
  72. package/src/udf.js +88 -7
  73. package/types/index.d.ts +1158 -27
  74. package/types/node-pool.d.ts +28 -0
  75. package/types/node-worker.d.ts +54 -0
  76. package/types/node.d.ts +69 -2
  77. package/types/postgres.d.ts +46 -0
  78. package/types/typed.d.ts +27 -4
  79. 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
+ }
@@ -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 argument: journal modes and similar keywords are a
67
- * closed word set, never interpolated user text.
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: () => '"rowid"',
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
- // the R*Tree module and the shape this store gives it: the row id
182
- // and the four box edges in (minx, maxx, miny, maxy) order, which is
183
- // the (w, e, s, n) a bbox index covers its columns in. The three
184
- // shadow tables SQLite creates beside a virtual table are its own
185
- // storage — deterministic from the name, and dropped with it.
186
- rtree: { module: 'rtree', columns: ['id', 'minx', 'maxx', 'miny', 'maxy'] },
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
- busyTimeout: (ms) => `PRAGMA busy_timeout = ${Math.trunc(ms)}`,
200
- journalMode: (mode) => `PRAGMA journal_mode = ${pragmaWord(mode)}`,
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: () =>