@orkestrel/database 0.0.6 → 0.0.7
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 +13 -35
- package/dist/src/browser/index.d.ts +92 -79
- package/dist/src/browser/index.js +357 -220
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +1796 -803
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +545 -706
- package/dist/src/core/index.d.ts +545 -706
- package/dist/src/core/index.js +1775 -790
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1508 -757
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +443 -569
- package/dist/src/server/index.d.ts +443 -569
- package/dist/src/server/index.js +1492 -741
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -10
package/dist/src/core/index.cjs
CHANGED
|
@@ -3,11 +3,11 @@ let _orkestrel_contract = require("@orkestrel/contract");
|
|
|
3
3
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
4
4
|
//#region src/core/constants.ts
|
|
5
5
|
/**
|
|
6
|
-
* The primary-key column assumed when {@link
|
|
6
|
+
* 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
|
|
10
|
-
* lean on, so a table
|
|
10
|
+
* lean on, so a table without a `primary` override keys its rows by `id`.
|
|
11
11
|
*/
|
|
12
12
|
var DEFAULT_PRIMARY = "id";
|
|
13
13
|
/**
|
|
@@ -15,7 +15,7 @@ var DEFAULT_PRIMARY = "id";
|
|
|
15
15
|
*
|
|
16
16
|
* @remarks
|
|
17
17
|
* A ReDoS bound (AGENTS §6.5): the SA1–SA4 migration lets a model supply `list`
|
|
18
|
-
*
|
|
18
|
+
* input over the wire, so `matchesLikePattern` / `matchesGlobPattern` run attacker-controlled
|
|
19
19
|
* patterns. 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 ×
|
|
@@ -24,10 +24,6 @@ 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
|
-
/** The number of bytes encoded by an RFC 4122 UUID. */
|
|
28
|
-
var UUID_BYTE_COUNT = 16;
|
|
29
|
-
/** The number of distinct values one UUID byte may hold. */
|
|
30
|
-
var UUID_BYTE_RANGE = 256;
|
|
31
27
|
//#endregion
|
|
32
28
|
//#region src/core/errors.ts
|
|
33
29
|
/**
|
|
@@ -37,8 +33,8 @@ var UUID_BYTE_RANGE = 256;
|
|
|
37
33
|
* Carries a {@link DatabaseErrorCode} and an optional `context` bag naming the
|
|
38
34
|
* offending table / key. Thrown for: operating on a closed database (`CLOSED`), a
|
|
39
35
|
* `resolve` miss (`NOT_FOUND`), an `add` onto an existing key (`CONFLICT`), a
|
|
40
|
-
* row that fails its table's contract (`VALIDATION`),
|
|
41
|
-
* {@link
|
|
36
|
+
* row that fails its table's contract (`VALIDATION`), an aborted operation whose
|
|
37
|
+
* {@link OperationOptions.signal} aborted (`ABORTED`, carrying `signal.reason` in
|
|
42
38
|
* `context`), an inapplicable {@link Migration} plan (`MIGRATION`), a
|
|
43
39
|
* driver that violates a {@link DriverInterface} invariant, thrown by the
|
|
44
40
|
* `conformDriver` helper (`CONFORMANCE`), and an unexpected infrastructure
|
|
@@ -75,6 +71,219 @@ function isDatabaseError(value) {
|
|
|
75
71
|
return value instanceof DatabaseError;
|
|
76
72
|
}
|
|
77
73
|
//#endregion
|
|
74
|
+
//#region src/core/validators.ts
|
|
75
|
+
/**
|
|
76
|
+
* Validate the paging fields of a portable query.
|
|
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.
|
|
101
|
+
*
|
|
102
|
+
* @param value - The value to test
|
|
103
|
+
* @returns Whether `value` is a string or finite number
|
|
104
|
+
*/
|
|
105
|
+
function isKey(value) {
|
|
106
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Test whether a value is a portable column schema.
|
|
110
|
+
*
|
|
111
|
+
* @param value - The value to test
|
|
112
|
+
* @returns Whether `value` is a complete {@link ColumnSchema}
|
|
113
|
+
*/
|
|
114
|
+
function isColumnSchema(value) {
|
|
115
|
+
try {
|
|
116
|
+
const column = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
117
|
+
const keys = Object.keys(column);
|
|
118
|
+
return keys.length === 4 && keys.includes("name") && keys.includes("storage") && keys.includes("optional") && keys.includes("nullable") && typeof column.name === "string" && column.name.length > 0 && (column.storage === "text" || column.storage === "integer" || column.storage === "real" || column.storage === "boolean" || column.storage === "json" || column.storage === "blob") && typeof column.optional === "boolean" && typeof column.nullable === "boolean";
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Test whether a value is a portable table schema.
|
|
125
|
+
*
|
|
126
|
+
* @param value - The value to test
|
|
127
|
+
* @returns Whether `value` is a complete {@link TableSchema}
|
|
128
|
+
*/
|
|
129
|
+
function isTableSchema(value) {
|
|
130
|
+
try {
|
|
131
|
+
const table = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
132
|
+
const keys = Object.keys(table);
|
|
133
|
+
if (keys.length !== 4 || !keys.includes("name") || !keys.includes("primary") || !keys.includes("columns") || !keys.includes("indexes") || typeof table.name !== "string" || table.name.length === 0 || typeof table.primary !== "string" || table.primary.length === 0 || !Array.isArray(table.columns) || !Array.isArray(table.indexes) || !table.columns.every(isColumnSchema)) return false;
|
|
134
|
+
const names = table.columns.map((column) => column.name);
|
|
135
|
+
if (new Set(names).size !== names.length || !names.includes(table.primary) || !table.indexes.every((index) => Array.isArray(index) && index.length > 0 && index.every((column) => typeof column === "string" && names.includes(column)))) return false;
|
|
136
|
+
const indexes = table.indexes.map((index) => JSON.stringify(index));
|
|
137
|
+
return new Set(indexes).size === indexes.length;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Test whether a value is a complete portable driver schema.
|
|
144
|
+
*
|
|
145
|
+
* @param value - The value to test
|
|
146
|
+
* @returns Whether `value` is a table-schema collection with unique table names
|
|
147
|
+
*/
|
|
148
|
+
function isDriverSchema(value) {
|
|
149
|
+
try {
|
|
150
|
+
const schema = (0, _orkestrel_contract.cloneJSONValue)(value);
|
|
151
|
+
if (!Array.isArray(schema) || !schema.every(isTableSchema)) return false;
|
|
152
|
+
const names = schema.map((table) => table.name);
|
|
153
|
+
return new Set(names).size === names.length;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Test whether a value is one ordered migration step.
|
|
160
|
+
*
|
|
161
|
+
* @param value - The value to test
|
|
162
|
+
* @returns Whether `value` is a complete {@link MigrationStep}
|
|
163
|
+
*/
|
|
164
|
+
function isMigrationStep(value) {
|
|
165
|
+
try {
|
|
166
|
+
const step = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
167
|
+
if (typeof step.operation !== "string") return false;
|
|
168
|
+
const keys = Object.keys(step);
|
|
169
|
+
switch (step.operation) {
|
|
170
|
+
case "table.add": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && isTableSchema(step.table);
|
|
171
|
+
case "table.remove": return keys.length === 2 && keys.includes("operation") && keys.includes("table") && typeof step.table === "string" && step.table.length > 0;
|
|
172
|
+
case "column.add": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && typeof step.table === "string" && step.table.length > 0 && isColumnSchema(step.column);
|
|
173
|
+
case "column.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("column") && typeof step.table === "string" && step.table.length > 0 && typeof step.column === "string" && step.column.length > 0;
|
|
174
|
+
case "index.add":
|
|
175
|
+
case "index.remove": return keys.length === 3 && keys.includes("operation") && keys.includes("table") && keys.includes("index") && typeof step.table === "string" && step.table.length > 0 && Array.isArray(step.index) && step.index.length > 0 && step.index.every((column) => typeof column === "string" && column.length > 0);
|
|
176
|
+
default: return false;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Test whether a value is an ordered migration plan.
|
|
184
|
+
*
|
|
185
|
+
* @param value - The value to test
|
|
186
|
+
* @returns Whether `value` is a complete {@link Migration}
|
|
187
|
+
*/
|
|
188
|
+
function isMigration(value) {
|
|
189
|
+
try {
|
|
190
|
+
const migration = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
191
|
+
const keys = Object.keys(migration);
|
|
192
|
+
return keys.length === 3 && keys.includes("from") && keys.includes("to") && keys.includes("steps") && typeof migration.from === "number" && Number.isFinite(migration.from) && typeof migration.to === "number" && Number.isFinite(migration.to) && Array.isArray(migration.steps) && migration.steps.every(isMigrationStep);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Test whether a value is persisted driver metadata.
|
|
199
|
+
*
|
|
200
|
+
* @param value - The value to test
|
|
201
|
+
* @returns Whether `value` is complete {@link DriverMetadata}
|
|
202
|
+
*/
|
|
203
|
+
function isDriverMetadata(value) {
|
|
204
|
+
try {
|
|
205
|
+
const metadata = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
206
|
+
const keys = Object.keys(metadata);
|
|
207
|
+
return keys.length === 2 && keys.includes("version") && keys.includes("schema") && typeof metadata.version === "number" && Number.isFinite(metadata.version) && isDriverSchema(metadata.schema);
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Test whether a value is one atomic migration request.
|
|
214
|
+
*
|
|
215
|
+
* @param value - The value to test
|
|
216
|
+
* @returns Whether `value` is a complete {@link MigrationInput}
|
|
217
|
+
*/
|
|
218
|
+
function isMigrationInput(value) {
|
|
219
|
+
try {
|
|
220
|
+
const input = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
221
|
+
const keys = Object.keys(input);
|
|
222
|
+
return (keys.length === 1 || keys.length === 2) && keys.includes("plan") && (keys.length === 1 || keys.includes("metadata")) && isMigration(input.plan) && (input.metadata === void 0 || isDriverMetadata(input.metadata));
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/core/cloners.ts
|
|
229
|
+
/**
|
|
230
|
+
* Clone unknown driver metadata into a distinct deeply frozen snapshot.
|
|
231
|
+
*
|
|
232
|
+
* @param value - Unknown metadata
|
|
233
|
+
* @returns Owned driver metadata
|
|
234
|
+
*/
|
|
235
|
+
function cloneDriverMetadata(value) {
|
|
236
|
+
try {
|
|
237
|
+
const metadata = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
238
|
+
if (isDriverMetadata(metadata)) return metadata;
|
|
239
|
+
throw new DatabaseError("VALIDATION", "Driver metadata is invalid", { path: "metadata" });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof DatabaseError) throw error;
|
|
242
|
+
throw new DatabaseError("VALIDATION", "Driver metadata is invalid", {
|
|
243
|
+
path: "metadata",
|
|
244
|
+
cause: error
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Clone unknown driver schema into a distinct deeply frozen snapshot.
|
|
250
|
+
*
|
|
251
|
+
* @param value - Unknown table schema collection
|
|
252
|
+
* @returns Owned driver schema
|
|
253
|
+
*/
|
|
254
|
+
function cloneDriverSchema(value) {
|
|
255
|
+
try {
|
|
256
|
+
const schema = (0, _orkestrel_contract.cloneJSONValue)(value);
|
|
257
|
+
if (isDriverSchema(schema)) return schema;
|
|
258
|
+
throw new DatabaseError("VALIDATION", "Driver schema is invalid", { path: "schema" });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error instanceof DatabaseError) throw error;
|
|
261
|
+
throw new DatabaseError("VALIDATION", "Driver schema is invalid", {
|
|
262
|
+
path: "schema",
|
|
263
|
+
cause: error
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Clone unknown migration input into a distinct deeply frozen snapshot.
|
|
269
|
+
*
|
|
270
|
+
* @param value - Unknown migration input
|
|
271
|
+
* @returns Owned migration input
|
|
272
|
+
*/
|
|
273
|
+
function cloneMigrationInput(value) {
|
|
274
|
+
try {
|
|
275
|
+
const input = (0, _orkestrel_contract.cloneJSONRecord)(value);
|
|
276
|
+
if (isMigrationInput(input)) return input;
|
|
277
|
+
throw new DatabaseError("VALIDATION", "Migration input is invalid", { path: "migration" });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error instanceof DatabaseError) throw error;
|
|
280
|
+
throw new DatabaseError("VALIDATION", "Migration input is invalid", {
|
|
281
|
+
path: "migration",
|
|
282
|
+
cause: error
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
78
287
|
//#region src/core/helpers.ts
|
|
79
288
|
/**
|
|
80
289
|
* A total ordering over arbitrary values — the comparator behind sorting and the
|
|
@@ -107,14 +316,15 @@ function compareValues(left, right) {
|
|
|
107
316
|
*
|
|
108
317
|
* @remarks
|
|
109
318
|
* Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
|
|
110
|
-
* Arrays compare by index (same length, every element `
|
|
319
|
+
* Arrays compare by index (same length, every element `equalsValue`). Plain
|
|
111
320
|
* records (via `isRecord`) compare by their OWN enumerable keys: same key
|
|
112
321
|
* COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
|
|
113
|
-
* with a `
|
|
322
|
+
* with a `equalsValue` value — so a key present with value `undefined` is NOT
|
|
114
323
|
* equal to that key being absent (both differ in `Object.keys` membership).
|
|
115
324
|
* Anything else (functions, class instances, mismatched shapes) falls through
|
|
116
|
-
* to `false`.
|
|
117
|
-
*
|
|
325
|
+
* to `false`. Container pairs are tracked iteratively, so self-referential and
|
|
326
|
+
* mutually cyclic arrays/records terminate without consuming the call stack.
|
|
327
|
+
* Hostile proxy traps and accessors are contained as a non-match.
|
|
118
328
|
*
|
|
119
329
|
* @param left - The left value
|
|
120
330
|
* @param right - The right value
|
|
@@ -122,32 +332,67 @@ function compareValues(left, right) {
|
|
|
122
332
|
*
|
|
123
333
|
* @example
|
|
124
334
|
* ```ts
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
335
|
+
* equalsValue(Number.NaN, Number.NaN) // true
|
|
336
|
+
* equalsValue({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true
|
|
337
|
+
* equalsValue({ a: undefined }, {}) // false — present-undefined ≠ absent
|
|
128
338
|
* ```
|
|
129
339
|
*/
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
340
|
+
function equalsValue(left, right) {
|
|
341
|
+
const pending = [[left, right]];
|
|
342
|
+
const compared = /* @__PURE__ */ new WeakMap();
|
|
343
|
+
try {
|
|
344
|
+
while (pending.length > 0) {
|
|
345
|
+
const pair = pending.pop();
|
|
346
|
+
if (pair === void 0) continue;
|
|
347
|
+
const [currentLeft, currentRight] = pair;
|
|
348
|
+
if (typeof currentLeft === "number" && typeof currentRight === "number") {
|
|
349
|
+
if (Number.isNaN(currentLeft) && Number.isNaN(currentRight) || currentLeft === currentRight) continue;
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
if (currentLeft === currentRight) continue;
|
|
353
|
+
const leftArray = Array.isArray(currentLeft);
|
|
354
|
+
const rightArray = Array.isArray(currentRight);
|
|
355
|
+
const leftRecord = (0, _orkestrel_contract.isRecord)(currentLeft);
|
|
356
|
+
const rightRecord = (0, _orkestrel_contract.isRecord)(currentRight);
|
|
357
|
+
if (leftArray !== rightArray || leftRecord !== rightRecord) return false;
|
|
358
|
+
if (!leftArray && !leftRecord || !rightArray && !rightRecord) return false;
|
|
359
|
+
const prior = compared.get(currentLeft);
|
|
360
|
+
if (prior?.has(currentRight)) continue;
|
|
361
|
+
if (prior === void 0) compared.set(currentLeft, new WeakSet([currentRight]));
|
|
362
|
+
else prior.add(currentRight);
|
|
363
|
+
if (leftArray && rightArray) {
|
|
364
|
+
if (currentLeft.length !== currentRight.length) return false;
|
|
365
|
+
for (let index = 0; index < currentLeft.length; index += 1) {
|
|
366
|
+
const leftOwn = Object.hasOwn(currentLeft, index);
|
|
367
|
+
if (leftOwn !== Object.hasOwn(currentRight, index)) return false;
|
|
368
|
+
if (leftOwn) pending.push([currentLeft[index], currentRight[index]]);
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (leftRecord && rightRecord) {
|
|
373
|
+
const leftKeys = Object.keys(currentLeft);
|
|
374
|
+
const rightKeys = Object.keys(currentRight);
|
|
375
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
376
|
+
for (const key of leftKeys) {
|
|
377
|
+
if (!Object.hasOwn(currentRight, key)) return false;
|
|
378
|
+
pending.push([currentLeft[key], currentRight[key]]);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
141
386
|
}
|
|
142
387
|
/**
|
|
143
388
|
* Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
|
|
144
|
-
* engine behind {@link
|
|
389
|
+
* engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
|
|
145
390
|
*
|
|
146
391
|
* @remarks
|
|
147
392
|
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
|
|
148
393
|
* `.*` segments separated by literals, matched against a long non-matching input, blow
|
|
149
394
|
* up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
|
|
150
|
-
* (AGENTS §6.5, now that the authed server runs model-supplied `list`
|
|
395
|
+
* (AGENTS §6.5, now that the authed server runs model-supplied `list` input over the
|
|
151
396
|
* wire). So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
|
|
152
397
|
* the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
|
|
153
398
|
* that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
|
|
@@ -170,7 +415,7 @@ function deepEqual(left, right) {
|
|
|
170
415
|
* @returns Whether `value` matches `pattern`
|
|
171
416
|
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
172
417
|
*/
|
|
173
|
-
function
|
|
418
|
+
function matchesWildcardPattern(value, pattern, any, single, fold) {
|
|
174
419
|
if (pattern.length > 1024) throw new DatabaseError("VALIDATION", `Pattern exceeds the maximum length of ${MAX_PATTERN_LENGTH}`, {
|
|
175
420
|
length: pattern.length,
|
|
176
421
|
limit: MAX_PATTERN_LENGTH
|
|
@@ -199,11 +444,11 @@ function wildcardMatch(value, pattern, any, single, fold) {
|
|
|
199
444
|
while (pi < needle.length && needle[pi] === any) pi += 1;
|
|
200
445
|
return pi === needle.length;
|
|
201
446
|
}
|
|
202
|
-
function
|
|
203
|
-
return
|
|
447
|
+
function matchesLikePattern(value, pattern) {
|
|
448
|
+
return matchesWildcardPattern(value, pattern, "%", "_", true);
|
|
204
449
|
}
|
|
205
|
-
function
|
|
206
|
-
return
|
|
450
|
+
function matchesGlobPattern(value, pattern) {
|
|
451
|
+
return matchesWildcardPattern(value, pattern, "*", "?", false);
|
|
207
452
|
}
|
|
208
453
|
/**
|
|
209
454
|
* Evaluate one {@link Condition} against a row — the per-operator predicate.
|
|
@@ -213,10 +458,10 @@ function globMatch(value, pattern) {
|
|
|
213
458
|
* string is one column; an array descends a nested value) — and applies the
|
|
214
459
|
* operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
|
|
215
460
|
* {@link compareValues}, the total order; the equality family (`equals` / `not`
|
|
216
|
-
* / `any` / `none`) uses {@link
|
|
461
|
+
* / `any` / `none`) uses {@link equalsValue} — STRUCTURAL equality, not the total
|
|
217
462
|
* order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
|
|
218
463
|
* operand only matches a structurally-equal value, never every row holding any
|
|
219
|
-
* object. This is a semantics change from ranking: `
|
|
464
|
+
* object. This is a semantics change from ranking: `equalsValue` is SameValueZero
|
|
220
465
|
* on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
|
|
221
466
|
* anything under the old rank-based comparison). `like` / `glob` / `starts` /
|
|
222
467
|
* `ends` match only strings; `absent` / `present` test nullishness. Total — a
|
|
@@ -231,19 +476,19 @@ function matchesCondition(row, condition) {
|
|
|
231
476
|
const first = condition.values[0];
|
|
232
477
|
const second = condition.values[1];
|
|
233
478
|
switch (condition.operator) {
|
|
234
|
-
case "equals": return
|
|
235
|
-
case "not": return !
|
|
479
|
+
case "equals": return equalsValue(value, first);
|
|
480
|
+
case "not": return !equalsValue(value, first);
|
|
236
481
|
case "above": return compareValues(value, first) > 0;
|
|
237
482
|
case "below": return compareValues(value, first) < 0;
|
|
238
483
|
case "from": return compareValues(value, first) >= 0;
|
|
239
484
|
case "to": return compareValues(value, first) <= 0;
|
|
240
485
|
case "between": return compareValues(value, first) >= 0 && compareValues(value, second) <= 0;
|
|
241
|
-
case "like": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) &&
|
|
242
|
-
case "glob": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) &&
|
|
486
|
+
case "like": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && matchesLikePattern(value, first);
|
|
487
|
+
case "glob": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && matchesGlobPattern(value, first);
|
|
243
488
|
case "starts": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && value.startsWith(first);
|
|
244
489
|
case "ends": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && value.endsWith(first);
|
|
245
|
-
case "any": return condition.values.some((candidate) =>
|
|
246
|
-
case "none": return !condition.values.some((candidate) =>
|
|
490
|
+
case "any": return condition.values.some((candidate) => equalsValue(value, candidate));
|
|
491
|
+
case "none": return !condition.values.some((candidate) => equalsValue(value, candidate));
|
|
247
492
|
case "absent": return value === void 0 || value === null;
|
|
248
493
|
case "present": return value !== void 0 && value !== null;
|
|
249
494
|
}
|
|
@@ -261,7 +506,7 @@ function matchesCondition(row, condition) {
|
|
|
261
506
|
* @param conditions - The conditions to fold
|
|
262
507
|
* @returns Whether the row satisfies the combined conditions
|
|
263
508
|
*/
|
|
264
|
-
function
|
|
509
|
+
function matchesQuery(row, conditions) {
|
|
265
510
|
let result = true;
|
|
266
511
|
let seeded = false;
|
|
267
512
|
for (const condition of conditions) {
|
|
@@ -275,11 +520,11 @@ function matchesCriteria(row, conditions) {
|
|
|
275
520
|
}
|
|
276
521
|
/**
|
|
277
522
|
* Filter rows by a list of conditions — the shared basis for a table's count
|
|
278
|
-
* and aggregate paths (no sort/page, unlike {@link
|
|
523
|
+
* and aggregate paths (no sort/page, unlike {@link applyQuery}).
|
|
279
524
|
*
|
|
280
525
|
* @remarks
|
|
281
526
|
* An empty condition list matches every row (returned as-is, no copy). Folds
|
|
282
|
-
* each row through {@link
|
|
527
|
+
* each row through {@link matchesQuery}.
|
|
283
528
|
*
|
|
284
529
|
* @param rows - The rows to filter
|
|
285
530
|
* @param conditions - The conditions to apply (empty matches everything)
|
|
@@ -295,7 +540,7 @@ function matchesCriteria(row, conditions) {
|
|
|
295
540
|
*/
|
|
296
541
|
function filterRows(rows, conditions) {
|
|
297
542
|
if (conditions.length === 0) return rows;
|
|
298
|
-
return rows.filter((row) =>
|
|
543
|
+
return rows.filter((row) => matchesQuery(row, conditions));
|
|
299
544
|
}
|
|
300
545
|
/**
|
|
301
546
|
* Sort rows by an ordering specification, leaving the input untouched.
|
|
@@ -320,26 +565,27 @@ function sortRows(rows, order) {
|
|
|
320
565
|
return sorted;
|
|
321
566
|
}
|
|
322
567
|
/**
|
|
323
|
-
* Apply a {@link
|
|
568
|
+
* Apply a {@link QueryInput} to rows — filter, then sort, then page.
|
|
324
569
|
*
|
|
325
570
|
* @remarks
|
|
326
571
|
* The whole portable read pipeline in one place: conditions filter, `order`
|
|
327
572
|
* sorts, and `offset` / `limit` window the result. Each step is skipped when its
|
|
328
|
-
* part of the
|
|
573
|
+
* part of the input is absent. The reference {@link DriverInterface} backends
|
|
329
574
|
* lean on this rather than each re-deriving it.
|
|
330
575
|
*
|
|
331
576
|
* @param rows - The rows to process (typically a table's full `scan`)
|
|
332
|
-
* @param
|
|
577
|
+
* @param input - The read specification, or `undefined` for all rows as-is
|
|
333
578
|
* @returns The filtered, sorted, paged rows
|
|
334
579
|
*/
|
|
335
|
-
function
|
|
580
|
+
function applyQuery(rows, input) {
|
|
581
|
+
validatePage(input);
|
|
336
582
|
let result = rows;
|
|
337
|
-
const conditions =
|
|
338
|
-
if (conditions !== void 0 && conditions.length > 0) result = result.filter((row) =>
|
|
339
|
-
const order =
|
|
583
|
+
const conditions = input?.conditions;
|
|
584
|
+
if (conditions !== void 0 && conditions.length > 0) result = result.filter((row) => matchesQuery(row, conditions));
|
|
585
|
+
const order = input?.order;
|
|
340
586
|
if (order !== void 0 && order.length > 0) result = sortRows(result, order);
|
|
341
|
-
const offset =
|
|
342
|
-
const limit =
|
|
587
|
+
const offset = input?.offset ?? 0;
|
|
588
|
+
const limit = input?.limit;
|
|
343
589
|
if (offset > 0 || limit !== void 0) result = result.slice(offset, limit !== void 0 ? offset + limit : void 0);
|
|
344
590
|
return result;
|
|
345
591
|
}
|
|
@@ -381,11 +627,24 @@ function computeAggregate(rows, operation, column) {
|
|
|
381
627
|
*/
|
|
382
628
|
function extractKey(row, column) {
|
|
383
629
|
const value = row[column];
|
|
384
|
-
|
|
385
|
-
|
|
630
|
+
return isKey(value) ? value : void 0;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Return a fresh row whose primary column is authoritatively bound to its storage key.
|
|
634
|
+
*
|
|
635
|
+
* @param row - The caller row
|
|
636
|
+
* @param primary - The primary column
|
|
637
|
+
* @param key - The authoritative storage key
|
|
638
|
+
* @returns A fresh row with the bound primary
|
|
639
|
+
*/
|
|
640
|
+
function bindRowKey(row, primary, key) {
|
|
641
|
+
return {
|
|
642
|
+
...row,
|
|
643
|
+
[primary]: key
|
|
644
|
+
};
|
|
386
645
|
}
|
|
387
646
|
/**
|
|
388
|
-
* Map a column's {@link ContractShape} to its portable {@link
|
|
647
|
+
* Map a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
|
|
389
648
|
* value a `TableSchema` carries so a native backend can declare a real column.
|
|
390
649
|
*
|
|
391
650
|
* @remarks
|
|
@@ -402,13 +661,13 @@ function extractKey(row, column) {
|
|
|
402
661
|
*
|
|
403
662
|
* @example
|
|
404
663
|
* ```ts
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
664
|
+
* shapeToColumnStorage(stringShape()) // 'text'
|
|
665
|
+
* shapeToColumnStorage(integerShape()) // 'integer'
|
|
666
|
+
* shapeToColumnStorage(optionalShape(integerShape())) // 'integer'
|
|
667
|
+
* shapeToColumnStorage(objectShape({ a: stringShape() })) // 'json'
|
|
409
668
|
* ```
|
|
410
669
|
*/
|
|
411
|
-
function
|
|
670
|
+
function shapeToColumnStorage(shape) {
|
|
412
671
|
switch (shape.type) {
|
|
413
672
|
case "string": return "text";
|
|
414
673
|
case "number": return shape.integer === true ? "integer" : "real";
|
|
@@ -418,7 +677,7 @@ function shapeToColumnType(shape) {
|
|
|
418
677
|
if (shape.values.every((value) => typeof value === "number")) return shape.values.every((value) => Number.isInteger(value)) ? "integer" : "real";
|
|
419
678
|
return "text";
|
|
420
679
|
case "optional":
|
|
421
|
-
case "nullable": return
|
|
680
|
+
case "nullable": return shapeToColumnStorage(shape.inner);
|
|
422
681
|
case "null":
|
|
423
682
|
case "object":
|
|
424
683
|
case "array":
|
|
@@ -428,48 +687,31 @@ function shapeToColumnType(shape) {
|
|
|
428
687
|
}
|
|
429
688
|
}
|
|
430
689
|
/**
|
|
431
|
-
*
|
|
432
|
-
* versioning driver's `meta()` narrows a stored (structured-clone or
|
|
433
|
-
* `JSON.parse`d) value through before trusting it, replacing the per-driver
|
|
434
|
-
* duplicated narrowing every backend used to hand-roll (AGENTS §14: never `as`).
|
|
435
|
-
*
|
|
436
|
-
* @remarks
|
|
437
|
-
* Total and total-recursive over the whole shape: a finite `version`, and a
|
|
438
|
-
* `schema` array of well-formed {@link TableSchema} entries — each a `name` /
|
|
439
|
-
* `primary` string pair, a `columns` array of well-formed {@link ColumnSchema}
|
|
440
|
-
* entries (a `name` string, a {@link ColumnType} literal, a `nullable`
|
|
441
|
-
* boolean), and an `indexes` array of string arrays. Anything off-shape
|
|
442
|
-
* (including a non-record) returns `false` rather than throwing.
|
|
690
|
+
* Project one contract shape into a portable column schema.
|
|
443
691
|
*
|
|
444
|
-
* @param
|
|
445
|
-
* @
|
|
446
|
-
*
|
|
447
|
-
* @example
|
|
448
|
-
* ```ts
|
|
449
|
-
* isDriverMeta({ version: 1, schema: [] }) // true
|
|
450
|
-
* isDriverMeta({ version: 1, schema: [{ name: 'users' }] }) // false
|
|
451
|
-
* ```
|
|
692
|
+
* @param name - The column name
|
|
693
|
+
* @param shape - The column contract shape
|
|
694
|
+
* @returns The portable storage and independent absence/null acceptance
|
|
452
695
|
*/
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
].some((type) => type === column.type) && (0, _orkestrel_contract.isBoolean)(column.nullable)) && (0, _orkestrel_contract.isArray)(table.indexes) && table.indexes.every((index) => (0, _orkestrel_contract.isArray)(index) && index.every((column) => (0, _orkestrel_contract.isString)(column))));
|
|
696
|
+
function shapeToColumnSchema(name, shape) {
|
|
697
|
+
const isColumn = (0, _orkestrel_contract.compileGuard)((0, _orkestrel_contract.objectShape)({ value: shape }));
|
|
698
|
+
return {
|
|
699
|
+
name,
|
|
700
|
+
storage: shapeToColumnStorage(shape),
|
|
701
|
+
optional: isColumn({}),
|
|
702
|
+
nullable: isColumn({ value: null })
|
|
703
|
+
};
|
|
462
704
|
}
|
|
463
705
|
/**
|
|
464
|
-
* Throw when an {@link
|
|
465
|
-
*
|
|
706
|
+
* Throw when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
|
|
707
|
+
* abort gate checked at operation boundaries and between streamed rows.
|
|
466
708
|
*
|
|
467
709
|
* @remarks
|
|
468
710
|
* A no-op for `undefined` or a live signal, so callers thread `options?.signal`
|
|
469
711
|
* straight through. When the signal has aborted, throws an `ABORTED`
|
|
470
712
|
* {@link DatabaseError} carrying the signal's `reason` in its context — callers
|
|
471
|
-
* mint signals with
|
|
472
|
-
* `new AbortController()
|
|
713
|
+
* mint signals with native APIs such as `AbortSignal.timeout(ms)` or
|
|
714
|
+
* `new AbortController()`.
|
|
473
715
|
*
|
|
474
716
|
* @param signal - The signal to check, if any
|
|
475
717
|
* @returns Nothing — returns normally while the signal is live
|
|
@@ -501,11 +743,12 @@ function checkAbort(signal) {
|
|
|
501
743
|
* `column.remove` / `index.add` / `index.remove` steps. Step order is
|
|
502
744
|
* deterministic: every `table.remove`, then every `table.add`, then each
|
|
503
745
|
* shared table's column/index changes in `declared` order. `from` / `to` are
|
|
504
|
-
* plan labels only
|
|
505
|
-
*
|
|
746
|
+
* plan labels only; versioning drivers persist and reconcile them through
|
|
747
|
+
* {@link DriverMetadata}.
|
|
506
748
|
*
|
|
507
749
|
* A column present in BOTH schemas under the same name but with a different
|
|
508
|
-
* `
|
|
750
|
+
* `storage`, `optional`, or `nullable` value throws a `MIGRATION`
|
|
751
|
+
* {@link DatabaseError} naming the
|
|
509
752
|
* table, the column, and the from→to difference — a name-only diff would
|
|
510
753
|
* otherwise silently produce NO step for the drift, and versioned
|
|
511
754
|
* reconciliation would stamp over it. There is no automatic in-place
|
|
@@ -518,42 +761,66 @@ function checkAbort(signal) {
|
|
|
518
761
|
* @param from - The plan's source version label (defaults to `0`)
|
|
519
762
|
* @param to - The plan's target version label (defaults to `1`)
|
|
520
763
|
* @returns The migration plan moving `deployed` toward `declared`
|
|
521
|
-
* @throws A `MIGRATION` {@link DatabaseError} when a shared
|
|
522
|
-
*
|
|
764
|
+
* @throws A `MIGRATION` {@link DatabaseError} when a shared table's primary or
|
|
765
|
+
* a shared column's `storage`, `optional`, or `nullable` differs, or when a
|
|
766
|
+
* required non-null column would be added to an existing table without a
|
|
767
|
+
* portable backfill
|
|
523
768
|
*
|
|
524
769
|
* @example
|
|
525
770
|
* ```ts
|
|
526
771
|
* const plan = planMigration(
|
|
527
|
-
* [{ name: 'users', primary: 'id', columns: [], indexes: [] }],
|
|
528
|
-
* [{ name: 'users', primary: 'id', columns: [{ name: 'age',
|
|
772
|
+
* [{ name: 'users', primary: 'id', columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }], indexes: [] }],
|
|
773
|
+
* [{ name: 'users', primary: 'id', columns: [{ name: 'id', storage: 'text', optional: false, nullable: false }, { name: 'age', storage: 'integer', optional: true, nullable: false }], indexes: [] }],
|
|
529
774
|
* )
|
|
530
775
|
* // plan.steps === [{ operation: 'column.add', table: 'users', column: { name: 'age', ... } }]
|
|
531
776
|
* ```
|
|
532
777
|
*/
|
|
533
778
|
function planMigration(deployed, declared, from = 0, to = 1) {
|
|
534
|
-
|
|
535
|
-
|
|
779
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) throw new DatabaseError("MIGRATION", "Migration versions must be finite", {
|
|
780
|
+
from,
|
|
781
|
+
to
|
|
782
|
+
});
|
|
783
|
+
let beforeSchema;
|
|
784
|
+
let targetSchema;
|
|
785
|
+
try {
|
|
786
|
+
beforeSchema = normalizeDriverSchema(deployed);
|
|
787
|
+
targetSchema = normalizeDriverSchema(declared);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
throw new DatabaseError("MIGRATION", "Migration schema is invalid", { cause: error });
|
|
790
|
+
}
|
|
791
|
+
const deployedByName = new Map(beforeSchema.map((table) => [table.name, table]));
|
|
792
|
+
const declaredByName = new Map(targetSchema.map((table) => [table.name, table]));
|
|
536
793
|
const steps = [];
|
|
537
|
-
for (const table of
|
|
794
|
+
for (const table of beforeSchema) if (!declaredByName.has(table.name)) steps.push({
|
|
538
795
|
operation: "table.remove",
|
|
539
796
|
table: table.name
|
|
540
797
|
});
|
|
541
|
-
for (const table of
|
|
798
|
+
for (const table of targetSchema) if (!deployedByName.has(table.name)) steps.push({
|
|
542
799
|
operation: "table.add",
|
|
543
800
|
table
|
|
544
801
|
});
|
|
545
|
-
for (const table of
|
|
802
|
+
for (const table of targetSchema) {
|
|
546
803
|
const before = deployedByName.get(table.name);
|
|
547
804
|
if (before === void 0) continue;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
805
|
+
if (before.primary !== table.primary) throw new DatabaseError("MIGRATION", `planMigration: primary column on table '${table.name}' changed from '${before.primary}' to '${table.primary}'`, {
|
|
806
|
+
table: table.name,
|
|
807
|
+
from: before.primary,
|
|
808
|
+
to: table.primary
|
|
809
|
+
});
|
|
810
|
+
const beforeColumnMap = new Map(before.columns.map((column) => [column.name, column]));
|
|
811
|
+
const afterColumnMap = new Map(table.columns.map((column) => [column.name, column]));
|
|
812
|
+
for (const index of before.indexes) if (!table.indexes.some((candidate) => equalsValue(candidate, index))) steps.push({
|
|
813
|
+
operation: "index.remove",
|
|
814
|
+
table: table.name,
|
|
815
|
+
index
|
|
816
|
+
});
|
|
817
|
+
for (const column of before.columns) if (!afterColumnMap.has(column.name)) steps.push({
|
|
551
818
|
operation: "column.remove",
|
|
552
819
|
table: table.name,
|
|
553
820
|
column: column.name
|
|
554
821
|
});
|
|
555
822
|
for (const column of table.columns) {
|
|
556
|
-
const previous =
|
|
823
|
+
const previous = beforeColumnMap.get(column.name);
|
|
557
824
|
if (previous === void 0) {
|
|
558
825
|
steps.push({
|
|
559
826
|
operation: "column.add",
|
|
@@ -562,35 +829,158 @@ function planMigration(deployed, declared, from = 0, to = 1) {
|
|
|
562
829
|
});
|
|
563
830
|
continue;
|
|
564
831
|
}
|
|
565
|
-
if (previous.
|
|
832
|
+
if (previous.storage !== column.storage || previous.optional !== column.optional || previous.nullable !== column.nullable) throw new DatabaseError("MIGRATION", `planMigration: column '${column.name}' on table '${table.name}' changed shape (storage ${previous.storage}→${column.storage}, optional ${previous.optional}→${column.optional}, nullable ${previous.nullable}→${column.nullable}) — in-place storage/optionality/nullability changes are not auto-migrated; add a new column, copy/convert the data, then remove the old column`, {
|
|
566
833
|
table: table.name,
|
|
567
834
|
column: column.name,
|
|
568
835
|
from: {
|
|
569
|
-
|
|
836
|
+
storage: previous.storage,
|
|
837
|
+
optional: previous.optional,
|
|
570
838
|
nullable: previous.nullable
|
|
571
839
|
},
|
|
572
840
|
to: {
|
|
573
|
-
|
|
841
|
+
storage: column.storage,
|
|
842
|
+
optional: column.optional,
|
|
574
843
|
nullable: column.nullable
|
|
575
844
|
}
|
|
576
845
|
});
|
|
577
846
|
}
|
|
578
|
-
for (const index of
|
|
579
|
-
operation: "index.remove",
|
|
580
|
-
table: table.name,
|
|
581
|
-
index
|
|
582
|
-
});
|
|
583
|
-
for (const index of table.indexes) if (!before.indexes.some((candidate) => deepEqual(candidate, index))) steps.push({
|
|
847
|
+
for (const index of table.indexes) if (!before.indexes.some((candidate) => equalsValue(candidate, index))) steps.push({
|
|
584
848
|
operation: "index.add",
|
|
585
849
|
table: table.name,
|
|
586
850
|
index
|
|
587
851
|
});
|
|
588
852
|
}
|
|
589
|
-
|
|
853
|
+
const projected = projectMigrationSchema(beforeSchema, steps);
|
|
854
|
+
if (!equalsValue(projected, targetSchema)) throw new DatabaseError("MIGRATION", "Migration plan does not project to the declared schema", {
|
|
855
|
+
projected,
|
|
856
|
+
declared: targetSchema
|
|
857
|
+
});
|
|
858
|
+
return cloneMigrationInput({ plan: {
|
|
590
859
|
from,
|
|
591
860
|
to,
|
|
592
861
|
steps
|
|
593
|
-
};
|
|
862
|
+
} }).plan;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Sequentially project migration steps over a canonical validated owned schema.
|
|
866
|
+
* Adding a required non-null column to an existing table rejects with
|
|
867
|
+
* `MIGRATION`; optional-only and nullable-only additions remain portable.
|
|
868
|
+
*
|
|
869
|
+
* @param schema - The initial deployed schema
|
|
870
|
+
* @param steps - The ordered migration steps
|
|
871
|
+
* @returns A fresh owned final schema
|
|
872
|
+
*/
|
|
873
|
+
function projectMigrationSchema(schema, steps) {
|
|
874
|
+
let owned;
|
|
875
|
+
let projectedSteps;
|
|
876
|
+
try {
|
|
877
|
+
owned = normalizeDriverSchema(schema);
|
|
878
|
+
projectedSteps = cloneMigrationInput({ plan: {
|
|
879
|
+
from: 0,
|
|
880
|
+
to: 1,
|
|
881
|
+
steps
|
|
882
|
+
} }).plan.steps;
|
|
883
|
+
} catch (error) {
|
|
884
|
+
throw new DatabaseError("MIGRATION", "Migration input is invalid", { cause: error });
|
|
885
|
+
}
|
|
886
|
+
const tables = new Map(owned.map((table) => [table.name, table]));
|
|
887
|
+
for (const step of projectedSteps) {
|
|
888
|
+
if (step.operation === "table.add") {
|
|
889
|
+
if (tables.has(step.table.name)) throw new DatabaseError("MIGRATION", `migrate: table '${step.table.name}' already exists`, { table: step.table.name });
|
|
890
|
+
tables.set(step.table.name, step.table);
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const table = tables.get(step.table);
|
|
894
|
+
if (table === void 0) throw new DatabaseError("MIGRATION", `migrate: table '${step.table}' does not exist`, { table: step.table });
|
|
895
|
+
if (step.operation === "table.remove") {
|
|
896
|
+
tables.delete(step.table);
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (step.operation === "column.add") {
|
|
900
|
+
if (table.columns.some((column) => column.name === step.column.name)) throw new DatabaseError("MIGRATION", `migrate: column '${step.column.name}' already exists`, {
|
|
901
|
+
table: step.table,
|
|
902
|
+
column: step.column.name
|
|
903
|
+
});
|
|
904
|
+
if (!step.column.optional && !step.column.nullable) throw new DatabaseError("MIGRATION", `migrate: required non-null column '${step.column.name}' cannot be added automatically to existing table '${step.table}'`, {
|
|
905
|
+
table: step.table,
|
|
906
|
+
column: step.column.name
|
|
907
|
+
});
|
|
908
|
+
tables.set(step.table, {
|
|
909
|
+
...table,
|
|
910
|
+
columns: [...table.columns, step.column]
|
|
911
|
+
});
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (step.operation === "column.remove") {
|
|
915
|
+
if (!table.columns.some((column) => column.name === step.column)) throw new DatabaseError("MIGRATION", `migrate: column '${step.column}' does not exist`, {
|
|
916
|
+
table: step.table,
|
|
917
|
+
column: step.column
|
|
918
|
+
});
|
|
919
|
+
if (table.primary === step.column) throw new DatabaseError("MIGRATION", "migrate: cannot remove the primary column", {
|
|
920
|
+
table: step.table,
|
|
921
|
+
column: step.column
|
|
922
|
+
});
|
|
923
|
+
if (table.indexes.some((index) => index.includes(step.column))) throw new DatabaseError("MIGRATION", "migrate: cannot remove an indexed column", {
|
|
924
|
+
table: step.table,
|
|
925
|
+
column: step.column
|
|
926
|
+
});
|
|
927
|
+
tables.set(step.table, {
|
|
928
|
+
...table,
|
|
929
|
+
columns: table.columns.filter((column) => column.name !== step.column)
|
|
930
|
+
});
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (step.operation === "index.add") {
|
|
934
|
+
if (step.index.length === 0 || step.index.some((name) => !table.columns.some((column) => column.name === name))) throw new DatabaseError("MIGRATION", "migrate: index references a missing column", {
|
|
935
|
+
table: step.table,
|
|
936
|
+
index: step.index
|
|
937
|
+
});
|
|
938
|
+
if (table.indexes.some((index) => equalsValue(index, step.index))) throw new DatabaseError("MIGRATION", "migrate: index already exists", {
|
|
939
|
+
table: step.table,
|
|
940
|
+
index: step.index
|
|
941
|
+
});
|
|
942
|
+
tables.set(step.table, {
|
|
943
|
+
...table,
|
|
944
|
+
indexes: [...table.indexes, step.index]
|
|
945
|
+
});
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (!table.indexes.some((index) => equalsValue(index, step.index))) throw new DatabaseError("MIGRATION", "migrate: index does not exist", {
|
|
949
|
+
table: step.table,
|
|
950
|
+
index: step.index
|
|
951
|
+
});
|
|
952
|
+
tables.set(step.table, {
|
|
953
|
+
...table,
|
|
954
|
+
indexes: table.indexes.filter((index) => !equalsValue(index, step.index))
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
return normalizeDriverSchema([...tables.values()]);
|
|
959
|
+
} catch (error) {
|
|
960
|
+
throw new DatabaseError("MIGRATION", "Projected migration schema is invalid", { cause: error });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Canonicalize an unknown driver schema into a distinct deeply frozen snapshot.
|
|
965
|
+
*
|
|
966
|
+
* @remarks
|
|
967
|
+
* Table and column lists are sorted by name. The index list is sorted by the
|
|
968
|
+
* complete serialized tuple while column order inside each compound index is
|
|
969
|
+
* preserved because it carries index semantics. Validation and ownership flow
|
|
970
|
+
* through {@link cloneDriverSchema} before and after projection.
|
|
971
|
+
*
|
|
972
|
+
* @param value - Unknown driver schema
|
|
973
|
+
* @returns A validated, owned canonical schema
|
|
974
|
+
*/
|
|
975
|
+
function normalizeDriverSchema(value) {
|
|
976
|
+
const tables = cloneDriverSchema(value).map((table) => ({
|
|
977
|
+
name: table.name,
|
|
978
|
+
primary: table.primary,
|
|
979
|
+
columns: [...table.columns].sort((left, right) => compareValues(left.name, right.name)),
|
|
980
|
+
indexes: [...table.indexes].sort((left, right) => compareValues(JSON.stringify(left), JSON.stringify(right)))
|
|
981
|
+
}));
|
|
982
|
+
tables.sort((left, right) => compareValues(left.name, right.name));
|
|
983
|
+
return cloneDriverSchema(tables);
|
|
594
984
|
}
|
|
595
985
|
/**
|
|
596
986
|
* Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
|
|
@@ -638,21 +1028,24 @@ function migrateRows(rows, steps) {
|
|
|
638
1028
|
* `read` of a missing key returns `undefined`; `write`/`read` round-trip with
|
|
639
1029
|
* DEEP copy-in/copy-out isolation (mutating the caller's row — including a
|
|
640
1030
|
* NESTED field — after `write`, or a row `read` returns, never perturbs
|
|
641
|
-
* stored state) and upsert-overwrite;
|
|
1031
|
+
* stored state) and upsert-overwrite; simultaneous same-key `insert` calls
|
|
1032
|
+
* produce exactly one commit and one `CONFLICT`; pre-aborted `write`,
|
|
1033
|
+
* `insert`, and `delete` calls leave storage unchanged; `delete` returns
|
|
1034
|
+
* `true` then `false`;
|
|
642
1035
|
* `keys`/`scan` yield in ascending key order; `clear` empties only its target
|
|
643
1036
|
* table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
|
|
644
1037
|
* NESTED field mutated in place on a read-back row between capture and
|
|
645
1038
|
* restore; a scoped `snapshot(['users'])` rolls back only the named table,
|
|
646
1039
|
* leaving a concurrent mutation to another table intact; a
|
|
647
1040
|
* non-`id` primary key (`posts.slug`) round-trips; a nested-object row
|
|
648
|
-
* round-trips structurally (via {@link
|
|
1041
|
+
* round-trips structurally (via {@link equalsValue}). The optional surface is
|
|
649
1042
|
* presence-gated: when `migrate` exists, a `column.remove` plan strips the
|
|
650
1043
|
* column from stored rows and a plan referencing an unknown table throws
|
|
651
1044
|
* `DatabaseError` `MIGRATION`; when `stream` exists, it yields only
|
|
652
1045
|
* condition-matching rows and honors `offset`/`limit`; when `transaction`
|
|
653
|
-
* exists, `commit` persists and `rollback` restores; when both `
|
|
654
|
-
* `stamp` exist, a fresh store's `
|
|
655
|
-
* `stamp({ version, schema })`, `
|
|
1046
|
+
* exists, `commit` persists and `rollback` restores; when both `metadata` and
|
|
1047
|
+
* `stamp` exist, a fresh store's `metadata()` is `undefined`, and after
|
|
1048
|
+
* `stamp({ version, schema })`, `metadata()` returns the exact stamped value.
|
|
656
1049
|
*
|
|
657
1050
|
* Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
|
|
658
1051
|
* finding built from the assertion, while an UNEXPECTED throw (a driver
|
|
@@ -684,23 +1077,27 @@ async function* driverFindings(factory) {
|
|
|
684
1077
|
columns: [
|
|
685
1078
|
{
|
|
686
1079
|
name: "id",
|
|
687
|
-
|
|
1080
|
+
storage: "text",
|
|
1081
|
+
optional: false,
|
|
688
1082
|
nullable: false
|
|
689
1083
|
},
|
|
690
1084
|
{
|
|
691
1085
|
name: "name",
|
|
692
|
-
|
|
1086
|
+
storage: "text",
|
|
1087
|
+
optional: false,
|
|
693
1088
|
nullable: false
|
|
694
1089
|
},
|
|
695
1090
|
{
|
|
696
1091
|
name: "age",
|
|
697
|
-
|
|
698
|
-
|
|
1092
|
+
storage: "integer",
|
|
1093
|
+
optional: true,
|
|
1094
|
+
nullable: false
|
|
699
1095
|
},
|
|
700
1096
|
{
|
|
701
1097
|
name: "meta",
|
|
702
|
-
|
|
703
|
-
|
|
1098
|
+
storage: "json",
|
|
1099
|
+
optional: true,
|
|
1100
|
+
nullable: false
|
|
704
1101
|
}
|
|
705
1102
|
],
|
|
706
1103
|
indexes: []
|
|
@@ -710,11 +1107,13 @@ async function* driverFindings(factory) {
|
|
|
710
1107
|
primary: "slug",
|
|
711
1108
|
columns: [{
|
|
712
1109
|
name: "slug",
|
|
713
|
-
|
|
1110
|
+
storage: "text",
|
|
1111
|
+
optional: false,
|
|
714
1112
|
nullable: false
|
|
715
1113
|
}, {
|
|
716
1114
|
name: "title",
|
|
717
|
-
|
|
1115
|
+
storage: "text",
|
|
1116
|
+
optional: false,
|
|
718
1117
|
nullable: false
|
|
719
1118
|
}],
|
|
720
1119
|
indexes: []
|
|
@@ -756,7 +1155,7 @@ async function* driverFindings(factory) {
|
|
|
756
1155
|
const driver = factory();
|
|
757
1156
|
await driver.open(CONFORMANCE_SCHEMA);
|
|
758
1157
|
const input = {
|
|
759
|
-
id: "
|
|
1158
|
+
id: "caller",
|
|
760
1159
|
name: "Ada",
|
|
761
1160
|
age: 30,
|
|
762
1161
|
meta: { tags: ["a"] }
|
|
@@ -771,7 +1170,7 @@ async function* driverFindings(factory) {
|
|
|
771
1170
|
age: 30,
|
|
772
1171
|
meta: { tags: ["a"] }
|
|
773
1172
|
};
|
|
774
|
-
if (stored === void 0 || !
|
|
1173
|
+
if (stored === void 0 || !equalsValue(stored, original)) {
|
|
775
1174
|
await driver.close();
|
|
776
1175
|
yield {
|
|
777
1176
|
check: "copy-in",
|
|
@@ -787,7 +1186,7 @@ async function* driverFindings(factory) {
|
|
|
787
1186
|
stored.name = "Mutated after read";
|
|
788
1187
|
if ((0, _orkestrel_contract.isRecord)(stored.meta) && Array.isArray(stored.meta.tags)) stored.meta.tags.push("mutated");
|
|
789
1188
|
const reread = await driver.read("users", "u1");
|
|
790
|
-
if (reread === void 0 || !
|
|
1189
|
+
if (reread === void 0 || !equalsValue(reread, original)) {
|
|
791
1190
|
await driver.close();
|
|
792
1191
|
yield {
|
|
793
1192
|
check: "copy-out",
|
|
@@ -801,19 +1200,23 @@ async function* driverFindings(factory) {
|
|
|
801
1200
|
break writeRead;
|
|
802
1201
|
}
|
|
803
1202
|
const overwrite = {
|
|
804
|
-
id: "
|
|
1203
|
+
id: "caller",
|
|
805
1204
|
name: "Ada Overwritten",
|
|
806
1205
|
age: 31
|
|
807
1206
|
};
|
|
808
1207
|
await driver.write("users", "u1", overwrite);
|
|
809
1208
|
const overwritten = await driver.read("users", "u1");
|
|
810
1209
|
await driver.close();
|
|
811
|
-
|
|
1210
|
+
const expectedOverwrite = {
|
|
1211
|
+
...overwrite,
|
|
1212
|
+
id: "u1"
|
|
1213
|
+
};
|
|
1214
|
+
if (overwritten === void 0 || !equalsValue(overwritten, expectedOverwrite)) yield {
|
|
812
1215
|
check: "upsert",
|
|
813
1216
|
message: "write must upsert-overwrite an existing key",
|
|
814
1217
|
context: {
|
|
815
1218
|
table: "users",
|
|
816
|
-
expected:
|
|
1219
|
+
expected: expectedOverwrite,
|
|
817
1220
|
actual: overwritten
|
|
818
1221
|
}
|
|
819
1222
|
};
|
|
@@ -824,6 +1227,47 @@ async function* driverFindings(factory) {
|
|
|
824
1227
|
context: { error }
|
|
825
1228
|
};
|
|
826
1229
|
}
|
|
1230
|
+
try {
|
|
1231
|
+
const driver = factory();
|
|
1232
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1233
|
+
const outcomes = await Promise.allSettled([driver.insert("users", "u1", {
|
|
1234
|
+
id: "u1",
|
|
1235
|
+
name: "Ada",
|
|
1236
|
+
age: 30
|
|
1237
|
+
}), driver.insert("users", "u1", {
|
|
1238
|
+
id: "u1",
|
|
1239
|
+
name: "Grace",
|
|
1240
|
+
age: 40
|
|
1241
|
+
})]);
|
|
1242
|
+
let fulfilled = 0;
|
|
1243
|
+
let conflicted = 0;
|
|
1244
|
+
for (const outcome of outcomes) if (outcome.status === "fulfilled") fulfilled += 1;
|
|
1245
|
+
else if (isDatabaseError(outcome.reason) && outcome.reason.code === "CONFLICT") conflicted += 1;
|
|
1246
|
+
const keys = await driver.keys("users");
|
|
1247
|
+
await driver.close();
|
|
1248
|
+
if (fulfilled !== 1 || conflicted !== 1 || !equalsValue(keys, ["u1"])) yield {
|
|
1249
|
+
check: "insert-atomic",
|
|
1250
|
+
message: "concurrent same-key inserts must produce one commit and one CONFLICT",
|
|
1251
|
+
context: {
|
|
1252
|
+
expected: {
|
|
1253
|
+
fulfilled: 1,
|
|
1254
|
+
conflicted: 1,
|
|
1255
|
+
keys: ["u1"]
|
|
1256
|
+
},
|
|
1257
|
+
actual: {
|
|
1258
|
+
fulfilled,
|
|
1259
|
+
conflicted,
|
|
1260
|
+
keys
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
yield {
|
|
1266
|
+
check: "insert-atomic",
|
|
1267
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1268
|
+
context: { error }
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
827
1271
|
deletePhase: try {
|
|
828
1272
|
const driver = factory();
|
|
829
1273
|
await driver.open(CONFORMANCE_SCHEMA);
|
|
@@ -864,6 +1308,69 @@ async function* driverFindings(factory) {
|
|
|
864
1308
|
context: { error }
|
|
865
1309
|
};
|
|
866
1310
|
}
|
|
1311
|
+
try {
|
|
1312
|
+
const driver = factory();
|
|
1313
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1314
|
+
await driver.write("users", "u1", {
|
|
1315
|
+
id: "u1",
|
|
1316
|
+
name: "Ada",
|
|
1317
|
+
age: 30
|
|
1318
|
+
});
|
|
1319
|
+
const controller = new AbortController();
|
|
1320
|
+
controller.abort("conformance abort");
|
|
1321
|
+
let writeError;
|
|
1322
|
+
let insertError;
|
|
1323
|
+
let deleteError;
|
|
1324
|
+
try {
|
|
1325
|
+
await driver.write("users", "u2", {
|
|
1326
|
+
id: "u2",
|
|
1327
|
+
name: "Grace",
|
|
1328
|
+
age: 40
|
|
1329
|
+
}, { signal: controller.signal });
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
writeError = error;
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
await driver.insert("users", "u2", {
|
|
1335
|
+
id: "u2",
|
|
1336
|
+
name: "Grace",
|
|
1337
|
+
age: 40
|
|
1338
|
+
}, { signal: controller.signal });
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
insertError = error;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
await driver.delete("users", "u1", { signal: controller.signal });
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
deleteError = error;
|
|
1346
|
+
}
|
|
1347
|
+
const keys = await driver.keys("users");
|
|
1348
|
+
await driver.close();
|
|
1349
|
+
if (!isDatabaseError(writeError) || writeError.code !== "ABORTED" || !isDatabaseError(insertError) || insertError.code !== "ABORTED" || !isDatabaseError(deleteError) || deleteError.code !== "ABORTED" || !equalsValue(keys, ["u1"])) yield {
|
|
1350
|
+
check: "mutation-abort",
|
|
1351
|
+
message: "pre-aborted write/insert/delete must reject ABORTED without changing rows",
|
|
1352
|
+
context: {
|
|
1353
|
+
expected: {
|
|
1354
|
+
write: "ABORTED",
|
|
1355
|
+
insert: "ABORTED",
|
|
1356
|
+
delete: "ABORTED",
|
|
1357
|
+
keys: ["u1"]
|
|
1358
|
+
},
|
|
1359
|
+
actual: {
|
|
1360
|
+
write: isDatabaseError(writeError) ? writeError.code : writeError,
|
|
1361
|
+
insert: isDatabaseError(insertError) ? insertError.code : insertError,
|
|
1362
|
+
delete: isDatabaseError(deleteError) ? deleteError.code : deleteError,
|
|
1363
|
+
keys
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
yield {
|
|
1369
|
+
check: "mutation-abort",
|
|
1370
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1371
|
+
context: { error }
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
867
1374
|
orderPhase: try {
|
|
868
1375
|
const driver = factory();
|
|
869
1376
|
await driver.open(CONFORMANCE_SCHEMA);
|
|
@@ -890,7 +1397,7 @@ async function* driverFindings(factory) {
|
|
|
890
1397
|
"c"
|
|
891
1398
|
];
|
|
892
1399
|
const keys = [...await driver.keys("users")];
|
|
893
|
-
if (!
|
|
1400
|
+
if (!equalsValue(keys, expected)) {
|
|
894
1401
|
await driver.close();
|
|
895
1402
|
yield {
|
|
896
1403
|
check: "keys-order",
|
|
@@ -907,7 +1414,7 @@ async function* driverFindings(factory) {
|
|
|
907
1414
|
for await (const row of driver.scan("users")) scanned.push(row);
|
|
908
1415
|
const scannedIds = scanned.map((row) => row.id);
|
|
909
1416
|
await driver.close();
|
|
910
|
-
if (!
|
|
1417
|
+
if (!equalsValue(scannedIds, expected)) yield {
|
|
911
1418
|
check: "scan-order",
|
|
912
1419
|
message: "scan must yield rows in ascending key order",
|
|
913
1420
|
context: {
|
|
@@ -985,7 +1492,7 @@ async function* driverFindings(factory) {
|
|
|
985
1492
|
await driver.delete("users", "u1");
|
|
986
1493
|
await rollback();
|
|
987
1494
|
const keys = [...await driver.keys("users")];
|
|
988
|
-
if (!
|
|
1495
|
+
if (!equalsValue(keys, ["u1"])) {
|
|
989
1496
|
await driver.close();
|
|
990
1497
|
yield {
|
|
991
1498
|
check: "snapshot-rollback",
|
|
@@ -1000,7 +1507,7 @@ async function* driverFindings(factory) {
|
|
|
1000
1507
|
}
|
|
1001
1508
|
const restored = await driver.read("users", "u1");
|
|
1002
1509
|
await driver.close();
|
|
1003
|
-
if (restored === void 0 || !
|
|
1510
|
+
if (restored === void 0 || !equalsValue(restored, original)) yield {
|
|
1004
1511
|
check: "snapshot-rollback-value",
|
|
1005
1512
|
message: "snapshot rollback must restore pre-snapshot row values",
|
|
1006
1513
|
context: {
|
|
@@ -1038,7 +1545,7 @@ async function* driverFindings(factory) {
|
|
|
1038
1545
|
await rollback();
|
|
1039
1546
|
const restored = await driver.read("users", "u3");
|
|
1040
1547
|
await driver.close();
|
|
1041
|
-
if (restored === void 0 || !
|
|
1548
|
+
if (restored === void 0 || !equalsValue(restored, original)) yield {
|
|
1042
1549
|
check: "snapshot-nested",
|
|
1043
1550
|
message: "snapshot rollback must restore pre-snapshot nested field values, unaffected by a later in-place mutation of a read-back row",
|
|
1044
1551
|
context: {
|
|
@@ -1058,7 +1565,7 @@ async function* driverFindings(factory) {
|
|
|
1058
1565
|
const driver = factory();
|
|
1059
1566
|
await driver.open(CONFORMANCE_SCHEMA);
|
|
1060
1567
|
await driver.write("posts", "hello-world", {
|
|
1061
|
-
slug: "
|
|
1568
|
+
slug: "caller",
|
|
1062
1569
|
title: "Hello"
|
|
1063
1570
|
});
|
|
1064
1571
|
const post = await driver.read("posts", "hello-world");
|
|
@@ -1095,7 +1602,7 @@ async function* driverFindings(factory) {
|
|
|
1095
1602
|
await driver.write("users", "u3", nested);
|
|
1096
1603
|
const readBack = await driver.read("users", "u3");
|
|
1097
1604
|
await driver.close();
|
|
1098
|
-
if (readBack === void 0 || !
|
|
1605
|
+
if (readBack === void 0 || !equalsValue(readBack, nested)) yield {
|
|
1099
1606
|
check: "nested-roundtrip",
|
|
1100
1607
|
message: "a nested-object row must round-trip structurally",
|
|
1101
1608
|
context: {
|
|
@@ -1118,8 +1625,9 @@ async function* driverFindings(factory) {
|
|
|
1118
1625
|
...CONFORMANCE_USERS_SCHEMA,
|
|
1119
1626
|
columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
|
|
1120
1627
|
name: "legacy",
|
|
1121
|
-
|
|
1122
|
-
|
|
1628
|
+
storage: "boolean",
|
|
1629
|
+
optional: true,
|
|
1630
|
+
nullable: false
|
|
1123
1631
|
}]
|
|
1124
1632
|
};
|
|
1125
1633
|
await driver.open([deployedUsers, CONFORMANCE_POSTS_SCHEMA]);
|
|
@@ -1130,7 +1638,7 @@ async function* driverFindings(factory) {
|
|
|
1130
1638
|
legacy: true
|
|
1131
1639
|
});
|
|
1132
1640
|
const removePlan = planMigration([deployedUsers], [CONFORMANCE_USERS_SCHEMA]);
|
|
1133
|
-
await driver.migrate(removePlan);
|
|
1641
|
+
await driver.migrate({ plan: removePlan });
|
|
1134
1642
|
const migrated = await driver.read("users", "u1");
|
|
1135
1643
|
if (migrated === void 0 || "legacy" in migrated) {
|
|
1136
1644
|
await driver.close();
|
|
@@ -1147,14 +1655,14 @@ async function* driverFindings(factory) {
|
|
|
1147
1655
|
}
|
|
1148
1656
|
let caught;
|
|
1149
1657
|
try {
|
|
1150
|
-
await driver.migrate({
|
|
1658
|
+
await driver.migrate({ plan: {
|
|
1151
1659
|
from: 0,
|
|
1152
1660
|
to: 1,
|
|
1153
1661
|
steps: [{
|
|
1154
1662
|
operation: "table.remove",
|
|
1155
1663
|
table: "ghost"
|
|
1156
1664
|
}]
|
|
1157
|
-
});
|
|
1665
|
+
} });
|
|
1158
1666
|
} catch (error) {
|
|
1159
1667
|
caught = error;
|
|
1160
1668
|
}
|
|
@@ -1196,16 +1704,16 @@ async function* driverFindings(factory) {
|
|
|
1196
1704
|
age: 30
|
|
1197
1705
|
}
|
|
1198
1706
|
]) await driver.write("users", row.id, row);
|
|
1199
|
-
const
|
|
1707
|
+
const input = { conditions: [{
|
|
1200
1708
|
column: "age",
|
|
1201
1709
|
operator: "above",
|
|
1202
1710
|
values: [10],
|
|
1203
1711
|
connector: "and"
|
|
1204
1712
|
}] };
|
|
1205
1713
|
const matched = [];
|
|
1206
|
-
for await (const row of driver.stream("users",
|
|
1714
|
+
for await (const row of driver.stream("users", input)) matched.push(row);
|
|
1207
1715
|
const matchedIds = matched.map((row) => row.id).sort();
|
|
1208
|
-
if (!
|
|
1716
|
+
if (!equalsValue(matchedIds, ["b", "c"])) {
|
|
1209
1717
|
await driver.close();
|
|
1210
1718
|
yield {
|
|
1211
1719
|
check: "stream-match",
|
|
@@ -1249,15 +1757,15 @@ async function* driverFindings(factory) {
|
|
|
1249
1757
|
name: "Ada",
|
|
1250
1758
|
age: 30
|
|
1251
1759
|
});
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1760
|
+
await driver.transaction(async (transaction) => {
|
|
1761
|
+
await transaction.write("users", "u2", {
|
|
1762
|
+
id: "u2",
|
|
1763
|
+
name: "Grace",
|
|
1764
|
+
age: 40
|
|
1765
|
+
});
|
|
1257
1766
|
});
|
|
1258
|
-
await committing.commit();
|
|
1259
1767
|
const afterCommit = [...await driver.keys("users")].sort();
|
|
1260
|
-
if (!
|
|
1768
|
+
if (!equalsValue(afterCommit, ["u1", "u2"])) {
|
|
1261
1769
|
await driver.close();
|
|
1262
1770
|
yield {
|
|
1263
1771
|
check: "transaction-commit",
|
|
@@ -1270,16 +1778,22 @@ async function* driverFindings(factory) {
|
|
|
1270
1778
|
};
|
|
1271
1779
|
break transactionPhase;
|
|
1272
1780
|
}
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1781
|
+
const reason = {};
|
|
1782
|
+
try {
|
|
1783
|
+
await driver.transaction(async (transaction) => {
|
|
1784
|
+
await transaction.write("users", "u3", {
|
|
1785
|
+
id: "u3",
|
|
1786
|
+
name: "Marie",
|
|
1787
|
+
age: 50
|
|
1788
|
+
});
|
|
1789
|
+
throw reason;
|
|
1790
|
+
});
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (error !== reason) throw error;
|
|
1793
|
+
}
|
|
1280
1794
|
const afterRollback = [...await driver.keys("users")].sort();
|
|
1281
1795
|
await driver.close();
|
|
1282
|
-
if (!
|
|
1796
|
+
if (!equalsValue(afterRollback, ["u1", "u2"])) yield {
|
|
1283
1797
|
check: "transaction-rollback",
|
|
1284
1798
|
message: "transaction rollback must restore pre-transaction state",
|
|
1285
1799
|
context: {
|
|
@@ -1295,33 +1809,33 @@ async function* driverFindings(factory) {
|
|
|
1295
1809
|
context: { error }
|
|
1296
1810
|
};
|
|
1297
1811
|
}
|
|
1298
|
-
|
|
1812
|
+
metadataPhase: try {
|
|
1299
1813
|
const driver = factory();
|
|
1300
|
-
if (driver.
|
|
1814
|
+
if (driver.metadata === void 0 || driver.stamp === void 0) break metadataPhase;
|
|
1301
1815
|
await driver.open(CONFORMANCE_SCHEMA);
|
|
1302
|
-
const fresh = await driver.
|
|
1816
|
+
const fresh = await driver.metadata();
|
|
1303
1817
|
if (fresh !== void 0) {
|
|
1304
1818
|
await driver.close();
|
|
1305
1819
|
yield {
|
|
1306
|
-
check: "
|
|
1307
|
-
message: "a fresh store must report undefined
|
|
1820
|
+
check: "metadata-fresh",
|
|
1821
|
+
message: "a fresh store must report undefined metadata",
|
|
1308
1822
|
context: {
|
|
1309
1823
|
expected: void 0,
|
|
1310
1824
|
actual: fresh
|
|
1311
1825
|
}
|
|
1312
1826
|
};
|
|
1313
|
-
break
|
|
1827
|
+
break metadataPhase;
|
|
1314
1828
|
}
|
|
1315
1829
|
const stamped = {
|
|
1316
1830
|
version: 1,
|
|
1317
1831
|
schema: CONFORMANCE_SCHEMA
|
|
1318
1832
|
};
|
|
1319
1833
|
await driver.stamp(stamped);
|
|
1320
|
-
const read = await driver.
|
|
1834
|
+
const read = await driver.metadata();
|
|
1321
1835
|
await driver.close();
|
|
1322
|
-
if (read === void 0 || !
|
|
1323
|
-
check: "
|
|
1324
|
-
message: "
|
|
1836
|
+
if (read === void 0 || !equalsValue(read, stamped)) yield {
|
|
1837
|
+
check: "metadata-stamp",
|
|
1838
|
+
message: "metadata() must return exactly the last-stamped value",
|
|
1325
1839
|
context: {
|
|
1326
1840
|
expected: stamped,
|
|
1327
1841
|
actual: read
|
|
@@ -1329,7 +1843,7 @@ async function* driverFindings(factory) {
|
|
|
1329
1843
|
};
|
|
1330
1844
|
} catch (error) {
|
|
1331
1845
|
yield {
|
|
1332
|
-
check: "
|
|
1846
|
+
check: "metadata-stamp",
|
|
1333
1847
|
message: error instanceof Error ? error.message : String(error),
|
|
1334
1848
|
context: { error }
|
|
1335
1849
|
};
|
|
@@ -1358,7 +1872,7 @@ async function* driverFindings(factory) {
|
|
|
1358
1872
|
});
|
|
1359
1873
|
await rollback();
|
|
1360
1874
|
const usersKeys = [...await driver.keys("users")];
|
|
1361
|
-
if (!
|
|
1875
|
+
if (!equalsValue(usersKeys, ["u1"])) {
|
|
1362
1876
|
await driver.close();
|
|
1363
1877
|
yield {
|
|
1364
1878
|
check: "snapshot-scoped-users",
|
|
@@ -1373,7 +1887,7 @@ async function* driverFindings(factory) {
|
|
|
1373
1887
|
}
|
|
1374
1888
|
const postsKeys = [...await driver.keys("posts")].sort();
|
|
1375
1889
|
await driver.close();
|
|
1376
|
-
if (!
|
|
1890
|
+
if (!equalsValue(postsKeys, ["p1", "p2"])) yield {
|
|
1377
1891
|
check: "snapshot-scoped-posts",
|
|
1378
1892
|
message: "a scoped snapshot must leave an unnamed table's mutations intact",
|
|
1379
1893
|
context: {
|
|
@@ -1446,62 +1960,451 @@ async function auditDriver(factory) {
|
|
|
1446
1960
|
for await (const finding of driverFindings(factory)) findings.push(finding);
|
|
1447
1961
|
return findings;
|
|
1448
1962
|
}
|
|
1449
|
-
/**
|
|
1450
|
-
* Generate an RFC 4122 version 4 UUID from a number source — no host crypto global.
|
|
1451
|
-
*
|
|
1452
|
-
* @remarks
|
|
1453
|
-
* Draws exactly {@link UUID_BYTE_COUNT} values from `random`, one per byte, then
|
|
1454
|
-
* forces the version (`4`) and variant (`10xx`) bits. The default source is
|
|
1455
|
-
* `Math.random` — a pure-ECMAScript intrinsic, so generation works on every host;
|
|
1456
|
-
* pass a seeded source (`seededRandom` from `@orkestrel/contract`) and reuse it
|
|
1457
|
-
* across calls for reproducible sequences in tests and fixtures — production
|
|
1458
|
-
* identifiers should keep the default source, whose engine entropy is far larger
|
|
1459
|
-
* than a 32-bit seed. Each byte is floored and masked, so a source straying
|
|
1460
|
-
* outside `[0, 1)` (negative, `>= 1`, `NaN`, `Infinity`) can never yield a
|
|
1461
|
-
* malformed UUID. Suitable as a collision-resistant record identifier — not a
|
|
1462
|
-
* cryptographic token; never use one as a secret.
|
|
1463
|
-
*
|
|
1464
|
-
* @param random - A number source returning values in the half-open range `[0, 1)` (defaults to `Math.random`)
|
|
1465
|
-
* @returns A lowercase RFC 4122 version 4 UUID
|
|
1466
|
-
*
|
|
1467
|
-
* @example
|
|
1468
|
-
* ```ts
|
|
1469
|
-
* import { generateUUID } from '@orkestrel/database'
|
|
1470
|
-
* import { seededRandom } from '@orkestrel/contract'
|
|
1471
|
-
*
|
|
1472
|
-
* generateUUID() // e.g. '9b2f7c1e-3d4a-4f6b-8e2d-5a1c0b9f8e7d'
|
|
1473
|
-
* generateUUID(seededRandom(42)) // the same UUID on every run
|
|
1474
|
-
* ```
|
|
1475
|
-
*/
|
|
1476
|
-
function generateUUID(random = Math.random) {
|
|
1477
|
-
const hex = Array.from({ length: 16 }, (_, index) => {
|
|
1478
|
-
const byte = Math.floor(random() * 256) & 255;
|
|
1479
|
-
if (index === 6) return byte & 15 | 64;
|
|
1480
|
-
if (index === 8) return byte & 63 | 128;
|
|
1481
|
-
return byte;
|
|
1482
|
-
}).map((byte) => byte.toString(16).padStart(2, "0"));
|
|
1483
|
-
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
|
1484
|
-
}
|
|
1485
1963
|
//#endregion
|
|
1486
|
-
//#region src/core/
|
|
1964
|
+
//#region src/core/TransactionIterator.ts
|
|
1487
1965
|
/**
|
|
1488
|
-
*
|
|
1966
|
+
* The internal continuation boundary for one transaction-scoped async iterable.
|
|
1489
1967
|
*
|
|
1490
1968
|
* @remarks
|
|
1491
|
-
*
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1494
|
-
* skipped. `update` and `remove` act on the row at the current position.
|
|
1969
|
+
* Each active continuation enters the owning transaction ledger independently,
|
|
1970
|
+
* so an idle iterator never pins settlement. A continuation requested after
|
|
1971
|
+
* admission closes rejects while still attempting source cleanup exactly once.
|
|
1495
1972
|
*/
|
|
1496
|
-
var
|
|
1497
|
-
#
|
|
1498
|
-
#
|
|
1499
|
-
#
|
|
1973
|
+
var TransactionIterator = class {
|
|
1974
|
+
#source;
|
|
1975
|
+
#scope;
|
|
1976
|
+
#cleaned = false;
|
|
1977
|
+
constructor(source, scope) {
|
|
1978
|
+
this.#source = source[Symbol.asyncIterator]();
|
|
1979
|
+
this.#scope = scope;
|
|
1980
|
+
}
|
|
1981
|
+
[Symbol.asyncIterator]() {
|
|
1982
|
+
return this;
|
|
1983
|
+
}
|
|
1984
|
+
next() {
|
|
1985
|
+
return this.#continue(() => this.#next());
|
|
1986
|
+
}
|
|
1987
|
+
return() {
|
|
1988
|
+
return this.#continue(() => this.#return());
|
|
1989
|
+
}
|
|
1990
|
+
throw(error) {
|
|
1991
|
+
return this.#continue(() => this.#throw(error));
|
|
1992
|
+
}
|
|
1993
|
+
async #next() {
|
|
1994
|
+
if (this.#cleaned) return {
|
|
1995
|
+
done: true,
|
|
1996
|
+
value: void 0
|
|
1997
|
+
};
|
|
1998
|
+
const result = await this.#source.next();
|
|
1999
|
+
if (result.done === true) this.#cleaned = true;
|
|
2000
|
+
return result;
|
|
2001
|
+
}
|
|
2002
|
+
async #return() {
|
|
2003
|
+
if (this.#cleaned || this.#source.return === void 0) {
|
|
2004
|
+
this.#cleaned = true;
|
|
2005
|
+
return {
|
|
2006
|
+
done: true,
|
|
2007
|
+
value: void 0
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
this.#cleaned = true;
|
|
2011
|
+
return this.#source.return();
|
|
2012
|
+
}
|
|
2013
|
+
async #throw(error) {
|
|
2014
|
+
if (this.#source.throw !== void 0) {
|
|
2015
|
+
const result = await this.#source.throw(error);
|
|
2016
|
+
if (result.done === true) this.#cleaned = true;
|
|
2017
|
+
return result;
|
|
2018
|
+
}
|
|
2019
|
+
try {
|
|
2020
|
+
await this.#return();
|
|
2021
|
+
} catch {}
|
|
2022
|
+
throw error;
|
|
2023
|
+
}
|
|
2024
|
+
#continue(operation) {
|
|
2025
|
+
if (!this.#scope.accepting) this.#cleanup();
|
|
2026
|
+
return this.#scope.track(operation);
|
|
2027
|
+
}
|
|
2028
|
+
#cleanup() {
|
|
2029
|
+
if (this.#cleaned) return;
|
|
2030
|
+
this.#cleaned = true;
|
|
2031
|
+
try {
|
|
2032
|
+
(this.#source.return?.())?.catch(() => {});
|
|
2033
|
+
} catch {}
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
//#endregion
|
|
2037
|
+
//#region src/core/TransactionScope.ts
|
|
2038
|
+
/**
|
|
2039
|
+
* The internal lifetime boundary for one database transaction callback.
|
|
2040
|
+
*
|
|
2041
|
+
* @remarks
|
|
2042
|
+
* Promise operations enter synchronously through {@link track}. Closing stops new
|
|
2043
|
+
* work while {@link drain} contains every operation already accepted, including
|
|
2044
|
+
* work the callback started without awaiting. {@link stream} applies the same
|
|
2045
|
+
* boundary to each iterator continuation without retaining an idle iterator.
|
|
2046
|
+
*/
|
|
2047
|
+
var TransactionScope = class {
|
|
2048
|
+
#operations = /* @__PURE__ */ new Set();
|
|
2049
|
+
#accepting = true;
|
|
2050
|
+
#failed = false;
|
|
2051
|
+
#error;
|
|
2052
|
+
get accepting() {
|
|
2053
|
+
return this.#accepting;
|
|
2054
|
+
}
|
|
2055
|
+
check() {
|
|
2056
|
+
if (!this.#accepting) throw new DatabaseError("CONFLICT", "Transaction scope has settled");
|
|
2057
|
+
}
|
|
2058
|
+
track(operation) {
|
|
2059
|
+
try {
|
|
2060
|
+
this.check();
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
return Promise.reject(error);
|
|
2063
|
+
}
|
|
2064
|
+
let promise;
|
|
2065
|
+
try {
|
|
2066
|
+
promise = operation();
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
promise = Promise.reject(error);
|
|
2069
|
+
}
|
|
2070
|
+
this.#operations.add(promise);
|
|
2071
|
+
promise.then(() => {
|
|
2072
|
+
this.#operations.delete(promise);
|
|
2073
|
+
}, (error) => {
|
|
2074
|
+
this.#operations.delete(promise);
|
|
2075
|
+
if (!this.#failed) {
|
|
2076
|
+
this.#failed = true;
|
|
2077
|
+
this.#error = error;
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
return promise;
|
|
2081
|
+
}
|
|
2082
|
+
stream(source) {
|
|
2083
|
+
return new TransactionIterator(source, this);
|
|
2084
|
+
}
|
|
2085
|
+
stop() {
|
|
2086
|
+
this.#accepting = false;
|
|
2087
|
+
}
|
|
2088
|
+
async drain() {
|
|
2089
|
+
while (this.#operations.size > 0) await Promise.allSettled(this.#operations);
|
|
2090
|
+
if (this.#failed) throw this.#error;
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
//#endregion
|
|
2094
|
+
//#region src/core/DatabaseContext.ts
|
|
2095
|
+
/**
|
|
2096
|
+
* The internal shared owner behind every typed view of one database.
|
|
2097
|
+
*
|
|
2098
|
+
* @remarks
|
|
2099
|
+
* A context owns the driver, merged physical schema, lifecycle, observation,
|
|
2100
|
+
* migration, and single transaction admission. It is deliberately omitted from
|
|
2101
|
+
* the public barrel; {@link Database} is the consumer-facing typed view.
|
|
2102
|
+
*/
|
|
2103
|
+
var DatabaseContext = class {
|
|
2104
|
+
#driver;
|
|
2105
|
+
#name;
|
|
2106
|
+
#version;
|
|
2107
|
+
#error;
|
|
2108
|
+
#emitter;
|
|
2109
|
+
#operations = /* @__PURE__ */ new Set();
|
|
2110
|
+
#schema = [];
|
|
2111
|
+
#transaction;
|
|
2112
|
+
#status = "idle";
|
|
2113
|
+
#ready;
|
|
2114
|
+
#failure;
|
|
2115
|
+
constructor(options) {
|
|
2116
|
+
this.#driver = options.driver;
|
|
2117
|
+
this.#name = options.name ?? "database";
|
|
2118
|
+
this.#version = options.version;
|
|
2119
|
+
this.#error = options.error;
|
|
2120
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
2121
|
+
...options.on === void 0 ? {} : { on: options.on },
|
|
2122
|
+
...options.error === void 0 ? {} : { error: options.error }
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
get driver() {
|
|
2126
|
+
return this.#driver;
|
|
2127
|
+
}
|
|
2128
|
+
get emitter() {
|
|
2129
|
+
return this.#emitter;
|
|
2130
|
+
}
|
|
2131
|
+
get error() {
|
|
2132
|
+
return this.#error;
|
|
2133
|
+
}
|
|
2134
|
+
get name() {
|
|
2135
|
+
return this.#name;
|
|
2136
|
+
}
|
|
2137
|
+
get accepting() {
|
|
2138
|
+
return this.#status !== "closed" && this.#transaction === void 0;
|
|
2139
|
+
}
|
|
2140
|
+
get status() {
|
|
2141
|
+
return this.#status;
|
|
2142
|
+
}
|
|
2143
|
+
get version() {
|
|
2144
|
+
return this.#version;
|
|
2145
|
+
}
|
|
2146
|
+
register(schema) {
|
|
2147
|
+
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2148
|
+
if (this.#ready !== void 0 || this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' cannot import tables after opening has started`, {
|
|
2149
|
+
name: this.#name,
|
|
2150
|
+
status: this.#status
|
|
2151
|
+
});
|
|
2152
|
+
const registered = normalizeDriverSchema(schema);
|
|
2153
|
+
const merged = [...this.#schema];
|
|
2154
|
+
for (const table of registered) {
|
|
2155
|
+
const existing = merged.find((candidate) => candidate.name === table.name);
|
|
2156
|
+
if (existing === void 0) {
|
|
2157
|
+
merged.push(table);
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
if (!equalsValue(existing, table)) throw new DatabaseError("VALIDATION", `Table '${table.name}' conflicts with its registered schema`, { table: table.name });
|
|
2161
|
+
}
|
|
2162
|
+
this.#schema = normalizeDriverSchema(merged);
|
|
2163
|
+
}
|
|
2164
|
+
async open() {
|
|
2165
|
+
this.#outside();
|
|
2166
|
+
await this.connect();
|
|
2167
|
+
}
|
|
2168
|
+
async close() {
|
|
2169
|
+
this.#outside();
|
|
2170
|
+
if (this.#status === "closed") return;
|
|
2171
|
+
this.#status = "closed";
|
|
2172
|
+
await this.#drain();
|
|
2173
|
+
const ready = this.#ready;
|
|
2174
|
+
if (ready !== void 0) await ready.catch(() => {});
|
|
2175
|
+
this.#ready = void 0;
|
|
2176
|
+
await this.#driver.close();
|
|
2177
|
+
this.#emitter.emit("close");
|
|
2178
|
+
}
|
|
2179
|
+
connect() {
|
|
2180
|
+
return this.#connect();
|
|
2181
|
+
}
|
|
2182
|
+
track(operation) {
|
|
2183
|
+
try {
|
|
2184
|
+
this.#admit();
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
return Promise.reject(error);
|
|
2187
|
+
}
|
|
2188
|
+
let promise;
|
|
2189
|
+
try {
|
|
2190
|
+
promise = operation();
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
promise = Promise.reject(error);
|
|
2193
|
+
}
|
|
2194
|
+
this.#operations.add(promise);
|
|
2195
|
+
promise.then(() => {
|
|
2196
|
+
this.#operations.delete(promise);
|
|
2197
|
+
}, () => {
|
|
2198
|
+
this.#operations.delete(promise);
|
|
2199
|
+
});
|
|
2200
|
+
return promise;
|
|
2201
|
+
}
|
|
2202
|
+
async transaction(scope, options) {
|
|
2203
|
+
checkAbort(options?.signal);
|
|
2204
|
+
this.#admit();
|
|
2205
|
+
const token = {};
|
|
2206
|
+
this.#transaction = token;
|
|
2207
|
+
try {
|
|
2208
|
+
await this.#drain();
|
|
2209
|
+
await this.#connect();
|
|
2210
|
+
if (this.#driver.transaction !== void 0) {
|
|
2211
|
+
const rejection = {
|
|
2212
|
+
rejected: false,
|
|
2213
|
+
error: void 0,
|
|
2214
|
+
marker: {}
|
|
2215
|
+
};
|
|
2216
|
+
try {
|
|
2217
|
+
const value = await this.#driver.transaction(async (storage) => {
|
|
2218
|
+
this.#emitter.emit("transaction");
|
|
2219
|
+
const outcome = await scope(storage, new TransactionScope());
|
|
2220
|
+
if (!outcome.success) {
|
|
2221
|
+
rejection.rejected = true;
|
|
2222
|
+
rejection.error = outcome.error;
|
|
2223
|
+
throw rejection.marker;
|
|
2224
|
+
}
|
|
2225
|
+
return outcome.value;
|
|
2226
|
+
});
|
|
2227
|
+
this.#emitter.emit("commit");
|
|
2228
|
+
return value;
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
if (rejection.rejected) {
|
|
2231
|
+
if (Object.is(error, rejection.marker)) {
|
|
2232
|
+
this.#emitter.emit("rollback", rejection.error);
|
|
2233
|
+
throw rejection.error;
|
|
2234
|
+
}
|
|
2235
|
+
throw this.#rollbackError(rejection.error, error);
|
|
2236
|
+
}
|
|
2237
|
+
throw error;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const rollback = await this.#driver.snapshot();
|
|
2241
|
+
this.#emitter.emit("transaction");
|
|
2242
|
+
const outcome = await scope(this.#driver, new TransactionScope());
|
|
2243
|
+
if (outcome.success) {
|
|
2244
|
+
this.#emitter.emit("commit");
|
|
2245
|
+
return outcome.value;
|
|
2246
|
+
}
|
|
2247
|
+
try {
|
|
2248
|
+
await rollback();
|
|
2249
|
+
} catch (cause) {
|
|
2250
|
+
throw this.#rollbackError(outcome.error, cause);
|
|
2251
|
+
}
|
|
2252
|
+
this.#emitter.emit("rollback", outcome.error);
|
|
2253
|
+
throw outcome.error;
|
|
2254
|
+
} finally {
|
|
2255
|
+
if (this.#transaction === token) this.#transaction = void 0;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
async migrate(deployed, options) {
|
|
2259
|
+
checkAbort(options?.signal);
|
|
2260
|
+
this.#outside();
|
|
2261
|
+
if (this.#ready !== void 0 || this.#status !== "idle") throw new DatabaseError("CONFLICT", `Database '${this.#name}' cannot apply an explicit deployed schema after opening`, {
|
|
2262
|
+
name: this.#name,
|
|
2263
|
+
status: this.#status
|
|
2264
|
+
});
|
|
2265
|
+
if (this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, { name: this.#name });
|
|
2266
|
+
const plan = planMigration(deployed, this.#schema);
|
|
2267
|
+
const readiness = this.#transition(deployed, plan).catch((error) => {
|
|
2268
|
+
if (this.#ready === readiness) this.#ready = void 0;
|
|
2269
|
+
this.#failure = { error };
|
|
2270
|
+
throw error;
|
|
2271
|
+
});
|
|
2272
|
+
this.#failure = void 0;
|
|
2273
|
+
this.#ready = readiness;
|
|
2274
|
+
await readiness;
|
|
2275
|
+
return plan;
|
|
2276
|
+
}
|
|
2277
|
+
#admit() {
|
|
2278
|
+
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2279
|
+
if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
|
|
2280
|
+
if (this.#failure !== void 0) throw this.#failure.error;
|
|
2281
|
+
}
|
|
2282
|
+
#outside() {
|
|
2283
|
+
if (this.#transaction !== void 0) throw new DatabaseError("CONFLICT", `Database '${this.#name}' has an active transaction`, { name: this.#name });
|
|
2284
|
+
}
|
|
2285
|
+
#connect() {
|
|
2286
|
+
if (this.#ready !== void 0) return this.#ready;
|
|
2287
|
+
if (this.#failure !== void 0) throw this.#failure.error;
|
|
2288
|
+
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2289
|
+
const readiness = this.#driver.open(this.#schema).then(async () => {
|
|
2290
|
+
if (this.#status === "idle") {
|
|
2291
|
+
this.#status = "open";
|
|
2292
|
+
this.#emitter.emit("open");
|
|
2293
|
+
}
|
|
2294
|
+
await this.#reconcile();
|
|
2295
|
+
}).catch((error) => {
|
|
2296
|
+
if (this.#ready === readiness) this.#ready = void 0;
|
|
2297
|
+
throw error;
|
|
2298
|
+
});
|
|
2299
|
+
this.#ready = readiness;
|
|
2300
|
+
return readiness;
|
|
2301
|
+
}
|
|
2302
|
+
async #drain() {
|
|
2303
|
+
if (this.#operations.size === 0) return;
|
|
2304
|
+
await Promise.allSettled(this.#operations);
|
|
2305
|
+
}
|
|
2306
|
+
async #reconcile() {
|
|
2307
|
+
if (this.#version === void 0 || this.#driver.metadata === void 0 || this.#driver.stamp === void 0) return;
|
|
2308
|
+
const metadata = await this.#driver.metadata();
|
|
2309
|
+
if (metadata === void 0) {
|
|
2310
|
+
await this.#stamp();
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
if (metadata.version > this.#version) throw new DatabaseError("MIGRATION", `Database '${this.#name}' store version ${metadata.version} is newer than declared version ${this.#version}`, {
|
|
2314
|
+
name: this.#name,
|
|
2315
|
+
stored: metadata.version,
|
|
2316
|
+
declared: this.#version
|
|
2317
|
+
});
|
|
2318
|
+
if (metadata.version === this.#version) {
|
|
2319
|
+
if (!equalsValue(normalizeDriverSchema(metadata.schema), this.#schema)) throw new DatabaseError("MIGRATION", `Database '${this.#name}' stored schema differs at version ${this.#version}`, {
|
|
2320
|
+
name: this.#name,
|
|
2321
|
+
version: this.#version
|
|
2322
|
+
});
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const plan = planMigration(metadata.schema, this.#schema, metadata.version, this.#version);
|
|
2326
|
+
if (plan.steps.length > 0 && this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, {
|
|
2327
|
+
name: this.#name,
|
|
2328
|
+
stored: metadata.version,
|
|
2329
|
+
declared: this.#version
|
|
2330
|
+
});
|
|
2331
|
+
await this.#apply(plan);
|
|
2332
|
+
}
|
|
2333
|
+
async #apply(plan) {
|
|
2334
|
+
if (this.#driver.transaction !== void 0) {
|
|
2335
|
+
await this.#driver.transaction(async (storage) => {
|
|
2336
|
+
if (plan.steps.length > 0 && storage.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' transaction does not support migration`, { name: this.#name });
|
|
2337
|
+
if (storage.migrate === void 0) await this.#stamp(storage);
|
|
2338
|
+
else await storage.migrate(this.#migration(plan));
|
|
2339
|
+
});
|
|
2340
|
+
this.#emitter.emit("migrate", plan);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
if (this.#driver.migrate === void 0) await this.#stamp();
|
|
2344
|
+
else await this.#driver.migrate(this.#migration(plan));
|
|
2345
|
+
this.#emitter.emit("migrate", plan);
|
|
2346
|
+
}
|
|
2347
|
+
async #transition(deployed, plan) {
|
|
2348
|
+
await this.#driver.open(deployed);
|
|
2349
|
+
await this.#apply(plan);
|
|
2350
|
+
if (this.#status === "idle") {
|
|
2351
|
+
this.#status = "open";
|
|
2352
|
+
this.#emitter.emit("open");
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async #stamp(storage) {
|
|
2356
|
+
const target = storage ?? this.#driver;
|
|
2357
|
+
if (this.#version === void 0 || target.stamp === void 0) return;
|
|
2358
|
+
const metadata = {
|
|
2359
|
+
version: this.#version,
|
|
2360
|
+
schema: this.#schema
|
|
2361
|
+
};
|
|
2362
|
+
await target.stamp(metadata);
|
|
2363
|
+
}
|
|
2364
|
+
#migration(plan) {
|
|
2365
|
+
if (this.#version === void 0) return { plan };
|
|
2366
|
+
return {
|
|
2367
|
+
plan,
|
|
2368
|
+
metadata: {
|
|
2369
|
+
version: this.#version,
|
|
2370
|
+
schema: this.#schema
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
#rollbackError(transaction, cause) {
|
|
2375
|
+
return new DatabaseError("DRIVER", `Database '${this.#name}' rollback failed`, {
|
|
2376
|
+
cause,
|
|
2377
|
+
transaction
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
//#endregion
|
|
2382
|
+
//#region src/core/Cursor.ts
|
|
2383
|
+
/**
|
|
2384
|
+
* A forward row cursor for bulk in-place mutation.
|
|
2385
|
+
*
|
|
2386
|
+
* @remarks
|
|
2387
|
+
* Iterates a snapshot of the table's keys captured when the cursor was opened,
|
|
2388
|
+
* reading each row lazily through the owning table — so a mutation made during
|
|
2389
|
+
* iteration cannot corrupt the walk, and a key removed mid-iteration is simply
|
|
2390
|
+
* skipped. `update` and `remove` act on the row at the current position.
|
|
2391
|
+
*/
|
|
2392
|
+
var Cursor = class {
|
|
2393
|
+
#keys;
|
|
2394
|
+
#read;
|
|
2395
|
+
#update;
|
|
2396
|
+
#remove;
|
|
2397
|
+
#track;
|
|
2398
|
+
#tail = Promise.resolve();
|
|
2399
|
+
#index = -1;
|
|
1500
2400
|
#value;
|
|
1501
2401
|
#closed = false;
|
|
1502
|
-
constructor(
|
|
1503
|
-
this.#table = table;
|
|
2402
|
+
constructor(keys, read, update, remove, track) {
|
|
1504
2403
|
this.#keys = keys;
|
|
2404
|
+
this.#read = read;
|
|
2405
|
+
this.#update = update;
|
|
2406
|
+
this.#remove = remove;
|
|
2407
|
+
this.#track = track;
|
|
1505
2408
|
}
|
|
1506
2409
|
get value() {
|
|
1507
2410
|
return this.#value;
|
|
@@ -1512,16 +2415,31 @@ var Cursor = class {
|
|
|
1512
2415
|
get done() {
|
|
1513
2416
|
return this.#closed || this.#index >= this.#keys.length;
|
|
1514
2417
|
}
|
|
1515
|
-
|
|
2418
|
+
next() {
|
|
2419
|
+
return this.#track(() => this.#queue(() => this.#advance()));
|
|
2420
|
+
}
|
|
2421
|
+
update(changes) {
|
|
2422
|
+
return this.#track(() => this.#queue(() => this.#revise(changes)));
|
|
2423
|
+
}
|
|
2424
|
+
remove() {
|
|
2425
|
+
return this.#track(() => this.#queue(() => this.#delete()));
|
|
2426
|
+
}
|
|
2427
|
+
close() {
|
|
2428
|
+
this.#closed = true;
|
|
2429
|
+
this.#value = void 0;
|
|
2430
|
+
}
|
|
2431
|
+
async #advance() {
|
|
1516
2432
|
if (this.#closed) return;
|
|
1517
2433
|
this.#index += 1;
|
|
1518
2434
|
while (this.#index < this.#keys.length) {
|
|
2435
|
+
if (this.#closed) return;
|
|
1519
2436
|
const key = this.#keys[this.#index];
|
|
1520
2437
|
if (key === void 0) {
|
|
1521
2438
|
this.#index += 1;
|
|
1522
2439
|
continue;
|
|
1523
2440
|
}
|
|
1524
|
-
const row = await this.#
|
|
2441
|
+
const row = await this.#read(key);
|
|
2442
|
+
if (this.#closed) return;
|
|
1525
2443
|
if (row !== void 0) {
|
|
1526
2444
|
this.#value = row;
|
|
1527
2445
|
return;
|
|
@@ -1530,97 +2448,103 @@ var Cursor = class {
|
|
|
1530
2448
|
}
|
|
1531
2449
|
this.#value = void 0;
|
|
1532
2450
|
}
|
|
1533
|
-
async
|
|
2451
|
+
async #revise(changes) {
|
|
1534
2452
|
if (this.#closed || this.#value === void 0) return;
|
|
1535
2453
|
const key = this.#keys[this.#index];
|
|
1536
2454
|
if (key === void 0) return;
|
|
1537
|
-
await this.#
|
|
1538
|
-
|
|
2455
|
+
await this.#update(key, changes);
|
|
2456
|
+
if (this.#closed) return;
|
|
2457
|
+
const row = await this.#read(key);
|
|
2458
|
+
if (this.#closed) return;
|
|
2459
|
+
this.#value = row;
|
|
1539
2460
|
}
|
|
1540
|
-
async
|
|
2461
|
+
async #delete() {
|
|
1541
2462
|
if (this.#closed || this.#value === void 0) return;
|
|
1542
2463
|
const key = this.#keys[this.#index];
|
|
1543
2464
|
if (key === void 0) return;
|
|
1544
|
-
await this.#
|
|
2465
|
+
await this.#remove(key);
|
|
2466
|
+
if (this.#closed) return;
|
|
1545
2467
|
this.#value = void 0;
|
|
1546
2468
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
this.#
|
|
2469
|
+
#queue(operation) {
|
|
2470
|
+
const result = this.#tail.then(operation);
|
|
2471
|
+
this.#tail = result.then(() => void 0, () => void 0);
|
|
2472
|
+
return result;
|
|
1550
2473
|
}
|
|
1551
2474
|
};
|
|
1552
2475
|
//#endregion
|
|
1553
|
-
//#region src/core/
|
|
2476
|
+
//#region src/core/DatabaseIterator.ts
|
|
1554
2477
|
/**
|
|
1555
|
-
*
|
|
2478
|
+
* The internal continuation admission boundary for a root database stream.
|
|
1556
2479
|
*
|
|
1557
2480
|
* @remarks
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1560
|
-
*
|
|
1561
|
-
*
|
|
2481
|
+
* Each continuation enters the shared root operation ledger independently, so
|
|
2482
|
+
* an idle iterator never delays a transaction or close. A continuation rejected
|
|
2483
|
+
* after transaction or close admission closes attempts source cleanup exactly
|
|
2484
|
+
* once and leaves the iterator terminal.
|
|
1562
2485
|
*/
|
|
1563
|
-
var
|
|
1564
|
-
#
|
|
1565
|
-
#
|
|
1566
|
-
#
|
|
1567
|
-
constructor(
|
|
1568
|
-
this.#
|
|
1569
|
-
this.#
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
return this.#apply("equals", [value]);
|
|
1574
|
-
}
|
|
1575
|
-
not(value) {
|
|
1576
|
-
return this.#apply("not", [value]);
|
|
1577
|
-
}
|
|
1578
|
-
above(value) {
|
|
1579
|
-
return this.#apply("above", [value]);
|
|
1580
|
-
}
|
|
1581
|
-
below(value) {
|
|
1582
|
-
return this.#apply("below", [value]);
|
|
1583
|
-
}
|
|
1584
|
-
from(value) {
|
|
1585
|
-
return this.#apply("from", [value]);
|
|
1586
|
-
}
|
|
1587
|
-
to(value) {
|
|
1588
|
-
return this.#apply("to", [value]);
|
|
1589
|
-
}
|
|
1590
|
-
between(lower, upper) {
|
|
1591
|
-
return this.#apply("between", [lower, upper]);
|
|
1592
|
-
}
|
|
1593
|
-
like(pattern) {
|
|
1594
|
-
return this.#apply("like", [pattern]);
|
|
1595
|
-
}
|
|
1596
|
-
glob(pattern) {
|
|
1597
|
-
return this.#apply("glob", [pattern]);
|
|
2486
|
+
var DatabaseIterator = class {
|
|
2487
|
+
#source;
|
|
2488
|
+
#context;
|
|
2489
|
+
#cleaned = false;
|
|
2490
|
+
constructor(source, context) {
|
|
2491
|
+
this.#source = source[Symbol.asyncIterator]();
|
|
2492
|
+
this.#context = context;
|
|
2493
|
+
}
|
|
2494
|
+
[Symbol.asyncIterator]() {
|
|
2495
|
+
return this;
|
|
1598
2496
|
}
|
|
1599
|
-
|
|
1600
|
-
return this.#
|
|
2497
|
+
next() {
|
|
2498
|
+
return this.#continue(() => this.#next());
|
|
1601
2499
|
}
|
|
1602
|
-
|
|
1603
|
-
return this.#
|
|
2500
|
+
return() {
|
|
2501
|
+
return this.#continue(() => this.#return());
|
|
1604
2502
|
}
|
|
1605
|
-
|
|
1606
|
-
return this.#
|
|
2503
|
+
throw(error) {
|
|
2504
|
+
return this.#continue(() => this.#throw(error));
|
|
1607
2505
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
2506
|
+
async #next() {
|
|
2507
|
+
if (this.#cleaned) return {
|
|
2508
|
+
done: true,
|
|
2509
|
+
value: void 0
|
|
2510
|
+
};
|
|
2511
|
+
await this.#context.connect();
|
|
2512
|
+
const result = await this.#source.next();
|
|
2513
|
+
if (result.done === true) this.#cleaned = true;
|
|
2514
|
+
return result;
|
|
1610
2515
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
2516
|
+
async #return() {
|
|
2517
|
+
if (this.#cleaned || this.#source.return === void 0) {
|
|
2518
|
+
this.#cleaned = true;
|
|
2519
|
+
return {
|
|
2520
|
+
done: true,
|
|
2521
|
+
value: void 0
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
this.#cleaned = true;
|
|
2525
|
+
return this.#source.return();
|
|
2526
|
+
}
|
|
2527
|
+
async #throw(error) {
|
|
2528
|
+
if (this.#source.throw !== void 0) {
|
|
2529
|
+
const result = await this.#source.throw(error);
|
|
2530
|
+
if (result.done === true) this.#cleaned = true;
|
|
2531
|
+
return result;
|
|
2532
|
+
}
|
|
2533
|
+
try {
|
|
2534
|
+
await this.#return();
|
|
2535
|
+
} catch {}
|
|
2536
|
+
throw error;
|
|
1613
2537
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
2538
|
+
#continue(operation) {
|
|
2539
|
+
if (!this.#context.accepting) this.#cleanup();
|
|
2540
|
+
return this.#context.track(operation);
|
|
1616
2541
|
}
|
|
1617
|
-
#
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
});
|
|
2542
|
+
#cleanup() {
|
|
2543
|
+
if (this.#cleaned) return;
|
|
2544
|
+
this.#cleaned = true;
|
|
2545
|
+
try {
|
|
2546
|
+
(this.#source.return?.())?.catch(() => {});
|
|
2547
|
+
} catch {}
|
|
1624
2548
|
}
|
|
1625
2549
|
};
|
|
1626
2550
|
//#endregion
|
|
@@ -1629,12 +2553,9 @@ var Clause = class {
|
|
|
1629
2553
|
* A fluent query builder bound to one table.
|
|
1630
2554
|
*
|
|
1631
2555
|
* @remarks
|
|
1632
|
-
* Accumulates conditions, ordering, JS filters, and a page
|
|
1633
|
-
* mutates and returns the same instance
|
|
1634
|
-
*
|
|
1635
|
-
* table resolves; a `filter` predicate is applied in memory after the read and
|
|
1636
|
-
* before paging, so it composes with the rest without a backend ever seeing a
|
|
1637
|
-
* JS callback.
|
|
2556
|
+
* Accumulates typed conditions, ordering, JS filters, and a page. Each builder
|
|
2557
|
+
* method mutates and returns the same instance. Portable inputs flow to the
|
|
2558
|
+
* table, while predicates remain an in-memory refinement.
|
|
1638
2559
|
*/
|
|
1639
2560
|
var Query = class {
|
|
1640
2561
|
#table;
|
|
@@ -1646,42 +2567,29 @@ var Query = class {
|
|
|
1646
2567
|
constructor(table) {
|
|
1647
2568
|
this.#table = table;
|
|
1648
2569
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
}
|
|
1652
|
-
and(column) {
|
|
1653
|
-
return this.#clause(column, "and");
|
|
1654
|
-
}
|
|
1655
|
-
or(column) {
|
|
1656
|
-
return this.#clause(column, "or");
|
|
1657
|
-
}
|
|
1658
|
-
filter(predicate) {
|
|
1659
|
-
this.#filters.push(predicate);
|
|
2570
|
+
condition(input) {
|
|
2571
|
+
this.#conditions.push(input);
|
|
1660
2572
|
return this;
|
|
1661
2573
|
}
|
|
1662
|
-
|
|
1663
|
-
this.#orders.push(
|
|
1664
|
-
column,
|
|
1665
|
-
direction: "ascending"
|
|
1666
|
-
});
|
|
2574
|
+
order(input) {
|
|
2575
|
+
this.#orders.push(input);
|
|
1667
2576
|
return this;
|
|
1668
2577
|
}
|
|
1669
|
-
|
|
1670
|
-
this.#
|
|
1671
|
-
column,
|
|
1672
|
-
direction: "descending"
|
|
1673
|
-
});
|
|
2578
|
+
filter(predicate) {
|
|
2579
|
+
this.#filters.push(predicate);
|
|
1674
2580
|
return this;
|
|
1675
2581
|
}
|
|
1676
2582
|
limit(count) {
|
|
2583
|
+
validatePage({ limit: count });
|
|
1677
2584
|
this.#limit = count;
|
|
1678
2585
|
return this;
|
|
1679
2586
|
}
|
|
1680
2587
|
offset(count) {
|
|
2588
|
+
validatePage({ offset: count });
|
|
1681
2589
|
this.#offset = count;
|
|
1682
2590
|
return this;
|
|
1683
2591
|
}
|
|
1684
|
-
async
|
|
2592
|
+
async collect() {
|
|
1685
2593
|
if (this.#filters.length === 0) return this.#table.records({
|
|
1686
2594
|
conditions: this.#conditions,
|
|
1687
2595
|
order: this.#orders,
|
|
@@ -1694,8 +2602,8 @@ var Query = class {
|
|
|
1694
2602
|
});
|
|
1695
2603
|
return this.#page(this.#filtered(fetched));
|
|
1696
2604
|
}
|
|
1697
|
-
async
|
|
1698
|
-
return (await this.
|
|
2605
|
+
async find() {
|
|
2606
|
+
return (await this.collect())[0];
|
|
1699
2607
|
}
|
|
1700
2608
|
async count() {
|
|
1701
2609
|
if (this.#filters.length === 0) return this.#table.count({ conditions: this.#conditions });
|
|
@@ -1703,17 +2611,10 @@ var Query = class {
|
|
|
1703
2611
|
return this.#filtered(fetched).length;
|
|
1704
2612
|
}
|
|
1705
2613
|
/**
|
|
1706
|
-
*
|
|
1707
|
-
* limit.
|
|
1708
|
-
*
|
|
1709
|
-
* @remarks
|
|
1710
|
-
* `order` and its comparators are IGNORED (streaming yields unsorted, as rows
|
|
1711
|
-
* are evaluated one at a time). Same abort semantics as
|
|
1712
|
-
* `TableInterface.scan`: the signal (if any) is checked before each yield,
|
|
1713
|
-
* and breaking out early closes the underlying source.
|
|
2614
|
+
* Lazily evaluate conditions, filters, offset, and limit.
|
|
1714
2615
|
*
|
|
1715
|
-
* @param options -
|
|
1716
|
-
* @returns
|
|
2616
|
+
* @param options - Optional abort options
|
|
2617
|
+
* @returns Matching rows in storage order
|
|
1717
2618
|
*/
|
|
1718
2619
|
async *stream(options) {
|
|
1719
2620
|
if (this.#filters.length === 0) {
|
|
@@ -1748,24 +2649,6 @@ var Query = class {
|
|
|
1748
2649
|
if (this.#filters.length === 0) return this.#table.aggregate(operation, column, { conditions: this.#conditions });
|
|
1749
2650
|
return this.#table.records({ conditions: this.#conditions }).then((fetched) => computeAggregate(this.#filtered(fetched), operation, column));
|
|
1750
2651
|
}
|
|
1751
|
-
sum(column) {
|
|
1752
|
-
return this.aggregate("sum", column);
|
|
1753
|
-
}
|
|
1754
|
-
average(column) {
|
|
1755
|
-
return this.aggregate("average", column);
|
|
1756
|
-
}
|
|
1757
|
-
minimum(column) {
|
|
1758
|
-
return this.aggregate("minimum", column);
|
|
1759
|
-
}
|
|
1760
|
-
maximum(column) {
|
|
1761
|
-
return this.aggregate("maximum", column);
|
|
1762
|
-
}
|
|
1763
|
-
#clause(column, connector) {
|
|
1764
|
-
return new Clause((condition) => {
|
|
1765
|
-
this.#conditions.push(condition);
|
|
1766
|
-
return this;
|
|
1767
|
-
}, column, connector);
|
|
1768
|
-
}
|
|
1769
2652
|
#filtered(rows) {
|
|
1770
2653
|
let result = rows;
|
|
1771
2654
|
for (const predicate of this.#filters) result = result.filter(predicate);
|
|
@@ -1808,8 +2691,10 @@ var Table = class {
|
|
|
1808
2691
|
#contract;
|
|
1809
2692
|
#guard;
|
|
1810
2693
|
#generate;
|
|
2694
|
+
#context;
|
|
2695
|
+
#scope;
|
|
1811
2696
|
#emitter;
|
|
1812
|
-
constructor(ready, driver, name, key, contract, generate,
|
|
2697
|
+
constructor(ready, driver, name, key, contract, generate, error, context, scope) {
|
|
1813
2698
|
this.#ready = ready;
|
|
1814
2699
|
this.#driver = driver;
|
|
1815
2700
|
this.#name = name;
|
|
@@ -1817,10 +2702,9 @@ var Table = class {
|
|
|
1817
2702
|
this.#contract = contract;
|
|
1818
2703
|
this.#guard = contract.is;
|
|
1819
2704
|
this.#generate = generate;
|
|
1820
|
-
this.#
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
});
|
|
2705
|
+
this.#context = context;
|
|
2706
|
+
this.#scope = scope;
|
|
2707
|
+
this.#emitter = new _orkestrel_emitter.Emitter({ ...error !== void 0 ? { error } : {} });
|
|
1824
2708
|
}
|
|
1825
2709
|
get emitter() {
|
|
1826
2710
|
return this.#emitter;
|
|
@@ -1834,58 +2718,77 @@ var Table = class {
|
|
|
1834
2718
|
get contract() {
|
|
1835
2719
|
return this.#contract;
|
|
1836
2720
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
2721
|
+
get(keys) {
|
|
2722
|
+
return this.#track(async () => {
|
|
2723
|
+
await this.#ready();
|
|
2724
|
+
if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#read(key));
|
|
2725
|
+
return this.#read(keys);
|
|
2726
|
+
});
|
|
1841
2727
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2728
|
+
resolve(keys) {
|
|
2729
|
+
return this.#track(async () => {
|
|
2730
|
+
await this.#ready();
|
|
2731
|
+
if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#resolveOne(key));
|
|
2732
|
+
return this.#resolveOne(keys);
|
|
2733
|
+
});
|
|
1846
2734
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2735
|
+
has(keys) {
|
|
2736
|
+
return this.#track(async () => {
|
|
2737
|
+
await this.#ready();
|
|
2738
|
+
if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, async (key) => await this.#read(key) !== void 0);
|
|
2739
|
+
return await this.#read(keys) !== void 0;
|
|
2740
|
+
});
|
|
1851
2741
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2742
|
+
keys() {
|
|
2743
|
+
return this.#track(async () => {
|
|
2744
|
+
await this.#ready();
|
|
2745
|
+
return this.#driver.keys(this.#name);
|
|
2746
|
+
});
|
|
1855
2747
|
}
|
|
1856
|
-
async records(
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
2748
|
+
async records(input, options) {
|
|
2749
|
+
validatePage(input);
|
|
2750
|
+
return this.#track(async () => {
|
|
2751
|
+
checkAbort(options?.signal);
|
|
2752
|
+
await this.#ready();
|
|
2753
|
+
const candidate = {
|
|
2754
|
+
...input?.conditions === void 0 ? {} : { conditions: input.conditions },
|
|
2755
|
+
...input?.order === void 0 ? {} : { order: input.order }
|
|
2756
|
+
};
|
|
2757
|
+
const source = await this.#driver.records?.(this.#name, candidate) ?? applyQuery(await this.#collect(), candidate);
|
|
2758
|
+
const rows = [];
|
|
2759
|
+
for (const row of source) if (this.#guard(row)) rows.push(row);
|
|
2760
|
+
const offset = input?.offset ?? 0;
|
|
2761
|
+
const limit = input?.limit;
|
|
2762
|
+
return rows.slice(offset, limit === void 0 ? void 0 : offset + limit);
|
|
2763
|
+
});
|
|
1863
2764
|
}
|
|
1864
2765
|
/**
|
|
1865
|
-
* Count rows matching `
|
|
2766
|
+
* Count contract-valid rows matching `input`'s conditions.
|
|
1866
2767
|
*
|
|
1867
2768
|
* @remarks
|
|
1868
|
-
*
|
|
1869
|
-
* contract guard
|
|
1870
|
-
*
|
|
1871
|
-
* `filterRows`-over-`#collect()` fallback count raw storage) — so it can
|
|
1872
|
-
* exceed `(await records(criteria)).length` when storage holds rows that
|
|
1873
|
-
* no longer conform to the table's contract (legacy or migrated data).
|
|
2769
|
+
* Paging is ignored. Candidate rows use the driver's native `records` hook
|
|
2770
|
+
* when present, then the table contract guard determines the count so legacy
|
|
2771
|
+
* invalid rows cannot make `count()` disagree with `records()`.
|
|
1874
2772
|
*
|
|
1875
|
-
* @param
|
|
2773
|
+
* @param input - Optional conditions to filter by (paging is ignored)
|
|
1876
2774
|
* @param options - `{ signal }` to abort
|
|
1877
|
-
* @returns The count of matching
|
|
2775
|
+
* @returns The count of matching contract-valid rows
|
|
1878
2776
|
*/
|
|
1879
|
-
async count(
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2777
|
+
async count(input, options) {
|
|
2778
|
+
validatePage(input);
|
|
2779
|
+
return this.#track(async () => {
|
|
2780
|
+
checkAbort(options?.signal);
|
|
2781
|
+
await this.#ready();
|
|
2782
|
+
const conditions = input?.conditions;
|
|
2783
|
+
const candidate = conditions === void 0 ? {} : { conditions };
|
|
2784
|
+
const rows = await this.#driver.records?.(this.#name, candidate) ?? filterRows(await this.#collect(), conditions ?? []);
|
|
2785
|
+
let count = 0;
|
|
2786
|
+
for (const row of rows) if (this.#guard(row)) count += 1;
|
|
2787
|
+
return count;
|
|
2788
|
+
});
|
|
1886
2789
|
}
|
|
1887
2790
|
/**
|
|
1888
|
-
* Compute an aggregate over `column` across rows matching `
|
|
2791
|
+
* Compute an aggregate over `column` across rows matching `input`'s
|
|
1889
2792
|
* conditions.
|
|
1890
2793
|
*
|
|
1891
2794
|
* @remarks
|
|
@@ -1897,115 +2800,138 @@ var Table = class {
|
|
|
1897
2800
|
*
|
|
1898
2801
|
* @param operation - The aggregate to compute
|
|
1899
2802
|
* @param column - The column to aggregate
|
|
1900
|
-
* @param
|
|
2803
|
+
* @param input - Optional conditions to filter by (paging is ignored)
|
|
1901
2804
|
* @param options - `{ signal }` to abort
|
|
1902
2805
|
* @returns The aggregate value, or `undefined` when undefined for the inputs
|
|
1903
2806
|
*/
|
|
1904
|
-
async aggregate(operation, column,
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2807
|
+
async aggregate(operation, column, input, options) {
|
|
2808
|
+
validatePage(input);
|
|
2809
|
+
return this.#track(async () => {
|
|
2810
|
+
checkAbort(options?.signal);
|
|
2811
|
+
await this.#ready();
|
|
2812
|
+
const conditions = input?.conditions;
|
|
2813
|
+
const filter = conditions ? { conditions } : {};
|
|
2814
|
+
const native = this.#driver.aggregate?.(this.#name, operation, column, filter);
|
|
2815
|
+
if (native !== void 0) return native;
|
|
2816
|
+
return computeAggregate(await this.#driver.records?.(this.#name, filter) ?? filterRows(await this.#collect(), input?.conditions ?? []), operation, column);
|
|
2817
|
+
});
|
|
1912
2818
|
}
|
|
1913
2819
|
/**
|
|
1914
|
-
* Stream the table's rows matching `
|
|
2820
|
+
* Stream the table's rows matching `input`, applying offset/limit paging.
|
|
1915
2821
|
*
|
|
1916
2822
|
* @remarks
|
|
1917
|
-
* `
|
|
1918
|
-
* table's contract guard
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
* the contract guard runs, when storage holds rows that no longer conform
|
|
1922
|
-
* to the table's contract.
|
|
2823
|
+
* `input.limit` counts rows that pass both the input conditions and the
|
|
2824
|
+
* table's contract guard. Native streams receive only the conditions; this
|
|
2825
|
+
* table rechecks them, narrows each candidate, and applies offset/limit last,
|
|
2826
|
+
* matching {@link records} when storage contains legacy invalid rows.
|
|
1923
2827
|
*
|
|
1924
|
-
* @param
|
|
2828
|
+
* @param input - Optional conditions plus offset/limit paging
|
|
1925
2829
|
* @param options - `{ signal }` to abort mid-stream
|
|
1926
2830
|
* @returns An async iterable of matching, guard-conforming rows
|
|
1927
2831
|
*/
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
if (this.#
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
return;
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const offset = criteria?.offset ?? 0;
|
|
1941
|
-
const limit = criteria?.limit;
|
|
1942
|
-
let matched = 0;
|
|
1943
|
-
let yielded = 0;
|
|
1944
|
-
for await (const row of this.#driver.scan(this.#name)) {
|
|
1945
|
-
checkAbort(options?.signal);
|
|
1946
|
-
if (limit !== void 0 && yielded >= limit) break;
|
|
1947
|
-
if (conditions !== void 0 && conditions.length > 0 && !matchesCriteria(row, conditions)) continue;
|
|
1948
|
-
if (matched < offset) {
|
|
1949
|
-
matched += 1;
|
|
1950
|
-
continue;
|
|
1951
|
-
}
|
|
1952
|
-
matched += 1;
|
|
1953
|
-
const narrowed = this.#cast(row);
|
|
1954
|
-
if (narrowed !== void 0) {
|
|
1955
|
-
yielded += 1;
|
|
1956
|
-
yield narrowed;
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
async set(rows, options) {
|
|
1961
|
-
checkAbort(options?.signal);
|
|
1962
|
-
await this.#ready();
|
|
1963
|
-
if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, false), options?.signal);
|
|
1964
|
-
return this.#put(rows, false);
|
|
2832
|
+
scan(input, options) {
|
|
2833
|
+
validatePage(input);
|
|
2834
|
+
const source = this.#scan(input, options);
|
|
2835
|
+
if (this.#context !== void 0) return new DatabaseIterator(source, this.#context);
|
|
2836
|
+
return this.#scope === void 0 ? source : this.#scope.stream(source);
|
|
2837
|
+
}
|
|
2838
|
+
set(rows, options) {
|
|
2839
|
+
return this.#track(async () => {
|
|
2840
|
+
await this.#wait(options?.signal);
|
|
2841
|
+
if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, false, options), options?.signal);
|
|
2842
|
+
return this.#put(rows, false, options);
|
|
2843
|
+
});
|
|
1965
2844
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2845
|
+
add(rows, options) {
|
|
2846
|
+
return this.#track(async () => {
|
|
2847
|
+
await this.#wait(options?.signal);
|
|
2848
|
+
if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, true, options), options?.signal);
|
|
2849
|
+
return this.#put(rows, true, options);
|
|
2850
|
+
});
|
|
1971
2851
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2852
|
+
update(keys, changes, options) {
|
|
2853
|
+
return this.#track(async () => {
|
|
2854
|
+
await this.#wait(options?.signal);
|
|
2855
|
+
if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#updateOne(key, changes, options), options?.signal);
|
|
2856
|
+
return this.#updateOne(keys, changes, options);
|
|
2857
|
+
});
|
|
1977
2858
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2859
|
+
remove(keys, options) {
|
|
2860
|
+
return this.#track(async () => {
|
|
2861
|
+
await this.#wait(options?.signal);
|
|
2862
|
+
if ((0, _orkestrel_contract.isArray)(keys)) return this.#each(keys, (key) => this.#delete(key, options), options?.signal);
|
|
2863
|
+
return this.#delete(keys, options);
|
|
2864
|
+
});
|
|
1983
2865
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2866
|
+
clear() {
|
|
2867
|
+
return this.#track(async () => {
|
|
2868
|
+
await this.#ready();
|
|
2869
|
+
await this.#driver.clear(this.#name);
|
|
2870
|
+
this.#emitter.emit("clear");
|
|
2871
|
+
});
|
|
1988
2872
|
}
|
|
1989
2873
|
query() {
|
|
1990
2874
|
return new Query(this);
|
|
1991
2875
|
}
|
|
1992
|
-
|
|
2876
|
+
cursor() {
|
|
2877
|
+
return this.#track(async () => {
|
|
2878
|
+
await this.#ready();
|
|
2879
|
+
let initializing = true;
|
|
2880
|
+
const cursor = new Cursor(await this.#driver.keys(this.#name), (key) => this.#readCursor(key), (key, changes) => this.#updateCursor(key, changes), (key) => this.#deleteCursor(key), (operation) => initializing ? operation() : this.#track(operation));
|
|
2881
|
+
await cursor.next();
|
|
2882
|
+
initializing = false;
|
|
2883
|
+
return cursor;
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
async *#scan(input, options) {
|
|
2887
|
+
checkAbort(options?.signal);
|
|
1993
2888
|
await this.#ready();
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
|
|
2889
|
+
const conditions = input?.conditions;
|
|
2890
|
+
const offset = input?.offset ?? 0;
|
|
2891
|
+
const limit = input?.limit;
|
|
2892
|
+
let matched = 0;
|
|
2893
|
+
let yielded = 0;
|
|
2894
|
+
const iterator = (this.#driver.stream === void 0 ? this.#driver.scan(this.#name) : this.#driver.stream(this.#name, conditions === void 0 ? {} : { conditions }))[Symbol.asyncIterator]();
|
|
2895
|
+
try {
|
|
2896
|
+
while (true) {
|
|
2897
|
+
await this.#ready();
|
|
2898
|
+
checkAbort(options?.signal);
|
|
2899
|
+
if (limit !== void 0 && yielded >= limit) return;
|
|
2900
|
+
const step = await iterator.next();
|
|
2901
|
+
await this.#ready();
|
|
2902
|
+
checkAbort(options?.signal);
|
|
2903
|
+
if (step.done === true) return;
|
|
2904
|
+
const row = step.value;
|
|
2905
|
+
if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
|
|
2906
|
+
const narrowed = this.#cast(row);
|
|
2907
|
+
if (narrowed === void 0) continue;
|
|
2908
|
+
if (matched < offset) {
|
|
2909
|
+
matched += 1;
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2912
|
+
matched += 1;
|
|
2913
|
+
yielded += 1;
|
|
2914
|
+
yield narrowed;
|
|
2915
|
+
}
|
|
2916
|
+
} finally {
|
|
2917
|
+
await iterator.return?.();
|
|
2918
|
+
}
|
|
1997
2919
|
}
|
|
1998
|
-
async #each(
|
|
2920
|
+
async #each(elements, operation, signal) {
|
|
1999
2921
|
const results = [];
|
|
2000
|
-
for (const
|
|
2922
|
+
for (const element of elements) {
|
|
2001
2923
|
checkAbort(signal);
|
|
2002
|
-
results.push(await operation(
|
|
2924
|
+
results.push(await operation(element));
|
|
2003
2925
|
}
|
|
2004
2926
|
return results;
|
|
2005
2927
|
}
|
|
2006
2928
|
async #read(key) {
|
|
2007
2929
|
return this.#cast(await this.#driver.read(this.#name, key));
|
|
2008
2930
|
}
|
|
2931
|
+
async #readCursor(key) {
|
|
2932
|
+
await this.#ready();
|
|
2933
|
+
return this.#read(key);
|
|
2934
|
+
}
|
|
2009
2935
|
async #resolveOne(key) {
|
|
2010
2936
|
const row = await this.#read(key);
|
|
2011
2937
|
if (row === void 0) throw new DatabaseError("NOT_FOUND", `No row '${key}' in table '${this.#name}'`, {
|
|
@@ -2014,29 +2940,79 @@ var Table = class {
|
|
|
2014
2940
|
});
|
|
2015
2941
|
return row;
|
|
2016
2942
|
}
|
|
2017
|
-
async #put(row,
|
|
2943
|
+
async #put(row, insert, options) {
|
|
2018
2944
|
const validated = this.#validate(this.#prepare(row));
|
|
2019
2945
|
const key = this.#resolveKey(validated);
|
|
2020
|
-
if (
|
|
2021
|
-
|
|
2022
|
-
key
|
|
2023
|
-
});
|
|
2024
|
-
await this.#driver.write(this.#name, key, validated);
|
|
2946
|
+
if (insert) await this.#driver.insert(this.#name, key, validated, options);
|
|
2947
|
+
else await this.#driver.write(this.#name, key, validated, options);
|
|
2025
2948
|
this.#emitter.emit("write", key);
|
|
2026
2949
|
return key;
|
|
2027
2950
|
}
|
|
2028
|
-
async #updateOne(key, changes) {
|
|
2951
|
+
async #updateOne(key, changes, options) {
|
|
2029
2952
|
const existing = await this.#driver.read(this.#name, key);
|
|
2030
|
-
if (existing === void 0)
|
|
2031
|
-
|
|
2953
|
+
if (existing === void 0) {
|
|
2954
|
+
checkAbort(options?.signal);
|
|
2955
|
+
return false;
|
|
2956
|
+
}
|
|
2957
|
+
const input = changes;
|
|
2958
|
+
if ((0, _orkestrel_contract.isRecord)(input) && Object.hasOwn(input, this.#key) && !equalsValue(input[this.#key], key)) throw new DatabaseError("VALIDATION", `Update cannot change primary column '${this.#key}' on table '${this.#name}'`, {
|
|
2959
|
+
table: this.#name,
|
|
2960
|
+
column: this.#key,
|
|
2961
|
+
key
|
|
2962
|
+
});
|
|
2963
|
+
await this.#driver.write(this.#name, key, this.#validate(Object.assign({}, existing, changes)), options);
|
|
2032
2964
|
this.#emitter.emit("write", key);
|
|
2033
2965
|
return true;
|
|
2034
2966
|
}
|
|
2035
|
-
async #
|
|
2036
|
-
|
|
2967
|
+
async #updateCursor(key, changes) {
|
|
2968
|
+
await this.#wait(void 0);
|
|
2969
|
+
return this.#updateOne(key, changes);
|
|
2970
|
+
}
|
|
2971
|
+
async #delete(key, options) {
|
|
2972
|
+
const removed = await this.#driver.delete(this.#name, key, options);
|
|
2037
2973
|
if (removed) this.#emitter.emit("remove", key);
|
|
2038
2974
|
return removed;
|
|
2039
2975
|
}
|
|
2976
|
+
async #deleteCursor(key) {
|
|
2977
|
+
await this.#wait(void 0);
|
|
2978
|
+
return this.#delete(key);
|
|
2979
|
+
}
|
|
2980
|
+
async #wait(signal) {
|
|
2981
|
+
checkAbort(signal);
|
|
2982
|
+
const ready = this.#ready();
|
|
2983
|
+
if (signal === void 0) {
|
|
2984
|
+
await ready;
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
const cleanup = new AbortController();
|
|
2988
|
+
try {
|
|
2989
|
+
await new Promise((resolve, reject) => {
|
|
2990
|
+
signal.addEventListener("abort", () => {
|
|
2991
|
+
ready.catch(() => {});
|
|
2992
|
+
try {
|
|
2993
|
+
checkAbort(signal);
|
|
2994
|
+
} catch (error) {
|
|
2995
|
+
reject(error);
|
|
2996
|
+
}
|
|
2997
|
+
}, {
|
|
2998
|
+
once: true,
|
|
2999
|
+
signal: cleanup.signal
|
|
3000
|
+
});
|
|
3001
|
+
ready.then(resolve, reject);
|
|
3002
|
+
if (signal.aborted) {
|
|
3003
|
+
ready.catch(() => {});
|
|
3004
|
+
try {
|
|
3005
|
+
checkAbort(signal);
|
|
3006
|
+
} catch (error) {
|
|
3007
|
+
reject(error);
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
} finally {
|
|
3012
|
+
cleanup.abort();
|
|
3013
|
+
}
|
|
3014
|
+
checkAbort(signal);
|
|
3015
|
+
}
|
|
2040
3016
|
async #collect() {
|
|
2041
3017
|
const rows = [];
|
|
2042
3018
|
for await (const row of this.#driver.scan(this.#name)) rows.push(row);
|
|
@@ -2045,21 +3021,38 @@ var Table = class {
|
|
|
2045
3021
|
#prepare(row) {
|
|
2046
3022
|
if (!(0, _orkestrel_contract.isRecord)(row)) throw new DatabaseError("VALIDATION", `Row for table '${this.#name}' is not a record`, { table: this.#name });
|
|
2047
3023
|
const prepared = { ...row };
|
|
2048
|
-
if (prepared[this.#key] === void 0) {
|
|
2049
|
-
|
|
3024
|
+
if (prepared[this.#key] === void 0) if (this.#generate !== void 0) try {
|
|
3025
|
+
prepared[this.#key] = this.#generate();
|
|
3026
|
+
} catch (cause) {
|
|
3027
|
+
throw new DatabaseError("VALIDATION", `Failed to generate primary column '${this.#key}' for table '${this.#name}'`, {
|
|
2050
3028
|
table: this.#name,
|
|
2051
|
-
column: this.#key
|
|
3029
|
+
column: this.#key,
|
|
3030
|
+
cause
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
else try {
|
|
3034
|
+
prepared[this.#key] = crypto.randomUUID();
|
|
3035
|
+
} catch (cause) {
|
|
3036
|
+
throw new DatabaseError("DRIVER", `Host failed to generate primary column '${this.#key}' for table '${this.#name}'`, {
|
|
3037
|
+
table: this.#name,
|
|
3038
|
+
column: this.#key,
|
|
3039
|
+
cause
|
|
2052
3040
|
});
|
|
2053
|
-
prepared[this.#key] = this.#generate();
|
|
2054
3041
|
}
|
|
2055
3042
|
return prepared;
|
|
2056
3043
|
}
|
|
2057
3044
|
#validate(row) {
|
|
2058
3045
|
const parsed = this.#contract.parse(row);
|
|
2059
|
-
if (parsed === void 0 || !(0, _orkestrel_contract.isRecord)(parsed))
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
3046
|
+
if (parsed === void 0 || !(0, _orkestrel_contract.isRecord)(parsed)) {
|
|
3047
|
+
const [fault] = this.#contract.explain(row);
|
|
3048
|
+
throw new DatabaseError("VALIDATION", `Row failed the '${this.#name}' contract`, {
|
|
3049
|
+
table: this.#name,
|
|
3050
|
+
...fault === void 0 ? {} : {
|
|
3051
|
+
field: fault.path,
|
|
3052
|
+
reason: fault.reason
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
2063
3056
|
return parsed;
|
|
2064
3057
|
}
|
|
2065
3058
|
#resolveKey(row) {
|
|
@@ -2073,80 +3066,99 @@ var Table = class {
|
|
|
2073
3066
|
#cast(row) {
|
|
2074
3067
|
return row !== void 0 && this.#guard(row) ? row : void 0;
|
|
2075
3068
|
}
|
|
3069
|
+
#track(operation) {
|
|
3070
|
+
if (this.#context !== void 0) return this.#context.track(operation);
|
|
3071
|
+
return this.#scope === void 0 ? operation() : this.#scope.track(operation);
|
|
3072
|
+
}
|
|
2076
3073
|
};
|
|
2077
3074
|
//#endregion
|
|
2078
|
-
//#region src/core/
|
|
3075
|
+
//#region src/core/DatabaseTransaction.ts
|
|
2079
3076
|
/**
|
|
2080
|
-
* A database
|
|
3077
|
+
* A table-only database view bound to one driver transaction scope.
|
|
3078
|
+
*
|
|
3079
|
+
* @typeParam T - The declared table shape map
|
|
2081
3080
|
*
|
|
2082
3081
|
* @remarks
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2085
|
-
*
|
|
2086
|
-
*
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
3082
|
+
* The view gives transaction work the same typed tables as its owning database
|
|
3083
|
+
* while enforcing a materially narrower contract and lifetime. `table` and
|
|
3084
|
+
* every table operation call the owning scope check, so a captured capability
|
|
3085
|
+
* cannot escape its transaction.
|
|
3086
|
+
*/
|
|
3087
|
+
var DatabaseTransaction = class {
|
|
3088
|
+
#driver;
|
|
3089
|
+
#tables;
|
|
3090
|
+
#primary;
|
|
3091
|
+
#generate;
|
|
3092
|
+
#error;
|
|
3093
|
+
#scope;
|
|
3094
|
+
constructor(driver, tables, primary, generate, error, scope) {
|
|
3095
|
+
this.#driver = driver;
|
|
3096
|
+
this.#tables = tables;
|
|
3097
|
+
this.#primary = primary;
|
|
3098
|
+
this.#generate = generate;
|
|
3099
|
+
this.#error = error;
|
|
3100
|
+
this.#scope = scope;
|
|
3101
|
+
}
|
|
3102
|
+
table(name) {
|
|
3103
|
+
this.#scope.check();
|
|
3104
|
+
const columns = this.#columns(name);
|
|
3105
|
+
return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
|
|
3106
|
+
}
|
|
3107
|
+
#build(name, key, contract) {
|
|
3108
|
+
return new Table(() => Promise.resolve(), this.#driver, name, key, contract, this.#generate, this.#error, void 0, this.#scope);
|
|
3109
|
+
}
|
|
3110
|
+
#key(name) {
|
|
3111
|
+
return this.#primary[name] ?? "id";
|
|
3112
|
+
}
|
|
3113
|
+
#columns(name) {
|
|
3114
|
+
const columns = this.#tables[name];
|
|
3115
|
+
if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
|
|
3116
|
+
return columns;
|
|
3117
|
+
}
|
|
3118
|
+
};
|
|
3119
|
+
//#endregion
|
|
3120
|
+
//#region src/core/Database.ts
|
|
3121
|
+
/**
|
|
3122
|
+
* A typed database view over one shared internal lifecycle and storage context.
|
|
2091
3123
|
*
|
|
2092
3124
|
* @remarks
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2095
|
-
*
|
|
2096
|
-
*
|
|
2097
|
-
* {@link DatabaseOptions.version} for the full reconciliation contract.
|
|
2098
|
-
* - **Observable (§13).** The owned {@link emitter} ({@link DatabaseEventMap}) carries the
|
|
2099
|
-
* connection + transaction lifecycle — `open` / `close` / `transaction` / `commit` /
|
|
2100
|
-
* `rollback` — for fire-and-forget observers, ALONGSIDE each table's per-row events. Every
|
|
2101
|
-
* event is emitted directly, strictly AFTER the relevant transition: `commit` only after
|
|
2102
|
-
* the scope succeeds, `rollback` only after every table is restored. The `rollback` emit
|
|
2103
|
-
* OBSERVES the propagated error — it never swallows it (the original throw propagates
|
|
2104
|
-
* exactly as before). The emitter isolates a listener throw and routes it to its `error`
|
|
2105
|
-
* handler (the `error` option), so observation can never reorder, throw into, or corrupt
|
|
2106
|
-
* the snapshot / commit / rollback flow.
|
|
3125
|
+
* Each view owns only its table contracts, primary columns, indexes, and key
|
|
3126
|
+
* generator. Imported views register their physical schemas with the same
|
|
3127
|
+
* internal context before opening begins, so every view observes one driver,
|
|
3128
|
+
* merged schema, emitter, status, transaction boundary, and terminal close.
|
|
2107
3129
|
*/
|
|
2108
3130
|
var Database = class Database {
|
|
2109
|
-
#
|
|
3131
|
+
#context;
|
|
2110
3132
|
#tables;
|
|
2111
|
-
#
|
|
3133
|
+
#primary;
|
|
2112
3134
|
#indexes;
|
|
2113
|
-
#name;
|
|
2114
3135
|
#generate;
|
|
2115
|
-
#version;
|
|
2116
|
-
#emitter;
|
|
2117
|
-
#status = "idle";
|
|
2118
|
-
#ready;
|
|
2119
3136
|
constructor(options) {
|
|
2120
|
-
this.#driver = options.driver;
|
|
2121
3137
|
this.#tables = options.tables;
|
|
2122
|
-
this.#
|
|
3138
|
+
this.#primary = options.primary ?? {};
|
|
2123
3139
|
this.#indexes = options.indexes ?? {};
|
|
2124
|
-
this.#
|
|
2125
|
-
this.#
|
|
2126
|
-
this.#
|
|
2127
|
-
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
2128
|
-
...options.on !== void 0 ? { on: options.on } : {},
|
|
2129
|
-
...options.error !== void 0 ? { error: options.error } : {}
|
|
2130
|
-
});
|
|
3140
|
+
this.#generate = options.generator;
|
|
3141
|
+
this.#context = new DatabaseContext(options);
|
|
3142
|
+
this.#context.register(this.#schema());
|
|
2131
3143
|
}
|
|
2132
3144
|
get emitter() {
|
|
2133
|
-
return this.#emitter;
|
|
3145
|
+
return this.#context.emitter;
|
|
2134
3146
|
}
|
|
2135
3147
|
get name() {
|
|
2136
|
-
return this.#name;
|
|
3148
|
+
return this.#context.name;
|
|
2137
3149
|
}
|
|
2138
3150
|
get status() {
|
|
2139
|
-
return this.#status;
|
|
3151
|
+
return this.#context.status;
|
|
2140
3152
|
}
|
|
2141
3153
|
table(name) {
|
|
2142
|
-
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
3154
|
+
if (this.#context.status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#context.name}' is closed`, { name: this.#context.name });
|
|
2143
3155
|
const columns = this.#columns(name);
|
|
2144
3156
|
return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
|
|
2145
3157
|
}
|
|
2146
|
-
import(tables,
|
|
3158
|
+
import(tables, primary) {
|
|
2147
3159
|
return this.#spawn(tables, {
|
|
2148
|
-
...this.#
|
|
2149
|
-
...
|
|
3160
|
+
...this.#primary,
|
|
3161
|
+
...primary
|
|
2150
3162
|
});
|
|
2151
3163
|
}
|
|
2152
3164
|
export() {
|
|
@@ -2154,109 +3166,44 @@ var Database = class Database {
|
|
|
2154
3166
|
for (const name of Object.keys(this.#tables)) {
|
|
2155
3167
|
const columns = this.#columns(name);
|
|
2156
3168
|
result[name] = {
|
|
2157
|
-
|
|
3169
|
+
primary: this.#key(name),
|
|
2158
3170
|
columns,
|
|
2159
3171
|
schema: (0, _orkestrel_contract.compileSchema)((0, _orkestrel_contract.objectShape)(columns))
|
|
2160
3172
|
};
|
|
2161
3173
|
}
|
|
2162
3174
|
return result;
|
|
2163
3175
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
3176
|
+
open() {
|
|
3177
|
+
return this.#context.open();
|
|
2166
3178
|
}
|
|
2167
|
-
|
|
2168
|
-
this.#
|
|
2169
|
-
this.#ready = void 0;
|
|
2170
|
-
await this.#driver.close();
|
|
2171
|
-
this.#emitter.emit("close");
|
|
3179
|
+
close() {
|
|
3180
|
+
return this.#context.close();
|
|
2172
3181
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
* When the driver implements the optional native {@link DriverInterface.transaction}
|
|
2179
|
-
* hook, that native `commit` / `rollback` handle drives the transaction; otherwise
|
|
2180
|
-
* the universal snapshot floor (`driver.snapshot()`) runs unchanged. Either path
|
|
2181
|
-
* emits the same `transaction` / `commit` / `rollback` lifecycle (AGENTS §13).
|
|
2182
|
-
* `options.signal` is checked ONCE at entry, before connecting or starting any
|
|
2183
|
-
* transactional work — an already-aborted signal throws `ABORTED` and neither the
|
|
2184
|
-
* native hook nor the snapshot floor is invoked. Nesting is unguarded and
|
|
2185
|
-
* unsupported exactly as before: this is a single-writer model, not reentrant.
|
|
2186
|
-
* On the native path, a `scope` throw rolls back via the native handle; a
|
|
2187
|
-
* native `commit` failure propagates as-is with no rollback attempt — the
|
|
2188
|
-
* engine owns transaction state after a failed COMMIT.
|
|
2189
|
-
*
|
|
2190
|
-
* @param scope - The transactional work to run
|
|
2191
|
-
* @param options - `{ signal }` to abort before the transaction starts
|
|
2192
|
-
* @returns The scope's resolved value
|
|
2193
|
-
* @throws An `ABORTED` {@link DatabaseError} when `options.signal` has already fired
|
|
2194
|
-
*/
|
|
2195
|
-
async transaction(scope, options) {
|
|
2196
|
-
checkAbort(options?.signal);
|
|
2197
|
-
await this.#connect();
|
|
2198
|
-
const native = await this.#driver.transaction?.();
|
|
2199
|
-
if (native !== void 0) {
|
|
2200
|
-
this.#emitter.emit("transaction");
|
|
2201
|
-
let value;
|
|
2202
|
-
try {
|
|
2203
|
-
value = await scope();
|
|
2204
|
-
} catch (error) {
|
|
2205
|
-
await native.rollback();
|
|
2206
|
-
this.#emitter.emit("rollback", error);
|
|
2207
|
-
throw error;
|
|
2208
|
-
}
|
|
2209
|
-
await native.commit();
|
|
2210
|
-
this.#emitter.emit("commit");
|
|
2211
|
-
return value;
|
|
2212
|
-
}
|
|
2213
|
-
const rollback = await this.#driver.snapshot();
|
|
2214
|
-
this.#emitter.emit("transaction");
|
|
2215
|
-
try {
|
|
2216
|
-
const value = await scope();
|
|
2217
|
-
this.#emitter.emit("commit");
|
|
2218
|
-
return value;
|
|
2219
|
-
} catch (error) {
|
|
2220
|
-
await rollback();
|
|
2221
|
-
this.#emitter.emit("rollback", error);
|
|
2222
|
-
throw error;
|
|
2223
|
-
}
|
|
3182
|
+
transaction(scope, options) {
|
|
3183
|
+
return this.#context.transaction(async (storage, lifetime) => {
|
|
3184
|
+
const transaction = new DatabaseTransaction(storage, this.#tables, this.#primary, this.#generate, this.#context.error, lifetime);
|
|
3185
|
+
return this.#settle(scope, transaction, lifetime);
|
|
3186
|
+
}, options);
|
|
2224
3187
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
* resulting plan through the driver's optional `migrate` hook.
|
|
2228
|
-
*
|
|
2229
|
-
* @param deployed - The schema currently deployed, as {@link TableSchema}s
|
|
2230
|
-
* @param options - `{ signal }` to abort before the migration starts
|
|
2231
|
-
* @returns The applied {@link Migration} plan
|
|
2232
|
-
* @throws A `MIGRATION` {@link DatabaseError} when the driver does not
|
|
2233
|
-
* implement `migrate`, or when a step references an unknown table
|
|
2234
|
-
* (propagated from the driver)
|
|
2235
|
-
* @throws An `ABORTED` {@link DatabaseError} when `options.signal` has
|
|
2236
|
-
* already fired at entry
|
|
2237
|
-
*/
|
|
2238
|
-
async migrate(deployed, options) {
|
|
2239
|
-
checkAbort(options?.signal);
|
|
2240
|
-
await this.#connect();
|
|
2241
|
-
const plan = planMigration(deployed, this.#schema());
|
|
2242
|
-
if (this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, { name: this.#name });
|
|
2243
|
-
await this.#apply(plan);
|
|
2244
|
-
return plan;
|
|
3188
|
+
migrate(deployed, options) {
|
|
3189
|
+
return this.#context.migrate(deployed, options);
|
|
2245
3190
|
}
|
|
2246
3191
|
#build(name, key, contract) {
|
|
2247
|
-
return new Table(() => this.#connect(), this.#driver, name, key, contract, this.#generate);
|
|
3192
|
+
return new Table(() => this.#context.connect(), this.#context.driver, name, key, contract, this.#generate, this.#context.error, this.#context);
|
|
2248
3193
|
}
|
|
2249
|
-
#spawn(tables,
|
|
2250
|
-
return
|
|
2251
|
-
driver: this.#driver,
|
|
3194
|
+
#spawn(tables, primary) {
|
|
3195
|
+
return Database.#attach({
|
|
3196
|
+
driver: this.#context.driver,
|
|
2252
3197
|
tables,
|
|
2253
|
-
|
|
2254
|
-
name: this.#name,
|
|
2255
|
-
...this.#
|
|
2256
|
-
|
|
3198
|
+
primary,
|
|
3199
|
+
name: this.#context.name,
|
|
3200
|
+
...this.#context.error === void 0 ? {} : { error: this.#context.error },
|
|
3201
|
+
...this.#generate === void 0 ? {} : { generator: this.#generate },
|
|
3202
|
+
...this.#context.version === void 0 ? {} : { version: this.#context.version }
|
|
3203
|
+
}, this.#context);
|
|
2257
3204
|
}
|
|
2258
3205
|
#key(name) {
|
|
2259
|
-
return this.#
|
|
3206
|
+
return this.#primary[name] ?? "id";
|
|
2260
3207
|
}
|
|
2261
3208
|
#columns(name) {
|
|
2262
3209
|
const columns = this.#tables[name];
|
|
@@ -2269,74 +3216,36 @@ var Database = class Database {
|
|
|
2269
3216
|
return {
|
|
2270
3217
|
name,
|
|
2271
3218
|
primary: this.#key(name),
|
|
2272
|
-
columns: Object.entries(columns).map(([column, shape]) =>
|
|
2273
|
-
return {
|
|
2274
|
-
name: column,
|
|
2275
|
-
type: shapeToColumnType(shape),
|
|
2276
|
-
nullable: shape.type === "optional" || shape.type === "nullable"
|
|
2277
|
-
};
|
|
2278
|
-
}),
|
|
3219
|
+
columns: Object.entries(columns).map(([column, shape]) => shapeToColumnSchema(column, shape)),
|
|
2279
3220
|
indexes: this.#indexes[name] ?? []
|
|
2280
3221
|
};
|
|
2281
3222
|
});
|
|
2282
3223
|
}
|
|
2283
|
-
#
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
name: this.#name,
|
|
2309
|
-
stored: meta.version,
|
|
2310
|
-
declared: this.#version
|
|
2311
|
-
});
|
|
2312
|
-
await this.#apply(plan);
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
async #apply(plan) {
|
|
2316
|
-
const native = await this.#driver.transaction?.();
|
|
2317
|
-
if (native !== void 0) {
|
|
2318
|
-
try {
|
|
2319
|
-
await this.#driver.migrate?.(plan);
|
|
2320
|
-
await this.#stamp();
|
|
2321
|
-
} catch (error) {
|
|
2322
|
-
await native.rollback();
|
|
2323
|
-
throw error;
|
|
2324
|
-
}
|
|
2325
|
-
await native.commit();
|
|
2326
|
-
this.#emitter.emit("migrate", plan);
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
await this.#driver.migrate?.(plan);
|
|
2330
|
-
await this.#stamp();
|
|
2331
|
-
this.#emitter.emit("migrate", plan);
|
|
2332
|
-
}
|
|
2333
|
-
async #stamp() {
|
|
2334
|
-
if (this.#version === void 0 || this.#driver.stamp === void 0) return;
|
|
2335
|
-
const meta = {
|
|
2336
|
-
version: this.#version,
|
|
2337
|
-
schema: this.#schema()
|
|
2338
|
-
};
|
|
2339
|
-
await this.#driver.stamp(meta);
|
|
3224
|
+
async #settle(scope, transaction, lifetime) {
|
|
3225
|
+
const outcome = await Promise.resolve().then(() => scope(transaction)).then((value) => ({
|
|
3226
|
+
success: true,
|
|
3227
|
+
value
|
|
3228
|
+
}), (error) => ({
|
|
3229
|
+
success: false,
|
|
3230
|
+
error
|
|
3231
|
+
}));
|
|
3232
|
+
lifetime.stop();
|
|
3233
|
+
const drained = await lifetime.drain().then(() => ({
|
|
3234
|
+
success: true,
|
|
3235
|
+
value: void 0
|
|
3236
|
+
}), (error) => ({
|
|
3237
|
+
success: false,
|
|
3238
|
+
error
|
|
3239
|
+
}));
|
|
3240
|
+
if (!outcome.success) return outcome;
|
|
3241
|
+
if (!drained.success) return drained;
|
|
3242
|
+
return outcome;
|
|
3243
|
+
}
|
|
3244
|
+
static #attach(options, context) {
|
|
3245
|
+
const database = new Database(options);
|
|
3246
|
+
database.#context = context;
|
|
3247
|
+
context.register(database.#schema());
|
|
3248
|
+
return database;
|
|
2340
3249
|
}
|
|
2341
3250
|
};
|
|
2342
3251
|
//#endregion
|
|
@@ -2352,29 +3261,54 @@ var Database = class Database {
|
|
|
2352
3261
|
* snapshot capture and restore — so a caller mutating a nested field of an input
|
|
2353
3262
|
* row, a returned row, or a row mutated in place between snapshot and rollback
|
|
2354
3263
|
* can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
|
|
2355
|
-
* would still share nested object/array references.
|
|
3264
|
+
* would still share nested object/array references. Metadata instead routes
|
|
3265
|
+
* through `cloneDriverMetadata`: `stamp` and migration snapshot exact JSON at
|
|
3266
|
+
* ingress, and `metadata` returns a distinct deeply frozen owned copy. `snapshot`
|
|
2356
3267
|
* clones every table to give transactions an exact rollback point. `scan` and
|
|
2357
3268
|
* `keys` yield in key order — sorted by the core {@link compareValues} total
|
|
2358
3269
|
* order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
|
|
2359
3270
|
* reads) backends honor, so an unordered read agrees across every backend rather
|
|
2360
3271
|
* than leaking Map insertion order. A persistent backend (IndexedDB, SQLite)
|
|
2361
|
-
* implements the same
|
|
3272
|
+
* implements the same required methods over real storage.
|
|
2362
3273
|
*/
|
|
2363
3274
|
var MemoryDriver = class {
|
|
2364
3275
|
#tables = /* @__PURE__ */ new Map();
|
|
2365
|
-
#
|
|
3276
|
+
#identities = /* @__PURE__ */ new Map();
|
|
3277
|
+
#schema = [];
|
|
3278
|
+
#metadata;
|
|
2366
3279
|
async open(schema) {
|
|
2367
|
-
|
|
3280
|
+
const owned = normalizeDriverSchema(schema);
|
|
3281
|
+
const deployed = normalizeDriverSchema(this.#metadata?.schema ?? owned);
|
|
3282
|
+
const names = new Set(deployed.map((table) => table.name));
|
|
3283
|
+
for (const name of this.#identities.keys()) if (!names.has(name)) this.#identities.delete(name);
|
|
3284
|
+
for (const table of deployed) {
|
|
3285
|
+
if (!this.#tables.has(table.name)) this.#tables.set(table.name, /* @__PURE__ */ new Map());
|
|
3286
|
+
if (!this.#identities.has(table.name)) this.#identities.set(table.name, {});
|
|
3287
|
+
}
|
|
3288
|
+
this.#schema = deployed;
|
|
2368
3289
|
}
|
|
2369
3290
|
async close() {}
|
|
2370
3291
|
async read(table, key) {
|
|
2371
3292
|
const row = this.#store(table).get(key);
|
|
2372
3293
|
return row === void 0 ? void 0 : structuredClone(row);
|
|
2373
3294
|
}
|
|
2374
|
-
async write(table, key, row) {
|
|
2375
|
-
|
|
3295
|
+
async write(table, key, row, options) {
|
|
3296
|
+
checkAbort(options?.signal);
|
|
3297
|
+
const primary = this.#table(table).primary;
|
|
3298
|
+
this.#store(table).set(key, structuredClone(bindRowKey(row, primary, key)));
|
|
3299
|
+
}
|
|
3300
|
+
async insert(table, key, row, options) {
|
|
3301
|
+
checkAbort(options?.signal);
|
|
3302
|
+
const store = this.#store(table);
|
|
3303
|
+
if (store.has(key)) throw new DatabaseError("CONFLICT", `Row '${key}' already exists in table '${table}'`, {
|
|
3304
|
+
table,
|
|
3305
|
+
key
|
|
3306
|
+
});
|
|
3307
|
+
const primary = this.#table(table).primary;
|
|
3308
|
+
store.set(key, structuredClone(bindRowKey(row, primary, key)));
|
|
2376
3309
|
}
|
|
2377
|
-
async delete(table, key) {
|
|
3310
|
+
async delete(table, key, options) {
|
|
3311
|
+
checkAbort(options?.signal);
|
|
2378
3312
|
return this.#store(table).delete(key);
|
|
2379
3313
|
}
|
|
2380
3314
|
async keys(table) {
|
|
@@ -2393,17 +3327,17 @@ var MemoryDriver = class {
|
|
|
2393
3327
|
* @remarks
|
|
2394
3328
|
* Iterates the table's keys in the same key order `scan` and `keys` yield
|
|
2395
3329
|
* (sorted by {@link compareValues}), testing each row against
|
|
2396
|
-
* `
|
|
3330
|
+
* `input.conditions` (via {@link matchesQuery}) before counting it
|
|
2397
3331
|
* toward `offset` / `limit`. Both are applied lazily as matches are found —
|
|
2398
3332
|
* `offset` matches are skipped without being yielded, and iteration stops the
|
|
2399
3333
|
* instant `limit` yields have been produced, so a large table is never fully
|
|
2400
|
-
* walked for a small page. `
|
|
3334
|
+
* walked for a small page. `input.order` is IGNORED (the same contract as
|
|
2401
3335
|
* `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
|
|
2402
3336
|
* order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
|
|
2403
3337
|
* §11), and an unknown table mirrors `scan`'s empty-yield behavior.
|
|
2404
3338
|
*
|
|
2405
3339
|
* @param table - The table to stream
|
|
2406
|
-
* @param
|
|
3340
|
+
* @param input - The filter / offset / limit to apply lazily
|
|
2407
3341
|
*
|
|
2408
3342
|
* @example
|
|
2409
3343
|
* ```ts
|
|
@@ -2412,18 +3346,22 @@ var MemoryDriver = class {
|
|
|
2412
3346
|
* }
|
|
2413
3347
|
* ```
|
|
2414
3348
|
*/
|
|
2415
|
-
|
|
3349
|
+
stream(table, input) {
|
|
3350
|
+
validatePage(input);
|
|
3351
|
+
return this.#stream(table, input);
|
|
3352
|
+
}
|
|
3353
|
+
async *#stream(table, input) {
|
|
2416
3354
|
const store = this.#store(table);
|
|
2417
|
-
const conditions =
|
|
2418
|
-
const offset =
|
|
2419
|
-
const limit =
|
|
3355
|
+
const conditions = input.conditions;
|
|
3356
|
+
const offset = input.offset ?? 0;
|
|
3357
|
+
const limit = input.limit;
|
|
2420
3358
|
let skipped = 0;
|
|
2421
3359
|
let yielded = 0;
|
|
2422
3360
|
for (const key of this.#ordered(table)) {
|
|
2423
3361
|
if (limit !== void 0 && yielded >= limit) return;
|
|
2424
3362
|
const row = store.get(key);
|
|
2425
3363
|
if (row === void 0) continue;
|
|
2426
|
-
if (conditions !== void 0 && conditions.length > 0 && !
|
|
3364
|
+
if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
|
|
2427
3365
|
if (skipped < offset) {
|
|
2428
3366
|
skipped += 1;
|
|
2429
3367
|
continue;
|
|
@@ -2439,90 +3377,149 @@ var MemoryDriver = class {
|
|
|
2439
3377
|
* Capture the current state and return a thunk that rolls back to it.
|
|
2440
3378
|
*
|
|
2441
3379
|
* @remarks
|
|
2442
|
-
*
|
|
2443
|
-
*
|
|
2444
|
-
*
|
|
2445
|
-
*
|
|
3380
|
+
* Capture owns rows, schema, and one session-local table identity. Replay
|
|
3381
|
+
* adapts rows to each surviving same-identity table's current schema before
|
|
3382
|
+
* changing storage. Removed or replaced tables are skipped; uncaptured and
|
|
3383
|
+
* later-added tables retain their current rows. Schema and metadata are never
|
|
3384
|
+
* restored.
|
|
2446
3385
|
*
|
|
2447
3386
|
* @param tables - The table names to scope the snapshot to; omitted captures every table
|
|
2448
3387
|
* @returns A thunk that restores the captured tables
|
|
2449
3388
|
*/
|
|
2450
3389
|
async snapshot(tables) {
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
for (const [key, row] of store) cloned.set(key, structuredClone(row));
|
|
2456
|
-
copy.set(name, cloned);
|
|
2457
|
-
}
|
|
2458
|
-
return async () => {
|
|
2459
|
-
this.#tables.clear();
|
|
2460
|
-
for (const [name, store] of copy) {
|
|
2461
|
-
const restored = /* @__PURE__ */ new Map();
|
|
2462
|
-
for (const [key, row] of store) restored.set(key, structuredClone(row));
|
|
2463
|
-
this.#tables.set(name, restored);
|
|
2464
|
-
}
|
|
2465
|
-
};
|
|
2466
|
-
}
|
|
2467
|
-
const copy = /* @__PURE__ */ new Map();
|
|
2468
|
-
for (const name of tables) {
|
|
3390
|
+
const names = tables === void 0 ? this.#schema.map((table) => table.name) : [...new Set(tables)].filter((name) => this.#schema.some((table) => table.name === name));
|
|
3391
|
+
const captured = /* @__PURE__ */ new Map();
|
|
3392
|
+
for (const name of names) {
|
|
3393
|
+
const schema = this.#schema.find((table) => table.name === name);
|
|
2469
3394
|
const store = this.#tables.get(name);
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
3395
|
+
const identity = this.#identities.get(name);
|
|
3396
|
+
if (schema === void 0 || store === void 0 || identity === void 0) continue;
|
|
3397
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3398
|
+
for (const [key, row] of store) rows.set(key, structuredClone(row));
|
|
3399
|
+
captured.set(name, {
|
|
3400
|
+
identity,
|
|
3401
|
+
rows,
|
|
3402
|
+
schema
|
|
3403
|
+
});
|
|
2474
3404
|
}
|
|
2475
3405
|
return async () => {
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
this.#tables.
|
|
3406
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
3407
|
+
for (const [name, capture] of captured) {
|
|
3408
|
+
const schema = this.#schema.find((table) => table.name === name);
|
|
3409
|
+
const store = this.#tables.get(name);
|
|
3410
|
+
if (schema === void 0 || store === void 0 || this.#identities.get(name) !== capture.identity) continue;
|
|
3411
|
+
const plan = planMigration([capture.schema], [schema]);
|
|
3412
|
+
const entries = [...capture.rows.entries()];
|
|
3413
|
+
const migrated = migrateRows(entries.map(([, row]) => row), plan.steps);
|
|
3414
|
+
if (migrated.length !== entries.length) throw new DatabaseError("MIGRATION", "Snapshot row count changed during migration", { table: name });
|
|
3415
|
+
const rows = /* @__PURE__ */ new Map();
|
|
3416
|
+
for (const [index, [key]] of entries.entries()) {
|
|
3417
|
+
const row = migrated[index];
|
|
3418
|
+
if (!isKey(key) || row === void 0) throw new DatabaseError("MIGRATION", "Snapshot row has no usable primary key", {
|
|
3419
|
+
table: name,
|
|
3420
|
+
column: schema.primary,
|
|
3421
|
+
index
|
|
3422
|
+
});
|
|
3423
|
+
rows.set(key, structuredClone(bindRowKey(row, schema.primary, key)));
|
|
3424
|
+
}
|
|
3425
|
+
replacements.set(store, rows);
|
|
3426
|
+
}
|
|
3427
|
+
for (const [store, rows] of replacements) {
|
|
3428
|
+
store.clear();
|
|
3429
|
+
for (const [key, row] of rows) store.set(key, row);
|
|
2480
3430
|
}
|
|
2481
3431
|
};
|
|
2482
3432
|
}
|
|
2483
3433
|
/**
|
|
2484
|
-
* Return the persisted {@link
|
|
3434
|
+
* Return the persisted {@link DriverMetadata}, or `undefined` when the store has
|
|
2485
3435
|
* never been stamped.
|
|
2486
3436
|
*
|
|
2487
3437
|
* @remarks
|
|
2488
3438
|
* In-process only — the metadata lives in this instance's memory, exactly
|
|
2489
|
-
* like the rest of this driver's storage.
|
|
2490
|
-
*
|
|
3439
|
+
* like the rest of this driver's storage. The returned value is a distinct
|
|
3440
|
+
* deeply frozen owned snapshot. A driver-conformance-valid implementation of
|
|
3441
|
+
* the optional `metadata` / `stamp` pair.
|
|
2491
3442
|
*
|
|
2492
|
-
* @returns The last-stamped {@link
|
|
3443
|
+
* @returns The last-stamped {@link DriverMetadata}, or `undefined`
|
|
2493
3444
|
*/
|
|
2494
|
-
async
|
|
2495
|
-
return this.#
|
|
3445
|
+
async metadata() {
|
|
3446
|
+
return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
|
|
2496
3447
|
}
|
|
2497
3448
|
/**
|
|
2498
|
-
* Persist
|
|
3449
|
+
* Persist an owned snapshot for a later `metadata()` to return.
|
|
2499
3450
|
*
|
|
2500
|
-
* @param
|
|
3451
|
+
* @param metadata - The {@link DriverMetadata} to persist
|
|
2501
3452
|
*/
|
|
2502
|
-
async stamp(
|
|
2503
|
-
this.#
|
|
3453
|
+
async stamp(metadata) {
|
|
3454
|
+
this.#metadata = cloneDriverMetadata(metadata);
|
|
2504
3455
|
}
|
|
2505
3456
|
/**
|
|
2506
3457
|
* Apply a {@link Migration} plan's steps against the in-memory store.
|
|
2507
3458
|
*
|
|
2508
3459
|
* @remarks
|
|
2509
|
-
*
|
|
2510
|
-
*
|
|
3460
|
+
* Steps apply against an isolated candidate. Rows, schema changes, and
|
|
3461
|
+
* optional metadata publish together only after the whole request succeeds.
|
|
2511
3462
|
*
|
|
2512
|
-
* @param
|
|
3463
|
+
* @param input - The migration plan and optional metadata to settle atomically
|
|
2513
3464
|
*/
|
|
2514
|
-
async migrate(
|
|
2515
|
-
|
|
3465
|
+
async migrate(input) {
|
|
3466
|
+
const owned = cloneMigrationInput(input);
|
|
3467
|
+
const schema = projectMigrationSchema(this.#schema, owned.plan.steps);
|
|
3468
|
+
if (owned.metadata !== void 0 && !equalsValue(normalizeDriverSchema(owned.metadata.schema), schema)) throw new DatabaseError("MIGRATION", "Migration metadata schema does not match the plan", {
|
|
3469
|
+
projected: schema,
|
|
3470
|
+
metadata: owned.metadata.schema
|
|
3471
|
+
});
|
|
3472
|
+
const candidate = this.#copy(this.#tables);
|
|
3473
|
+
const identities = this.#projectIdentities(this.#identities, owned.plan.steps);
|
|
3474
|
+
for (const step of owned.plan.steps) this.#migrate(candidate, step);
|
|
3475
|
+
this.#tables.clear();
|
|
3476
|
+
for (const [name, store] of candidate) this.#tables.set(name, store);
|
|
3477
|
+
this.#identities = identities;
|
|
3478
|
+
this.#schema = schema;
|
|
3479
|
+
if (owned.metadata !== void 0) this.#metadata = owned.metadata;
|
|
3480
|
+
}
|
|
3481
|
+
#ordered(table) {
|
|
3482
|
+
return [...this.#store(table).keys()].sort(compareValues);
|
|
3483
|
+
}
|
|
3484
|
+
#require(tables, table) {
|
|
3485
|
+
const store = tables.get(table);
|
|
3486
|
+
if (store === void 0) throw new DatabaseError("MIGRATION", `migrate: unknown table '${table}'`, { table });
|
|
3487
|
+
return store;
|
|
3488
|
+
}
|
|
3489
|
+
#copy(tables) {
|
|
3490
|
+
const copy = /* @__PURE__ */ new Map();
|
|
3491
|
+
for (const [name, store] of tables) {
|
|
3492
|
+
const cloned = /* @__PURE__ */ new Map();
|
|
3493
|
+
for (const [key, row] of store) cloned.set(key, structuredClone(row));
|
|
3494
|
+
copy.set(name, cloned);
|
|
3495
|
+
}
|
|
3496
|
+
return copy;
|
|
3497
|
+
}
|
|
3498
|
+
#projectIdentities(identities, steps) {
|
|
3499
|
+
const projected = new Map(identities);
|
|
3500
|
+
for (const step of steps) {
|
|
3501
|
+
if (step.operation === "table.add") projected.set(step.table.name, {});
|
|
3502
|
+
if (step.operation === "table.remove") projected.delete(step.table);
|
|
3503
|
+
}
|
|
3504
|
+
return projected;
|
|
3505
|
+
}
|
|
3506
|
+
#table(name) {
|
|
3507
|
+
const schema = this.#schema.find((table) => table.name === name);
|
|
3508
|
+
if (schema === void 0) throw new DatabaseError("NOT_FOUND", `Unknown table '${name}'`, { table: name });
|
|
3509
|
+
return schema;
|
|
3510
|
+
}
|
|
3511
|
+
#migrate(tables, step) {
|
|
3512
|
+
switch (step.operation) {
|
|
2516
3513
|
case "table.add":
|
|
2517
|
-
if (!
|
|
3514
|
+
if (!tables.has(step.table.name)) tables.set(step.table.name, /* @__PURE__ */ new Map());
|
|
2518
3515
|
break;
|
|
2519
3516
|
case "table.remove":
|
|
2520
|
-
this.#require(step.table);
|
|
2521
|
-
|
|
3517
|
+
this.#require(tables, step.table);
|
|
3518
|
+
tables.delete(step.table);
|
|
2522
3519
|
break;
|
|
2523
3520
|
case "column.add":
|
|
2524
3521
|
case "column.remove": {
|
|
2525
|
-
const store = this.#require(step.table);
|
|
3522
|
+
const store = this.#require(tables, step.table);
|
|
2526
3523
|
const rows = [...store.entries()];
|
|
2527
3524
|
const migrated = migrateRows(rows.map(([, row]) => row), [step]);
|
|
2528
3525
|
for (const [index, [key]] of rows.entries()) {
|
|
@@ -2536,25 +3533,13 @@ var MemoryDriver = class {
|
|
|
2536
3533
|
break;
|
|
2537
3534
|
}
|
|
2538
3535
|
case "index.add":
|
|
2539
|
-
case "index.remove":
|
|
2540
|
-
this.#require(step.table);
|
|
2541
|
-
break;
|
|
3536
|
+
case "index.remove": this.#require(tables, step.table);
|
|
2542
3537
|
}
|
|
2543
3538
|
}
|
|
2544
|
-
#ordered(table) {
|
|
2545
|
-
return [...this.#store(table).keys()].sort(compareValues);
|
|
2546
|
-
}
|
|
2547
|
-
#require(table) {
|
|
2548
|
-
const store = this.#tables.get(table);
|
|
2549
|
-
if (store === void 0) throw new DatabaseError("MIGRATION", `migrate: unknown table '${table}'`, { table });
|
|
2550
|
-
return store;
|
|
2551
|
-
}
|
|
2552
3539
|
#store(table) {
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
this.#tables.set(table, store);
|
|
2557
|
-
}
|
|
3540
|
+
this.#table(table);
|
|
3541
|
+
const store = this.#tables.get(table);
|
|
3542
|
+
if (store === void 0) throw new DatabaseError("NOT_FOUND", `Table '${table}' has no backing store`, { table });
|
|
2558
3543
|
return store;
|
|
2559
3544
|
}
|
|
2560
3545
|
};
|
|
@@ -2569,10 +3554,10 @@ var MemoryDriver = class {
|
|
|
2569
3554
|
* level. The `const` type parameter captures the literal names and columns, so
|
|
2570
3555
|
* `db.table('users')` is checked against the schema and typed by `Infer` of its
|
|
2571
3556
|
* columns — no annotations. Name a non-`id` primary-key column per table via the
|
|
2572
|
-
* optional `
|
|
3557
|
+
* optional `primary` and `indexes` maps.
|
|
2573
3558
|
*
|
|
2574
|
-
* @param options - The driver,
|
|
2575
|
-
*
|
|
3559
|
+
* @param options - The driver, `tables`, and optional `primary`, `indexes`,
|
|
3560
|
+
* `name`, `generator`, `version`, and emitter hooks
|
|
2576
3561
|
* @returns A typed {@link DatabaseInterface}
|
|
2577
3562
|
*
|
|
2578
3563
|
* @example
|
|
@@ -2586,7 +3571,7 @@ var MemoryDriver = class {
|
|
|
2586
3571
|
* users: { id: stringShape(), age: integerShape() },
|
|
2587
3572
|
* posts: { slug: stringShape(), title: stringShape() },
|
|
2588
3573
|
* },
|
|
2589
|
-
*
|
|
3574
|
+
* primary: { posts: 'slug' },
|
|
2590
3575
|
* })
|
|
2591
3576
|
* await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
|
|
2592
3577
|
* ```
|
|
@@ -2607,40 +3592,48 @@ function createMemoryDriver() {
|
|
|
2607
3592
|
return new MemoryDriver();
|
|
2608
3593
|
}
|
|
2609
3594
|
//#endregion
|
|
2610
|
-
exports.Clause = Clause;
|
|
2611
|
-
exports.Cursor = Cursor;
|
|
2612
3595
|
exports.DEFAULT_PRIMARY = DEFAULT_PRIMARY;
|
|
2613
3596
|
exports.Database = Database;
|
|
2614
3597
|
exports.DatabaseError = DatabaseError;
|
|
2615
3598
|
exports.MAX_PATTERN_LENGTH = MAX_PATTERN_LENGTH;
|
|
2616
3599
|
exports.MemoryDriver = MemoryDriver;
|
|
2617
|
-
exports.
|
|
2618
|
-
exports.Table = Table;
|
|
2619
|
-
exports.UUID_BYTE_COUNT = UUID_BYTE_COUNT;
|
|
2620
|
-
exports.UUID_BYTE_RANGE = UUID_BYTE_RANGE;
|
|
2621
|
-
exports.applyCriteria = applyCriteria;
|
|
3600
|
+
exports.applyQuery = applyQuery;
|
|
2622
3601
|
exports.auditDriver = auditDriver;
|
|
3602
|
+
exports.bindRowKey = bindRowKey;
|
|
2623
3603
|
exports.checkAbort = checkAbort;
|
|
3604
|
+
exports.cloneDriverMetadata = cloneDriverMetadata;
|
|
3605
|
+
exports.cloneDriverSchema = cloneDriverSchema;
|
|
3606
|
+
exports.cloneMigrationInput = cloneMigrationInput;
|
|
2624
3607
|
exports.compareValues = compareValues;
|
|
2625
3608
|
exports.computeAggregate = computeAggregate;
|
|
2626
3609
|
exports.conformDriver = conformDriver;
|
|
2627
3610
|
exports.createDatabase = createDatabase;
|
|
2628
3611
|
exports.createMemoryDriver = createMemoryDriver;
|
|
2629
|
-
exports.deepEqual = deepEqual;
|
|
2630
3612
|
exports.driverFindings = driverFindings;
|
|
3613
|
+
exports.equalsValue = equalsValue;
|
|
2631
3614
|
exports.extractKey = extractKey;
|
|
2632
3615
|
exports.filterRows = filterRows;
|
|
2633
|
-
exports.
|
|
2634
|
-
exports.globMatch = globMatch;
|
|
3616
|
+
exports.isColumnSchema = isColumnSchema;
|
|
2635
3617
|
exports.isDatabaseError = isDatabaseError;
|
|
2636
|
-
exports.
|
|
2637
|
-
exports.
|
|
3618
|
+
exports.isDriverMetadata = isDriverMetadata;
|
|
3619
|
+
exports.isDriverSchema = isDriverSchema;
|
|
3620
|
+
exports.isKey = isKey;
|
|
3621
|
+
exports.isMigration = isMigration;
|
|
3622
|
+
exports.isMigrationInput = isMigrationInput;
|
|
3623
|
+
exports.isMigrationStep = isMigrationStep;
|
|
3624
|
+
exports.isTableSchema = isTableSchema;
|
|
2638
3625
|
exports.matchesCondition = matchesCondition;
|
|
2639
|
-
exports.
|
|
3626
|
+
exports.matchesGlobPattern = matchesGlobPattern;
|
|
3627
|
+
exports.matchesLikePattern = matchesLikePattern;
|
|
3628
|
+
exports.matchesQuery = matchesQuery;
|
|
3629
|
+
exports.matchesWildcardPattern = matchesWildcardPattern;
|
|
2640
3630
|
exports.migrateRows = migrateRows;
|
|
3631
|
+
exports.normalizeDriverSchema = normalizeDriverSchema;
|
|
2641
3632
|
exports.planMigration = planMigration;
|
|
2642
|
-
exports.
|
|
3633
|
+
exports.projectMigrationSchema = projectMigrationSchema;
|
|
3634
|
+
exports.shapeToColumnSchema = shapeToColumnSchema;
|
|
3635
|
+
exports.shapeToColumnStorage = shapeToColumnStorage;
|
|
2643
3636
|
exports.sortRows = sortRows;
|
|
2644
|
-
exports.
|
|
3637
|
+
exports.validatePage = validatePage;
|
|
2645
3638
|
|
|
2646
3639
|
//# sourceMappingURL=index.cjs.map
|