@orkestrel/database 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/src/browser/index.d.ts +95 -70
- package/dist/src/browser/index.js +115 -86
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +595 -410
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +712 -313
- package/dist/src/core/index.d.ts +712 -313
- package/dist/src/core/index.js +588 -409
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +236 -332
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +213 -223
- package/dist/src/server/index.d.ts +213 -223
- package/dist/src/server/index.js +230 -324
- package/dist/src/server/index.js.map +1 -1
- package/package.json +22 -18
package/dist/src/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, `'id'`, 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,13 @@ 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, 1024 characters, before
|
|
14
|
+
* rejecting it.
|
|
14
15
|
*
|
|
15
16
|
* @remarks
|
|
16
|
-
* A
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
19
20
|
* backtracking regex (`.*`-segments-separated-by-literals against a long input is the
|
|
20
21
|
* catastrophic shape JS cannot bound without atomic groups), so it is O(value ×
|
|
21
22
|
* pattern). Capping the pattern length bounds that pattern factor, leaving a match
|
|
@@ -23,10 +24,85 @@ var DEFAULT_PRIMARY = "id";
|
|
|
23
24
|
* `VALIDATION` {@link DatabaseError}; the cap is generous for any legitimate search.
|
|
24
25
|
*/
|
|
25
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 `users` and `posts` 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. The
|
|
98
|
+
* array and each schema in it are frozen, so a consumer holding it cannot change
|
|
99
|
+
* what a later phase opens.
|
|
100
|
+
*/
|
|
101
|
+
var CONFORMANCE_SCHEMA = Object.freeze([CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA]);
|
|
26
102
|
//#endregion
|
|
27
103
|
//#region src/core/errors.ts
|
|
28
104
|
/**
|
|
29
|
-
*
|
|
105
|
+
* Represents an error thrown by the database layer.
|
|
30
106
|
*
|
|
31
107
|
* @remarks
|
|
32
108
|
* Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
|
|
@@ -37,7 +113,7 @@ var MAX_PATTERN_LENGTH = 1024;
|
|
|
37
113
|
* `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
|
|
38
114
|
* driver that violates a {@link DriverInterface} invariant, thrown by the
|
|
39
115
|
* `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
|
|
40
|
-
* fault surfaced by a driver seam —
|
|
116
|
+
* fault surfaced by a driver seam — for example a filesystem failure while
|
|
41
117
|
* persisting (`DRIVER`) — as opposed to expected domain conditions, which
|
|
42
118
|
* keep their specific codes.
|
|
43
119
|
*/
|
|
@@ -52,10 +128,10 @@ var DatabaseError = class extends Error {
|
|
|
52
128
|
}
|
|
53
129
|
};
|
|
54
130
|
/**
|
|
55
|
-
*
|
|
131
|
+
* Narrows an unknown caught value to a {@link DatabaseError}.
|
|
56
132
|
*
|
|
57
133
|
* @param value - The value to test (typically a `catch` binding)
|
|
58
|
-
* @returns
|
|
134
|
+
* @returns True if `value` is a {@link DatabaseError}; false otherwise
|
|
59
135
|
*
|
|
60
136
|
* @example
|
|
61
137
|
* ```ts
|
|
@@ -72,43 +148,23 @@ function isDatabaseError(value) {
|
|
|
72
148
|
//#endregion
|
|
73
149
|
//#region src/core/validators.ts
|
|
74
150
|
/**
|
|
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.
|
|
151
|
+
* Checks whether a value is a usable database key.
|
|
100
152
|
*
|
|
101
153
|
* @param value - The value to test
|
|
102
|
-
* @returns
|
|
154
|
+
* @returns True if `value` is a string or a finite number; false otherwise
|
|
103
155
|
*/
|
|
104
156
|
function isKey(value) {
|
|
105
157
|
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
106
158
|
}
|
|
107
159
|
/**
|
|
108
|
-
*
|
|
160
|
+
* Checks whether a value is a portable column schema.
|
|
161
|
+
*
|
|
162
|
+
* @remarks
|
|
163
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
164
|
+
* contained as a non-match rather than a throw.
|
|
109
165
|
*
|
|
110
166
|
* @param value - The value to test
|
|
111
|
-
* @returns
|
|
167
|
+
* @returns True if `value` is a complete {@link ColumnSchema}; false otherwise
|
|
112
168
|
*/
|
|
113
169
|
function isColumnSchema(value) {
|
|
114
170
|
try {
|
|
@@ -120,10 +176,14 @@ function isColumnSchema(value) {
|
|
|
120
176
|
}
|
|
121
177
|
}
|
|
122
178
|
/**
|
|
123
|
-
*
|
|
179
|
+
* Checks whether a value is a portable table schema.
|
|
180
|
+
*
|
|
181
|
+
* @remarks
|
|
182
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
183
|
+
* contained as a non-match rather than a throw.
|
|
124
184
|
*
|
|
125
185
|
* @param value - The value to test
|
|
126
|
-
* @returns
|
|
186
|
+
* @returns True if `value` is a complete {@link TableSchema}; false otherwise
|
|
127
187
|
*/
|
|
128
188
|
function isTableSchema(value) {
|
|
129
189
|
try {
|
|
@@ -139,10 +199,14 @@ function isTableSchema(value) {
|
|
|
139
199
|
}
|
|
140
200
|
}
|
|
141
201
|
/**
|
|
142
|
-
*
|
|
202
|
+
* Checks whether a value is a complete portable driver schema.
|
|
203
|
+
*
|
|
204
|
+
* @remarks
|
|
205
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
206
|
+
* contained as a non-match rather than a throw.
|
|
143
207
|
*
|
|
144
208
|
* @param value - The value to test
|
|
145
|
-
* @returns
|
|
209
|
+
* @returns True if `value` is a table-schema collection with unique table names; false otherwise
|
|
146
210
|
*/
|
|
147
211
|
function isDriverSchema(value) {
|
|
148
212
|
try {
|
|
@@ -155,10 +219,14 @@ function isDriverSchema(value) {
|
|
|
155
219
|
}
|
|
156
220
|
}
|
|
157
221
|
/**
|
|
158
|
-
*
|
|
222
|
+
* Checks whether a value is one ordered migration step.
|
|
223
|
+
*
|
|
224
|
+
* @remarks
|
|
225
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
226
|
+
* contained as a non-match rather than a throw.
|
|
159
227
|
*
|
|
160
228
|
* @param value - The value to test
|
|
161
|
-
* @returns
|
|
229
|
+
* @returns True if `value` is a complete {@link MigrationStep}; false otherwise
|
|
162
230
|
*/
|
|
163
231
|
function isMigrationStep(value) {
|
|
164
232
|
try {
|
|
@@ -179,10 +247,14 @@ function isMigrationStep(value) {
|
|
|
179
247
|
}
|
|
180
248
|
}
|
|
181
249
|
/**
|
|
182
|
-
*
|
|
250
|
+
* Checks whether a value is an ordered migration plan.
|
|
251
|
+
*
|
|
252
|
+
* @remarks
|
|
253
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
254
|
+
* contained as a non-match rather than a throw.
|
|
183
255
|
*
|
|
184
256
|
* @param value - The value to test
|
|
185
|
-
* @returns
|
|
257
|
+
* @returns True if `value` is a complete {@link Migration}; false otherwise
|
|
186
258
|
*/
|
|
187
259
|
function isMigration(value) {
|
|
188
260
|
try {
|
|
@@ -194,10 +266,16 @@ function isMigration(value) {
|
|
|
194
266
|
}
|
|
195
267
|
}
|
|
196
268
|
/**
|
|
197
|
-
*
|
|
269
|
+
* Checks whether a value is persisted driver metadata.
|
|
270
|
+
*
|
|
271
|
+
* @remarks
|
|
272
|
+
* The boundary check a versioning driver's `metadata()` narrows a stored or
|
|
273
|
+
* deserialized record through, so no call site needs an assertion. Total over any
|
|
274
|
+
* input: a hostile getter, a revoked proxy, or a cyclic value is contained as a
|
|
275
|
+
* non-match rather than a throw.
|
|
198
276
|
*
|
|
199
277
|
* @param value - The value to test
|
|
200
|
-
* @returns
|
|
278
|
+
* @returns True if `value` is complete {@link DriverMetadata}; false otherwise
|
|
201
279
|
*/
|
|
202
280
|
function isDriverMetadata(value) {
|
|
203
281
|
try {
|
|
@@ -209,10 +287,14 @@ function isDriverMetadata(value) {
|
|
|
209
287
|
}
|
|
210
288
|
}
|
|
211
289
|
/**
|
|
212
|
-
*
|
|
290
|
+
* Checks whether a value is one atomic migration request.
|
|
291
|
+
*
|
|
292
|
+
* @remarks
|
|
293
|
+
* Total over any input: a hostile getter, a revoked proxy, or a cyclic value is
|
|
294
|
+
* contained as a non-match rather than a throw.
|
|
213
295
|
*
|
|
214
296
|
* @param value - The value to test
|
|
215
|
-
* @returns
|
|
297
|
+
* @returns True if `value` is a complete {@link MigrationInput}; false otherwise
|
|
216
298
|
*/
|
|
217
299
|
function isMigrationInput(value) {
|
|
218
300
|
try {
|
|
@@ -226,7 +308,12 @@ function isMigrationInput(value) {
|
|
|
226
308
|
//#endregion
|
|
227
309
|
//#region src/core/cloners.ts
|
|
228
310
|
/**
|
|
229
|
-
*
|
|
311
|
+
* Clones unknown driver metadata into a distinct deeply frozen snapshot.
|
|
312
|
+
*
|
|
313
|
+
* @remarks
|
|
314
|
+
* The clone is validated as {@link DriverMetadata} before it is returned, so a
|
|
315
|
+
* malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
|
|
316
|
+
* `context.path === 'metadata'` rather than surfacing a raw Contract or caller error.
|
|
230
317
|
*
|
|
231
318
|
* @param value - Unknown metadata
|
|
232
319
|
* @returns Owned driver metadata
|
|
@@ -245,7 +332,12 @@ function cloneDriverMetadata(value) {
|
|
|
245
332
|
}
|
|
246
333
|
}
|
|
247
334
|
/**
|
|
248
|
-
*
|
|
335
|
+
* Clones unknown driver schema into a distinct deeply frozen snapshot.
|
|
336
|
+
*
|
|
337
|
+
* @remarks
|
|
338
|
+
* The clone is validated as a table-schema collection before it is returned, so a
|
|
339
|
+
* malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
|
|
340
|
+
* `context.path === 'schema'` rather than surfacing a raw Contract or caller error.
|
|
249
341
|
*
|
|
250
342
|
* @param value - Unknown table schema collection
|
|
251
343
|
* @returns Owned driver schema
|
|
@@ -264,7 +356,12 @@ function cloneDriverSchema(value) {
|
|
|
264
356
|
}
|
|
265
357
|
}
|
|
266
358
|
/**
|
|
267
|
-
*
|
|
359
|
+
* Clones unknown migration input into a distinct deeply frozen snapshot.
|
|
360
|
+
*
|
|
361
|
+
* @remarks
|
|
362
|
+
* The clone is validated as a {@link MigrationInput} before it is returned, so a
|
|
363
|
+
* malformed or hostile value throws a `VALIDATION` {@link DatabaseError} at
|
|
364
|
+
* `context.path === 'migration'` rather than surfacing a raw Contract or caller error.
|
|
268
365
|
*
|
|
269
366
|
* @param value - Unknown migration input
|
|
270
367
|
* @returns Owned migration input
|
|
@@ -285,8 +382,32 @@ function cloneMigrationInput(value) {
|
|
|
285
382
|
//#endregion
|
|
286
383
|
//#region src/core/helpers.ts
|
|
287
384
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
385
|
+
* Validates the paging fields of a portable query.
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* A present `limit` or `offset` must be a finite nonnegative integer; zero is
|
|
389
|
+
* valid. Validation is deterministic (`limit` before `offset`). Non-finite
|
|
390
|
+
* values are rendered as strings in error context so JSON serialization cannot
|
|
391
|
+
* collapse `NaN` or infinity to `null`.
|
|
392
|
+
*
|
|
393
|
+
* @param input - The portable query whose paging fields to validate
|
|
394
|
+
* @throws {@link DatabaseError} `VALIDATION` when a paging field is invalid
|
|
395
|
+
*/
|
|
396
|
+
function validatePage(input) {
|
|
397
|
+
const limit = input?.limit;
|
|
398
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new DatabaseError("VALIDATION", "Query limit must be a nonnegative integer", {
|
|
399
|
+
field: "limit",
|
|
400
|
+
value: Number.isFinite(limit) ? limit : String(limit)
|
|
401
|
+
});
|
|
402
|
+
const offset = input?.offset;
|
|
403
|
+
if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) throw new DatabaseError("VALIDATION", "Query offset must be a nonnegative integer", {
|
|
404
|
+
field: "offset",
|
|
405
|
+
value: Number.isFinite(offset) ? offset : String(offset)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Compares two arbitrary values under one total order — the comparator behind
|
|
410
|
+
* sorting and the range operators.
|
|
290
411
|
*
|
|
291
412
|
* @remarks
|
|
292
413
|
* Values of different types order by a fixed type rank (`undefined` < `null` <
|
|
@@ -310,15 +431,16 @@ function compareValues(left, right) {
|
|
|
310
431
|
return 0;
|
|
311
432
|
}
|
|
312
433
|
/**
|
|
313
|
-
*
|
|
314
|
-
* checks and any test/fixture that needs "same data", not
|
|
434
|
+
* Compares two values structurally by SameValueZero leaves — the comparator
|
|
435
|
+
* behind conformance checks and any test/fixture that needs "same data", not
|
|
436
|
+
* "same reference".
|
|
315
437
|
*
|
|
316
438
|
* @remarks
|
|
317
439
|
* Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
|
|
318
440
|
* Arrays compare by index (same length, every element `equalsValue`). Plain
|
|
319
|
-
* records (
|
|
320
|
-
*
|
|
321
|
-
* with a `equalsValue` value — so a key present with value `undefined` is
|
|
441
|
+
* records (through `isRecord`) compare by their own enumerable keys: same key
|
|
442
|
+
* count and, for every key in `left`, `right` has that key (`Object.hasOwn`)
|
|
443
|
+
* with a `equalsValue` value — so a key present with value `undefined` is not
|
|
322
444
|
* equal to that key being absent (both differ in `Object.keys` membership).
|
|
323
445
|
* Anything else (functions, class instances, mismatched shapes) falls through
|
|
324
446
|
* to `false`. Container pairs are tracked iteratively, so self-referential and
|
|
@@ -327,7 +449,7 @@ function compareValues(left, right) {
|
|
|
327
449
|
*
|
|
328
450
|
* @param left - The left value
|
|
329
451
|
* @param right - The right value
|
|
330
|
-
* @returns
|
|
452
|
+
* @returns True if `left` and `right` are structurally equal; false otherwise
|
|
331
453
|
*
|
|
332
454
|
* @example
|
|
333
455
|
* ```ts
|
|
@@ -384,47 +506,16 @@ function equalsValue(left, right) {
|
|
|
384
506
|
}
|
|
385
507
|
}
|
|
386
508
|
/**
|
|
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
|
|
509
|
+
* Matches a value against a wildcard pattern in linear time — the shared, ReDoS-safe
|
|
419
510
|
* engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
|
|
420
511
|
*
|
|
421
512
|
* @remarks
|
|
422
|
-
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is
|
|
513
|
+
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is catastrophic on a hostile pattern:
|
|
423
514
|
* `.*` 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
|
-
*
|
|
427
|
-
* the `any` wildcard records its position and, on a later mismatch, backtracks
|
|
515
|
+
* up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it,
|
|
516
|
+
* while a `LIKE` / `GLOB` pattern is a caller-supplied operand this package cannot
|
|
517
|
+
* trust. So this builds no regex. It runs the classic greedy two-pointer wildcard match:
|
|
518
|
+
* the `any` wildcard records its position and, on a later mismatch, backtracks only to
|
|
428
519
|
* that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
|
|
429
520
|
* never the exponential / polynomial backtracking a regex would do. The pattern length
|
|
430
521
|
* is capped at {@link MAX_PATTERN_LENGTH} (a `VALIDATION` {@link DatabaseError} over it),
|
|
@@ -432,17 +523,17 @@ function matchesFuzzy(value, query) {
|
|
|
432
523
|
* pattern.
|
|
433
524
|
*
|
|
434
525
|
* The `any` wildcard matches any run (including empty); `single` matches exactly one
|
|
435
|
-
* char; every other pattern char matches itself
|
|
526
|
+
* char; every other pattern char matches itself literally (a pattern `.` / `(` / `\` is
|
|
436
527
|
* a literal — the regex-metacharacter hazard is gone with the regex). `any` is tested
|
|
437
|
-
*
|
|
438
|
-
* shadows the wildcard. Case folding is applied to
|
|
528
|
+
* before a literal match, so a value that literally contains the wildcard char never
|
|
529
|
+
* shadows the wildcard. Case folding is applied to both sides when `fold` is set.
|
|
439
530
|
*
|
|
440
531
|
* @param value - The value to test
|
|
441
532
|
* @param pattern - The wildcard pattern
|
|
442
533
|
* @param any - The any-run wildcard char (`%` for `LIKE`, `*` for `GLOB`)
|
|
443
534
|
* @param single - The single-char wildcard char (`_` for `LIKE`, `?` for `GLOB`)
|
|
444
|
-
* @param fold - Whether to match case-
|
|
445
|
-
* @returns
|
|
535
|
+
* @param fold - Whether to match case-insensitively (`LIKE` folds; `GLOB` does not)
|
|
536
|
+
* @returns True if `value` matches `pattern`; false otherwise
|
|
446
537
|
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
447
538
|
*/
|
|
448
539
|
function matchesWildcardPattern(value, pattern, any, single, fold) {
|
|
@@ -474,32 +565,73 @@ function matchesWildcardPattern(value, pattern, any, single, fold) {
|
|
|
474
565
|
while (pi < needle.length && needle[pi] === any) pi += 1;
|
|
475
566
|
return pi === needle.length;
|
|
476
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Matches a value against a SQL `LIKE` pattern, folding case.
|
|
570
|
+
*
|
|
571
|
+
* @remarks
|
|
572
|
+
* `%` matches any run of characters (including none) and `_` matches exactly one
|
|
573
|
+
* character; every other pattern character matches itself literally. Runs on
|
|
574
|
+
* {@link matchesWildcardPattern}, so the match is linear in the value length and
|
|
575
|
+
* the pattern is capped at {@link MAX_PATTERN_LENGTH}.
|
|
576
|
+
*
|
|
577
|
+
* @param value - The value to test
|
|
578
|
+
* @param pattern - The `LIKE` pattern
|
|
579
|
+
* @returns True if `value` matches `pattern` under case folding; false otherwise
|
|
580
|
+
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```ts
|
|
584
|
+
* matchesLikePattern('Hello', 'h%o') // true — `%` spans any run, and case folds
|
|
585
|
+
* matchesLikePattern('Hello', 'h_llo') // true — `_` matches exactly one character
|
|
586
|
+
* ```
|
|
587
|
+
*/
|
|
477
588
|
function matchesLikePattern(value, pattern) {
|
|
478
589
|
return matchesWildcardPattern(value, pattern, "%", "_", true);
|
|
479
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Matches a value against a `GLOB` pattern, preserving case.
|
|
593
|
+
*
|
|
594
|
+
* @remarks
|
|
595
|
+
* `*` matches any run of characters (including none) and `?` matches exactly one
|
|
596
|
+
* character; every other pattern character matches itself literally, so a
|
|
597
|
+
* character class such as `[a-z]` is not interpreted. Runs on
|
|
598
|
+
* {@link matchesWildcardPattern}, so the match is linear in the value length and
|
|
599
|
+
* the pattern is capped at {@link MAX_PATTERN_LENGTH}.
|
|
600
|
+
*
|
|
601
|
+
* @param value - The value to test
|
|
602
|
+
* @param pattern - The `GLOB` pattern
|
|
603
|
+
* @returns True if `value` matches `pattern` case-sensitively; false otherwise
|
|
604
|
+
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* matchesGlobPattern('hello', 'h*o') // true — `*` spans any run
|
|
609
|
+
* matchesGlobPattern('Hello', 'h*o') // false — `GLOB` is case-sensitive
|
|
610
|
+
* ```
|
|
611
|
+
*/
|
|
480
612
|
function matchesGlobPattern(value, pattern) {
|
|
481
613
|
return matchesWildcardPattern(value, pattern, "*", "?", false);
|
|
482
614
|
}
|
|
483
615
|
/**
|
|
484
|
-
*
|
|
616
|
+
* Evaluates one {@link Condition} against a row — the per-operator predicate.
|
|
485
617
|
*
|
|
486
618
|
* @remarks
|
|
487
619
|
* Reads the condition's column — a `FieldPath`, resolved with `resolveField` (a
|
|
488
620
|
* string is one column; an array descends a nested value) — and applies the
|
|
489
621
|
* operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
|
|
490
622
|
* {@link compareValues}, the total order; the equality family (`equals` / `not`
|
|
491
|
-
* / `any` / `none`) uses {@link equalsValue} —
|
|
623
|
+
* / `any` / `none`) uses {@link equalsValue} — structural equality, not the total
|
|
492
624
|
* order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
|
|
493
625
|
* operand only matches a structurally-equal value, never every row holding any
|
|
494
626
|
* object. This is a semantics change from ranking: `equalsValue` is SameValueZero
|
|
495
627
|
* on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
|
|
496
628
|
* anything under the old rank-based comparison). `like` / `glob` / `starts` /
|
|
497
629
|
* `ends` match only strings; `absent` / `present` test nullishness. Total — a
|
|
498
|
-
* type mismatch is
|
|
630
|
+
* type mismatch is a non-match.
|
|
499
631
|
*
|
|
500
632
|
* @param row - The row to test
|
|
501
633
|
* @param condition - The condition to apply
|
|
502
|
-
* @returns
|
|
634
|
+
* @returns True if the row satisfies the condition; false otherwise
|
|
503
635
|
*/
|
|
504
636
|
function matchesCondition(row, condition) {
|
|
505
637
|
const value = resolveField(row, condition.column);
|
|
@@ -524,7 +656,7 @@ function matchesCondition(row, condition) {
|
|
|
524
656
|
}
|
|
525
657
|
}
|
|
526
658
|
/**
|
|
527
|
-
*
|
|
659
|
+
* Folds a row through a list of conditions, joining each by its connector.
|
|
528
660
|
*
|
|
529
661
|
* @remarks
|
|
530
662
|
* Evaluated left-to-right: the first condition seeds the result, and each later
|
|
@@ -534,7 +666,7 @@ function matchesCondition(row, condition) {
|
|
|
534
666
|
*
|
|
535
667
|
* @param row - The row to test
|
|
536
668
|
* @param conditions - The conditions to fold
|
|
537
|
-
* @returns
|
|
669
|
+
* @returns True if the row satisfies the combined conditions; false otherwise
|
|
538
670
|
*/
|
|
539
671
|
function matchesQuery(row, conditions) {
|
|
540
672
|
let result = true;
|
|
@@ -549,7 +681,7 @@ function matchesQuery(row, conditions) {
|
|
|
549
681
|
return result;
|
|
550
682
|
}
|
|
551
683
|
/**
|
|
552
|
-
*
|
|
684
|
+
* Filters rows by a list of conditions — the shared basis for a table's count
|
|
553
685
|
* and aggregate paths (no sort/page, unlike {@link applyQuery}).
|
|
554
686
|
*
|
|
555
687
|
* @remarks
|
|
@@ -573,7 +705,7 @@ function filterRows(rows, conditions) {
|
|
|
573
705
|
return rows.filter((row) => matchesQuery(row, conditions));
|
|
574
706
|
}
|
|
575
707
|
/**
|
|
576
|
-
*
|
|
708
|
+
* Sorts rows by an ordering specification, leaving the input untouched.
|
|
577
709
|
*
|
|
578
710
|
* @remarks
|
|
579
711
|
* Applies the terms in priority order — the first term that distinguishes two
|
|
@@ -595,7 +727,7 @@ function sortRows(rows, order) {
|
|
|
595
727
|
return sorted;
|
|
596
728
|
}
|
|
597
729
|
/**
|
|
598
|
-
*
|
|
730
|
+
* Applies a {@link QueryInput} to rows — filter, then sort, then page.
|
|
599
731
|
*
|
|
600
732
|
* @remarks
|
|
601
733
|
* The whole portable read pipeline in one place: conditions filter, `order`
|
|
@@ -620,7 +752,7 @@ function applyQuery(rows, input) {
|
|
|
620
752
|
return result;
|
|
621
753
|
}
|
|
622
754
|
/**
|
|
623
|
-
*
|
|
755
|
+
* Computes an aggregate over a column across rows.
|
|
624
756
|
*
|
|
625
757
|
* @remarks
|
|
626
758
|
* `count` returns the row count. The numeric aggregates coerce each cell with
|
|
@@ -646,10 +778,12 @@ function computeAggregate(rows, operation, column) {
|
|
|
646
778
|
const total = numbers.reduce((sum, value) => sum + value, 0);
|
|
647
779
|
return operation === "average" ? total / numbers.length : total;
|
|
648
780
|
}
|
|
649
|
-
|
|
781
|
+
let result = operation === "minimum" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
782
|
+
for (const value of numbers) result = operation === "minimum" ? Math.min(result, value) : Math.max(result, value);
|
|
783
|
+
return result;
|
|
650
784
|
}
|
|
651
785
|
/**
|
|
652
|
-
*
|
|
786
|
+
* Reads a row's primary key from a column, when it is a usable {@link Key}.
|
|
653
787
|
*
|
|
654
788
|
* @param row - The row to read
|
|
655
789
|
* @param column - The primary-key column name
|
|
@@ -660,7 +794,7 @@ function extractKey(row, column) {
|
|
|
660
794
|
return isKey(value) ? value : void 0;
|
|
661
795
|
}
|
|
662
796
|
/**
|
|
663
|
-
*
|
|
797
|
+
* Returns a fresh row whose primary column is authoritatively bound to its storage key.
|
|
664
798
|
*
|
|
665
799
|
* @param row - The caller row
|
|
666
800
|
* @param primary - The primary column
|
|
@@ -674,7 +808,7 @@ function bindRowKey(row, primary, key) {
|
|
|
674
808
|
};
|
|
675
809
|
}
|
|
676
810
|
/**
|
|
677
|
-
*
|
|
811
|
+
* Maps a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
|
|
678
812
|
* value a `TableSchema` carries so a native backend can declare a real column.
|
|
679
813
|
*
|
|
680
814
|
* @remarks
|
|
@@ -698,7 +832,7 @@ function bindRowKey(row, primary, key) {
|
|
|
698
832
|
* ```
|
|
699
833
|
*/
|
|
700
834
|
function shapeToColumnStorage(shape) {
|
|
701
|
-
switch (shape.
|
|
835
|
+
switch (shape.category) {
|
|
702
836
|
case "string": return "text";
|
|
703
837
|
case "number": return shape.integer === true ? "integer" : "real";
|
|
704
838
|
case "boolean": return "boolean";
|
|
@@ -717,7 +851,7 @@ function shapeToColumnStorage(shape) {
|
|
|
717
851
|
}
|
|
718
852
|
}
|
|
719
853
|
/**
|
|
720
|
-
*
|
|
854
|
+
* Projects one contract shape into a portable column schema.
|
|
721
855
|
*
|
|
722
856
|
* @param name - The column name
|
|
723
857
|
* @param shape - The column contract shape
|
|
@@ -733,7 +867,55 @@ function shapeToColumnSchema(name, shape) {
|
|
|
733
867
|
};
|
|
734
868
|
}
|
|
735
869
|
/**
|
|
736
|
-
*
|
|
870
|
+
* Reads one flat column's declaration out of a table schema.
|
|
871
|
+
*
|
|
872
|
+
* @remarks
|
|
873
|
+
* The single lookup behind every declared-column question — storage type,
|
|
874
|
+
* optionality, and nullability all come off the returned {@link ColumnSchema},
|
|
875
|
+
* so a caller that needs more than one of them reads them from one result. A
|
|
876
|
+
* nested {@link FieldPath} names no declared column, so resolve the path's head
|
|
877
|
+
* before calling. A schema that does not declare the column returns `undefined`.
|
|
878
|
+
*
|
|
879
|
+
* @param name - The flat column name
|
|
880
|
+
* @param schema - The table's schema
|
|
881
|
+
* @returns The column's {@link ColumnSchema}, or `undefined` when the schema does not declare it
|
|
882
|
+
*
|
|
883
|
+
* @example
|
|
884
|
+
* ```ts
|
|
885
|
+
* findColumn('age', schema)?.storage // 'integer'
|
|
886
|
+
* findColumn('absent', schema) // undefined
|
|
887
|
+
* ```
|
|
888
|
+
*/
|
|
889
|
+
function findColumn(name, schema) {
|
|
890
|
+
return schema.columns.find((candidate) => candidate.name === name);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Resolves the primary-key column one table keys its rows by.
|
|
894
|
+
*
|
|
895
|
+
* @remarks
|
|
896
|
+
* A table absent from the {@link PrimaryMap} keys its rows by
|
|
897
|
+
* {@link DEFAULT_PRIMARY}, so this is total over any table name.
|
|
898
|
+
*
|
|
899
|
+
* @param primary - The per-table primary-key overrides
|
|
900
|
+
* @param name - The table name
|
|
901
|
+
* @returns The table's primary-key column
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* ```ts
|
|
905
|
+
* resolvePrimary({ posts: 'slug' }, 'posts') // 'slug'
|
|
906
|
+
* resolvePrimary({ posts: 'slug' }, 'users') // 'id' — the default primary
|
|
907
|
+
* ```
|
|
908
|
+
*/
|
|
909
|
+
function resolvePrimary(primary, name) {
|
|
910
|
+
return primary[name] ?? "id";
|
|
911
|
+
}
|
|
912
|
+
function requireColumns(tables, name) {
|
|
913
|
+
const columns = tables[name];
|
|
914
|
+
if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
|
|
915
|
+
return columns;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Throws when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
|
|
737
919
|
* abort gate checked at operation boundaries and between streamed rows.
|
|
738
920
|
*
|
|
739
921
|
* @remarks
|
|
@@ -761,7 +943,7 @@ function checkAbort(signal) {
|
|
|
761
943
|
if (signal?.aborted) throw new DatabaseError("ABORTED", "Operation aborted", { reason: signal.reason });
|
|
762
944
|
}
|
|
763
945
|
/**
|
|
764
|
-
*
|
|
946
|
+
* Diffs a deployed and a declared table set structurally into a {@link Migration}
|
|
765
947
|
* plan.
|
|
766
948
|
*
|
|
767
949
|
* @remarks
|
|
@@ -776,17 +958,16 @@ function checkAbort(signal) {
|
|
|
776
958
|
* plan labels only; versioning drivers persist and reconcile them through
|
|
777
959
|
* {@link DriverMetadata}.
|
|
778
960
|
*
|
|
779
|
-
* A column present in
|
|
961
|
+
* A column present in both schemas under the same name but with a different
|
|
780
962
|
* `storage`, `optional`, or `nullable` value throws a `MIGRATION`
|
|
781
|
-
* {@link DatabaseError} naming the
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
* @param deployed - The table schemas currently applied
|
|
963
|
+
* {@link DatabaseError} naming the table, the column, and the from→to
|
|
964
|
+
* difference — a name-only diff would otherwise silently produce no step for
|
|
965
|
+
* the drift, and versioned reconciliation would stamp over it. There is no
|
|
966
|
+
* automatic in-place type-change step: the manual path is to add a new column,
|
|
967
|
+
* copy/convert the data at the application layer, then remove the old column —
|
|
968
|
+
* two separate plans, never a single implicit "alter" step.
|
|
969
|
+
*
|
|
970
|
+
* @param deployed - The already-applied table schemas
|
|
790
971
|
* @param declared - The table schemas the caller wants applied
|
|
791
972
|
* @param from - The plan's source version label (defaults to `0`)
|
|
792
973
|
* @param to - The plan's target version label (defaults to `1`)
|
|
@@ -892,7 +1073,9 @@ function planMigration(deployed, declared, from = 0, to = 1) {
|
|
|
892
1073
|
} }).plan;
|
|
893
1074
|
}
|
|
894
1075
|
/**
|
|
895
|
-
*
|
|
1076
|
+
* Projects migration steps sequentially over a canonical validated owned schema.
|
|
1077
|
+
*
|
|
1078
|
+
* @remarks
|
|
896
1079
|
* Adding a required non-null column to an existing table rejects with
|
|
897
1080
|
* `MIGRATION`; optional-only and nullable-only additions remain portable.
|
|
898
1081
|
*
|
|
@@ -991,7 +1174,7 @@ function projectMigrationSchema(schema, steps) {
|
|
|
991
1174
|
}
|
|
992
1175
|
}
|
|
993
1176
|
/**
|
|
994
|
-
*
|
|
1177
|
+
* Canonicalizes an unknown driver schema into a distinct deeply frozen snapshot.
|
|
995
1178
|
*
|
|
996
1179
|
* @remarks
|
|
997
1180
|
* Table and column lists are sorted by name. The index list is sorted by the
|
|
@@ -1013,11 +1196,11 @@ function normalizeDriverSchema(value) {
|
|
|
1013
1196
|
return cloneDriverSchema(tables);
|
|
1014
1197
|
}
|
|
1015
1198
|
/**
|
|
1016
|
-
*
|
|
1199
|
+
* Applies one table's {@link MigrationStep}s to its rows — a pure row transform.
|
|
1017
1200
|
*
|
|
1018
1201
|
* @remarks
|
|
1019
1202
|
* `column.remove` drops that field from every row (a fresh copy — inputs are
|
|
1020
|
-
* never mutated
|
|
1203
|
+
* never mutated); `column.add` leaves rows as-is (an absent field
|
|
1021
1204
|
* reads as `undefined`, backfill is application policy). `table.add` /
|
|
1022
1205
|
* `table.remove` / `index.add` / `index.remove` are no-ops here (they operate
|
|
1023
1206
|
* on storage shape, not row shape). Steps for tables other than the one
|
|
@@ -1044,7 +1227,7 @@ function migrateRows(rows, steps) {
|
|
|
1044
1227
|
});
|
|
1045
1228
|
}
|
|
1046
1229
|
/**
|
|
1047
|
-
*
|
|
1230
|
+
* Walks the driver-conformance battery against a fresh {@link DriverInterface}
|
|
1048
1231
|
* per phase, yielding one {@link ConformanceFinding} per violated invariant —
|
|
1049
1232
|
* the shared invariant suite every backend (in-memory, SQLite, IndexedDB)
|
|
1050
1233
|
* must uphold to be a drop-in {@link DriverInterface}.
|
|
@@ -1055,34 +1238,34 @@ function migrateRows(rows, steps) {
|
|
|
1055
1238
|
* driver's own README. Opens a fixed two-table schema (`users` keyed by the
|
|
1056
1239
|
* default `id`, `posts` keyed by a non-id `slug`) and, calling `factory()`
|
|
1057
1240
|
* fresh for each phase so failures stay isolated, verifies: `open`/`close`;
|
|
1058
|
-
* `read` of a missing key returns `undefined`; `write`/`read` round-trip
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
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
|
-
*
|
|
1241
|
+
* `read` of a missing key returns `undefined`; `write`/`read` round-trip
|
|
1242
|
+
* with deep copy-in/copy-out isolation (mutating the caller's row —
|
|
1243
|
+
* including a nested field — after `write`, or a row `read` returns, never
|
|
1244
|
+
* perturbs stored state) and upsert-overwrite; simultaneous same-key
|
|
1245
|
+
* `insert` calls produce exactly one commit and one `CONFLICT`; pre-aborted
|
|
1246
|
+
* `write`, `insert`, and `delete` calls leave storage unchanged; `delete`
|
|
1247
|
+
* returns `true` then `false`; `keys`/`scan` yield in ascending key order;
|
|
1248
|
+
* `clear` empties only its target table; `snapshot`'s rollback thunk
|
|
1249
|
+
* restores pre-snapshot state, including a nested field mutated in place on
|
|
1250
|
+
* a read-back row between capture and restore; a scoped
|
|
1251
|
+
* `snapshot(['users'])` rolls back only the named table, leaving a
|
|
1252
|
+
* concurrent mutation to another table intact; a non-`id` primary key
|
|
1253
|
+
* (`posts.slug`) round-trips; a nested-object row round-trips structurally
|
|
1254
|
+
* (through {@link equalsValue}). The optional surface is presence-gated: when
|
|
1255
|
+
* `migrate` exists, a `column.remove` plan strips the column from stored
|
|
1256
|
+
* rows and a plan referencing an unknown table throws `DatabaseError`
|
|
1257
|
+
* `MIGRATION`; when `stream` exists, it yields only condition-matching rows
|
|
1258
|
+
* and honors `offset`/`limit`; when `transaction` exists, `commit` persists
|
|
1259
|
+
* and `rollback` restores; when both `metadata` and `stamp` exist, a fresh
|
|
1260
|
+
* store's `metadata()` is `undefined`, and after
|
|
1078
1261
|
* `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
|
|
1079
1262
|
*
|
|
1080
|
-
* Each phase runs within a `try`/`catch`: an
|
|
1081
|
-
* finding built from the assertion, while an
|
|
1263
|
+
* Each phase runs within a `try`/`catch`: an expected mismatch yields a
|
|
1264
|
+
* finding built from the assertion, while an unexpected throw (a driver
|
|
1082
1265
|
* crash mid-phase) is caught and yielded as a finding too, naming the phase
|
|
1083
1266
|
* as `check` and carrying the caught error in `context.error` — a broken
|
|
1084
1267
|
* driver can never escape the battery as an unhandled rejection. Within a
|
|
1085
|
-
* phase, the
|
|
1268
|
+
* phase, the first violated assertion yields and the phase stops (matching
|
|
1086
1269
|
* the historical fail-fast shape at phase granularity); the generator then
|
|
1087
1270
|
* moves on to the next phase regardless. Because this is a **generator**,
|
|
1088
1271
|
* consuming only the first yielded value reproduces true fail-fast (later
|
|
@@ -1093,62 +1276,14 @@ function migrateRows(rows, steps) {
|
|
|
1093
1276
|
*
|
|
1094
1277
|
* @example
|
|
1095
1278
|
* ```ts
|
|
1096
|
-
* import { createMemoryDriver,
|
|
1279
|
+
* import { createMemoryDriver, scanDriver } from '@orkestrel/database'
|
|
1097
1280
|
*
|
|
1098
|
-
* for await (const finding of
|
|
1281
|
+
* for await (const finding of scanDriver(() => createMemoryDriver())) {
|
|
1099
1282
|
* console.log(finding.check, finding.message)
|
|
1100
1283
|
* }
|
|
1101
1284
|
* ```
|
|
1102
1285
|
*/
|
|
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];
|
|
1286
|
+
async function* scanDriver(factory) {
|
|
1152
1287
|
try {
|
|
1153
1288
|
const driver = factory();
|
|
1154
1289
|
await driver.open(CONFORMANCE_SCHEMA);
|
|
@@ -1935,17 +2070,19 @@ async function* driverFindings(factory) {
|
|
|
1935
2070
|
}
|
|
1936
2071
|
}
|
|
1937
2072
|
/**
|
|
1938
|
-
*
|
|
2073
|
+
* Runs the driver-conformance battery, throwing on the first violated
|
|
1939
2074
|
* invariant — the fail-fast entry point most callers (test setup, CI smoke
|
|
1940
2075
|
* checks) want.
|
|
1941
2076
|
*
|
|
1942
2077
|
* @remarks
|
|
1943
|
-
*
|
|
1944
|
-
* lazy,
|
|
1945
|
-
*
|
|
2078
|
+
* Consumes only the first value {@link scanDriver} yields: because that
|
|
2079
|
+
* generator is lazy, every later phase never runs — true fail-fast, not
|
|
2080
|
+
* merely "report only the first". The
|
|
1946
2081
|
* thrown error is byte-compatible with the historical shape: a
|
|
1947
2082
|
* `CONFORMANCE` {@link DatabaseError} whose `message` is the finding's
|
|
1948
|
-
* `message` and whose `context` is `{ check, ...finding.context }`.
|
|
2083
|
+
* `message` and whose `context` is `{ check, ...finding.context }`. The battery
|
|
2084
|
+
* takes a driver factory and reports through a throw, so it binds no test
|
|
2085
|
+
* framework and runs from any runner.
|
|
1949
2086
|
*
|
|
1950
2087
|
* @param factory - Mints a fresh, unopened driver instance (called once per phase)
|
|
1951
2088
|
* @returns Nothing — resolves once every phase has passed
|
|
@@ -1959,18 +2096,18 @@ async function* driverFindings(factory) {
|
|
|
1959
2096
|
* ```
|
|
1960
2097
|
*/
|
|
1961
2098
|
async function conformDriver(factory) {
|
|
1962
|
-
for await (const finding of
|
|
2099
|
+
for await (const finding of scanDriver(factory)) throw new DatabaseError("CONFORMANCE", finding.message, {
|
|
1963
2100
|
check: finding.check,
|
|
1964
2101
|
...finding.context
|
|
1965
2102
|
});
|
|
1966
2103
|
}
|
|
1967
2104
|
/**
|
|
1968
|
-
*
|
|
2105
|
+
* Runs the full driver-conformance battery and collects every violation — the
|
|
1969
2106
|
* audit entry point for a driver author who wants a complete report rather
|
|
1970
2107
|
* than a single fail-fast throw.
|
|
1971
2108
|
*
|
|
1972
2109
|
* @remarks
|
|
1973
|
-
* Drains {@link
|
|
2110
|
+
* Drains {@link scanDriver} to completion: every phase runs regardless
|
|
1974
2111
|
* of earlier violations, so a driver breaking two independent invariants
|
|
1975
2112
|
* reports both. An empty array means the driver is fully conformant.
|
|
1976
2113
|
*
|
|
@@ -1987,26 +2124,32 @@ async function conformDriver(factory) {
|
|
|
1987
2124
|
*/
|
|
1988
2125
|
async function auditDriver(factory) {
|
|
1989
2126
|
const findings = [];
|
|
1990
|
-
for await (const finding of
|
|
2127
|
+
for await (const finding of scanDriver(factory)) findings.push(finding);
|
|
1991
2128
|
return findings;
|
|
1992
2129
|
}
|
|
1993
2130
|
//#endregion
|
|
1994
|
-
//#region src/core/
|
|
2131
|
+
//#region src/core/ScopedIterator.ts
|
|
1995
2132
|
/**
|
|
1996
|
-
*
|
|
2133
|
+
* Forms the internal continuation admission boundary for one scoped async iterable.
|
|
1997
2134
|
*
|
|
1998
2135
|
* @remarks
|
|
1999
|
-
* Each
|
|
2000
|
-
* so an idle iterator never
|
|
2001
|
-
*
|
|
2136
|
+
* Each continuation enters the owning {@link AdmissionInterface} ledger
|
|
2137
|
+
* independently, so an idle iterator never delays a transaction, a settlement,
|
|
2138
|
+
* or a close. `ready` runs inside the tracked continuation before the source
|
|
2139
|
+
* advances, which is where a root stream re-establishes the lazy connection and
|
|
2140
|
+
* a transaction-scoped stream does nothing. A continuation requested after
|
|
2141
|
+
* admission closes attempts source cleanup exactly once and leaves the iterator
|
|
2142
|
+
* terminal.
|
|
2002
2143
|
*/
|
|
2003
|
-
var
|
|
2144
|
+
var ScopedIterator = class {
|
|
2004
2145
|
#source;
|
|
2005
|
-
#
|
|
2146
|
+
#admission;
|
|
2147
|
+
#ready;
|
|
2006
2148
|
#cleaned = false;
|
|
2007
|
-
constructor(source,
|
|
2149
|
+
constructor(source, admission, ready) {
|
|
2008
2150
|
this.#source = source[Symbol.asyncIterator]();
|
|
2009
|
-
this.#
|
|
2151
|
+
this.#admission = admission;
|
|
2152
|
+
this.#ready = ready;
|
|
2010
2153
|
}
|
|
2011
2154
|
[Symbol.asyncIterator]() {
|
|
2012
2155
|
return this;
|
|
@@ -2025,6 +2168,7 @@ var TransactionIterator = class {
|
|
|
2025
2168
|
done: true,
|
|
2026
2169
|
value: void 0
|
|
2027
2170
|
};
|
|
2171
|
+
await this.#ready();
|
|
2028
2172
|
const result = await this.#source.next();
|
|
2029
2173
|
if (result.done === true) this.#cleaned = true;
|
|
2030
2174
|
return result;
|
|
@@ -2052,8 +2196,8 @@ var TransactionIterator = class {
|
|
|
2052
2196
|
throw error;
|
|
2053
2197
|
}
|
|
2054
2198
|
#continue(operation) {
|
|
2055
|
-
if (!this.#
|
|
2056
|
-
return this.#
|
|
2199
|
+
if (!this.#admission.accepting) this.#cleanup();
|
|
2200
|
+
return this.#admission.track(operation);
|
|
2057
2201
|
}
|
|
2058
2202
|
#cleanup() {
|
|
2059
2203
|
if (this.#cleaned) return;
|
|
@@ -2066,7 +2210,7 @@ var TransactionIterator = class {
|
|
|
2066
2210
|
//#endregion
|
|
2067
2211
|
//#region src/core/TransactionScope.ts
|
|
2068
2212
|
/**
|
|
2069
|
-
*
|
|
2213
|
+
* Forms the internal lifetime boundary for one database transaction callback.
|
|
2070
2214
|
*
|
|
2071
2215
|
* @remarks
|
|
2072
2216
|
* Promise operations enter synchronously through {@link track}. Closing stops new
|
|
@@ -2110,7 +2254,7 @@ var TransactionScope = class {
|
|
|
2110
2254
|
return promise;
|
|
2111
2255
|
}
|
|
2112
2256
|
stream(source) {
|
|
2113
|
-
return new
|
|
2257
|
+
return new ScopedIterator(source, this, () => Promise.resolve());
|
|
2114
2258
|
}
|
|
2115
2259
|
stop() {
|
|
2116
2260
|
this.#accepting = false;
|
|
@@ -2123,7 +2267,7 @@ var TransactionScope = class {
|
|
|
2123
2267
|
//#endregion
|
|
2124
2268
|
//#region src/core/DatabaseContext.ts
|
|
2125
2269
|
/**
|
|
2126
|
-
*
|
|
2270
|
+
* Owns the internal shared state behind every typed view of one database.
|
|
2127
2271
|
*
|
|
2128
2272
|
* @remarks
|
|
2129
2273
|
* A context owns the driver, merged physical schema, lifecycle, observation,
|
|
@@ -2207,7 +2351,21 @@ var DatabaseContext = class {
|
|
|
2207
2351
|
this.#emitter.emit("close");
|
|
2208
2352
|
}
|
|
2209
2353
|
connect() {
|
|
2210
|
-
return this.#
|
|
2354
|
+
if (this.#ready !== void 0) return this.#ready;
|
|
2355
|
+
if (this.#failure !== void 0) throw this.#failure.error;
|
|
2356
|
+
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2357
|
+
const readiness = this.#driver.open(this.#schema).then(async () => {
|
|
2358
|
+
if (this.#status === "idle") {
|
|
2359
|
+
this.#status = "open";
|
|
2360
|
+
this.#emitter.emit("open");
|
|
2361
|
+
}
|
|
2362
|
+
await this.#reconcile();
|
|
2363
|
+
}).catch((error) => {
|
|
2364
|
+
if (this.#ready === readiness) this.#ready = void 0;
|
|
2365
|
+
throw error;
|
|
2366
|
+
});
|
|
2367
|
+
this.#ready = readiness;
|
|
2368
|
+
return readiness;
|
|
2211
2369
|
}
|
|
2212
2370
|
track(operation) {
|
|
2213
2371
|
try {
|
|
@@ -2236,7 +2394,7 @@ var DatabaseContext = class {
|
|
|
2236
2394
|
this.#transaction = token;
|
|
2237
2395
|
try {
|
|
2238
2396
|
await this.#drain();
|
|
2239
|
-
await this
|
|
2397
|
+
await this.connect();
|
|
2240
2398
|
if (this.#driver.transaction !== void 0) {
|
|
2241
2399
|
const rejection = {
|
|
2242
2400
|
rejected: false,
|
|
@@ -2312,23 +2470,6 @@ var DatabaseContext = class {
|
|
|
2312
2470
|
#outside() {
|
|
2313
2471
|
if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
|
|
2314
2472
|
}
|
|
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
2473
|
async #drain() {
|
|
2333
2474
|
if (this.#operations.size === 0) return;
|
|
2334
2475
|
await Promise.allSettled(this.#operations);
|
|
@@ -2411,12 +2552,12 @@ var DatabaseContext = class {
|
|
|
2411
2552
|
//#endregion
|
|
2412
2553
|
//#region src/core/Cursor.ts
|
|
2413
2554
|
/**
|
|
2414
|
-
*
|
|
2555
|
+
* Walks a table's rows forward for bulk in-place mutation.
|
|
2415
2556
|
*
|
|
2416
2557
|
* @remarks
|
|
2417
2558
|
* Iterates a snapshot of the table's keys captured when the cursor was opened,
|
|
2418
2559
|
* 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
|
|
2560
|
+
* iteration cannot corrupt the walk, and a key removed mid-iteration is
|
|
2420
2561
|
* skipped. `update` and `remove` act on the row at the current position.
|
|
2421
2562
|
*/
|
|
2422
2563
|
var Cursor = class {
|
|
@@ -2503,84 +2644,9 @@ var Cursor = class {
|
|
|
2503
2644
|
}
|
|
2504
2645
|
};
|
|
2505
2646
|
//#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
2647
|
//#region src/core/Query.ts
|
|
2582
2648
|
/**
|
|
2583
|
-
*
|
|
2649
|
+
* Builds a read against one table through a fluent chain.
|
|
2584
2650
|
*
|
|
2585
2651
|
* @remarks
|
|
2586
2652
|
* Accumulates typed conditions, ordering, JS filters, and a page. Each builder
|
|
@@ -2641,7 +2707,7 @@ var Query = class {
|
|
|
2641
2707
|
return this.#filtered(fetched).length;
|
|
2642
2708
|
}
|
|
2643
2709
|
/**
|
|
2644
|
-
*
|
|
2710
|
+
* Evaluates conditions, filters, offset, and limit lazily.
|
|
2645
2711
|
*
|
|
2646
2712
|
* @param options - Optional abort options
|
|
2647
2713
|
* @returns Matching rows in storage order
|
|
@@ -2693,23 +2759,23 @@ var Query = class {
|
|
|
2693
2759
|
//#endregion
|
|
2694
2760
|
//#region src/core/Table.ts
|
|
2695
2761
|
/**
|
|
2696
|
-
*
|
|
2762
|
+
* Exposes typed keyed CRUD plus fluent query and cursor access over a driver.
|
|
2697
2763
|
*
|
|
2698
2764
|
* @remarks
|
|
2699
2765
|
* The table's contract is the load-bearing piece: writes go through `parse`
|
|
2700
2766
|
* (coercing inputs and rejecting rows that don't fit with a `VALIDATION` throw),
|
|
2701
2767
|
* reads come back through the contract guard (narrowing a stored {@link Row} to
|
|
2702
|
-
* the table's type — no assertion
|
|
2768
|
+
* the table's type — no assertion), and `contract` is exposed for
|
|
2703
2769
|
* introspection and seeding. The driver only stores and scans; all querying is
|
|
2704
2770
|
* the shared core engine in `helpers.ts`.
|
|
2705
2771
|
*
|
|
2706
2772
|
* @remarks
|
|
2707
|
-
* - **Observable
|
|
2773
|
+
* - **Observable.** The owned {@link emitter} ({@link TableEventMap}) carries the
|
|
2708
2774
|
* per-row mutation moments — `write` (set / add / update), `remove`, `clear` — for
|
|
2709
|
-
* fire-and-forget observers (cache invalidation, sync, an audit log),
|
|
2710
|
-
* database-level lifecycle. Events carry the affected
|
|
2775
|
+
* fire-and-forget observers (cache invalidation, sync, an audit log), alongside the
|
|
2776
|
+
* database-level lifecycle. Events carry the affected key only (no value payload, to
|
|
2711
2777
|
* keep fan-out lean); reads / queries / counts are not emitted. Every event is emitted
|
|
2712
|
-
* directly, strictly
|
|
2778
|
+
* directly, strictly after the driver write / delete / clear completes; the emitter
|
|
2713
2779
|
* isolates a listener throw and routes it to its `error` handler (the `error` option),
|
|
2714
2780
|
* so a buggy observer can never corrupt a write or perturb a transaction.
|
|
2715
2781
|
*/
|
|
@@ -2793,7 +2859,7 @@ var Table = class {
|
|
|
2793
2859
|
});
|
|
2794
2860
|
}
|
|
2795
2861
|
/**
|
|
2796
|
-
*
|
|
2862
|
+
* Counts contract-valid rows matching `input`'s conditions.
|
|
2797
2863
|
*
|
|
2798
2864
|
* @remarks
|
|
2799
2865
|
* Paging is ignored. Candidate rows use the driver's native `records` hook
|
|
@@ -2818,11 +2884,11 @@ var Table = class {
|
|
|
2818
2884
|
});
|
|
2819
2885
|
}
|
|
2820
2886
|
/**
|
|
2821
|
-
*
|
|
2887
|
+
* Computes an aggregate over `column` across rows matching `input`'s
|
|
2822
2888
|
* conditions.
|
|
2823
2889
|
*
|
|
2824
2890
|
* @remarks
|
|
2825
|
-
*
|
|
2891
|
+
* Unlike {@link count}, `aggregate` operates on stored rows without the
|
|
2826
2892
|
* contract guard {@link records} / {@link scan} apply — a non-conforming
|
|
2827
2893
|
* stored row still contributes to the computed aggregate when it matches
|
|
2828
2894
|
* the conditions, even though it would never appear in `records()`'s
|
|
@@ -2847,7 +2913,7 @@ var Table = class {
|
|
|
2847
2913
|
});
|
|
2848
2914
|
}
|
|
2849
2915
|
/**
|
|
2850
|
-
*
|
|
2916
|
+
* Streams the table's rows matching `input`, applying offset/limit paging.
|
|
2851
2917
|
*
|
|
2852
2918
|
* @remarks
|
|
2853
2919
|
* `input.limit` counts rows that pass both the input conditions and the
|
|
@@ -2862,7 +2928,7 @@ var Table = class {
|
|
|
2862
2928
|
scan(input, options) {
|
|
2863
2929
|
validatePage(input);
|
|
2864
2930
|
const source = this.#scan(input, options);
|
|
2865
|
-
if (this.#context !== void 0) return new
|
|
2931
|
+
if (this.#context !== void 0) return new ScopedIterator(source, this.#context, this.#ready);
|
|
2866
2932
|
return this.#scope === void 0 ? source : this.#scope.stream(source);
|
|
2867
2933
|
}
|
|
2868
2934
|
set(rows, options) {
|
|
@@ -3106,7 +3172,7 @@ var Table = class {
|
|
|
3106
3172
|
//#endregion
|
|
3107
3173
|
//#region src/core/DatabaseTransaction.ts
|
|
3108
3174
|
/**
|
|
3109
|
-
*
|
|
3175
|
+
* Binds a table-only database view to one driver transaction scope.
|
|
3110
3176
|
*
|
|
3111
3177
|
* @typeParam T - The declared table shape map
|
|
3112
3178
|
*
|
|
@@ -3133,31 +3199,27 @@ var DatabaseTransaction = class {
|
|
|
3133
3199
|
}
|
|
3134
3200
|
table(name) {
|
|
3135
3201
|
this.#scope.check();
|
|
3136
|
-
const columns = this.#
|
|
3137
|
-
return this.#build(name, this.#
|
|
3202
|
+
const columns = requireColumns(this.#tables, name);
|
|
3203
|
+
return this.#build(name, resolvePrimary(this.#primary, name), createContract(objectShape(columns)));
|
|
3138
3204
|
}
|
|
3139
3205
|
#build(name, key, contract) {
|
|
3140
3206
|
return new Table(() => Promise.resolve(), this.#driver, name, key, contract, this.#generate, this.#error, void 0, this.#scope);
|
|
3141
3207
|
}
|
|
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
3208
|
};
|
|
3151
3209
|
//#endregion
|
|
3152
3210
|
//#region src/core/Database.ts
|
|
3153
3211
|
/**
|
|
3154
|
-
*
|
|
3212
|
+
* Exposes a typed view over one shared internal lifecycle and storage context.
|
|
3155
3213
|
*
|
|
3156
3214
|
* @remarks
|
|
3157
3215
|
* Each view owns only its table contracts, primary columns, indexes, and key
|
|
3158
3216
|
* generator. Imported views register their physical schemas with the same
|
|
3159
3217
|
* internal context before opening begins, so every view observes one driver,
|
|
3160
3218
|
* merged schema, emitter, status, transaction boundary, and terminal close.
|
|
3219
|
+
*
|
|
3220
|
+
* The view owns the driver and its declared `tables`, connects that driver lazily on
|
|
3221
|
+
* first use, `import`s further tables and `export`s their portable definitions, and
|
|
3222
|
+
* runs `transaction` scopes over the shared context.
|
|
3161
3223
|
*/
|
|
3162
3224
|
var Database = class Database {
|
|
3163
3225
|
#context;
|
|
@@ -3184,8 +3246,8 @@ var Database = class Database {
|
|
|
3184
3246
|
}
|
|
3185
3247
|
table(name) {
|
|
3186
3248
|
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.#
|
|
3249
|
+
const columns = requireColumns(this.#tables, name);
|
|
3250
|
+
return this.#build(name, resolvePrimary(this.#primary, name), createContract(objectShape(columns)));
|
|
3189
3251
|
}
|
|
3190
3252
|
import(tables, primary) {
|
|
3191
3253
|
return this.#spawn(tables, {
|
|
@@ -3196,9 +3258,9 @@ var Database = class Database {
|
|
|
3196
3258
|
export() {
|
|
3197
3259
|
const result = {};
|
|
3198
3260
|
for (const name of Object.keys(this.#tables)) {
|
|
3199
|
-
const columns = this.#
|
|
3261
|
+
const columns = requireColumns(this.#tables, name);
|
|
3200
3262
|
result[name] = {
|
|
3201
|
-
primary: this.#
|
|
3263
|
+
primary: resolvePrimary(this.#primary, name),
|
|
3202
3264
|
columns,
|
|
3203
3265
|
schema: compileSchema(objectShape(columns))
|
|
3204
3266
|
};
|
|
@@ -3234,20 +3296,12 @@ var Database = class Database {
|
|
|
3234
3296
|
...this.#context.version === void 0 ? {} : { version: this.#context.version }
|
|
3235
3297
|
}, this.#context);
|
|
3236
3298
|
}
|
|
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
3299
|
#schema() {
|
|
3246
3300
|
return Object.keys(this.#tables).map((name) => {
|
|
3247
|
-
const columns = this.#
|
|
3301
|
+
const columns = requireColumns(this.#tables, name);
|
|
3248
3302
|
return {
|
|
3249
3303
|
name,
|
|
3250
|
-
primary: this.#
|
|
3304
|
+
primary: resolvePrimary(this.#primary, name),
|
|
3251
3305
|
columns: Object.entries(columns).map(([column, shape]) => shapeToColumnSchema(column, shape)),
|
|
3252
3306
|
indexes: this.#indexes[name] ?? []
|
|
3253
3307
|
};
|
|
@@ -3283,16 +3337,16 @@ var Database = class Database {
|
|
|
3283
3337
|
//#endregion
|
|
3284
3338
|
//#region src/core/drivers/MemoryDriver.ts
|
|
3285
3339
|
/**
|
|
3286
|
-
*
|
|
3340
|
+
* Implements the reference {@link DriverInterface} — nested maps, no I/O.
|
|
3287
3341
|
*
|
|
3288
3342
|
* @remarks
|
|
3289
3343
|
* The in-between made concrete: it runs identically in a browser or on a server,
|
|
3290
3344
|
* so it is the storage behind tests, ephemeral caches, and any code that wants
|
|
3291
|
-
* the database API without a persistent backend. Rows are
|
|
3345
|
+
* the database API without a persistent backend. Rows are deep-copied (through
|
|
3292
3346
|
* `structuredClone`) in and out — at `write`, `read`, `scan`, `stream`, and both
|
|
3293
3347
|
* snapshot capture and restore — so a caller mutating a nested field of an input
|
|
3294
3348
|
* row, a returned row, or a row mutated in place between snapshot and rollback
|
|
3295
|
-
* can never perturb stored state
|
|
3349
|
+
* can never perturb stored state; a shallow `{ ...row }` spread
|
|
3296
3350
|
* would still share nested object/array references. Metadata instead routes
|
|
3297
3351
|
* through `cloneDriverMetadata`: `stamp` and migration snapshot exact JSON at
|
|
3298
3352
|
* ingress, and `metadata` returns a distinct deeply frozen owned copy. `snapshot`
|
|
@@ -3354,19 +3408,19 @@ var MemoryDriver = class {
|
|
|
3354
3408
|
}
|
|
3355
3409
|
}
|
|
3356
3410
|
/**
|
|
3357
|
-
*
|
|
3411
|
+
* Iterates rows lazily with native filtering — the {@link DriverInterface.stream} hook.
|
|
3358
3412
|
*
|
|
3359
3413
|
* @remarks
|
|
3360
3414
|
* Iterates the table's keys in the same key order `scan` and `keys` yield
|
|
3361
3415
|
* (sorted by {@link compareValues}), testing each row against
|
|
3362
|
-
* `input.conditions` (
|
|
3416
|
+
* `input.conditions` (through {@link matchesQuery}) before counting it
|
|
3363
3417
|
* toward `offset` / `limit`. Both are applied lazily as matches are found —
|
|
3364
3418
|
* `offset` matches are skipped without being yielded, and iteration stops the
|
|
3365
3419
|
* instant `limit` yields have been produced, so a large table is never fully
|
|
3366
|
-
* walked for a small page. `input.order` is
|
|
3420
|
+
* walked for a small page. `input.order` is ignored (the same contract as
|
|
3367
3421
|
* `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
|
|
3368
|
-
* order, sorted output is `records()`'s job. Rows yield copy-out
|
|
3369
|
-
*
|
|
3422
|
+
* order, sorted output is `records()`'s job. Rows yield copy-out, and an
|
|
3423
|
+
* unknown table mirrors `scan`'s empty-yield behavior.
|
|
3370
3424
|
*
|
|
3371
3425
|
* @param table - The table to stream
|
|
3372
3426
|
* @param input - The filter / offset / limit to apply lazily
|
|
@@ -3382,31 +3436,11 @@ var MemoryDriver = class {
|
|
|
3382
3436
|
validatePage(input);
|
|
3383
3437
|
return this.#stream(table, input);
|
|
3384
3438
|
}
|
|
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
3439
|
async clear(table) {
|
|
3406
3440
|
this.#store(table).clear();
|
|
3407
3441
|
}
|
|
3408
3442
|
/**
|
|
3409
|
-
*
|
|
3443
|
+
* Captures the current state and returns a thunk that rolls back to it.
|
|
3410
3444
|
*
|
|
3411
3445
|
* @remarks
|
|
3412
3446
|
* Capture owns rows, schema, and one session-local table identity. Replay
|
|
@@ -3463,7 +3497,7 @@ var MemoryDriver = class {
|
|
|
3463
3497
|
};
|
|
3464
3498
|
}
|
|
3465
3499
|
/**
|
|
3466
|
-
*
|
|
3500
|
+
* Returns the persisted {@link DriverMetadata}, or `undefined` when the store has
|
|
3467
3501
|
* never been stamped.
|
|
3468
3502
|
*
|
|
3469
3503
|
* @remarks
|
|
@@ -3478,7 +3512,7 @@ var MemoryDriver = class {
|
|
|
3478
3512
|
return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
|
|
3479
3513
|
}
|
|
3480
3514
|
/**
|
|
3481
|
-
*
|
|
3515
|
+
* Persists an owned snapshot for a later `metadata()` to return.
|
|
3482
3516
|
*
|
|
3483
3517
|
* @param metadata - The {@link DriverMetadata} to persist
|
|
3484
3518
|
*/
|
|
@@ -3486,7 +3520,7 @@ var MemoryDriver = class {
|
|
|
3486
3520
|
this.#metadata = cloneDriverMetadata(metadata);
|
|
3487
3521
|
}
|
|
3488
3522
|
/**
|
|
3489
|
-
*
|
|
3523
|
+
* Applies a {@link Migration} plan's steps against the in-memory store.
|
|
3490
3524
|
*
|
|
3491
3525
|
* @remarks
|
|
3492
3526
|
* Steps apply against an isolated candidate. Rows, schema changes, and
|
|
@@ -3568,6 +3602,26 @@ var MemoryDriver = class {
|
|
|
3568
3602
|
case "index.remove": this.#require(tables, step.table);
|
|
3569
3603
|
}
|
|
3570
3604
|
}
|
|
3605
|
+
async *#stream(table, input) {
|
|
3606
|
+
const store = this.#store(table);
|
|
3607
|
+
const conditions = input.conditions;
|
|
3608
|
+
const offset = input.offset ?? 0;
|
|
3609
|
+
const limit = input.limit;
|
|
3610
|
+
let skipped = 0;
|
|
3611
|
+
let yielded = 0;
|
|
3612
|
+
for (const key of this.#ordered(table)) {
|
|
3613
|
+
if (limit !== void 0 && yielded >= limit) return;
|
|
3614
|
+
const row = store.get(key);
|
|
3615
|
+
if (row === void 0) continue;
|
|
3616
|
+
if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
|
|
3617
|
+
if (skipped < offset) {
|
|
3618
|
+
skipped += 1;
|
|
3619
|
+
continue;
|
|
3620
|
+
}
|
|
3621
|
+
yield structuredClone(row);
|
|
3622
|
+
yielded += 1;
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3571
3625
|
#store(table) {
|
|
3572
3626
|
this.#table(table);
|
|
3573
3627
|
const store = this.#tables.get(table);
|
|
@@ -3578,41 +3632,50 @@ var MemoryDriver = class {
|
|
|
3578
3632
|
//#endregion
|
|
3579
3633
|
//#region src/core/factories.ts
|
|
3580
3634
|
/**
|
|
3581
|
-
*
|
|
3635
|
+
* Creates a database over a driver and a declared `tables` schema.
|
|
3582
3636
|
*
|
|
3583
3637
|
* @remarks
|
|
3584
3638
|
* `tables` maps each name to its columns (a `column → shape` map); the database
|
|
3585
3639
|
* wraps each in an `objectShape`, so you never write `objectShape` at the table
|
|
3586
3640
|
* level. The `const` type parameter captures the literal names and columns, so
|
|
3587
3641
|
* `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.
|
|
3642
|
+
* columns — no annotations. Name a non-`id` primary-key column per table through
|
|
3643
|
+
* the optional `primary` and `indexes` maps.
|
|
3590
3644
|
*
|
|
3591
3645
|
* @param options - The driver, `tables`, and optional `primary`, `indexes`,
|
|
3592
3646
|
* `name`, `generator`, `version`, and emitter hooks
|
|
3593
3647
|
* @returns A typed {@link DatabaseInterface}
|
|
3594
3648
|
*
|
|
3595
|
-
* @example
|
|
3649
|
+
* @example Create a database
|
|
3596
3650
|
* ```ts
|
|
3597
3651
|
* import { createDatabase, createMemoryDriver } from '@orkestrel/database'
|
|
3598
3652
|
* import { integerShape, stringShape } from '@orkestrel/contract'
|
|
3599
3653
|
*
|
|
3600
3654
|
* const db = createDatabase({
|
|
3601
|
-
* driver: createMemoryDriver(),
|
|
3655
|
+
* driver: createMemoryDriver(), // any DriverInterface — a persistent backend swaps in, same API
|
|
3602
3656
|
* tables: {
|
|
3603
|
-
* users: { id: stringShape(), age: integerShape() },
|
|
3657
|
+
* users: { id: stringShape(), name: stringShape(), age: integerShape() },
|
|
3604
3658
|
* posts: { slug: stringShape(), title: stringShape() },
|
|
3605
3659
|
* },
|
|
3606
|
-
* primary: { posts: 'slug' },
|
|
3660
|
+
* primary: { posts: 'slug' }, // non-`id` primary-key columns, per table
|
|
3607
3661
|
* })
|
|
3608
|
-
*
|
|
3662
|
+
*
|
|
3663
|
+
* const users = db.table('users') // hold the handle; TableInterface<{ id; name; age }>
|
|
3664
|
+
*
|
|
3665
|
+
* await users.set({ id: 'u1', name: 'Ada', age: 36 }) // coerced + validated through the contract
|
|
3666
|
+
* await users.get('u1') // typed { id; name; age } | undefined — narrowed, never `as`
|
|
3667
|
+
* await users
|
|
3668
|
+
* .query()
|
|
3669
|
+
* .condition({ column: 'age', operator: 'from', values: [18], connector: 'and' })
|
|
3670
|
+
* .order({ column: 'age', direction: 'descending' })
|
|
3671
|
+
* .collect() // typed rows
|
|
3609
3672
|
* ```
|
|
3610
3673
|
*/
|
|
3611
3674
|
function createDatabase(options) {
|
|
3612
3675
|
return new Database(options);
|
|
3613
3676
|
}
|
|
3614
3677
|
/**
|
|
3615
|
-
*
|
|
3678
|
+
* Creates the in-memory reference {@link DriverInterface}.
|
|
3616
3679
|
*
|
|
3617
3680
|
* @remarks
|
|
3618
3681
|
* Backed by nested maps with no I/O — the same driver runs in a browser or on a
|
|
@@ -3624,6 +3687,122 @@ function createMemoryDriver() {
|
|
|
3624
3687
|
return new MemoryDriver();
|
|
3625
3688
|
}
|
|
3626
3689
|
//#endregion
|
|
3627
|
-
|
|
3690
|
+
//#region src/core/DriverIterator.ts
|
|
3691
|
+
/**
|
|
3692
|
+
* Forms the internal continuation boundary for a root driver async iterator.
|
|
3693
|
+
*
|
|
3694
|
+
* @remarks
|
|
3695
|
+
* A driver transaction can begin while a caller holds an idle root iterator.
|
|
3696
|
+
* Every `next` therefore checks the driver's root-state guard immediately
|
|
3697
|
+
* before and after advancing the source. A failed continuation terminalizes the
|
|
3698
|
+
* iterator, discards any row produced before the post-advance guard failed, and
|
|
3699
|
+
* attempts source cleanup exactly once.
|
|
3700
|
+
*
|
|
3701
|
+
* A driver implementing the published `DriverInterface` extension seam wraps its
|
|
3702
|
+
* own source iterator in one so a root `scan` / `stream` cannot outlive the
|
|
3703
|
+
* driver state it was opened against.
|
|
3704
|
+
*
|
|
3705
|
+
* @typeParam T - The value the wrapped source yields
|
|
3706
|
+
*
|
|
3707
|
+
* @example
|
|
3708
|
+
* ```ts
|
|
3709
|
+
* import type { Row } from '@orkestrel/database'
|
|
3710
|
+
* import { DatabaseError, DriverIterator } from '@orkestrel/database'
|
|
3711
|
+
*
|
|
3712
|
+
* // Inside a driver's `scan`, over its own row source and root-state guard.
|
|
3713
|
+
* declare const rows: AsyncIterator<Row>
|
|
3714
|
+
* declare const transacting: () => boolean
|
|
3715
|
+
* const scan = new DriverIterator(rows, () => {
|
|
3716
|
+
* if (transacting()) {
|
|
3717
|
+
* throw new DatabaseError('CONFLICT', 'scan: a transaction is active')
|
|
3718
|
+
* }
|
|
3719
|
+
* })
|
|
3720
|
+
* for await (const row of scan) row // one row at a time, guarded around each advance
|
|
3721
|
+
* ```
|
|
3722
|
+
*/
|
|
3723
|
+
var DriverIterator = class {
|
|
3724
|
+
#source;
|
|
3725
|
+
#guard;
|
|
3726
|
+
#terminal = false;
|
|
3727
|
+
#cleaned = false;
|
|
3728
|
+
/**
|
|
3729
|
+
* Wraps one source iterator in the continuation boundary.
|
|
3730
|
+
*
|
|
3731
|
+
* @param source - The driver's own row iterator, advanced once per `next`
|
|
3732
|
+
* @param guard - The root-state check, run immediately before and after each advance; it throws to terminalize the iteration
|
|
3733
|
+
*/
|
|
3734
|
+
constructor(source, guard) {
|
|
3735
|
+
this.#source = source;
|
|
3736
|
+
this.#guard = guard;
|
|
3737
|
+
}
|
|
3738
|
+
[Symbol.asyncIterator]() {
|
|
3739
|
+
return this;
|
|
3740
|
+
}
|
|
3741
|
+
async next() {
|
|
3742
|
+
if (this.#terminal) return {
|
|
3743
|
+
done: true,
|
|
3744
|
+
value: void 0
|
|
3745
|
+
};
|
|
3746
|
+
try {
|
|
3747
|
+
this.#guard();
|
|
3748
|
+
const result = await this.#source.next();
|
|
3749
|
+
this.#guard();
|
|
3750
|
+
if (result.done === true) {
|
|
3751
|
+
this.#terminal = true;
|
|
3752
|
+
this.#cleaned = true;
|
|
3753
|
+
}
|
|
3754
|
+
return result;
|
|
3755
|
+
} catch (error) {
|
|
3756
|
+
this.#terminal = true;
|
|
3757
|
+
await this.#discard();
|
|
3758
|
+
throw error;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
async return() {
|
|
3762
|
+
if (this.#terminal) return {
|
|
3763
|
+
done: true,
|
|
3764
|
+
value: void 0
|
|
3765
|
+
};
|
|
3766
|
+
this.#terminal = true;
|
|
3767
|
+
if (this.#cleaned || this.#source.return === void 0) {
|
|
3768
|
+
this.#cleaned = true;
|
|
3769
|
+
return {
|
|
3770
|
+
done: true,
|
|
3771
|
+
value: void 0
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
this.#cleaned = true;
|
|
3775
|
+
return this.#source.return();
|
|
3776
|
+
}
|
|
3777
|
+
async throw(error) {
|
|
3778
|
+
if (this.#terminal) throw error;
|
|
3779
|
+
if (this.#source.throw === void 0) {
|
|
3780
|
+
this.#terminal = true;
|
|
3781
|
+
await this.#discard();
|
|
3782
|
+
throw error;
|
|
3783
|
+
}
|
|
3784
|
+
try {
|
|
3785
|
+
const result = await this.#source.throw(error);
|
|
3786
|
+
if (result.done === true) {
|
|
3787
|
+
this.#terminal = true;
|
|
3788
|
+
this.#cleaned = true;
|
|
3789
|
+
}
|
|
3790
|
+
return result;
|
|
3791
|
+
} catch (cause) {
|
|
3792
|
+
this.#terminal = true;
|
|
3793
|
+
await this.#discard();
|
|
3794
|
+
throw cause;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
async #discard() {
|
|
3798
|
+
if (this.#cleaned) return;
|
|
3799
|
+
this.#cleaned = true;
|
|
3800
|
+
try {
|
|
3801
|
+
await this.#source.return?.();
|
|
3802
|
+
} catch {}
|
|
3803
|
+
}
|
|
3804
|
+
};
|
|
3805
|
+
//#endregion
|
|
3806
|
+
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
3807
|
|
|
3629
3808
|
//# sourceMappingURL=index.js.map
|