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