@orkestrel/database 0.0.11 → 0.0.13
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 +2 -2
- package/dist/src/browser/index.d.ts +48 -26
- package/dist/src/browser/index.js +87 -63
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +502 -384
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +455 -269
- package/dist/src/core/index.d.ts +455 -269
- package/dist/src/core/index.js +495 -383
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +190 -306
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +126 -153
- package/dist/src/server/index.d.ts +126 -153
- package/dist/src/server/index.js +184 -298
- package/dist/src/server/index.js.map +1 -1
- package/package.json +23 -17
package/dist/src/server/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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.
|
|
@@ -18,7 +18,7 @@ import { createSQLiteDatabase, isSQLiteError } from "@orkestrel/sqlite";
|
|
|
18
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
20
|
* compares UTF-16 CODE-UNIT order. The two orders diverge for supplementary-
|
|
21
|
-
* plane characters (code points ≥ U+10000,
|
|
21
|
+
* plane characters (code points ≥ U+10000, for example many emoji): a lead surrogate
|
|
22
22
|
* (`\uD800`–`\uDBFF`) sorts BELOW ``–`` in code-unit order, while
|
|
23
23
|
* its code point sorts ABOVE them. So `matchesConditionExactly`'s range family and
|
|
24
24
|
* `matchesOrderExactly` exclude `text`, refining through the core engine instead.
|
|
@@ -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
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`.
|
|
@@ -140,15 +140,15 @@ function matchesDeclaredStorage(value, storage) {
|
|
|
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,8 +168,8 @@ 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
|
|
@@ -182,22 +182,22 @@ function matchesConditionExactly(condition, schema) {
|
|
|
182
182
|
*
|
|
183
183
|
* @param order - The order term to test
|
|
184
184
|
* @param schema - The table's schema
|
|
185
|
-
* @returns
|
|
185
|
+
* @returns True if `order` is exact; false otherwise
|
|
186
186
|
*/
|
|
187
187
|
function matchesOrderExactly(order, schema) {
|
|
188
188
|
if (!isString(order.column)) return false;
|
|
189
|
-
const column =
|
|
189
|
+
const column = findColumn(order.column, schema);
|
|
190
190
|
if (column === void 0) return false;
|
|
191
|
-
return !column.optional && !column.nullable && EXACT_RANGE_COLUMN_STORAGE.
|
|
191
|
+
return !column.optional && !column.nullable && EXACT_RANGE_COLUMN_STORAGE.includes(column.storage);
|
|
192
192
|
}
|
|
193
193
|
/**
|
|
194
|
-
*
|
|
195
|
-
* term is exact. `limit` / `offset` never affect exactness (SQL
|
|
196
|
-
* `OFFSET` are always engine-identical).
|
|
194
|
+
* Reports whether a whole {@link QueryInput} is exact — every condition and
|
|
195
|
+
* every order term is exact. `limit` / `offset` never affect exactness (SQL
|
|
196
|
+
* `LIMIT` / `OFFSET` are always engine-identical).
|
|
197
197
|
*
|
|
198
198
|
* @param input - The query input to test
|
|
199
199
|
* @param schema - The table's schema
|
|
200
|
-
* @returns
|
|
200
|
+
* @returns True if every part of `input` is exact; false otherwise
|
|
201
201
|
*/
|
|
202
202
|
function matchesQueryExactly(input, schema) {
|
|
203
203
|
const conditions = input.conditions ?? [];
|
|
@@ -205,25 +205,25 @@ function matchesQueryExactly(input, schema) {
|
|
|
205
205
|
return conditions.every((condition) => matchesConditionExactly(condition, schema)) && order.every((term) => matchesOrderExactly(term, schema));
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
208
|
-
*
|
|
208
|
+
* Reports whether SQLite can execute an aggregate exactly like the core engine.
|
|
209
209
|
*
|
|
210
210
|
* @param operation - Aggregate operation
|
|
211
211
|
* @param column - Aggregate field
|
|
212
212
|
* @param schema - Current table schema
|
|
213
|
-
* @returns
|
|
213
|
+
* @returns True if native aggregation is exact; false otherwise
|
|
214
214
|
*/
|
|
215
215
|
function matchesAggregateExactly(operation, column, schema) {
|
|
216
216
|
if (operation === "count") return true;
|
|
217
217
|
if (operation === "sum" || operation === "average" || !isString(column)) return false;
|
|
218
|
-
const declared =
|
|
218
|
+
const declared = findColumn(column, schema);
|
|
219
219
|
return declared !== void 0 && (declared.storage === "integer" || declared.storage === "real") && !(declared.optional && declared.nullable);
|
|
220
220
|
}
|
|
221
221
|
/**
|
|
222
|
-
*
|
|
222
|
+
* Checks a declared SQLite type against a portable storage affinity.
|
|
223
223
|
*
|
|
224
224
|
* @param declared - Native declared type
|
|
225
225
|
* @param storage - Portable column storage
|
|
226
|
-
* @returns
|
|
226
|
+
* @returns True if SQLite's official affinity rules yield the expected affinity; false otherwise
|
|
227
227
|
*/
|
|
228
228
|
function matchesSQLiteAffinity(declared, storage) {
|
|
229
229
|
if (!isString(declared)) return false;
|
|
@@ -240,7 +240,7 @@ function matchesSQLiteAffinity(declared, storage) {
|
|
|
240
240
|
return affinity === "REAL";
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
243
|
-
*
|
|
243
|
+
* Quotes a SQL identifier (a table or column name) so any characters are literal.
|
|
244
244
|
*
|
|
245
245
|
* @remarks
|
|
246
246
|
* Wraps the name in double quotes and doubles any embedded quote — the standard
|
|
@@ -259,7 +259,7 @@ function quoteIdentifier(identifier) {
|
|
|
259
259
|
return "\"" + identifier.replaceAll("\"", "\"\"") + "\"";
|
|
260
260
|
}
|
|
261
261
|
/**
|
|
262
|
-
*
|
|
262
|
+
* Encodes a JS value to its stored {@link SQLiteValue} for a declared column.
|
|
263
263
|
*
|
|
264
264
|
* @remarks
|
|
265
265
|
* The codec is total: a malformed value encodes to SQL `NULL`. Absence always
|
|
@@ -298,7 +298,7 @@ function encodeValue(value, column) {
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
/**
|
|
301
|
-
*
|
|
301
|
+
* Decodes a stored {@link SQLiteValue} back to its JS value for a declared column —
|
|
302
302
|
* the exact inverse of {@link encodeValue}.
|
|
303
303
|
*
|
|
304
304
|
* @remarks
|
|
@@ -339,7 +339,7 @@ function decodeValue(value, column) {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
/**
|
|
342
|
-
*
|
|
342
|
+
* Encodes a whole {@link Row} to a {@link SQLiteRow} by its table's schema.
|
|
343
343
|
*
|
|
344
344
|
* @remarks
|
|
345
345
|
* Encodes each declared column's value with {@link encodeValue}; columns the row
|
|
@@ -361,7 +361,7 @@ function encodeRow(row, schema) {
|
|
|
361
361
|
return result;
|
|
362
362
|
}
|
|
363
363
|
/**
|
|
364
|
-
*
|
|
364
|
+
* Extracts a stored row's values in a declared positional order.
|
|
365
365
|
*
|
|
366
366
|
* @remarks
|
|
367
367
|
* SQLite statements bind arrays positionally. Every requested column must be
|
|
@@ -392,7 +392,7 @@ function extractValues(row, names, table) {
|
|
|
392
392
|
return values;
|
|
393
393
|
}
|
|
394
394
|
/**
|
|
395
|
-
*
|
|
395
|
+
* Decodes a stored {@link SQLiteRow} back to a {@link Row} by its table's schema.
|
|
396
396
|
*
|
|
397
397
|
* @remarks
|
|
398
398
|
* Decodes each declared column with {@link decodeValue} and **omits** any column
|
|
@@ -421,7 +421,7 @@ function decodeRow(row, schema) {
|
|
|
421
421
|
return result;
|
|
422
422
|
}
|
|
423
423
|
/**
|
|
424
|
-
*
|
|
424
|
+
* Builds a collision-free SQL index name for a table + column-group index —
|
|
425
425
|
* shared by the compiler module's `schemaToIndexes` and `stepToSQL`,
|
|
426
426
|
* so a plan-built index name always matches one `open` would have created.
|
|
427
427
|
*
|
|
@@ -447,9 +447,39 @@ function deriveSQLiteIndexName(table, columns) {
|
|
|
447
447
|
return "idx_" + [table, ...columns].map((part) => String(part.length) + "_" + part).join("_");
|
|
448
448
|
}
|
|
449
449
|
//#endregion
|
|
450
|
+
//#region src/server/inferers.ts
|
|
451
|
+
/**
|
|
452
|
+
* Reads the storage type a nested (`json_extract`) operand encodes as from its
|
|
453
|
+
* RUNTIME value — NOT `json`.
|
|
454
|
+
*
|
|
455
|
+
* @remarks
|
|
456
|
+
* `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
|
|
457
|
+
* `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
|
|
458
|
+
* same scalar to compare. A boolean → `'boolean'` (→ `1` / `0`); a number →
|
|
459
|
+
* `'integer'` / `'real'`; a bigint → `'integer'`; a string → `'text'`; `null` /
|
|
460
|
+
* `undefined` → `'text'` (encodes to `null`); an object / array → `'json'` (the
|
|
461
|
+
* edge of comparing against a json subtree).
|
|
462
|
+
*
|
|
463
|
+
* @param value - The runtime operand value
|
|
464
|
+
* @returns The {@link ColumnStorage} to encode it as
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```ts
|
|
468
|
+
* inferValueStorage(true) // 'boolean'
|
|
469
|
+
* inferValueStorage(9) // 'integer'
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
function inferValueStorage(value) {
|
|
473
|
+
if (typeof value === "boolean") return "boolean";
|
|
474
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
475
|
+
if (typeof value === "bigint") return "integer";
|
|
476
|
+
if (typeof value === "object" && value !== null) return "json";
|
|
477
|
+
return "text";
|
|
478
|
+
}
|
|
479
|
+
//#endregion
|
|
450
480
|
//#region src/server/compilers.ts
|
|
451
481
|
/**
|
|
452
|
-
*
|
|
482
|
+
* Maps a portable {@link ColumnStorage} to its SQLite column type.
|
|
453
483
|
*
|
|
454
484
|
* @param storage - The portable column type
|
|
455
485
|
* @returns The SQLite column type keyword
|
|
@@ -465,7 +495,7 @@ function compileColumnSQL(storage) {
|
|
|
465
495
|
}
|
|
466
496
|
}
|
|
467
497
|
/**
|
|
468
|
-
*
|
|
498
|
+
* Compiles a {@link FieldPath} to the SQL expression that reads it.
|
|
469
499
|
*
|
|
470
500
|
* @param path - The field path
|
|
471
501
|
* @returns The SQL expression selecting the value
|
|
@@ -478,7 +508,7 @@ function compileFieldSQL(path) {
|
|
|
478
508
|
return "json_extract(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
479
509
|
}
|
|
480
510
|
/**
|
|
481
|
-
*
|
|
511
|
+
* Compiles an {@link AggregateOperation} over a {@link FieldPath}.
|
|
482
512
|
*
|
|
483
513
|
* @param operation - The aggregate to compute
|
|
484
514
|
* @param column - The column or nested path to aggregate
|
|
@@ -494,7 +524,7 @@ function compileAggregateSQL(operation, column) {
|
|
|
494
524
|
}
|
|
495
525
|
}
|
|
496
526
|
/**
|
|
497
|
-
*
|
|
527
|
+
* Compiles a NESTED {@link FieldPath} to the `json_type(<col>, <path>)` SQL
|
|
498
528
|
* expression — the {@link compileFieldSQL} `json_extract` sibling used to tell a
|
|
499
529
|
* PRESENT JSON `null` apart from an ABSENT path (both read back as SQL `NULL`
|
|
500
530
|
* through `json_extract`, but `json_type` reports `'null'` for the former and
|
|
@@ -515,65 +545,7 @@ function compileJSONTypeSQL(path) {
|
|
|
515
545
|
return "json_type(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
516
546
|
}
|
|
517
547
|
/**
|
|
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
|
|
548
|
+
* Compiles one condition to its `<column> <operator>` SQL fragment and the parameters
|
|
577
549
|
* it binds — engine-exact under SQL's three-valued NULL logic.
|
|
578
550
|
*
|
|
579
551
|
* @remarks
|
|
@@ -644,7 +616,7 @@ function inferValueStorage(value) {
|
|
|
644
616
|
function compileConditionSQL(condition, schema) {
|
|
645
617
|
const column = compileFieldSQL(condition.column);
|
|
646
618
|
const nested = !isString(condition.column);
|
|
647
|
-
const declared = isString(condition.column) ?
|
|
619
|
+
const declared = isString(condition.column) ? findColumn(condition.column, schema) : void 0;
|
|
648
620
|
const first = condition.values[0];
|
|
649
621
|
const second = condition.values[1];
|
|
650
622
|
const nullOperand = first === null || first === void 0;
|
|
@@ -763,7 +735,7 @@ function compileConditionSQL(condition, schema) {
|
|
|
763
735
|
};
|
|
764
736
|
}
|
|
765
737
|
/**
|
|
766
|
-
*
|
|
738
|
+
* Folds the conditions into one WHERE clause, parenthesizing progressively
|
|
767
739
|
* left-to-right so the grouping matches the engine's `matchesQuery` fold.
|
|
768
740
|
*
|
|
769
741
|
* @remarks
|
|
@@ -779,11 +751,11 @@ function compileConditionSQL(condition, schema) {
|
|
|
779
751
|
*
|
|
780
752
|
* @example
|
|
781
753
|
* ```ts
|
|
782
|
-
*
|
|
754
|
+
* compileWhereSQL([{ column: 'age', operator: 'from', values: [18], connector: 'and' }], schema)
|
|
783
755
|
* // { sql: 'WHERE "age" >= ?', parameters: [18] }
|
|
784
756
|
* ```
|
|
785
757
|
*/
|
|
786
|
-
function
|
|
758
|
+
function compileWhereSQL(conditions, schema) {
|
|
787
759
|
const [first, ...remaining] = conditions;
|
|
788
760
|
if (first === void 0) return {
|
|
789
761
|
sql: "",
|
|
@@ -804,14 +776,14 @@ function compileWhere(conditions, schema) {
|
|
|
804
776
|
};
|
|
805
777
|
}
|
|
806
778
|
/**
|
|
807
|
-
*
|
|
779
|
+
* Compiles the ORDER BY clause from the order terms, always ending with the
|
|
808
780
|
* primary key as the final determinant.
|
|
809
781
|
*
|
|
810
782
|
* @remarks
|
|
811
783
|
* The native `records` read then resolves ties in key order, matching a
|
|
812
784
|
* primary-key-ordered `scan` and the core engine's stable `sortRows` over a
|
|
813
785
|
* key-ordered scan (and IndexedDB's key-ordered reads), so a native read equals
|
|
814
|
-
* the scan path
|
|
786
|
+
* the scan path — native ↔ engine parity. SQLite without an
|
|
815
787
|
* `ORDER BY` returns rowid (insertion) order, and an explicit order alone breaks
|
|
816
788
|
* ties by rowid too — both diverge from every key-ordered backend. The
|
|
817
789
|
* tie-breaker is ASCENDING regardless of the explicit directions: the engine's
|
|
@@ -825,17 +797,17 @@ function compileWhere(conditions, schema) {
|
|
|
825
797
|
*
|
|
826
798
|
* @example
|
|
827
799
|
* ```ts
|
|
828
|
-
*
|
|
800
|
+
* compileOrderSQL([{ column: 'age', direction: 'descending' }], schema)
|
|
829
801
|
* // 'ORDER BY "age" DESC, "id"'
|
|
830
802
|
* ```
|
|
831
803
|
*/
|
|
832
|
-
function
|
|
804
|
+
function compileOrderSQL(order, schema) {
|
|
833
805
|
const terms = (order ?? []).map((term) => compileFieldSQL(term.column) + (term.direction === "descending" ? " DESC" : " ASC"));
|
|
834
806
|
if (!(order ?? []).some((term) => isString(term.column) && term.column === schema.primary)) terms.push(quoteIdentifier(schema.primary));
|
|
835
807
|
return terms.length === 0 ? "" : "ORDER BY " + terms.join(", ");
|
|
836
808
|
}
|
|
837
809
|
/**
|
|
838
|
-
*
|
|
810
|
+
* Compiles the LIMIT / OFFSET clause.
|
|
839
811
|
*
|
|
840
812
|
* @remarks
|
|
841
813
|
* An offset without a limit uses `LIMIT -1` (SQLite's "no limit") so OFFSET is
|
|
@@ -847,10 +819,10 @@ function compileOrder(order, schema) {
|
|
|
847
819
|
*
|
|
848
820
|
* @example
|
|
849
821
|
* ```ts
|
|
850
|
-
*
|
|
822
|
+
* compilePageSQL(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?', parameters: [5] }
|
|
851
823
|
* ```
|
|
852
824
|
*/
|
|
853
|
-
function
|
|
825
|
+
function compilePageSQL(limit, offset) {
|
|
854
826
|
validatePage({
|
|
855
827
|
...limit === void 0 ? {} : { limit },
|
|
856
828
|
...offset === void 0 ? {} : { offset }
|
|
@@ -873,7 +845,7 @@ function compilePage(limit, offset) {
|
|
|
873
845
|
};
|
|
874
846
|
}
|
|
875
847
|
/**
|
|
876
|
-
*
|
|
848
|
+
* Compiles a {@link QueryInput} into the SQL clause that follows a table name, with
|
|
877
849
|
* its bound parameters in clause order.
|
|
878
850
|
*
|
|
879
851
|
* @remarks
|
|
@@ -883,11 +855,13 @@ function compilePage(limit, offset) {
|
|
|
883
855
|
* over a JS `scan`. The WHERE fold is parenthesized **left-to-right** to mirror
|
|
884
856
|
* the core engine's `matchesQuery` (not SQL's native AND-over-OR precedence),
|
|
885
857
|
* so a native and an engine read return identical rows. Each operand is encoded
|
|
886
|
-
*
|
|
858
|
+
* through `encodeValue`: a flat column uses its declared schema type, while a nested
|
|
887
859
|
* `FieldPath` (a `json_extract` read) encodes each operand as the native scalar
|
|
888
860
|
* the extract returns — derived from the operand's runtime type — so it compares.
|
|
889
|
-
*
|
|
890
|
-
* `starts` / `ends`
|
|
861
|
+
* Every operator maps per the databases guide's operator table, with
|
|
862
|
+
* `starts` / `ends` compiling to a CODE-POINT `substr` slice guarded by
|
|
863
|
+
* `typeof(<column>) = 'text'` (case-sensitive, matching the engine's
|
|
864
|
+
* `String.prototype.startsWith` / `endsWith`) and an empty `any` / `none` list
|
|
891
865
|
* collapsing to a constant. An `undefined` input (or one with no parts)
|
|
892
866
|
* compiles to an empty clause.
|
|
893
867
|
*
|
|
@@ -903,9 +877,9 @@ function compilePage(limit, offset) {
|
|
|
903
877
|
*/
|
|
904
878
|
function compileQuerySQL(input, schema) {
|
|
905
879
|
validatePage(input);
|
|
906
|
-
const where =
|
|
907
|
-
const orderBy =
|
|
908
|
-
const page =
|
|
880
|
+
const where = compileWhereSQL(input?.conditions ?? [], schema);
|
|
881
|
+
const orderBy = compileOrderSQL(input?.order, schema);
|
|
882
|
+
const page = compilePageSQL(input?.limit, input?.offset);
|
|
909
883
|
return {
|
|
910
884
|
sql: [
|
|
911
885
|
where.sql,
|
|
@@ -916,7 +890,7 @@ function compileQuerySQL(input, schema) {
|
|
|
916
890
|
};
|
|
917
891
|
}
|
|
918
892
|
/**
|
|
919
|
-
*
|
|
893
|
+
* Projects a {@link TableSchema} to its `CREATE TABLE IF NOT EXISTS` statement.
|
|
920
894
|
*
|
|
921
895
|
* @param schema - The table schema
|
|
922
896
|
* @returns The complete table declaration
|
|
@@ -926,7 +900,7 @@ function schemaToTable(schema) {
|
|
|
926
900
|
return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(schema.name) + " (" + columns.join(", ") + ", PRIMARY KEY (" + quoteIdentifier(schema.primary) + "))";
|
|
927
901
|
}
|
|
928
902
|
/**
|
|
929
|
-
*
|
|
903
|
+
* Projects a {@link TableSchema} to its declared SQLite indexes.
|
|
930
904
|
*
|
|
931
905
|
* @param schema - The table schema
|
|
932
906
|
* @returns One statement per declared index
|
|
@@ -935,7 +909,7 @@ function schemaToIndexes(schema) {
|
|
|
935
909
|
return schema.indexes.map((group) => "CREATE INDEX IF NOT EXISTS " + quoteIdentifier(deriveSQLiteIndexName(schema.name, group)) + " ON " + quoteIdentifier(schema.name) + " (" + group.map(quoteIdentifier).join(", ") + ")");
|
|
936
910
|
}
|
|
937
911
|
/**
|
|
938
|
-
*
|
|
912
|
+
* Projects one {@link MigrationStep} to SQLite DDL.
|
|
939
913
|
*
|
|
940
914
|
* @param step - The migration step
|
|
941
915
|
* @returns The statements that apply the step
|
|
@@ -951,97 +925,9 @@ function stepToSQL(step) {
|
|
|
951
925
|
}
|
|
952
926
|
}
|
|
953
927
|
//#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
928
|
//#region src/server/drivers/JSONDriver.ts
|
|
1043
929
|
/**
|
|
1044
|
-
*
|
|
930
|
+
* Implements a persistent {@link DriverInterface} backed by a single JSON file — the
|
|
1045
931
|
* reference {@link MemoryDriver} plus file load / flush.
|
|
1046
932
|
*
|
|
1047
933
|
* @remarks
|
|
@@ -1055,15 +941,15 @@ var DriverIterator = class {
|
|
|
1055
941
|
* primary (the table contract), so the key is recovered on load with
|
|
1056
942
|
* {@link extractKey} and the file need not store it. The parsed JSON crosses the
|
|
1057
943
|
* boundary as `unknown` and is narrowed with {@link isRecord} / {@link extractKey},
|
|
1058
|
-
* never asserted
|
|
944
|
+
* never asserted. A read that reports no document there starts empty —
|
|
1059
945
|
* `ENOENT` for a plain absence, and `ENOTDIR` for a path whose parent is not a
|
|
1060
946
|
* directory, which no later write could find either; every other read failure or
|
|
1061
947
|
* invalid existing document fails closed without publication, mutation, or
|
|
1062
|
-
* automatic repair. It
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
* driver.
|
|
948
|
+
* automatic repair. It implements the optional native `stream` hook that
|
|
949
|
+
* `TableInterface.scan` prefers over `scan`, and neither `records` nor
|
|
950
|
+
* `aggregate`, so the core engine's `matchesQuery` answers every query on
|
|
951
|
+
* either path. For development, small datasets, and portable / inspectable
|
|
952
|
+
* data; for large or concurrent workloads reach for a SQLite-backed driver.
|
|
1067
953
|
*
|
|
1068
954
|
* Metadata crosses {@link cloneDriverMetadata} at parsed-file ingress, public and
|
|
1069
955
|
* scoped write ingress, candidate/root publication, serialization, and copy-out.
|
|
@@ -1127,7 +1013,7 @@ var JSONDriver = class {
|
|
|
1127
1013
|
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1128
1014
|
}
|
|
1129
1015
|
/**
|
|
1130
|
-
*
|
|
1016
|
+
* Iterates rows lazily with native filtering — delegates to the inner {@link MemoryDriver}.
|
|
1131
1017
|
*
|
|
1132
1018
|
* @remarks
|
|
1133
1019
|
* Semantics are the memory driver's own: `input.conditions` filters, `offset`
|
|
@@ -1146,7 +1032,7 @@ var JSONDriver = class {
|
|
|
1146
1032
|
await this.#enqueue(() => this.#clear(table));
|
|
1147
1033
|
}
|
|
1148
1034
|
/**
|
|
1149
|
-
*
|
|
1035
|
+
* Runs an isolated native transaction callback over a candidate memory store.
|
|
1150
1036
|
*
|
|
1151
1037
|
* @remarks
|
|
1152
1038
|
* Single-writer: nesting and root operations while active throw `CONFLICT`.
|
|
@@ -1163,7 +1049,7 @@ var JSONDriver = class {
|
|
|
1163
1049
|
return this.#enqueue(() => this.#transact(scope));
|
|
1164
1050
|
}
|
|
1165
1051
|
/**
|
|
1166
|
-
*
|
|
1052
|
+
* Captures an owned row snapshot at an exact writer-queue position.
|
|
1167
1053
|
*
|
|
1168
1054
|
* @remarks
|
|
1169
1055
|
* Capture owns table names, schemas, rows, and one session-local identity per
|
|
@@ -1191,7 +1077,7 @@ var JSONDriver = class {
|
|
|
1191
1077
|
return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
|
|
1192
1078
|
}
|
|
1193
1079
|
/**
|
|
1194
|
-
*
|
|
1080
|
+
* Persists an owned metadata snapshot for a later `metadata()` to copy out.
|
|
1195
1081
|
*
|
|
1196
1082
|
* @remarks
|
|
1197
1083
|
* Root stamping conflicts while a transaction is active. The scoped
|
|
@@ -1206,7 +1092,7 @@ var JSONDriver = class {
|
|
|
1206
1092
|
await this.#enqueue(() => this.#stamp(owned));
|
|
1207
1093
|
}
|
|
1208
1094
|
/**
|
|
1209
|
-
*
|
|
1095
|
+
* Applies one atomic {@link MigrationInput} through an isolated candidate.
|
|
1210
1096
|
*
|
|
1211
1097
|
* @remarks
|
|
1212
1098
|
* The candidate receives every plan step plus optional metadata. Its complete
|
|
@@ -1420,7 +1306,8 @@ var JSONDriver = class {
|
|
|
1420
1306
|
return this.#requireCandidate(token).keys(table);
|
|
1421
1307
|
}
|
|
1422
1308
|
#scanCandidate(token, table) {
|
|
1423
|
-
|
|
1309
|
+
const source = this.#requireCandidate(token).scan(table);
|
|
1310
|
+
return new DriverIterator(source[Symbol.asyncIterator](), () => {
|
|
1424
1311
|
this.#requireCandidate(token);
|
|
1425
1312
|
});
|
|
1426
1313
|
}
|
|
@@ -1667,8 +1554,8 @@ var JSONDriver = class {
|
|
|
1667
1554
|
//#endregion
|
|
1668
1555
|
//#region src/server/drivers/SQLiteDriver.ts
|
|
1669
1556
|
/**
|
|
1670
|
-
*
|
|
1671
|
-
* built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
1557
|
+
* Implements the {@link DriverInterface} over SQLite — the server-native, trusted-mode
|
|
1558
|
+
* backend built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
1672
1559
|
*
|
|
1673
1560
|
* @remarks
|
|
1674
1561
|
* A thin adapter: it implements the storage primitives the core database layer
|
|
@@ -1679,41 +1566,40 @@ var JSONDriver = class {
|
|
|
1679
1566
|
* reopen-safe), and readies a reserved `_metadata` single-row table `metadata()` /
|
|
1680
1567
|
* `stamp()` read and write — **a user table named `_metadata` collides with it**;
|
|
1681
1568
|
* 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
|
|
1569
|
+
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so
|
|
1570
|
+
* the typed layer above imposes the exact shape. `write` is an
|
|
1684
1571
|
* `INSERT OR REPLACE` upsert, while `insert` uses a plain `INSERT` and maps its
|
|
1685
1572
|
* atomic primary-key constraint failure to `CONFLICT`; every other backend
|
|
1686
1573
|
* `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
|
|
1574
|
+
* below. Querying, ordering, paging, and aggregation is native: `records` /
|
|
1575
|
+
* `stream` compile a `QueryInput` to SQL with `compileQuerySQL`, and
|
|
1576
|
+
* `aggregate` runs a SQL `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (through
|
|
1577
|
+
* `compileAggregateSQL`) over the same compiled WHERE. `transaction` runs a
|
|
1578
|
+
* callback inside native `BEGIN` / `COMMIT` / `ROLLBACK`, passing a scoped
|
|
1579
|
+
* storage capability that becomes invalid after settlement. `migrate` runs the
|
|
1580
|
+
* plan's projected DDL ({@link import('../compilers.js').stepToSQL}) inside
|
|
1581
|
+
* whichever native transaction is active: joined into the active transaction
|
|
1582
|
+
* callback when one exists (the core's versioned reconcile path wraps migrate +
|
|
1583
|
+
* stamp in one native `BEGIN`, and node:sqlite rejects a nested `BEGIN`), or
|
|
1584
|
+
* inside its own `database.transact` otherwise — a mid-plan failure rolls
|
|
1585
|
+
* back atomically either way, an improvement over the non-atomic `MemoryDriver`
|
|
1586
|
+
* / `JSONDriver` migrate; a step referencing an undeclared table throws
|
|
1701
1587
|
* `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
|
-
*
|
|
1588
|
+
* capture-replay (SELECT the named tables' rows, replay through DELETE + INSERT OR
|
|
1589
|
+
* REPLACE inside a native transaction on rollback) rather than a SQL
|
|
1590
|
+
* `SAVEPOINT`, since the core `transaction` calls the rollback thunk only on
|
|
1591
|
+
* failure with no commit-on-success signal — a long-lived `SAVEPOINT` would
|
|
1592
|
+
* leave the connection uncommitted (lost on close). Every backend interaction
|
|
1593
|
+
* runs through `#guard`, which maps a thrown backend `SQLiteError` (or any
|
|
1594
|
+
* unexpected non-`SQLiteError` throw) to a typed {@link DatabaseError} — never
|
|
1595
|
+
* a raw backend error escapes `DriverInterface`: `CONSTRAINT` → `CONFLICT`, the
|
|
1596
|
+
* wrapper's own `CLOSED` → `CLOSED`, `BUSY` (a locked database that outlasted
|
|
1597
|
+
* the configured `timeout`) → a retryable `DRIVER` (`context.retryable` is
|
|
1598
|
+
* `true`), and `UNKNOWN` / any other throw → `DRIVER`. The original error is
|
|
1599
|
+
* preserved as `context.cause`. A `DatabaseError` this driver throws directly
|
|
1600
|
+
* (`CLOSED` from the `#require` gate, `NOT_FOUND` from `#table`, `MIGRATION`
|
|
1601
|
+
* from a migration-plan fault) passes through `#guard` unchanged, never
|
|
1602
|
+
* re-wrapped.
|
|
1717
1603
|
*/
|
|
1718
1604
|
var SQLiteDriver = class {
|
|
1719
1605
|
#path;
|
|
@@ -1750,7 +1636,7 @@ var SQLiteDriver = class {
|
|
|
1750
1636
|
for (const [name, value] of Object.entries(this.#options.pragmas ?? {})) database.pragma(name, value);
|
|
1751
1637
|
const map = /* @__PURE__ */ new Map();
|
|
1752
1638
|
const identities = /* @__PURE__ */ new Map();
|
|
1753
|
-
database.
|
|
1639
|
+
database.transact(() => {
|
|
1754
1640
|
this.#ensureMetadataTable(database);
|
|
1755
1641
|
const stored = this.#readMetadata(database);
|
|
1756
1642
|
const deployed = normalizeDriverSchema(stored?.schema ?? owned);
|
|
@@ -1768,11 +1654,11 @@ var SQLiteDriver = class {
|
|
|
1768
1654
|
for (const table of deployed) {
|
|
1769
1655
|
const absent = missing.get(table.name);
|
|
1770
1656
|
if (absent === void 0) {
|
|
1771
|
-
database.
|
|
1772
|
-
for (const sql of schemaToIndexes(table)) database.
|
|
1657
|
+
database.execute(schemaToTable(table));
|
|
1658
|
+
for (const sql of schemaToIndexes(table)) database.execute(sql);
|
|
1773
1659
|
} else for (const [index, group] of table.indexes.entries()) if (absent.includes(deriveSQLiteIndexName(table.name, group))) {
|
|
1774
1660
|
const sql = schemaToIndexes(table)[index];
|
|
1775
|
-
if (sql !== void 0) database.
|
|
1661
|
+
if (sql !== void 0) database.execute(sql);
|
|
1776
1662
|
}
|
|
1777
1663
|
identities.set(table.name, previousIdentities.get(table.name) ?? {});
|
|
1778
1664
|
}
|
|
@@ -1844,7 +1730,7 @@ var SQLiteDriver = class {
|
|
|
1844
1730
|
const conditionsExact = conditions.every((condition) => matchesConditionExactly(condition, schema));
|
|
1845
1731
|
const columnExact = matchesAggregateExactly(operation, column, schema);
|
|
1846
1732
|
if (conditionsExact && columnExact) return this.#guard(() => {
|
|
1847
|
-
const { sql, parameters } =
|
|
1733
|
+
const { sql, parameters } = compileWhereSQL(conditions, schema);
|
|
1848
1734
|
const value = this.#require().prepare("SELECT " + compileAggregateSQL(operation, column) + " AS value FROM " + quoteIdentifier(table) + (sql === "" ? "" : " " + sql)).get(parameters)?.value;
|
|
1849
1735
|
return value === null || value === void 0 ? void 0 : Number(value);
|
|
1850
1736
|
});
|
|
@@ -1857,7 +1743,7 @@ var SQLiteDriver = class {
|
|
|
1857
1743
|
return new DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1858
1744
|
}
|
|
1859
1745
|
/**
|
|
1860
|
-
*
|
|
1746
|
+
* Begins a native transaction — real `BEGIN`, `COMMIT`, `ROLLBACK`.
|
|
1861
1747
|
*
|
|
1862
1748
|
* @remarks
|
|
1863
1749
|
* The callback receives a scoped {@link StorageInterface}. Fulfillment
|
|
@@ -1904,23 +1790,22 @@ var SQLiteDriver = class {
|
|
|
1904
1790
|
}
|
|
1905
1791
|
}
|
|
1906
1792
|
/**
|
|
1907
|
-
*
|
|
1793
|
+
* Applies a {@link Migration} plan by executing each step's projected DDL
|
|
1908
1794
|
* ({@link import('../compilers.js').stepToSQL}).
|
|
1909
1795
|
*
|
|
1910
1796
|
* @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
|
|
1797
|
+
* Atomicity is provided by whichever native transaction is active: when this
|
|
1798
|
+
* driver's own `transaction()` callback is active (the core's versioned
|
|
1799
|
+
* reconcile / migrate path joins migrate + stamp under one native `BEGIN`),
|
|
1800
|
+
* the plan's DDL runs directly inside that enclosing transaction — a mid-plan
|
|
1801
|
+
* failure rejects the callback and the driver rolls it back. node:sqlite (and
|
|
1802
|
+
* SQLite generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
1918
1803
|
* second native transaction while one is already open. Otherwise (no
|
|
1919
1804
|
* enclosing transaction), `migrate` wraps the plan in its own native
|
|
1920
1805
|
* `database.transaction` — atomic on its own: a mid-plan failure rolls
|
|
1921
1806
|
* back every DDL statement already applied by the plan. A scoped migration
|
|
1922
1807
|
* uses one fixed internal savepoint literal because the published SQLite
|
|
1923
|
-
* wrapper intentionally exposes raw `
|
|
1808
|
+
* wrapper intentionally exposes raw `execute` but no savepoint manager. That
|
|
1924
1809
|
* savepoint contains a caught inner migration so the outer callback
|
|
1925
1810
|
* transaction remains active and may continue safely. A step referencing a
|
|
1926
1811
|
* table not in this driver's declared schema (and that is not itself a
|
|
@@ -1940,7 +1825,7 @@ var SQLiteDriver = class {
|
|
|
1940
1825
|
metadata: owned.metadata.schema
|
|
1941
1826
|
});
|
|
1942
1827
|
this.#guard(() => {
|
|
1943
|
-
database.
|
|
1828
|
+
database.transact(() => {
|
|
1944
1829
|
this.#applyPlan(database, owned);
|
|
1945
1830
|
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
1946
1831
|
});
|
|
@@ -1949,7 +1834,7 @@ var SQLiteDriver = class {
|
|
|
1949
1834
|
this.#identities = identities;
|
|
1950
1835
|
}
|
|
1951
1836
|
/**
|
|
1952
|
-
*
|
|
1837
|
+
* Reads the persisted {@link DriverMetadata} from the reserved `_metadata` table.
|
|
1953
1838
|
*
|
|
1954
1839
|
* @returns The last-stamped `DriverMetadata`, or `undefined` when never stamped
|
|
1955
1840
|
* (or the stored row is malformed)
|
|
@@ -1959,7 +1844,7 @@ var SQLiteDriver = class {
|
|
|
1959
1844
|
return this.#metadata();
|
|
1960
1845
|
}
|
|
1961
1846
|
/**
|
|
1962
|
-
*
|
|
1847
|
+
* Persists an owned metadata snapshot into the reserved `_metadata` table's
|
|
1963
1848
|
* single row.
|
|
1964
1849
|
*
|
|
1965
1850
|
* @param metadata - The {@link DriverMetadata} to persist
|
|
@@ -2014,11 +1899,11 @@ var SQLiteDriver = class {
|
|
|
2014
1899
|
}
|
|
2015
1900
|
this.#guard(() => {
|
|
2016
1901
|
const current = this.#require();
|
|
2017
|
-
current.
|
|
1902
|
+
current.transact(() => {
|
|
2018
1903
|
for (const [name, replacement] of replacements) {
|
|
2019
|
-
current.
|
|
1904
|
+
current.execute("DELETE FROM " + quoteIdentifier(name));
|
|
2020
1905
|
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.
|
|
1906
|
+
for (const values of replacement.values) statement.execute(values);
|
|
2022
1907
|
}
|
|
2023
1908
|
});
|
|
2024
1909
|
});
|
|
@@ -2039,7 +1924,7 @@ var SQLiteDriver = class {
|
|
|
2039
1924
|
const values = extractValues(encoded, names, table);
|
|
2040
1925
|
const statement = this.#require().prepare("INSERT OR REPLACE INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2041
1926
|
checkAbort(options?.signal);
|
|
2042
|
-
statement.
|
|
1927
|
+
statement.execute(values);
|
|
2043
1928
|
});
|
|
2044
1929
|
}
|
|
2045
1930
|
async #insert(table, key, row, options) {
|
|
@@ -2050,7 +1935,7 @@ var SQLiteDriver = class {
|
|
|
2050
1935
|
const values = extractValues(encoded, names, table);
|
|
2051
1936
|
const statement = this.#require().prepare("INSERT INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2052
1937
|
checkAbort(options?.signal);
|
|
2053
|
-
statement.
|
|
1938
|
+
statement.execute(values);
|
|
2054
1939
|
});
|
|
2055
1940
|
}
|
|
2056
1941
|
async #delete(table, key, options) {
|
|
@@ -2058,7 +1943,7 @@ var SQLiteDriver = class {
|
|
|
2058
1943
|
return this.#guard(() => {
|
|
2059
1944
|
const statement = this.#require().prepare("DELETE FROM " + quoteIdentifier(table) + " WHERE " + quoteIdentifier(schema.primary) + " = ?");
|
|
2060
1945
|
checkAbort(options?.signal);
|
|
2061
|
-
return statement.
|
|
1946
|
+
return statement.execute([this.#key(key, schema)]).changes > 0;
|
|
2062
1947
|
});
|
|
2063
1948
|
}
|
|
2064
1949
|
async #keys(table) {
|
|
@@ -2120,14 +2005,14 @@ var SQLiteDriver = class {
|
|
|
2120
2005
|
async #clear(table) {
|
|
2121
2006
|
this.#table(table);
|
|
2122
2007
|
this.#guard(() => {
|
|
2123
|
-
this.#require().prepare("DELETE FROM " + quoteIdentifier(table)).
|
|
2008
|
+
this.#require().prepare("DELETE FROM " + quoteIdentifier(table)).execute();
|
|
2124
2009
|
});
|
|
2125
2010
|
}
|
|
2126
2011
|
async #metadata() {
|
|
2127
2012
|
return this.#guard(() => this.#readMetadata(this.#require()));
|
|
2128
2013
|
}
|
|
2129
2014
|
#ensureMetadataTable(database) {
|
|
2130
|
-
database.
|
|
2015
|
+
database.execute("CREATE TABLE IF NOT EXISTS " + quoteIdentifier(METADATA_TABLE) + " (\"id\" INTEGER, \"version\" INTEGER, \"schema\" TEXT, PRIMARY KEY (\"id\"))");
|
|
2131
2016
|
}
|
|
2132
2017
|
#validateTable(database, schema) {
|
|
2133
2018
|
const object = database.prepare("SELECT \"type\" AS \"category\" FROM \"sqlite_schema\" WHERE \"name\" = ?").get([schema.name]);
|
|
@@ -2262,7 +2147,7 @@ var SQLiteDriver = class {
|
|
|
2262
2147
|
this.#guard(() => this.#writeMetadata(database, owned));
|
|
2263
2148
|
}
|
|
2264
2149
|
#writeMetadata(database, metadata) {
|
|
2265
|
-
database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").
|
|
2150
|
+
database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").execute([metadata.version, JSON.stringify(metadata.schema)]);
|
|
2266
2151
|
}
|
|
2267
2152
|
#capability(token) {
|
|
2268
2153
|
return {
|
|
@@ -2321,16 +2206,16 @@ var SQLiteDriver = class {
|
|
|
2321
2206
|
metadata: owned.metadata.schema
|
|
2322
2207
|
});
|
|
2323
2208
|
this.#guard(() => {
|
|
2324
|
-
database.
|
|
2209
|
+
database.execute("SAVEPOINT \"_orkestrel_migration\"");
|
|
2325
2210
|
try {
|
|
2326
2211
|
this.#applyPlan(database, owned);
|
|
2327
2212
|
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
2328
|
-
database.
|
|
2213
|
+
database.execute("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
2329
2214
|
} catch (error) {
|
|
2330
2215
|
try {
|
|
2331
|
-
database.
|
|
2216
|
+
database.execute("ROLLBACK TO SAVEPOINT \"_orkestrel_migration\"");
|
|
2332
2217
|
} finally {
|
|
2333
|
-
database.
|
|
2218
|
+
database.execute("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
2334
2219
|
}
|
|
2335
2220
|
throw error;
|
|
2336
2221
|
}
|
|
@@ -2406,13 +2291,13 @@ var SQLiteDriver = class {
|
|
|
2406
2291
|
});
|
|
2407
2292
|
}
|
|
2408
2293
|
#applyPlan(database, input) {
|
|
2409
|
-
for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.
|
|
2294
|
+
for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.execute(sql);
|
|
2410
2295
|
}
|
|
2411
2296
|
};
|
|
2412
2297
|
//#endregion
|
|
2413
2298
|
//#region src/server/factories.ts
|
|
2414
2299
|
/**
|
|
2415
|
-
*
|
|
2300
|
+
* Creates a persistent JSON-file {@link DriverInterface} for the core database layer.
|
|
2416
2301
|
*
|
|
2417
2302
|
* @remarks
|
|
2418
2303
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
@@ -2420,9 +2305,10 @@ var SQLiteDriver = class {
|
|
|
2420
2305
|
* `Table` / `Query` API is unchanged; only where the bytes live changes.
|
|
2421
2306
|
* The driver is the reference `MemoryDriver` plus JSON-file persistence: `open` loads
|
|
2422
2307
|
* the file, every mutation flushes the whole store back, and querying runs through
|
|
2423
|
-
* the core engine
|
|
2424
|
-
* `
|
|
2425
|
-
*
|
|
2308
|
+
* the core engine's `matchesQuery`. The driver implements the native `stream`
|
|
2309
|
+
* hook and neither `records` nor `aggregate`, so the engine answers every query
|
|
2310
|
+
* on either path. A missing, corrupt, or wrong-shaped file starts empty rather
|
|
2311
|
+
* than throwing.
|
|
2426
2312
|
*
|
|
2427
2313
|
* @param path - The JSON file path data is loaded from and flushed to
|
|
2428
2314
|
* @returns A {@link DriverInterface} backed by a JSON file
|
|
@@ -2444,7 +2330,7 @@ function createJSONDriver(path) {
|
|
|
2444
2330
|
return new JSONDriver(path);
|
|
2445
2331
|
}
|
|
2446
2332
|
/**
|
|
2447
|
-
*
|
|
2333
|
+
* Creates a trusted-mode SQLite {@link DriverInterface} for the core database layer.
|
|
2448
2334
|
*
|
|
2449
2335
|
* @remarks
|
|
2450
2336
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
@@ -2483,6 +2369,6 @@ function createSQLiteDriver(options) {
|
|
|
2483
2369
|
return new SQLiteDriver(options);
|
|
2484
2370
|
}
|
|
2485
2371
|
//#endregion
|
|
2486
|
-
export { EXACT_COLUMN_STORAGE, EXACT_RANGE_COLUMN_STORAGE, JSONDriver, METADATA_TABLE, SQLiteDriver, compileAggregateSQL, compileColumnSQL, compileConditionSQL, compileFieldSQL, compileJSONTypeSQL,
|
|
2372
|
+
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
2373
|
|
|
2488
2374
|
//# sourceMappingURL=index.js.map
|