@jarenjs/db 0.49.2 → 0.66.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/ARCHITECTURE.md +420 -71
  2. package/README.md +711 -79
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +309 -45
  5. package/docs/LIVE-FORMAT.md +156 -19
  6. package/docs/MIGRATION-FORMAT.md +247 -40
  7. package/docs/MODEL-FORMAT.md +968 -86
  8. package/package.json +21 -8
  9. package/schemas/jaren-migration.draft-07.schema.json +73 -0
  10. package/schemas/jaren-migration.schema.json +73 -0
  11. package/schemas/jaren-model.draft-07.schema.json +224 -162
  12. package/schemas/jaren-model.schema.json +224 -162
  13. package/src/algebra.js +227 -9
  14. package/src/backup.js +161 -0
  15. package/src/cancellation.js +48 -0
  16. package/src/capture.js +255 -44
  17. package/src/cli.js +337 -50
  18. package/src/cursor.js +411 -0
  19. package/src/dag-job.js +154 -21
  20. package/src/ddl.js +125 -11
  21. package/src/dialect.js +267 -112
  22. package/src/dialects/expression-read.js +158 -0
  23. package/src/dialects/postgres.js +618 -0
  24. package/src/dialects/rtree-ddl.js +129 -0
  25. package/src/dialects/sqlite.js +245 -12
  26. package/src/document-files.js +311 -0
  27. package/src/document-steps.js +422 -0
  28. package/src/documents.js +335 -0
  29. package/src/driver.js +503 -69
  30. package/src/drivers/bun.js +37 -1
  31. package/src/drivers/indexeddb-snapshot.js +149 -0
  32. package/src/drivers/node-pool.js +11 -0
  33. package/src/drivers/node-worker-endpoint.js +105 -0
  34. package/src/drivers/node-worker.js +204 -0
  35. package/src/drivers/node.js +41 -7
  36. package/src/drivers/postgres.js +331 -0
  37. package/src/drivers/wasm-oo1.js +97 -0
  38. package/src/drivers/wasm-session.js +67 -0
  39. package/src/drivers/wasm.js +18 -83
  40. package/src/drivers/worker-pool.js +183 -0
  41. package/src/drivers/worker-protocol.js +79 -0
  42. package/src/drivers/worker-queue.js +60 -0
  43. package/src/emit-model.js +14 -0
  44. package/src/emit.js +349 -51
  45. package/src/entity.js +102 -59
  46. package/src/errors.js +430 -2
  47. package/src/expression.js +284 -0
  48. package/src/graph.js +64 -8
  49. package/src/index.js +48 -19
  50. package/src/introspect.js +583 -0
  51. package/src/jobs.js +870 -99
  52. package/src/json-bytes.js +58 -0
  53. package/src/live-time.js +12 -3
  54. package/src/live.js +11 -1
  55. package/src/maintenance.js +175 -0
  56. package/src/migrate.js +606 -333
  57. package/src/model.js +241 -8
  58. package/src/plan.js +1238 -160
  59. package/src/pragmas.js +314 -0
  60. package/src/profile.js +151 -3
  61. package/src/query.js +1748 -312
  62. package/src/residual.js +17 -0
  63. package/src/series.js +12 -4
  64. package/src/store.js +1672 -276
  65. package/src/tracker.js +367 -68
  66. package/src/udf.js +88 -7
  67. package/types/index.d.ts +1246 -32
  68. package/types/node-pool.d.ts +28 -0
  69. package/types/node-worker.d.ts +54 -0
  70. package/types/node.d.ts +72 -3
  71. package/types/postgres.d.ts +46 -0
  72. package/types/typed.d.ts +81 -3
  73. package/types/wasm.d.ts +21 -0
  74. package/dist/types/algebra.d.ts +0 -230
  75. package/dist/types/app.d.ts +0 -49
  76. package/dist/types/capture.d.ts +0 -85
  77. package/dist/types/cli.d.ts +0 -2
  78. package/dist/types/dag-job.d.ts +0 -40
  79. package/dist/types/ddl.d.ts +0 -229
  80. package/dist/types/derive.d.ts +0 -250
  81. package/dist/types/dialect.d.ts +0 -154
  82. package/dist/types/dialects/sqlite.d.ts +0 -9
  83. package/dist/types/driver.d.ts +0 -110
  84. package/dist/types/drivers/bun.d.ts +0 -47
  85. package/dist/types/drivers/node.d.ts +0 -37
  86. package/dist/types/drivers/wasm.d.ts +0 -65
  87. package/dist/types/emit-model.d.ts +0 -44
  88. package/dist/types/emit.d.ts +0 -75
  89. package/dist/types/entity.d.ts +0 -23
  90. package/dist/types/errors.d.ts +0 -170
  91. package/dist/types/graph.d.ts +0 -28
  92. package/dist/types/index.d.ts +0 -37
  93. package/dist/types/jobs.d.ts +0 -140
  94. package/dist/types/knn.d.ts +0 -69
  95. package/dist/types/live-time.d.ts +0 -141
  96. package/dist/types/live.d.ts +0 -64
  97. package/dist/types/migrate.d.ts +0 -170
  98. package/dist/types/model.d.ts +0 -36
  99. package/dist/types/patch-sql.d.ts +0 -37
  100. package/dist/types/plan.d.ts +0 -142
  101. package/dist/types/profile.d.ts +0 -80
  102. package/dist/types/query.d.ts +0 -112
  103. package/dist/types/residual.d.ts +0 -64
  104. package/dist/types/series.d.ts +0 -227
  105. package/dist/types/store.d.ts +0 -60
  106. package/dist/types/tracker.d.ts +0 -43
  107. package/dist/types/typed.d.ts +0 -15
  108. package/dist/types/types.d.ts +0 -26
  109. package/dist/types/udf.d.ts +0 -75
  110. package/dist/types/window.d.ts +0 -52
@@ -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) {
@@ -33,7 +35,8 @@ function jsonPathText(segments) {
33
35
  let text = '$';
34
36
  for (const segment of segments) {
35
37
  if ('index' in segment) {
36
- text += `[${segment.index}]`;
38
+ // SQLite counts from the end as `[#-1]`; a bare `[-1]` is a bad path
39
+ text += segment.index < 0 ? `[#${segment.index}]` : `[${segment.index}]`;
37
40
  continue;
38
41
  }
39
42
  // eslint-disable-next-line no-control-regex
@@ -62,8 +65,9 @@ function typeFor(schemaType, hint) {
62
65
  }
63
66
 
64
67
  /**
65
- * A guarded PRAGMA argument: journal modes and similar keywords are a
66
- * 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.
67
71
  * @param {string} word
68
72
  * @returns {string}
69
73
  */
@@ -73,20 +77,209 @@ function pragmaWord(word) {
73
77
  return String(word);
74
78
  }
75
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
+
76
248
  export const sqliteDialect = createDialect({
77
249
  name: 'sqlite',
78
250
  capabilities: {
79
251
  jsonb: true,
80
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,
81
257
  returning: true,
82
258
  upsert: true,
83
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,
84
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,
85
274
  // a GROUP BY / ORDER BY term may name a result alias, so a bucket
86
275
  // ladder is written once rather than three times
87
276
  groupByAlias: true,
88
277
  },
89
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',
90
283
  // RFC 3339 text → epoch milliseconds, in SQL: the migration planner
91
284
  // populates derived instant columns with it (rounded to the ms;
92
285
  // finer precision is the write contract's business, §10.3)
@@ -141,6 +334,16 @@ export const sqliteDialect = createDialect({
141
334
  jsonAgg: (exprSql) => `json_group_array(${exprSql})`,
142
335
  jsonTypeOf: (columnSql, pathText) =>
143
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',
144
347
  valueTypeOf: (paramSql) => `typeof(${paramSql})`,
145
348
  // the half-open range over the prefix, which an index on the value
146
349
  // can seek; `substr(value, 1, n) = p` and `value LIKE 'p%'` both read
@@ -171,19 +374,29 @@ export const sqliteDialect = createDialect({
171
374
  groupAggregate: (fn, valueSql) => (valueSql === null
172
375
  ? 'COUNT(*)'
173
376
  : `${{ sum: 'SUM', avg: 'AVG', min: 'MIN', max: 'MAX' }[fn]}(${valueSql})`),
174
- rowIdentity: () => '"rowid"',
377
+ rowIdentity,
175
378
  // membership of the row identity in a bound list — the fetch of a
176
379
  // k-nearest plan's candidates. `IN` over the rowid is a primary-key
177
380
  // lookup per value; a NULL in the list matches no row, which is what
178
381
  // lets a caller pad a batch
179
382
  identityIn: (identitySql, paramSqls) => `${identitySql} IN (${paramSqls.join(', ')})`,
180
- // the R*Tree module and the shape this store gives it: the row id
181
- // and the four box edges in (minx, maxx, miny, maxy) order, which is
182
- // the (w, e, s, n) a bbox index covers its columns in. The three
183
- // shadow tables SQLite creates beside a virtual table are its own
184
- // storage — deterministic from the name, and dropped with it.
185
- 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,
186
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}`),
187
400
  excludedRef: (columnSql) => `excluded.${columnSql}`,
188
401
  tx: {
189
402
  begin: 'BEGIN',
@@ -195,13 +408,25 @@ export const sqliteDialect = createDialect({
195
408
  rollbackTo: (n) => `ROLLBACK TO SAVEPOINT ${quoteIdentifier(n)}`,
196
409
  },
197
410
  pragma: {
198
- busyTimeout: (ms) => `PRAGMA busy_timeout = ${Math.trunc(ms)}`,
199
- journalMode: (mode) => `PRAGMA journal_mode = ${pragmaWord(mode)}`,
411
+ // the one configuration spelling: the name comes from the store's
412
+ // closed pragma table and the value from its validators, and both
413
+ // are guarded again here
414
+ set: (name, value) => `PRAGMA ${pragmaWord(name)} = ${pragmaValue(value)}`,
200
415
  foreignKeys: (on) => `PRAGMA foreign_keys = ${on ? 'ON' : 'OFF'}`,
201
416
  foreignKeyCheck: () => 'PRAGMA foreign_key_check',
417
+ // the maintenance operations: a checkpoint mode is a closed word,
418
+ // an integrity-check limit a whole integer
419
+ walCheckpoint: (mode) => `PRAGMA wal_checkpoint(${pragmaWord(mode)})`,
420
+ integrityCheck: (limit) => (limit === undefined
421
+ ? 'PRAGMA integrity_check'
422
+ : `PRAGMA integrity_check(${pragmaValue(limit)})`),
423
+ optimize: () => 'PRAGMA optimize',
202
424
  },
203
425
  introspect: {
204
426
  version: () => 'SELECT sqlite_version() AS version',
427
+ // the read-back of one configuration pragma: `PRAGMA name` answers
428
+ // one row whose single column carries the value in effect
429
+ pragma: (name) => `PRAGMA ${pragmaWord(name)}`,
205
430
  compileOptions: () =>
206
431
  'SELECT compile_options AS name FROM pragma_compile_options',
207
432
  tableExists: () =>
@@ -229,6 +454,14 @@ export const sqliteDialect = createDialect({
229
454
  'SELECT type, name, sql FROM sqlite_schema '
230
455
  + `WHERE tbl_name = ${stringLiteral(table)} AND sql IS NOT NULL `
231
456
  + 'ORDER BY type, name',
457
+ // every table this store might own: the engine's own are excluded
458
+ // by name, and a VIEW is reported rather than derived
459
+ tables: () =>
460
+ "SELECT name, type FROM sqlite_schema WHERE type IN ('table', 'view') "
461
+ + "AND name NOT LIKE 'sqlite_%' ORDER BY type, name",
462
+ // the CREATE text is where a generated column's expression lives
463
+ generated: (table) =>
464
+ `SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = ${stringLiteral(table)}`,
232
465
  // the whole declared schema, for shape-equality comparison after a
233
466
  // rebuild: every object that carries SQL text, in a stable order
234
467
  schemaDump: () =>