@orkestrel/database 0.0.12 → 0.0.14
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/README.md +12 -8
- package/dist/src/browser/index.d.ts +95 -70
- package/dist/src/browser/index.js +115 -86
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +595 -410
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +712 -313
- package/dist/src/core/index.d.ts +712 -313
- package/dist/src/core/index.js +588 -409
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +236 -332
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +213 -223
- package/dist/src/server/index.d.ts +213 -223
- package/dist/src/server/index.js +230 -324
- package/dist/src/server/index.js.map +1 -1
- package/package.json +22 -18
package/dist/src/server/index.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { DatabaseError, MemoryDriver, applyQuery, bindRowKey, checkAbort, cloneDriverMetadata, cloneMigrationInput, computeAggregate, equalsValue, extractKey, filterRows, isDatabaseError, isKey, matchesQuery, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, validatePage } from "../core/index.js";
|
|
1
|
+
import { DatabaseError, DriverIterator, MemoryDriver, applyQuery, bindRowKey, checkAbort, cloneDriverMetadata, cloneMigrationInput, computeAggregate, equalsValue, extractKey, filterRows, findColumn, isDatabaseError, isKey, matchesQuery, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, validatePage } from "../core/index.js";
|
|
2
2
|
import { cloneJSONValue, isBoolean, isFiniteNumber, isRecord, isString } from "@orkestrel/contract";
|
|
3
3
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
import { createSQLiteDatabase, isSQLiteError } from "@orkestrel/sqlite";
|
|
6
6
|
//#region src/server/constants.ts
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Lists the declared {@link ColumnStorage}s whose SQL equality comparisons (`equals` /
|
|
9
9
|
* `not` / `any` / `none`) and `starts` / `ends` compiles are provably
|
|
10
10
|
* engine-exact under declared-type trust — `text` / `integer` / `real` /
|
|
11
11
|
* `boolean`; a `json` or `blob` column always refines instead.
|
|
12
12
|
*
|
|
13
13
|
* @remarks
|
|
14
|
-
* This set governs equality and prefix/suffix matching only.
|
|
14
|
+
* This set governs equality and prefix/suffix matching only. Range
|
|
15
15
|
* comparisons (`above` / `below` / `from` / `to` / `between`) and `ORDER BY`
|
|
16
|
-
* are exact for `integer` / `real` / `boolean` but
|
|
16
|
+
* are exact for `integer` / `real` / `boolean` but not for `text`: compiled
|
|
17
17
|
* SQL orders/ranges under SQLite's default BINARY collation, which compares
|
|
18
|
-
* TEXT byte-for-byte as UTF-8 — equivalent to Unicode
|
|
18
|
+
* TEXT byte-for-byte as UTF-8 — equivalent to Unicode code-point order —
|
|
19
19
|
* while the core engine's `compareValues` orders JS strings with `<`, which
|
|
20
|
-
* compares UTF-16
|
|
21
|
-
* plane characters (code points ≥ U+10000,
|
|
22
|
-
* (`\uD800`–`\uDBFF`) sorts
|
|
23
|
-
* its code point sorts
|
|
20
|
+
* compares UTF-16 code-unit order. The two orders diverge for supplementary-
|
|
21
|
+
* plane characters (code points ≥ U+10000, for example many emoji): a lead surrogate
|
|
22
|
+
* (`\uD800`–`\uDBFF`) sorts below ``–`` in code-unit order, while
|
|
23
|
+
* its code point sorts above them. So `matchesConditionExactly`'s range family and
|
|
24
24
|
* `matchesOrderExactly` exclude `text`, refining through the core engine instead.
|
|
25
25
|
*/
|
|
26
26
|
var EXACT_COLUMN_STORAGE = Object.freeze([
|
|
@@ -30,7 +30,7 @@ var EXACT_COLUMN_STORAGE = Object.freeze([
|
|
|
30
30
|
"boolean"
|
|
31
31
|
]);
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* Lists the declared {@link ColumnStorage}s whose SQL range comparisons
|
|
34
34
|
* (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are
|
|
35
35
|
* provably engine-exact — `integer` / `real` / `boolean` only. `text` is
|
|
36
36
|
* excluded: see {@link EXACT_COLUMN_STORAGE}'s remarks for the BINARY-collation
|
|
@@ -43,7 +43,7 @@ var EXACT_RANGE_COLUMN_STORAGE = Object.freeze([
|
|
|
43
43
|
"boolean"
|
|
44
44
|
]);
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Names the reserved metadata table the {@link SQLiteDriver} creates on `open` to
|
|
47
47
|
* persist its stamped `DriverMetadata` (`version` + declared schema JSON) — the
|
|
48
48
|
* SQLite realization of the `metadata` / `stamp` driver hooks.
|
|
49
49
|
*
|
|
@@ -55,7 +55,7 @@ var METADATA_TABLE = "_metadata";
|
|
|
55
55
|
//#endregion
|
|
56
56
|
//#region src/server/helpers.ts
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* Reports whether a caught filesystem error says that nothing is there to read.
|
|
59
59
|
*
|
|
60
60
|
* @remarks
|
|
61
61
|
* Two codes carry that meaning: `ENOENT` is a plain absence, and `ENOTDIR` is a
|
|
@@ -72,7 +72,7 @@ var METADATA_TABLE = "_metadata";
|
|
|
72
72
|
* answers `false` rather than being read for a `code` any object could carry.
|
|
73
73
|
*
|
|
74
74
|
* @param error - The caught value to classify; any runtime is accepted
|
|
75
|
-
* @returns
|
|
75
|
+
* @returns True if the error reports that the path holds nothing; false otherwise
|
|
76
76
|
*
|
|
77
77
|
* @example
|
|
78
78
|
* ```ts
|
|
@@ -86,21 +86,21 @@ function matchesAbsentPath(error) {
|
|
|
86
86
|
return error.code === "ENOENT" || error.code === "ENOTDIR";
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
* the operand side of the declared-type-trust proof.
|
|
89
|
+
* Reports whether a value's runtime type matches a column's declared exact type
|
|
90
|
+
* — the operand side of the declared-type-trust proof.
|
|
91
91
|
*
|
|
92
92
|
* @remarks
|
|
93
|
-
* `text` ↔ string, `integer` / `real` ↔
|
|
93
|
+
* `text` ↔ string, `integer` / `real` ↔ finite number (`NaN` / `±Infinity`
|
|
94
94
|
* fail), `boolean` ↔ boolean. Backs {@link matchesConditionExactly}'s operand checks.
|
|
95
95
|
*
|
|
96
96
|
* @param value - The condition operand to test
|
|
97
|
-
* @param
|
|
98
|
-
* @returns
|
|
97
|
+
* @param storage - The column's declared portable storage type
|
|
98
|
+
* @returns True if the operand's runtime type matches the declared type; false otherwise
|
|
99
99
|
*
|
|
100
100
|
* @example
|
|
101
101
|
* ```ts
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* matchesDeclaredStorage('Ada', 'text') // true
|
|
103
|
+
* matchesDeclaredStorage(Number.NaN, 'integer') // false — only finite numbers
|
|
104
104
|
* ```
|
|
105
105
|
*/
|
|
106
106
|
function matchesDeclaredStorage(value, storage) {
|
|
@@ -109,9 +109,9 @@ function matchesDeclaredStorage(value, storage) {
|
|
|
109
109
|
return isFiniteNumber(value);
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
* the core engine's `matchesCondition` for every value its
|
|
114
|
-
* type can store.
|
|
112
|
+
* Reports whether one {@link Condition} compiles to SQL that is provably
|
|
113
|
+
* identical to the core engine's `matchesCondition` for every value its
|
|
114
|
+
* column's declared type can store.
|
|
115
115
|
*
|
|
116
116
|
* @remarks
|
|
117
117
|
* `false` for a nested `FieldPath` (an array) or a column absent from `schema`.
|
|
@@ -123,32 +123,32 @@ function matchesDeclaredStorage(value, storage) {
|
|
|
123
123
|
* Required non-null `equals` / `not` require an operand matching the declared
|
|
124
124
|
* storage and exclude `json` / `blob`. `above` / `below` / `from` / `to` /
|
|
125
125
|
* `between` are exact only for {@link EXACT_RANGE_COLUMN_STORAGE} (`integer` /
|
|
126
|
-
* `real` / `boolean`) — a `text` column's range conditions
|
|
127
|
-
* SQLite's default BINARY collation orders TEXT by Unicode
|
|
128
|
-
* the core engine's `compareValues` orders JS strings by UTF-16
|
|
126
|
+
* `real` / `boolean`) — a `text` column's range conditions refine, because
|
|
127
|
+
* SQLite's default BINARY collation orders TEXT by Unicode code point while
|
|
128
|
+
* the core engine's `compareValues` orders JS strings by UTF-16 code unit,
|
|
129
129
|
* and the two diverge for supplementary-plane characters (see
|
|
130
130
|
* {@link EXACT_COLUMN_STORAGE}'s remarks for the full rationale).
|
|
131
|
-
* `any` / `none` require a
|
|
131
|
+
* `any` / `none` require a non-empty list where every element matches (an empty
|
|
132
132
|
* list is exact under neither: the engine's `any([])` matches nothing while
|
|
133
133
|
* `none([])` matches everything, and SQL `IN ()` is a syntax error) — these
|
|
134
134
|
* stay exact on `text` (byte equality is collation-independent and engine-
|
|
135
135
|
* identical). `starts` / `ends` are exact only on a `text` column with a
|
|
136
136
|
* string operand (case-sensitive `substr` compile, see {@link compileConditionSQL}) —
|
|
137
|
-
* likewise collation-independent. `like` / `glob` are
|
|
137
|
+
* likewise collation-independent. `like` / `glob` are never exact — SQLite
|
|
138
138
|
* `LIKE` folds case ASCII-only against the engine's Unicode fold, and `GLOB`
|
|
139
139
|
* has character classes the engine treats literally.
|
|
140
140
|
*
|
|
141
141
|
* @param condition - The condition to test
|
|
142
142
|
* @param schema - The table's schema
|
|
143
|
-
* @returns
|
|
143
|
+
* @returns True if `condition` is exact; false otherwise
|
|
144
144
|
*/
|
|
145
145
|
function matchesConditionExactly(condition, schema) {
|
|
146
146
|
if (!isString(condition.column)) return false;
|
|
147
|
-
const column =
|
|
147
|
+
const column = findColumn(condition.column, schema);
|
|
148
148
|
if (column === void 0) return false;
|
|
149
149
|
if (condition.operator === "absent" || condition.operator === "present") return !(column.optional && column.nullable);
|
|
150
150
|
if (column.optional || column.nullable) return false;
|
|
151
|
-
if (!EXACT_COLUMN_STORAGE.
|
|
151
|
+
if (!EXACT_COLUMN_STORAGE.includes(column.storage)) return false;
|
|
152
152
|
const first = condition.values[0];
|
|
153
153
|
const second = condition.values[1];
|
|
154
154
|
switch (condition.operator) {
|
|
@@ -157,8 +157,8 @@ function matchesConditionExactly(condition, schema) {
|
|
|
157
157
|
case "above":
|
|
158
158
|
case "below":
|
|
159
159
|
case "from":
|
|
160
|
-
case "to": return EXACT_RANGE_COLUMN_STORAGE.
|
|
161
|
-
case "between": return EXACT_RANGE_COLUMN_STORAGE.
|
|
160
|
+
case "to": return EXACT_RANGE_COLUMN_STORAGE.includes(column.storage) && matchesDeclaredStorage(first, column.storage);
|
|
161
|
+
case "between": return EXACT_RANGE_COLUMN_STORAGE.includes(column.storage) && matchesDeclaredStorage(first, column.storage) && matchesDeclaredStorage(second, column.storage);
|
|
162
162
|
case "any":
|
|
163
163
|
case "none": return condition.values.length > 0 && condition.values.every((value) => matchesDeclaredStorage(value, column.storage));
|
|
164
164
|
case "starts":
|
|
@@ -168,36 +168,43 @@ function matchesConditionExactly(condition, schema) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
*
|
|
172
|
-
* matches the engine's {@link import('@src/core').sortRows} exactly.
|
|
171
|
+
* Reports whether one {@link Order} term's column compiles to an `ORDER BY`
|
|
172
|
+
* that matches the engine's {@link import('@src/core').sortRows} exactly.
|
|
173
173
|
*
|
|
174
174
|
* @remarks
|
|
175
175
|
* `false` for a nested `FieldPath`, a column absent from `schema`, or a
|
|
176
176
|
* declared type outside {@link EXACT_RANGE_COLUMN_STORAGE} (`integer` / `real` /
|
|
177
|
-
* `boolean`). `text` is
|
|
177
|
+
* `boolean`). `text` is not exact here: SQLite's default BINARY collation
|
|
178
178
|
* orders TEXT by Unicode code point while the core engine's `compareValues`
|
|
179
179
|
* orders JS strings by UTF-16 code unit, and the two diverge for
|
|
180
180
|
* supplementary-plane characters (see {@link EXACT_COLUMN_STORAGE}'s remarks) —
|
|
181
|
-
* a `text` order term
|
|
181
|
+
* a `text` order term refines through the core engine instead. The column must
|
|
182
|
+
* also be required and non-null: an optional or nullable column refines, because
|
|
183
|
+
* SQL orders its `NULL`s ahead of every value while the core total order ranks
|
|
184
|
+
* `undefined` before `null` before every other value.
|
|
182
185
|
*
|
|
183
186
|
* @param order - The order term to test
|
|
184
187
|
* @param schema - The table's schema
|
|
185
|
-
* @returns
|
|
188
|
+
* @returns True if `order` is exact; false otherwise
|
|
186
189
|
*/
|
|
187
190
|
function matchesOrderExactly(order, schema) {
|
|
188
191
|
if (!isString(order.column)) return false;
|
|
189
|
-
const column =
|
|
192
|
+
const column = findColumn(order.column, schema);
|
|
190
193
|
if (column === void 0) return false;
|
|
191
|
-
return !column.optional && !column.nullable && EXACT_RANGE_COLUMN_STORAGE.
|
|
194
|
+
return !column.optional && !column.nullable && EXACT_RANGE_COLUMN_STORAGE.includes(column.storage);
|
|
192
195
|
}
|
|
193
196
|
/**
|
|
194
|
-
*
|
|
195
|
-
* term is exact. `limit` / `offset` never affect exactness (SQL
|
|
196
|
-
* `OFFSET` are always engine-identical).
|
|
197
|
+
* Reports whether a whole {@link QueryInput} is exact — every condition and
|
|
198
|
+
* every order term is exact. `limit` / `offset` never affect exactness (SQL
|
|
199
|
+
* `LIMIT` / `OFFSET` are always engine-identical).
|
|
200
|
+
*
|
|
201
|
+
* @remarks
|
|
202
|
+
* The gate {@link import('./drivers/SQLiteDriver.js').SQLiteDriver} checks before
|
|
203
|
+
* trusting a native SQL read over a full-scan refine through the core engine.
|
|
197
204
|
*
|
|
198
205
|
* @param input - The query input to test
|
|
199
206
|
* @param schema - The table's schema
|
|
200
|
-
* @returns
|
|
207
|
+
* @returns True if every part of `input` is exact; false otherwise
|
|
201
208
|
*/
|
|
202
209
|
function matchesQueryExactly(input, schema) {
|
|
203
210
|
const conditions = input.conditions ?? [];
|
|
@@ -205,25 +212,25 @@ function matchesQueryExactly(input, schema) {
|
|
|
205
212
|
return conditions.every((condition) => matchesConditionExactly(condition, schema)) && order.every((term) => matchesOrderExactly(term, schema));
|
|
206
213
|
}
|
|
207
214
|
/**
|
|
208
|
-
*
|
|
215
|
+
* Reports whether SQLite can execute an aggregate exactly like the core engine.
|
|
209
216
|
*
|
|
210
217
|
* @param operation - Aggregate operation
|
|
211
218
|
* @param column - Aggregate field
|
|
212
219
|
* @param schema - Current table schema
|
|
213
|
-
* @returns
|
|
220
|
+
* @returns True if native aggregation is exact; false otherwise
|
|
214
221
|
*/
|
|
215
222
|
function matchesAggregateExactly(operation, column, schema) {
|
|
216
223
|
if (operation === "count") return true;
|
|
217
224
|
if (operation === "sum" || operation === "average" || !isString(column)) return false;
|
|
218
|
-
const declared =
|
|
225
|
+
const declared = findColumn(column, schema);
|
|
219
226
|
return declared !== void 0 && (declared.storage === "integer" || declared.storage === "real") && !(declared.optional && declared.nullable);
|
|
220
227
|
}
|
|
221
228
|
/**
|
|
222
|
-
*
|
|
229
|
+
* Checks a declared SQLite type against a portable storage affinity.
|
|
223
230
|
*
|
|
224
231
|
* @param declared - Native declared type
|
|
225
232
|
* @param storage - Portable column storage
|
|
226
|
-
* @returns
|
|
233
|
+
* @returns True if SQLite's official affinity rules yield the expected affinity; false otherwise
|
|
227
234
|
*/
|
|
228
235
|
function matchesSQLiteAffinity(declared, storage) {
|
|
229
236
|
if (!isString(declared)) return false;
|
|
@@ -240,7 +247,7 @@ function matchesSQLiteAffinity(declared, storage) {
|
|
|
240
247
|
return affinity === "REAL";
|
|
241
248
|
}
|
|
242
249
|
/**
|
|
243
|
-
*
|
|
250
|
+
* Quotes a SQL identifier (a table or column name) so any characters are literal.
|
|
244
251
|
*
|
|
245
252
|
* @remarks
|
|
246
253
|
* Wraps the name in double quotes and doubles any embedded quote — the standard
|
|
@@ -259,7 +266,7 @@ function quoteIdentifier(identifier) {
|
|
|
259
266
|
return "\"" + identifier.replaceAll("\"", "\"\"") + "\"";
|
|
260
267
|
}
|
|
261
268
|
/**
|
|
262
|
-
*
|
|
269
|
+
* Encodes a JS value to its stored {@link SQLiteValue} for a declared column.
|
|
263
270
|
*
|
|
264
271
|
* @remarks
|
|
265
272
|
* The codec is total: a malformed value encodes to SQL `NULL`. Absence always
|
|
@@ -298,7 +305,7 @@ function encodeValue(value, column) {
|
|
|
298
305
|
}
|
|
299
306
|
}
|
|
300
307
|
/**
|
|
301
|
-
*
|
|
308
|
+
* Decodes a stored {@link SQLiteValue} back to its JS value for a declared column —
|
|
302
309
|
* the exact inverse of {@link encodeValue}.
|
|
303
310
|
*
|
|
304
311
|
* @remarks
|
|
@@ -339,7 +346,7 @@ function decodeValue(value, column) {
|
|
|
339
346
|
}
|
|
340
347
|
}
|
|
341
348
|
/**
|
|
342
|
-
*
|
|
349
|
+
* Encodes a whole {@link Row} to a {@link SQLiteRow} by its table's schema.
|
|
343
350
|
*
|
|
344
351
|
* @remarks
|
|
345
352
|
* Encodes each declared column's value with {@link encodeValue}; columns the row
|
|
@@ -361,7 +368,7 @@ function encodeRow(row, schema) {
|
|
|
361
368
|
return result;
|
|
362
369
|
}
|
|
363
370
|
/**
|
|
364
|
-
*
|
|
371
|
+
* Extracts a stored row's values in a declared positional order.
|
|
365
372
|
*
|
|
366
373
|
* @remarks
|
|
367
374
|
* SQLite statements bind arrays positionally. Every requested column must be
|
|
@@ -392,7 +399,7 @@ function extractValues(row, names, table) {
|
|
|
392
399
|
return values;
|
|
393
400
|
}
|
|
394
401
|
/**
|
|
395
|
-
*
|
|
402
|
+
* Decodes a stored {@link SQLiteRow} back to a {@link Row} by its table's schema.
|
|
396
403
|
*
|
|
397
404
|
* @remarks
|
|
398
405
|
* Decodes each declared column with {@link decodeValue} and **omits** any column
|
|
@@ -421,12 +428,12 @@ function decodeRow(row, schema) {
|
|
|
421
428
|
return result;
|
|
422
429
|
}
|
|
423
430
|
/**
|
|
424
|
-
*
|
|
431
|
+
* Builds a collision-free SQL index name for a table + column-group index —
|
|
425
432
|
* shared by the compiler module's `schemaToIndexes` and `stepToSQL`,
|
|
426
433
|
* so a plan-built index name always matches one `open` would have created.
|
|
427
434
|
*
|
|
428
435
|
* @remarks
|
|
429
|
-
* A naive `idx_<table>_<cols joined by _>` is
|
|
436
|
+
* A naive `idx_<table>_<cols joined by _>` is ambiguous: table `'a_b'` with
|
|
430
437
|
* column `'c'` and table `'a'` with columns `['b', 'c']` both produce
|
|
431
438
|
* `idx_a_b_c`. This encodes each part (the table name, then each column name)
|
|
432
439
|
* length-prefixed (`<len>_<part>`) so the boundary between parts is always
|
|
@@ -447,9 +454,39 @@ function deriveSQLiteIndexName(table, columns) {
|
|
|
447
454
|
return "idx_" + [table, ...columns].map((part) => String(part.length) + "_" + part).join("_");
|
|
448
455
|
}
|
|
449
456
|
//#endregion
|
|
457
|
+
//#region src/server/inferers.ts
|
|
458
|
+
/**
|
|
459
|
+
* Reads the storage type a nested (`json_extract`) operand encodes as from its
|
|
460
|
+
* runtime value, never as `json`.
|
|
461
|
+
*
|
|
462
|
+
* @remarks
|
|
463
|
+
* `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
|
|
464
|
+
* `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
|
|
465
|
+
* same scalar to compare. A boolean → `'boolean'` (→ `1` / `0`); a number →
|
|
466
|
+
* `'integer'` / `'real'`; a bigint → `'integer'`; a string → `'text'`; `null` /
|
|
467
|
+
* `undefined` → `'text'` (encodes to `null`); an object / array → `'json'` (the
|
|
468
|
+
* edge of comparing against a json subtree).
|
|
469
|
+
*
|
|
470
|
+
* @param value - The runtime operand value
|
|
471
|
+
* @returns The {@link ColumnStorage} to encode it as
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```ts
|
|
475
|
+
* inferValueStorage(true) // 'boolean'
|
|
476
|
+
* inferValueStorage(9) // 'integer'
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
function inferValueStorage(value) {
|
|
480
|
+
if (typeof value === "boolean") return "boolean";
|
|
481
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
482
|
+
if (typeof value === "bigint") return "integer";
|
|
483
|
+
if (typeof value === "object" && value !== null) return "json";
|
|
484
|
+
return "text";
|
|
485
|
+
}
|
|
486
|
+
//#endregion
|
|
450
487
|
//#region src/server/compilers.ts
|
|
451
488
|
/**
|
|
452
|
-
*
|
|
489
|
+
* Maps a portable {@link ColumnStorage} to its SQLite column type.
|
|
453
490
|
*
|
|
454
491
|
* @param storage - The portable column type
|
|
455
492
|
* @returns The SQLite column type keyword
|
|
@@ -465,7 +502,11 @@ function compileColumnSQL(storage) {
|
|
|
465
502
|
}
|
|
466
503
|
}
|
|
467
504
|
/**
|
|
468
|
-
*
|
|
505
|
+
* Compiles a {@link FieldPath} to the SQL expression that reads it.
|
|
506
|
+
*
|
|
507
|
+
* @remarks
|
|
508
|
+
* A flat path compiles to the quoted column; a nested path compiles to a
|
|
509
|
+
* `json_extract` over the head column with the rest of the path as its accessor.
|
|
469
510
|
*
|
|
470
511
|
* @param path - The field path
|
|
471
512
|
* @returns The SQL expression selecting the value
|
|
@@ -478,7 +519,7 @@ function compileFieldSQL(path) {
|
|
|
478
519
|
return "json_extract(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
479
520
|
}
|
|
480
521
|
/**
|
|
481
|
-
*
|
|
522
|
+
* Compiles an {@link AggregateOperation} over a {@link FieldPath}.
|
|
482
523
|
*
|
|
483
524
|
* @param operation - The aggregate to compute
|
|
484
525
|
* @param column - The column or nested path to aggregate
|
|
@@ -494,9 +535,9 @@ function compileAggregateSQL(operation, column) {
|
|
|
494
535
|
}
|
|
495
536
|
}
|
|
496
537
|
/**
|
|
497
|
-
*
|
|
538
|
+
* Compiles a nested {@link FieldPath} to the `json_type(<col>, <path>)` SQL
|
|
498
539
|
* expression — the {@link compileFieldSQL} `json_extract` sibling used to tell a
|
|
499
|
-
*
|
|
540
|
+
* present JSON `null` apart from an absent path (both read back as SQL `NULL`
|
|
500
541
|
* through `json_extract`, but `json_type` reports `'null'` for the former and
|
|
501
542
|
* SQL `NULL` for the latter).
|
|
502
543
|
*
|
|
@@ -515,83 +556,25 @@ function compileJSONTypeSQL(path) {
|
|
|
515
556
|
return "json_type(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
516
557
|
}
|
|
517
558
|
/**
|
|
518
|
-
*
|
|
519
|
-
* operand is matched literally under the `LIKE … ESCAPE '\'` clause.
|
|
520
|
-
*
|
|
521
|
-
* @param text - The raw operand text
|
|
522
|
-
* @returns The text with LIKE metacharacters escaped
|
|
523
|
-
*
|
|
524
|
-
* @example
|
|
525
|
-
* ```ts
|
|
526
|
-
* escapeLike('50%_off') // '50\\%\\_off'
|
|
527
|
-
* ```
|
|
528
|
-
*/
|
|
529
|
-
function escapeLike(text) {
|
|
530
|
-
return text.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* The declared storage type of a flat (string) column, read from the schema.
|
|
534
|
-
*
|
|
535
|
-
* @param column - The column name
|
|
536
|
-
* @param schema - The table's schema
|
|
537
|
-
* @returns The column's {@link ColumnStorage}, or `undefined` if the schema does not carry it
|
|
538
|
-
*
|
|
539
|
-
* @example
|
|
540
|
-
* ```ts
|
|
541
|
-
* findColumnStorage('age', schema) // 'integer'
|
|
542
|
-
* ```
|
|
543
|
-
*/
|
|
544
|
-
function findColumnStorage(column, schema) {
|
|
545
|
-
return schema.columns.find((candidate) => candidate.name === column)?.storage;
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* The storage type a nested (`json_extract`) operand encodes as, derived from its
|
|
549
|
-
* RUNTIME value — NOT `json`.
|
|
550
|
-
*
|
|
551
|
-
* @remarks
|
|
552
|
-
* `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
|
|
553
|
-
* `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
|
|
554
|
-
* same scalar to compare. A boolean → `'boolean'` (→ `1` / `0`); a number →
|
|
555
|
-
* `'integer'` / `'real'`; a bigint → `'integer'`; a string → `'text'`; `null` /
|
|
556
|
-
* `undefined` → `'text'` (encodes to `null`); an object / array → `'json'` (the
|
|
557
|
-
* edge of comparing against a json subtree).
|
|
558
|
-
*
|
|
559
|
-
* @param value - The runtime operand value
|
|
560
|
-
* @returns The {@link ColumnStorage} to encode it as
|
|
561
|
-
*
|
|
562
|
-
* @example
|
|
563
|
-
* ```ts
|
|
564
|
-
* inferValueStorage(true) // 'boolean'
|
|
565
|
-
* inferValueStorage(9) // 'integer'
|
|
566
|
-
* ```
|
|
567
|
-
*/
|
|
568
|
-
function inferValueStorage(value) {
|
|
569
|
-
if (typeof value === "boolean") return "boolean";
|
|
570
|
-
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
571
|
-
if (typeof value === "bigint") return "integer";
|
|
572
|
-
if (typeof value === "object" && value !== null) return "json";
|
|
573
|
-
return "text";
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* Compile one condition to its `<column> <operator>` SQL fragment and the parameters
|
|
559
|
+
* Compiles one condition to its `<column> <operator>` SQL fragment and the parameters
|
|
577
560
|
* it binds — engine-exact under SQL's three-valued NULL logic.
|
|
578
561
|
*
|
|
579
562
|
* @remarks
|
|
580
563
|
* Every operand is run through `encodeValue`, so a bound value matches the SQL
|
|
581
|
-
* the column side compiles to. A flat column encodes operands with its
|
|
564
|
+
* the column side compiles to. A flat column encodes operands with its declared
|
|
582
565
|
* schema type (a flat `json` column → `JSON.stringify`); a nested `FieldPath`
|
|
583
|
-
* encodes each operand as the
|
|
566
|
+
* encodes each operand as the native scalar `json_extract` returns, derived from
|
|
584
567
|
* the operand's runtime type (per-operand, since `between` / `any` / `none` can
|
|
585
568
|
* mix types). `any` / `none` collapse an empty list to a constant (`0` matches
|
|
586
569
|
* nothing, `1` matches all) with no parameters.
|
|
587
570
|
*
|
|
588
|
-
* The core engine's total order ranks `undefined` (rank 0)
|
|
589
|
-
* (rank 1) (see `compareValues`), so a
|
|
571
|
+
* The core engine's total order ranks `undefined` (rank 0) below `null`
|
|
572
|
+
* (rank 1) (see `compareValues`), so a missing/`NULL` column matches
|
|
590
573
|
* `below` / `to` / a scalar `not` / `none` — the opposite of raw SQL, where a
|
|
591
574
|
* comparison against `NULL` is `NULL` (excluded). This fragment replicates the
|
|
592
575
|
* engine exactly. Truth table (`value` = the engine's decoded field read; a
|
|
593
|
-
*
|
|
594
|
-
* flat `value` is
|
|
576
|
+
* flat column's stored `NULL` decodes to `undefined` per `decodeRow`, so a
|
|
577
|
+
* flat `value` is never a present `null` — only a nested path can be
|
|
595
578
|
* present-but-`null`):
|
|
596
579
|
*
|
|
597
580
|
* ```text
|
|
@@ -617,15 +600,15 @@ function inferValueStorage(value) {
|
|
|
617
600
|
* refines every optional or nullable scalar comparison through the core engine.
|
|
618
601
|
* This compiler still emits a total SQL fragment for direct consumers.
|
|
619
602
|
*
|
|
620
|
-
* A
|
|
621
|
-
* `json_extract` reads back as SQL `NULL` — indistinguishable from an
|
|
603
|
+
* A nested path can be present-but-`null` (a stored JSON `null`), which
|
|
604
|
+
* `json_extract` reads back as SQL `NULL` — indistinguishable from an absent
|
|
622
605
|
* path. `json_type(col, path)` disambiguates them (`'null'` for present-null,
|
|
623
606
|
* SQL `NULL` for absent), so nested `equals` / `not` against a `null` operand
|
|
624
607
|
* compile through `json_type` instead of `IS NULL` / `IS NOT NULL`.
|
|
625
608
|
*
|
|
626
609
|
* Every other MATCH-on-null-or-absent row is expressed uniformly (flat and
|
|
627
610
|
* nested alike) as `(<column> <op> ? OR <column> IS NULL)` — for a nested
|
|
628
|
-
* path, `json_extract` already collapses
|
|
611
|
+
* path, `json_extract` already collapses both absent and present-null to SQL
|
|
629
612
|
* `NULL`, so `IS NULL` catches both in one clause; for a flat column there is
|
|
630
613
|
* only the absent case to catch.
|
|
631
614
|
*
|
|
@@ -644,7 +627,7 @@ function inferValueStorage(value) {
|
|
|
644
627
|
function compileConditionSQL(condition, schema) {
|
|
645
628
|
const column = compileFieldSQL(condition.column);
|
|
646
629
|
const nested = !isString(condition.column);
|
|
647
|
-
const declared = isString(condition.column) ?
|
|
630
|
+
const declared = isString(condition.column) ? findColumn(condition.column, schema) : void 0;
|
|
648
631
|
const first = condition.values[0];
|
|
649
632
|
const second = condition.values[1];
|
|
650
633
|
const nullOperand = first === null || first === void 0;
|
|
@@ -763,13 +746,13 @@ function compileConditionSQL(condition, schema) {
|
|
|
763
746
|
};
|
|
764
747
|
}
|
|
765
748
|
/**
|
|
766
|
-
*
|
|
749
|
+
* Folds the conditions into one WHERE clause, parenthesizing progressively
|
|
767
750
|
* left-to-right so the grouping matches the engine's `matchesQuery` fold.
|
|
768
751
|
*
|
|
769
752
|
* @remarks
|
|
770
753
|
* The first condition's connector is ignored, per the {@link Condition} types.
|
|
771
754
|
* Every fragment (see {@link compileConditionSQL}'s truth table) replicates the core
|
|
772
|
-
* engine's total order
|
|
755
|
+
* engine's total order exactly under SQL's three-valued NULL logic, so this
|
|
773
756
|
* clause matches `applyQuery` row-for-row over the same table — a native
|
|
774
757
|
* `records` / `count` read never disagrees with a scan-and-filter fallback.
|
|
775
758
|
*
|
|
@@ -779,11 +762,11 @@ function compileConditionSQL(condition, schema) {
|
|
|
779
762
|
*
|
|
780
763
|
* @example
|
|
781
764
|
* ```ts
|
|
782
|
-
*
|
|
765
|
+
* compileWhereSQL([{ column: 'age', operator: 'from', values: [18], connector: 'and' }], schema)
|
|
783
766
|
* // { sql: 'WHERE "age" >= ?', parameters: [18] }
|
|
784
767
|
* ```
|
|
785
768
|
*/
|
|
786
|
-
function
|
|
769
|
+
function compileWhereSQL(conditions, schema) {
|
|
787
770
|
const [first, ...remaining] = conditions;
|
|
788
771
|
if (first === void 0) return {
|
|
789
772
|
sql: "",
|
|
@@ -804,14 +787,14 @@ function compileWhere(conditions, schema) {
|
|
|
804
787
|
};
|
|
805
788
|
}
|
|
806
789
|
/**
|
|
807
|
-
*
|
|
790
|
+
* Compiles the ORDER BY clause from the order terms, always ending with the
|
|
808
791
|
* primary key as the final determinant.
|
|
809
792
|
*
|
|
810
793
|
* @remarks
|
|
811
794
|
* The native `records` read then resolves ties in key order, matching a
|
|
812
795
|
* primary-key-ordered `scan` and the core engine's stable `sortRows` over a
|
|
813
796
|
* key-ordered scan (and IndexedDB's key-ordered reads), so a native read equals
|
|
814
|
-
* the scan path
|
|
797
|
+
* the scan path — native ↔ engine parity. SQLite without an
|
|
815
798
|
* `ORDER BY` returns rowid (insertion) order, and an explicit order alone breaks
|
|
816
799
|
* ties by rowid too — both diverge from every key-ordered backend. The
|
|
817
800
|
* tie-breaker is ASCENDING regardless of the explicit directions: the engine's
|
|
@@ -825,17 +808,17 @@ function compileWhere(conditions, schema) {
|
|
|
825
808
|
*
|
|
826
809
|
* @example
|
|
827
810
|
* ```ts
|
|
828
|
-
*
|
|
811
|
+
* compileOrderSQL([{ column: 'age', direction: 'descending' }], schema)
|
|
829
812
|
* // 'ORDER BY "age" DESC, "id"'
|
|
830
813
|
* ```
|
|
831
814
|
*/
|
|
832
|
-
function
|
|
815
|
+
function compileOrderSQL(order, schema) {
|
|
833
816
|
const terms = (order ?? []).map((term) => compileFieldSQL(term.column) + (term.direction === "descending" ? " DESC" : " ASC"));
|
|
834
817
|
if (!(order ?? []).some((term) => isString(term.column) && term.column === schema.primary)) terms.push(quoteIdentifier(schema.primary));
|
|
835
818
|
return terms.length === 0 ? "" : "ORDER BY " + terms.join(", ");
|
|
836
819
|
}
|
|
837
820
|
/**
|
|
838
|
-
*
|
|
821
|
+
* Compiles the LIMIT / OFFSET clause.
|
|
839
822
|
*
|
|
840
823
|
* @remarks
|
|
841
824
|
* An offset without a limit uses `LIMIT -1` (SQLite's "no limit") so OFFSET is
|
|
@@ -847,10 +830,10 @@ function compileOrder(order, schema) {
|
|
|
847
830
|
*
|
|
848
831
|
* @example
|
|
849
832
|
* ```ts
|
|
850
|
-
*
|
|
833
|
+
* compilePageSQL(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?', parameters: [5] }
|
|
851
834
|
* ```
|
|
852
835
|
*/
|
|
853
|
-
function
|
|
836
|
+
function compilePageSQL(limit, offset) {
|
|
854
837
|
validatePage({
|
|
855
838
|
...limit === void 0 ? {} : { limit },
|
|
856
839
|
...offset === void 0 ? {} : { offset }
|
|
@@ -873,7 +856,7 @@ function compilePage(limit, offset) {
|
|
|
873
856
|
};
|
|
874
857
|
}
|
|
875
858
|
/**
|
|
876
|
-
*
|
|
859
|
+
* Compiles a {@link QueryInput} into the SQL clause that follows a table name, with
|
|
877
860
|
* its bound parameters in clause order.
|
|
878
861
|
*
|
|
879
862
|
* @remarks
|
|
@@ -883,11 +866,13 @@ function compilePage(limit, offset) {
|
|
|
883
866
|
* over a JS `scan`. The WHERE fold is parenthesized **left-to-right** to mirror
|
|
884
867
|
* the core engine's `matchesQuery` (not SQL's native AND-over-OR precedence),
|
|
885
868
|
* so a native and an engine read return identical rows. Each operand is encoded
|
|
886
|
-
*
|
|
869
|
+
* through `encodeValue`: a flat column uses its declared schema type, while a nested
|
|
887
870
|
* `FieldPath` (a `json_extract` read) encodes each operand as the native scalar
|
|
888
871
|
* the extract returns — derived from the operand's runtime type — so it compares.
|
|
889
|
-
*
|
|
890
|
-
* `starts` / `ends`
|
|
872
|
+
* Every operator maps per the databases guide's operator table, with
|
|
873
|
+
* `starts` / `ends` compiling to a code-point `substr` slice guarded by
|
|
874
|
+
* `typeof(<column>) = 'text'` (case-sensitive, matching the engine's
|
|
875
|
+
* `String.prototype.startsWith` / `endsWith`) and an empty `any` / `none` list
|
|
891
876
|
* collapsing to a constant. An `undefined` input (or one with no parts)
|
|
892
877
|
* compiles to an empty clause.
|
|
893
878
|
*
|
|
@@ -903,9 +888,9 @@ function compilePage(limit, offset) {
|
|
|
903
888
|
*/
|
|
904
889
|
function compileQuerySQL(input, schema) {
|
|
905
890
|
validatePage(input);
|
|
906
|
-
const where =
|
|
907
|
-
const orderBy =
|
|
908
|
-
const page =
|
|
891
|
+
const where = compileWhereSQL(input?.conditions ?? [], schema);
|
|
892
|
+
const orderBy = compileOrderSQL(input?.order, schema);
|
|
893
|
+
const page = compilePageSQL(input?.limit, input?.offset);
|
|
909
894
|
return {
|
|
910
895
|
sql: [
|
|
911
896
|
where.sql,
|
|
@@ -916,7 +901,7 @@ function compileQuerySQL(input, schema) {
|
|
|
916
901
|
};
|
|
917
902
|
}
|
|
918
903
|
/**
|
|
919
|
-
*
|
|
904
|
+
* Projects a {@link TableSchema} to its `CREATE TABLE IF NOT EXISTS` statement.
|
|
920
905
|
*
|
|
921
906
|
* @param schema - The table schema
|
|
922
907
|
* @returns The complete table declaration
|
|
@@ -926,7 +911,11 @@ function schemaToTable(schema) {
|
|
|
926
911
|
return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(schema.name) + " (" + columns.join(", ") + ", PRIMARY KEY (" + quoteIdentifier(schema.primary) + "))";
|
|
927
912
|
}
|
|
928
913
|
/**
|
|
929
|
-
*
|
|
914
|
+
* Projects a {@link TableSchema} to its declared SQLite indexes.
|
|
915
|
+
*
|
|
916
|
+
* @remarks
|
|
917
|
+
* Each statement is a `CREATE INDEX IF NOT EXISTS` named by
|
|
918
|
+
* {@link deriveSQLiteIndexName}, so a reopen re-issues the set safely.
|
|
930
919
|
*
|
|
931
920
|
* @param schema - The table schema
|
|
932
921
|
* @returns One statement per declared index
|
|
@@ -935,7 +924,11 @@ function schemaToIndexes(schema) {
|
|
|
935
924
|
return schema.indexes.map((group) => "CREATE INDEX IF NOT EXISTS " + quoteIdentifier(deriveSQLiteIndexName(schema.name, group)) + " ON " + quoteIdentifier(schema.name) + " (" + group.map(quoteIdentifier).join(", ") + ")");
|
|
936
925
|
}
|
|
937
926
|
/**
|
|
938
|
-
*
|
|
927
|
+
* Projects one {@link MigrationStep} to SQLite DDL.
|
|
928
|
+
*
|
|
929
|
+
* @remarks
|
|
930
|
+
* These are the statements the SQLite driver's `migrate` executes for the step,
|
|
931
|
+
* inside whichever native transaction is active.
|
|
939
932
|
*
|
|
940
933
|
* @param step - The migration step
|
|
941
934
|
* @returns The statements that apply the step
|
|
@@ -951,97 +944,9 @@ function stepToSQL(step) {
|
|
|
951
944
|
}
|
|
952
945
|
}
|
|
953
946
|
//#endregion
|
|
954
|
-
//#region src/core/DriverIterator.ts
|
|
955
|
-
/**
|
|
956
|
-
* The internal continuation boundary for a root driver async iterator.
|
|
957
|
-
*
|
|
958
|
-
* @remarks
|
|
959
|
-
* A driver transaction can begin while a caller holds an idle root iterator.
|
|
960
|
-
* Every `next` therefore checks the driver's root-state guard immediately
|
|
961
|
-
* before and after advancing the source. A failed continuation terminalizes the
|
|
962
|
-
* iterator, discards any row produced before the post-advance guard failed, and
|
|
963
|
-
* attempts source cleanup exactly once.
|
|
964
|
-
*/
|
|
965
|
-
var DriverIterator = class {
|
|
966
|
-
#source;
|
|
967
|
-
#guard;
|
|
968
|
-
#terminal = false;
|
|
969
|
-
#cleaned = false;
|
|
970
|
-
constructor(source, guard) {
|
|
971
|
-
this.#source = source;
|
|
972
|
-
this.#guard = guard;
|
|
973
|
-
}
|
|
974
|
-
[Symbol.asyncIterator]() {
|
|
975
|
-
return this;
|
|
976
|
-
}
|
|
977
|
-
async next() {
|
|
978
|
-
if (this.#terminal) return {
|
|
979
|
-
done: true,
|
|
980
|
-
value: void 0
|
|
981
|
-
};
|
|
982
|
-
try {
|
|
983
|
-
this.#guard();
|
|
984
|
-
const result = await this.#source.next();
|
|
985
|
-
this.#guard();
|
|
986
|
-
if (result.done === true) {
|
|
987
|
-
this.#terminal = true;
|
|
988
|
-
this.#cleaned = true;
|
|
989
|
-
}
|
|
990
|
-
return result;
|
|
991
|
-
} catch (error) {
|
|
992
|
-
this.#terminal = true;
|
|
993
|
-
await this.#discard();
|
|
994
|
-
throw error;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
async return() {
|
|
998
|
-
if (this.#terminal) return {
|
|
999
|
-
done: true,
|
|
1000
|
-
value: void 0
|
|
1001
|
-
};
|
|
1002
|
-
this.#terminal = true;
|
|
1003
|
-
if (this.#cleaned || this.#source.return === void 0) {
|
|
1004
|
-
this.#cleaned = true;
|
|
1005
|
-
return {
|
|
1006
|
-
done: true,
|
|
1007
|
-
value: void 0
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
this.#cleaned = true;
|
|
1011
|
-
return this.#source.return();
|
|
1012
|
-
}
|
|
1013
|
-
async throw(error) {
|
|
1014
|
-
if (this.#terminal) throw error;
|
|
1015
|
-
if (this.#source.throw === void 0) {
|
|
1016
|
-
this.#terminal = true;
|
|
1017
|
-
await this.#discard();
|
|
1018
|
-
throw error;
|
|
1019
|
-
}
|
|
1020
|
-
try {
|
|
1021
|
-
const result = await this.#source.throw(error);
|
|
1022
|
-
if (result.done === true) {
|
|
1023
|
-
this.#terminal = true;
|
|
1024
|
-
this.#cleaned = true;
|
|
1025
|
-
}
|
|
1026
|
-
return result;
|
|
1027
|
-
} catch (cause) {
|
|
1028
|
-
this.#terminal = true;
|
|
1029
|
-
await this.#discard();
|
|
1030
|
-
throw cause;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
async #discard() {
|
|
1034
|
-
if (this.#cleaned) return;
|
|
1035
|
-
this.#cleaned = true;
|
|
1036
|
-
try {
|
|
1037
|
-
await this.#source.return?.();
|
|
1038
|
-
} catch {}
|
|
1039
|
-
}
|
|
1040
|
-
};
|
|
1041
|
-
//#endregion
|
|
1042
947
|
//#region src/server/drivers/JSONDriver.ts
|
|
1043
948
|
/**
|
|
1044
|
-
*
|
|
949
|
+
* Implements a persistent {@link DriverInterface} backed by a single JSON file — the
|
|
1045
950
|
* reference {@link MemoryDriver} plus file load / flush.
|
|
1046
951
|
*
|
|
1047
952
|
* @remarks
|
|
@@ -1055,15 +960,15 @@ var DriverIterator = class {
|
|
|
1055
960
|
* primary (the table contract), so the key is recovered on load with
|
|
1056
961
|
* {@link extractKey} and the file need not store it. The parsed JSON crosses the
|
|
1057
962
|
* boundary as `unknown` and is narrowed with {@link isRecord} / {@link extractKey},
|
|
1058
|
-
* never asserted
|
|
963
|
+
* never asserted. A read that reports no document there starts empty —
|
|
1059
964
|
* `ENOENT` for a plain absence, and `ENOTDIR` for a path whose parent is not a
|
|
1060
965
|
* directory, which no later write could find either; every other read failure or
|
|
1061
966
|
* invalid existing document fails closed without publication, mutation, or
|
|
1062
|
-
* automatic repair. It
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
* driver.
|
|
967
|
+
* automatic repair. It implements the optional native `stream` hook that
|
|
968
|
+
* `TableInterface.scan` prefers over `scan`, and neither `records` nor
|
|
969
|
+
* `aggregate`, so the core engine's `matchesQuery` answers every query on
|
|
970
|
+
* either path. For development, small datasets, and portable / inspectable
|
|
971
|
+
* data; for large or concurrent workloads reach for a SQLite-backed driver.
|
|
1067
972
|
*
|
|
1068
973
|
* Metadata crosses {@link cloneDriverMetadata} at parsed-file ingress, public and
|
|
1069
974
|
* scoped write ingress, candidate/root publication, serialization, and copy-out.
|
|
@@ -1127,7 +1032,7 @@ var JSONDriver = class {
|
|
|
1127
1032
|
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1128
1033
|
}
|
|
1129
1034
|
/**
|
|
1130
|
-
*
|
|
1035
|
+
* Iterates rows lazily with native filtering — delegates to the inner {@link MemoryDriver}.
|
|
1131
1036
|
*
|
|
1132
1037
|
* @remarks
|
|
1133
1038
|
* Semantics are the memory driver's own: `input.conditions` filters, `offset`
|
|
@@ -1146,7 +1051,7 @@ var JSONDriver = class {
|
|
|
1146
1051
|
await this.#enqueue(() => this.#clear(table));
|
|
1147
1052
|
}
|
|
1148
1053
|
/**
|
|
1149
|
-
*
|
|
1054
|
+
* Runs an isolated native transaction callback over a candidate memory store.
|
|
1150
1055
|
*
|
|
1151
1056
|
* @remarks
|
|
1152
1057
|
* Single-writer: nesting and root operations while active throw `CONFLICT`.
|
|
@@ -1163,7 +1068,7 @@ var JSONDriver = class {
|
|
|
1163
1068
|
return this.#enqueue(() => this.#transact(scope));
|
|
1164
1069
|
}
|
|
1165
1070
|
/**
|
|
1166
|
-
*
|
|
1071
|
+
* Captures an owned row snapshot at an exact writer-queue position.
|
|
1167
1072
|
*
|
|
1168
1073
|
* @remarks
|
|
1169
1074
|
* Capture owns table names, schemas, rows, and one session-local identity per
|
|
@@ -1191,7 +1096,7 @@ var JSONDriver = class {
|
|
|
1191
1096
|
return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
|
|
1192
1097
|
}
|
|
1193
1098
|
/**
|
|
1194
|
-
*
|
|
1099
|
+
* Persists an owned metadata snapshot for a later `metadata()` to copy out.
|
|
1195
1100
|
*
|
|
1196
1101
|
* @remarks
|
|
1197
1102
|
* Root stamping conflicts while a transaction is active. The scoped
|
|
@@ -1206,7 +1111,7 @@ var JSONDriver = class {
|
|
|
1206
1111
|
await this.#enqueue(() => this.#stamp(owned));
|
|
1207
1112
|
}
|
|
1208
1113
|
/**
|
|
1209
|
-
*
|
|
1114
|
+
* Applies one atomic {@link MigrationInput} through an isolated candidate.
|
|
1210
1115
|
*
|
|
1211
1116
|
* @remarks
|
|
1212
1117
|
* The candidate receives every plan step plus optional metadata. Its complete
|
|
@@ -1420,7 +1325,8 @@ var JSONDriver = class {
|
|
|
1420
1325
|
return this.#requireCandidate(token).keys(table);
|
|
1421
1326
|
}
|
|
1422
1327
|
#scanCandidate(token, table) {
|
|
1423
|
-
|
|
1328
|
+
const source = this.#requireCandidate(token).scan(table);
|
|
1329
|
+
return new DriverIterator(source[Symbol.asyncIterator](), () => {
|
|
1424
1330
|
this.#requireCandidate(token);
|
|
1425
1331
|
});
|
|
1426
1332
|
}
|
|
@@ -1667,8 +1573,8 @@ var JSONDriver = class {
|
|
|
1667
1573
|
//#endregion
|
|
1668
1574
|
//#region src/server/drivers/SQLiteDriver.ts
|
|
1669
1575
|
/**
|
|
1670
|
-
*
|
|
1671
|
-
* built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
1576
|
+
* Implements the {@link DriverInterface} over SQLite — the server-native, trusted-mode
|
|
1577
|
+
* backend built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
1672
1578
|
*
|
|
1673
1579
|
* @remarks
|
|
1674
1580
|
* A thin adapter: it implements the storage primitives the core database layer
|
|
@@ -1679,41 +1585,40 @@ var JSONDriver = class {
|
|
|
1679
1585
|
* reopen-safe), and readies a reserved `_metadata` single-row table `metadata()` /
|
|
1680
1586
|
* `stamp()` read and write — **a user table named `_metadata` collides with it**;
|
|
1681
1587
|
* avoid the name. Rows cross the boundary through the codecs in `helpers.ts`
|
|
1682
|
-
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so
|
|
1683
|
-
* typed layer above imposes the exact shape
|
|
1588
|
+
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so
|
|
1589
|
+
* the typed layer above imposes the exact shape. `write` is an
|
|
1684
1590
|
* `INSERT OR REPLACE` upsert, while `insert` uses a plain `INSERT` and maps its
|
|
1685
1591
|
* atomic primary-key constraint failure to `CONFLICT`; every other backend
|
|
1686
1592
|
* `SQLiteError` is contained by the same `DatabaseError` boundary described
|
|
1687
|
-
* below. Querying, ordering, paging, and
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
* `
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
* `JSONDriver` migrate; a step referencing an undeclared table throws
|
|
1593
|
+
* below. Querying, ordering, paging, and aggregation is native: `records` /
|
|
1594
|
+
* `stream` compile a `QueryInput` to SQL with `compileQuerySQL`, and
|
|
1595
|
+
* `aggregate` runs a SQL `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (through
|
|
1596
|
+
* `compileAggregateSQL`) over the same compiled WHERE. `transaction` runs a
|
|
1597
|
+
* callback inside native `BEGIN` / `COMMIT` / `ROLLBACK`, passing a scoped
|
|
1598
|
+
* storage capability that becomes invalid after settlement. `migrate` runs the
|
|
1599
|
+
* plan's projected DDL ({@link import('../compilers.js').stepToSQL}) inside
|
|
1600
|
+
* whichever native transaction is active: joined into the active transaction
|
|
1601
|
+
* callback when one exists (the core's versioned reconcile path wraps migrate +
|
|
1602
|
+
* stamp in one native `BEGIN`, and node:sqlite rejects a nested `BEGIN`), or
|
|
1603
|
+
* inside its own `database.transact` otherwise — a mid-plan failure rolls
|
|
1604
|
+
* back atomically either way, an improvement over the non-atomic `MemoryDriver`
|
|
1605
|
+
* / `JSONDriver` migrate; a step referencing an undeclared table throws
|
|
1701
1606
|
* `DatabaseError` `MIGRATION` before any DDL for that step runs. `snapshot` is
|
|
1702
|
-
* capture-replay (SELECT the
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1607
|
+
* capture-replay (SELECT the named tables' rows, replay through DELETE + INSERT OR
|
|
1608
|
+
* REPLACE inside a native transaction on rollback) rather than a SQL
|
|
1609
|
+
* `SAVEPOINT`, since the core `transaction` calls the rollback thunk only on
|
|
1610
|
+
* failure with no commit-on-success signal — a long-lived `SAVEPOINT` would
|
|
1611
|
+
* leave the connection uncommitted (lost on close). Every backend interaction
|
|
1612
|
+
* runs through `#guard`, which maps a thrown backend `SQLiteError` (or any
|
|
1613
|
+
* unexpected non-`SQLiteError` throw) to a typed {@link DatabaseError} — never
|
|
1614
|
+
* a raw backend error escapes `DriverInterface`: `CONSTRAINT` → `CONFLICT`, the
|
|
1615
|
+
* wrapper's own `CLOSED` → `CLOSED`, `BUSY` (a locked database that outlasted
|
|
1616
|
+
* the configured `timeout`) → a retryable `DRIVER` (`context.retryable` is
|
|
1617
|
+
* `true`), and `UNKNOWN` / any other throw → `DRIVER`. The original error is
|
|
1618
|
+
* preserved as `context.cause`. A `DatabaseError` this driver throws directly
|
|
1619
|
+
* (`CLOSED` from the `#require` gate, `NOT_FOUND` from `#table`, `MIGRATION`
|
|
1620
|
+
* from a migration-plan fault) passes through `#guard` unchanged, never
|
|
1621
|
+
* re-wrapped.
|
|
1717
1622
|
*/
|
|
1718
1623
|
var SQLiteDriver = class {
|
|
1719
1624
|
#path;
|
|
@@ -1750,7 +1655,7 @@ var SQLiteDriver = class {
|
|
|
1750
1655
|
for (const [name, value] of Object.entries(this.#options.pragmas ?? {})) database.pragma(name, value);
|
|
1751
1656
|
const map = /* @__PURE__ */ new Map();
|
|
1752
1657
|
const identities = /* @__PURE__ */ new Map();
|
|
1753
|
-
database.
|
|
1658
|
+
database.transact(() => {
|
|
1754
1659
|
this.#ensureMetadataTable(database);
|
|
1755
1660
|
const stored = this.#readMetadata(database);
|
|
1756
1661
|
const deployed = normalizeDriverSchema(stored?.schema ?? owned);
|
|
@@ -1768,11 +1673,11 @@ var SQLiteDriver = class {
|
|
|
1768
1673
|
for (const table of deployed) {
|
|
1769
1674
|
const absent = missing.get(table.name);
|
|
1770
1675
|
if (absent === void 0) {
|
|
1771
|
-
database.
|
|
1772
|
-
for (const sql of schemaToIndexes(table)) database.
|
|
1676
|
+
database.execute(schemaToTable(table));
|
|
1677
|
+
for (const sql of schemaToIndexes(table)) database.execute(sql);
|
|
1773
1678
|
} else for (const [index, group] of table.indexes.entries()) if (absent.includes(deriveSQLiteIndexName(table.name, group))) {
|
|
1774
1679
|
const sql = schemaToIndexes(table)[index];
|
|
1775
|
-
if (sql !== void 0) database.
|
|
1680
|
+
if (sql !== void 0) database.execute(sql);
|
|
1776
1681
|
}
|
|
1777
1682
|
identities.set(table.name, previousIdentities.get(table.name) ?? {});
|
|
1778
1683
|
}
|
|
@@ -1844,7 +1749,7 @@ var SQLiteDriver = class {
|
|
|
1844
1749
|
const conditionsExact = conditions.every((condition) => matchesConditionExactly(condition, schema));
|
|
1845
1750
|
const columnExact = matchesAggregateExactly(operation, column, schema);
|
|
1846
1751
|
if (conditionsExact && columnExact) return this.#guard(() => {
|
|
1847
|
-
const { sql, parameters } =
|
|
1752
|
+
const { sql, parameters } = compileWhereSQL(conditions, schema);
|
|
1848
1753
|
const value = this.#require().prepare("SELECT " + compileAggregateSQL(operation, column) + " AS value FROM " + quoteIdentifier(table) + (sql === "" ? "" : " " + sql)).get(parameters)?.value;
|
|
1849
1754
|
return value === null || value === void 0 ? void 0 : Number(value);
|
|
1850
1755
|
});
|
|
@@ -1857,7 +1762,7 @@ var SQLiteDriver = class {
|
|
|
1857
1762
|
return new DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1858
1763
|
}
|
|
1859
1764
|
/**
|
|
1860
|
-
*
|
|
1765
|
+
* Begins a native transaction — real `BEGIN`, `COMMIT`, `ROLLBACK`.
|
|
1861
1766
|
*
|
|
1862
1767
|
* @remarks
|
|
1863
1768
|
* The callback receives a scoped {@link StorageInterface}. Fulfillment
|
|
@@ -1904,23 +1809,22 @@ var SQLiteDriver = class {
|
|
|
1904
1809
|
}
|
|
1905
1810
|
}
|
|
1906
1811
|
/**
|
|
1907
|
-
*
|
|
1812
|
+
* Applies a {@link Migration} plan by executing each step's projected DDL
|
|
1908
1813
|
* ({@link import('../compilers.js').stepToSQL}).
|
|
1909
1814
|
*
|
|
1910
1815
|
* @remarks
|
|
1911
|
-
* Atomicity is provided by whichever native transaction is active: when
|
|
1912
|
-
*
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
*
|
|
1917
|
-
* generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
1816
|
+
* Atomicity is provided by whichever native transaction is active: when this
|
|
1817
|
+
* driver's own `transaction()` callback is active (the core's versioned
|
|
1818
|
+
* reconcile / migrate path joins migrate + stamp under one native `BEGIN`),
|
|
1819
|
+
* the plan's DDL runs directly inside that enclosing transaction — a mid-plan
|
|
1820
|
+
* failure rejects the callback and the driver rolls it back. node:sqlite (and
|
|
1821
|
+
* SQLite generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
1918
1822
|
* second native transaction while one is already open. Otherwise (no
|
|
1919
1823
|
* enclosing transaction), `migrate` wraps the plan in its own native
|
|
1920
1824
|
* `database.transaction` — atomic on its own: a mid-plan failure rolls
|
|
1921
1825
|
* back every DDL statement already applied by the plan. A scoped migration
|
|
1922
1826
|
* uses one fixed internal savepoint literal because the published SQLite
|
|
1923
|
-
* wrapper intentionally exposes raw `
|
|
1827
|
+
* wrapper intentionally exposes raw `execute` but no savepoint manager. That
|
|
1924
1828
|
* savepoint contains a caught inner migration so the outer callback
|
|
1925
1829
|
* transaction remains active and may continue safely. A step referencing a
|
|
1926
1830
|
* table not in this driver's declared schema (and that is not itself a
|
|
@@ -1940,7 +1844,7 @@ var SQLiteDriver = class {
|
|
|
1940
1844
|
metadata: owned.metadata.schema
|
|
1941
1845
|
});
|
|
1942
1846
|
this.#guard(() => {
|
|
1943
|
-
database.
|
|
1847
|
+
database.transact(() => {
|
|
1944
1848
|
this.#applyPlan(database, owned);
|
|
1945
1849
|
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
1946
1850
|
});
|
|
@@ -1949,7 +1853,7 @@ var SQLiteDriver = class {
|
|
|
1949
1853
|
this.#identities = identities;
|
|
1950
1854
|
}
|
|
1951
1855
|
/**
|
|
1952
|
-
*
|
|
1856
|
+
* Reads the persisted {@link DriverMetadata} from the reserved `_metadata` table.
|
|
1953
1857
|
*
|
|
1954
1858
|
* @returns The last-stamped `DriverMetadata`, or `undefined` when never stamped
|
|
1955
1859
|
* (or the stored row is malformed)
|
|
@@ -1959,7 +1863,7 @@ var SQLiteDriver = class {
|
|
|
1959
1863
|
return this.#metadata();
|
|
1960
1864
|
}
|
|
1961
1865
|
/**
|
|
1962
|
-
*
|
|
1866
|
+
* Persists an owned metadata snapshot into the reserved `_metadata` table's
|
|
1963
1867
|
* single row.
|
|
1964
1868
|
*
|
|
1965
1869
|
* @param metadata - The {@link DriverMetadata} to persist
|
|
@@ -2014,11 +1918,11 @@ var SQLiteDriver = class {
|
|
|
2014
1918
|
}
|
|
2015
1919
|
this.#guard(() => {
|
|
2016
1920
|
const current = this.#require();
|
|
2017
|
-
current.
|
|
1921
|
+
current.transact(() => {
|
|
2018
1922
|
for (const [name, replacement] of replacements) {
|
|
2019
|
-
current.
|
|
1923
|
+
current.execute("DELETE FROM " + quoteIdentifier(name));
|
|
2020
1924
|
const statement = current.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(name) + " (" + replacement.names.map(quoteIdentifier).join(", ") + ") VALUES (" + replacement.names.map(() => "?").join(", ") + ")");
|
|
2021
|
-
for (const values of replacement.values) statement.
|
|
1925
|
+
for (const values of replacement.values) statement.execute(values);
|
|
2022
1926
|
}
|
|
2023
1927
|
});
|
|
2024
1928
|
});
|
|
@@ -2039,7 +1943,7 @@ var SQLiteDriver = class {
|
|
|
2039
1943
|
const values = extractValues(encoded, names, table);
|
|
2040
1944
|
const statement = this.#require().prepare("INSERT OR REPLACE INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2041
1945
|
checkAbort(options?.signal);
|
|
2042
|
-
statement.
|
|
1946
|
+
statement.execute(values);
|
|
2043
1947
|
});
|
|
2044
1948
|
}
|
|
2045
1949
|
async #insert(table, key, row, options) {
|
|
@@ -2050,7 +1954,7 @@ var SQLiteDriver = class {
|
|
|
2050
1954
|
const values = extractValues(encoded, names, table);
|
|
2051
1955
|
const statement = this.#require().prepare("INSERT INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2052
1956
|
checkAbort(options?.signal);
|
|
2053
|
-
statement.
|
|
1957
|
+
statement.execute(values);
|
|
2054
1958
|
});
|
|
2055
1959
|
}
|
|
2056
1960
|
async #delete(table, key, options) {
|
|
@@ -2058,7 +1962,7 @@ var SQLiteDriver = class {
|
|
|
2058
1962
|
return this.#guard(() => {
|
|
2059
1963
|
const statement = this.#require().prepare("DELETE FROM " + quoteIdentifier(table) + " WHERE " + quoteIdentifier(schema.primary) + " = ?");
|
|
2060
1964
|
checkAbort(options?.signal);
|
|
2061
|
-
return statement.
|
|
1965
|
+
return statement.execute([this.#key(key, schema)]).changes > 0;
|
|
2062
1966
|
});
|
|
2063
1967
|
}
|
|
2064
1968
|
async #keys(table) {
|
|
@@ -2120,14 +2024,14 @@ var SQLiteDriver = class {
|
|
|
2120
2024
|
async #clear(table) {
|
|
2121
2025
|
this.#table(table);
|
|
2122
2026
|
this.#guard(() => {
|
|
2123
|
-
this.#require().prepare("DELETE FROM " + quoteIdentifier(table)).
|
|
2027
|
+
this.#require().prepare("DELETE FROM " + quoteIdentifier(table)).execute();
|
|
2124
2028
|
});
|
|
2125
2029
|
}
|
|
2126
2030
|
async #metadata() {
|
|
2127
2031
|
return this.#guard(() => this.#readMetadata(this.#require()));
|
|
2128
2032
|
}
|
|
2129
2033
|
#ensureMetadataTable(database) {
|
|
2130
|
-
database.
|
|
2034
|
+
database.execute("CREATE TABLE IF NOT EXISTS " + quoteIdentifier(METADATA_TABLE) + " (\"id\" INTEGER, \"version\" INTEGER, \"schema\" TEXT, PRIMARY KEY (\"id\"))");
|
|
2131
2035
|
}
|
|
2132
2036
|
#validateTable(database, schema) {
|
|
2133
2037
|
const object = database.prepare("SELECT \"type\" AS \"category\" FROM \"sqlite_schema\" WHERE \"name\" = ?").get([schema.name]);
|
|
@@ -2262,7 +2166,7 @@ var SQLiteDriver = class {
|
|
|
2262
2166
|
this.#guard(() => this.#writeMetadata(database, owned));
|
|
2263
2167
|
}
|
|
2264
2168
|
#writeMetadata(database, metadata) {
|
|
2265
|
-
database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").
|
|
2169
|
+
database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").execute([metadata.version, JSON.stringify(metadata.schema)]);
|
|
2266
2170
|
}
|
|
2267
2171
|
#capability(token) {
|
|
2268
2172
|
return {
|
|
@@ -2321,16 +2225,16 @@ var SQLiteDriver = class {
|
|
|
2321
2225
|
metadata: owned.metadata.schema
|
|
2322
2226
|
});
|
|
2323
2227
|
this.#guard(() => {
|
|
2324
|
-
database.
|
|
2228
|
+
database.execute("SAVEPOINT \"_orkestrel_migration\"");
|
|
2325
2229
|
try {
|
|
2326
2230
|
this.#applyPlan(database, owned);
|
|
2327
2231
|
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
2328
|
-
database.
|
|
2232
|
+
database.execute("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
2329
2233
|
} catch (error) {
|
|
2330
2234
|
try {
|
|
2331
|
-
database.
|
|
2235
|
+
database.execute("ROLLBACK TO SAVEPOINT \"_orkestrel_migration\"");
|
|
2332
2236
|
} finally {
|
|
2333
|
-
database.
|
|
2237
|
+
database.execute("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
2334
2238
|
}
|
|
2335
2239
|
throw error;
|
|
2336
2240
|
}
|
|
@@ -2406,13 +2310,13 @@ var SQLiteDriver = class {
|
|
|
2406
2310
|
});
|
|
2407
2311
|
}
|
|
2408
2312
|
#applyPlan(database, input) {
|
|
2409
|
-
for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.
|
|
2313
|
+
for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.execute(sql);
|
|
2410
2314
|
}
|
|
2411
2315
|
};
|
|
2412
2316
|
//#endregion
|
|
2413
2317
|
//#region src/server/factories.ts
|
|
2414
2318
|
/**
|
|
2415
|
-
*
|
|
2319
|
+
* Creates a persistent JSON-file {@link DriverInterface} for a given path.
|
|
2416
2320
|
*
|
|
2417
2321
|
* @remarks
|
|
2418
2322
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
@@ -2420,9 +2324,10 @@ var SQLiteDriver = class {
|
|
|
2420
2324
|
* `Table` / `Query` API is unchanged; only where the bytes live changes.
|
|
2421
2325
|
* The driver is the reference `MemoryDriver` plus JSON-file persistence: `open` loads
|
|
2422
2326
|
* the file, every mutation flushes the whole store back, and querying runs through
|
|
2423
|
-
* the core engine
|
|
2424
|
-
* `
|
|
2425
|
-
*
|
|
2327
|
+
* the core engine's `matchesQuery`. The driver implements the native `stream`
|
|
2328
|
+
* hook and neither `records` nor `aggregate`, so the engine answers every query
|
|
2329
|
+
* on either path. A missing, corrupt, or wrong-shaped file starts empty rather
|
|
2330
|
+
* than throwing.
|
|
2426
2331
|
*
|
|
2427
2332
|
* @param path - The JSON file path data is loaded from and flushed to
|
|
2428
2333
|
* @returns A {@link DriverInterface} backed by a JSON file
|
|
@@ -2444,7 +2349,8 @@ function createJSONDriver(path) {
|
|
|
2444
2349
|
return new JSONDriver(path);
|
|
2445
2350
|
}
|
|
2446
2351
|
/**
|
|
2447
|
-
*
|
|
2352
|
+
* Creates a trusted-mode, server-native SQLite {@link DriverInterface} for a database path,
|
|
2353
|
+
* or for `:memory:` when the options bag omits one.
|
|
2448
2354
|
*
|
|
2449
2355
|
* @remarks
|
|
2450
2356
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
@@ -2483,6 +2389,6 @@ function createSQLiteDriver(options) {
|
|
|
2483
2389
|
return new SQLiteDriver(options);
|
|
2484
2390
|
}
|
|
2485
2391
|
//#endregion
|
|
2486
|
-
export { EXACT_COLUMN_STORAGE, EXACT_RANGE_COLUMN_STORAGE, JSONDriver, METADATA_TABLE, SQLiteDriver, compileAggregateSQL, compileColumnSQL, compileConditionSQL, compileFieldSQL, compileJSONTypeSQL,
|
|
2392
|
+
export { EXACT_COLUMN_STORAGE, EXACT_RANGE_COLUMN_STORAGE, JSONDriver, METADATA_TABLE, SQLiteDriver, compileAggregateSQL, compileColumnSQL, compileConditionSQL, compileFieldSQL, compileJSONTypeSQL, compileOrderSQL, compilePageSQL, compileQuerySQL, compileWhereSQL, createJSONDriver, createSQLiteDriver, decodeRow, decodeValue, deriveSQLiteIndexName, encodeRow, encodeValue, extractValues, inferValueStorage, matchesAbsentPath, matchesAggregateExactly, matchesConditionExactly, matchesDeclaredStorage, matchesOrderExactly, matchesQueryExactly, matchesSQLiteAffinity, quoteIdentifier, schemaToIndexes, schemaToTable, stepToSQL };
|
|
2487
2393
|
|
|
2488
2394
|
//# sourceMappingURL=index.js.map
|