@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
|
@@ -6,7 +6,7 @@ let node_path = require("node:path");
|
|
|
6
6
|
let _orkestrel_sqlite = require("@orkestrel/sqlite");
|
|
7
7
|
//#region src/server/constants.ts
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Lists the declared {@link ColumnStorage}s whose SQL EQUALITY comparisons (`equals` /
|
|
10
10
|
* `not` / `any` / `none`) and `starts` / `ends` compiles are provably
|
|
11
11
|
* engine-exact under declared-type trust — `text` / `integer` / `real` /
|
|
12
12
|
* `boolean`; a `json` or `blob` column always refines instead.
|
|
@@ -19,7 +19,7 @@ let _orkestrel_sqlite = require("@orkestrel/sqlite");
|
|
|
19
19
|
* TEXT byte-for-byte as UTF-8 — equivalent to Unicode CODE-POINT order —
|
|
20
20
|
* while the core engine's `compareValues` orders JS strings with `<`, which
|
|
21
21
|
* compares UTF-16 CODE-UNIT order. The two orders diverge for supplementary-
|
|
22
|
-
* plane characters (code points ≥ U+10000,
|
|
22
|
+
* plane characters (code points ≥ U+10000, for example many emoji): a lead surrogate
|
|
23
23
|
* (`\uD800`–`\uDBFF`) sorts BELOW ``–`` in code-unit order, while
|
|
24
24
|
* its code point sorts ABOVE them. So `matchesConditionExactly`'s range family and
|
|
25
25
|
* `matchesOrderExactly` exclude `text`, refining through the core engine instead.
|
|
@@ -31,7 +31,7 @@ var EXACT_COLUMN_STORAGE = Object.freeze([
|
|
|
31
31
|
"boolean"
|
|
32
32
|
]);
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Lists the declared {@link ColumnStorage}s whose SQL RANGE comparisons
|
|
35
35
|
* (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are
|
|
36
36
|
* provably engine-exact — `integer` / `real` / `boolean` only. `text` is
|
|
37
37
|
* excluded: see {@link EXACT_COLUMN_STORAGE}'s remarks for the BINARY-collation
|
|
@@ -44,7 +44,7 @@ var EXACT_RANGE_COLUMN_STORAGE = Object.freeze([
|
|
|
44
44
|
"boolean"
|
|
45
45
|
]);
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* Names the reserved metadata table the {@link SQLiteDriver} creates on `open` to
|
|
48
48
|
* persist its stamped `DriverMetadata` (`version` + declared schema JSON) — the
|
|
49
49
|
* SQLite realization of the `metadata` / `stamp` driver hooks.
|
|
50
50
|
*
|
|
@@ -56,7 +56,7 @@ var METADATA_TABLE = "_metadata";
|
|
|
56
56
|
//#endregion
|
|
57
57
|
//#region src/server/helpers.ts
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* Reports whether a caught filesystem error says that nothing is there to read.
|
|
60
60
|
*
|
|
61
61
|
* @remarks
|
|
62
62
|
* Two codes carry that meaning: `ENOENT` is a plain absence, and `ENOTDIR` is a
|
|
@@ -73,7 +73,7 @@ var METADATA_TABLE = "_metadata";
|
|
|
73
73
|
* answers `false` rather than being read for a `code` any object could carry.
|
|
74
74
|
*
|
|
75
75
|
* @param error - The caught value to classify; any runtime is accepted
|
|
76
|
-
* @returns
|
|
76
|
+
* @returns True if the error reports that the path holds nothing; false otherwise
|
|
77
77
|
*
|
|
78
78
|
* @example
|
|
79
79
|
* ```ts
|
|
@@ -87,21 +87,21 @@ function matchesAbsentPath(error) {
|
|
|
87
87
|
return error.code === "ENOENT" || error.code === "ENOTDIR";
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
* the operand side of the declared-type-trust proof.
|
|
90
|
+
* Reports whether a value's runtime type matches a column's declared exact type
|
|
91
|
+
* — the operand side of the declared-type-trust proof.
|
|
92
92
|
*
|
|
93
93
|
* @remarks
|
|
94
94
|
* `text` ↔ string, `integer` / `real` ↔ FINITE number (`NaN` / `±Infinity`
|
|
95
95
|
* fail), `boolean` ↔ boolean. Backs {@link matchesConditionExactly}'s operand checks.
|
|
96
96
|
*
|
|
97
97
|
* @param value - The condition operand to test
|
|
98
|
-
* @param
|
|
99
|
-
* @returns
|
|
98
|
+
* @param storage - The column's declared portable storage type
|
|
99
|
+
* @returns True if the operand's runtime type matches the declared type; false otherwise
|
|
100
100
|
*
|
|
101
101
|
* @example
|
|
102
102
|
* ```ts
|
|
103
|
-
*
|
|
104
|
-
*
|
|
103
|
+
* matchesDeclaredStorage('Ada', 'text') // true
|
|
104
|
+
* matchesDeclaredStorage(Number.NaN, 'integer') // false — only finite numbers
|
|
105
105
|
* ```
|
|
106
106
|
*/
|
|
107
107
|
function matchesDeclaredStorage(value, storage) {
|
|
@@ -110,9 +110,9 @@ function matchesDeclaredStorage(value, storage) {
|
|
|
110
110
|
return (0, _orkestrel_contract.isFiniteNumber)(value);
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
114
|
-
* the core engine's `matchesCondition` for every value its
|
|
115
|
-
* type can store.
|
|
113
|
+
* Reports whether one {@link Condition} compiles to SQL that is PROVABLY
|
|
114
|
+
* identical to the core engine's `matchesCondition` for every value its
|
|
115
|
+
* column's declared type can store.
|
|
116
116
|
*
|
|
117
117
|
* @remarks
|
|
118
118
|
* `false` for a nested `FieldPath` (an array) or a column absent from `schema`.
|
|
@@ -141,15 +141,15 @@ function matchesDeclaredStorage(value, storage) {
|
|
|
141
141
|
*
|
|
142
142
|
* @param condition - The condition to test
|
|
143
143
|
* @param schema - The table's schema
|
|
144
|
-
* @returns
|
|
144
|
+
* @returns True if `condition` is exact; false otherwise
|
|
145
145
|
*/
|
|
146
146
|
function matchesConditionExactly(condition, schema) {
|
|
147
147
|
if (!(0, _orkestrel_contract.isString)(condition.column)) return false;
|
|
148
|
-
const column =
|
|
148
|
+
const column = (0, _src_core.findColumn)(condition.column, schema);
|
|
149
149
|
if (column === void 0) return false;
|
|
150
150
|
if (condition.operator === "absent" || condition.operator === "present") return !(column.optional && column.nullable);
|
|
151
151
|
if (column.optional || column.nullable) return false;
|
|
152
|
-
if (!EXACT_COLUMN_STORAGE.
|
|
152
|
+
if (!EXACT_COLUMN_STORAGE.includes(column.storage)) return false;
|
|
153
153
|
const first = condition.values[0];
|
|
154
154
|
const second = condition.values[1];
|
|
155
155
|
switch (condition.operator) {
|
|
@@ -158,8 +158,8 @@ function matchesConditionExactly(condition, schema) {
|
|
|
158
158
|
case "above":
|
|
159
159
|
case "below":
|
|
160
160
|
case "from":
|
|
161
|
-
case "to": return EXACT_RANGE_COLUMN_STORAGE.
|
|
162
|
-
case "between": return EXACT_RANGE_COLUMN_STORAGE.
|
|
161
|
+
case "to": return EXACT_RANGE_COLUMN_STORAGE.includes(column.storage) && matchesDeclaredStorage(first, column.storage);
|
|
162
|
+
case "between": return EXACT_RANGE_COLUMN_STORAGE.includes(column.storage) && matchesDeclaredStorage(first, column.storage) && matchesDeclaredStorage(second, column.storage);
|
|
163
163
|
case "any":
|
|
164
164
|
case "none": return condition.values.length > 0 && condition.values.every((value) => matchesDeclaredStorage(value, column.storage));
|
|
165
165
|
case "starts":
|
|
@@ -169,8 +169,8 @@ function matchesConditionExactly(condition, schema) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
|
-
*
|
|
173
|
-
* matches the engine's {@link import('@src/core').sortRows} exactly.
|
|
172
|
+
* Reports whether one {@link Order} term's column compiles to an `ORDER BY`
|
|
173
|
+
* that matches the engine's {@link import('@src/core').sortRows} exactly.
|
|
174
174
|
*
|
|
175
175
|
* @remarks
|
|
176
176
|
* `false` for a nested `FieldPath`, a column absent from `schema`, or a
|
|
@@ -183,22 +183,22 @@ function matchesConditionExactly(condition, schema) {
|
|
|
183
183
|
*
|
|
184
184
|
* @param order - The order term to test
|
|
185
185
|
* @param schema - The table's schema
|
|
186
|
-
* @returns
|
|
186
|
+
* @returns True if `order` is exact; false otherwise
|
|
187
187
|
*/
|
|
188
188
|
function matchesOrderExactly(order, schema) {
|
|
189
189
|
if (!(0, _orkestrel_contract.isString)(order.column)) return false;
|
|
190
|
-
const column =
|
|
190
|
+
const column = (0, _src_core.findColumn)(order.column, schema);
|
|
191
191
|
if (column === void 0) return false;
|
|
192
|
-
return !column.optional && !column.nullable && EXACT_RANGE_COLUMN_STORAGE.
|
|
192
|
+
return !column.optional && !column.nullable && EXACT_RANGE_COLUMN_STORAGE.includes(column.storage);
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
|
-
*
|
|
196
|
-
* term is exact. `limit` / `offset` never affect exactness (SQL
|
|
197
|
-
* `OFFSET` are always engine-identical).
|
|
195
|
+
* Reports whether a whole {@link QueryInput} is exact — every condition and
|
|
196
|
+
* every order term is exact. `limit` / `offset` never affect exactness (SQL
|
|
197
|
+
* `LIMIT` / `OFFSET` are always engine-identical).
|
|
198
198
|
*
|
|
199
199
|
* @param input - The query input to test
|
|
200
200
|
* @param schema - The table's schema
|
|
201
|
-
* @returns
|
|
201
|
+
* @returns True if every part of `input` is exact; false otherwise
|
|
202
202
|
*/
|
|
203
203
|
function matchesQueryExactly(input, schema) {
|
|
204
204
|
const conditions = input.conditions ?? [];
|
|
@@ -206,25 +206,25 @@ function matchesQueryExactly(input, schema) {
|
|
|
206
206
|
return conditions.every((condition) => matchesConditionExactly(condition, schema)) && order.every((term) => matchesOrderExactly(term, schema));
|
|
207
207
|
}
|
|
208
208
|
/**
|
|
209
|
-
*
|
|
209
|
+
* Reports whether SQLite can execute an aggregate exactly like the core engine.
|
|
210
210
|
*
|
|
211
211
|
* @param operation - Aggregate operation
|
|
212
212
|
* @param column - Aggregate field
|
|
213
213
|
* @param schema - Current table schema
|
|
214
|
-
* @returns
|
|
214
|
+
* @returns True if native aggregation is exact; false otherwise
|
|
215
215
|
*/
|
|
216
216
|
function matchesAggregateExactly(operation, column, schema) {
|
|
217
217
|
if (operation === "count") return true;
|
|
218
218
|
if (operation === "sum" || operation === "average" || !(0, _orkestrel_contract.isString)(column)) return false;
|
|
219
|
-
const declared =
|
|
219
|
+
const declared = (0, _src_core.findColumn)(column, schema);
|
|
220
220
|
return declared !== void 0 && (declared.storage === "integer" || declared.storage === "real") && !(declared.optional && declared.nullable);
|
|
221
221
|
}
|
|
222
222
|
/**
|
|
223
|
-
*
|
|
223
|
+
* Checks a declared SQLite type against a portable storage affinity.
|
|
224
224
|
*
|
|
225
225
|
* @param declared - Native declared type
|
|
226
226
|
* @param storage - Portable column storage
|
|
227
|
-
* @returns
|
|
227
|
+
* @returns True if SQLite's official affinity rules yield the expected affinity; false otherwise
|
|
228
228
|
*/
|
|
229
229
|
function matchesSQLiteAffinity(declared, storage) {
|
|
230
230
|
if (!(0, _orkestrel_contract.isString)(declared)) return false;
|
|
@@ -241,7 +241,7 @@ function matchesSQLiteAffinity(declared, storage) {
|
|
|
241
241
|
return affinity === "REAL";
|
|
242
242
|
}
|
|
243
243
|
/**
|
|
244
|
-
*
|
|
244
|
+
* Quotes a SQL identifier (a table or column name) so any characters are literal.
|
|
245
245
|
*
|
|
246
246
|
* @remarks
|
|
247
247
|
* Wraps the name in double quotes and doubles any embedded quote — the standard
|
|
@@ -260,7 +260,7 @@ function quoteIdentifier(identifier) {
|
|
|
260
260
|
return "\"" + identifier.replaceAll("\"", "\"\"") + "\"";
|
|
261
261
|
}
|
|
262
262
|
/**
|
|
263
|
-
*
|
|
263
|
+
* Encodes a JS value to its stored {@link SQLiteValue} for a declared column.
|
|
264
264
|
*
|
|
265
265
|
* @remarks
|
|
266
266
|
* The codec is total: a malformed value encodes to SQL `NULL`. Absence always
|
|
@@ -299,7 +299,7 @@ function encodeValue(value, column) {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
302
|
-
*
|
|
302
|
+
* Decodes a stored {@link SQLiteValue} back to its JS value for a declared column —
|
|
303
303
|
* the exact inverse of {@link encodeValue}.
|
|
304
304
|
*
|
|
305
305
|
* @remarks
|
|
@@ -340,7 +340,7 @@ function decodeValue(value, column) {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
/**
|
|
343
|
-
*
|
|
343
|
+
* Encodes a whole {@link Row} to a {@link SQLiteRow} by its table's schema.
|
|
344
344
|
*
|
|
345
345
|
* @remarks
|
|
346
346
|
* Encodes each declared column's value with {@link encodeValue}; columns the row
|
|
@@ -362,7 +362,7 @@ function encodeRow(row, schema) {
|
|
|
362
362
|
return result;
|
|
363
363
|
}
|
|
364
364
|
/**
|
|
365
|
-
*
|
|
365
|
+
* Extracts a stored row's values in a declared positional order.
|
|
366
366
|
*
|
|
367
367
|
* @remarks
|
|
368
368
|
* SQLite statements bind arrays positionally. Every requested column must be
|
|
@@ -393,7 +393,7 @@ function extractValues(row, names, table) {
|
|
|
393
393
|
return values;
|
|
394
394
|
}
|
|
395
395
|
/**
|
|
396
|
-
*
|
|
396
|
+
* Decodes a stored {@link SQLiteRow} back to a {@link Row} by its table's schema.
|
|
397
397
|
*
|
|
398
398
|
* @remarks
|
|
399
399
|
* Decodes each declared column with {@link decodeValue} and **omits** any column
|
|
@@ -422,7 +422,7 @@ function decodeRow(row, schema) {
|
|
|
422
422
|
return result;
|
|
423
423
|
}
|
|
424
424
|
/**
|
|
425
|
-
*
|
|
425
|
+
* Builds a collision-free SQL index name for a table + column-group index —
|
|
426
426
|
* shared by the compiler module's `schemaToIndexes` and `stepToSQL`,
|
|
427
427
|
* so a plan-built index name always matches one `open` would have created.
|
|
428
428
|
*
|
|
@@ -448,9 +448,39 @@ function deriveSQLiteIndexName(table, columns) {
|
|
|
448
448
|
return "idx_" + [table, ...columns].map((part) => String(part.length) + "_" + part).join("_");
|
|
449
449
|
}
|
|
450
450
|
//#endregion
|
|
451
|
+
//#region src/server/inferers.ts
|
|
452
|
+
/**
|
|
453
|
+
* Reads the storage type a nested (`json_extract`) operand encodes as from its
|
|
454
|
+
* RUNTIME value — NOT `json`.
|
|
455
|
+
*
|
|
456
|
+
* @remarks
|
|
457
|
+
* `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
|
|
458
|
+
* `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
|
|
459
|
+
* same scalar to compare. A boolean → `'boolean'` (→ `1` / `0`); a number →
|
|
460
|
+
* `'integer'` / `'real'`; a bigint → `'integer'`; a string → `'text'`; `null` /
|
|
461
|
+
* `undefined` → `'text'` (encodes to `null`); an object / array → `'json'` (the
|
|
462
|
+
* edge of comparing against a json subtree).
|
|
463
|
+
*
|
|
464
|
+
* @param value - The runtime operand value
|
|
465
|
+
* @returns The {@link ColumnStorage} to encode it as
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```ts
|
|
469
|
+
* inferValueStorage(true) // 'boolean'
|
|
470
|
+
* inferValueStorage(9) // 'integer'
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
function inferValueStorage(value) {
|
|
474
|
+
if (typeof value === "boolean") return "boolean";
|
|
475
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
476
|
+
if (typeof value === "bigint") return "integer";
|
|
477
|
+
if (typeof value === "object" && value !== null) return "json";
|
|
478
|
+
return "text";
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
451
481
|
//#region src/server/compilers.ts
|
|
452
482
|
/**
|
|
453
|
-
*
|
|
483
|
+
* Maps a portable {@link ColumnStorage} to its SQLite column type.
|
|
454
484
|
*
|
|
455
485
|
* @param storage - The portable column type
|
|
456
486
|
* @returns The SQLite column type keyword
|
|
@@ -466,7 +496,7 @@ function compileColumnSQL(storage) {
|
|
|
466
496
|
}
|
|
467
497
|
}
|
|
468
498
|
/**
|
|
469
|
-
*
|
|
499
|
+
* Compiles a {@link FieldPath} to the SQL expression that reads it.
|
|
470
500
|
*
|
|
471
501
|
* @param path - The field path
|
|
472
502
|
* @returns The SQL expression selecting the value
|
|
@@ -479,7 +509,7 @@ function compileFieldSQL(path) {
|
|
|
479
509
|
return "json_extract(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
480
510
|
}
|
|
481
511
|
/**
|
|
482
|
-
*
|
|
512
|
+
* Compiles an {@link AggregateOperation} over a {@link FieldPath}.
|
|
483
513
|
*
|
|
484
514
|
* @param operation - The aggregate to compute
|
|
485
515
|
* @param column - The column or nested path to aggregate
|
|
@@ -495,7 +525,7 @@ function compileAggregateSQL(operation, column) {
|
|
|
495
525
|
}
|
|
496
526
|
}
|
|
497
527
|
/**
|
|
498
|
-
*
|
|
528
|
+
* Compiles a NESTED {@link FieldPath} to the `json_type(<col>, <path>)` SQL
|
|
499
529
|
* expression — the {@link compileFieldSQL} `json_extract` sibling used to tell a
|
|
500
530
|
* PRESENT JSON `null` apart from an ABSENT path (both read back as SQL `NULL`
|
|
501
531
|
* through `json_extract`, but `json_type` reports `'null'` for the former and
|
|
@@ -516,65 +546,7 @@ function compileJSONTypeSQL(path) {
|
|
|
516
546
|
return "json_type(" + quoteIdentifier(column) + ", '$" + rest + "')";
|
|
517
547
|
}
|
|
518
548
|
/**
|
|
519
|
-
*
|
|
520
|
-
* operand is matched literally under the `LIKE … ESCAPE '\'` clause.
|
|
521
|
-
*
|
|
522
|
-
* @param text - The raw operand text
|
|
523
|
-
* @returns The text with LIKE metacharacters escaped
|
|
524
|
-
*
|
|
525
|
-
* @example
|
|
526
|
-
* ```ts
|
|
527
|
-
* escapeLike('50%_off') // '50\\%\\_off'
|
|
528
|
-
* ```
|
|
529
|
-
*/
|
|
530
|
-
function escapeLike(text) {
|
|
531
|
-
return text.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* The declared storage type of a flat (string) column, read from the schema.
|
|
535
|
-
*
|
|
536
|
-
* @param column - The column name
|
|
537
|
-
* @param schema - The table's schema
|
|
538
|
-
* @returns The column's {@link ColumnStorage}, or `undefined` if the schema does not carry it
|
|
539
|
-
*
|
|
540
|
-
* @example
|
|
541
|
-
* ```ts
|
|
542
|
-
* findColumnStorage('age', schema) // 'integer'
|
|
543
|
-
* ```
|
|
544
|
-
*/
|
|
545
|
-
function findColumnStorage(column, schema) {
|
|
546
|
-
return schema.columns.find((candidate) => candidate.name === column)?.storage;
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* The storage type a nested (`json_extract`) operand encodes as, derived from its
|
|
550
|
-
* RUNTIME value — NOT `json`.
|
|
551
|
-
*
|
|
552
|
-
* @remarks
|
|
553
|
-
* `json_extract` returns the unquoted, natively-typed scalar (a JSON boolean as
|
|
554
|
-
* `1` / `0`, a number as-is, a string as-is), so the operand must encode to that
|
|
555
|
-
* same scalar to compare. A boolean → `'boolean'` (→ `1` / `0`); a number →
|
|
556
|
-
* `'integer'` / `'real'`; a bigint → `'integer'`; a string → `'text'`; `null` /
|
|
557
|
-
* `undefined` → `'text'` (encodes to `null`); an object / array → `'json'` (the
|
|
558
|
-
* edge of comparing against a json subtree).
|
|
559
|
-
*
|
|
560
|
-
* @param value - The runtime operand value
|
|
561
|
-
* @returns The {@link ColumnStorage} to encode it as
|
|
562
|
-
*
|
|
563
|
-
* @example
|
|
564
|
-
* ```ts
|
|
565
|
-
* inferValueStorage(true) // 'boolean'
|
|
566
|
-
* inferValueStorage(9) // 'integer'
|
|
567
|
-
* ```
|
|
568
|
-
*/
|
|
569
|
-
function inferValueStorage(value) {
|
|
570
|
-
if (typeof value === "boolean") return "boolean";
|
|
571
|
-
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "real";
|
|
572
|
-
if (typeof value === "bigint") return "integer";
|
|
573
|
-
if (typeof value === "object" && value !== null) return "json";
|
|
574
|
-
return "text";
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Compile one condition to its `<column> <operator>` SQL fragment and the parameters
|
|
549
|
+
* Compiles one condition to its `<column> <operator>` SQL fragment and the parameters
|
|
578
550
|
* it binds — engine-exact under SQL's three-valued NULL logic.
|
|
579
551
|
*
|
|
580
552
|
* @remarks
|
|
@@ -645,7 +617,7 @@ function inferValueStorage(value) {
|
|
|
645
617
|
function compileConditionSQL(condition, schema) {
|
|
646
618
|
const column = compileFieldSQL(condition.column);
|
|
647
619
|
const nested = !(0, _orkestrel_contract.isString)(condition.column);
|
|
648
|
-
const declared = (0, _orkestrel_contract.isString)(condition.column) ?
|
|
620
|
+
const declared = (0, _orkestrel_contract.isString)(condition.column) ? (0, _src_core.findColumn)(condition.column, schema) : void 0;
|
|
649
621
|
const first = condition.values[0];
|
|
650
622
|
const second = condition.values[1];
|
|
651
623
|
const nullOperand = first === null || first === void 0;
|
|
@@ -764,7 +736,7 @@ function compileConditionSQL(condition, schema) {
|
|
|
764
736
|
};
|
|
765
737
|
}
|
|
766
738
|
/**
|
|
767
|
-
*
|
|
739
|
+
* Folds the conditions into one WHERE clause, parenthesizing progressively
|
|
768
740
|
* left-to-right so the grouping matches the engine's `matchesQuery` fold.
|
|
769
741
|
*
|
|
770
742
|
* @remarks
|
|
@@ -780,11 +752,11 @@ function compileConditionSQL(condition, schema) {
|
|
|
780
752
|
*
|
|
781
753
|
* @example
|
|
782
754
|
* ```ts
|
|
783
|
-
*
|
|
755
|
+
* compileWhereSQL([{ column: 'age', operator: 'from', values: [18], connector: 'and' }], schema)
|
|
784
756
|
* // { sql: 'WHERE "age" >= ?', parameters: [18] }
|
|
785
757
|
* ```
|
|
786
758
|
*/
|
|
787
|
-
function
|
|
759
|
+
function compileWhereSQL(conditions, schema) {
|
|
788
760
|
const [first, ...remaining] = conditions;
|
|
789
761
|
if (first === void 0) return {
|
|
790
762
|
sql: "",
|
|
@@ -805,14 +777,14 @@ function compileWhere(conditions, schema) {
|
|
|
805
777
|
};
|
|
806
778
|
}
|
|
807
779
|
/**
|
|
808
|
-
*
|
|
780
|
+
* Compiles the ORDER BY clause from the order terms, always ending with the
|
|
809
781
|
* primary key as the final determinant.
|
|
810
782
|
*
|
|
811
783
|
* @remarks
|
|
812
784
|
* The native `records` read then resolves ties in key order, matching a
|
|
813
785
|
* primary-key-ordered `scan` and the core engine's stable `sortRows` over a
|
|
814
786
|
* key-ordered scan (and IndexedDB's key-ordered reads), so a native read equals
|
|
815
|
-
* the scan path
|
|
787
|
+
* the scan path — native ↔ engine parity. SQLite without an
|
|
816
788
|
* `ORDER BY` returns rowid (insertion) order, and an explicit order alone breaks
|
|
817
789
|
* ties by rowid too — both diverge from every key-ordered backend. The
|
|
818
790
|
* tie-breaker is ASCENDING regardless of the explicit directions: the engine's
|
|
@@ -826,17 +798,17 @@ function compileWhere(conditions, schema) {
|
|
|
826
798
|
*
|
|
827
799
|
* @example
|
|
828
800
|
* ```ts
|
|
829
|
-
*
|
|
801
|
+
* compileOrderSQL([{ column: 'age', direction: 'descending' }], schema)
|
|
830
802
|
* // 'ORDER BY "age" DESC, "id"'
|
|
831
803
|
* ```
|
|
832
804
|
*/
|
|
833
|
-
function
|
|
805
|
+
function compileOrderSQL(order, schema) {
|
|
834
806
|
const terms = (order ?? []).map((term) => compileFieldSQL(term.column) + (term.direction === "descending" ? " DESC" : " ASC"));
|
|
835
807
|
if (!(order ?? []).some((term) => (0, _orkestrel_contract.isString)(term.column) && term.column === schema.primary)) terms.push(quoteIdentifier(schema.primary));
|
|
836
808
|
return terms.length === 0 ? "" : "ORDER BY " + terms.join(", ");
|
|
837
809
|
}
|
|
838
810
|
/**
|
|
839
|
-
*
|
|
811
|
+
* Compiles the LIMIT / OFFSET clause.
|
|
840
812
|
*
|
|
841
813
|
* @remarks
|
|
842
814
|
* An offset without a limit uses `LIMIT -1` (SQLite's "no limit") so OFFSET is
|
|
@@ -848,10 +820,10 @@ function compileOrder(order, schema) {
|
|
|
848
820
|
*
|
|
849
821
|
* @example
|
|
850
822
|
* ```ts
|
|
851
|
-
*
|
|
823
|
+
* compilePageSQL(undefined, 5) // { sql: 'LIMIT -1 OFFSET ?', parameters: [5] }
|
|
852
824
|
* ```
|
|
853
825
|
*/
|
|
854
|
-
function
|
|
826
|
+
function compilePageSQL(limit, offset) {
|
|
855
827
|
(0, _src_core.validatePage)({
|
|
856
828
|
...limit === void 0 ? {} : { limit },
|
|
857
829
|
...offset === void 0 ? {} : { offset }
|
|
@@ -874,7 +846,7 @@ function compilePage(limit, offset) {
|
|
|
874
846
|
};
|
|
875
847
|
}
|
|
876
848
|
/**
|
|
877
|
-
*
|
|
849
|
+
* Compiles a {@link QueryInput} into the SQL clause that follows a table name, with
|
|
878
850
|
* its bound parameters in clause order.
|
|
879
851
|
*
|
|
880
852
|
* @remarks
|
|
@@ -884,11 +856,13 @@ function compilePage(limit, offset) {
|
|
|
884
856
|
* over a JS `scan`. The WHERE fold is parenthesized **left-to-right** to mirror
|
|
885
857
|
* the core engine's `matchesQuery` (not SQL's native AND-over-OR precedence),
|
|
886
858
|
* so a native and an engine read return identical rows. Each operand is encoded
|
|
887
|
-
*
|
|
859
|
+
* through `encodeValue`: a flat column uses its declared schema type, while a nested
|
|
888
860
|
* `FieldPath` (a `json_extract` read) encodes each operand as the native scalar
|
|
889
861
|
* the extract returns — derived from the operand's runtime type — so it compares.
|
|
890
|
-
*
|
|
891
|
-
* `starts` / `ends`
|
|
862
|
+
* Every operator maps per the databases guide's operator table, with
|
|
863
|
+
* `starts` / `ends` compiling to a CODE-POINT `substr` slice guarded by
|
|
864
|
+
* `typeof(<column>) = 'text'` (case-sensitive, matching the engine's
|
|
865
|
+
* `String.prototype.startsWith` / `endsWith`) and an empty `any` / `none` list
|
|
892
866
|
* collapsing to a constant. An `undefined` input (or one with no parts)
|
|
893
867
|
* compiles to an empty clause.
|
|
894
868
|
*
|
|
@@ -904,9 +878,9 @@ function compilePage(limit, offset) {
|
|
|
904
878
|
*/
|
|
905
879
|
function compileQuerySQL(input, schema) {
|
|
906
880
|
(0, _src_core.validatePage)(input);
|
|
907
|
-
const where =
|
|
908
|
-
const orderBy =
|
|
909
|
-
const page =
|
|
881
|
+
const where = compileWhereSQL(input?.conditions ?? [], schema);
|
|
882
|
+
const orderBy = compileOrderSQL(input?.order, schema);
|
|
883
|
+
const page = compilePageSQL(input?.limit, input?.offset);
|
|
910
884
|
return {
|
|
911
885
|
sql: [
|
|
912
886
|
where.sql,
|
|
@@ -917,7 +891,7 @@ function compileQuerySQL(input, schema) {
|
|
|
917
891
|
};
|
|
918
892
|
}
|
|
919
893
|
/**
|
|
920
|
-
*
|
|
894
|
+
* Projects a {@link TableSchema} to its `CREATE TABLE IF NOT EXISTS` statement.
|
|
921
895
|
*
|
|
922
896
|
* @param schema - The table schema
|
|
923
897
|
* @returns The complete table declaration
|
|
@@ -927,7 +901,7 @@ function schemaToTable(schema) {
|
|
|
927
901
|
return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(schema.name) + " (" + columns.join(", ") + ", PRIMARY KEY (" + quoteIdentifier(schema.primary) + "))";
|
|
928
902
|
}
|
|
929
903
|
/**
|
|
930
|
-
*
|
|
904
|
+
* Projects a {@link TableSchema} to its declared SQLite indexes.
|
|
931
905
|
*
|
|
932
906
|
* @param schema - The table schema
|
|
933
907
|
* @returns One statement per declared index
|
|
@@ -936,7 +910,7 @@ function schemaToIndexes(schema) {
|
|
|
936
910
|
return schema.indexes.map((group) => "CREATE INDEX IF NOT EXISTS " + quoteIdentifier(deriveSQLiteIndexName(schema.name, group)) + " ON " + quoteIdentifier(schema.name) + " (" + group.map(quoteIdentifier).join(", ") + ")");
|
|
937
911
|
}
|
|
938
912
|
/**
|
|
939
|
-
*
|
|
913
|
+
* Projects one {@link MigrationStep} to SQLite DDL.
|
|
940
914
|
*
|
|
941
915
|
* @param step - The migration step
|
|
942
916
|
* @returns The statements that apply the step
|
|
@@ -952,97 +926,9 @@ function stepToSQL(step) {
|
|
|
952
926
|
}
|
|
953
927
|
}
|
|
954
928
|
//#endregion
|
|
955
|
-
//#region src/core/DriverIterator.ts
|
|
956
|
-
/**
|
|
957
|
-
* The internal continuation boundary for a root driver async iterator.
|
|
958
|
-
*
|
|
959
|
-
* @remarks
|
|
960
|
-
* A driver transaction can begin while a caller holds an idle root iterator.
|
|
961
|
-
* Every `next` therefore checks the driver's root-state guard immediately
|
|
962
|
-
* before and after advancing the source. A failed continuation terminalizes the
|
|
963
|
-
* iterator, discards any row produced before the post-advance guard failed, and
|
|
964
|
-
* attempts source cleanup exactly once.
|
|
965
|
-
*/
|
|
966
|
-
var DriverIterator = class {
|
|
967
|
-
#source;
|
|
968
|
-
#guard;
|
|
969
|
-
#terminal = false;
|
|
970
|
-
#cleaned = false;
|
|
971
|
-
constructor(source, guard) {
|
|
972
|
-
this.#source = source;
|
|
973
|
-
this.#guard = guard;
|
|
974
|
-
}
|
|
975
|
-
[Symbol.asyncIterator]() {
|
|
976
|
-
return this;
|
|
977
|
-
}
|
|
978
|
-
async next() {
|
|
979
|
-
if (this.#terminal) return {
|
|
980
|
-
done: true,
|
|
981
|
-
value: void 0
|
|
982
|
-
};
|
|
983
|
-
try {
|
|
984
|
-
this.#guard();
|
|
985
|
-
const result = await this.#source.next();
|
|
986
|
-
this.#guard();
|
|
987
|
-
if (result.done === true) {
|
|
988
|
-
this.#terminal = true;
|
|
989
|
-
this.#cleaned = true;
|
|
990
|
-
}
|
|
991
|
-
return result;
|
|
992
|
-
} catch (error) {
|
|
993
|
-
this.#terminal = true;
|
|
994
|
-
await this.#discard();
|
|
995
|
-
throw error;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
async return() {
|
|
999
|
-
if (this.#terminal) return {
|
|
1000
|
-
done: true,
|
|
1001
|
-
value: void 0
|
|
1002
|
-
};
|
|
1003
|
-
this.#terminal = true;
|
|
1004
|
-
if (this.#cleaned || this.#source.return === void 0) {
|
|
1005
|
-
this.#cleaned = true;
|
|
1006
|
-
return {
|
|
1007
|
-
done: true,
|
|
1008
|
-
value: void 0
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
this.#cleaned = true;
|
|
1012
|
-
return this.#source.return();
|
|
1013
|
-
}
|
|
1014
|
-
async throw(error) {
|
|
1015
|
-
if (this.#terminal) throw error;
|
|
1016
|
-
if (this.#source.throw === void 0) {
|
|
1017
|
-
this.#terminal = true;
|
|
1018
|
-
await this.#discard();
|
|
1019
|
-
throw error;
|
|
1020
|
-
}
|
|
1021
|
-
try {
|
|
1022
|
-
const result = await this.#source.throw(error);
|
|
1023
|
-
if (result.done === true) {
|
|
1024
|
-
this.#terminal = true;
|
|
1025
|
-
this.#cleaned = true;
|
|
1026
|
-
}
|
|
1027
|
-
return result;
|
|
1028
|
-
} catch (cause) {
|
|
1029
|
-
this.#terminal = true;
|
|
1030
|
-
await this.#discard();
|
|
1031
|
-
throw cause;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
async #discard() {
|
|
1035
|
-
if (this.#cleaned) return;
|
|
1036
|
-
this.#cleaned = true;
|
|
1037
|
-
try {
|
|
1038
|
-
await this.#source.return?.();
|
|
1039
|
-
} catch {}
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
//#endregion
|
|
1043
929
|
//#region src/server/drivers/JSONDriver.ts
|
|
1044
930
|
/**
|
|
1045
|
-
*
|
|
931
|
+
* Implements a persistent {@link DriverInterface} backed by a single JSON file — the
|
|
1046
932
|
* reference {@link MemoryDriver} plus file load / flush.
|
|
1047
933
|
*
|
|
1048
934
|
* @remarks
|
|
@@ -1056,15 +942,15 @@ var DriverIterator = class {
|
|
|
1056
942
|
* primary (the table contract), so the key is recovered on load with
|
|
1057
943
|
* {@link extractKey} and the file need not store it. The parsed JSON crosses the
|
|
1058
944
|
* boundary as `unknown` and is narrowed with {@link isRecord} / {@link extractKey},
|
|
1059
|
-
* never asserted
|
|
945
|
+
* never asserted. A read that reports no document there starts empty —
|
|
1060
946
|
* `ENOENT` for a plain absence, and `ENOTDIR` for a path whose parent is not a
|
|
1061
947
|
* directory, which no later write could find either; every other read failure or
|
|
1062
948
|
* invalid existing document fails closed without publication, mutation, or
|
|
1063
|
-
* automatic repair. It
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
* driver.
|
|
949
|
+
* automatic repair. It implements the optional native `stream` hook that
|
|
950
|
+
* `TableInterface.scan` prefers over `scan`, and neither `records` nor
|
|
951
|
+
* `aggregate`, so the core engine's `matchesQuery` answers every query on
|
|
952
|
+
* either path. For development, small datasets, and portable / inspectable
|
|
953
|
+
* data; for large or concurrent workloads reach for a SQLite-backed driver.
|
|
1068
954
|
*
|
|
1069
955
|
* Metadata crosses {@link cloneDriverMetadata} at parsed-file ingress, public and
|
|
1070
956
|
* scoped write ingress, candidate/root publication, serialization, and copy-out.
|
|
@@ -1125,10 +1011,10 @@ var JSONDriver = class {
|
|
|
1125
1011
|
return this.#memory.keys(table);
|
|
1126
1012
|
}
|
|
1127
1013
|
scan(table) {
|
|
1128
|
-
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1014
|
+
return new _src_core.DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1129
1015
|
}
|
|
1130
1016
|
/**
|
|
1131
|
-
*
|
|
1017
|
+
* Iterates rows lazily with native filtering — delegates to the inner {@link MemoryDriver}.
|
|
1132
1018
|
*
|
|
1133
1019
|
* @remarks
|
|
1134
1020
|
* Semantics are the memory driver's own: `input.conditions` filters, `offset`
|
|
@@ -1140,14 +1026,14 @@ var JSONDriver = class {
|
|
|
1140
1026
|
*/
|
|
1141
1027
|
stream(table, input) {
|
|
1142
1028
|
(0, _src_core.validatePage)(input);
|
|
1143
|
-
return new DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1029
|
+
return new _src_core.DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1144
1030
|
}
|
|
1145
1031
|
async clear(table) {
|
|
1146
1032
|
this.#root();
|
|
1147
1033
|
await this.#enqueue(() => this.#clear(table));
|
|
1148
1034
|
}
|
|
1149
1035
|
/**
|
|
1150
|
-
*
|
|
1036
|
+
* Runs an isolated native transaction callback over a candidate memory store.
|
|
1151
1037
|
*
|
|
1152
1038
|
* @remarks
|
|
1153
1039
|
* Single-writer: nesting and root operations while active throw `CONFLICT`.
|
|
@@ -1164,7 +1050,7 @@ var JSONDriver = class {
|
|
|
1164
1050
|
return this.#enqueue(() => this.#transact(scope));
|
|
1165
1051
|
}
|
|
1166
1052
|
/**
|
|
1167
|
-
*
|
|
1053
|
+
* Captures an owned row snapshot at an exact writer-queue position.
|
|
1168
1054
|
*
|
|
1169
1055
|
* @remarks
|
|
1170
1056
|
* Capture owns table names, schemas, rows, and one session-local identity per
|
|
@@ -1192,7 +1078,7 @@ var JSONDriver = class {
|
|
|
1192
1078
|
return this.#metadata === void 0 ? void 0 : (0, _src_core.cloneDriverMetadata)(this.#metadata);
|
|
1193
1079
|
}
|
|
1194
1080
|
/**
|
|
1195
|
-
*
|
|
1081
|
+
* Persists an owned metadata snapshot for a later `metadata()` to copy out.
|
|
1196
1082
|
*
|
|
1197
1083
|
* @remarks
|
|
1198
1084
|
* Root stamping conflicts while a transaction is active. The scoped
|
|
@@ -1207,7 +1093,7 @@ var JSONDriver = class {
|
|
|
1207
1093
|
await this.#enqueue(() => this.#stamp(owned));
|
|
1208
1094
|
}
|
|
1209
1095
|
/**
|
|
1210
|
-
*
|
|
1096
|
+
* Applies one atomic {@link MigrationInput} through an isolated candidate.
|
|
1211
1097
|
*
|
|
1212
1098
|
* @remarks
|
|
1213
1099
|
* The candidate receives every plan step plus optional metadata. Its complete
|
|
@@ -1421,7 +1307,8 @@ var JSONDriver = class {
|
|
|
1421
1307
|
return this.#requireCandidate(token).keys(table);
|
|
1422
1308
|
}
|
|
1423
1309
|
#scanCandidate(token, table) {
|
|
1424
|
-
|
|
1310
|
+
const source = this.#requireCandidate(token).scan(table);
|
|
1311
|
+
return new _src_core.DriverIterator(source[Symbol.asyncIterator](), () => {
|
|
1425
1312
|
this.#requireCandidate(token);
|
|
1426
1313
|
});
|
|
1427
1314
|
}
|
|
@@ -1668,8 +1555,8 @@ var JSONDriver = class {
|
|
|
1668
1555
|
//#endregion
|
|
1669
1556
|
//#region src/server/drivers/SQLiteDriver.ts
|
|
1670
1557
|
/**
|
|
1671
|
-
*
|
|
1672
|
-
* built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
1558
|
+
* Implements the {@link DriverInterface} over SQLite — the server-native, trusted-mode
|
|
1559
|
+
* backend built on the published `@orkestrel/sqlite` synchronous wrapper.
|
|
1673
1560
|
*
|
|
1674
1561
|
* @remarks
|
|
1675
1562
|
* A thin adapter: it implements the storage primitives the core database layer
|
|
@@ -1680,41 +1567,40 @@ var JSONDriver = class {
|
|
|
1680
1567
|
* reopen-safe), and readies a reserved `_metadata` single-row table `metadata()` /
|
|
1681
1568
|
* `stamp()` read and write — **a user table named `_metadata` collides with it**;
|
|
1682
1569
|
* avoid the name. Rows cross the boundary through the codecs in `helpers.ts`
|
|
1683
|
-
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so
|
|
1684
|
-
* typed layer above imposes the exact shape
|
|
1570
|
+
* (`json` columns store / parse JSON text, a `boolean` stores `1` / `0`), so
|
|
1571
|
+
* the typed layer above imposes the exact shape. `write` is an
|
|
1685
1572
|
* `INSERT OR REPLACE` upsert, while `insert` uses a plain `INSERT` and maps its
|
|
1686
1573
|
* atomic primary-key constraint failure to `CONFLICT`; every other backend
|
|
1687
1574
|
* `SQLiteError` is contained by the same `DatabaseError` boundary described
|
|
1688
|
-
* below. Querying, ordering, paging, and
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
* `
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
*
|
|
1701
|
-
* `JSONDriver` migrate; a step referencing an undeclared table throws
|
|
1575
|
+
* below. Querying, ordering, paging, and aggregation is native: `records` /
|
|
1576
|
+
* `stream` compile a `QueryInput` to SQL with `compileQuerySQL`, and
|
|
1577
|
+
* `aggregate` runs a SQL `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` (through
|
|
1578
|
+
* `compileAggregateSQL`) over the same compiled WHERE. `transaction` runs a
|
|
1579
|
+
* callback inside native `BEGIN` / `COMMIT` / `ROLLBACK`, passing a scoped
|
|
1580
|
+
* storage capability that becomes invalid after settlement. `migrate` runs the
|
|
1581
|
+
* plan's projected DDL ({@link import('../compilers.js').stepToSQL}) inside
|
|
1582
|
+
* whichever native transaction is active: joined into the active transaction
|
|
1583
|
+
* callback when one exists (the core's versioned reconcile path wraps migrate +
|
|
1584
|
+
* stamp in one native `BEGIN`, and node:sqlite rejects a nested `BEGIN`), or
|
|
1585
|
+
* inside its own `database.transact` otherwise — a mid-plan failure rolls
|
|
1586
|
+
* back atomically either way, an improvement over the non-atomic `MemoryDriver`
|
|
1587
|
+
* / `JSONDriver` migrate; a step referencing an undeclared table throws
|
|
1702
1588
|
* `DatabaseError` `MIGRATION` before any DDL for that step runs. `snapshot` is
|
|
1703
|
-
* capture-replay (SELECT the
|
|
1704
|
-
*
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1589
|
+
* capture-replay (SELECT the named tables' rows, replay through DELETE + INSERT OR
|
|
1590
|
+
* REPLACE inside a native transaction on rollback) rather than a SQL
|
|
1591
|
+
* `SAVEPOINT`, since the core `transaction` calls the rollback thunk only on
|
|
1592
|
+
* failure with no commit-on-success signal — a long-lived `SAVEPOINT` would
|
|
1593
|
+
* leave the connection uncommitted (lost on close). Every backend interaction
|
|
1594
|
+
* runs through `#guard`, which maps a thrown backend `SQLiteError` (or any
|
|
1595
|
+
* unexpected non-`SQLiteError` throw) to a typed {@link DatabaseError} — never
|
|
1596
|
+
* a raw backend error escapes `DriverInterface`: `CONSTRAINT` → `CONFLICT`, the
|
|
1597
|
+
* wrapper's own `CLOSED` → `CLOSED`, `BUSY` (a locked database that outlasted
|
|
1598
|
+
* the configured `timeout`) → a retryable `DRIVER` (`context.retryable` is
|
|
1599
|
+
* `true`), and `UNKNOWN` / any other throw → `DRIVER`. The original error is
|
|
1600
|
+
* preserved as `context.cause`. A `DatabaseError` this driver throws directly
|
|
1601
|
+
* (`CLOSED` from the `#require` gate, `NOT_FOUND` from `#table`, `MIGRATION`
|
|
1602
|
+
* from a migration-plan fault) passes through `#guard` unchanged, never
|
|
1603
|
+
* re-wrapped.
|
|
1718
1604
|
*/
|
|
1719
1605
|
var SQLiteDriver = class {
|
|
1720
1606
|
#path;
|
|
@@ -1751,7 +1637,7 @@ var SQLiteDriver = class {
|
|
|
1751
1637
|
for (const [name, value] of Object.entries(this.#options.pragmas ?? {})) database.pragma(name, value);
|
|
1752
1638
|
const map = /* @__PURE__ */ new Map();
|
|
1753
1639
|
const identities = /* @__PURE__ */ new Map();
|
|
1754
|
-
database.
|
|
1640
|
+
database.transact(() => {
|
|
1755
1641
|
this.#ensureMetadataTable(database);
|
|
1756
1642
|
const stored = this.#readMetadata(database);
|
|
1757
1643
|
const deployed = (0, _src_core.normalizeDriverSchema)(stored?.schema ?? owned);
|
|
@@ -1769,11 +1655,11 @@ var SQLiteDriver = class {
|
|
|
1769
1655
|
for (const table of deployed) {
|
|
1770
1656
|
const absent = missing.get(table.name);
|
|
1771
1657
|
if (absent === void 0) {
|
|
1772
|
-
database.
|
|
1773
|
-
for (const sql of schemaToIndexes(table)) database.
|
|
1658
|
+
database.execute(schemaToTable(table));
|
|
1659
|
+
for (const sql of schemaToIndexes(table)) database.execute(sql);
|
|
1774
1660
|
} else for (const [index, group] of table.indexes.entries()) if (absent.includes(deriveSQLiteIndexName(table.name, group))) {
|
|
1775
1661
|
const sql = schemaToIndexes(table)[index];
|
|
1776
|
-
if (sql !== void 0) database.
|
|
1662
|
+
if (sql !== void 0) database.execute(sql);
|
|
1777
1663
|
}
|
|
1778
1664
|
identities.set(table.name, previousIdentities.get(table.name) ?? {});
|
|
1779
1665
|
}
|
|
@@ -1819,7 +1705,7 @@ var SQLiteDriver = class {
|
|
|
1819
1705
|
return this.#keys(table);
|
|
1820
1706
|
}
|
|
1821
1707
|
scan(table) {
|
|
1822
|
-
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1708
|
+
return new _src_core.DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => this.#root());
|
|
1823
1709
|
}
|
|
1824
1710
|
async clear(table) {
|
|
1825
1711
|
this.#root();
|
|
@@ -1845,7 +1731,7 @@ var SQLiteDriver = class {
|
|
|
1845
1731
|
const conditionsExact = conditions.every((condition) => matchesConditionExactly(condition, schema));
|
|
1846
1732
|
const columnExact = matchesAggregateExactly(operation, column, schema);
|
|
1847
1733
|
if (conditionsExact && columnExact) return this.#guard(() => {
|
|
1848
|
-
const { sql, parameters } =
|
|
1734
|
+
const { sql, parameters } = compileWhereSQL(conditions, schema);
|
|
1849
1735
|
const value = this.#require().prepare("SELECT " + compileAggregateSQL(operation, column) + " AS value FROM " + quoteIdentifier(table) + (sql === "" ? "" : " " + sql)).get(parameters)?.value;
|
|
1850
1736
|
return value === null || value === void 0 ? void 0 : Number(value);
|
|
1851
1737
|
});
|
|
@@ -1855,10 +1741,10 @@ var SQLiteDriver = class {
|
|
|
1855
1741
|
}
|
|
1856
1742
|
stream(table, input) {
|
|
1857
1743
|
(0, _src_core.validatePage)(input);
|
|
1858
|
-
return new DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1744
|
+
return new _src_core.DriverIterator(this.#stream(table, input)[Symbol.asyncIterator](), () => this.#root());
|
|
1859
1745
|
}
|
|
1860
1746
|
/**
|
|
1861
|
-
*
|
|
1747
|
+
* Begins a native transaction — real `BEGIN`, `COMMIT`, `ROLLBACK`.
|
|
1862
1748
|
*
|
|
1863
1749
|
* @remarks
|
|
1864
1750
|
* The callback receives a scoped {@link StorageInterface}. Fulfillment
|
|
@@ -1905,23 +1791,22 @@ var SQLiteDriver = class {
|
|
|
1905
1791
|
}
|
|
1906
1792
|
}
|
|
1907
1793
|
/**
|
|
1908
|
-
*
|
|
1794
|
+
* Applies a {@link Migration} plan by executing each step's projected DDL
|
|
1909
1795
|
* ({@link import('../compilers.js').stepToSQL}).
|
|
1910
1796
|
*
|
|
1911
1797
|
* @remarks
|
|
1912
|
-
* Atomicity is provided by whichever native transaction is active: when
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
*
|
|
1917
|
-
*
|
|
1918
|
-
* generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
1798
|
+
* Atomicity is provided by whichever native transaction is active: when this
|
|
1799
|
+
* driver's own `transaction()` callback is active (the core's versioned
|
|
1800
|
+
* reconcile / migrate path joins migrate + stamp under one native `BEGIN`),
|
|
1801
|
+
* the plan's DDL runs directly inside that enclosing transaction — a mid-plan
|
|
1802
|
+
* failure rejects the callback and the driver rolls it back. node:sqlite (and
|
|
1803
|
+
* SQLite generally) rejects a nested `BEGIN`, so this driver must never open a
|
|
1919
1804
|
* second native transaction while one is already open. Otherwise (no
|
|
1920
1805
|
* enclosing transaction), `migrate` wraps the plan in its own native
|
|
1921
1806
|
* `database.transaction` — atomic on its own: a mid-plan failure rolls
|
|
1922
1807
|
* back every DDL statement already applied by the plan. A scoped migration
|
|
1923
1808
|
* uses one fixed internal savepoint literal because the published SQLite
|
|
1924
|
-
* wrapper intentionally exposes raw `
|
|
1809
|
+
* wrapper intentionally exposes raw `execute` but no savepoint manager. That
|
|
1925
1810
|
* savepoint contains a caught inner migration so the outer callback
|
|
1926
1811
|
* transaction remains active and may continue safely. A step referencing a
|
|
1927
1812
|
* table not in this driver's declared schema (and that is not itself a
|
|
@@ -1941,7 +1826,7 @@ var SQLiteDriver = class {
|
|
|
1941
1826
|
metadata: owned.metadata.schema
|
|
1942
1827
|
});
|
|
1943
1828
|
this.#guard(() => {
|
|
1944
|
-
database.
|
|
1829
|
+
database.transact(() => {
|
|
1945
1830
|
this.#applyPlan(database, owned);
|
|
1946
1831
|
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
1947
1832
|
});
|
|
@@ -1950,7 +1835,7 @@ var SQLiteDriver = class {
|
|
|
1950
1835
|
this.#identities = identities;
|
|
1951
1836
|
}
|
|
1952
1837
|
/**
|
|
1953
|
-
*
|
|
1838
|
+
* Reads the persisted {@link DriverMetadata} from the reserved `_metadata` table.
|
|
1954
1839
|
*
|
|
1955
1840
|
* @returns The last-stamped `DriverMetadata`, or `undefined` when never stamped
|
|
1956
1841
|
* (or the stored row is malformed)
|
|
@@ -1960,7 +1845,7 @@ var SQLiteDriver = class {
|
|
|
1960
1845
|
return this.#metadata();
|
|
1961
1846
|
}
|
|
1962
1847
|
/**
|
|
1963
|
-
*
|
|
1848
|
+
* Persists an owned metadata snapshot into the reserved `_metadata` table's
|
|
1964
1849
|
* single row.
|
|
1965
1850
|
*
|
|
1966
1851
|
* @param metadata - The {@link DriverMetadata} to persist
|
|
@@ -2015,11 +1900,11 @@ var SQLiteDriver = class {
|
|
|
2015
1900
|
}
|
|
2016
1901
|
this.#guard(() => {
|
|
2017
1902
|
const current = this.#require();
|
|
2018
|
-
current.
|
|
1903
|
+
current.transact(() => {
|
|
2019
1904
|
for (const [name, replacement] of replacements) {
|
|
2020
|
-
current.
|
|
1905
|
+
current.execute("DELETE FROM " + quoteIdentifier(name));
|
|
2021
1906
|
const statement = current.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(name) + " (" + replacement.names.map(quoteIdentifier).join(", ") + ") VALUES (" + replacement.names.map(() => "?").join(", ") + ")");
|
|
2022
|
-
for (const values of replacement.values) statement.
|
|
1907
|
+
for (const values of replacement.values) statement.execute(values);
|
|
2023
1908
|
}
|
|
2024
1909
|
});
|
|
2025
1910
|
});
|
|
@@ -2040,7 +1925,7 @@ var SQLiteDriver = class {
|
|
|
2040
1925
|
const values = extractValues(encoded, names, table);
|
|
2041
1926
|
const statement = this.#require().prepare("INSERT OR REPLACE INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2042
1927
|
(0, _src_core.checkAbort)(options?.signal);
|
|
2043
|
-
statement.
|
|
1928
|
+
statement.execute(values);
|
|
2044
1929
|
});
|
|
2045
1930
|
}
|
|
2046
1931
|
async #insert(table, key, row, options) {
|
|
@@ -2051,7 +1936,7 @@ var SQLiteDriver = class {
|
|
|
2051
1936
|
const values = extractValues(encoded, names, table);
|
|
2052
1937
|
const statement = this.#require().prepare("INSERT INTO " + quoteIdentifier(table) + " (" + names.map(quoteIdentifier).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")");
|
|
2053
1938
|
(0, _src_core.checkAbort)(options?.signal);
|
|
2054
|
-
statement.
|
|
1939
|
+
statement.execute(values);
|
|
2055
1940
|
});
|
|
2056
1941
|
}
|
|
2057
1942
|
async #delete(table, key, options) {
|
|
@@ -2059,7 +1944,7 @@ var SQLiteDriver = class {
|
|
|
2059
1944
|
return this.#guard(() => {
|
|
2060
1945
|
const statement = this.#require().prepare("DELETE FROM " + quoteIdentifier(table) + " WHERE " + quoteIdentifier(schema.primary) + " = ?");
|
|
2061
1946
|
(0, _src_core.checkAbort)(options?.signal);
|
|
2062
|
-
return statement.
|
|
1947
|
+
return statement.execute([this.#key(key, schema)]).changes > 0;
|
|
2063
1948
|
});
|
|
2064
1949
|
}
|
|
2065
1950
|
async #keys(table) {
|
|
@@ -2121,14 +2006,14 @@ var SQLiteDriver = class {
|
|
|
2121
2006
|
async #clear(table) {
|
|
2122
2007
|
this.#table(table);
|
|
2123
2008
|
this.#guard(() => {
|
|
2124
|
-
this.#require().prepare("DELETE FROM " + quoteIdentifier(table)).
|
|
2009
|
+
this.#require().prepare("DELETE FROM " + quoteIdentifier(table)).execute();
|
|
2125
2010
|
});
|
|
2126
2011
|
}
|
|
2127
2012
|
async #metadata() {
|
|
2128
2013
|
return this.#guard(() => this.#readMetadata(this.#require()));
|
|
2129
2014
|
}
|
|
2130
2015
|
#ensureMetadataTable(database) {
|
|
2131
|
-
database.
|
|
2016
|
+
database.execute("CREATE TABLE IF NOT EXISTS " + quoteIdentifier(METADATA_TABLE) + " (\"id\" INTEGER, \"version\" INTEGER, \"schema\" TEXT, PRIMARY KEY (\"id\"))");
|
|
2132
2017
|
}
|
|
2133
2018
|
#validateTable(database, schema) {
|
|
2134
2019
|
const object = database.prepare("SELECT \"type\" AS \"category\" FROM \"sqlite_schema\" WHERE \"name\" = ?").get([schema.name]);
|
|
@@ -2263,7 +2148,7 @@ var SQLiteDriver = class {
|
|
|
2263
2148
|
this.#guard(() => this.#writeMetadata(database, owned));
|
|
2264
2149
|
}
|
|
2265
2150
|
#writeMetadata(database, metadata) {
|
|
2266
|
-
database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").
|
|
2151
|
+
database.prepare("INSERT OR REPLACE INTO " + quoteIdentifier(METADATA_TABLE) + " (\"id\", \"version\", \"schema\") VALUES (1, ?, ?)").execute([metadata.version, JSON.stringify(metadata.schema)]);
|
|
2267
2152
|
}
|
|
2268
2153
|
#capability(token) {
|
|
2269
2154
|
return {
|
|
@@ -2300,7 +2185,7 @@ var SQLiteDriver = class {
|
|
|
2300
2185
|
return this.#keys(table);
|
|
2301
2186
|
}
|
|
2302
2187
|
#scanTransaction(token, table) {
|
|
2303
|
-
return new DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => {
|
|
2188
|
+
return new _src_core.DriverIterator(this.#scan(table)[Symbol.asyncIterator](), () => {
|
|
2304
2189
|
this.#requireTransaction(token);
|
|
2305
2190
|
});
|
|
2306
2191
|
}
|
|
@@ -2322,16 +2207,16 @@ var SQLiteDriver = class {
|
|
|
2322
2207
|
metadata: owned.metadata.schema
|
|
2323
2208
|
});
|
|
2324
2209
|
this.#guard(() => {
|
|
2325
|
-
database.
|
|
2210
|
+
database.execute("SAVEPOINT \"_orkestrel_migration\"");
|
|
2326
2211
|
try {
|
|
2327
2212
|
this.#applyPlan(database, owned);
|
|
2328
2213
|
if (owned.metadata !== void 0) this.#writeMetadata(database, owned.metadata);
|
|
2329
|
-
database.
|
|
2214
|
+
database.execute("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
2330
2215
|
} catch (error) {
|
|
2331
2216
|
try {
|
|
2332
|
-
database.
|
|
2217
|
+
database.execute("ROLLBACK TO SAVEPOINT \"_orkestrel_migration\"");
|
|
2333
2218
|
} finally {
|
|
2334
|
-
database.
|
|
2219
|
+
database.execute("RELEASE SAVEPOINT \"_orkestrel_migration\"");
|
|
2335
2220
|
}
|
|
2336
2221
|
throw error;
|
|
2337
2222
|
}
|
|
@@ -2407,13 +2292,13 @@ var SQLiteDriver = class {
|
|
|
2407
2292
|
});
|
|
2408
2293
|
}
|
|
2409
2294
|
#applyPlan(database, input) {
|
|
2410
|
-
for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.
|
|
2295
|
+
for (const step of input.plan.steps) for (const sql of stepToSQL(step)) database.execute(sql);
|
|
2411
2296
|
}
|
|
2412
2297
|
};
|
|
2413
2298
|
//#endregion
|
|
2414
2299
|
//#region src/server/factories.ts
|
|
2415
2300
|
/**
|
|
2416
|
-
*
|
|
2301
|
+
* Creates a persistent JSON-file {@link DriverInterface} for the core database layer.
|
|
2417
2302
|
*
|
|
2418
2303
|
* @remarks
|
|
2419
2304
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
@@ -2421,9 +2306,10 @@ var SQLiteDriver = class {
|
|
|
2421
2306
|
* `Table` / `Query` API is unchanged; only where the bytes live changes.
|
|
2422
2307
|
* The driver is the reference `MemoryDriver` plus JSON-file persistence: `open` loads
|
|
2423
2308
|
* the file, every mutation flushes the whole store back, and querying runs through
|
|
2424
|
-
* the core engine
|
|
2425
|
-
* `
|
|
2426
|
-
*
|
|
2309
|
+
* the core engine's `matchesQuery`. The driver implements the native `stream`
|
|
2310
|
+
* hook and neither `records` nor `aggregate`, so the engine answers every query
|
|
2311
|
+
* on either path. A missing, corrupt, or wrong-shaped file starts empty rather
|
|
2312
|
+
* than throwing.
|
|
2427
2313
|
*
|
|
2428
2314
|
* @param path - The JSON file path data is loaded from and flushed to
|
|
2429
2315
|
* @returns A {@link DriverInterface} backed by a JSON file
|
|
@@ -2445,7 +2331,7 @@ function createJSONDriver(path) {
|
|
|
2445
2331
|
return new JSONDriver(path);
|
|
2446
2332
|
}
|
|
2447
2333
|
/**
|
|
2448
|
-
*
|
|
2334
|
+
* Creates a trusted-mode SQLite {@link DriverInterface} for the core database layer.
|
|
2449
2335
|
*
|
|
2450
2336
|
* @remarks
|
|
2451
2337
|
* Pass it to `createDatabase` from `@orkestrel/database` to run the typed
|
|
@@ -2494,10 +2380,10 @@ exports.compileColumnSQL = compileColumnSQL;
|
|
|
2494
2380
|
exports.compileConditionSQL = compileConditionSQL;
|
|
2495
2381
|
exports.compileFieldSQL = compileFieldSQL;
|
|
2496
2382
|
exports.compileJSONTypeSQL = compileJSONTypeSQL;
|
|
2497
|
-
exports.
|
|
2498
|
-
exports.
|
|
2383
|
+
exports.compileOrderSQL = compileOrderSQL;
|
|
2384
|
+
exports.compilePageSQL = compilePageSQL;
|
|
2499
2385
|
exports.compileQuerySQL = compileQuerySQL;
|
|
2500
|
-
exports.
|
|
2386
|
+
exports.compileWhereSQL = compileWhereSQL;
|
|
2501
2387
|
exports.createJSONDriver = createJSONDriver;
|
|
2502
2388
|
exports.createSQLiteDriver = createSQLiteDriver;
|
|
2503
2389
|
exports.decodeRow = decodeRow;
|
|
@@ -2505,9 +2391,7 @@ exports.decodeValue = decodeValue;
|
|
|
2505
2391
|
exports.deriveSQLiteIndexName = deriveSQLiteIndexName;
|
|
2506
2392
|
exports.encodeRow = encodeRow;
|
|
2507
2393
|
exports.encodeValue = encodeValue;
|
|
2508
|
-
exports.escapeLike = escapeLike;
|
|
2509
2394
|
exports.extractValues = extractValues;
|
|
2510
|
-
exports.findColumnStorage = findColumnStorage;
|
|
2511
2395
|
exports.inferValueStorage = inferValueStorage;
|
|
2512
2396
|
exports.matchesAbsentPath = matchesAbsentPath;
|
|
2513
2397
|
exports.matchesAggregateExactly = matchesAggregateExactly;
|