@orkestrel/database 0.0.5 → 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 +100 -87
- package/dist/src/browser/index.js +369 -219
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +2389 -1244
- 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 +2368 -1231
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1620 -827
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +466 -571
- package/dist/src/server/index.d.ts +466 -571
- package/dist/src/server/index.js +1603 -811
- package/dist/src/server/index.js.map +1 -1
- package/package.json +21 -19
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
|
|
@@ -53,7 +49,7 @@ var DatabaseError = class extends Error {
|
|
|
53
49
|
super(message);
|
|
54
50
|
this.name = "DatabaseError";
|
|
55
51
|
this.code = code;
|
|
56
|
-
this.context = context;
|
|
52
|
+
if (context !== void 0) this.context = context;
|
|
57
53
|
}
|
|
58
54
|
};
|
|
59
55
|
/**
|
|
@@ -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
|
|
@@ -91,16 +300,7 @@ function isDatabaseError(value) {
|
|
|
91
300
|
* @returns `-1`, `0`, or `1`
|
|
92
301
|
*/
|
|
93
302
|
function compareValues(left, right) {
|
|
94
|
-
const
|
|
95
|
-
if (value === void 0) return 0;
|
|
96
|
-
if (value === null) return 1;
|
|
97
|
-
if (typeof value === "boolean") return 2;
|
|
98
|
-
if (typeof value === "number") return 3;
|
|
99
|
-
if (typeof value === "string") return 4;
|
|
100
|
-
return 5;
|
|
101
|
-
};
|
|
102
|
-
const leftRank = rankOf(left);
|
|
103
|
-
const rightRank = rankOf(right);
|
|
303
|
+
const [leftRank = 5, rightRank = 5] = [left, right].map((value) => value === void 0 ? 0 : value === null ? 1 : typeof value === "boolean" ? 2 : typeof value === "number" ? 3 : typeof value === "string" ? 4 : 5);
|
|
104
304
|
if (leftRank !== rightRank) return leftRank < rightRank ? -1 : 1;
|
|
105
305
|
if (typeof left === "number" && typeof right === "number") {
|
|
106
306
|
if (Number.isNaN(left) || Number.isNaN(right)) return Number.isNaN(left) ? Number.isNaN(right) ? 0 : 1 : -1;
|
|
@@ -116,14 +316,15 @@ function compareValues(left, right) {
|
|
|
116
316
|
*
|
|
117
317
|
* @remarks
|
|
118
318
|
* Primitives compare by SameValueZero (`NaN` equals itself; `+0` equals `-0`).
|
|
119
|
-
* Arrays compare by index (same length, every element `
|
|
319
|
+
* Arrays compare by index (same length, every element `equalsValue`). Plain
|
|
120
320
|
* records (via `isRecord`) compare by their OWN enumerable keys: same key
|
|
121
321
|
* COUNT and, for every key in `left`, `right` has that key (`Object.hasOwn`)
|
|
122
|
-
* with a `
|
|
322
|
+
* with a `equalsValue` value — so a key present with value `undefined` is NOT
|
|
123
323
|
* equal to that key being absent (both differ in `Object.keys` membership).
|
|
124
324
|
* Anything else (functions, class instances, mismatched shapes) falls through
|
|
125
|
-
* to `false`.
|
|
126
|
-
*
|
|
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.
|
|
127
328
|
*
|
|
128
329
|
* @param left - The left value
|
|
129
330
|
* @param right - The right value
|
|
@@ -131,32 +332,67 @@ function compareValues(left, right) {
|
|
|
131
332
|
*
|
|
132
333
|
* @example
|
|
133
334
|
* ```ts
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
|
137
338
|
* ```
|
|
138
339
|
*/
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
}
|
|
150
386
|
}
|
|
151
387
|
/**
|
|
152
388
|
* Match a value against a wildcard pattern in LINEAR time — the shared, ReDoS-SAFE
|
|
153
|
-
* engine behind {@link
|
|
389
|
+
* engine behind {@link matchesLikePattern} and {@link matchesGlobPattern}.
|
|
154
390
|
*
|
|
155
391
|
* @remarks
|
|
156
392
|
* A backtracking RegExp (`a%b%c` → `^a.*b.*c$`) is CATASTROPHIC on a hostile pattern:
|
|
157
393
|
* `.*` segments separated by literals, matched against a long non-matching input, blow
|
|
158
394
|
* up super-linearly — and JS has no atomic groups / possessive quantifiers to bound it
|
|
159
|
-
* (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
|
|
160
396
|
* wire). So this builds NO regex. It runs the classic GREEDY TWO-POINTER wildcard match:
|
|
161
397
|
* the `any` wildcard records its position and, on a later mismatch, backtracks ONLY to
|
|
162
398
|
* that last `any` (letting it absorb one more char) — so the work is O(value × pattern),
|
|
@@ -179,7 +415,7 @@ function deepEqual(left, right) {
|
|
|
179
415
|
* @returns Whether `value` matches `pattern`
|
|
180
416
|
* @throws A `VALIDATION` {@link DatabaseError} when `pattern` exceeds {@link MAX_PATTERN_LENGTH}
|
|
181
417
|
*/
|
|
182
|
-
function
|
|
418
|
+
function matchesWildcardPattern(value, pattern, any, single, fold) {
|
|
183
419
|
if (pattern.length > 1024) throw new DatabaseError("VALIDATION", `Pattern exceeds the maximum length of ${MAX_PATTERN_LENGTH}`, {
|
|
184
420
|
length: pattern.length,
|
|
185
421
|
limit: MAX_PATTERN_LENGTH
|
|
@@ -208,11 +444,11 @@ function wildcardMatch(value, pattern, any, single, fold) {
|
|
|
208
444
|
while (pi < needle.length && needle[pi] === any) pi += 1;
|
|
209
445
|
return pi === needle.length;
|
|
210
446
|
}
|
|
211
|
-
function
|
|
212
|
-
return
|
|
447
|
+
function matchesLikePattern(value, pattern) {
|
|
448
|
+
return matchesWildcardPattern(value, pattern, "%", "_", true);
|
|
213
449
|
}
|
|
214
|
-
function
|
|
215
|
-
return
|
|
450
|
+
function matchesGlobPattern(value, pattern) {
|
|
451
|
+
return matchesWildcardPattern(value, pattern, "*", "?", false);
|
|
216
452
|
}
|
|
217
453
|
/**
|
|
218
454
|
* Evaluate one {@link Condition} against a row — the per-operator predicate.
|
|
@@ -222,10 +458,10 @@ function globMatch(value, pattern) {
|
|
|
222
458
|
* string is one column; an array descends a nested value) — and applies the
|
|
223
459
|
* operator. Range operators (`above` / `below` / `from` / `to` / `between`) use
|
|
224
460
|
* {@link compareValues}, the total order; the equality family (`equals` / `not`
|
|
225
|
-
* / `any` / `none`) uses {@link
|
|
461
|
+
* / `any` / `none`) uses {@link equalsValue} — STRUCTURAL equality, not the total
|
|
226
462
|
* order's rank-5-collapses-all-objects behavior, so `equals` on an object/array
|
|
227
463
|
* operand only matches a structurally-equal value, never every row holding any
|
|
228
|
-
* object. This is a semantics change from ranking: `
|
|
464
|
+
* object. This is a semantics change from ranking: `equalsValue` is SameValueZero
|
|
229
465
|
* on leaves, so `NaN` now equals `NaN` under `equals` / `any` (it never matched
|
|
230
466
|
* anything under the old rank-based comparison). `like` / `glob` / `starts` /
|
|
231
467
|
* `ends` match only strings; `absent` / `present` test nullishness. Total — a
|
|
@@ -240,19 +476,19 @@ function matchesCondition(row, condition) {
|
|
|
240
476
|
const first = condition.values[0];
|
|
241
477
|
const second = condition.values[1];
|
|
242
478
|
switch (condition.operator) {
|
|
243
|
-
case "equals": return
|
|
244
|
-
case "not": return !
|
|
479
|
+
case "equals": return equalsValue(value, first);
|
|
480
|
+
case "not": return !equalsValue(value, first);
|
|
245
481
|
case "above": return compareValues(value, first) > 0;
|
|
246
482
|
case "below": return compareValues(value, first) < 0;
|
|
247
483
|
case "from": return compareValues(value, first) >= 0;
|
|
248
484
|
case "to": return compareValues(value, first) <= 0;
|
|
249
485
|
case "between": return compareValues(value, first) >= 0 && compareValues(value, second) <= 0;
|
|
250
|
-
case "like": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) &&
|
|
251
|
-
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);
|
|
252
488
|
case "starts": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && value.startsWith(first);
|
|
253
489
|
case "ends": return (0, _orkestrel_contract.isString)(value) && (0, _orkestrel_contract.isString)(first) && value.endsWith(first);
|
|
254
|
-
case "any": return condition.values.some((candidate) =>
|
|
255
|
-
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));
|
|
256
492
|
case "absent": return value === void 0 || value === null;
|
|
257
493
|
case "present": return value !== void 0 && value !== null;
|
|
258
494
|
}
|
|
@@ -270,7 +506,7 @@ function matchesCondition(row, condition) {
|
|
|
270
506
|
* @param conditions - The conditions to fold
|
|
271
507
|
* @returns Whether the row satisfies the combined conditions
|
|
272
508
|
*/
|
|
273
|
-
function
|
|
509
|
+
function matchesQuery(row, conditions) {
|
|
274
510
|
let result = true;
|
|
275
511
|
let seeded = false;
|
|
276
512
|
for (const condition of conditions) {
|
|
@@ -284,11 +520,11 @@ function matchesCriteria(row, conditions) {
|
|
|
284
520
|
}
|
|
285
521
|
/**
|
|
286
522
|
* Filter rows by a list of conditions — the shared basis for a table's count
|
|
287
|
-
* and aggregate paths (no sort/page, unlike {@link
|
|
523
|
+
* and aggregate paths (no sort/page, unlike {@link applyQuery}).
|
|
288
524
|
*
|
|
289
525
|
* @remarks
|
|
290
526
|
* An empty condition list matches every row (returned as-is, no copy). Folds
|
|
291
|
-
* each row through {@link
|
|
527
|
+
* each row through {@link matchesQuery}.
|
|
292
528
|
*
|
|
293
529
|
* @param rows - The rows to filter
|
|
294
530
|
* @param conditions - The conditions to apply (empty matches everything)
|
|
@@ -304,7 +540,7 @@ function matchesCriteria(row, conditions) {
|
|
|
304
540
|
*/
|
|
305
541
|
function filterRows(rows, conditions) {
|
|
306
542
|
if (conditions.length === 0) return rows;
|
|
307
|
-
return rows.filter((row) =>
|
|
543
|
+
return rows.filter((row) => matchesQuery(row, conditions));
|
|
308
544
|
}
|
|
309
545
|
/**
|
|
310
546
|
* Sort rows by an ordering specification, leaving the input untouched.
|
|
@@ -329,26 +565,27 @@ function sortRows(rows, order) {
|
|
|
329
565
|
return sorted;
|
|
330
566
|
}
|
|
331
567
|
/**
|
|
332
|
-
* Apply a {@link
|
|
568
|
+
* Apply a {@link QueryInput} to rows — filter, then sort, then page.
|
|
333
569
|
*
|
|
334
570
|
* @remarks
|
|
335
571
|
* The whole portable read pipeline in one place: conditions filter, `order`
|
|
336
572
|
* sorts, and `offset` / `limit` window the result. Each step is skipped when its
|
|
337
|
-
* part of the
|
|
573
|
+
* part of the input is absent. The reference {@link DriverInterface} backends
|
|
338
574
|
* lean on this rather than each re-deriving it.
|
|
339
575
|
*
|
|
340
576
|
* @param rows - The rows to process (typically a table's full `scan`)
|
|
341
|
-
* @param
|
|
577
|
+
* @param input - The read specification, or `undefined` for all rows as-is
|
|
342
578
|
* @returns The filtered, sorted, paged rows
|
|
343
579
|
*/
|
|
344
|
-
function
|
|
580
|
+
function applyQuery(rows, input) {
|
|
581
|
+
validatePage(input);
|
|
345
582
|
let result = rows;
|
|
346
|
-
const conditions =
|
|
347
|
-
if (conditions !== void 0 && conditions.length > 0) result = result.filter((row) =>
|
|
348
|
-
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;
|
|
349
586
|
if (order !== void 0 && order.length > 0) result = sortRows(result, order);
|
|
350
|
-
const offset =
|
|
351
|
-
const limit =
|
|
587
|
+
const offset = input?.offset ?? 0;
|
|
588
|
+
const limit = input?.limit;
|
|
352
589
|
if (offset > 0 || limit !== void 0) result = result.slice(offset, limit !== void 0 ? offset + limit : void 0);
|
|
353
590
|
return result;
|
|
354
591
|
}
|
|
@@ -390,11 +627,24 @@ function computeAggregate(rows, operation, column) {
|
|
|
390
627
|
*/
|
|
391
628
|
function extractKey(row, column) {
|
|
392
629
|
const value = row[column];
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
};
|
|
395
645
|
}
|
|
396
646
|
/**
|
|
397
|
-
* Map a column's {@link ContractShape} to its portable {@link
|
|
647
|
+
* Map a column's {@link ContractShape} to its portable {@link ColumnStorage} — the
|
|
398
648
|
* value a `TableSchema` carries so a native backend can declare a real column.
|
|
399
649
|
*
|
|
400
650
|
* @remarks
|
|
@@ -411,13 +661,13 @@ function extractKey(row, column) {
|
|
|
411
661
|
*
|
|
412
662
|
* @example
|
|
413
663
|
* ```ts
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
664
|
+
* shapeToColumnStorage(stringShape()) // 'text'
|
|
665
|
+
* shapeToColumnStorage(integerShape()) // 'integer'
|
|
666
|
+
* shapeToColumnStorage(optionalShape(integerShape())) // 'integer'
|
|
667
|
+
* shapeToColumnStorage(objectShape({ a: stringShape() })) // 'json'
|
|
418
668
|
* ```
|
|
419
669
|
*/
|
|
420
|
-
function
|
|
670
|
+
function shapeToColumnStorage(shape) {
|
|
421
671
|
switch (shape.type) {
|
|
422
672
|
case "string": return "text";
|
|
423
673
|
case "number": return shape.integer === true ? "integer" : "real";
|
|
@@ -427,7 +677,7 @@ function shapeToColumnType(shape) {
|
|
|
427
677
|
if (shape.values.every((value) => typeof value === "number")) return shape.values.every((value) => Number.isInteger(value)) ? "integer" : "real";
|
|
428
678
|
return "text";
|
|
429
679
|
case "optional":
|
|
430
|
-
case "nullable": return
|
|
680
|
+
case "nullable": return shapeToColumnStorage(shape.inner);
|
|
431
681
|
case "null":
|
|
432
682
|
case "object":
|
|
433
683
|
case "array":
|
|
@@ -437,53 +687,31 @@ function shapeToColumnType(shape) {
|
|
|
437
687
|
}
|
|
438
688
|
}
|
|
439
689
|
/**
|
|
440
|
-
*
|
|
441
|
-
* versioning driver's `meta()` narrows a stored (structured-clone or
|
|
442
|
-
* `JSON.parse`d) value through before trusting it, replacing the per-driver
|
|
443
|
-
* duplicated narrowing every backend used to hand-roll (AGENTS §14: never `as`).
|
|
690
|
+
* Project one contract shape into a portable column schema.
|
|
444
691
|
*
|
|
445
|
-
* @
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
* `primary` string pair, a `columns` array of well-formed {@link ColumnSchema}
|
|
449
|
-
* entries (a `name` string, a {@link ColumnType} literal, a `nullable`
|
|
450
|
-
* boolean), and an `indexes` array of string arrays. Anything off-shape
|
|
451
|
-
* (including a non-record) returns `false` rather than throwing.
|
|
452
|
-
*
|
|
453
|
-
* @param value - The value to test
|
|
454
|
-
* @returns `true` when `value` is a well-formed `DriverMeta`
|
|
455
|
-
*
|
|
456
|
-
* @example
|
|
457
|
-
* ```ts
|
|
458
|
-
* isDriverMeta({ version: 1, schema: [] }) // true
|
|
459
|
-
* isDriverMeta({ version: 1, schema: [{ name: 'users' }] }) // false
|
|
460
|
-
* ```
|
|
692
|
+
* @param name - The column name
|
|
693
|
+
* @param shape - The column contract shape
|
|
694
|
+
* @returns The portable storage and independent absence/null acceptance
|
|
461
695
|
*/
|
|
462
|
-
function
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
];
|
|
471
|
-
const isColumnType = (candidate) => (0, _orkestrel_contract.isString)(candidate) && COLUMN_TYPES.some((type) => type === candidate);
|
|
472
|
-
const isColumnSchema = (candidate) => (0, _orkestrel_contract.isRecord)(candidate) && (0, _orkestrel_contract.isString)(candidate.name) && isColumnType(candidate.type) && (0, _orkestrel_contract.isBoolean)(candidate.nullable);
|
|
473
|
-
const isIndexGroup = (candidate) => (0, _orkestrel_contract.isArray)(candidate) && candidate.every((entry) => (0, _orkestrel_contract.isString)(entry));
|
|
474
|
-
const isTableSchema = (candidate) => (0, _orkestrel_contract.isRecord)(candidate) && (0, _orkestrel_contract.isString)(candidate.name) && (0, _orkestrel_contract.isString)(candidate.primary) && (0, _orkestrel_contract.isArray)(candidate.columns) && candidate.columns.every(isColumnSchema) && (0, _orkestrel_contract.isArray)(candidate.indexes) && candidate.indexes.every(isIndexGroup);
|
|
475
|
-
return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isFiniteNumber)(value.version) && (0, _orkestrel_contract.isArray)(value.schema) && value.schema.every(isTableSchema);
|
|
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
|
+
};
|
|
476
704
|
}
|
|
477
705
|
/**
|
|
478
|
-
* Throw when an {@link
|
|
479
|
-
*
|
|
706
|
+
* Throw when an {@link OperationOptions.signal | AbortSignal} has fired — the shared
|
|
707
|
+
* abort gate checked at operation boundaries and between streamed rows.
|
|
480
708
|
*
|
|
481
709
|
* @remarks
|
|
482
710
|
* A no-op for `undefined` or a live signal, so callers thread `options?.signal`
|
|
483
711
|
* straight through. When the signal has aborted, throws an `ABORTED`
|
|
484
712
|
* {@link DatabaseError} carrying the signal's `reason` in its context — callers
|
|
485
|
-
* mint signals with
|
|
486
|
-
* `new AbortController()
|
|
713
|
+
* mint signals with native APIs such as `AbortSignal.timeout(ms)` or
|
|
714
|
+
* `new AbortController()`.
|
|
487
715
|
*
|
|
488
716
|
* @param signal - The signal to check, if any
|
|
489
717
|
* @returns Nothing — returns normally while the signal is live
|
|
@@ -515,11 +743,12 @@ function checkAbort(signal) {
|
|
|
515
743
|
* `column.remove` / `index.add` / `index.remove` steps. Step order is
|
|
516
744
|
* deterministic: every `table.remove`, then every `table.add`, then each
|
|
517
745
|
* shared table's column/index changes in `declared` order. `from` / `to` are
|
|
518
|
-
* plan labels only
|
|
519
|
-
*
|
|
746
|
+
* plan labels only; versioning drivers persist and reconcile them through
|
|
747
|
+
* {@link DriverMetadata}.
|
|
520
748
|
*
|
|
521
749
|
* A column present in BOTH schemas under the same name but with a different
|
|
522
|
-
* `
|
|
750
|
+
* `storage`, `optional`, or `nullable` value throws a `MIGRATION`
|
|
751
|
+
* {@link DatabaseError} naming the
|
|
523
752
|
* table, the column, and the from→to difference — a name-only diff would
|
|
524
753
|
* otherwise silently produce NO step for the drift, and versioned
|
|
525
754
|
* reconciliation would stamp over it. There is no automatic in-place
|
|
@@ -532,42 +761,66 @@ function checkAbort(signal) {
|
|
|
532
761
|
* @param from - The plan's source version label (defaults to `0`)
|
|
533
762
|
* @param to - The plan's target version label (defaults to `1`)
|
|
534
763
|
* @returns The migration plan moving `deployed` toward `declared`
|
|
535
|
-
* @throws A `MIGRATION` {@link DatabaseError} when a shared
|
|
536
|
-
*
|
|
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
|
|
537
768
|
*
|
|
538
769
|
* @example
|
|
539
770
|
* ```ts
|
|
540
771
|
* const plan = planMigration(
|
|
541
|
-
* [{ name: 'users', primary: 'id', columns: [], indexes: [] }],
|
|
542
|
-
* [{ 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: [] }],
|
|
543
774
|
* )
|
|
544
775
|
* // plan.steps === [{ operation: 'column.add', table: 'users', column: { name: 'age', ... } }]
|
|
545
776
|
* ```
|
|
546
777
|
*/
|
|
547
778
|
function planMigration(deployed, declared, from = 0, to = 1) {
|
|
548
|
-
|
|
549
|
-
|
|
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]));
|
|
550
793
|
const steps = [];
|
|
551
|
-
for (const table of
|
|
794
|
+
for (const table of beforeSchema) if (!declaredByName.has(table.name)) steps.push({
|
|
552
795
|
operation: "table.remove",
|
|
553
796
|
table: table.name
|
|
554
797
|
});
|
|
555
|
-
for (const table of
|
|
798
|
+
for (const table of targetSchema) if (!deployedByName.has(table.name)) steps.push({
|
|
556
799
|
operation: "table.add",
|
|
557
800
|
table
|
|
558
801
|
});
|
|
559
|
-
for (const table of
|
|
802
|
+
for (const table of targetSchema) {
|
|
560
803
|
const before = deployedByName.get(table.name);
|
|
561
804
|
if (before === void 0) continue;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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({
|
|
565
818
|
operation: "column.remove",
|
|
566
819
|
table: table.name,
|
|
567
820
|
column: column.name
|
|
568
821
|
});
|
|
569
822
|
for (const column of table.columns) {
|
|
570
|
-
const previous =
|
|
823
|
+
const previous = beforeColumnMap.get(column.name);
|
|
571
824
|
if (previous === void 0) {
|
|
572
825
|
steps.push({
|
|
573
826
|
operation: "column.add",
|
|
@@ -576,36 +829,158 @@ function planMigration(deployed, declared, from = 0, to = 1) {
|
|
|
576
829
|
});
|
|
577
830
|
continue;
|
|
578
831
|
}
|
|
579
|
-
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`, {
|
|
580
833
|
table: table.name,
|
|
581
834
|
column: column.name,
|
|
582
835
|
from: {
|
|
583
|
-
|
|
836
|
+
storage: previous.storage,
|
|
837
|
+
optional: previous.optional,
|
|
584
838
|
nullable: previous.nullable
|
|
585
839
|
},
|
|
586
840
|
to: {
|
|
587
|
-
|
|
841
|
+
storage: column.storage,
|
|
842
|
+
optional: column.optional,
|
|
588
843
|
nullable: column.nullable
|
|
589
844
|
}
|
|
590
845
|
});
|
|
591
846
|
}
|
|
592
|
-
|
|
593
|
-
for (const index of before.indexes) if (!table.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
|
|
594
|
-
operation: "index.remove",
|
|
595
|
-
table: table.name,
|
|
596
|
-
index
|
|
597
|
-
});
|
|
598
|
-
for (const index of table.indexes) if (!before.indexes.some((candidate) => sameIndex(candidate, index))) steps.push({
|
|
847
|
+
for (const index of table.indexes) if (!before.indexes.some((candidate) => equalsValue(candidate, index))) steps.push({
|
|
599
848
|
operation: "index.add",
|
|
600
849
|
table: table.name,
|
|
601
850
|
index
|
|
602
851
|
});
|
|
603
852
|
}
|
|
604
|
-
|
|
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: {
|
|
605
859
|
from,
|
|
606
860
|
to,
|
|
607
861
|
steps
|
|
608
|
-
};
|
|
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);
|
|
609
984
|
}
|
|
610
985
|
/**
|
|
611
986
|
* Apply one table's {@link MigrationStep}s to its rows — a pure row transform.
|
|
@@ -653,21 +1028,24 @@ function migrateRows(rows, steps) {
|
|
|
653
1028
|
* `read` of a missing key returns `undefined`; `write`/`read` round-trip with
|
|
654
1029
|
* DEEP copy-in/copy-out isolation (mutating the caller's row — including a
|
|
655
1030
|
* NESTED field — after `write`, or a row `read` returns, never perturbs
|
|
656
|
-
* 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`;
|
|
657
1035
|
* `keys`/`scan` yield in ascending key order; `clear` empties only its target
|
|
658
1036
|
* table; `snapshot`'s rollback thunk restores pre-snapshot state, including a
|
|
659
1037
|
* NESTED field mutated in place on a read-back row between capture and
|
|
660
1038
|
* restore; a scoped `snapshot(['users'])` rolls back only the named table,
|
|
661
1039
|
* leaving a concurrent mutation to another table intact; a
|
|
662
1040
|
* non-`id` primary key (`posts.slug`) round-trips; a nested-object row
|
|
663
|
-
* round-trips structurally (via {@link
|
|
1041
|
+
* round-trips structurally (via {@link equalsValue}). The optional surface is
|
|
664
1042
|
* presence-gated: when `migrate` exists, a `column.remove` plan strips the
|
|
665
1043
|
* column from stored rows and a plan referencing an unknown table throws
|
|
666
1044
|
* `DatabaseError` `MIGRATION`; when `stream` exists, it yields only
|
|
667
1045
|
* condition-matching rows and honors `offset`/`limit`; when `transaction`
|
|
668
|
-
* exists, `commit` persists and `rollback` restores; when both `
|
|
669
|
-
* `stamp` exist, a fresh store's `
|
|
670
|
-
* `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.
|
|
671
1049
|
*
|
|
672
1050
|
* Each phase runs within a `try`/`catch`: an EXPECTED mismatch yields a
|
|
673
1051
|
* finding built from the assertion, while an UNEXPECTED throw (a driver
|
|
@@ -699,23 +1077,27 @@ async function* driverFindings(factory) {
|
|
|
699
1077
|
columns: [
|
|
700
1078
|
{
|
|
701
1079
|
name: "id",
|
|
702
|
-
|
|
1080
|
+
storage: "text",
|
|
1081
|
+
optional: false,
|
|
703
1082
|
nullable: false
|
|
704
1083
|
},
|
|
705
1084
|
{
|
|
706
1085
|
name: "name",
|
|
707
|
-
|
|
1086
|
+
storage: "text",
|
|
1087
|
+
optional: false,
|
|
708
1088
|
nullable: false
|
|
709
1089
|
},
|
|
710
1090
|
{
|
|
711
1091
|
name: "age",
|
|
712
|
-
|
|
713
|
-
|
|
1092
|
+
storage: "integer",
|
|
1093
|
+
optional: true,
|
|
1094
|
+
nullable: false
|
|
714
1095
|
},
|
|
715
1096
|
{
|
|
716
1097
|
name: "meta",
|
|
717
|
-
|
|
718
|
-
|
|
1098
|
+
storage: "json",
|
|
1099
|
+
optional: true,
|
|
1100
|
+
nullable: false
|
|
719
1101
|
}
|
|
720
1102
|
],
|
|
721
1103
|
indexes: []
|
|
@@ -725,539 +1107,801 @@ async function* driverFindings(factory) {
|
|
|
725
1107
|
primary: "slug",
|
|
726
1108
|
columns: [{
|
|
727
1109
|
name: "slug",
|
|
728
|
-
|
|
1110
|
+
storage: "text",
|
|
1111
|
+
optional: false,
|
|
729
1112
|
nullable: false
|
|
730
1113
|
}, {
|
|
731
1114
|
name: "title",
|
|
732
|
-
|
|
1115
|
+
storage: "text",
|
|
1116
|
+
optional: false,
|
|
733
1117
|
nullable: false
|
|
734
1118
|
}],
|
|
735
1119
|
indexes: []
|
|
736
1120
|
};
|
|
737
1121
|
const CONFORMANCE_SCHEMA = [CONFORMANCE_USERS_SCHEMA, CONFORMANCE_POSTS_SCHEMA];
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
})
|
|
743
|
-
|
|
744
|
-
{
|
|
1122
|
+
try {
|
|
1123
|
+
const driver = factory();
|
|
1124
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1125
|
+
await driver.close();
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
yield {
|
|
745
1128
|
check: "open-close",
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1129
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1130
|
+
context: { error }
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
const driver = factory();
|
|
1135
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1136
|
+
const missing = await driver.read("users", "nope");
|
|
1137
|
+
await driver.close();
|
|
1138
|
+
if (missing !== void 0) yield {
|
|
1139
|
+
check: "read-missing",
|
|
1140
|
+
message: "read of a missing key must return undefined",
|
|
1141
|
+
context: {
|
|
1142
|
+
table: "users",
|
|
1143
|
+
expected: void 0,
|
|
1144
|
+
actual: missing
|
|
750
1145
|
}
|
|
751
|
-
}
|
|
752
|
-
|
|
1146
|
+
};
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
yield {
|
|
753
1149
|
check: "read-missing",
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1150
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1151
|
+
context: { error }
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
writeRead: try {
|
|
1155
|
+
const driver = factory();
|
|
1156
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1157
|
+
const input = {
|
|
1158
|
+
id: "caller",
|
|
1159
|
+
name: "Ada",
|
|
1160
|
+
age: 30,
|
|
1161
|
+
meta: { tags: ["a"] }
|
|
1162
|
+
};
|
|
1163
|
+
await driver.write("users", "u1", input);
|
|
1164
|
+
input.name = "Mutated after write";
|
|
1165
|
+
if ((0, _orkestrel_contract.isRecord)(input.meta) && Array.isArray(input.meta.tags)) input.meta.tags.push("mutated");
|
|
1166
|
+
const stored = await driver.read("users", "u1");
|
|
1167
|
+
const original = {
|
|
1168
|
+
id: "u1",
|
|
1169
|
+
name: "Ada",
|
|
1170
|
+
age: 30,
|
|
1171
|
+
meta: { tags: ["a"] }
|
|
1172
|
+
};
|
|
1173
|
+
if (stored === void 0 || !equalsValue(stored, original)) {
|
|
1174
|
+
await driver.close();
|
|
1175
|
+
yield {
|
|
1176
|
+
check: "copy-in",
|
|
1177
|
+
message: "write must deep-copy the input row (including nested fields) rather than store it by reference",
|
|
1178
|
+
context: {
|
|
760
1179
|
table: "users",
|
|
761
|
-
expected:
|
|
762
|
-
actual:
|
|
763
|
-
}
|
|
1180
|
+
expected: original,
|
|
1181
|
+
actual: stored
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
break writeRead;
|
|
1185
|
+
}
|
|
1186
|
+
stored.name = "Mutated after read";
|
|
1187
|
+
if ((0, _orkestrel_contract.isRecord)(stored.meta) && Array.isArray(stored.meta.tags)) stored.meta.tags.push("mutated");
|
|
1188
|
+
const reread = await driver.read("users", "u1");
|
|
1189
|
+
if (reread === void 0 || !equalsValue(reread, original)) {
|
|
1190
|
+
await driver.close();
|
|
1191
|
+
yield {
|
|
1192
|
+
check: "copy-out",
|
|
1193
|
+
message: "read must deep-copy the stored row (including nested fields) rather than return it by reference",
|
|
1194
|
+
context: {
|
|
1195
|
+
table: "users",
|
|
1196
|
+
expected: original,
|
|
1197
|
+
actual: reread
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
break writeRead;
|
|
1201
|
+
}
|
|
1202
|
+
const overwrite = {
|
|
1203
|
+
id: "caller",
|
|
1204
|
+
name: "Ada Overwritten",
|
|
1205
|
+
age: 31
|
|
1206
|
+
};
|
|
1207
|
+
await driver.write("users", "u1", overwrite);
|
|
1208
|
+
const overwritten = await driver.read("users", "u1");
|
|
1209
|
+
await driver.close();
|
|
1210
|
+
const expectedOverwrite = {
|
|
1211
|
+
...overwrite,
|
|
1212
|
+
id: "u1"
|
|
1213
|
+
};
|
|
1214
|
+
if (overwritten === void 0 || !equalsValue(overwritten, expectedOverwrite)) yield {
|
|
1215
|
+
check: "upsert",
|
|
1216
|
+
message: "write must upsert-overwrite an existing key",
|
|
1217
|
+
context: {
|
|
1218
|
+
table: "users",
|
|
1219
|
+
expected: expectedOverwrite,
|
|
1220
|
+
actual: overwritten
|
|
764
1221
|
}
|
|
765
|
-
}
|
|
766
|
-
|
|
1222
|
+
};
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
yield {
|
|
767
1225
|
check: "write-read",
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
actual: reread
|
|
804
|
-
});
|
|
1226
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1227
|
+
context: { error }
|
|
1228
|
+
};
|
|
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
|
|
805
1261
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
+
}
|
|
1271
|
+
deletePhase: try {
|
|
1272
|
+
const driver = factory();
|
|
1273
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1274
|
+
await driver.write("users", "u1", {
|
|
1275
|
+
id: "u1",
|
|
1276
|
+
name: "Ada",
|
|
1277
|
+
age: 30
|
|
1278
|
+
});
|
|
1279
|
+
const first = await driver.delete("users", "u1");
|
|
1280
|
+
if (first !== true) {
|
|
1281
|
+
await driver.close();
|
|
1282
|
+
yield {
|
|
1283
|
+
check: "delete-true",
|
|
1284
|
+
message: "delete of an existing key must return true",
|
|
1285
|
+
context: {
|
|
815
1286
|
table: "users",
|
|
816
|
-
expected:
|
|
817
|
-
actual:
|
|
818
|
-
}
|
|
1287
|
+
expected: true,
|
|
1288
|
+
actual: first
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
break deletePhase;
|
|
1292
|
+
}
|
|
1293
|
+
const second = await driver.delete("users", "u1");
|
|
1294
|
+
await driver.close();
|
|
1295
|
+
if (second !== false) yield {
|
|
1296
|
+
check: "delete-false",
|
|
1297
|
+
message: "delete of an already-removed key must return false",
|
|
1298
|
+
context: {
|
|
1299
|
+
table: "users",
|
|
1300
|
+
expected: false,
|
|
1301
|
+
actual: second
|
|
819
1302
|
}
|
|
820
|
-
}
|
|
821
|
-
|
|
1303
|
+
};
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
yield {
|
|
822
1306
|
check: "delete",
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1307
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1308
|
+
context: { error }
|
|
1309
|
+
};
|
|
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
|
|
839
1364
|
}
|
|
840
|
-
const second = await driver.delete("users", "u1");
|
|
841
|
-
await driver.close();
|
|
842
|
-
if (second !== false) return findingOf("delete-false", "delete of an already-removed key must return false", {
|
|
843
|
-
table: "users",
|
|
844
|
-
expected: false,
|
|
845
|
-
actual: second
|
|
846
|
-
});
|
|
847
1365
|
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
await driver.close();
|
|
889
|
-
if (!deepEqual(scannedIds, expected)) return findingOf("scan-order", "scan must yield rows in ascending key order", {
|
|
1366
|
+
};
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
yield {
|
|
1369
|
+
check: "mutation-abort",
|
|
1370
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1371
|
+
context: { error }
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
orderPhase: try {
|
|
1375
|
+
const driver = factory();
|
|
1376
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1377
|
+
for (const row of [
|
|
1378
|
+
{
|
|
1379
|
+
id: "c",
|
|
1380
|
+
name: "C",
|
|
1381
|
+
age: 3
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
id: "a",
|
|
1385
|
+
name: "A",
|
|
1386
|
+
age: 1
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
id: "b",
|
|
1390
|
+
name: "B",
|
|
1391
|
+
age: 2
|
|
1392
|
+
}
|
|
1393
|
+
]) await driver.write("users", row.id, row);
|
|
1394
|
+
const expected = [
|
|
1395
|
+
"a",
|
|
1396
|
+
"b",
|
|
1397
|
+
"c"
|
|
1398
|
+
];
|
|
1399
|
+
const keys = [...await driver.keys("users")];
|
|
1400
|
+
if (!equalsValue(keys, expected)) {
|
|
1401
|
+
await driver.close();
|
|
1402
|
+
yield {
|
|
1403
|
+
check: "keys-order",
|
|
1404
|
+
message: "keys must be returned in ascending key order",
|
|
1405
|
+
context: {
|
|
890
1406
|
table: "users",
|
|
891
1407
|
expected,
|
|
892
|
-
actual:
|
|
893
|
-
}
|
|
1408
|
+
actual: keys
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
break orderPhase;
|
|
1412
|
+
}
|
|
1413
|
+
const scanned = [];
|
|
1414
|
+
for await (const row of driver.scan("users")) scanned.push(row);
|
|
1415
|
+
const scannedIds = scanned.map((row) => row.id);
|
|
1416
|
+
await driver.close();
|
|
1417
|
+
if (!equalsValue(scannedIds, expected)) yield {
|
|
1418
|
+
check: "scan-order",
|
|
1419
|
+
message: "scan must yield rows in ascending key order",
|
|
1420
|
+
context: {
|
|
1421
|
+
table: "users",
|
|
1422
|
+
expected,
|
|
1423
|
+
actual: scannedIds
|
|
894
1424
|
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1425
|
+
};
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
yield {
|
|
1428
|
+
check: "order",
|
|
1429
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1430
|
+
context: { error }
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
clearPhase: try {
|
|
1434
|
+
const driver = factory();
|
|
1435
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1436
|
+
await driver.write("users", "u1", {
|
|
1437
|
+
id: "u1",
|
|
1438
|
+
name: "Ada",
|
|
1439
|
+
age: 30
|
|
1440
|
+
});
|
|
1441
|
+
await driver.write("posts", "p1", {
|
|
1442
|
+
slug: "p1",
|
|
1443
|
+
title: "Post"
|
|
1444
|
+
});
|
|
1445
|
+
await driver.clear("users");
|
|
1446
|
+
const usersKeys = await driver.keys("users");
|
|
1447
|
+
const postsKeys = await driver.keys("posts");
|
|
1448
|
+
await driver.close();
|
|
1449
|
+
if (usersKeys.length !== 0) {
|
|
1450
|
+
yield {
|
|
1451
|
+
check: "clear-target",
|
|
1452
|
+
message: "clear must empty the targeted table",
|
|
1453
|
+
context: {
|
|
915
1454
|
table: "users",
|
|
916
1455
|
expected: [],
|
|
917
1456
|
actual: usersKeys
|
|
918
|
-
});
|
|
919
|
-
if (postsKeys.length !== 1) return findingOf("clear-other", "clear must not affect other tables", {
|
|
920
|
-
table: "posts",
|
|
921
|
-
expected: 1,
|
|
922
|
-
actual: postsKeys.length
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
},
|
|
926
|
-
{
|
|
927
|
-
check: "snapshot",
|
|
928
|
-
run: async () => {
|
|
929
|
-
const driver = factory();
|
|
930
|
-
await driver.open(CONFORMANCE_SCHEMA);
|
|
931
|
-
const original = {
|
|
932
|
-
id: "u1",
|
|
933
|
-
name: "Ada",
|
|
934
|
-
age: 30
|
|
935
|
-
};
|
|
936
|
-
await driver.write("users", "u1", original);
|
|
937
|
-
const rollback = await driver.snapshot();
|
|
938
|
-
await driver.write("users", "u2", {
|
|
939
|
-
id: "u2",
|
|
940
|
-
name: "Grace",
|
|
941
|
-
age: 40
|
|
942
|
-
});
|
|
943
|
-
await driver.delete("users", "u1");
|
|
944
|
-
await rollback();
|
|
945
|
-
const keys = [...await driver.keys("users")];
|
|
946
|
-
if (!deepEqual(keys, ["u1"])) {
|
|
947
|
-
await driver.close();
|
|
948
|
-
return findingOf("snapshot-rollback", "snapshot rollback must restore the pre-snapshot key set", {
|
|
949
|
-
table: "users",
|
|
950
|
-
expected: ["u1"],
|
|
951
|
-
actual: keys
|
|
952
|
-
});
|
|
953
1457
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1458
|
+
};
|
|
1459
|
+
break clearPhase;
|
|
1460
|
+
}
|
|
1461
|
+
if (postsKeys.length !== 1) yield {
|
|
1462
|
+
check: "clear-other",
|
|
1463
|
+
message: "clear must not affect other tables",
|
|
1464
|
+
context: {
|
|
1465
|
+
table: "posts",
|
|
1466
|
+
expected: 1,
|
|
1467
|
+
actual: postsKeys.length
|
|
961
1468
|
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1469
|
+
};
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
yield {
|
|
1472
|
+
check: "clear",
|
|
1473
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1474
|
+
context: { error }
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
snapshotPhase: try {
|
|
1478
|
+
const driver = factory();
|
|
1479
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1480
|
+
const original = {
|
|
1481
|
+
id: "u1",
|
|
1482
|
+
name: "Ada",
|
|
1483
|
+
age: 30
|
|
1484
|
+
};
|
|
1485
|
+
await driver.write("users", "u1", original);
|
|
1486
|
+
const rollback = await driver.snapshot();
|
|
1487
|
+
await driver.write("users", "u2", {
|
|
1488
|
+
id: "u2",
|
|
1489
|
+
name: "Grace",
|
|
1490
|
+
age: 40
|
|
1491
|
+
});
|
|
1492
|
+
await driver.delete("users", "u1");
|
|
1493
|
+
await rollback();
|
|
1494
|
+
const keys = [...await driver.keys("users")];
|
|
1495
|
+
if (!equalsValue(keys, ["u1"])) {
|
|
1496
|
+
await driver.close();
|
|
1497
|
+
yield {
|
|
1498
|
+
check: "snapshot-rollback",
|
|
1499
|
+
message: "snapshot rollback must restore the pre-snapshot key set",
|
|
1500
|
+
context: {
|
|
988
1501
|
table: "users",
|
|
989
|
-
expected:
|
|
990
|
-
actual:
|
|
991
|
-
}
|
|
1502
|
+
expected: ["u1"],
|
|
1503
|
+
actual: keys
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
break snapshotPhase;
|
|
1507
|
+
}
|
|
1508
|
+
const restored = await driver.read("users", "u1");
|
|
1509
|
+
await driver.close();
|
|
1510
|
+
if (restored === void 0 || !equalsValue(restored, original)) yield {
|
|
1511
|
+
check: "snapshot-rollback-value",
|
|
1512
|
+
message: "snapshot rollback must restore pre-snapshot row values",
|
|
1513
|
+
context: {
|
|
1514
|
+
table: "users",
|
|
1515
|
+
expected: original,
|
|
1516
|
+
actual: restored
|
|
992
1517
|
}
|
|
993
|
-
}
|
|
994
|
-
|
|
1518
|
+
};
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
yield {
|
|
1521
|
+
check: "snapshot",
|
|
1522
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1523
|
+
context: { error }
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
try {
|
|
1527
|
+
const driver = factory();
|
|
1528
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1529
|
+
const original = {
|
|
1530
|
+
id: "u3",
|
|
1531
|
+
name: "Nested",
|
|
1532
|
+
age: 20,
|
|
1533
|
+
meta: { tags: ["a"] }
|
|
1534
|
+
};
|
|
1535
|
+
await driver.write("users", "u3", original);
|
|
1536
|
+
const rollback = await driver.snapshot();
|
|
1537
|
+
const before = await driver.read("users", "u3");
|
|
1538
|
+
if ((0, _orkestrel_contract.isRecord)(before) && (0, _orkestrel_contract.isRecord)(before.meta) && Array.isArray(before.meta.tags)) before.meta.tags.push("mutated-before-restore");
|
|
1539
|
+
await driver.write("users", "u3", {
|
|
1540
|
+
id: "u3",
|
|
1541
|
+
name: "Nested",
|
|
1542
|
+
age: 20,
|
|
1543
|
+
meta: { tags: ["a", "mutated-after-write"] }
|
|
1544
|
+
});
|
|
1545
|
+
await rollback();
|
|
1546
|
+
const restored = await driver.read("users", "u3");
|
|
1547
|
+
await driver.close();
|
|
1548
|
+
if (restored === void 0 || !equalsValue(restored, original)) yield {
|
|
1549
|
+
check: "snapshot-nested",
|
|
1550
|
+
message: "snapshot rollback must restore pre-snapshot nested field values, unaffected by a later in-place mutation of a read-back row",
|
|
1551
|
+
context: {
|
|
1552
|
+
table: "users",
|
|
1553
|
+
expected: original,
|
|
1554
|
+
actual: restored
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
yield {
|
|
1559
|
+
check: "snapshot-nested",
|
|
1560
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1561
|
+
context: { error }
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
const driver = factory();
|
|
1566
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1567
|
+
await driver.write("posts", "hello-world", {
|
|
1568
|
+
slug: "caller",
|
|
1569
|
+
title: "Hello"
|
|
1570
|
+
});
|
|
1571
|
+
const post = await driver.read("posts", "hello-world");
|
|
1572
|
+
const key = post === void 0 ? void 0 : extractKey(post, "slug");
|
|
1573
|
+
await driver.close();
|
|
1574
|
+
if (key !== "hello-world") yield {
|
|
995
1575
|
check: "non-id-primary",
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
title: "Hello"
|
|
1002
|
-
});
|
|
1003
|
-
const post = await driver.read("posts", "hello-world");
|
|
1004
|
-
const key = post === void 0 ? void 0 : extractKey(post, "slug");
|
|
1005
|
-
await driver.close();
|
|
1006
|
-
if (key !== "hello-world") return findingOf("non-id-primary", "a non-id primary key column must round-trip through the store", {
|
|
1007
|
-
table: "posts",
|
|
1008
|
-
expected: "hello-world",
|
|
1009
|
-
actual: key
|
|
1010
|
-
});
|
|
1576
|
+
message: "a non-id primary key column must round-trip through the store",
|
|
1577
|
+
context: {
|
|
1578
|
+
table: "posts",
|
|
1579
|
+
expected: "hello-world",
|
|
1580
|
+
actual: key
|
|
1011
1581
|
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1582
|
+
};
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
yield {
|
|
1585
|
+
check: "non-id-primary",
|
|
1586
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1587
|
+
context: { error }
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
try {
|
|
1591
|
+
const driver = factory();
|
|
1592
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1593
|
+
const nested = {
|
|
1594
|
+
id: "u3",
|
|
1595
|
+
name: "Nested",
|
|
1596
|
+
age: 20,
|
|
1597
|
+
meta: {
|
|
1598
|
+
tags: ["a", "b"],
|
|
1599
|
+
deep: { flag: true }
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
await driver.write("users", "u3", nested);
|
|
1603
|
+
const readBack = await driver.read("users", "u3");
|
|
1604
|
+
await driver.close();
|
|
1605
|
+
if (readBack === void 0 || !equalsValue(readBack, nested)) yield {
|
|
1014
1606
|
check: "nested-roundtrip",
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1607
|
+
message: "a nested-object row must round-trip structurally",
|
|
1608
|
+
context: {
|
|
1609
|
+
table: "users",
|
|
1610
|
+
expected: nested,
|
|
1611
|
+
actual: readBack
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
yield {
|
|
1616
|
+
check: "nested-roundtrip",
|
|
1617
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1618
|
+
context: { error }
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
migratePhase: try {
|
|
1622
|
+
const driver = factory();
|
|
1623
|
+
if (driver.migrate === void 0) break migratePhase;
|
|
1624
|
+
const deployedUsers = {
|
|
1625
|
+
...CONFORMANCE_USERS_SCHEMA,
|
|
1626
|
+
columns: [...CONFORMANCE_USERS_SCHEMA.columns, {
|
|
1627
|
+
name: "legacy",
|
|
1628
|
+
storage: "boolean",
|
|
1629
|
+
optional: true,
|
|
1630
|
+
nullable: false
|
|
1631
|
+
}]
|
|
1632
|
+
};
|
|
1633
|
+
await driver.open([deployedUsers, CONFORMANCE_POSTS_SCHEMA]);
|
|
1634
|
+
await driver.write("users", "u1", {
|
|
1635
|
+
id: "u1",
|
|
1636
|
+
name: "Ada",
|
|
1637
|
+
age: 30,
|
|
1638
|
+
legacy: true
|
|
1639
|
+
});
|
|
1640
|
+
const removePlan = planMigration([deployedUsers], [CONFORMANCE_USERS_SCHEMA]);
|
|
1641
|
+
await driver.migrate({ plan: removePlan });
|
|
1642
|
+
const migrated = await driver.read("users", "u1");
|
|
1643
|
+
if (migrated === void 0 || "legacy" in migrated) {
|
|
1644
|
+
await driver.close();
|
|
1645
|
+
yield {
|
|
1646
|
+
check: "migrate-column-remove",
|
|
1647
|
+
message: "a column.remove migration must strip the column from stored rows",
|
|
1648
|
+
context: {
|
|
1031
1649
|
table: "users",
|
|
1032
|
-
expected:
|
|
1033
|
-
actual:
|
|
1034
|
-
}
|
|
1650
|
+
expected: void 0,
|
|
1651
|
+
actual: migrated === void 0 ? void 0 : migrated.legacy
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
break migratePhase;
|
|
1655
|
+
}
|
|
1656
|
+
let caught;
|
|
1657
|
+
try {
|
|
1658
|
+
await driver.migrate({ plan: {
|
|
1659
|
+
from: 0,
|
|
1660
|
+
to: 1,
|
|
1661
|
+
steps: [{
|
|
1662
|
+
operation: "table.remove",
|
|
1663
|
+
table: "ghost"
|
|
1664
|
+
}]
|
|
1665
|
+
} });
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
caught = error;
|
|
1668
|
+
}
|
|
1669
|
+
await driver.close();
|
|
1670
|
+
if (!isDatabaseError(caught) || caught.code !== "MIGRATION") yield {
|
|
1671
|
+
check: "migrate-unknown-table",
|
|
1672
|
+
message: "a migration step referencing an unknown table must throw a MIGRATION DatabaseError",
|
|
1673
|
+
context: {
|
|
1674
|
+
table: "ghost",
|
|
1675
|
+
expected: "MIGRATION",
|
|
1676
|
+
actual: isDatabaseError(caught) ? caught.code : caught
|
|
1035
1677
|
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1678
|
+
};
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
yield {
|
|
1038
1681
|
check: "migrate",
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1682
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1683
|
+
context: { error }
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
streamPhase: try {
|
|
1687
|
+
const driver = factory();
|
|
1688
|
+
if (driver.stream === void 0) break streamPhase;
|
|
1689
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1690
|
+
for (const row of [
|
|
1691
|
+
{
|
|
1692
|
+
id: "a",
|
|
1693
|
+
name: "A",
|
|
1694
|
+
age: 10
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
id: "b",
|
|
1698
|
+
name: "B",
|
|
1699
|
+
age: 20
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
id: "c",
|
|
1703
|
+
name: "C",
|
|
1704
|
+
age: 30
|
|
1705
|
+
}
|
|
1706
|
+
]) await driver.write("users", row.id, row);
|
|
1707
|
+
const input = { conditions: [{
|
|
1708
|
+
column: "age",
|
|
1709
|
+
operator: "above",
|
|
1710
|
+
values: [10],
|
|
1711
|
+
connector: "and"
|
|
1712
|
+
}] };
|
|
1713
|
+
const matched = [];
|
|
1714
|
+
for await (const row of driver.stream("users", input)) matched.push(row);
|
|
1715
|
+
const matchedIds = matched.map((row) => row.id).sort();
|
|
1716
|
+
if (!equalsValue(matchedIds, ["b", "c"])) {
|
|
1717
|
+
await driver.close();
|
|
1718
|
+
yield {
|
|
1719
|
+
check: "stream-match",
|
|
1720
|
+
message: "stream must yield only condition-matching rows",
|
|
1721
|
+
context: {
|
|
1722
|
+
table: "users",
|
|
1723
|
+
expected: ["b", "c"],
|
|
1724
|
+
actual: matchedIds
|
|
1080
1725
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1726
|
+
};
|
|
1727
|
+
break streamPhase;
|
|
1728
|
+
}
|
|
1729
|
+
const paged = [];
|
|
1730
|
+
for await (const row of driver.stream("users", {
|
|
1731
|
+
offset: 1,
|
|
1732
|
+
limit: 1
|
|
1733
|
+
})) paged.push(row);
|
|
1734
|
+
await driver.close();
|
|
1735
|
+
if (paged.length !== 1) yield {
|
|
1736
|
+
check: "stream-page",
|
|
1737
|
+
message: "stream must honor offset and limit",
|
|
1738
|
+
context: {
|
|
1739
|
+
table: "users",
|
|
1740
|
+
expected: 1,
|
|
1741
|
+
actual: paged.length
|
|
1087
1742
|
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1743
|
+
};
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
yield {
|
|
1090
1746
|
check: "stream",
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const matched = [];
|
|
1119
|
-
for await (const row of driver.stream("users", criteria)) matched.push(row);
|
|
1120
|
-
const matchedIds = matched.map((row) => row.id).sort();
|
|
1121
|
-
if (!deepEqual(matchedIds, ["b", "c"])) {
|
|
1122
|
-
await driver.close();
|
|
1123
|
-
return findingOf("stream-match", "stream must yield only condition-matching rows", {
|
|
1124
|
-
table: "users",
|
|
1125
|
-
expected: ["b", "c"],
|
|
1126
|
-
actual: matchedIds
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
const paged = [];
|
|
1130
|
-
for await (const row of driver.stream("users", {
|
|
1131
|
-
offset: 1,
|
|
1132
|
-
limit: 1
|
|
1133
|
-
})) paged.push(row);
|
|
1134
|
-
await driver.close();
|
|
1135
|
-
if (paged.length !== 1) return findingOf("stream-page", "stream must honor offset and limit", {
|
|
1747
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1748
|
+
context: { error }
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
transactionPhase: try {
|
|
1752
|
+
const driver = factory();
|
|
1753
|
+
if (driver.transaction === void 0) break transactionPhase;
|
|
1754
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1755
|
+
await driver.write("users", "u1", {
|
|
1756
|
+
id: "u1",
|
|
1757
|
+
name: "Ada",
|
|
1758
|
+
age: 30
|
|
1759
|
+
});
|
|
1760
|
+
await driver.transaction(async (transaction) => {
|
|
1761
|
+
await transaction.write("users", "u2", {
|
|
1762
|
+
id: "u2",
|
|
1763
|
+
name: "Grace",
|
|
1764
|
+
age: 40
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
1767
|
+
const afterCommit = [...await driver.keys("users")].sort();
|
|
1768
|
+
if (!equalsValue(afterCommit, ["u1", "u2"])) {
|
|
1769
|
+
await driver.close();
|
|
1770
|
+
yield {
|
|
1771
|
+
check: "transaction-commit",
|
|
1772
|
+
message: "transaction commit must persist writes made during the scope",
|
|
1773
|
+
context: {
|
|
1136
1774
|
table: "users",
|
|
1137
|
-
expected:
|
|
1138
|
-
actual:
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
},
|
|
1142
|
-
{
|
|
1143
|
-
check: "transaction",
|
|
1144
|
-
run: async () => {
|
|
1145
|
-
const driver = factory();
|
|
1146
|
-
if (driver.transaction === void 0) return void 0;
|
|
1147
|
-
await driver.open(CONFORMANCE_SCHEMA);
|
|
1148
|
-
await driver.write("users", "u1", {
|
|
1149
|
-
id: "u1",
|
|
1150
|
-
name: "Ada",
|
|
1151
|
-
age: 30
|
|
1152
|
-
});
|
|
1153
|
-
const committing = await driver.transaction();
|
|
1154
|
-
await driver.write("users", "u2", {
|
|
1155
|
-
id: "u2",
|
|
1156
|
-
name: "Grace",
|
|
1157
|
-
age: 40
|
|
1158
|
-
});
|
|
1159
|
-
await committing.commit();
|
|
1160
|
-
const afterCommit = [...await driver.keys("users")].sort();
|
|
1161
|
-
if (!deepEqual(afterCommit, ["u1", "u2"])) {
|
|
1162
|
-
await driver.close();
|
|
1163
|
-
return findingOf("transaction-commit", "transaction commit must persist writes made during the scope", {
|
|
1164
|
-
table: "users",
|
|
1165
|
-
expected: ["u1", "u2"],
|
|
1166
|
-
actual: afterCommit
|
|
1167
|
-
});
|
|
1775
|
+
expected: ["u1", "u2"],
|
|
1776
|
+
actual: afterCommit
|
|
1168
1777
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1778
|
+
};
|
|
1779
|
+
break transactionPhase;
|
|
1780
|
+
}
|
|
1781
|
+
const reason = {};
|
|
1782
|
+
try {
|
|
1783
|
+
await driver.transaction(async (transaction) => {
|
|
1784
|
+
await transaction.write("users", "u3", {
|
|
1171
1785
|
id: "u3",
|
|
1172
1786
|
name: "Marie",
|
|
1173
1787
|
age: 50
|
|
1174
1788
|
});
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1789
|
+
throw reason;
|
|
1790
|
+
});
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
if (error !== reason) throw error;
|
|
1793
|
+
}
|
|
1794
|
+
const afterRollback = [...await driver.keys("users")].sort();
|
|
1795
|
+
await driver.close();
|
|
1796
|
+
if (!equalsValue(afterRollback, ["u1", "u2"])) yield {
|
|
1797
|
+
check: "transaction-rollback",
|
|
1798
|
+
message: "transaction rollback must restore pre-transaction state",
|
|
1799
|
+
context: {
|
|
1800
|
+
table: "users",
|
|
1801
|
+
expected: ["u1", "u2"],
|
|
1802
|
+
actual: afterRollback
|
|
1183
1803
|
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1804
|
+
};
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
yield {
|
|
1807
|
+
check: "transaction",
|
|
1808
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1809
|
+
context: { error }
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
metadataPhase: try {
|
|
1813
|
+
const driver = factory();
|
|
1814
|
+
if (driver.metadata === void 0 || driver.stamp === void 0) break metadataPhase;
|
|
1815
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1816
|
+
const fresh = await driver.metadata();
|
|
1817
|
+
if (fresh !== void 0) {
|
|
1818
|
+
await driver.close();
|
|
1819
|
+
yield {
|
|
1820
|
+
check: "metadata-fresh",
|
|
1821
|
+
message: "a fresh store must report undefined metadata",
|
|
1822
|
+
context: {
|
|
1823
|
+
expected: void 0,
|
|
1824
|
+
actual: fresh
|
|
1198
1825
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1826
|
+
};
|
|
1827
|
+
break metadataPhase;
|
|
1828
|
+
}
|
|
1829
|
+
const stamped = {
|
|
1830
|
+
version: 1,
|
|
1831
|
+
schema: CONFORMANCE_SCHEMA
|
|
1832
|
+
};
|
|
1833
|
+
await driver.stamp(stamped);
|
|
1834
|
+
const read = await driver.metadata();
|
|
1835
|
+
await driver.close();
|
|
1836
|
+
if (read === void 0 || !equalsValue(read, stamped)) yield {
|
|
1837
|
+
check: "metadata-stamp",
|
|
1838
|
+
message: "metadata() must return exactly the last-stamped value",
|
|
1839
|
+
context: {
|
|
1840
|
+
expected: stamped,
|
|
1841
|
+
actual: read
|
|
1210
1842
|
}
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1843
|
+
};
|
|
1844
|
+
} catch (error) {
|
|
1845
|
+
yield {
|
|
1846
|
+
check: "metadata-stamp",
|
|
1847
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1848
|
+
context: { error }
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
scopedPhase: try {
|
|
1852
|
+
const driver = factory();
|
|
1853
|
+
await driver.open(CONFORMANCE_SCHEMA);
|
|
1854
|
+
await driver.write("users", "u1", {
|
|
1855
|
+
id: "u1",
|
|
1856
|
+
name: "Ada",
|
|
1857
|
+
age: 30
|
|
1858
|
+
});
|
|
1859
|
+
await driver.write("posts", "p1", {
|
|
1860
|
+
slug: "p1",
|
|
1861
|
+
title: "Post"
|
|
1862
|
+
});
|
|
1863
|
+
const rollback = await driver.snapshot(["users"]);
|
|
1864
|
+
await driver.write("users", "u2", {
|
|
1865
|
+
id: "u2",
|
|
1866
|
+
name: "Grace",
|
|
1867
|
+
age: 40
|
|
1868
|
+
});
|
|
1869
|
+
await driver.write("posts", "p2", {
|
|
1870
|
+
slug: "p2",
|
|
1871
|
+
title: "Another post"
|
|
1872
|
+
});
|
|
1873
|
+
await rollback();
|
|
1874
|
+
const usersKeys = [...await driver.keys("users")];
|
|
1875
|
+
if (!equalsValue(usersKeys, ["u1"])) {
|
|
1876
|
+
await driver.close();
|
|
1877
|
+
yield {
|
|
1878
|
+
check: "snapshot-scoped-users",
|
|
1879
|
+
message: "a scoped snapshot must roll back only the named table",
|
|
1880
|
+
context: {
|
|
1881
|
+
table: "users",
|
|
1882
|
+
expected: ["u1"],
|
|
1883
|
+
actual: usersKeys
|
|
1245
1884
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (!deepEqual(postsKeys, ["p1", "p2"])) return findingOf("snapshot-scoped-posts", "a scoped snapshot must leave an unnamed table's mutations intact", {
|
|
1249
|
-
table: "posts",
|
|
1250
|
-
expected: ["p1", "p2"],
|
|
1251
|
-
actual: postsKeys
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1885
|
+
};
|
|
1886
|
+
break scopedPhase;
|
|
1254
1887
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1888
|
+
const postsKeys = [...await driver.keys("posts")].sort();
|
|
1889
|
+
await driver.close();
|
|
1890
|
+
if (!equalsValue(postsKeys, ["p1", "p2"])) yield {
|
|
1891
|
+
check: "snapshot-scoped-posts",
|
|
1892
|
+
message: "a scoped snapshot must leave an unnamed table's mutations intact",
|
|
1893
|
+
context: {
|
|
1894
|
+
table: "posts",
|
|
1895
|
+
expected: ["p1", "p2"],
|
|
1896
|
+
actual: postsKeys
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1259
1899
|
} catch (error) {
|
|
1260
|
-
yield
|
|
1900
|
+
yield {
|
|
1901
|
+
check: "snapshot-scoped",
|
|
1902
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1903
|
+
context: { error }
|
|
1904
|
+
};
|
|
1261
1905
|
}
|
|
1262
1906
|
}
|
|
1263
1907
|
/**
|
|
@@ -1316,40 +1960,424 @@ async function auditDriver(factory) {
|
|
|
1316
1960
|
for await (const finding of driverFindings(factory)) findings.push(finding);
|
|
1317
1961
|
return findings;
|
|
1318
1962
|
}
|
|
1963
|
+
//#endregion
|
|
1964
|
+
//#region src/core/TransactionIterator.ts
|
|
1319
1965
|
/**
|
|
1320
|
-
*
|
|
1966
|
+
* The internal continuation boundary for one transaction-scoped async iterable.
|
|
1321
1967
|
*
|
|
1322
1968
|
* @remarks
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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.
|
|
1972
|
+
*/
|
|
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.
|
|
1336
2040
|
*
|
|
1337
|
-
* @
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
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.
|
|
1341
2097
|
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
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.
|
|
1345
2102
|
*/
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
+
};
|
|
1353
2381
|
//#endregion
|
|
1354
2382
|
//#region src/core/Cursor.ts
|
|
1355
2383
|
/**
|
|
@@ -1362,14 +2390,21 @@ function generateUUID(random = Math.random) {
|
|
|
1362
2390
|
* skipped. `update` and `remove` act on the row at the current position.
|
|
1363
2391
|
*/
|
|
1364
2392
|
var Cursor = class {
|
|
1365
|
-
#table;
|
|
1366
2393
|
#keys;
|
|
2394
|
+
#read;
|
|
2395
|
+
#update;
|
|
2396
|
+
#remove;
|
|
2397
|
+
#track;
|
|
2398
|
+
#tail = Promise.resolve();
|
|
1367
2399
|
#index = -1;
|
|
1368
2400
|
#value;
|
|
1369
2401
|
#closed = false;
|
|
1370
|
-
constructor(
|
|
1371
|
-
this.#table = table;
|
|
2402
|
+
constructor(keys, read, update, remove, track) {
|
|
1372
2403
|
this.#keys = keys;
|
|
2404
|
+
this.#read = read;
|
|
2405
|
+
this.#update = update;
|
|
2406
|
+
this.#remove = remove;
|
|
2407
|
+
this.#track = track;
|
|
1373
2408
|
}
|
|
1374
2409
|
get value() {
|
|
1375
2410
|
return this.#value;
|
|
@@ -1380,11 +2415,31 @@ var Cursor = class {
|
|
|
1380
2415
|
get done() {
|
|
1381
2416
|
return this.#closed || this.#index >= this.#keys.length;
|
|
1382
2417
|
}
|
|
1383
|
-
|
|
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() {
|
|
1384
2432
|
if (this.#closed) return;
|
|
1385
2433
|
this.#index += 1;
|
|
1386
2434
|
while (this.#index < this.#keys.length) {
|
|
1387
|
-
|
|
2435
|
+
if (this.#closed) return;
|
|
2436
|
+
const key = this.#keys[this.#index];
|
|
2437
|
+
if (key === void 0) {
|
|
2438
|
+
this.#index += 1;
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const row = await this.#read(key);
|
|
2442
|
+
if (this.#closed) return;
|
|
1388
2443
|
if (row !== void 0) {
|
|
1389
2444
|
this.#value = row;
|
|
1390
2445
|
return;
|
|
@@ -1393,94 +2448,103 @@ var Cursor = class {
|
|
|
1393
2448
|
}
|
|
1394
2449
|
this.#value = void 0;
|
|
1395
2450
|
}
|
|
1396
|
-
async
|
|
2451
|
+
async #revise(changes) {
|
|
1397
2452
|
if (this.#closed || this.#value === void 0) return;
|
|
1398
2453
|
const key = this.#keys[this.#index];
|
|
1399
|
-
|
|
1400
|
-
|
|
2454
|
+
if (key === void 0) return;
|
|
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;
|
|
1401
2460
|
}
|
|
1402
|
-
async
|
|
2461
|
+
async #delete() {
|
|
1403
2462
|
if (this.#closed || this.#value === void 0) return;
|
|
1404
|
-
|
|
2463
|
+
const key = this.#keys[this.#index];
|
|
2464
|
+
if (key === void 0) return;
|
|
2465
|
+
await this.#remove(key);
|
|
2466
|
+
if (this.#closed) return;
|
|
1405
2467
|
this.#value = void 0;
|
|
1406
2468
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
this.#
|
|
2469
|
+
#queue(operation) {
|
|
2470
|
+
const result = this.#tail.then(operation);
|
|
2471
|
+
this.#tail = result.then(() => void 0, () => void 0);
|
|
2472
|
+
return result;
|
|
1410
2473
|
}
|
|
1411
2474
|
};
|
|
1412
2475
|
//#endregion
|
|
1413
|
-
//#region src/core/
|
|
2476
|
+
//#region src/core/DatabaseIterator.ts
|
|
1414
2477
|
/**
|
|
1415
|
-
*
|
|
2478
|
+
* The internal continuation admission boundary for a root database stream.
|
|
1416
2479
|
*
|
|
1417
2480
|
* @remarks
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
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.
|
|
1422
2485
|
*/
|
|
1423
|
-
var
|
|
1424
|
-
#
|
|
1425
|
-
#
|
|
1426
|
-
#
|
|
1427
|
-
constructor(
|
|
1428
|
-
this.#
|
|
1429
|
-
this.#
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
return this.#apply("equals", [value]);
|
|
1434
|
-
}
|
|
1435
|
-
not(value) {
|
|
1436
|
-
return this.#apply("not", [value]);
|
|
1437
|
-
}
|
|
1438
|
-
above(value) {
|
|
1439
|
-
return this.#apply("above", [value]);
|
|
1440
|
-
}
|
|
1441
|
-
below(value) {
|
|
1442
|
-
return this.#apply("below", [value]);
|
|
1443
|
-
}
|
|
1444
|
-
from(value) {
|
|
1445
|
-
return this.#apply("from", [value]);
|
|
1446
|
-
}
|
|
1447
|
-
to(value) {
|
|
1448
|
-
return this.#apply("to", [value]);
|
|
1449
|
-
}
|
|
1450
|
-
between(lower, upper) {
|
|
1451
|
-
return this.#apply("between", [lower, upper]);
|
|
1452
|
-
}
|
|
1453
|
-
like(pattern) {
|
|
1454
|
-
return this.#apply("like", [pattern]);
|
|
1455
|
-
}
|
|
1456
|
-
glob(pattern) {
|
|
1457
|
-
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;
|
|
1458
2496
|
}
|
|
1459
|
-
|
|
1460
|
-
return this.#
|
|
2497
|
+
next() {
|
|
2498
|
+
return this.#continue(() => this.#next());
|
|
1461
2499
|
}
|
|
1462
|
-
|
|
1463
|
-
return this.#
|
|
2500
|
+
return() {
|
|
2501
|
+
return this.#continue(() => this.#return());
|
|
1464
2502
|
}
|
|
1465
|
-
|
|
1466
|
-
return this.#
|
|
2503
|
+
throw(error) {
|
|
2504
|
+
return this.#continue(() => this.#throw(error));
|
|
1467
2505
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
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;
|
|
1470
2515
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
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;
|
|
1473
2537
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
2538
|
+
#continue(operation) {
|
|
2539
|
+
if (!this.#context.accepting) this.#cleanup();
|
|
2540
|
+
return this.#context.track(operation);
|
|
1476
2541
|
}
|
|
1477
|
-
#
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
});
|
|
2542
|
+
#cleanup() {
|
|
2543
|
+
if (this.#cleaned) return;
|
|
2544
|
+
this.#cleaned = true;
|
|
2545
|
+
try {
|
|
2546
|
+
(this.#source.return?.())?.catch(() => {});
|
|
2547
|
+
} catch {}
|
|
1484
2548
|
}
|
|
1485
2549
|
};
|
|
1486
2550
|
//#endregion
|
|
@@ -1489,12 +2553,9 @@ var Clause = class {
|
|
|
1489
2553
|
* A fluent query builder bound to one table.
|
|
1490
2554
|
*
|
|
1491
2555
|
* @remarks
|
|
1492
|
-
* Accumulates conditions, ordering, JS filters, and a page
|
|
1493
|
-
* mutates and returns the same instance
|
|
1494
|
-
*
|
|
1495
|
-
* table resolves; a `filter` predicate is applied in memory after the read and
|
|
1496
|
-
* before paging, so it composes with the rest without a backend ever seeing a
|
|
1497
|
-
* 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.
|
|
1498
2559
|
*/
|
|
1499
2560
|
var Query = class {
|
|
1500
2561
|
#table;
|
|
@@ -1506,47 +2567,34 @@ var Query = class {
|
|
|
1506
2567
|
constructor(table) {
|
|
1507
2568
|
this.#table = table;
|
|
1508
2569
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
}
|
|
1512
|
-
and(column) {
|
|
1513
|
-
return this.#clause(column, "and");
|
|
1514
|
-
}
|
|
1515
|
-
or(column) {
|
|
1516
|
-
return this.#clause(column, "or");
|
|
1517
|
-
}
|
|
1518
|
-
filter(predicate) {
|
|
1519
|
-
this.#filters.push(predicate);
|
|
2570
|
+
condition(input) {
|
|
2571
|
+
this.#conditions.push(input);
|
|
1520
2572
|
return this;
|
|
1521
2573
|
}
|
|
1522
|
-
|
|
1523
|
-
this.#orders.push(
|
|
1524
|
-
column,
|
|
1525
|
-
direction: "ascending"
|
|
1526
|
-
});
|
|
2574
|
+
order(input) {
|
|
2575
|
+
this.#orders.push(input);
|
|
1527
2576
|
return this;
|
|
1528
2577
|
}
|
|
1529
|
-
|
|
1530
|
-
this.#
|
|
1531
|
-
column,
|
|
1532
|
-
direction: "descending"
|
|
1533
|
-
});
|
|
2578
|
+
filter(predicate) {
|
|
2579
|
+
this.#filters.push(predicate);
|
|
1534
2580
|
return this;
|
|
1535
2581
|
}
|
|
1536
2582
|
limit(count) {
|
|
2583
|
+
validatePage({ limit: count });
|
|
1537
2584
|
this.#limit = count;
|
|
1538
2585
|
return this;
|
|
1539
2586
|
}
|
|
1540
2587
|
offset(count) {
|
|
2588
|
+
validatePage({ offset: count });
|
|
1541
2589
|
this.#offset = count;
|
|
1542
2590
|
return this;
|
|
1543
2591
|
}
|
|
1544
|
-
async
|
|
2592
|
+
async collect() {
|
|
1545
2593
|
if (this.#filters.length === 0) return this.#table.records({
|
|
1546
2594
|
conditions: this.#conditions,
|
|
1547
2595
|
order: this.#orders,
|
|
1548
|
-
limit: this.#limit,
|
|
1549
|
-
offset: this.#offset
|
|
2596
|
+
...this.#limit !== void 0 ? { limit: this.#limit } : {},
|
|
2597
|
+
...this.#offset !== void 0 ? { offset: this.#offset } : {}
|
|
1550
2598
|
});
|
|
1551
2599
|
const fetched = await this.#table.records({
|
|
1552
2600
|
conditions: this.#conditions,
|
|
@@ -1554,8 +2602,8 @@ var Query = class {
|
|
|
1554
2602
|
});
|
|
1555
2603
|
return this.#page(this.#filtered(fetched));
|
|
1556
2604
|
}
|
|
1557
|
-
async
|
|
1558
|
-
return (await this.
|
|
2605
|
+
async find() {
|
|
2606
|
+
return (await this.collect())[0];
|
|
1559
2607
|
}
|
|
1560
2608
|
async count() {
|
|
1561
2609
|
if (this.#filters.length === 0) return this.#table.count({ conditions: this.#conditions });
|
|
@@ -1563,24 +2611,17 @@ var Query = class {
|
|
|
1563
2611
|
return this.#filtered(fetched).length;
|
|
1564
2612
|
}
|
|
1565
2613
|
/**
|
|
1566
|
-
*
|
|
1567
|
-
* limit.
|
|
2614
|
+
* Lazily evaluate conditions, filters, offset, and limit.
|
|
1568
2615
|
*
|
|
1569
|
-
* @
|
|
1570
|
-
*
|
|
1571
|
-
* are evaluated one at a time). Same abort semantics as
|
|
1572
|
-
* `TableInterface.scan`: the signal (if any) is checked before each yield,
|
|
1573
|
-
* and breaking out early closes the underlying source.
|
|
1574
|
-
*
|
|
1575
|
-
* @param options - `signal` to cancel the iteration; checked before each yield
|
|
1576
|
-
* @returns An async iterable of matching rows
|
|
2616
|
+
* @param options - Optional abort options
|
|
2617
|
+
* @returns Matching rows in storage order
|
|
1577
2618
|
*/
|
|
1578
2619
|
async *stream(options) {
|
|
1579
2620
|
if (this.#filters.length === 0) {
|
|
1580
2621
|
yield* this.#table.scan({
|
|
1581
2622
|
conditions: this.#conditions,
|
|
1582
|
-
limit: this.#limit,
|
|
1583
|
-
offset: this.#offset
|
|
2623
|
+
...this.#limit !== void 0 ? { limit: this.#limit } : {},
|
|
2624
|
+
...this.#offset !== void 0 ? { offset: this.#offset } : {}
|
|
1584
2625
|
}, options);
|
|
1585
2626
|
return;
|
|
1586
2627
|
}
|
|
@@ -1608,24 +2649,6 @@ var Query = class {
|
|
|
1608
2649
|
if (this.#filters.length === 0) return this.#table.aggregate(operation, column, { conditions: this.#conditions });
|
|
1609
2650
|
return this.#table.records({ conditions: this.#conditions }).then((fetched) => computeAggregate(this.#filtered(fetched), operation, column));
|
|
1610
2651
|
}
|
|
1611
|
-
sum(column) {
|
|
1612
|
-
return this.aggregate("sum", column);
|
|
1613
|
-
}
|
|
1614
|
-
average(column) {
|
|
1615
|
-
return this.aggregate("average", column);
|
|
1616
|
-
}
|
|
1617
|
-
minimum(column) {
|
|
1618
|
-
return this.aggregate("minimum", column);
|
|
1619
|
-
}
|
|
1620
|
-
maximum(column) {
|
|
1621
|
-
return this.aggregate("maximum", column);
|
|
1622
|
-
}
|
|
1623
|
-
#clause(column, connector) {
|
|
1624
|
-
return new Clause((condition) => {
|
|
1625
|
-
this.#conditions.push(condition);
|
|
1626
|
-
return this;
|
|
1627
|
-
}, column, connector);
|
|
1628
|
-
}
|
|
1629
2652
|
#filtered(rows) {
|
|
1630
2653
|
let result = rows;
|
|
1631
2654
|
for (const predicate of this.#filters) result = result.filter(predicate);
|
|
@@ -1668,19 +2691,20 @@ var Table = class {
|
|
|
1668
2691
|
#contract;
|
|
1669
2692
|
#guard;
|
|
1670
2693
|
#generate;
|
|
2694
|
+
#context;
|
|
2695
|
+
#scope;
|
|
1671
2696
|
#emitter;
|
|
1672
|
-
constructor(ready, driver, name, key, contract, generate,
|
|
2697
|
+
constructor(ready, driver, name, key, contract, generate, error, context, scope) {
|
|
1673
2698
|
this.#ready = ready;
|
|
1674
2699
|
this.#driver = driver;
|
|
1675
2700
|
this.#name = name;
|
|
1676
2701
|
this.#key = key;
|
|
1677
2702
|
this.#contract = contract;
|
|
1678
|
-
this.#guard = contract.is;
|
|
1679
|
-
this.#generate = generate;
|
|
1680
|
-
this.#
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
});
|
|
2703
|
+
this.#guard = contract.is;
|
|
2704
|
+
this.#generate = generate;
|
|
2705
|
+
this.#context = context;
|
|
2706
|
+
this.#scope = scope;
|
|
2707
|
+
this.#emitter = new _orkestrel_emitter.Emitter({ ...error !== void 0 ? { error } : {} });
|
|
1684
2708
|
}
|
|
1685
2709
|
get emitter() {
|
|
1686
2710
|
return this.#emitter;
|
|
@@ -1694,58 +2718,77 @@ var Table = class {
|
|
|
1694
2718
|
get contract() {
|
|
1695
2719
|
return this.#contract;
|
|
1696
2720
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
+
});
|
|
1701
2727
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
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
|
+
});
|
|
1706
2734
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
+
});
|
|
1711
2741
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2742
|
+
keys() {
|
|
2743
|
+
return this.#track(async () => {
|
|
2744
|
+
await this.#ready();
|
|
2745
|
+
return this.#driver.keys(this.#name);
|
|
2746
|
+
});
|
|
1715
2747
|
}
|
|
1716
|
-
async records(
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
+
});
|
|
1723
2764
|
}
|
|
1724
2765
|
/**
|
|
1725
|
-
* Count rows matching `
|
|
2766
|
+
* Count contract-valid rows matching `input`'s conditions.
|
|
1726
2767
|
*
|
|
1727
2768
|
* @remarks
|
|
1728
|
-
*
|
|
1729
|
-
* contract guard
|
|
1730
|
-
*
|
|
1731
|
-
* `filterRows`-over-`#collect()` fallback count raw storage) — so it can
|
|
1732
|
-
* exceed `(await records(criteria)).length` when storage holds rows that
|
|
1733
|
-
* 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()`.
|
|
1734
2772
|
*
|
|
1735
|
-
* @param
|
|
2773
|
+
* @param input - Optional conditions to filter by (paging is ignored)
|
|
1736
2774
|
* @param options - `{ signal }` to abort
|
|
1737
|
-
* @returns The count of matching
|
|
2775
|
+
* @returns The count of matching contract-valid rows
|
|
1738
2776
|
*/
|
|
1739
|
-
async count(
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
+
});
|
|
1746
2789
|
}
|
|
1747
2790
|
/**
|
|
1748
|
-
* Compute an aggregate over `column` across rows matching `
|
|
2791
|
+
* Compute an aggregate over `column` across rows matching `input`'s
|
|
1749
2792
|
* conditions.
|
|
1750
2793
|
*
|
|
1751
2794
|
* @remarks
|
|
@@ -1757,115 +2800,138 @@ var Table = class {
|
|
|
1757
2800
|
*
|
|
1758
2801
|
* @param operation - The aggregate to compute
|
|
1759
2802
|
* @param column - The column to aggregate
|
|
1760
|
-
* @param
|
|
2803
|
+
* @param input - Optional conditions to filter by (paging is ignored)
|
|
1761
2804
|
* @param options - `{ signal }` to abort
|
|
1762
2805
|
* @returns The aggregate value, or `undefined` when undefined for the inputs
|
|
1763
2806
|
*/
|
|
1764
|
-
async aggregate(operation, column,
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
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
|
+
});
|
|
1772
2818
|
}
|
|
1773
2819
|
/**
|
|
1774
|
-
* Stream the table's rows matching `
|
|
2820
|
+
* Stream the table's rows matching `input`, applying offset/limit paging.
|
|
1775
2821
|
*
|
|
1776
2822
|
* @remarks
|
|
1777
|
-
* `
|
|
1778
|
-
* table's contract guard
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
* the contract guard runs, when storage holds rows that no longer conform
|
|
1782
|
-
* 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.
|
|
1783
2827
|
*
|
|
1784
|
-
* @param
|
|
2828
|
+
* @param input - Optional conditions plus offset/limit paging
|
|
1785
2829
|
* @param options - `{ signal }` to abort mid-stream
|
|
1786
2830
|
* @returns An async iterable of matching, guard-conforming rows
|
|
1787
2831
|
*/
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
if (this.#
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
return;
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
const offset = criteria?.offset ?? 0;
|
|
1801
|
-
const limit = criteria?.limit;
|
|
1802
|
-
let matched = 0;
|
|
1803
|
-
let yielded = 0;
|
|
1804
|
-
for await (const row of this.#driver.scan(this.#name)) {
|
|
1805
|
-
checkAbort(options?.signal);
|
|
1806
|
-
if (limit !== void 0 && yielded >= limit) break;
|
|
1807
|
-
if (conditions !== void 0 && conditions.length > 0 && !matchesCriteria(row, conditions)) continue;
|
|
1808
|
-
if (matched < offset) {
|
|
1809
|
-
matched += 1;
|
|
1810
|
-
continue;
|
|
1811
|
-
}
|
|
1812
|
-
matched += 1;
|
|
1813
|
-
const narrowed = this.#cast(row);
|
|
1814
|
-
if (narrowed !== void 0) {
|
|
1815
|
-
yielded += 1;
|
|
1816
|
-
yield narrowed;
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
async set(rows, options) {
|
|
1821
|
-
checkAbort(options?.signal);
|
|
1822
|
-
await this.#ready();
|
|
1823
|
-
if ((0, _orkestrel_contract.isArray)(rows)) return this.#each(rows, (row) => this.#put(row, false), options?.signal);
|
|
1824
|
-
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
|
+
});
|
|
1825
2844
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
+
});
|
|
1831
2851
|
}
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
+
});
|
|
1837
2858
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
+
});
|
|
1843
2865
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2866
|
+
clear() {
|
|
2867
|
+
return this.#track(async () => {
|
|
2868
|
+
await this.#ready();
|
|
2869
|
+
await this.#driver.clear(this.#name);
|
|
2870
|
+
this.#emitter.emit("clear");
|
|
2871
|
+
});
|
|
1848
2872
|
}
|
|
1849
2873
|
query() {
|
|
1850
2874
|
return new Query(this);
|
|
1851
2875
|
}
|
|
1852
|
-
|
|
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);
|
|
1853
2888
|
await this.#ready();
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
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
|
+
}
|
|
1857
2919
|
}
|
|
1858
|
-
async #each(
|
|
2920
|
+
async #each(elements, operation, signal) {
|
|
1859
2921
|
const results = [];
|
|
1860
|
-
for (const
|
|
2922
|
+
for (const element of elements) {
|
|
1861
2923
|
checkAbort(signal);
|
|
1862
|
-
results.push(await operation(
|
|
2924
|
+
results.push(await operation(element));
|
|
1863
2925
|
}
|
|
1864
2926
|
return results;
|
|
1865
2927
|
}
|
|
1866
2928
|
async #read(key) {
|
|
1867
2929
|
return this.#cast(await this.#driver.read(this.#name, key));
|
|
1868
2930
|
}
|
|
2931
|
+
async #readCursor(key) {
|
|
2932
|
+
await this.#ready();
|
|
2933
|
+
return this.#read(key);
|
|
2934
|
+
}
|
|
1869
2935
|
async #resolveOne(key) {
|
|
1870
2936
|
const row = await this.#read(key);
|
|
1871
2937
|
if (row === void 0) throw new DatabaseError("NOT_FOUND", `No row '${key}' in table '${this.#name}'`, {
|
|
@@ -1874,29 +2940,79 @@ var Table = class {
|
|
|
1874
2940
|
});
|
|
1875
2941
|
return row;
|
|
1876
2942
|
}
|
|
1877
|
-
async #put(row,
|
|
2943
|
+
async #put(row, insert, options) {
|
|
1878
2944
|
const validated = this.#validate(this.#prepare(row));
|
|
1879
2945
|
const key = this.#resolveKey(validated);
|
|
1880
|
-
if (
|
|
1881
|
-
|
|
1882
|
-
key
|
|
1883
|
-
});
|
|
1884
|
-
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);
|
|
1885
2948
|
this.#emitter.emit("write", key);
|
|
1886
2949
|
return key;
|
|
1887
2950
|
}
|
|
1888
|
-
async #updateOne(key, changes) {
|
|
2951
|
+
async #updateOne(key, changes, options) {
|
|
1889
2952
|
const existing = await this.#driver.read(this.#name, key);
|
|
1890
|
-
if (existing === void 0)
|
|
1891
|
-
|
|
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);
|
|
1892
2964
|
this.#emitter.emit("write", key);
|
|
1893
2965
|
return true;
|
|
1894
2966
|
}
|
|
1895
|
-
async #
|
|
1896
|
-
|
|
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);
|
|
1897
2973
|
if (removed) this.#emitter.emit("remove", key);
|
|
1898
2974
|
return removed;
|
|
1899
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
|
+
}
|
|
1900
3016
|
async #collect() {
|
|
1901
3017
|
const rows = [];
|
|
1902
3018
|
for await (const row of this.#driver.scan(this.#name)) rows.push(row);
|
|
@@ -1905,21 +3021,38 @@ var Table = class {
|
|
|
1905
3021
|
#prepare(row) {
|
|
1906
3022
|
if (!(0, _orkestrel_contract.isRecord)(row)) throw new DatabaseError("VALIDATION", `Row for table '${this.#name}' is not a record`, { table: this.#name });
|
|
1907
3023
|
const prepared = { ...row };
|
|
1908
|
-
if (prepared[this.#key] === void 0) {
|
|
1909
|
-
|
|
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}'`, {
|
|
1910
3028
|
table: this.#name,
|
|
1911
|
-
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
|
|
1912
3040
|
});
|
|
1913
|
-
prepared[this.#key] = this.#generate();
|
|
1914
3041
|
}
|
|
1915
3042
|
return prepared;
|
|
1916
3043
|
}
|
|
1917
3044
|
#validate(row) {
|
|
1918
3045
|
const parsed = this.#contract.parse(row);
|
|
1919
|
-
if (parsed === void 0 || !(0, _orkestrel_contract.isRecord)(parsed))
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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
|
+
}
|
|
1923
3056
|
return parsed;
|
|
1924
3057
|
}
|
|
1925
3058
|
#resolveKey(row) {
|
|
@@ -1933,265 +3066,186 @@ var Table = class {
|
|
|
1933
3066
|
#cast(row) {
|
|
1934
3067
|
return row !== void 0 && this.#guard(row) ? row : void 0;
|
|
1935
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
|
+
}
|
|
1936
3073
|
};
|
|
1937
3074
|
//#endregion
|
|
1938
|
-
//#region src/core/
|
|
3075
|
+
//#region src/core/DatabaseTransaction.ts
|
|
1939
3076
|
/**
|
|
1940
|
-
* A database
|
|
3077
|
+
* A table-only database view bound to one driver transaction scope.
|
|
3078
|
+
*
|
|
3079
|
+
* @typeParam T - The declared table shape map
|
|
1941
3080
|
*
|
|
1942
3081
|
* @remarks
|
|
1943
|
-
*
|
|
1944
|
-
*
|
|
1945
|
-
*
|
|
1946
|
-
*
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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.
|
|
1951
3123
|
*
|
|
1952
3124
|
* @remarks
|
|
1953
|
-
*
|
|
1954
|
-
*
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
* {@link DatabaseOptions.version} for the full reconciliation contract.
|
|
1958
|
-
* - **Observable (§13).** The owned {@link emitter} ({@link DatabaseEventMap}) carries the
|
|
1959
|
-
* connection + transaction lifecycle — `open` / `close` / `transaction` / `commit` /
|
|
1960
|
-
* `rollback` — for fire-and-forget observers, ALONGSIDE each table's per-row events. Every
|
|
1961
|
-
* event is emitted directly, strictly AFTER the relevant transition: `commit` only after
|
|
1962
|
-
* the scope succeeds, `rollback` only after every table is restored. The `rollback` emit
|
|
1963
|
-
* OBSERVES the propagated error — it never swallows it (the original throw propagates
|
|
1964
|
-
* exactly as before). The emitter isolates a listener throw and routes it to its `error`
|
|
1965
|
-
* handler (the `error` option), so observation can never reorder, throw into, or corrupt
|
|
1966
|
-
* 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.
|
|
1967
3129
|
*/
|
|
1968
3130
|
var Database = class Database {
|
|
1969
|
-
#
|
|
3131
|
+
#context;
|
|
1970
3132
|
#tables;
|
|
1971
|
-
#
|
|
3133
|
+
#primary;
|
|
1972
3134
|
#indexes;
|
|
1973
|
-
#name;
|
|
1974
3135
|
#generate;
|
|
1975
|
-
#version;
|
|
1976
|
-
#emitter;
|
|
1977
|
-
#status = "idle";
|
|
1978
|
-
#ready;
|
|
1979
3136
|
constructor(options) {
|
|
1980
|
-
this.#driver = options.driver;
|
|
1981
3137
|
this.#tables = options.tables;
|
|
1982
|
-
this.#
|
|
3138
|
+
this.#primary = options.primary ?? {};
|
|
1983
3139
|
this.#indexes = options.indexes ?? {};
|
|
1984
|
-
this.#
|
|
1985
|
-
this.#
|
|
1986
|
-
this.#
|
|
1987
|
-
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1988
|
-
on: options.on,
|
|
1989
|
-
error: options.error
|
|
1990
|
-
});
|
|
3140
|
+
this.#generate = options.generator;
|
|
3141
|
+
this.#context = new DatabaseContext(options);
|
|
3142
|
+
this.#context.register(this.#schema());
|
|
1991
3143
|
}
|
|
1992
3144
|
get emitter() {
|
|
1993
|
-
return this.#emitter;
|
|
3145
|
+
return this.#context.emitter;
|
|
1994
3146
|
}
|
|
1995
3147
|
get name() {
|
|
1996
|
-
return this.#name;
|
|
3148
|
+
return this.#context.name;
|
|
1997
3149
|
}
|
|
1998
3150
|
get status() {
|
|
1999
|
-
return this.#status;
|
|
3151
|
+
return this.#context.status;
|
|
2000
3152
|
}
|
|
2001
3153
|
table(name) {
|
|
2002
|
-
if (this.#status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#name}' is closed`, { name: this.#name });
|
|
2003
|
-
|
|
3154
|
+
if (this.#context.status === "closed") throw new DatabaseError("CLOSED", `Database '${this.#context.name}' is closed`, { name: this.#context.name });
|
|
3155
|
+
const columns = this.#columns(name);
|
|
3156
|
+
return this.#build(name, this.#key(name), (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.objectShape)(columns)));
|
|
2004
3157
|
}
|
|
2005
|
-
import(tables,
|
|
3158
|
+
import(tables, primary) {
|
|
2006
3159
|
return this.#spawn(tables, {
|
|
2007
|
-
...this.#
|
|
2008
|
-
...
|
|
3160
|
+
...this.#primary,
|
|
3161
|
+
...primary
|
|
2009
3162
|
});
|
|
2010
3163
|
}
|
|
2011
3164
|
export() {
|
|
2012
3165
|
const result = {};
|
|
2013
3166
|
for (const name of Object.keys(this.#tables)) {
|
|
2014
|
-
const columns = this.#
|
|
3167
|
+
const columns = this.#columns(name);
|
|
2015
3168
|
result[name] = {
|
|
2016
|
-
|
|
3169
|
+
primary: this.#key(name),
|
|
2017
3170
|
columns,
|
|
2018
3171
|
schema: (0, _orkestrel_contract.compileSchema)((0, _orkestrel_contract.objectShape)(columns))
|
|
2019
3172
|
};
|
|
2020
3173
|
}
|
|
2021
3174
|
return result;
|
|
2022
3175
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
3176
|
+
open() {
|
|
3177
|
+
return this.#context.open();
|
|
2025
3178
|
}
|
|
2026
|
-
|
|
2027
|
-
this.#
|
|
2028
|
-
this.#ready = void 0;
|
|
2029
|
-
await this.#driver.close();
|
|
2030
|
-
this.#emitter.emit("close");
|
|
3179
|
+
close() {
|
|
3180
|
+
return this.#context.close();
|
|
2031
3181
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
* When the driver implements the optional native {@link DriverInterface.transaction}
|
|
2038
|
-
* hook, that native `commit` / `rollback` handle drives the transaction; otherwise
|
|
2039
|
-
* the universal snapshot floor (`driver.snapshot()`) runs unchanged. Either path
|
|
2040
|
-
* emits the same `transaction` / `commit` / `rollback` lifecycle (AGENTS §13).
|
|
2041
|
-
* `options.signal` is checked ONCE at entry, before connecting or starting any
|
|
2042
|
-
* transactional work — an already-aborted signal throws `ABORTED` and neither the
|
|
2043
|
-
* native hook nor the snapshot floor is invoked. Nesting is unguarded and
|
|
2044
|
-
* unsupported exactly as before: this is a single-writer model, not reentrant.
|
|
2045
|
-
* On the native path, a `scope` throw rolls back via the native handle; a
|
|
2046
|
-
* native `commit` failure propagates as-is with no rollback attempt — the
|
|
2047
|
-
* engine owns transaction state after a failed COMMIT.
|
|
2048
|
-
*
|
|
2049
|
-
* @param scope - The transactional work to run
|
|
2050
|
-
* @param options - `{ signal }` to abort before the transaction starts
|
|
2051
|
-
* @returns The scope's resolved value
|
|
2052
|
-
* @throws An `ABORTED` {@link DatabaseError} when `options.signal` has already fired
|
|
2053
|
-
*/
|
|
2054
|
-
async transaction(scope, options) {
|
|
2055
|
-
checkAbort(options?.signal);
|
|
2056
|
-
await this.#connect();
|
|
2057
|
-
const native = await this.#driver.transaction?.();
|
|
2058
|
-
if (native !== void 0) {
|
|
2059
|
-
this.#emitter.emit("transaction");
|
|
2060
|
-
let value;
|
|
2061
|
-
try {
|
|
2062
|
-
value = await scope();
|
|
2063
|
-
} catch (error) {
|
|
2064
|
-
await native.rollback();
|
|
2065
|
-
this.#emitter.emit("rollback", error);
|
|
2066
|
-
throw error;
|
|
2067
|
-
}
|
|
2068
|
-
await native.commit();
|
|
2069
|
-
this.#emitter.emit("commit");
|
|
2070
|
-
return value;
|
|
2071
|
-
}
|
|
2072
|
-
const rollback = await this.#driver.snapshot();
|
|
2073
|
-
this.#emitter.emit("transaction");
|
|
2074
|
-
try {
|
|
2075
|
-
const value = await scope();
|
|
2076
|
-
this.#emitter.emit("commit");
|
|
2077
|
-
return value;
|
|
2078
|
-
} catch (error) {
|
|
2079
|
-
await rollback();
|
|
2080
|
-
this.#emitter.emit("rollback", error);
|
|
2081
|
-
throw error;
|
|
2082
|
-
}
|
|
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);
|
|
2083
3187
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
* resulting plan through the driver's optional `migrate` hook.
|
|
2087
|
-
*
|
|
2088
|
-
* @param deployed - The schema currently deployed, as {@link TableSchema}s
|
|
2089
|
-
* @param options - `{ signal }` to abort before the migration starts
|
|
2090
|
-
* @returns The applied {@link Migration} plan
|
|
2091
|
-
* @throws A `MIGRATION` {@link DatabaseError} when the driver does not
|
|
2092
|
-
* implement `migrate`, or when a step references an unknown table
|
|
2093
|
-
* (propagated from the driver)
|
|
2094
|
-
* @throws An `ABORTED` {@link DatabaseError} when `options.signal` has
|
|
2095
|
-
* already fired at entry
|
|
2096
|
-
*/
|
|
2097
|
-
async migrate(deployed, options) {
|
|
2098
|
-
checkAbort(options?.signal);
|
|
2099
|
-
await this.#connect();
|
|
2100
|
-
const plan = planMigration(deployed, this.#schema());
|
|
2101
|
-
if (this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, { name: this.#name });
|
|
2102
|
-
await this.#apply(plan);
|
|
2103
|
-
return plan;
|
|
3188
|
+
migrate(deployed, options) {
|
|
3189
|
+
return this.#context.migrate(deployed, options);
|
|
2104
3190
|
}
|
|
2105
3191
|
#build(name, key, contract) {
|
|
2106
|
-
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);
|
|
2107
3193
|
}
|
|
2108
|
-
#spawn(tables,
|
|
2109
|
-
return
|
|
2110
|
-
driver: this.#driver,
|
|
3194
|
+
#spawn(tables, primary) {
|
|
3195
|
+
return Database.#attach({
|
|
3196
|
+
driver: this.#context.driver,
|
|
2111
3197
|
tables,
|
|
2112
|
-
|
|
2113
|
-
name: this.#name,
|
|
2114
|
-
...this.#
|
|
2115
|
-
|
|
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);
|
|
2116
3204
|
}
|
|
2117
3205
|
#key(name) {
|
|
2118
|
-
return this.#
|
|
3206
|
+
return this.#primary[name] ?? "id";
|
|
3207
|
+
}
|
|
3208
|
+
#columns(name) {
|
|
3209
|
+
const columns = this.#tables[name];
|
|
3210
|
+
if (columns === void 0) throw new DatabaseError("NOT_FOUND", `Table '${name}' is not declared`, { table: name });
|
|
3211
|
+
return columns;
|
|
2119
3212
|
}
|
|
2120
3213
|
#schema() {
|
|
2121
3214
|
return Object.keys(this.#tables).map((name) => {
|
|
2122
|
-
const columns = this.#
|
|
3215
|
+
const columns = this.#columns(name);
|
|
2123
3216
|
return {
|
|
2124
3217
|
name,
|
|
2125
3218
|
primary: this.#key(name),
|
|
2126
|
-
columns: Object.
|
|
2127
|
-
const shape = columns[column];
|
|
2128
|
-
return {
|
|
2129
|
-
name: column,
|
|
2130
|
-
type: shapeToColumnType(shape),
|
|
2131
|
-
nullable: shape.type === "optional" || shape.type === "nullable"
|
|
2132
|
-
};
|
|
2133
|
-
}),
|
|
3219
|
+
columns: Object.entries(columns).map(([column, shape]) => shapeToColumnSchema(column, shape)),
|
|
2134
3220
|
indexes: this.#indexes[name] ?? []
|
|
2135
3221
|
};
|
|
2136
3222
|
});
|
|
2137
3223
|
}
|
|
2138
|
-
#
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
if (
|
|
2156
|
-
|
|
2157
|
-
stored: meta.version,
|
|
2158
|
-
declared: this.#version
|
|
2159
|
-
});
|
|
2160
|
-
if (meta.version < this.#version) {
|
|
2161
|
-
const plan = planMigration(meta.schema, declared, meta.version, this.#version);
|
|
2162
|
-
if (plan.steps.length > 0 && this.#driver.migrate === void 0) throw new DatabaseError("MIGRATION", `Database '${this.#name}' driver does not support migration`, {
|
|
2163
|
-
name: this.#name,
|
|
2164
|
-
stored: meta.version,
|
|
2165
|
-
declared: this.#version
|
|
2166
|
-
});
|
|
2167
|
-
await this.#apply(plan);
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
async #apply(plan) {
|
|
2171
|
-
const native = await this.#driver.transaction?.();
|
|
2172
|
-
if (native !== void 0) {
|
|
2173
|
-
try {
|
|
2174
|
-
await this.#driver.migrate?.(plan);
|
|
2175
|
-
await this.#stamp();
|
|
2176
|
-
} catch (error) {
|
|
2177
|
-
await native.rollback();
|
|
2178
|
-
throw error;
|
|
2179
|
-
}
|
|
2180
|
-
await native.commit();
|
|
2181
|
-
this.#emitter.emit("migrate", plan);
|
|
2182
|
-
return;
|
|
2183
|
-
}
|
|
2184
|
-
await this.#driver.migrate?.(plan);
|
|
2185
|
-
await this.#stamp();
|
|
2186
|
-
this.#emitter.emit("migrate", plan);
|
|
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;
|
|
2187
3243
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
};
|
|
2194
|
-
await this.#driver.stamp(meta);
|
|
3244
|
+
static #attach(options, context) {
|
|
3245
|
+
const database = new Database(options);
|
|
3246
|
+
database.#context = context;
|
|
3247
|
+
context.register(database.#schema());
|
|
3248
|
+
return database;
|
|
2195
3249
|
}
|
|
2196
3250
|
};
|
|
2197
3251
|
//#endregion
|
|
@@ -2207,29 +3261,54 @@ var Database = class Database {
|
|
|
2207
3261
|
* snapshot capture and restore — so a caller mutating a nested field of an input
|
|
2208
3262
|
* row, a returned row, or a row mutated in place between snapshot and rollback
|
|
2209
3263
|
* can never perturb stored state (AGENTS §11); a shallow `{ ...row }` spread
|
|
2210
|
-
* 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`
|
|
2211
3267
|
* clones every table to give transactions an exact rollback point. `scan` and
|
|
2212
3268
|
* `keys` yield in key order — sorted by the core {@link compareValues} total
|
|
2213
3269
|
* order, the same contract the SQLite (`ORDER BY`) and IndexedDB (key-ordered
|
|
2214
3270
|
* reads) backends honor, so an unordered read agrees across every backend rather
|
|
2215
3271
|
* than leaking Map insertion order. A persistent backend (IndexedDB, SQLite)
|
|
2216
|
-
* implements the same
|
|
3272
|
+
* implements the same required methods over real storage.
|
|
2217
3273
|
*/
|
|
2218
3274
|
var MemoryDriver = class {
|
|
2219
3275
|
#tables = /* @__PURE__ */ new Map();
|
|
2220
|
-
#
|
|
3276
|
+
#identities = /* @__PURE__ */ new Map();
|
|
3277
|
+
#schema = [];
|
|
3278
|
+
#metadata;
|
|
2221
3279
|
async open(schema) {
|
|
2222
|
-
|
|
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;
|
|
2223
3289
|
}
|
|
2224
3290
|
async close() {}
|
|
2225
3291
|
async read(table, key) {
|
|
2226
3292
|
const row = this.#store(table).get(key);
|
|
2227
3293
|
return row === void 0 ? void 0 : structuredClone(row);
|
|
2228
3294
|
}
|
|
2229
|
-
async write(table, key, row) {
|
|
2230
|
-
|
|
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)));
|
|
2231
3309
|
}
|
|
2232
|
-
async delete(table, key) {
|
|
3310
|
+
async delete(table, key, options) {
|
|
3311
|
+
checkAbort(options?.signal);
|
|
2233
3312
|
return this.#store(table).delete(key);
|
|
2234
3313
|
}
|
|
2235
3314
|
async keys(table) {
|
|
@@ -2248,17 +3327,17 @@ var MemoryDriver = class {
|
|
|
2248
3327
|
* @remarks
|
|
2249
3328
|
* Iterates the table's keys in the same key order `scan` and `keys` yield
|
|
2250
3329
|
* (sorted by {@link compareValues}), testing each row against
|
|
2251
|
-
* `
|
|
3330
|
+
* `input.conditions` (via {@link matchesQuery}) before counting it
|
|
2252
3331
|
* toward `offset` / `limit`. Both are applied lazily as matches are found —
|
|
2253
3332
|
* `offset` matches are skipped without being yielded, and iteration stops the
|
|
2254
3333
|
* instant `limit` yields have been produced, so a large table is never fully
|
|
2255
|
-
* walked for a small page. `
|
|
3334
|
+
* walked for a small page. `input.order` is IGNORED (the same contract as
|
|
2256
3335
|
* `TableInterface.scan` and `QueryInterface.stream`): streaming yields key
|
|
2257
3336
|
* order, sorted output is `records()`'s job. Rows yield copy-out (AGENTS
|
|
2258
3337
|
* §11), and an unknown table mirrors `scan`'s empty-yield behavior.
|
|
2259
3338
|
*
|
|
2260
3339
|
* @param table - The table to stream
|
|
2261
|
-
* @param
|
|
3340
|
+
* @param input - The filter / offset / limit to apply lazily
|
|
2262
3341
|
*
|
|
2263
3342
|
* @example
|
|
2264
3343
|
* ```ts
|
|
@@ -2267,18 +3346,22 @@ var MemoryDriver = class {
|
|
|
2267
3346
|
* }
|
|
2268
3347
|
* ```
|
|
2269
3348
|
*/
|
|
2270
|
-
|
|
3349
|
+
stream(table, input) {
|
|
3350
|
+
validatePage(input);
|
|
3351
|
+
return this.#stream(table, input);
|
|
3352
|
+
}
|
|
3353
|
+
async *#stream(table, input) {
|
|
2271
3354
|
const store = this.#store(table);
|
|
2272
|
-
const conditions =
|
|
2273
|
-
const offset =
|
|
2274
|
-
const limit =
|
|
3355
|
+
const conditions = input.conditions;
|
|
3356
|
+
const offset = input.offset ?? 0;
|
|
3357
|
+
const limit = input.limit;
|
|
2275
3358
|
let skipped = 0;
|
|
2276
3359
|
let yielded = 0;
|
|
2277
3360
|
for (const key of this.#ordered(table)) {
|
|
2278
3361
|
if (limit !== void 0 && yielded >= limit) return;
|
|
2279
3362
|
const row = store.get(key);
|
|
2280
3363
|
if (row === void 0) continue;
|
|
2281
|
-
if (conditions !== void 0 && conditions.length > 0 && !
|
|
3364
|
+
if (conditions !== void 0 && conditions.length > 0 && !matchesQuery(row, conditions)) continue;
|
|
2282
3365
|
if (skipped < offset) {
|
|
2283
3366
|
skipped += 1;
|
|
2284
3367
|
continue;
|
|
@@ -2294,115 +3377,169 @@ var MemoryDriver = class {
|
|
|
2294
3377
|
* Capture the current state and return a thunk that rolls back to it.
|
|
2295
3378
|
*
|
|
2296
3379
|
* @remarks
|
|
2297
|
-
*
|
|
2298
|
-
*
|
|
2299
|
-
*
|
|
2300
|
-
*
|
|
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.
|
|
2301
3385
|
*
|
|
2302
3386
|
* @param tables - The table names to scope the snapshot to; omitted captures every table
|
|
2303
3387
|
* @returns A thunk that restores the captured tables
|
|
2304
3388
|
*/
|
|
2305
3389
|
async snapshot(tables) {
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
for (const [key, row] of store) cloned.set(key, structuredClone(row));
|
|
2311
|
-
copy.set(name, cloned);
|
|
2312
|
-
}
|
|
2313
|
-
return async () => {
|
|
2314
|
-
this.#tables.clear();
|
|
2315
|
-
for (const [name, store] of copy) {
|
|
2316
|
-
const restored = /* @__PURE__ */ new Map();
|
|
2317
|
-
for (const [key, row] of store) restored.set(key, structuredClone(row));
|
|
2318
|
-
this.#tables.set(name, restored);
|
|
2319
|
-
}
|
|
2320
|
-
};
|
|
2321
|
-
}
|
|
2322
|
-
const copy = /* @__PURE__ */ new Map();
|
|
2323
|
-
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);
|
|
2324
3394
|
const store = this.#tables.get(name);
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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
|
+
});
|
|
2329
3404
|
}
|
|
2330
3405
|
return async () => {
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
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);
|
|
2335
3430
|
}
|
|
2336
3431
|
};
|
|
2337
3432
|
}
|
|
2338
3433
|
/**
|
|
2339
|
-
* Return the persisted {@link
|
|
3434
|
+
* Return the persisted {@link DriverMetadata}, or `undefined` when the store has
|
|
2340
3435
|
* never been stamped.
|
|
2341
3436
|
*
|
|
2342
3437
|
* @remarks
|
|
2343
3438
|
* In-process only — the metadata lives in this instance's memory, exactly
|
|
2344
|
-
* like the rest of this driver's storage.
|
|
2345
|
-
*
|
|
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.
|
|
2346
3442
|
*
|
|
2347
|
-
* @returns The last-stamped {@link
|
|
3443
|
+
* @returns The last-stamped {@link DriverMetadata}, or `undefined`
|
|
2348
3444
|
*/
|
|
2349
|
-
async
|
|
2350
|
-
return this.#
|
|
3445
|
+
async metadata() {
|
|
3446
|
+
return this.#metadata === void 0 ? void 0 : cloneDriverMetadata(this.#metadata);
|
|
2351
3447
|
}
|
|
2352
3448
|
/**
|
|
2353
|
-
* Persist
|
|
3449
|
+
* Persist an owned snapshot for a later `metadata()` to return.
|
|
2354
3450
|
*
|
|
2355
|
-
* @param
|
|
3451
|
+
* @param metadata - The {@link DriverMetadata} to persist
|
|
2356
3452
|
*/
|
|
2357
|
-
async stamp(
|
|
2358
|
-
this.#
|
|
3453
|
+
async stamp(metadata) {
|
|
3454
|
+
this.#metadata = cloneDriverMetadata(metadata);
|
|
2359
3455
|
}
|
|
2360
3456
|
/**
|
|
2361
3457
|
* Apply a {@link Migration} plan's steps against the in-memory store.
|
|
2362
3458
|
*
|
|
2363
3459
|
* @remarks
|
|
2364
|
-
*
|
|
2365
|
-
*
|
|
3460
|
+
* Steps apply against an isolated candidate. Rows, schema changes, and
|
|
3461
|
+
* optional metadata publish together only after the whole request succeeds.
|
|
2366
3462
|
*
|
|
2367
|
-
* @param
|
|
3463
|
+
* @param input - The migration plan and optional metadata to settle atomically
|
|
2368
3464
|
*/
|
|
2369
|
-
async migrate(
|
|
2370
|
-
|
|
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) {
|
|
2371
3513
|
case "table.add":
|
|
2372
|
-
if (!
|
|
3514
|
+
if (!tables.has(step.table.name)) tables.set(step.table.name, /* @__PURE__ */ new Map());
|
|
2373
3515
|
break;
|
|
2374
3516
|
case "table.remove":
|
|
2375
|
-
this.#require(step.table);
|
|
2376
|
-
|
|
3517
|
+
this.#require(tables, step.table);
|
|
3518
|
+
tables.delete(step.table);
|
|
2377
3519
|
break;
|
|
2378
3520
|
case "column.add":
|
|
2379
3521
|
case "column.remove": {
|
|
2380
|
-
const store = this.#require(step.table);
|
|
3522
|
+
const store = this.#require(tables, step.table);
|
|
2381
3523
|
const rows = [...store.entries()];
|
|
2382
3524
|
const migrated = migrateRows(rows.map(([, row]) => row), [step]);
|
|
2383
|
-
|
|
3525
|
+
for (const [index, [key]] of rows.entries()) {
|
|
3526
|
+
const row = migrated[index];
|
|
3527
|
+
if (row === void 0) throw new DatabaseError("MIGRATION", "migrate: transformed row is missing", {
|
|
3528
|
+
table: step.table,
|
|
3529
|
+
index
|
|
3530
|
+
});
|
|
3531
|
+
store.set(key, row);
|
|
3532
|
+
}
|
|
2384
3533
|
break;
|
|
2385
3534
|
}
|
|
2386
3535
|
case "index.add":
|
|
2387
|
-
case "index.remove":
|
|
2388
|
-
this.#require(step.table);
|
|
2389
|
-
break;
|
|
3536
|
+
case "index.remove": this.#require(tables, step.table);
|
|
2390
3537
|
}
|
|
2391
3538
|
}
|
|
2392
|
-
#ordered(table) {
|
|
2393
|
-
return [...this.#store(table).keys()].sort(compareValues);
|
|
2394
|
-
}
|
|
2395
|
-
#require(table) {
|
|
2396
|
-
const store = this.#tables.get(table);
|
|
2397
|
-
if (store === void 0) throw new DatabaseError("MIGRATION", `migrate: unknown table '${table}'`, { table });
|
|
2398
|
-
return store;
|
|
2399
|
-
}
|
|
2400
3539
|
#store(table) {
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
this.#tables.set(table, store);
|
|
2405
|
-
}
|
|
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 });
|
|
2406
3543
|
return store;
|
|
2407
3544
|
}
|
|
2408
3545
|
};
|
|
@@ -2417,10 +3554,10 @@ var MemoryDriver = class {
|
|
|
2417
3554
|
* level. The `const` type parameter captures the literal names and columns, so
|
|
2418
3555
|
* `db.table('users')` is checked against the schema and typed by `Infer` of its
|
|
2419
3556
|
* columns — no annotations. Name a non-`id` primary-key column per table via the
|
|
2420
|
-
* optional `
|
|
3557
|
+
* optional `primary` and `indexes` maps.
|
|
2421
3558
|
*
|
|
2422
|
-
* @param options - The driver,
|
|
2423
|
-
*
|
|
3559
|
+
* @param options - The driver, `tables`, and optional `primary`, `indexes`,
|
|
3560
|
+
* `name`, `generator`, `version`, and emitter hooks
|
|
2424
3561
|
* @returns A typed {@link DatabaseInterface}
|
|
2425
3562
|
*
|
|
2426
3563
|
* @example
|
|
@@ -2434,7 +3571,7 @@ var MemoryDriver = class {
|
|
|
2434
3571
|
* users: { id: stringShape(), age: integerShape() },
|
|
2435
3572
|
* posts: { slug: stringShape(), title: stringShape() },
|
|
2436
3573
|
* },
|
|
2437
|
-
*
|
|
3574
|
+
* primary: { posts: 'slug' },
|
|
2438
3575
|
* })
|
|
2439
3576
|
* await db.table('users').set({ id: 'u1', age: 36 }) // typed; coerced + validated
|
|
2440
3577
|
* ```
|
|
@@ -2455,40 +3592,48 @@ function createMemoryDriver() {
|
|
|
2455
3592
|
return new MemoryDriver();
|
|
2456
3593
|
}
|
|
2457
3594
|
//#endregion
|
|
2458
|
-
exports.Clause = Clause;
|
|
2459
|
-
exports.Cursor = Cursor;
|
|
2460
3595
|
exports.DEFAULT_PRIMARY = DEFAULT_PRIMARY;
|
|
2461
3596
|
exports.Database = Database;
|
|
2462
3597
|
exports.DatabaseError = DatabaseError;
|
|
2463
3598
|
exports.MAX_PATTERN_LENGTH = MAX_PATTERN_LENGTH;
|
|
2464
3599
|
exports.MemoryDriver = MemoryDriver;
|
|
2465
|
-
exports.
|
|
2466
|
-
exports.Table = Table;
|
|
2467
|
-
exports.UUID_BYTE_COUNT = UUID_BYTE_COUNT;
|
|
2468
|
-
exports.UUID_BYTE_RANGE = UUID_BYTE_RANGE;
|
|
2469
|
-
exports.applyCriteria = applyCriteria;
|
|
3600
|
+
exports.applyQuery = applyQuery;
|
|
2470
3601
|
exports.auditDriver = auditDriver;
|
|
3602
|
+
exports.bindRowKey = bindRowKey;
|
|
2471
3603
|
exports.checkAbort = checkAbort;
|
|
3604
|
+
exports.cloneDriverMetadata = cloneDriverMetadata;
|
|
3605
|
+
exports.cloneDriverSchema = cloneDriverSchema;
|
|
3606
|
+
exports.cloneMigrationInput = cloneMigrationInput;
|
|
2472
3607
|
exports.compareValues = compareValues;
|
|
2473
3608
|
exports.computeAggregate = computeAggregate;
|
|
2474
3609
|
exports.conformDriver = conformDriver;
|
|
2475
3610
|
exports.createDatabase = createDatabase;
|
|
2476
3611
|
exports.createMemoryDriver = createMemoryDriver;
|
|
2477
|
-
exports.deepEqual = deepEqual;
|
|
2478
3612
|
exports.driverFindings = driverFindings;
|
|
3613
|
+
exports.equalsValue = equalsValue;
|
|
2479
3614
|
exports.extractKey = extractKey;
|
|
2480
3615
|
exports.filterRows = filterRows;
|
|
2481
|
-
exports.
|
|
2482
|
-
exports.globMatch = globMatch;
|
|
3616
|
+
exports.isColumnSchema = isColumnSchema;
|
|
2483
3617
|
exports.isDatabaseError = isDatabaseError;
|
|
2484
|
-
exports.
|
|
2485
|
-
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;
|
|
2486
3625
|
exports.matchesCondition = matchesCondition;
|
|
2487
|
-
exports.
|
|
3626
|
+
exports.matchesGlobPattern = matchesGlobPattern;
|
|
3627
|
+
exports.matchesLikePattern = matchesLikePattern;
|
|
3628
|
+
exports.matchesQuery = matchesQuery;
|
|
3629
|
+
exports.matchesWildcardPattern = matchesWildcardPattern;
|
|
2488
3630
|
exports.migrateRows = migrateRows;
|
|
3631
|
+
exports.normalizeDriverSchema = normalizeDriverSchema;
|
|
2489
3632
|
exports.planMigration = planMigration;
|
|
2490
|
-
exports.
|
|
3633
|
+
exports.projectMigrationSchema = projectMigrationSchema;
|
|
3634
|
+
exports.shapeToColumnSchema = shapeToColumnSchema;
|
|
3635
|
+
exports.shapeToColumnStorage = shapeToColumnStorage;
|
|
2491
3636
|
exports.sortRows = sortRows;
|
|
2492
|
-
exports.
|
|
3637
|
+
exports.validatePage = validatePage;
|
|
2493
3638
|
|
|
2494
3639
|
//# sourceMappingURL=index.cjs.map
|