@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/core/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { cloneJSONRecord, cloneJSONValue, compileGuard, compileSchema, createCon
|
|
|
2
2
|
import { Emitter } from "@orkestrel/emitter";
|
|
3
3
|
//#region src/core/constants.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Supplies the primary-key column assumed when {@link PrimaryMap} does not name one.
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
8
|
* `id` is the convention IndexedDB (`keyPath: 'id'`) and SQL (`id` / rowid) both
|
|
@@ -10,12 +10,12 @@ import { Emitter } from "@orkestrel/emitter";
|
|
|
10
10
|
*/
|
|
11
11
|
var DEFAULT_PRIMARY = "id";
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Sets the longest `LIKE` / `GLOB` pattern the wildcard matcher accepts before rejecting it.
|
|
14
14
|
*
|
|
15
15
|
* @remarks
|
|
16
|
-
* A
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* A `LIKE` / `GLOB` pattern is a caller-supplied operand, so
|
|
17
|
+
* `matchesLikePattern` / `matchesGlobPattern` run patterns this package cannot
|
|
18
|
+
* trust. The matcher is the LINEAR greedy two-pointer wildcard match — never a
|
|
19
19
|
* backtracking regex (`.*`-segments-separated-by-literals against a long input is the
|
|
20
20
|
* catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
|
|
21
21
|
* pattern). Capping the pattern length bounds that pattern factor, leaving a match
|
|
@@ -23,10 +23,83 @@ var DEFAULT_PRIMARY = "id";
|
|
|
23
23
|
* `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
|
|
24
24
|
*/
|
|
25
25
|
var MAX_PATTERN_LENGTH = 1024;
|
|
26
|
+
/**
|
|
27
|
+
* Describes the `users` table the driver-conformance battery opens — keyed by the default
|
|
28
|
+
* `id` primary column.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* `age` is optional and `meta` is a declared `json` column, so the battery's
|
|
32
|
+
* nested-round-trip phase is fair to a typed-column backend: a SQL driver
|
|
33
|
+
* persists only declared columns, while a schemaless backend ignores the
|
|
34
|
+
* declarations entirely.
|
|
35
|
+
*/
|
|
36
|
+
var CONFORMANCE_USERS_SCHEMA = Object.freeze({
|
|
37
|
+
name: "users",
|
|
38
|
+
primary: "id",
|
|
39
|
+
columns: Object.freeze([
|
|
40
|
+
{
|
|
41
|
+
name: "id",
|
|
42
|
+
storage: "text",
|
|
43
|
+
optional: false,
|
|
44
|
+
nullable: false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "name",
|
|
48
|
+
storage: "text",
|
|
49
|
+
optional: false,
|
|
50
|
+
nullable: false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "age",
|
|
54
|
+
storage: "integer",
|
|
55
|
+
optional: true,
|
|
56
|
+
nullable: false
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "meta",
|
|
60
|
+
storage: "json",
|
|
61
|
+
optional: true,
|
|
62
|
+
nullable: false
|
|
63
|
+
}
|
|
64
|
+
]),
|
|
65
|
+
indexes: Object.freeze([])
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Describes the `posts` table the driver-conformance battery opens — keyed by a non-`id`
|
|
69
|
+
* `slug` primary column.
|
|
70
|
+
*
|
|
71
|
+
* @remarks
|
|
72
|
+
* Pairs with {@link CONFORMANCE_USERS_SCHEMA} so one battery exercises both
|
|
73
|
+
* primary-key shapes: the default `id` and an explicit override.
|
|
74
|
+
*/
|
|
75
|
+
var CONFORMANCE_POSTS_SCHEMA = Object.freeze({
|
|
76
|
+
name: "posts",
|
|
77
|
+
primary: "slug",
|
|
78
|
+
columns: Object.freeze([{
|
|
79
|
+
name: "slug",
|
|
80
|
+
storage: "text",
|
|
81
|
+
optional: false,
|
|
82
|
+
nullable: false
|
|
83
|
+
}, {
|
|
84
|
+
name: "title",
|
|
85
|
+
storage: "text",
|
|
86
|
+
optional: false,
|
|
87
|
+
nullable: false
|
|
88
|
+
}]),
|
|
89
|
+
indexes: Object.freeze([])
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Holds the fixed two-table schema every driver-conformance phase opens.
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
95
|
+
* Each phase mints a fresh driver and opens this exact schema, so a finding
|
|
96
|
+
* names a violated invariant rather than a setup difference between phases.
|
|
97
|
+
*/
|
|
98
|
+
var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
|
|
26
99
|
//#endregion
|
|
27
100
|
//#region src/core/errors.ts
|
|
28
101
|
/**
|
|
29
|
-
*
|
|
102
|
+
* Represents an error thrown by the database layer.
|
|
30
103
|
*
|
|
31
104
|
* @remarks
|
|
32
105
|
* Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
|
|
@@ -37,7 +110,7 @@ var MAX_PATTERN_LENGTH = 1024;
|
|
|
37
110
|
* `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
|
|
38
111
|
* driver that violates a {@link DriverInterface} invariant, thrown by the
|
|
39
112
|
* `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
|
|
40
|
-
* fault surfaced by a driver seam —
|
|
113
|
+
* fault surfaced by a driver seam — for example a filesystem failure while
|
|
41
114
|
* persisting (`DRIVER`) — as opposed to expected domain conditions, which
|
|
42
115
|
* keep their specific codes.
|
|
43
116
|
*/
|
|
@@ -52,10 +125,10 @@ var DatabaseError = class extends Error {
|
|
|
52
125
|
}
|
|
53
126
|
};
|
|
54
127
|
/**
|
|
55
|
-
*
|
|
128
|
+
* Narrows an unknown caught value to a {@link DatabaseError}.
|
|
56
129
|
*
|
|
57
130
|
* @param value - The value to test (typically a `catch` binding)
|
|
58
|
-
* @returns
|
|
131
|
+
* @returns True if `value` is a {@link DatabaseError}; false otherwise
|
|
59
132
|
*
|
|
60
133
|
* @example
|
|
61
134
|
* ```ts
|
|
@@ -72,43 +145,19 @@ function isDatabaseError(value) {
|
|
|
72
145
|
//#endregion
|
|
73
146
|
//#region src/core/validators.ts
|
|
74
147
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* @remarks
|
|
78
|
-
* A present `limit` or `offset` must be a finite nonnegative integer; zero is
|
|
79
|
-
* valid. Validation is deterministic (`limit` before `offset`). Non-finite
|
|
80
|
-
* values are rendered as strings in error context so JSON serialization cannot
|
|
81
|
-
* collapse `NaN` or infinity to `null`.
|
|
82
|
-
*
|
|
83
|
-
* @param input - The portable query whose paging fields to validate
|
|
84
|
-
* @throws {@link DatabaseError} `VALIDATION` when a paging field is invalid
|
|
85
|
-
*/
|
|
86
|
-
function validatePage(input) {
|
|
87
|
-
const limit = input?.limit;
|
|
88
|
-
if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
|
|
89
|
-
field: "limit",
|
|
90
|
-
value: Number.isFinite(limit) ? limit : String(limit)
|
|
91
|
-
});
|
|
92
|
-
const offset = input?.offset;
|
|
93
|
-
if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
|
|
94
|
-
field: "offset",
|
|
95
|
-
value: Number.isFinite(offset) ? offset : String(offset)
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Test whether a value is a usable database key.
|
|
148
|
+
* Checks whether a value is a usable database key.
|
|
100
149
|
*
|
|
101
150
|
* @param value - The value to test
|
|
102
|
-
* @returns
|
|
151
|
+
* @returns True if `value` is a string or a finite number; false otherwise
|
|
103
152
|
*/
|
|
104
153
|
function isKey(value) {
|
|
105
154
|
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
106
155
|
}
|
|
107
156
|
/**
|
|
108
|
-
*
|
|
157
|
+
* Checks whether a value is a portable column schema.
|
|
109
158
|
*
|
|
110
159
|
* @param value - The value to test
|
|
111
|
-
* @returns
|
|
160
|
+
* @returns True if `value` is a complete {@link ColumnSchema}; false otherwise
|
|
112
161
|
*/
|
|
113
162
|
function isColumnSchema(value) {
|
|
114
163
|
try {
|
|
@@ -120,10 +169,10 @@ function isColumnSchema(value) {
|
|
|
120
169
|
}
|
|
121
170
|
}
|
|
122
171
|
/**
|
|
123
|
-
*
|
|
172
|
+
* Checks whether a value is a portable table schema.
|
|
124
173
|
*
|
|
125
174
|
* @param value - The value to test
|
|
126
|
-
* @returns
|
|
175
|
+
* @returns True if `value` is a complete {@link TableSchema}; false otherwise
|
|
127
176
|
*/
|
|
128
177
|
function isTableSchema(value) {
|
|
129
178
|
try {
|
|
@@ -139,10 +188,10 @@ function isTableSchema(value) {
|
|
|
139
188
|
}
|
|
140
189
|
}
|
|
141
190
|
/**
|
|
142
|
-
*
|
|
191
|
+
* Checks whether a value is a complete portable driver schema.
|
|
143
192
|
*
|
|
144
193
|
* @param value - The value to test
|
|
145
|
-
* @returns
|
|
194
|
+
* @returns True if `value` is a table-schema collection with unique table names; false otherwise
|
|
146
195
|
*/
|
|
147
196
|
function isDriverSchema(value) {
|
|
148
197
|
try {
|
|
@@ -155,10 +204,10 @@ function isDriverSchema(value) {
|
|
|
155
204
|
}
|
|
156
205
|
}
|
|
157
206
|
/**
|
|
158
|
-
*
|
|
207
|
+
* Checks whether a value is one ordered migration step.
|
|
159
208
|
*
|
|
160
209
|
* @param value - The value to test
|
|
161
|
-
* @returns
|
|
210
|
+
* @returns True if `value` is a complete {@link MigrationStep}; false otherwise
|
|
162
211
|
*/
|
|
163
212
|
function isMigrationStep(value) {
|
|
164
213
|
try {
|
|
@@ -179,10 +228,10 @@ function isMigrationStep(value) {
|
|
|
179
228
|
}
|
|
180
229
|
}
|
|
181
230
|
/**
|
|
182
|
-
*
|
|
231
|
+
* Checks whether a value is an ordered migration plan.
|
|
183
232
|
*
|
|
184
233
|
* @param value - The value to test
|
|
185
|
-
* @returns
|
|
234
|
+
* @returns True if `value` is a complete {@link Migration}; false otherwise
|
|
186
235
|
*/
|
|
187
236
|
function isMigration(value) {
|
|
188
237
|
try {
|
|
@@ -194,10 +243,10 @@ function isMigration(value) {
|
|
|
194
243
|
}
|
|
195
244
|
}
|
|
196
245
|
/**
|
|
197
|
-
*
|
|
246
|
+
* Checks whether a value is persisted driver metadata.
|
|
198
247
|
*
|
|
199
248
|
* @param value - The value to test
|
|
200
|
-
* @returns
|
|
249
|
+
* @returns True if `value` is complete {@link DriverMetadata}; false otherwise
|
|
201
250
|
*/
|
|
202
251
|
function isDriverMetadata(value) {
|
|
203
252
|
try {
|
|
@@ -209,10 +258,10 @@ function isDriverMetadata(value) {
|
|
|
209
258
|
}
|
|
210
259
|
}
|
|
211
260
|
/**
|
|
212
|
-
*
|
|
261
|
+
* Checks whether a value is one atomic migration request.
|
|
213
262
|
*
|
|
214
263
|
* @param value - The value to test
|
|
215
|
-
* @returns
|
|
264
|
+
* @returns True if `value` is a complete {@link MigrationInput}; false otherwise
|
|
216
265
|
*/
|
|
217
266
|
function isMigrationInput(value) {
|
|
218
267
|
try {
|
|
@@ -226,7 +275,7 @@ function isMigrationInput(value) {
|
|
|
226
275
|
//#endregion
|
|
227
276
|
//#region src/core/cloners.ts
|
|
228
277
|
/**
|
|
229
|
-
*
|
|
278
|
+
* Clones unknown driver metadata into a distinct deeply frozen snapshot.
|
|
230
279
|
*
|
|
231
280
|
* @param value - Unknown metadata
|
|
232
281
|
* @returns Owned driver metadata
|
|
@@ -245,7 +294,7 @@ function cloneDriverMetadata(value) {
|
|
|
245
294
|
}
|
|
246
295
|
}
|
|
247
296
|
/**
|
|
248
|
-
*
|
|
297
|
+
* Clones unknown driver schema into a distinct deeply frozen snapshot.
|
|
249
298
|
*
|
|
250
299
|
* @param value - Unknown table schema collection
|
|
251
300
|
* @returns Owned driver schema
|
|
@@ -264,7 +313,7 @@ function cloneDriverSchema(value) {
|
|
|
264
313
|
}
|
|
265
314
|
}
|
|
266
315
|
/**
|
|
267
|
-
*
|
|
316
|
+
* Clones unknown migration input into a distinct deeply frozen snapshot.
|
|
268
317
|
*
|
|
269
318
|
* @param value - Unknown migration input
|
|
270
319
|
* @returns Owned migration input
|
|
@@ -285,8 +334,32 @@ function cloneMigrationInput(value) {
|
|
|
285
334
|
//#endregion
|
|
286
335
|
//#region src/core/helpers.ts
|
|
287
336
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
337
|
+
* Validates the paging fields of a portable query.
|
|
338
|
+
*
|
|
339
|
+
* @remarks
|
|
340
|
+
* A present `limit` or `offset` must be a finite nonnegative integer; zero is
|
|
341
|
+
* valid. Validation is deterministic (`limit` before `offset`). Non-finite
|
|
342
|
+
* values are rendered as strings in error context so JSON serialization cannot
|
|
343
|
+
* collapse `NaN` or infinity to `null`.
|
|
344
|
+
*
|
|
345
|
+
* @param input - The portable query whose paging fields to validate
|
|
346
|
+
* @throws {@link DatabaseError} `VALIDATION` when a paging field is invalid
|
|
347
|
+
*/
|
|
348
|
+
function validatePage(input) {
|
|
349
|
+
const limit = input?.limit;
|
|
350
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
|
|
351
|
+
field: "limit",
|
|
352
|
+
value: Number.isFinite(limit) ? limit : String(limit)
|
|
353
|
+
});
|
|
354
|
+
const offset = input?.offset;
|
|
355
|
+
if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
|
|
356
|
+
field: "offset",
|
|
357
|
+
value: Number.isFinite(offset) ? offset : String(offset)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Compares two arbitrary values under one total order — the comparator behind
|
|
362
|
+
* sorting and the range operators.
|
|
290
363
|
*
|
|
291
364
|
* @remarks
|
|
292
365
|
* Values of different types order by a fixed type rank (`undefined` < `null` <
|
|
@@ -310,13 +383,14 @@ function compareValues(left, right) {
|
|
|
310
383
|
return 0;
|
|
311
384
|
}
|
|
312
385
|
/**
|
|
313
|
-
*
|
|
314
|
-
* checks and any test/fixture that needs "same data", not
|
|
386
|
+
* Compares two values structurally by SameValueZero leaves — the comparator
|
|
387
|
+
* behind conformance checks and any test/fixture that needs "same data", not
|
|
388
|
+
* "same reference".
|
|
315
389
|
*
|
|
316
390
|
* @remarks
|
|
317
391
|
* Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
|
|
318
392
|
* Arrays compare by index (same length, every element `equalsValue`). Plain
|
|
319
|
-
* records (
|
|
393
|
+
* records (through `isRecord`) compare by their OWN enumerable keys: same key
|
|
320
394
|
* COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
|
|
321
395
|
* with a `equalsValue` value — so a key present with value `undefined` is NOT
|
|
322
396
|
* equal to that key being absent (both differ in `Object.keys` membership).
|
|
@@ -327,7 +401,7 @@ function compareValues(left, right) {
|
|
|
327
401
|
*
|
|
328
402
|
* @param left - The left value
|
|
329
403
|
* @param right - The right value
|
|
330
|
-
* @returns
|
|
404
|
+
* @returns True if `left` and `right` are structurally equal; false otherwise
|
|
331
405
|
*
|
|
332
406
|
* @example
|
|
333
407
|
* ```ts
|
|
@@ -384,46 +458,15 @@ function equalsValue(left, right) {
|
|
|
384
458
|
}
|
|
385
459
|
}
|
|
386
460
|
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* @remarks
|
|
390
|
-
* Every query character must appear in order in the value, but the characters
|
|
391
|
-
* do not need to be contiguous. Query characters are literal, including
|
|
392
|
-
* whitespace. Matching applies JavaScript `toLowerCase()` to both inputs
|
|
393
|
-
* without locale-specific folding or Unicode normalization. An empty query
|
|
394
|
-
* matches every value.
|
|
395
|
-
*
|
|
396
|
-
* @param value - The text searched for the query's characters
|
|
397
|
-
* @param query - The characters that must all appear in order
|
|
398
|
-
* @returns Whether the case-folded query is a subsequence of the case-folded value
|
|
399
|
-
*
|
|
400
|
-
* @example
|
|
401
|
-
* ```ts
|
|
402
|
-
* matchesFuzzy('Database', 'dbe') // true
|
|
403
|
-
* matchesFuzzy('Database', 'abd') // false
|
|
404
|
-
* ```
|
|
405
|
-
*/
|
|
406
|
-
function matchesFuzzy(value, query) {
|
|
407
|
-
const folded = value.toLowerCase();
|
|
408
|
-
const wanted = query.toLowerCase();
|
|
409
|
-
let cursor = 0;
|
|
410
|
-
for (const char of wanted) {
|
|
411
|
-
const found = folded.indexOf(char, cursor);
|
|
412
|
-
if (found === -1) return false;
|
|
413
|
-
cursor = found + 1;
|
|
414
|
-
}
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
|
|
461
|
+
* Matches a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
|
|
419
462
|
* engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
|
|
420
463
|
*
|
|
421
464
|
* @remarks
|
|
422
465
|
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
|
|
423
466
|
* `.*` segments separated by literals, matched against a long non-matching input, blow
|
|
424
|
-
* up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
|
|
425
|
-
*
|
|
426
|
-
*
|
|
467
|
+
* up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
|
|
468
|
+
* while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
|
|
469
|
+
* trust. So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
|
|
427
470
|
* the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
|
|
428
471
|
* that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
|
|
429
472
|
* never the exponential / polynomial backtracking a regex would do. The pattern length
|
|
@@ -442,7 +485,7 @@ function matchesFuzzy(value, query) {
|
|
|
442
485
|
* @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
|
|
443
486
|
* @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
|
|
444
487
|
* @param fold - Whether to match case-INSENSITIVELY (`LIKE` folds; `GLOB` does not)
|
|
445
|
-
* @returns
|
|
488
|
+
* @returns True if `value` matches `pattern`; false otherwise
|
|
446
489
|
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
447
490
|
*/
|
|
448
491
|
function matchesWildcardPattern(value, pattern, any, single, fold) {
|
|
@@ -474,14 +517,55 @@ function matchesWildcardPattern(value, pattern, any, single, fold) {
|
|
|
474
517
|
while (pi < needle.length && needle[pi] === any) pi += 1;
|
|
475
518
|
return pi === needle.length;
|
|
476
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Matches a value against a SQL `LIKE` pattern, folding case.
|
|
522
|
+
*
|
|
523
|
+
* @remarks
|
|
524
|
+
* `%` matches any run of characters (including none) and `_` matches exactly one
|
|
525
|
+
* character; every other pattern character matches itself literally. Runs on
|
|
526
|
+
* {@link matchesWildcardPattern}, so the match is linear in the value length and
|
|
527
|
+
* the pattern is capped at {@link MAX_PATTERN_LENGTH}.
|
|
528
|
+
*
|
|
529
|
+
* @param value - The value to test
|
|
530
|
+
* @param pattern - The `LIKE` pattern
|
|
531
|
+
* @returns True if `value` matches `pattern` under case folding; false otherwise
|
|
532
|
+
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* matchesLikePattern('Hello', 'h%o') // true — `%` spans any run, and case folds
|
|
537
|
+
* matchesLikePattern('Hello', 'h_llo') // true — `_` matches exactly one character
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
477
540
|
function matchesLikePattern(value, pattern) {
|
|
478
541
|
return matchesWildcardPattern(value, pattern, "%", "_", true);
|
|
479
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Matches a value against a `GLOB` pattern, preserving case.
|
|
545
|
+
*
|
|
546
|
+
* @remarks
|
|
547
|
+
* `*` matches any run of characters (including none) and `?` matches exactly one
|
|
548
|
+
* character; every other pattern character matches itself literally, so a
|
|
549
|
+
* character class such as `[a-z]` is NOT interpreted. Runs on
|
|
550
|
+
* {@link matchesWildcardPattern}, so the match is linear in the value length and
|
|
551
|
+
* the pattern is capped at {@link MAX_PATTERN_LENGTH}.
|
|
552
|
+
*
|
|
553
|
+
* @param value - The value to test
|
|
554
|
+
* @param pattern - The `GLOB` pattern
|
|
555
|
+
* @returns True if `value` matches `pattern` case-sensitively; false otherwise
|
|
556
|
+
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```ts
|
|
560
|
+
* matchesGlobPattern('hello', 'h*o') // true — `*` spans any run
|
|
561
|
+
* matchesGlobPattern('Hello', 'h*o') // false — `GLOB` is case-sensitive
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
480
564
|
function matchesGlobPattern(value, pattern) {
|
|
481
565
|
return matchesWildcardPattern(value, pattern, "*", "?", false);
|
|
482
566
|
}
|
|
483
567
|
/**
|
|
484
|
-
*
|
|
568
|
+
* Evaluates one {@link Condition} against a row — the per-operator predicate.
|
|
485
569
|
*
|
|
486
570
|
* @remarks
|
|
487
571
|
* Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
|
|
@@ -495,11 +579,11 @@ function matchesGlobPattern(value, pattern) {
|
|
|
495
579
|
* on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
|
|
496
580
|
* anything under the old rank-based comparison). `like` / `glob` / `starts` /
|
|
497
581
|
* `ends` match only strings; `absent` / `present` test nullishness. Total — a
|
|
498
|
-
* type mismatch is
|
|
582
|
+
* type mismatch is a non-match.
|
|
499
583
|
*
|
|
500
584
|
* @param row - The row to test
|
|
501
585
|
* @param condition - The condition to apply
|
|
502
|
-
* @returns
|
|
586
|
+
* @returns True if the row satisfies the condition; false otherwise
|
|
503
587
|
*/
|
|
504
588
|
function matchesCondition(row, condition) {
|
|
505
589
|
const value = resolveField(row, condition.column);
|
|
@@ -524,7 +608,7 @@ function matchesCondition(row, condition) {
|
|
|
524
608
|
}
|
|
525
609
|
}
|
|
526
610
|
/**
|
|
527
|
-
*
|
|
611
|
+
* Folds a row through a list of conditions, joining each by its connector.
|
|
528
612
|
*
|
|
529
613
|
* @remarks
|
|
530
614
|
* Evaluated left-to-right: the first condition seeds the result, and each later
|
|
@@ -534,7 +618,7 @@ function matchesCondition(row, condition) {
|
|
|
534
618
|
*
|
|
535
619
|
* @param row - The row to test
|
|
536
620
|
* @param conditions - The conditions to fold
|
|
537
|
-
* @returns
|
|
621
|
+
* @returns True if the row satisfies the combined conditions; false otherwise
|
|
538
622
|
*/
|
|
539
623
|
function matchesQuery(row, conditions) {
|
|
540
624
|
let result = true;
|
|
@@ -549,7 +633,7 @@ function matchesQuery(row, conditions) {
|
|
|
549
633
|
return result;
|
|
550
634
|
}
|
|
551
635
|
/**
|
|
552
|
-
*
|
|
636
|
+
* Filters rows by a list of conditions — the shared basis for a table's count
|
|
553
637
|
* and aggregate paths (no sort/page, unlike {@link applyQuery}).
|
|
554
638
|
*
|
|
555
639
|
* @remarks
|
|
@@ -573,7 +657,7 @@ function filterRows(rows, conditions) {
|
|
|
573
657
|
return rows.filter((row) => matchesQuery(row, conditions));
|
|
574
658
|
}
|
|
575
659
|
/**
|
|
576
|
-
*
|
|
660
|
+
* Sorts rows by an ordering specification, leaving the input untouched.
|
|
577
661
|
*
|
|
578
662
|
* @remarks
|
|
579
663
|
* Applies the terms in priority order — the first term that distinguishes two
|
|
@@ -595,7 +679,7 @@ function sortRows(rows, order) {
|
|
|
595
679
|
return sorted;
|
|
596
680
|
}
|
|
597
681
|
/**
|
|
598
|
-
*
|
|
682
|
+
* Applies a {@link QueryInput} to rows — filter, then sort, then page.
|
|
599
683
|
*
|
|
600
684
|
* @remarks
|
|
601
685
|
* The whole portable read pipeline in one place: conditions filter, `order`
|
|
@@ -620,7 +704,7 @@ function applyQuery(rows, input) {
|
|
|
620
704
|
return result;
|
|
621
705
|
}
|
|
622
706
|
/**
|
|
623
|
-
*
|
|
707
|
+
* Computes an aggregate over a column across rows.
|
|
624
708
|
*
|
|
625
709
|
* @remarks
|
|
626
710
|
* `count` returns the row count. The numeric aggregates coerce each cell with
|
|
@@ -649,7 +733,7 @@ function computeAggregate(rows, operation, column) {
|
|
|
649
733
|
return operation === "minimum" ? Math.min(...numbers) : Math.max(...numbers);
|
|
650
734
|
}
|
|
651
735
|
/**
|
|
652
|
-
*
|
|
736
|
+
* Reads a row's primary key from a column, when it is a usable {@link Key}.
|
|
653
737
|
*
|
|
654
738
|
* @param row - The row to read
|
|
655
739
|
* @param column - The primary-key column name
|
|
@@ -660,7 +744,7 @@ function extractKey(row, column) {
|
|
|
660
744
|
return isKey(value) ? value : void 0;
|
|
661
745
|
}
|
|
662
746
|
/**
|
|
663
|
-
*
|
|
747
|
+
* Returns a fresh row whose primary column is authoritatively bound to its storage key.
|
|
664
748
|
*
|
|
665
749
|
* @param row - The caller row
|
|
666
750
|
* @param primary - The primary column
|
|
@@ -674,7 +758,7 @@ function bindRowKey(row, primary, key) {
|
|
|
674
758
|
};
|
|
675
759
|
}
|
|
676
760
|
/**
|
|
677
|
-
*
|
|
761
|
+
* Maps a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
|
|
678
762
|
* value a `TableSchema` carries so a native backend can declare a real column.
|
|
679
763
|
*
|
|
680
764
|
* @remarks
|
|
@@ -698,7 +782,7 @@ function bindRowKey(row, primary, key) {
|
|
|
698
782
|
* ```
|
|
699
783
|
*/
|
|
700
784
|
function shapeToColumnStorage(shape) {
|
|
701
|
-
switch (shape.
|
|
785
|
+
switch (shape.category) {
|
|
702
786
|
case "string": return "text";
|
|
703
787
|
case "number": return shape.integer === true ? "integer" : "real";
|
|
704
788
|
case "boolean": return "boolean";
|
|
@@ -717,7 +801,7 @@ function shapeToColumnStorage(shape) {
|
|
|
717
801
|
}
|
|
718
802
|
}
|
|
719
803
|
/**
|
|
720
|
-
*
|
|
804
|
+
* Projects one contract shape into a portable column schema.
|
|
721
805
|
*
|
|
722
806
|
* @param name - The column name
|
|
723
807
|
* @param shape - The column contract shape
|
|
@@ -733,7 +817,55 @@ function shapeToColumnSchema(name, shape) {
|
|
|
733
817
|
};
|
|
734
818
|
}
|
|
735
819
|
/**
|
|
736
|
-
*
|
|
820
|
+
* Reads one flat column's declaration out of a table schema.
|
|
821
|
+
*
|
|
822
|
+
* @remarks
|
|
823
|
+
* The single lookup behind every declared-column question — storage type,
|
|
824
|
+
* optionality, and nullability all come off the returned {@link ColumnSchema},
|
|
825
|
+
* so a caller that needs more than one of them reads them from one result. A
|
|
826
|
+
* nested {@link FieldPath} names no declared column, so resolve the path's head
|
|
827
|
+
* before calling. A schema that does not declare the column returns `undefined`.
|
|
828
|
+
*
|
|
829
|
+
* @param name - The flat column name
|
|
830
|
+
* @param schema - The table's schema
|
|
831
|
+
* @returns The column's {@link ColumnSchema}, or `undefined` when the schema does not declare it
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```ts
|
|
835
|
+
* findColumn('age', schema)?.storage // 'integer'
|
|
836
|
+
* findColumn('absent', schema) // undefined
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
function findColumn(name, schema) {
|
|
840
|
+
return schema.columns.find((candidate) => candidate.name === name);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Resolves the primary-key column one table keys its rows by.
|
|
844
|
+
*
|
|
845
|
+
* @remarks
|
|
846
|
+
* A table absent from the {@link PrimaryMap} keys its rows by
|
|
847
|
+
* {@link DEFAULT_PRIMARY}, so this is total over any table name.
|
|
848
|
+
*
|
|
849
|
+
* @param primary - The per-table primary-key overrides
|
|
850
|
+
* @param name - The table name
|
|
851
|
+
* @returns The table's primary-key column
|
|
852
|
+
*
|
|
853
|
+
* @example
|
|
854
|
+
* ```ts
|
|
855
|
+
* resolvePrimary({ posts: 'slug' }, 'posts') // 'slug'
|
|
856
|
+
* resolvePrimary({ posts: 'slug' }, 'users') // 'id' — the default primary
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
function resolvePrimary(primary, name) {
|
|
860
|
+
return primary[name] ?? "id";
|
|
861
|
+
}
|
|
862
|
+
function requireColumns(tables, name) {
|
|
863
|
+
const columns = tables[name];
|
|
864
|
+
if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
|
|
865
|
+
return columns;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Throws when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
|
|
737
869
|
* abort gate checked at operation boundaries and between streamed rows.
|
|
738
870
|
*
|
|
739
871
|
* @remarks
|
|
@@ -761,7 +893,7 @@ function checkAbort(signal) {
|
|
|
761
893
|
if (signal?.aborted) throw new DatabaseError("ABORTED", "Operation aborted", { reason: signal.reason });
|
|
762
894
|
}
|
|
763
895
|
/**
|
|
764
|
-
*
|
|
896
|
+
* Diffs a deployed and a declared table set structurally into a {@link Migration}
|
|
765
897
|
* plan.
|
|
766
898
|
*
|
|
767
899
|
* @remarks
|
|
@@ -778,13 +910,12 @@ function checkAbort(signal) {
|
|
|
778
910
|
*
|
|
779
911
|
* A column present in BOTH schemas under the same name but with a different
|
|
780
912
|
* `storage`, `optional`, or `nullable` value throws a `MIGRATION`
|
|
781
|
-
* {@link DatabaseError} naming the
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
* plans, never a single implicit "alter" step.
|
|
913
|
+
* {@link DatabaseError} naming the table, the column, and the from→to
|
|
914
|
+
* difference — a name-only diff would otherwise silently produce NO step for
|
|
915
|
+
* the drift, and versioned reconciliation would stamp over it. There is no
|
|
916
|
+
* automatic in-place type-change step: the manual path is to add a new column,
|
|
917
|
+
* copy/convert the data at the application layer, then remove the old column —
|
|
918
|
+
* two separate plans, never a single implicit "alter" step.
|
|
788
919
|
*
|
|
789
920
|
* @param deployed - The table schemas currently applied
|
|
790
921
|
* @param declared - The table schemas the caller wants applied
|
|
@@ -892,7 +1023,7 @@ function planMigration(deployed, declared, from = 0, to = 1) {
|
|
|
892
1023
|
} }).plan;
|
|
893
1024
|
}
|
|
894
1025
|
/**
|
|
895
|
-
*
|
|
1026
|
+
* Projects migration steps sequentially over a canonical validated owned schema.
|
|
896
1027
|
* Adding a required non-null column to an existing table rejects with
|
|
897
1028
|
* `MIGRATION`; optional-only and nullable-only additions remain portable.
|
|
898
1029
|
*
|
|
@@ -991,7 +1122,7 @@ function projectMigrationSchema(schema, steps) {
|
|
|
991
1122
|
}
|
|
992
1123
|
}
|
|
993
1124
|
/**
|
|
994
|
-
*
|
|
1125
|
+
* Canonicalizes an unknown driver schema into a distinct deeply frozen snapshot.
|
|
995
1126
|
*
|
|
996
1127
|
* @remarks
|
|
997
1128
|
* Table and column lists are sorted by name. The index list is sorted by the
|
|
@@ -1013,11 +1144,11 @@ function normalizeDriverSchema(value) {
|
|
|
1013
1144
|
return cloneDriverSchema(tables);
|
|
1014
1145
|
}
|
|
1015
1146
|
/**
|
|
1016
|
-
*
|
|
1147
|
+
* Applies one table's {@link MigrationStep}s to its rows — a pure row transform.
|
|
1017
1148
|
*
|
|
1018
1149
|
* @remarks
|
|
1019
1150
|
* `column.remove` drops that field from every row (a fresh copy — inputs are
|
|
1020
|
-
* never mutated
|
|
1151
|
+
* never mutated); `column.add` leaves rows as-is (an absent field
|
|
1021
1152
|
* reads as `undefined`, backfill is application policy). `table.add` /
|
|
1022
1153
|
* `table.remove` / `index.add` / `index.remove` are no-ops here (they operate
|
|
1023
1154
|
* on storage shape, not row shape). Steps for tables other than the one
|
|
@@ -1044,7 +1175,7 @@ function migrateRows(rows, steps) {
|
|
|
1044
1175
|
});
|
|
1045
1176
|
}
|
|
1046
1177
|
/**
|
|
1047
|
-
*
|
|
1178
|
+
* Walks the driver-conformance battery against a fresh {@link DriverInterface}
|
|
1048
1179
|
* per phase, yielding one {@link ConformanceFinding} per violated invariant —
|
|
1049
1180
|
* the shared invariant suite every backend (in-memory, SQLite, IndexedDB)
|
|
1050
1181
|
* must uphold to be a drop-in {@link DriverInterface}.
|
|
@@ -1055,26 +1186,26 @@ function migrateRows(rows, steps) {
|
|
|
1055
1186
|
* driver's own README. Opens a fixed two-table schema (`users` keyed by the
|
|
1056
1187
|
* default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
|
|
1057
1188
|
* fresh for each phase so failures stay isolated, verifies: `open`/`close`;
|
|
1058
|
-
* `read` of a missing key returns `undefined`; `write`/`read` round-trip
|
|
1059
|
-
* DEEP copy-in/copy-out isolation (mutating the caller's row —
|
|
1060
|
-
* NESTED field — after `write`, or a row `read` returns, never
|
|
1061
|
-
* stored state) and upsert-overwrite; simultaneous same-key
|
|
1062
|
-
* produce exactly one commit and one `CONFLICT`; pre-aborted
|
|
1063
|
-
* `insert`, and `delete` calls leave storage unchanged; `delete`
|
|
1064
|
-
* `true` then `false`;
|
|
1065
|
-
* `
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
* `
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1189
|
+
* `read` of a missing key returns `undefined`; `write`/`read` round-trip
|
|
1190
|
+
* with DEEP copy-in/copy-out isolation (mutating the caller's row —
|
|
1191
|
+
* including a NESTED field — after `write`, or a row `read` returns, never
|
|
1192
|
+
* perturbs stored state) and upsert-overwrite; simultaneous same-key
|
|
1193
|
+
* `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
|
|
1194
|
+
* `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
|
|
1195
|
+
* returns `true` then `false`; `keys`/`scan` yield in ascending key order;
|
|
1196
|
+
* `clear` empties only its target table; `snapshot`'s rollback thunk
|
|
1197
|
+
* restores pre-snapshot state, including a NESTED field mutated in place on
|
|
1198
|
+
* a read-back row between capture and restore; a scoped
|
|
1199
|
+
* `snapshot(['users'])` rolls back only the named table, leaving a
|
|
1200
|
+
* concurrent mutation to another table intact; a non-`id` primary key
|
|
1201
|
+
* (`posts.slug`) round-trips; a nested-object row round-trips structurally
|
|
1202
|
+
* (through {@link equalsValue}). The optional surface is presence-gated: when
|
|
1203
|
+
* `migrate` exists, a `column.remove` plan strips the column from stored
|
|
1204
|
+
* rows and a plan referencing an unknown table throws `DatabaseError`
|
|
1205
|
+
* `MIGRATION`; when `stream` exists, it yields only condition-matching rows
|
|
1206
|
+
* and honors `offset`/`limit`; when `transaction` exists, `commit` persists
|
|
1207
|
+
* and `rollback` restores; when both `metadata` and `stamp` exist, a fresh
|
|
1208
|
+
* store's `metadata()` is `undefined`, and after
|
|
1078
1209
|
* `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
|
|
1079
1210
|
*
|
|
1080
1211
|
* Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
|
|
@@ -1093,62 +1224,14 @@ function migrateRows(rows, steps) {
|
|
|
1093
1224
|
*
|
|
1094
1225
|
* @example
|
|
1095
1226
|
* ```ts
|
|
1096
|
-
* import { createMemoryDriver,
|
|
1227
|
+
* import { createMemoryDriver, scanDriver } from '@orkestrel/database'
|
|
1097
1228
|
*
|
|
1098
|
-
* for await (const finding of
|
|
1229
|
+
* for await (const finding of scanDriver(() => createMemoryDriver())) {
|
|
1099
1230
|
* console.log(finding.check, finding.message)
|
|
1100
1231
|
* }
|
|
1101
1232
|
* ```
|
|
1102
1233
|
*/
|
|
1103
|
-
async function*
|
|
1104
|
-
const CONFORMANCE_USERS_SCHEMA = {
|
|
1105
|
-
name: "users",
|
|
1106
|
-
primary: "id",
|
|
1107
|
-
columns: [
|
|
1108
|
-
{
|
|
1109
|
-
name: "id",
|
|
1110
|
-
storage: "text",
|
|
1111
|
-
optional: false,
|
|
1112
|
-
nullable: false
|
|
1113
|
-
},
|
|
1114
|
-
{
|
|
1115
|
-
name: "name",
|
|
1116
|
-
storage: "text",
|
|
1117
|
-
optional: false,
|
|
1118
|
-
nullable: false
|
|
1119
|
-
},
|
|
1120
|
-
{
|
|
1121
|
-
name: "age",
|
|
1122
|
-
storage: "integer",
|
|
1123
|
-
optional: true,
|
|
1124
|
-
nullable: false
|
|
1125
|
-
},
|
|
1126
|
-
{
|
|
1127
|
-
name: "meta",
|
|
1128
|
-
storage: "json",
|
|
1129
|
-
optional: true,
|
|
1130
|
-
nullable: false
|
|
1131
|
-
}
|
|
1132
|
-
],
|
|
1133
|
-
indexes: []
|
|
1134
|
-
};
|
|
1135
|
-
const CONFORMANCE_POSTS_SCHEMA = {
|
|
1136
|
-
name: "posts",
|
|
1137
|
-
primary: "slug",
|
|
1138
|
-
columns: [{
|
|
1139
|
-
name: "slug",
|
|
1140
|
-
storage: "text",
|
|
1141
|
-
optional: false,
|
|
1142
|
-
nullable: false
|
|
1143
|
-
}, {
|
|
1144
|
-
name: "title",
|
|
1145
|
-
storage: "text",
|
|
1146
|
-
optional: false,
|
|
1147
|
-
nullable: false
|
|
1148
|
-
}],
|
|
1149
|
-
indexes: []
|
|
1150
|
-
};
|
|
1151
|
-
const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA];
|
|
1234
|
+
async function* scanDriver(factory) {
|
|
1152
1235
|
try {
|
|
1153
1236
|
const driver = factory();
|
|
1154
1237
|
await driver.open(CONFORMANCE_SCHEMA);
|
|
@@ -1935,14 +2018,14 @@ async function* driverFindings(factory) {
|
|
|
1935
2018
|
}
|
|
1936
2019
|
}
|
|
1937
2020
|
/**
|
|
1938
|
-
*
|
|
2021
|
+
* Runs the driver-conformance battery, throwing on the first violated
|
|
1939
2022
|
* invariant — the fail-fast entry point most callers (test setup, CI smoke
|
|
1940
2023
|
* checks) want.
|
|
1941
2024
|
*
|
|
1942
2025
|
* @remarks
|
|
1943
|
-
*
|
|
1944
|
-
* lazy,
|
|
1945
|
-
*
|
|
2026
|
+
* Consumes only the first value {@link scanDriver} yields: because that
|
|
2027
|
+
* generator is lazy, every LATER phase never runs — true fail-fast, not
|
|
2028
|
+
* merely "report only the first". The
|
|
1946
2029
|
* thrown error is byte-compatible with the historical shape: a
|
|
1947
2030
|
* `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
|
|
1948
2031
|
* `message` and whose `context` is `{ check, ...finding.context }`.
|
|
@@ -1959,18 +2042,18 @@ async function* driverFindings(factory) {
|
|
|
1959
2042
|
* ```
|
|
1960
2043
|
*/
|
|
1961
2044
|
async function conformDriver(factory) {
|
|
1962
|
-
for await (const finding of
|
|
2045
|
+
for await (const finding of scanDriver(factory)) throw new DatabaseError("CONFORMANCE", finding.message, {
|
|
1963
2046
|
check: finding.check,
|
|
1964
2047
|
...finding.context
|
|
1965
2048
|
});
|
|
1966
2049
|
}
|
|
1967
2050
|
/**
|
|
1968
|
-
*
|
|
2051
|
+
* Runs the FULL driver-conformance battery and collects every violation — the
|
|
1969
2052
|
* audit entry point for a driver author who wants a complete report rather
|
|
1970
2053
|
* than a single fail-fast throw.
|
|
1971
2054
|
*
|
|
1972
2055
|
* @remarks
|
|
1973
|
-
* Drains {@link
|
|
2056
|
+
* Drains {@link scanDriver} to completion: every phase runs regardless
|
|
1974
2057
|
* of earlier violations, so a driver breaking two independent invariants
|
|
1975
2058
|
* reports both. An empty array means the driver is fully conformant.
|
|
1976
2059
|
*
|
|
@@ -1987,26 +2070,32 @@ async function conformDriver(factory) {
|
|
|
1987
2070
|
*/
|
|
1988
2071
|
async function auditDriver(factory) {
|
|
1989
2072
|
const findings = [];
|
|
1990
|
-
for await (const finding of
|
|
2073
|
+
for await (const finding of scanDriver(factory)) findings.push(finding);
|
|
1991
2074
|
return findings;
|
|
1992
2075
|
}
|
|
1993
2076
|
//#endregion
|
|
1994
|
-
//#region src/core/
|
|
2077
|
+
//#region src/core/ScopedIterator.ts
|
|
1995
2078
|
/**
|
|
1996
|
-
*
|
|
2079
|
+
* Forms the internal continuation admission boundary for one scoped async iterable.
|
|
1997
2080
|
*
|
|
1998
2081
|
* @remarks
|
|
1999
|
-
* Each
|
|
2000
|
-
* so an idle iterator never
|
|
2001
|
-
*
|
|
2082
|
+
* Each continuation enters the owning {@link AdmissionInterface} ledger
|
|
2083
|
+
* independently, so an idle iterator never delays a transaction, a settlement,
|
|
2084
|
+
* or a close. `ready` runs inside the tracked continuation before the source
|
|
2085
|
+
* advances, which is where a root stream re-establishes the lazy connection and
|
|
2086
|
+
* a transaction-scoped stream does nothing. A continuation requested after
|
|
2087
|
+
* admission closes attempts source cleanup exactly once and leaves the iterator
|
|
2088
|
+
* terminal.
|
|
2002
2089
|
*/
|
|
2003
|
-
var
|
|
2090
|
+
var ScopedIterator = class {
|
|
2004
2091
|
#source;
|
|
2005
|
-
#
|
|
2092
|
+
#admission;
|
|
2093
|
+
#ready;
|
|
2006
2094
|
#cleaned = false;
|
|
2007
|
-
constructor(source,
|
|
2095
|
+
constructor(source, admission, ready) {
|
|
2008
2096
|
this.#source = source[Symbol.asyncIterator]();
|
|
2009
|
-
this.#
|
|
2097
|
+
this.#admission = admission;
|
|
2098
|
+
this.#ready = ready;
|
|
2010
2099
|
}
|
|
2011
2100
|
[Symbol.asyncIterator]() {
|
|
2012
2101
|
return this;
|
|
@@ -2025,6 +2114,7 @@ var TransactionIterator = class {
|
|
|
2025
2114
|
done: true,
|
|
2026
2115
|
value: void 0
|
|
2027
2116
|
};
|
|
2117
|
+
await this.#ready();
|
|
2028
2118
|
const result = await this.#source.next();
|
|
2029
2119
|
if (result.done === true) this.#cleaned = true;
|
|
2030
2120
|
return result;
|
|
@@ -2052,8 +2142,8 @@ var TransactionIterator = class {
|
|
|
2052
2142
|
throw error;
|
|
2053
2143
|
}
|
|
2054
2144
|
#continue(operation) {
|
|
2055
|
-
if (!this.#
|
|
2056
|
-
return this.#
|
|
2145
|
+
if (!this.#admission.accepting) this.#cleanup();
|
|
2146
|
+
return this.#admission.track(operation);
|
|
2057
2147
|
}
|
|
2058
2148
|
#cleanup() {
|
|
2059
2149
|
if (this.#cleaned) return;
|
|
@@ -2066,7 +2156,7 @@ var TransactionIterator = class {
|
|
|
2066
2156
|
//#endregion
|
|
2067
2157
|
//#region src/core/TransactionScope.ts
|
|
2068
2158
|
/**
|
|
2069
|
-
*
|
|
2159
|
+
* Forms the internal lifetime boundary for one database transaction callback.
|
|
2070
2160
|
*
|
|
2071
2161
|
* @remarks
|
|
2072
2162
|
* Promise operations enter synchronously through {@link track}. Closing stops new
|
|
@@ -2110,7 +2200,7 @@ var TransactionScope = class {
|
|
|
2110
2200
|
return promise;
|
|
2111
2201
|
}
|
|
2112
2202
|
stream(source) {
|
|
2113
|
-
return new
|
|
2203
|
+
return new ScopedIterator(source, this, () => Promise.resolve());
|
|
2114
2204
|
}
|
|
2115
2205
|
stop() {
|
|
2116
2206
|
this.#accepting = false;
|
|
@@ -2123,7 +2213,7 @@ var TransactionScope = class {
|
|
|
2123
2213
|
//#endregion
|
|
2124
2214
|
//#region src/core/DatabaseContext.ts
|
|
2125
2215
|
/**
|
|
2126
|
-
*
|
|
2216
|
+
* Owns the internal shared state behind every typed view of one database.
|
|
2127
2217
|
*
|
|
2128
2218
|
* @remarks
|
|
2129
2219
|
* A context owns the driver, merged physical schema, lifecycle, observation,
|
|
@@ -2207,7 +2297,21 @@ var DatabaseContext = class {
|
|
|
2207
2297
|
this.#emitter.emit("close");
|
|
2208
2298
|
}
|
|
2209
2299
|
connect() {
|
|
2210
|
-
return this.#
|
|
2300
|
+
if (this.#ready !== void 0) return this.#ready;
|
|
2301
|
+
if (this.#failure !== void 0) throw this.#failure.error;
|
|
2302
|
+
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2303
|
+
const readiness = this.#driver.open(this.#schema).then(async () => {
|
|
2304
|
+
if (this.#status === "idle") {
|
|
2305
|
+
this.#status = "open";
|
|
2306
|
+
this.#emitter.emit("open");
|
|
2307
|
+
}
|
|
2308
|
+
await this.#reconcile();
|
|
2309
|
+
}).catch((error) => {
|
|
2310
|
+
if (this.#ready === readiness) this.#ready = void 0;
|
|
2311
|
+
throw error;
|
|
2312
|
+
});
|
|
2313
|
+
this.#ready = readiness;
|
|
2314
|
+
return readiness;
|
|
2211
2315
|
}
|
|
2212
2316
|
track(operation) {
|
|
2213
2317
|
try {
|
|
@@ -2236,7 +2340,7 @@ var DatabaseContext = class {
|
|
|
2236
2340
|
this.#transaction = token;
|
|
2237
2341
|
try {
|
|
2238
2342
|
await this.#drain();
|
|
2239
|
-
await this
|
|
2343
|
+
await this.connect();
|
|
2240
2344
|
if (this.#driver.transaction !== void 0) {
|
|
2241
2345
|
const rejection = {
|
|
2242
2346
|
rejected: false,
|
|
@@ -2312,23 +2416,6 @@ var DatabaseContext = class {
|
|
|
2312
2416
|
#outside() {
|
|
2313
2417
|
if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
|
|
2314
2418
|
}
|
|
2315
|
-
#connect() {
|
|
2316
|
-
if (this.#ready !== void 0) return this.#ready;
|
|
2317
|
-
if (this.#failure !== void 0) throw this.#failure.error;
|
|
2318
|
-
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2319
|
-
const readiness = this.#driver.open(this.#schema).then(async () => {
|
|
2320
|
-
if (this.#status === "idle") {
|
|
2321
|
-
this.#status = "open";
|
|
2322
|
-
this.#emitter.emit("open");
|
|
2323
|
-
}
|
|
2324
|
-
await this.#reconcile();
|
|
2325
|
-
}).catch((error) => {
|
|
2326
|
-
if (this.#ready === readiness) this.#ready = void 0;
|
|
2327
|
-
throw error;
|
|
2328
|
-
});
|
|
2329
|
-
this.#ready = readiness;
|
|
2330
|
-
return readiness;
|
|
2331
|
-
}
|
|
2332
2419
|
async #drain() {
|
|
2333
2420
|
if (this.#operations.size === 0) return;
|
|
2334
2421
|
await Promise.allSettled(this.#operations);
|
|
@@ -2411,12 +2498,12 @@ var DatabaseContext = class {
|
|
|
2411
2498
|
//#endregion
|
|
2412
2499
|
//#region src/core/Cursor.ts
|
|
2413
2500
|
/**
|
|
2414
|
-
*
|
|
2501
|
+
* Walks a table's rows forward for bulk in-place mutation.
|
|
2415
2502
|
*
|
|
2416
2503
|
* @remarks
|
|
2417
2504
|
* Iterates a snapshot of the table's keys captured when the cursor was opened,
|
|
2418
2505
|
* reading each row lazily through the owning table — so a mutation made during
|
|
2419
|
-
* iteration cannot corrupt the walk, and a key removed mid-iteration is
|
|
2506
|
+
* iteration cannot corrupt the walk, and a key removed mid-iteration is
|
|
2420
2507
|
* skipped. `update` and `remove` act on the row at the current position.
|
|
2421
2508
|
*/
|
|
2422
2509
|
var Cursor = class {
|
|
@@ -2503,84 +2590,9 @@ var Cursor = class {
|
|
|
2503
2590
|
}
|
|
2504
2591
|
};
|
|
2505
2592
|
//#endregion
|
|
2506
|
-
//#region src/core/DatabaseIterator.ts
|
|
2507
|
-
/**
|
|
2508
|
-
* The internal continuation admission boundary for a root database stream.
|
|
2509
|
-
*
|
|
2510
|
-
* @remarks
|
|
2511
|
-
* Each continuation enters the shared root operation ledger independently, so
|
|
2512
|
-
* an idle iterator never delays a transaction or close. A continuation rejected
|
|
2513
|
-
* after transaction or close admission closes attempts source cleanup exactly
|
|
2514
|
-
* once and leaves the iterator terminal.
|
|
2515
|
-
*/
|
|
2516
|
-
var DatabaseIterator = class {
|
|
2517
|
-
#source;
|
|
2518
|
-
#context;
|
|
2519
|
-
#cleaned = false;
|
|
2520
|
-
constructor(source, context) {
|
|
2521
|
-
this.#source = source[Symbol.asyncIterator]();
|
|
2522
|
-
this.#context = context;
|
|
2523
|
-
}
|
|
2524
|
-
[Symbol.asyncIterator]() {
|
|
2525
|
-
return this;
|
|
2526
|
-
}
|
|
2527
|
-
next() {
|
|
2528
|
-
return this.#continue(() => this.#next());
|
|
2529
|
-
}
|
|
2530
|
-
return() {
|
|
2531
|
-
return this.#continue(() => this.#return());
|
|
2532
|
-
}
|
|
2533
|
-
throw(error) {
|
|
2534
|
-
return this.#continue(() => this.#throw(error));
|
|
2535
|
-
}
|
|
2536
|
-
async #next() {
|
|
2537
|
-
if (this.#cleaned) return {
|
|
2538
|
-
done: true,
|
|
2539
|
-
value: void 0
|
|
2540
|
-
};
|
|
2541
|
-
await this.#context.connect();
|
|
2542
|
-
const result = await this.#source.next();
|
|
2543
|
-
if (result.done === true) this.#cleaned = true;
|
|
2544
|
-
return result;
|
|
2545
|
-
}
|
|
2546
|
-
async #return() {
|
|
2547
|
-
if (this.#cleaned || this.#source.return === void 0) {
|
|
2548
|
-
this.#cleaned = true;
|
|
2549
|
-
return {
|
|
2550
|
-
done: true,
|
|
2551
|
-
value: void 0
|
|
2552
|
-
};
|
|
2553
|
-
}
|
|
2554
|
-
this.#cleaned = true;
|
|
2555
|
-
return this.#source.return();
|
|
2556
|
-
}
|
|
2557
|
-
async #throw(error) {
|
|
2558
|
-
if (this.#source.throw !== void 0) {
|
|
2559
|
-
const result = await this.#source.throw(error);
|
|
2560
|
-
if (result.done === true) this.#cleaned = true;
|
|
2561
|
-
return result;
|
|
2562
|
-
}
|
|
2563
|
-
try {
|
|
2564
|
-
await this.#return();
|
|
2565
|
-
} catch {}
|
|
2566
|
-
throw error;
|
|
2567
|
-
}
|
|
2568
|
-
#continue(operation) {
|
|
2569
|
-
if (!this.#context.accepting) this.#cleanup();
|
|
2570
|
-
return this.#context.track(operation);
|
|
2571
|
-
}
|
|
2572
|
-
#cleanup() {
|
|
2573
|
-
if (this.#cleaned) return;
|
|
2574
|
-
this.#cleaned = true;
|
|
2575
|
-
try {
|
|
2576
|
-
(this.#source.return?.())?.catch(() => {});
|
|
2577
|
-
} catch {}
|
|
2578
|
-
}
|
|
2579
|
-
};
|
|
2580
|
-
//#endregion
|
|
2581
2593
|
//#region src/core/Query.ts
|
|
2582
2594
|
/**
|
|
2583
|
-
*
|
|
2595
|
+
* Builds a read against one table through a fluent chain.
|
|
2584
2596
|
*
|
|
2585
2597
|
* @remarks
|
|
2586
2598
|
* Accumulates typed conditions, ordering, JS filters, and a page. Each builder
|
|
@@ -2641,7 +2653,7 @@ var Query = class {
|
|
|
2641
2653
|
return this.#filtered(fetched).length;
|
|
2642
2654
|
}
|
|
2643
2655
|
/**
|
|
2644
|
-
*
|
|
2656
|
+
* Evaluates conditions, filters, offset, and limit lazily.
|
|
2645
2657
|
*
|
|
2646
2658
|
* @param options - Optional abort options
|
|
2647
2659
|
* @returns Matching rows in storage order
|
|
@@ -2693,18 +2705,18 @@ var Query = class {
|
|
|
2693
2705
|
//#endregion
|
|
2694
2706
|
//#region src/core/Table.ts
|
|
2695
2707
|
/**
|
|
2696
|
-
*
|
|
2708
|
+
* Exposes typed keyed CRUD plus fluent query and cursor access over a driver.
|
|
2697
2709
|
*
|
|
2698
2710
|
* @remarks
|
|
2699
2711
|
* The table's contract is the load-bearing piece: writes go through `parse`
|
|
2700
2712
|
* (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
|
|
2701
2713
|
* reads come back through the contract guard (narrowing a stored {@link Row} to
|
|
2702
|
-
* the table's type — no assertion
|
|
2714
|
+
* the table's type — no assertion), and `contract` is exposed for
|
|
2703
2715
|
* introspection and seeding. The driver only stores and scans; all querying is
|
|
2704
2716
|
* the shared core engine in `helpers.ts`.
|
|
2705
2717
|
*
|
|
2706
2718
|
* @remarks
|
|
2707
|
-
* - **Observable
|
|
2719
|
+
* - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
|
|
2708
2720
|
* per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
|
|
2709
2721
|
* fire-and-forget observers (cache invalidation, sync, an audit log), ALONGSIDE the
|
|
2710
2722
|
* database-level lifecycle. Events carry the affected KEY only (no value payload, to
|
|
@@ -2793,7 +2805,7 @@ var Table = class {
|
|
|
2793
2805
|
});
|
|
2794
2806
|
}
|
|
2795
2807
|
/**
|
|
2796
|
-
*
|
|
2808
|
+
* Counts contract-valid rows matching `input`'s conditions.
|
|
2797
2809
|
*
|
|
2798
2810
|
* @remarks
|
|
2799
2811
|
* Paging is ignored. Candidate rows use the driver's native `records` hook
|
|
@@ -2818,11 +2830,11 @@ var Table = class {
|
|
|
2818
2830
|
});
|
|
2819
2831
|
}
|
|
2820
2832
|
/**
|
|
2821
|
-
*
|
|
2833
|
+
* Computes an aggregate over `column` across rows matching `input`'s
|
|
2822
2834
|
* conditions.
|
|
2823
2835
|
*
|
|
2824
2836
|
* @remarks
|
|
2825
|
-
*
|
|
2837
|
+
* Unlike {@link count}, `aggregate` operates on STORED rows WITHOUT the
|
|
2826
2838
|
* contract guard {@link records} / {@link scan} apply — a non-conforming
|
|
2827
2839
|
* stored row still contributes to the computed aggregate when it matches
|
|
2828
2840
|
* the conditions, even though it would never appear in `records()`'s
|
|
@@ -2847,7 +2859,7 @@ var Table = class {
|
|
|
2847
2859
|
});
|
|
2848
2860
|
}
|
|
2849
2861
|
/**
|
|
2850
|
-
*
|
|
2862
|
+
* Streams the table's rows matching `input`, applying offset/limit paging.
|
|
2851
2863
|
*
|
|
2852
2864
|
* @remarks
|
|
2853
2865
|
* `input.limit` counts rows that pass both the input conditions and the
|
|
@@ -2862,7 +2874,7 @@ var Table = class {
|
|
|
2862
2874
|
scan(input, options) {
|
|
2863
2875
|
validatePage(input);
|
|
2864
2876
|
const source = this.#scan(input, options);
|
|
2865
|
-
if (this.#context !== void 0) return new
|
|
2877
|
+
if (this.#context !== void 0) return new ScopedIterator(source, this.#context, this.#ready);
|
|
2866
2878
|
return this.#scope === void 0 ? source : this.#scope.stream(source);
|
|
2867
2879
|
}
|
|
2868
2880
|
set(rows, options) {
|
|
@@ -3106,7 +3118,7 @@ var Table = class {
|
|
|
3106
3118
|
//#endregion
|
|
3107
3119
|
//#region src/core/DatabaseTransaction.ts
|
|
3108
3120
|
/**
|
|
3109
|
-
*
|
|
3121
|
+
* Binds a table-only database view to one driver transaction scope.
|
|
3110
3122
|
*
|
|
3111
3123
|
* @typeParam T - The declared table shape map
|
|
3112
3124
|
*
|
|
@@ -3133,25 +3145,17 @@ var DatabaseTransaction = class {
|
|
|
3133
3145
|
}
|
|
3134
3146
|
table(name) {
|
|
3135
3147
|
this.#scope.check();
|
|
3136
|
-
const columns = this.#
|
|
3137
|
-
return this.#build(name, this.#
|
|
3148
|
+
const columns = requireColumns(this.#tables, name);
|
|
3149
|
+
return this.#build(name, resolvePrimary(this.#primary, name), createContract(objectShape(columns)));
|
|
3138
3150
|
}
|
|
3139
3151
|
#build(name, key, contract) {
|
|
3140
3152
|
return new Table(() => Promise.resolve(), this.#driver, name, key, contract, this.#generate, this.#error, void 0, this.#scope);
|
|
3141
3153
|
}
|
|
3142
|
-
#key(name) {
|
|
3143
|
-
return this.#primary[name] ?? "id";
|
|
3144
|
-
}
|
|
3145
|
-
#columns(name) {
|
|
3146
|
-
const columns = this.#tables[name];
|
|
3147
|
-
if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
|
|
3148
|
-
return columns;
|
|
3149
|
-
}
|
|
3150
3154
|
};
|
|
3151
3155
|
//#endregion
|
|
3152
3156
|
//#region src/core/Database.ts
|
|
3153
3157
|
/**
|
|
3154
|
-
*
|
|
3158
|
+
* Exposes a typed view over one shared internal lifecycle and storage context.
|
|
3155
3159
|
*
|
|
3156
3160
|
* @remarks
|
|
3157
3161
|
* Each view owns only its table contracts, primary columns, indexes, and key
|
|
@@ -3184,8 +3188,8 @@ var Database = class Database {
|
|
|
3184
3188
|
}
|
|
3185
3189
|
table(name) {
|
|
3186
3190
|
if (this.#context.status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#context.name}' is closed`, { name: this.#context.name });
|
|
3187
|
-
const columns = this.#
|
|
3188
|
-
return this.#build(name, this.#
|
|
3191
|
+
const columns = requireColumns(this.#tables, name);
|
|
3192
|
+
return this.#build(name, resolvePrimary(this.#primary, name), createContract(objectShape(columns)));
|
|
3189
3193
|
}
|
|
3190
3194
|
import(tables, primary) {
|
|
3191
3195
|
return this.#spawn(tables, {
|
|
@@ -3196,9 +3200,9 @@ var Database = class Database {
|
|
|
3196
3200
|
export() {
|
|
3197
3201
|
const result = {};
|
|
3198
3202
|
for (const name of Object.keys(this.#tables)) {
|
|
3199
|
-
const columns = this.#
|
|
3203
|
+
const columns = requireColumns(this.#tables, name);
|
|
3200
3204
|
result[name] = {
|
|
3201
|
-
primary: this.#
|
|
3205
|
+
primary: resolvePrimary(this.#primary, name),
|
|
3202
3206
|
columns,
|
|
3203
3207
|
schema: compileSchema(objectShape(columns))
|
|
3204
3208
|
};
|
|
@@ -3234,20 +3238,12 @@ var Database = class Database {
|
|
|
3234
3238
|
...this.#context.version === void 0 ? {} : { version: this.#context.version }
|
|
3235
3239
|
}, this.#context);
|
|
3236
3240
|
}
|
|
3237
|
-
#key(name) {
|
|
3238
|
-
return this.#primary[name] ?? "id";
|
|
3239
|
-
}
|
|
3240
|
-
#columns(name) {
|
|
3241
|
-
const columns = this.#tables[name];
|
|
3242
|
-
if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
|
|
3243
|
-
return columns;
|
|
3244
|
-
}
|
|
3245
3241
|
#schema() {
|
|
3246
3242
|
return Object.keys(this.#tables).map((name) => {
|
|
3247
|
-
const columns = this.#
|
|
3243
|
+
const columns = requireColumns(this.#tables, name);
|
|
3248
3244
|
return {
|
|
3249
3245
|
name,
|
|
3250
|
-
primary: this.#
|
|
3246
|
+
primary: resolvePrimary(this.#primary, name),
|
|
3251
3247
|
columns: Object.entries(columns).map(([column, shape]) => shapeToColumnSchema(column, shape)),
|
|
3252
3248
|
indexes: this.#indexes[name] ?? []
|
|
3253
3249
|
};
|
|
@@ -3283,16 +3279,16 @@ var Database = class Database {
|
|
|
3283
3279
|
//#endregion
|
|
3284
3280
|
//#region src/core/drivers/MemoryDriver.ts
|
|
3285
3281
|
/**
|
|
3286
|
-
*
|
|
3282
|
+
* Implements the reference {@link DriverInterface} — nested maps, no I/O.
|
|
3287
3283
|
*
|
|
3288
3284
|
* @remarks
|
|
3289
3285
|
* The in-between made concrete: it runs identically in a browser or on a server,
|
|
3290
3286
|
* so it is the storage behind tests, ephemeral caches, and any code that wants
|
|
3291
|
-
* the database API without a persistent backend. Rows are DEEP-copied (
|
|
3287
|
+
* the database API without a persistent backend. Rows are DEEP-copied (through
|
|
3292
3288
|
* `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
|
|
3293
3289
|
* snapshot capture and restore — so a caller mutating a nested field of an input
|
|
3294
3290
|
* row, a returned row, or a row mutated in place between snapshot and rollback
|
|
3295
|
-
* can never perturb stored state
|
|
3291
|
+
* can never perturb stored state; a shallow `{ ...row }` spread
|
|
3296
3292
|
* would still share nested object/array references. Metadata instead routes
|
|
3297
3293
|
* through `cloneDriverMetadata`: `stamp` and migration snapshot exact JSON at
|
|
3298
3294
|
* ingress, and `metadata` returns a distinct deeply frozen owned copy. `snapshot`
|
|
@@ -3354,19 +3350,19 @@ var MemoryDriver = class {
|
|
|
3354
3350
|
}
|
|
3355
3351
|
}
|
|
3356
3352
|
/**
|
|
3357
|
-
*
|
|
3353
|
+
* Iterates rows lazily with native filtering — the {@link DriverInterface.stream} hook.
|
|
3358
3354
|
*
|
|
3359
3355
|
* @remarks
|
|
3360
3356
|
* Iterates the table's keys in the same key order `scan` and `keys` yield
|
|
3361
3357
|
* (sorted by {@link compareValues}), testing each row against
|
|
3362
|
-
* `input.conditions` (
|
|
3358
|
+
* `input.conditions` (through {@link matchesQuery}) before counting it
|
|
3363
3359
|
* toward `offset` / `limit`. Both are applied lazily as matches are found —
|
|
3364
3360
|
* `offset` matches are skipped without being yielded, and iteration stops the
|
|
3365
3361
|
* instant `limit` yields have been produced, so a large table is never fully
|
|
3366
3362
|
* walked for a small page. `input.order` is IGNORED (the same contract as
|
|
3367
3363
|
* `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
|
|
3368
|
-
* order, sorted output is `records()`'s job. Rows yield copy-out
|
|
3369
|
-
*
|
|
3364
|
+
* order, sorted output is `records()`'s job. Rows yield copy-out, and an
|
|
3365
|
+
* unknown table mirrors `scan`'s empty-yield behavior.
|
|
3370
3366
|
*
|
|
3371
3367
|
* @param table - The table to stream
|
|
3372
3368
|
* @param input - The filter / offset / limit to apply lazily
|
|
@@ -3382,31 +3378,11 @@ var MemoryDriver = class {
|
|
|
3382
3378
|
validatePage(input);
|
|
3383
3379
|
return this.#stream(table, input);
|
|
3384
3380
|
}
|
|
3385
|
-
async *#stream(table, input) {
|
|
3386
|
-
const store = this.#store(table);
|
|
3387
|
-
const conditions = input.conditions;
|
|
3388
|
-
const offset = input.offset ?? 0;
|
|
3389
|
-
const limit = input.limit;
|
|
3390
|
-
let skipped = 0;
|
|
3391
|
-
let yielded = 0;
|
|
3392
|
-
for (const key of this.#ordered(table)) {
|
|
3393
|
-
if (limit !== void 0 && yielded >= limit) return;
|
|
3394
|
-
const row = store.get(key);
|
|
3395
|
-
if (row === void 0) continue;
|
|
3396
|
-
if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
|
|
3397
|
-
if (skipped < offset) {
|
|
3398
|
-
skipped += 1;
|
|
3399
|
-
continue;
|
|
3400
|
-
}
|
|
3401
|
-
yield structuredClone(row);
|
|
3402
|
-
yielded += 1;
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
3381
|
async clear(table) {
|
|
3406
3382
|
this.#store(table).clear();
|
|
3407
3383
|
}
|
|
3408
3384
|
/**
|
|
3409
|
-
*
|
|
3385
|
+
* Captures the current state and returns a thunk that rolls back to it.
|
|
3410
3386
|
*
|
|
3411
3387
|
* @remarks
|
|
3412
3388
|
* Capture owns rows, schema, and one session-local table identity. Replay
|
|
@@ -3463,7 +3439,7 @@ var MemoryDriver = class {
|
|
|
3463
3439
|
};
|
|
3464
3440
|
}
|
|
3465
3441
|
/**
|
|
3466
|
-
*
|
|
3442
|
+
* Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
|
|
3467
3443
|
* never been stamped.
|
|
3468
3444
|
*
|
|
3469
3445
|
* @remarks
|
|
@@ -3478,7 +3454,7 @@ var MemoryDriver = class {
|
|
|
3478
3454
|
return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
|
|
3479
3455
|
}
|
|
3480
3456
|
/**
|
|
3481
|
-
*
|
|
3457
|
+
* Persists an owned snapshot for a later `metadata()` to return.
|
|
3482
3458
|
*
|
|
3483
3459
|
* @param metadata - The {@link DriverMetadata} to persist
|
|
3484
3460
|
*/
|
|
@@ -3486,7 +3462,7 @@ var MemoryDriver = class {
|
|
|
3486
3462
|
this.#metadata = cloneDriverMetadata(metadata);
|
|
3487
3463
|
}
|
|
3488
3464
|
/**
|
|
3489
|
-
*
|
|
3465
|
+
* Applies a {@link Migration} plan's steps against the in-memory store.
|
|
3490
3466
|
*
|
|
3491
3467
|
* @remarks
|
|
3492
3468
|
* Steps apply against an isolated candidate. Rows, schema changes, and
|
|
@@ -3568,6 +3544,26 @@ var MemoryDriver = class {
|
|
|
3568
3544
|
case "index.remove": this.#require(tables, step.table);
|
|
3569
3545
|
}
|
|
3570
3546
|
}
|
|
3547
|
+
async *#stream(table, input) {
|
|
3548
|
+
const store = this.#store(table);
|
|
3549
|
+
const conditions = input.conditions;
|
|
3550
|
+
const offset = input.offset ?? 0;
|
|
3551
|
+
const limit = input.limit;
|
|
3552
|
+
let skipped = 0;
|
|
3553
|
+
let yielded = 0;
|
|
3554
|
+
for (const key of this.#ordered(table)) {
|
|
3555
|
+
if (limit !== void 0 && yielded >= limit) return;
|
|
3556
|
+
const row = store.get(key);
|
|
3557
|
+
if (row === void 0) continue;
|
|
3558
|
+
if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
|
|
3559
|
+
if (skipped < offset) {
|
|
3560
|
+
skipped += 1;
|
|
3561
|
+
continue;
|
|
3562
|
+
}
|
|
3563
|
+
yield structuredClone(row);
|
|
3564
|
+
yielded += 1;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3571
3567
|
#store(table) {
|
|
3572
3568
|
this.#table(table);
|
|
3573
3569
|
const store = this.#tables.get(table);
|
|
@@ -3578,15 +3574,15 @@ var MemoryDriver = class {
|
|
|
3578
3574
|
//#endregion
|
|
3579
3575
|
//#region src/core/factories.ts
|
|
3580
3576
|
/**
|
|
3581
|
-
*
|
|
3577
|
+
* Creates a database over a driver and a declared `tables` schema.
|
|
3582
3578
|
*
|
|
3583
3579
|
* @remarks
|
|
3584
3580
|
* `tables` maps each name to its columns (a `column → shape` map); the database
|
|
3585
3581
|
* wraps each in an `objectShape`, so you never write `objectShape` at the table
|
|
3586
3582
|
* level. The `const` type parameter captures the literal names and columns, so
|
|
3587
3583
|
* `db.table('users')` is checked against the schema and typed by `Infer` of its
|
|
3588
|
-
* columns — no annotations. Name a non-`id` primary-key column per table
|
|
3589
|
-
* optional `primary` and `indexes` maps.
|
|
3584
|
+
* columns — no annotations. Name a non-`id` primary-key column per table through
|
|
3585
|
+
* the optional `primary` and `indexes` maps.
|
|
3590
3586
|
*
|
|
3591
3587
|
* @param options - The driver, `tables`, and optional `primary`, `indexes`,
|
|
3592
3588
|
* `name`, `generator`, `version`, and emitter hooks
|
|
@@ -3612,7 +3608,7 @@ function createDatabase(options) {
|
|
|
3612
3608
|
return new Database(options);
|
|
3613
3609
|
}
|
|
3614
3610
|
/**
|
|
3615
|
-
*
|
|
3611
|
+
* Creates the in-memory reference {@link DriverInterface}.
|
|
3616
3612
|
*
|
|
3617
3613
|
* @remarks
|
|
3618
3614
|
* Backed by nested maps with no I/O — the same driver runs in a browser or on a
|
|
@@ -3624,6 +3620,122 @@ function createMemoryDriver() {
|
|
|
3624
3620
|
return new MemoryDriver();
|
|
3625
3621
|
}
|
|
3626
3622
|
//#endregion
|
|
3627
|
-
|
|
3623
|
+
//#region src/core/DriverIterator.ts
|
|
3624
|
+
/**
|
|
3625
|
+
* Forms the internal continuation boundary for a root driver async iterator.
|
|
3626
|
+
*
|
|
3627
|
+
* @remarks
|
|
3628
|
+
* A driver transaction can begin while a caller holds an idle root iterator.
|
|
3629
|
+
* Every `next` therefore checks the driver's root-state guard immediately
|
|
3630
|
+
* before and after advancing the source. A failed continuation terminalizes the
|
|
3631
|
+
* iterator, discards any row produced before the post-advance guard failed, and
|
|
3632
|
+
* attempts source cleanup exactly once.
|
|
3633
|
+
*
|
|
3634
|
+
* A driver implementing the published `DriverInterface` extension seam wraps its
|
|
3635
|
+
* own source iterator in one so a root `scan` / `stream` cannot outlive the
|
|
3636
|
+
* driver state it was opened against.
|
|
3637
|
+
*
|
|
3638
|
+
* @typeParam T - The value the wrapped source yields
|
|
3639
|
+
*
|
|
3640
|
+
* @example
|
|
3641
|
+
* ```ts
|
|
3642
|
+
* import type { Row } from '@orkestrel/database'
|
|
3643
|
+
* import { DatabaseError, DriverIterator } from '@orkestrel/database'
|
|
3644
|
+
*
|
|
3645
|
+
* // Inside a driver's `scan`, over its own row source and root-state guard.
|
|
3646
|
+
* declare const rows: AsyncIterator<Row>
|
|
3647
|
+
* declare const transacting: () => boolean
|
|
3648
|
+
* const scan = new DriverIterator(rows, () => {
|
|
3649
|
+
* if (transacting()) {
|
|
3650
|
+
* throw new DatabaseError('CONFLICT', 'scan: a transaction is active')
|
|
3651
|
+
* }
|
|
3652
|
+
* })
|
|
3653
|
+
* for await (const row of scan) row // one row at a time, guarded around each advance
|
|
3654
|
+
* ```
|
|
3655
|
+
*/
|
|
3656
|
+
var DriverIterator = class {
|
|
3657
|
+
#source;
|
|
3658
|
+
#guard;
|
|
3659
|
+
#terminal = false;
|
|
3660
|
+
#cleaned = false;
|
|
3661
|
+
/**
|
|
3662
|
+
* Wraps one source iterator in the continuation boundary.
|
|
3663
|
+
*
|
|
3664
|
+
* @param source - The driver's own row iterator, advanced once per `next`
|
|
3665
|
+
* @param guard - The root-state check, run immediately before and after each advance; it throws to terminalize the iteration
|
|
3666
|
+
*/
|
|
3667
|
+
constructor(source, guard) {
|
|
3668
|
+
this.#source = source;
|
|
3669
|
+
this.#guard = guard;
|
|
3670
|
+
}
|
|
3671
|
+
[Symbol.asyncIterator]() {
|
|
3672
|
+
return this;
|
|
3673
|
+
}
|
|
3674
|
+
async next() {
|
|
3675
|
+
if (this.#terminal) return {
|
|
3676
|
+
done: true,
|
|
3677
|
+
value: void 0
|
|
3678
|
+
};
|
|
3679
|
+
try {
|
|
3680
|
+
this.#guard();
|
|
3681
|
+
const result = await this.#source.next();
|
|
3682
|
+
this.#guard();
|
|
3683
|
+
if (result.done === true) {
|
|
3684
|
+
this.#terminal = true;
|
|
3685
|
+
this.#cleaned = true;
|
|
3686
|
+
}
|
|
3687
|
+
return result;
|
|
3688
|
+
} catch (error) {
|
|
3689
|
+
this.#terminal = true;
|
|
3690
|
+
await this.#discard();
|
|
3691
|
+
throw error;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
async return() {
|
|
3695
|
+
if (this.#terminal) return {
|
|
3696
|
+
done: true,
|
|
3697
|
+
value: void 0
|
|
3698
|
+
};
|
|
3699
|
+
this.#terminal = true;
|
|
3700
|
+
if (this.#cleaned || this.#source.return === void 0) {
|
|
3701
|
+
this.#cleaned = true;
|
|
3702
|
+
return {
|
|
3703
|
+
done: true,
|
|
3704
|
+
value: void 0
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
this.#cleaned = true;
|
|
3708
|
+
return this.#source.return();
|
|
3709
|
+
}
|
|
3710
|
+
async throw(error) {
|
|
3711
|
+
if (this.#terminal) throw error;
|
|
3712
|
+
if (this.#source.throw === void 0) {
|
|
3713
|
+
this.#terminal = true;
|
|
3714
|
+
await this.#discard();
|
|
3715
|
+
throw error;
|
|
3716
|
+
}
|
|
3717
|
+
try {
|
|
3718
|
+
const result = await this.#source.throw(error);
|
|
3719
|
+
if (result.done === true) {
|
|
3720
|
+
this.#terminal = true;
|
|
3721
|
+
this.#cleaned = true;
|
|
3722
|
+
}
|
|
3723
|
+
return result;
|
|
3724
|
+
} catch (cause) {
|
|
3725
|
+
this.#terminal = true;
|
|
3726
|
+
await this.#discard();
|
|
3727
|
+
throw cause;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
async #discard() {
|
|
3731
|
+
if (this.#cleaned) return;
|
|
3732
|
+
this.#cleaned = true;
|
|
3733
|
+
try {
|
|
3734
|
+
await this.#source.return?.();
|
|
3735
|
+
} catch {}
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
//#endregion
|
|
3739
|
+
export { CONFORMANCE_POSTS_SCHEMA, CONFORMANCE_SCHEMA, CONFORMANCE_USERS_SCHEMA, DEFAULT_PRIMARY, Database, DatabaseError, DriverIterator, MAX_PATTERN_LENGTH, MemoryDriver, applyQuery, auditDriver, bindRowKey, checkAbort, cloneDriverMetadata, cloneDriverSchema, cloneMigrationInput, compareValues, computeAggregate, conformDriver, createDatabase, createMemoryDriver, equalsValue, extractKey, filterRows, findColumn, isColumnSchema, isDatabaseError, isDriverMetadata, isDriverSchema, isKey, isMigration, isMigrationInput, isMigrationStep, isTableSchema, matchesCondition, matchesGlobPattern, matchesLikePattern, matchesQuery, matchesWildcardPattern, migrateRows, normalizeDriverSchema, planMigration, projectMigrationSchema, requireColumns, resolvePrimary, scanDriver, shapeToColumnSchema, shapeToColumnStorage, sortRows, validatePage };
|
|
3628
3740
|
|
|
3629
3741
|
//# sourceMappingURL=index.js.map
|