@minnowdb/core 0.10.0 → 0.10.2

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.
Files changed (37) hide show
  1. package/dist/engine/auto-store.d.ts +15 -3
  2. package/dist/engine/auto-store.js +48 -6
  3. package/dist/engine/client.js +23 -7
  4. package/dist/engine/database.js +347 -862
  5. package/dist/engine/index-terms.js +627 -0
  6. package/dist/engine/index.d.ts +1 -1
  7. package/dist/engine/index.js +2 -1
  8. package/dist/engine/live-maintenance.js +220 -0
  9. package/dist/engine/schema.js +2 -1
  10. package/dist/engine/sql-functions.js +3 -2
  11. package/dist/engine/sql-quote.js +6 -0
  12. package/dist/engine/vector.js +1 -3
  13. package/dist/engine/worker-host.js +2 -0
  14. package/dist/engine/worker-server.js +4 -5
  15. package/dist/engine/worker-store-auto.js +2 -2
  16. package/dist/engine/write-coordinator.js +21 -1
  17. package/dist/storage/indexeddb.d.ts +18 -0
  18. package/dist/storage/indexeddb.js +293 -211
  19. package/dist/storage/opfs/coordination-helpers.js +54 -0
  20. package/dist/storage/opfs/index.d.ts +1 -1
  21. package/dist/storage/opfs/index.js +3 -2
  22. package/dist/storage/opfs/leader.js +44 -5
  23. package/dist/storage/opfs/rpc.js +3 -1
  24. package/dist/storage/opfs/store.d.ts +12 -0
  25. package/dist/storage/opfs/store.js +59 -12
  26. package/dist/storage/toolkit/wal.js +16 -0
  27. package/dist/storage/toolkit/wire.js +3 -3
  28. package/dist/storage/types.d.ts +35 -4
  29. package/dist/storage/types.js +17 -4
  30. package/dist/testing/block-store-conformance.js +40 -1
  31. package/dist/testing/index.d.ts +1 -0
  32. package/dist/testing/index.js +9 -0
  33. package/dist/testing/interaction-simulator.d.ts +319 -0
  34. package/dist/testing/interaction-simulator.js +1631 -0
  35. package/dist/transactions/index.d.ts +7 -0
  36. package/dist/transactions/index.js +17 -3
  37. package/package.json +4 -1
@@ -0,0 +1,1631 @@
1
+ import { MinnowDatabase } from "../engine/database.js";
2
+ import { FaultInjectingBlockStore } from "./index.js";
3
+ class InteractionFailure extends Error {
4
+ index;
5
+ interaction;
6
+ trace;
7
+ constructor(message, index, interaction, trace, options) {
8
+ super(`${message}
9
+ at interaction ${String(index)} (${interaction.kind})
10
+ recent SQL:
11
+ ${trace.join("\n ")}`, options);
12
+ this.index = index;
13
+ this.interaction = interaction;
14
+ this.trace = trace;
15
+ this.name = "InteractionFailure";
16
+ }
17
+ }
18
+ const TEXT_POOL = [
19
+ "alpha",
20
+ "bravo",
21
+ "charlie",
22
+ "delta",
23
+ "echo",
24
+ "fox trot",
25
+ "Golf",
26
+ "hotel",
27
+ "o'clock",
28
+ "zulu",
29
+ "",
30
+ "42"
31
+ ];
32
+ const FAULT_POINTS = [
33
+ "beforeBlockWrite",
34
+ "afterBlockWrite",
35
+ "beforeTransactionCommit",
36
+ "afterTransactionCommit",
37
+ "crash"
38
+ ];
39
+ function createRandom(seed) {
40
+ const next = mulberry32(seed);
41
+ return {
42
+ next,
43
+ int: (maximum) => Math.floor(next() * maximum),
44
+ pick: (values) => {
45
+ const value = values[Math.floor(next() * values.length)];
46
+ if (value === void 0)
47
+ throw new Error("pick from an empty list");
48
+ return value;
49
+ },
50
+ chance: (probability) => next() < probability
51
+ };
52
+ }
53
+ function generateInteractionPlan(seed, options = {}) {
54
+ if (!Number.isSafeInteger(seed))
55
+ throw new RangeError("Plan seed must be a whole number");
56
+ const length = options.length ?? 120;
57
+ const connections = options.connections ?? 3;
58
+ const tableLimit = options.tables ?? 2;
59
+ const keySpace = options.keySpace ?? 24;
60
+ const faultPoints = options.faultPoints ?? FAULT_POINTS;
61
+ if (faultPoints.length === 0 || faultPoints.some((point) => !FAULT_POINTS.includes(point))) {
62
+ throw new RangeError("Plan faultPoints must name at least one known fault point");
63
+ }
64
+ checkRange("length", length, 1, 1e5);
65
+ checkRange("connections", connections, 1, 16);
66
+ checkRange("tables", tableLimit, 1, 8);
67
+ checkRange("keySpace", keySpace, 2, 1e4);
68
+ const random = createRandom(seed);
69
+ const interactions = [];
70
+ const live = /* @__PURE__ */ new Map();
71
+ const indexCount = /* @__PURE__ */ new Map();
72
+ let tableSerial = 0;
73
+ const connection = () => random.int(connections);
74
+ const anyTable = () => {
75
+ const tables = [...live.values()];
76
+ return tables.length === 0 ? void 0 : random.pick(tables);
77
+ };
78
+ const createTable = () => {
79
+ const columns = [];
80
+ const count = 2 + random.int(3);
81
+ for (let index = 0; index < count; index++) {
82
+ columns.push({
83
+ name: `c${String(index)}`,
84
+ type: random.pick(["integer", "real", "text", "boolean"]),
85
+ nullable: random.chance(0.5)
86
+ });
87
+ }
88
+ const table = { name: `t${String(tableSerial++)}`, columns };
89
+ live.set(table.name, table);
90
+ interactions.push({
91
+ kind: "createTable",
92
+ connection: connection(),
93
+ table,
94
+ expectExisting: false
95
+ });
96
+ if (random.chance(0.3)) {
97
+ interactions.push({
98
+ kind: "createTable",
99
+ connection: connection(),
100
+ table,
101
+ expectExisting: true
102
+ });
103
+ }
104
+ };
105
+ createTable();
106
+ for (let step = 0; step < length; step++) {
107
+ const table = anyTable();
108
+ if (table === void 0 || live.size < tableLimit && random.chance(0.04)) {
109
+ createTable();
110
+ continue;
111
+ }
112
+ const roll = random.next();
113
+ if (roll < 0.2) {
114
+ const rows = Array.from({ length: 1 + random.int(4) }, () => generateRow(random, table, keySpace));
115
+ interactions.push({
116
+ kind: "insert",
117
+ connection: connection(),
118
+ table: table.name,
119
+ rows: distinctIds(rows),
120
+ viaParameters: random.chance(0.5)
121
+ });
122
+ } else if (roll < 0.3) {
123
+ interactions.push({
124
+ kind: "update",
125
+ connection: connection(),
126
+ table: table.name,
127
+ predicate: generatePredicate(random, table, keySpace, 2),
128
+ assignments: generateAssignments(random, table)
129
+ });
130
+ } else if (roll < 0.37) {
131
+ interactions.push({
132
+ kind: "delete",
133
+ connection: connection(),
134
+ table: table.name,
135
+ predicate: generatePredicate(random, table, keySpace, 2)
136
+ });
137
+ } else if (roll < 0.52) {
138
+ interactions.push({
139
+ kind: "select",
140
+ connection: connection(),
141
+ table: table.name,
142
+ predicate: generatePredicate(random, table, keySpace, 3),
143
+ limit: random.chance(0.4) ? random.int(6) : null,
144
+ descending: random.chance(0.3)
145
+ });
146
+ } else if (roll < 0.6) {
147
+ interactions.push({
148
+ kind: "partition",
149
+ connection: connection(),
150
+ table: table.name,
151
+ predicate: generatePredicate(random, table, keySpace, 3)
152
+ });
153
+ } else if (roll < 0.65) {
154
+ interactions.push({
155
+ kind: "unionAll",
156
+ connection: connection(),
157
+ table: table.name,
158
+ left: generatePredicate(random, table, keySpace, 2),
159
+ right: generatePredicate(random, table, keySpace, 2)
160
+ });
161
+ } else if (roll < 0.69) {
162
+ const serial = (indexCount.get(table.name) ?? 0) + 1;
163
+ indexCount.set(table.name, serial);
164
+ const first = random.pick(table.columns).name;
165
+ const columns = random.chance(0.4) ? [
166
+ first,
167
+ random.pick(table.columns.filter((column) => column.name !== first).map((column) => column.name).concat("id"))
168
+ ] : [first];
169
+ interactions.push({
170
+ kind: "createIndex",
171
+ connection: connection(),
172
+ table: table.name,
173
+ name: `${table.name}_ix${String(serial)}`,
174
+ columns
175
+ });
176
+ } else if (roll < 0.72 && live.size > 1) {
177
+ live.delete(table.name);
178
+ interactions.push({
179
+ kind: "dropTable",
180
+ connection: connection(),
181
+ table: table.name,
182
+ expectMissing: false
183
+ });
184
+ if (random.chance(0.4)) {
185
+ interactions.push({
186
+ kind: "dropTable",
187
+ connection: connection(),
188
+ table: table.name,
189
+ expectMissing: true
190
+ });
191
+ }
192
+ } else if (roll < 0.79) {
193
+ const statements = [];
194
+ const count = 1 + random.int(3);
195
+ for (let index = 0; index < count; index++) {
196
+ const inner = random.next();
197
+ if (inner < 0.5) {
198
+ statements.push({
199
+ kind: "insert",
200
+ rows: distinctIds(Array.from({ length: 1 + random.int(2) }, () => generateRow(random, table, keySpace)))
201
+ });
202
+ } else if (inner < 0.8) {
203
+ statements.push({
204
+ kind: "update",
205
+ predicate: generatePredicate(random, table, keySpace, 2),
206
+ assignments: generateAssignments(random, table)
207
+ });
208
+ } else {
209
+ statements.push({
210
+ kind: "delete",
211
+ predicate: generatePredicate(random, table, keySpace, 1)
212
+ });
213
+ }
214
+ }
215
+ const owner = connection();
216
+ interactions.push({
217
+ kind: "transaction",
218
+ connection: owner,
219
+ observer: connections === 1 ? owner : (owner + 1 + random.int(connections - 1)) % connections,
220
+ table: table.name,
221
+ statements,
222
+ outcome: random.chance(0.5) ? "commit" : "rollback"
223
+ });
224
+ } else if (roll < 0.89 && connections > 1) {
225
+ const operations = [];
226
+ const used = /* @__PURE__ */ new Set();
227
+ const count = Math.min(connections * 2, 2 + random.int(connections * 2));
228
+ for (let index = 0; index < count; index++) {
229
+ const id = 1 + random.int(keySpace);
230
+ if (used.has(id))
231
+ continue;
232
+ used.add(id);
233
+ operations.push({
234
+ connection: index % connections,
235
+ mutation: generateKeyedMutation(random, table, id)
236
+ });
237
+ }
238
+ const readers = Array.from({ length: connections }, (_, index) => index).filter(() => random.chance(0.4));
239
+ interactions.push({ kind: "concurrent", table: table.name, operations, readers });
240
+ } else if (roll < 0.92) {
241
+ interactions.push({
242
+ kind: "fault",
243
+ connection: connection(),
244
+ table: table.name,
245
+ mutation: generateKeyedMutation(random, table, 1 + random.int(keySpace)),
246
+ point: random.pick(faultPoints)
247
+ });
248
+ } else if (roll < 0.95) {
249
+ interactions.push({ kind: "reopen", connection: connection() });
250
+ } else if (roll < 0.97) {
251
+ interactions.push({ kind: "maintenance", connection: connection(), table: table.name });
252
+ } else {
253
+ interactions.push({ kind: "checkpoint" });
254
+ }
255
+ if (step % 25 === 24)
256
+ interactions.push({ kind: "checkpoint" });
257
+ }
258
+ interactions.push({ kind: "checkpoint" });
259
+ return { version: 1, seed, connections, interactions };
260
+ }
261
+ function checkRange(name, value, minimum, maximum) {
262
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
263
+ throw new RangeError(`Plan ${name} must be a whole number from ${String(minimum)} through ${String(maximum)}`);
264
+ }
265
+ }
266
+ function distinctIds(rows) {
267
+ const seen = /* @__PURE__ */ new Set();
268
+ return rows.filter((row) => {
269
+ const id = idOf(row);
270
+ if (seen.has(id))
271
+ return false;
272
+ seen.add(id);
273
+ return true;
274
+ });
275
+ }
276
+ function generateValue(random, column) {
277
+ if (column.nullable && random.chance(0.15))
278
+ return null;
279
+ switch (column.type) {
280
+ case "integer":
281
+ return random.int(101) - 50;
282
+ case "real":
283
+ return (random.int(161) - 80) / 4;
284
+ case "text":
285
+ return random.pick(TEXT_POOL);
286
+ case "boolean":
287
+ return random.chance(0.5);
288
+ }
289
+ }
290
+ function generateRow(random, table, keySpace) {
291
+ const row = { id: 1 + random.int(keySpace) };
292
+ for (const column of table.columns)
293
+ row[column.name] = generateValue(random, column);
294
+ return row;
295
+ }
296
+ function generatePredicate(random, table, keySpace, depth) {
297
+ const roll = random.next();
298
+ if (depth > 0 && roll < 0.25) {
299
+ return {
300
+ kind: random.chance(0.5) ? "and" : "or",
301
+ left: generatePredicate(random, table, keySpace, depth - 1),
302
+ right: generatePredicate(random, table, keySpace, depth - 1)
303
+ };
304
+ }
305
+ if (depth > 0 && roll < 0.35) {
306
+ return { kind: "not", inner: generatePredicate(random, table, keySpace, depth - 1) };
307
+ }
308
+ if (roll < 0.4)
309
+ return { kind: "literal", value: random.chance(0.5) };
310
+ if (roll < 0.55) {
311
+ return { kind: "isNull", column: random.pick(table.columns).name, negated: random.chance(0.5) };
312
+ }
313
+ if (roll < 0.7) {
314
+ return {
315
+ kind: "compare",
316
+ column: "id",
317
+ op: random.pick(["=", "<>", "<", "<=", ">", ">="]),
318
+ value: 1 + random.int(keySpace)
319
+ };
320
+ }
321
+ const column = random.pick(table.columns);
322
+ const value = random.chance(0.05) ? null : generateValue(random, { ...column, nullable: false });
323
+ const operators = column.type === "boolean" ? ["=", "<>"] : ["=", "<>", "<", "<=", ">", ">="];
324
+ return { kind: "compare", column: column.name, op: random.pick(operators), value };
325
+ }
326
+ function generateAssignments(random, table) {
327
+ const count = 1 + random.int(Math.min(2, table.columns.length));
328
+ const chosen = /* @__PURE__ */ new Set();
329
+ const assignments = [];
330
+ while (assignments.length < count) {
331
+ const column = random.pick(table.columns);
332
+ if (chosen.has(column.name))
333
+ continue;
334
+ chosen.add(column.name);
335
+ if (column.type === "integer" && random.chance(0.5)) {
336
+ assignments.push({ kind: "increment", column: column.name, by: random.int(21) - 10 });
337
+ } else {
338
+ assignments.push({ kind: "set", column: column.name, value: generateValue(random, column) });
339
+ }
340
+ }
341
+ return assignments;
342
+ }
343
+ function generateKeyedMutation(random, table, id) {
344
+ const roll = random.next();
345
+ if (roll < 0.35)
346
+ return { kind: "insert", row: { ...generateRow(random, table, 1), id } };
347
+ if (roll < 0.6)
348
+ return { kind: "upsert", row: { ...generateRow(random, table, 1), id } };
349
+ if (roll < 0.85)
350
+ return { kind: "updateKey", id, assignments: generateAssignments(random, table) };
351
+ return { kind: "deleteKey", id };
352
+ }
353
+ function parseInteractionPlan(source) {
354
+ const value = JSON.parse(source);
355
+ if (!isRecord(value))
356
+ throw new TypeError("Interaction plan must be an object");
357
+ if (value.version !== 1)
358
+ throw new TypeError("Interaction plan version must be 1");
359
+ if (!Number.isSafeInteger(value.seed))
360
+ throw new TypeError("Interaction plan seed must be a whole number");
361
+ const connections = value.connections;
362
+ if (typeof connections !== "number" || !Number.isSafeInteger(connections) || connections < 1 || connections > 16) {
363
+ throw new TypeError("Interaction plan connection count is invalid");
364
+ }
365
+ const interactions = value.interactions;
366
+ if (!Array.isArray(interactions) || interactions.length > 5e5) {
367
+ throw new TypeError("Interaction plan interactions must be a bounded array");
368
+ }
369
+ interactions.forEach((interaction, index) => {
370
+ validateInteraction(interaction, connections, index);
371
+ });
372
+ return value;
373
+ }
374
+ function isRecord(value) {
375
+ return typeof value === "object" && value !== null && !Array.isArray(value);
376
+ }
377
+ function validateInteraction(value, connections, index) {
378
+ const where = `interaction ${String(index)}`;
379
+ if (!isRecord(value))
380
+ throw new TypeError(`${where} must be an object`);
381
+ const kind = value.kind;
382
+ const checkConnection = (field) => {
383
+ const number = value[field];
384
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0 || number >= connections) {
385
+ throw new TypeError(`${where} has an invalid ${field}`);
386
+ }
387
+ };
388
+ const checkTableName = (field = "table") => {
389
+ if (typeof value[field] !== "string" || !/^[a-z][a-z0-9_]*$/u.test(value[field])) {
390
+ throw new TypeError(`${where} has an invalid ${field}`);
391
+ }
392
+ };
393
+ switch (kind) {
394
+ case "createTable": {
395
+ checkConnection("connection");
396
+ validateTable(value.table, where);
397
+ if (typeof value.expectExisting !== "boolean")
398
+ throw new TypeError(`${where} expectExisting must be boolean`);
399
+ return;
400
+ }
401
+ case "insert": {
402
+ checkConnection("connection");
403
+ checkTableName();
404
+ if (!Array.isArray(value.rows) || value.rows.length > 1e3)
405
+ throw new TypeError(`${where} rows are unbounded`);
406
+ value.rows.forEach((row) => {
407
+ validateRow(row, where);
408
+ });
409
+ if (typeof value.viaParameters !== "boolean")
410
+ throw new TypeError(`${where} viaParameters must be boolean`);
411
+ return;
412
+ }
413
+ case "update": {
414
+ checkConnection("connection");
415
+ checkTableName();
416
+ validatePredicate(value.predicate, where, 0);
417
+ validateAssignments(value.assignments, where);
418
+ return;
419
+ }
420
+ case "delete":
421
+ case "partition": {
422
+ checkConnection("connection");
423
+ checkTableName();
424
+ validatePredicate(value.predicate, where, 0);
425
+ return;
426
+ }
427
+ case "select": {
428
+ checkConnection("connection");
429
+ checkTableName();
430
+ validatePredicate(value.predicate, where, 0);
431
+ const limit = value.limit;
432
+ if (limit !== null && (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 0)) {
433
+ throw new TypeError(`${where} limit is invalid`);
434
+ }
435
+ if (typeof value.descending !== "boolean")
436
+ throw new TypeError(`${where} descending must be boolean`);
437
+ return;
438
+ }
439
+ case "unionAll": {
440
+ checkConnection("connection");
441
+ checkTableName();
442
+ validatePredicate(value.left, where, 0);
443
+ validatePredicate(value.right, where, 0);
444
+ return;
445
+ }
446
+ case "createIndex": {
447
+ checkConnection("connection");
448
+ checkTableName();
449
+ checkTableName("name");
450
+ if (!Array.isArray(value.columns) || value.columns.length === 0 || value.columns.length > 4) {
451
+ throw new TypeError(`${where} index columns are invalid`);
452
+ }
453
+ return;
454
+ }
455
+ case "dropTable": {
456
+ checkConnection("connection");
457
+ checkTableName();
458
+ if (typeof value.expectMissing !== "boolean")
459
+ throw new TypeError(`${where} expectMissing must be boolean`);
460
+ return;
461
+ }
462
+ case "transaction": {
463
+ checkConnection("connection");
464
+ checkConnection("observer");
465
+ checkTableName();
466
+ if (!Array.isArray(value.statements) || value.statements.length === 0 || value.statements.length > 100) {
467
+ throw new TypeError(`${where} statements are invalid`);
468
+ }
469
+ for (const statement of value.statements) {
470
+ if (!isRecord(statement))
471
+ throw new TypeError(`${where} statement must be an object`);
472
+ if (statement.kind === "insert") {
473
+ if (!Array.isArray(statement.rows))
474
+ throw new TypeError(`${where} insert rows are invalid`);
475
+ statement.rows.forEach((row) => {
476
+ validateRow(row, where);
477
+ });
478
+ } else if (statement.kind === "update") {
479
+ validatePredicate(statement.predicate, where, 0);
480
+ validateAssignments(statement.assignments, where);
481
+ } else if (statement.kind === "delete") {
482
+ validatePredicate(statement.predicate, where, 0);
483
+ } else
484
+ throw new TypeError(`${where} statement kind is invalid`);
485
+ }
486
+ if (value.outcome !== "commit" && value.outcome !== "rollback") {
487
+ throw new TypeError(`${where} outcome is invalid`);
488
+ }
489
+ return;
490
+ }
491
+ case "concurrent": {
492
+ checkTableName();
493
+ if (!Array.isArray(value.operations) || value.operations.length > connections * 8) {
494
+ throw new TypeError(`${where} operations are unbounded`);
495
+ }
496
+ const ids = /* @__PURE__ */ new Set();
497
+ for (const operation of value.operations) {
498
+ if (!isRecord(operation))
499
+ throw new TypeError(`${where} operation must be an object`);
500
+ const target = operation.connection;
501
+ if (typeof target !== "number" || !Number.isSafeInteger(target) || target < 0 || target >= connections) {
502
+ throw new TypeError(`${where} operation connection is invalid`);
503
+ }
504
+ const id = validateKeyedMutation(operation.mutation, where);
505
+ if (ids.has(id))
506
+ throw new TypeError(`${where} concurrent operations must target distinct keys`);
507
+ ids.add(id);
508
+ }
509
+ if (!Array.isArray(value.readers))
510
+ throw new TypeError(`${where} readers must be an array`);
511
+ for (const reader of value.readers) {
512
+ if (typeof reader !== "number" || !Number.isSafeInteger(reader) || reader < 0 || reader >= connections) {
513
+ throw new TypeError(`${where} reader is invalid`);
514
+ }
515
+ }
516
+ return;
517
+ }
518
+ case "fault": {
519
+ checkConnection("connection");
520
+ checkTableName();
521
+ validateKeyedMutation(value.mutation, where);
522
+ if (typeof value.point !== "string" || !FAULT_POINTS.includes(value.point)) {
523
+ throw new TypeError(`${where} fault point is invalid`);
524
+ }
525
+ return;
526
+ }
527
+ case "reopen": {
528
+ checkConnection("connection");
529
+ return;
530
+ }
531
+ case "maintenance": {
532
+ checkConnection("connection");
533
+ checkTableName();
534
+ return;
535
+ }
536
+ case "checkpoint":
537
+ return;
538
+ default:
539
+ throw new TypeError(`${where} kind is invalid: ${String(kind)}`);
540
+ }
541
+ }
542
+ function validateTable(value, where) {
543
+ if (!isRecord(value) || typeof value.name !== "string" || !/^[a-z][a-z0-9_]*$/u.test(value.name)) {
544
+ throw new TypeError(`${where} table is invalid`);
545
+ }
546
+ if (!Array.isArray(value.columns) || value.columns.length === 0 || value.columns.length > 32) {
547
+ throw new TypeError(`${where} table columns are invalid`);
548
+ }
549
+ for (const column of value.columns) {
550
+ if (!isRecord(column) || typeof column.name !== "string" || !/^[a-z][a-z0-9_]*$/u.test(column.name) || column.name === "id" || !["integer", "real", "text", "boolean"].includes(column.type) || typeof column.nullable !== "boolean") {
551
+ throw new TypeError(`${where} table column is invalid`);
552
+ }
553
+ }
554
+ }
555
+ function validateRow(value, where) {
556
+ if (!isRecord(value))
557
+ throw new TypeError(`${where} row must be an object`);
558
+ if (typeof value.id !== "number" || !Number.isSafeInteger(value.id)) {
559
+ throw new TypeError(`${where} row id must be a whole number`);
560
+ }
561
+ for (const cell of Object.values(value))
562
+ validateValue(cell, where);
563
+ }
564
+ function validateValue(value, where) {
565
+ if (value === null || typeof value === "string" || typeof value === "boolean")
566
+ return;
567
+ if (typeof value === "number" && Number.isFinite(value))
568
+ return;
569
+ throw new TypeError(`${where} has an invalid value`);
570
+ }
571
+ function validatePredicate(value, where, depth) {
572
+ if (depth > 32)
573
+ throw new TypeError(`${where} predicate is too deep`);
574
+ if (!isRecord(value))
575
+ throw new TypeError(`${where} predicate must be an object`);
576
+ switch (value.kind) {
577
+ case "compare":
578
+ if (typeof value.column !== "string")
579
+ throw new TypeError(`${where} predicate column is invalid`);
580
+ if (!["=", "<>", "<", "<=", ">", ">="].includes(value.op)) {
581
+ throw new TypeError(`${where} predicate operator is invalid`);
582
+ }
583
+ validateValue(value.value, where);
584
+ return;
585
+ case "isNull":
586
+ if (typeof value.column !== "string" || typeof value.negated !== "boolean") {
587
+ throw new TypeError(`${where} IS NULL predicate is invalid`);
588
+ }
589
+ return;
590
+ case "and":
591
+ case "or":
592
+ validatePredicate(value.left, where, depth + 1);
593
+ validatePredicate(value.right, where, depth + 1);
594
+ return;
595
+ case "not":
596
+ validatePredicate(value.inner, where, depth + 1);
597
+ return;
598
+ case "literal":
599
+ if (typeof value.value !== "boolean")
600
+ throw new TypeError(`${where} literal predicate is invalid`);
601
+ return;
602
+ default:
603
+ throw new TypeError(`${where} predicate kind is invalid`);
604
+ }
605
+ }
606
+ function validateAssignments(value, where) {
607
+ if (!Array.isArray(value) || value.length === 0 || value.length > 32) {
608
+ throw new TypeError(`${where} assignments are invalid`);
609
+ }
610
+ for (const assignment of value) {
611
+ if (!isRecord(assignment) || typeof assignment.column !== "string" || assignment.column === "id") {
612
+ throw new TypeError(`${where} assignment is invalid`);
613
+ }
614
+ if (assignment.kind === "set")
615
+ validateValue(assignment.value, where);
616
+ else if (assignment.kind === "increment") {
617
+ if (typeof assignment.by !== "number" || !Number.isSafeInteger(assignment.by)) {
618
+ throw new TypeError(`${where} increment is invalid`);
619
+ }
620
+ } else
621
+ throw new TypeError(`${where} assignment kind is invalid`);
622
+ }
623
+ }
624
+ function validateKeyedMutation(value, where) {
625
+ if (!isRecord(value))
626
+ throw new TypeError(`${where} mutation must be an object`);
627
+ if (value.kind === "insert" || value.kind === "upsert") {
628
+ validateRow(value.row, where);
629
+ return value.row.id;
630
+ }
631
+ if (value.kind === "updateKey" || value.kind === "deleteKey") {
632
+ if (typeof value.id !== "number" || !Number.isSafeInteger(value.id)) {
633
+ throw new TypeError(`${where} mutation id is invalid`);
634
+ }
635
+ if (value.kind === "updateKey")
636
+ validateAssignments(value.assignments, where);
637
+ return value.id;
638
+ }
639
+ throw new TypeError(`${where} mutation kind is invalid`);
640
+ }
641
+ class ShadowModel {
642
+ tables = /* @__PURE__ */ new Map();
643
+ clone() {
644
+ const copy = new ShadowModel();
645
+ for (const [name, table] of this.tables) {
646
+ copy.tables.set(name, { definition: table.definition, rows: new Map(table.rows) });
647
+ }
648
+ return copy;
649
+ }
650
+ table(name) {
651
+ const table = this.tables.get(name);
652
+ if (table === void 0)
653
+ throw new Error(`Model has no table ${name}`);
654
+ return table;
655
+ }
656
+ columns(name) {
657
+ return ["id", ...this.table(name).definition.columns.map((column) => column.name)];
658
+ }
659
+ matching(name, predicate) {
660
+ return this.sorted(name).filter((row) => evaluate(predicate, row) === true);
661
+ }
662
+ sorted(name) {
663
+ return [...this.table(name).rows.values()].sort((left, right) => idOf(left) - idOf(right));
664
+ }
665
+ insert(name, rows) {
666
+ const table = this.table(name);
667
+ const staged = /* @__PURE__ */ new Set();
668
+ for (const row of rows) {
669
+ const id = idOf(row);
670
+ if (table.rows.has(id) || staged.has(id))
671
+ return false;
672
+ staged.add(id);
673
+ }
674
+ for (const row of rows)
675
+ table.rows.set(idOf(row), completeRow(table.definition, row));
676
+ return true;
677
+ }
678
+ update(name, predicate, assignments) {
679
+ const table = this.table(name);
680
+ let count = 0;
681
+ for (const row of this.matching(name, predicate)) {
682
+ table.rows.set(idOf(row), assign(row, assignments));
683
+ count++;
684
+ }
685
+ return count;
686
+ }
687
+ delete(name, predicate) {
688
+ const table = this.table(name);
689
+ let count = 0;
690
+ for (const row of this.matching(name, predicate)) {
691
+ table.rows.delete(idOf(row));
692
+ count++;
693
+ }
694
+ return count;
695
+ }
696
+ applyKeyed(name, mutation) {
697
+ const table = this.table(name);
698
+ switch (mutation.kind) {
699
+ case "insert":
700
+ return this.insert(name, [mutation.row]);
701
+ case "upsert":
702
+ table.rows.set(idOf(mutation.row), completeRow(table.definition, mutation.row));
703
+ return true;
704
+ case "updateKey": {
705
+ const row = table.rows.get(mutation.id);
706
+ if (row !== void 0)
707
+ table.rows.set(mutation.id, assign(row, mutation.assignments));
708
+ return true;
709
+ }
710
+ case "deleteKey":
711
+ table.rows.delete(mutation.id);
712
+ return true;
713
+ }
714
+ }
715
+ }
716
+ function idOf(row) {
717
+ const id = row.id;
718
+ if (typeof id !== "number")
719
+ throw new Error("Row without a numeric id");
720
+ return id;
721
+ }
722
+ function completeRow(definition, row) {
723
+ const complete = { id: idOf(row) };
724
+ for (const column of definition.columns)
725
+ complete[column.name] = row[column.name] ?? null;
726
+ return complete;
727
+ }
728
+ function assign(row, assignments) {
729
+ const next = { ...row };
730
+ for (const assignment of assignments) {
731
+ if (assignment.kind === "set")
732
+ next[assignment.column] = assignment.value;
733
+ else {
734
+ const current = row[assignment.column];
735
+ next[assignment.column] = typeof current === "number" ? current + assignment.by : null;
736
+ }
737
+ }
738
+ return next;
739
+ }
740
+ function compareValues(left, right) {
741
+ if (typeof left === "number" && typeof right === "number")
742
+ return left < right ? -1 : left > right ? 1 : 0;
743
+ if (typeof left === "string" && typeof right === "string")
744
+ return compareCodepoints(left, right);
745
+ if (typeof left === "boolean" && typeof right === "boolean")
746
+ return Number(left) - Number(right);
747
+ throw new Error(`Model cannot compare ${typeof left} with ${typeof right}`);
748
+ }
749
+ function compareCodepoints(left, right) {
750
+ let leftIndex = 0;
751
+ let rightIndex = 0;
752
+ while (leftIndex < left.length && rightIndex < right.length) {
753
+ const leftPoint = left.codePointAt(leftIndex) ?? 0;
754
+ const rightPoint = right.codePointAt(rightIndex) ?? 0;
755
+ if (leftPoint !== rightPoint)
756
+ return leftPoint - rightPoint;
757
+ leftIndex += leftPoint > 65535 ? 2 : 1;
758
+ rightIndex += rightPoint > 65535 ? 2 : 1;
759
+ }
760
+ return left.length - leftIndex - (right.length - rightIndex);
761
+ }
762
+ function evaluate(predicate, row) {
763
+ switch (predicate.kind) {
764
+ case "literal":
765
+ return predicate.value;
766
+ case "isNull": {
767
+ const isNull = (row[predicate.column] ?? null) === null;
768
+ return predicate.negated ? !isNull : isNull;
769
+ }
770
+ case "compare": {
771
+ const value = row[predicate.column] ?? null;
772
+ if (value === null || predicate.value === null)
773
+ return null;
774
+ const order = compareValues(value, predicate.value);
775
+ switch (predicate.op) {
776
+ case "=":
777
+ return order === 0;
778
+ case "<>":
779
+ return order !== 0;
780
+ case "<":
781
+ return order < 0;
782
+ case "<=":
783
+ return order <= 0;
784
+ case ">":
785
+ return order > 0;
786
+ case ">=":
787
+ return order >= 0;
788
+ }
789
+ }
790
+ case "not": {
791
+ const inner = evaluate(predicate.inner, row);
792
+ return inner === null ? null : !inner;
793
+ }
794
+ case "and": {
795
+ const left = evaluate(predicate.left, row);
796
+ const right = evaluate(predicate.right, row);
797
+ if (left === false || right === false)
798
+ return false;
799
+ if (left === null || right === null)
800
+ return null;
801
+ return true;
802
+ }
803
+ case "or": {
804
+ const left = evaluate(predicate.left, row);
805
+ const right = evaluate(predicate.right, row);
806
+ if (left === true || right === true)
807
+ return true;
808
+ if (left === null || right === null)
809
+ return null;
810
+ return false;
811
+ }
812
+ }
813
+ }
814
+ function quote(identifier) {
815
+ return `"${identifier.replaceAll('"', '""')}"`;
816
+ }
817
+ function renderLiteral(value) {
818
+ if (value === null)
819
+ return "NULL";
820
+ if (typeof value === "boolean")
821
+ return value ? "TRUE" : "FALSE";
822
+ if (typeof value === "number") {
823
+ if (Number.isInteger(value))
824
+ return String(value);
825
+ const text = value.toFixed(2).replace(/0$/u, "");
826
+ return text;
827
+ }
828
+ return `'${value.replaceAll("'", "''")}'`;
829
+ }
830
+ function renderPredicate(predicate) {
831
+ switch (predicate.kind) {
832
+ case "literal":
833
+ return predicate.value ? "TRUE" : "FALSE";
834
+ case "isNull":
835
+ return `${quote(predicate.column)} IS ${predicate.negated ? "NOT " : ""}NULL`;
836
+ case "compare":
837
+ return `${quote(predicate.column)} ${predicate.op} ${renderLiteral(predicate.value)}`;
838
+ case "not":
839
+ return `NOT (${renderPredicate(predicate.inner)})`;
840
+ case "and":
841
+ return `(${renderPredicate(predicate.left)} AND ${renderPredicate(predicate.right)})`;
842
+ case "or":
843
+ return `(${renderPredicate(predicate.left)} OR ${renderPredicate(predicate.right)})`;
844
+ }
845
+ }
846
+ function renderAssignments(assignments) {
847
+ return assignments.map((assignment) => assignment.kind === "set" ? `${quote(assignment.column)} = ${renderLiteral(assignment.value)}` : `${quote(assignment.column)} = ${quote(assignment.column)} + ${renderLiteral(assignment.by)}`).join(", ");
848
+ }
849
+ function sqlType(type) {
850
+ switch (type) {
851
+ case "integer":
852
+ return "INTEGER";
853
+ case "real":
854
+ return "DOUBLE PRECISION";
855
+ case "text":
856
+ return "TEXT";
857
+ case "boolean":
858
+ return "BOOLEAN";
859
+ }
860
+ }
861
+ function renderCreateTable(table) {
862
+ const columns = table.columns.map((column) => `${quote(column.name)} ${sqlType(column.type)}${column.nullable ? "" : " NOT NULL"}`);
863
+ return `CREATE TABLE ${quote(table.name)} (id INTEGER PRIMARY KEY, ${columns.join(", ")})`;
864
+ }
865
+ function renderInsert(table, columns, rows, viaParameters) {
866
+ const params = [];
867
+ const tuples = rows.map((row) => {
868
+ const cells = columns.map((column) => {
869
+ const value = row[column] ?? null;
870
+ if (!viaParameters)
871
+ return renderLiteral(value);
872
+ params.push(value);
873
+ return "?";
874
+ });
875
+ return `(${cells.join(", ")})`;
876
+ });
877
+ return {
878
+ sql: `INSERT INTO ${quote(table)} (${columns.map(quote).join(", ")}) VALUES ${tuples.join(", ")}`,
879
+ params
880
+ };
881
+ }
882
+ function renderKeyed(table, columns, mutation) {
883
+ switch (mutation.kind) {
884
+ case "insert":
885
+ return renderInsert(table, columns, [mutation.row], false).sql;
886
+ case "upsert": {
887
+ const insert = renderInsert(table, columns, [mutation.row], false).sql;
888
+ const updates = columns.filter((column) => column !== "id").map((column) => `${quote(column)} = excluded.${quote(column)}`).join(", ");
889
+ return `${insert} ON CONFLICT (id) DO UPDATE SET ${updates}`;
890
+ }
891
+ case "updateKey":
892
+ return `UPDATE ${quote(table)} SET ${renderAssignments(mutation.assignments)} WHERE id = ${String(mutation.id)}`;
893
+ case "deleteKey":
894
+ return `DELETE FROM ${quote(table)} WHERE id = ${String(mutation.id)}`;
895
+ }
896
+ }
897
+ async function runInteractionPlan(plan, driver, options = {}) {
898
+ parseInteractionPlan(JSON.stringify(plan));
899
+ const runner = new PlanRunner(plan, driver, options.traceLength ?? 40);
900
+ try {
901
+ return await runner.run();
902
+ } finally {
903
+ await driver.close?.();
904
+ }
905
+ }
906
+ class PlanRunner {
907
+ #plan;
908
+ #driver;
909
+ #traceLength;
910
+ #trace = [];
911
+ #connections = [];
912
+ #model = new ShadowModel();
913
+ #index = 0;
914
+ #statements = 0;
915
+ #queries = 0;
916
+ #acceptedWrites = 0;
917
+ #rejectedConflicts = 0;
918
+ #expectedFailures = 0;
919
+ #faultsInjected = 0;
920
+ #faultsSkipped = 0;
921
+ #reopens = 0;
922
+ #checkpoints = 0;
923
+ constructor(plan, driver, traceLength) {
924
+ this.#plan = plan;
925
+ this.#driver = driver;
926
+ this.#traceLength = traceLength;
927
+ }
928
+ async run() {
929
+ for (let index = 0; index < this.#plan.connections; index++) {
930
+ this.#connections.push(await this.#driver.open(index));
931
+ }
932
+ let judged = 0;
933
+ let stoppedBy;
934
+ for (const [index, interaction] of this.#plan.interactions.entries()) {
935
+ this.#index = index;
936
+ try {
937
+ await this.#step(interaction);
938
+ } catch (error) {
939
+ if (!isStoreUnresponsive(error))
940
+ throw error;
941
+ stoppedBy = describeError(error);
942
+ break;
943
+ }
944
+ judged += 1;
945
+ }
946
+ let rows = 0;
947
+ for (const table of this.#model.tables.values())
948
+ rows += table.rows.size;
949
+ return {
950
+ seed: this.#plan.seed,
951
+ interactions: judged,
952
+ statements: this.#statements,
953
+ queries: this.#queries,
954
+ acceptedWrites: this.#acceptedWrites,
955
+ rejectedConflicts: this.#rejectedConflicts,
956
+ expectedFailures: this.#expectedFailures,
957
+ faultsInjected: this.#faultsInjected,
958
+ faultsSkipped: this.#faultsSkipped,
959
+ reopens: this.#reopens,
960
+ checkpoints: this.#checkpoints,
961
+ tablesAtEnd: this.#model.tables.size,
962
+ rowsAtEnd: rows,
963
+ transientsAccepted: stoppedBy === void 0 ? 0 : 1,
964
+ stoppedBy
965
+ };
966
+ }
967
+ #connection(index) {
968
+ const connection = this.#connections[index];
969
+ if (connection === void 0)
970
+ throw new Error(`No connection ${String(index)}`);
971
+ return connection;
972
+ }
973
+ #record(sql) {
974
+ this.#trace.push(sql);
975
+ if (this.#trace.length > this.#traceLength)
976
+ this.#trace.shift();
977
+ }
978
+ #fail(message, cause) {
979
+ const interaction = this.#plan.interactions[this.#index];
980
+ if (interaction === void 0)
981
+ throw new Error(message, { cause });
982
+ throw new InteractionFailure(message, this.#index, interaction, this.#trace, { cause });
983
+ }
984
+ async #execute(connection, sql, params) {
985
+ this.#record(`[${String(connection)}] ${sql}${params === void 0 ? "" : ` -- ${JSON.stringify(params)}`}`);
986
+ this.#statements++;
987
+ return this.#connection(connection).execute(sql, params);
988
+ }
989
+ async #query(connection, sql) {
990
+ this.#record(`[${String(connection)}] ${sql}`);
991
+ this.#queries++;
992
+ return this.#connection(connection).query(sql);
993
+ }
994
+ async #step(interaction) {
995
+ switch (interaction.kind) {
996
+ case "createTable":
997
+ return this.#createTable(interaction);
998
+ case "insert":
999
+ return this.#insert(interaction);
1000
+ case "update":
1001
+ return this.#update(interaction);
1002
+ case "delete":
1003
+ return this.#delete(interaction);
1004
+ case "select":
1005
+ return this.#select(interaction);
1006
+ case "partition":
1007
+ return this.#partition(interaction);
1008
+ case "unionAll":
1009
+ return this.#unionAll(interaction);
1010
+ case "createIndex":
1011
+ return this.#createIndex(interaction);
1012
+ case "dropTable":
1013
+ return this.#dropTable(interaction);
1014
+ case "transaction":
1015
+ return this.#transaction(interaction);
1016
+ case "concurrent":
1017
+ return this.#concurrent(interaction);
1018
+ case "fault":
1019
+ return this.#fault(interaction);
1020
+ case "reopen":
1021
+ return this.#reopen(interaction.connection);
1022
+ case "maintenance":
1023
+ return this.#maintenance(interaction);
1024
+ case "checkpoint":
1025
+ return this.#checkpoint();
1026
+ }
1027
+ }
1028
+ async #createTable(interaction) {
1029
+ const sql = renderCreateTable(interaction.table);
1030
+ const exists = this.#model.tables.has(interaction.table.name);
1031
+ if (interaction.expectExisting !== exists) {
1032
+ this.#fail(`Plan expects table ${interaction.table.name} ${exists ? "absent" : "present"}`);
1033
+ }
1034
+ const outcome = await this.#attempt(interaction.connection, sql);
1035
+ if (exists) {
1036
+ if (outcome.error === void 0)
1037
+ this.#fail("double-create-failure: creating an existing table succeeded");
1038
+ this.#expectedFailures++;
1039
+ return;
1040
+ }
1041
+ if (outcome.error !== void 0)
1042
+ this.#fail(`CREATE TABLE failed: ${describeError(outcome.error)}`, outcome.error);
1043
+ this.#model.tables.set(interaction.table.name, {
1044
+ definition: interaction.table,
1045
+ rows: /* @__PURE__ */ new Map()
1046
+ });
1047
+ }
1048
+ async #insert(interaction) {
1049
+ const columns = this.#model.columns(interaction.table);
1050
+ const rendered = renderInsert(interaction.table, columns, interaction.rows, interaction.viaParameters);
1051
+ const before = this.#model.clone();
1052
+ const accepted = this.#model.insert(interaction.table, interaction.rows);
1053
+ const outcome = await this.#attempt(interaction.connection, rendered.sql, rendered.params);
1054
+ if (!accepted) {
1055
+ this.#model = before;
1056
+ if (outcome.error === void 0)
1057
+ this.#fail("insert-select: a duplicate primary key was accepted");
1058
+ if (!isUniqueViolation(outcome.error)) {
1059
+ this.#fail(`duplicate insert failed with the wrong error: ${describeError(outcome.error)}`, outcome.error);
1060
+ }
1061
+ this.#expectedFailures++;
1062
+ await this.#expectRows(interaction.connection, interaction.table, this.#model.sorted(interaction.table), "after a rejected insert");
1063
+ return;
1064
+ }
1065
+ if (outcome.error !== void 0)
1066
+ this.#fail(`INSERT failed: ${describeError(outcome.error)}`, outcome.error);
1067
+ if (outcome.result?.rowCount !== interaction.rows.length) {
1068
+ this.#fail(`INSERT reported ${String(outcome.result?.rowCount)} rows, expected ${String(interaction.rows.length)}`);
1069
+ }
1070
+ this.#acceptedWrites++;
1071
+ const ids = interaction.rows.map((row) => String(idOf(row))).join(", ");
1072
+ const predicate = `id IN (${ids})`;
1073
+ const expected = this.#model.sorted(interaction.table).filter((row) => interaction.rows.some((inserted) => idOf(inserted) === idOf(row)));
1074
+ await this.#expectRows(interaction.connection, interaction.table, expected, "insert-select", predicate);
1075
+ }
1076
+ async #update(interaction) {
1077
+ const before = this.#model.sorted(interaction.table);
1078
+ const expected = this.#model.update(interaction.table, interaction.predicate, interaction.assignments);
1079
+ const sql = `UPDATE ${quote(interaction.table)} SET ${renderAssignments(interaction.assignments)} WHERE ${renderPredicate(interaction.predicate)}`;
1080
+ const result = await this.#execute(interaction.connection, sql).catch((error) => this.#fail(`UPDATE failed: ${describeError(error)}`, error));
1081
+ if (result.rowCount !== expected) {
1082
+ this.#fail(`update-count: UPDATE reported ${String(result.rowCount)} rows, model matched ${String(expected)}
1083
+ model rows before: ${canonicalRows(before, this.#model.columns(interaction.table))}`);
1084
+ }
1085
+ this.#acceptedWrites++;
1086
+ await this.#expectRows(interaction.connection, interaction.table, this.#model.sorted(interaction.table), "after UPDATE");
1087
+ }
1088
+ async #delete(interaction) {
1089
+ const before = this.#model.sorted(interaction.table);
1090
+ const expected = this.#model.delete(interaction.table, interaction.predicate);
1091
+ const where = renderPredicate(interaction.predicate);
1092
+ const result = await this.#execute(interaction.connection, `DELETE FROM ${quote(interaction.table)} WHERE ${where}`).catch((error) => this.#fail(`DELETE failed: ${describeError(error)}`, error));
1093
+ if (result.rowCount !== expected) {
1094
+ this.#fail(`delete-count: DELETE reported ${String(result.rowCount)} rows, model matched ${String(expected)}
1095
+ model rows before: ${canonicalRows(before, this.#model.columns(interaction.table))}`);
1096
+ }
1097
+ this.#acceptedWrites++;
1098
+ const survivors = await this.#count(interaction.connection, interaction.table, where);
1099
+ if (survivors !== 0)
1100
+ this.#fail(`delete-select: ${String(survivors)} rows still match ${where}`);
1101
+ }
1102
+ async #select(interaction) {
1103
+ const matching = this.#model.matching(interaction.table, interaction.predicate);
1104
+ if (interaction.descending)
1105
+ matching.reverse();
1106
+ const expected = interaction.limit === null ? matching : matching.slice(0, interaction.limit);
1107
+ const order = `ORDER BY id${interaction.descending ? " DESC" : ""}`;
1108
+ const limit = interaction.limit === null ? "" : ` LIMIT ${String(interaction.limit)}`;
1109
+ const sql = `SELECT * FROM ${quote(interaction.table)} WHERE ${renderPredicate(interaction.predicate)} ${order}${limit}`;
1110
+ const result = await this.#query(interaction.connection, sql).catch((error) => this.#fail(`SELECT failed: ${describeError(error)}`, error));
1111
+ this.#compareRows(result, expected, this.#model.columns(interaction.table), interaction.limit === null ? "select" : "select-limit");
1112
+ }
1113
+ async #partition(interaction) {
1114
+ const where = renderPredicate(interaction.predicate);
1115
+ const total = await this.#count(interaction.connection, interaction.table, "TRUE");
1116
+ const positive = await this.#count(interaction.connection, interaction.table, where);
1117
+ const negative = await this.#count(interaction.connection, interaction.table, `NOT (${where})`);
1118
+ const unknown = await this.#count(interaction.connection, interaction.table, `(${where}) IS NULL`);
1119
+ if (positive + negative + unknown !== total) {
1120
+ this.#fail(`where-true-false-null: ${String(positive)} + ${String(negative)} + ${String(unknown)} != ${String(total)} for ${where}`);
1121
+ }
1122
+ const rows = this.#model.sorted(interaction.table);
1123
+ const modelPositive = rows.filter((row) => evaluate(interaction.predicate, row) === true).length;
1124
+ const modelUnknown = rows.filter((row) => evaluate(interaction.predicate, row) === null).length;
1125
+ if (total !== rows.length || positive !== modelPositive || unknown !== modelUnknown) {
1126
+ this.#fail(`where-true-false-null: engine ${String(positive)}/${String(negative)}/${String(unknown)} of ${String(total)}, model ${String(modelPositive)}/${String(rows.length - modelPositive - modelUnknown)}/${String(modelUnknown)} of ${String(rows.length)} for ${where}`);
1127
+ }
1128
+ }
1129
+ async #unionAll(interaction) {
1130
+ const left = renderPredicate(interaction.left);
1131
+ const right = renderPredicate(interaction.right);
1132
+ const table = quote(interaction.table);
1133
+ const sql = `SELECT id FROM ${table} WHERE ${left} UNION ALL SELECT id FROM ${table} WHERE ${right}`;
1134
+ const result = await this.#query(interaction.connection, sql).catch((error) => this.#fail(`UNION ALL failed: ${describeError(error)}`, error));
1135
+ const expected = this.#model.matching(interaction.table, interaction.left).length + this.#model.matching(interaction.table, interaction.right).length;
1136
+ if (result.rows.length !== expected) {
1137
+ this.#fail(`union-all-cardinality: ${String(result.rows.length)} rows, expected ${String(expected)}
1138
+ received ids: ${result.rows.map((row) => String(row.id)).join(", ")}
1139
+ model rows: ${canonicalRows(this.#model.sorted(interaction.table), this.#model.columns(interaction.table))}`);
1140
+ }
1141
+ }
1142
+ async #createIndex(interaction) {
1143
+ const sql = `CREATE INDEX ${quote(interaction.name)} ON ${quote(interaction.table)} (${interaction.columns.map(quote).join(", ")})`;
1144
+ await this.#execute(interaction.connection, sql).catch((error) => this.#fail(`CREATE INDEX failed: ${describeError(error)}`, error));
1145
+ await this.#expectRows(interaction.connection, interaction.table, this.#model.sorted(interaction.table), "after CREATE INDEX");
1146
+ }
1147
+ async #dropTable(interaction) {
1148
+ const exists = this.#model.tables.has(interaction.table);
1149
+ if (interaction.expectMissing === exists) {
1150
+ this.#fail(`Plan expects table ${interaction.table} ${exists ? "absent" : "present"}`);
1151
+ }
1152
+ const outcome = await this.#attempt(interaction.connection, `DROP TABLE ${quote(interaction.table)}`);
1153
+ if (!exists) {
1154
+ if (outcome.error === void 0)
1155
+ this.#fail("dropping a missing table succeeded");
1156
+ this.#expectedFailures++;
1157
+ return;
1158
+ }
1159
+ if (outcome.error !== void 0)
1160
+ this.#fail(`DROP TABLE failed: ${describeError(outcome.error)}`, outcome.error);
1161
+ this.#model.tables.delete(interaction.table);
1162
+ const select = await this.#attemptQuery(interaction.connection, `SELECT * FROM ${quote(interaction.table)}`);
1163
+ if (select.error === void 0)
1164
+ this.#fail("drop-select: a dropped table still answers queries");
1165
+ }
1166
+ async #transaction(interaction) {
1167
+ const committed = this.#model.clone();
1168
+ try {
1169
+ await this.#runTransaction(interaction, committed);
1170
+ return;
1171
+ } catch (error) {
1172
+ if (!(error instanceof RefusedTransaction))
1173
+ throw error;
1174
+ this.#model = committed;
1175
+ if (error.expired) {
1176
+ this.#expectedFailures++;
1177
+ await this.#execute(interaction.connection, "ROLLBACK").catch((rollback) => this.#fail(`ROLLBACK after an expired transaction failed: ${describeError(rollback)}`, rollback));
1178
+ } else {
1179
+ this.#rejectedConflicts++;
1180
+ }
1181
+ await this.#expectRows(interaction.connection, interaction.table, this.#model.sorted(interaction.table), `after a refused transaction (${error.reason})`);
1182
+ }
1183
+ }
1184
+ #refuseTransaction(step, error) {
1185
+ if (isExpiredTransaction(error))
1186
+ throw new RefusedTransaction("idle rollback", error);
1187
+ if (isConflict(error))
1188
+ throw new RefusedTransaction("lost commit race", error);
1189
+ this.#fail(`${step} failed: ${describeError(error)}`, error);
1190
+ }
1191
+ async #expectRowsInTransaction(connection, table, expected, context) {
1192
+ try {
1193
+ await this.#expectRows(connection, table, expected, context);
1194
+ } catch (error) {
1195
+ if (isExpiredTransaction(error) || isConflict(error))
1196
+ this.#refuseTransaction(context, error);
1197
+ throw error;
1198
+ }
1199
+ }
1200
+ async #runTransaction(interaction, committed) {
1201
+ const columns = this.#model.columns(interaction.table);
1202
+ await this.#execute(interaction.connection, "BEGIN").catch((error) => this.#refuseTransaction("BEGIN", error));
1203
+ let poisoned = false;
1204
+ for (const statement of interaction.statements) {
1205
+ if (statement.kind === "insert") {
1206
+ const accepted = this.#model.insert(interaction.table, statement.rows);
1207
+ const rendered = renderInsert(interaction.table, columns, statement.rows, true);
1208
+ const outcome = await this.#attempt(interaction.connection, rendered.sql, rendered.params);
1209
+ if (accepted && outcome.error !== void 0)
1210
+ this.#refuseTransaction("INSERT inside a transaction", outcome.error);
1211
+ if (!accepted) {
1212
+ if (outcome.error === void 0)
1213
+ this.#fail("a duplicate primary key was accepted inside a transaction");
1214
+ this.#expectedFailures++;
1215
+ poisoned = true;
1216
+ break;
1217
+ }
1218
+ } else if (statement.kind === "update") {
1219
+ this.#model.update(interaction.table, statement.predicate, statement.assignments);
1220
+ const sql = `UPDATE ${quote(interaction.table)} SET ${renderAssignments(statement.assignments)} WHERE ${renderPredicate(statement.predicate)}`;
1221
+ await this.#execute(interaction.connection, sql).catch((error) => this.#refuseTransaction("UPDATE inside a transaction", error));
1222
+ } else {
1223
+ this.#model.delete(interaction.table, statement.predicate);
1224
+ await this.#execute(interaction.connection, `DELETE FROM ${quote(interaction.table)} WHERE ${renderPredicate(statement.predicate)}`).catch((error) => this.#refuseTransaction("DELETE inside a transaction", error));
1225
+ }
1226
+ }
1227
+ if (!poisoned) {
1228
+ await this.#expectRowsInTransaction(interaction.connection, interaction.table, this.#model.sorted(interaction.table), "inside an open transaction (owner)");
1229
+ if (interaction.observer !== interaction.connection) {
1230
+ await this.#expectRows(interaction.observer, interaction.table, committed.sorted(interaction.table), "inside an open transaction (observer)");
1231
+ }
1232
+ }
1233
+ const end = poisoned ? "ROLLBACK" : interaction.outcome === "commit" ? "COMMIT" : "ROLLBACK";
1234
+ await this.#execute(interaction.connection, end).catch((error) => this.#refuseTransaction(end, error));
1235
+ if (end === "ROLLBACK")
1236
+ this.#model = committed;
1237
+ else
1238
+ this.#acceptedWrites++;
1239
+ await this.#expectRows(interaction.connection, interaction.table, this.#model.sorted(interaction.table), `after ${end}`);
1240
+ if (interaction.observer !== interaction.connection) {
1241
+ await this.#expectRows(interaction.observer, interaction.table, this.#model.sorted(interaction.table), `after ${end} (observer)`);
1242
+ }
1243
+ }
1244
+ async #concurrent(interaction) {
1245
+ const columns = this.#model.columns(interaction.table);
1246
+ const before = this.#model.clone();
1247
+ const writes = interaction.operations.map(async (operation) => {
1248
+ const sql = renderKeyed(interaction.table, columns, operation.mutation);
1249
+ const outcome = await this.#attempt(operation.connection, sql);
1250
+ return { operation, outcome };
1251
+ });
1252
+ const reads = interaction.readers.map(async (reader) => ({
1253
+ reader,
1254
+ outcome: await this.#attemptQuery(reader, `SELECT * FROM ${quote(interaction.table)} ORDER BY id`)
1255
+ }));
1256
+ const [writeOutcomes, readOutcomes] = await Promise.all([
1257
+ Promise.all(writes),
1258
+ Promise.all(reads)
1259
+ ]);
1260
+ const touched = /* @__PURE__ */ new Set();
1261
+ for (const { operation, outcome } of writeOutcomes) {
1262
+ const id = keyOf(operation.mutation);
1263
+ touched.add(id);
1264
+ const wouldAccept = before.clone().applyKeyed(interaction.table, operation.mutation);
1265
+ if (outcome.error === void 0) {
1266
+ if (!wouldAccept)
1267
+ this.#fail(`concurrent insert of an existing key ${String(id)} was accepted`);
1268
+ this.#model.applyKeyed(interaction.table, operation.mutation);
1269
+ this.#acceptedWrites++;
1270
+ } else if (isUniqueViolation(outcome.error)) {
1271
+ if (wouldAccept)
1272
+ this.#fail(`concurrent write on key ${String(id)} was refused as a duplicate: ${describeError(outcome.error)}`);
1273
+ this.#expectedFailures++;
1274
+ } else if (isConflict(outcome.error)) {
1275
+ this.#rejectedConflicts++;
1276
+ } else {
1277
+ this.#fail(`concurrent write on key ${String(id)} failed unexpectedly: ${describeError(outcome.error)}`);
1278
+ }
1279
+ }
1280
+ for (const { reader, outcome } of readOutcomes) {
1281
+ if (outcome.error !== void 0)
1282
+ this.#fail(`a read during concurrent writes failed on connection ${String(reader)}: ${describeError(outcome.error)}`);
1283
+ const seen = /* @__PURE__ */ new Map();
1284
+ for (const row of outcome.result?.rows ?? []) {
1285
+ const id = row.id;
1286
+ if (typeof id !== "number")
1287
+ this.#fail("a concurrent read returned a row without a numeric id");
1288
+ seen.set(id, canonical(row, columns));
1289
+ }
1290
+ const beforeRows = new Map(before.sorted(interaction.table).map((row) => [idOf(row), canonical(row, columns)]));
1291
+ const afterRows = new Map(this.#model.sorted(interaction.table).map((row) => [idOf(row), canonical(row, columns)]));
1292
+ const ids = /* @__PURE__ */ new Set([...beforeRows.keys(), ...afterRows.keys(), ...seen.keys()]);
1293
+ for (const id of ids) {
1294
+ const observed = seen.get(id);
1295
+ if (touched.has(id)) {
1296
+ const allowed = [beforeRows.get(id), afterRows.get(id)];
1297
+ if (!allowed.includes(observed)) {
1298
+ this.#fail(`concurrent-explicability: reader ${String(reader)} saw key ${String(id)} as ${observed ?? "absent"}, neither before (${beforeRows.get(id) ?? "absent"}) nor after (${afterRows.get(id) ?? "absent"})`);
1299
+ }
1300
+ } else if (observed !== beforeRows.get(id)) {
1301
+ this.#fail(`concurrent-explicability: reader ${String(reader)} saw untouched key ${String(id)} as ${observed ?? "absent"}, expected ${beforeRows.get(id) ?? "absent"}`);
1302
+ }
1303
+ }
1304
+ }
1305
+ for (let connection = 0; connection < this.#plan.connections; connection++) {
1306
+ await this.#expectRows(connection, interaction.table, this.#model.sorted(interaction.table), "after a concurrent round");
1307
+ }
1308
+ }
1309
+ async #fault(interaction) {
1310
+ const connection = this.#connection(interaction.connection);
1311
+ const columns = this.#model.columns(interaction.table);
1312
+ const sql = renderKeyed(interaction.table, columns, interaction.mutation);
1313
+ const before = this.#model.clone();
1314
+ const after = this.#model.clone();
1315
+ const accepted = after.applyKeyed(interaction.table, interaction.mutation);
1316
+ if (!accepted) {
1317
+ const outcome2 = await this.#attempt(interaction.connection, sql);
1318
+ if (outcome2.error === void 0)
1319
+ this.#fail("a duplicate insert was accepted during a fault step");
1320
+ this.#expectedFailures++;
1321
+ return;
1322
+ }
1323
+ let outcome;
1324
+ if (interaction.point === "crash") {
1325
+ if (connection.crash === void 0) {
1326
+ this.#faultsSkipped++;
1327
+ return;
1328
+ }
1329
+ const pending = this.#attempt(interaction.connection, sql);
1330
+ await connection.crash();
1331
+ outcome = await pending;
1332
+ if (outcome.error === void 0) {
1333
+ this.#model = after;
1334
+ }
1335
+ } else {
1336
+ const faults = this.#driver.faults;
1337
+ if (faults === void 0) {
1338
+ this.#faultsSkipped++;
1339
+ return;
1340
+ }
1341
+ faults.arm(interaction.point, 1);
1342
+ try {
1343
+ outcome = await this.#attempt(interaction.connection, sql);
1344
+ } finally {
1345
+ faults.disarm();
1346
+ }
1347
+ if (!faults.fired()) {
1348
+ if (outcome.error !== void 0)
1349
+ this.#fail(`mutation failed without its fault firing: ${describeError(outcome.error)}`, outcome.error);
1350
+ this.#model = after;
1351
+ this.#acceptedWrites++;
1352
+ return;
1353
+ }
1354
+ }
1355
+ this.#faultsInjected++;
1356
+ if (outcome.error !== void 0 && !isUnknownOutcome(outcome.error) && !isInjectedFault(outcome.error) && !isConflict(outcome.error)) {
1357
+ this.#fail(`fault ${interaction.point} surfaced an unexpected error: ${describeError(outcome.error)}`);
1358
+ }
1359
+ await this.#reopenConnection(interaction.connection);
1360
+ const actual = await this.#read(interaction.connection, interaction.table);
1361
+ const beforeRows = canonicalRows(before.sorted(interaction.table), columns);
1362
+ const afterRows = canonicalRows(after.sorted(interaction.table), columns);
1363
+ const observed = canonicalRows(actual, columns);
1364
+ if (observed === afterRows)
1365
+ this.#model = after;
1366
+ else if (observed === beforeRows) {
1367
+ if (outcome.error === void 0)
1368
+ this.#fail(`fault-atomicity: ${interaction.point} reported success but the mutation is not durable`);
1369
+ this.#model = before;
1370
+ } else {
1371
+ this.#fail(`fault-atomicity: after ${interaction.point} the table is neither before nor after the mutation
1372
+ observed ${observed}
1373
+ before ${beforeRows}
1374
+ after ${afterRows}`);
1375
+ }
1376
+ }
1377
+ async #reopenConnection(index) {
1378
+ this.#record(`[${String(index)}] -- reopen`);
1379
+ await this.#connection(index).reopen();
1380
+ this.#reopens++;
1381
+ }
1382
+ async #reopen(index) {
1383
+ await this.#reopenConnection(index);
1384
+ for (const table of this.#model.tables.keys()) {
1385
+ await this.#expectRows(index, table, this.#model.sorted(table), "after reopen");
1386
+ }
1387
+ }
1388
+ async #maintenance(interaction) {
1389
+ const connection = this.#connection(interaction.connection);
1390
+ if (connection.maintain === void 0)
1391
+ return;
1392
+ this.#record(`[${String(interaction.connection)}] -- maintenance ${interaction.table}`);
1393
+ await connection.maintain(interaction.table).catch((error) => this.#fail(`maintenance failed: ${describeError(error)}`, error));
1394
+ await this.#expectRows(interaction.connection, interaction.table, this.#model.sorted(interaction.table), "after maintenance");
1395
+ }
1396
+ async #checkpoint() {
1397
+ this.#checkpoints++;
1398
+ for (let connection = 0; connection < this.#plan.connections; connection++) {
1399
+ for (const table of this.#model.tables.keys()) {
1400
+ await this.#expectRows(connection, table, this.#model.sorted(table), "at checkpoint");
1401
+ }
1402
+ }
1403
+ }
1404
+ async #count(connection, table, where) {
1405
+ const result = await this.#query(connection, `SELECT COUNT(*) AS n FROM ${quote(table)} WHERE ${where}`).catch((error) => this.#fail(`COUNT failed: ${describeError(error)}`, error));
1406
+ const value = result.rows[0]?.n;
1407
+ if (typeof value === "number")
1408
+ return value;
1409
+ if (typeof value === "bigint")
1410
+ return Number(value);
1411
+ this.#fail(`COUNT(*) returned ${JSON.stringify(value)}`);
1412
+ }
1413
+ async #read(connection, table, where = "TRUE") {
1414
+ const result = await this.#query(connection, `SELECT * FROM ${quote(table)} WHERE ${where} ORDER BY id`).catch((error) => this.#fail(`SELECT failed: ${describeError(error)}`, error));
1415
+ return [...result.rows];
1416
+ }
1417
+ async #expectRows(connection, table, expected, context, where = "TRUE") {
1418
+ const actual = await this.#read(connection, table, where);
1419
+ const columns = this.#model.columns(table);
1420
+ const observed = canonicalRows(actual, columns);
1421
+ const wanted = canonicalRows(expected, columns);
1422
+ if (observed !== wanted) {
1423
+ this.#fail(`${context}: connection ${String(connection)} disagrees with the model for ${table}
1424
+ observed ${observed}
1425
+ expected ${wanted}`);
1426
+ }
1427
+ }
1428
+ #compareRows(result, expected, columns, property) {
1429
+ const missing = columns.filter((column) => !result.columns.includes(column));
1430
+ if (missing.length > 0)
1431
+ this.#fail(`${property}: result lacks columns ${missing.join(", ")}`);
1432
+ const observed = result.rows.map((row) => canonical(row, columns)).join("\n");
1433
+ const wanted = expected.map((row) => canonical(row, columns)).join("\n");
1434
+ if (observed !== wanted) {
1435
+ this.#fail(`${property}: rows differ
1436
+ observed ${observed.replaceAll("\n", " | ")}
1437
+ expected ${wanted.replaceAll("\n", " | ")}`);
1438
+ }
1439
+ }
1440
+ async #attempt(connection, sql, params) {
1441
+ try {
1442
+ return { result: await this.#execute(connection, sql, params) };
1443
+ } catch (error) {
1444
+ return { error };
1445
+ }
1446
+ }
1447
+ async #attemptQuery(connection, sql) {
1448
+ try {
1449
+ return { result: await this.#query(connection, sql) };
1450
+ } catch (error) {
1451
+ return { error };
1452
+ }
1453
+ }
1454
+ }
1455
+ function keyOf(mutation) {
1456
+ return mutation.kind === "insert" || mutation.kind === "upsert" ? idOf(mutation.row) : mutation.id;
1457
+ }
1458
+ function canonical(row, columns) {
1459
+ return columns.map((column) => `${column}=${canonicalValue(row[column])}`).join(",");
1460
+ }
1461
+ function canonicalValue(value) {
1462
+ if (value === null || value === void 0)
1463
+ return "NULL";
1464
+ if (typeof value === "bigint")
1465
+ return String(value);
1466
+ if (typeof value === "number")
1467
+ return Object.is(value, -0) ? "0" : String(value);
1468
+ if (typeof value === "boolean")
1469
+ return value ? "true" : "false";
1470
+ if (typeof value === "string")
1471
+ return JSON.stringify(value);
1472
+ return `?${typeof value}`;
1473
+ }
1474
+ function canonicalRows(rows, columns) {
1475
+ return rows.map((row) => canonical(row, columns)).join(" | ");
1476
+ }
1477
+ function describeError(error) {
1478
+ if (error instanceof Error)
1479
+ return `${error.name}: ${error.message}`;
1480
+ if (isRecord(error) && typeof error.message === "string") {
1481
+ return `${typeof error.name === "string" ? error.name : "Error"}: ${error.message}`;
1482
+ }
1483
+ return String(error);
1484
+ }
1485
+ function errorText(error) {
1486
+ return describeError(error);
1487
+ }
1488
+ function isUniqueViolation(error) {
1489
+ return /UniqueConstraint|duplicate value|unique/iu.test(errorText(error));
1490
+ }
1491
+ function isConflict(error) {
1492
+ return /WriteConflict|Manifest changed|conflict/iu.test(errorText(error));
1493
+ }
1494
+ function isExpiredTransaction(error) {
1495
+ return /TransactionExpired|transaction expired/iu.test(errorText(error));
1496
+ }
1497
+ class RefusedTransaction extends Error {
1498
+ reason;
1499
+ constructor(reason, cause) {
1500
+ super(`SQL transaction refused: ${reason}`, { cause });
1501
+ this.reason = reason;
1502
+ this.name = "RefusedTransaction";
1503
+ }
1504
+ get expired() {
1505
+ return this.reason === "idle rollback";
1506
+ }
1507
+ }
1508
+ function isUnknownOutcome(error) {
1509
+ return /OutcomeUnknown|UnknownOutcome|ConnectionLost|Worker.*(terminated|failed|closed)|is closed/iu.test(errorText(error));
1510
+ }
1511
+ function isInjectedFault(error) {
1512
+ return /injected/iu.test(errorText(error));
1513
+ }
1514
+ function isStoreUnresponsive(error) {
1515
+ for (let current = error, depth = 0; current !== void 0 && depth < 8; depth += 1) {
1516
+ if (errorText(current).includes("StorageUnresponsive"))
1517
+ return true;
1518
+ current = isRecord(current) ? current.cause : void 0;
1519
+ }
1520
+ return false;
1521
+ }
1522
+ function mulberry32(seed) {
1523
+ let state = seed >>> 0;
1524
+ return () => {
1525
+ state = state + 1831565813 >>> 0;
1526
+ let mixed = state;
1527
+ mixed = Math.imul(mixed ^ mixed >>> 15, mixed | 1);
1528
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
1529
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
1530
+ };
1531
+ }
1532
+ function createDatabaseDriver(source, options = {}) {
1533
+ const controller = new InjectedFaultController();
1534
+ const owned = /* @__PURE__ */ new Set();
1535
+ const wrap = (store) => new FaultInjectingBlockStore(store, (point) => {
1536
+ controller.inject(point);
1537
+ });
1538
+ const shared = typeof source === "function" ? void 0 : wrap(source);
1539
+ const acquire = async () => {
1540
+ if (shared !== void 0)
1541
+ return shared;
1542
+ const store = await source();
1543
+ owned.add(store);
1544
+ return store;
1545
+ };
1546
+ const release = (store) => {
1547
+ if (store === void 0)
1548
+ return;
1549
+ owned.delete(store);
1550
+ store.close();
1551
+ };
1552
+ const databases = /* @__PURE__ */ new Set();
1553
+ const open = async () => {
1554
+ const store = await acquire();
1555
+ const database = new MinnowDatabase(shared === void 0 ? wrap(store) : store, {
1556
+ ...options.databaseOptions
1557
+ });
1558
+ databases.add(database);
1559
+ return { database, store: shared === void 0 ? store : void 0 };
1560
+ };
1561
+ return {
1562
+ faults: controller,
1563
+ open: async () => {
1564
+ let { database, store } = await open();
1565
+ return {
1566
+ execute: async (sql, params) => {
1567
+ const result = await database.execute(sql, params);
1568
+ return "rowCount" in result ? { kind: result.kind, rowCount: result.rowCount } : { kind: result.kind };
1569
+ },
1570
+ query: async (sql, params) => {
1571
+ const result = await database.query(sql, params === void 0 ? { memoize: false } : { memoize: false, params: [...params] });
1572
+ return { columns: result.columns, rows: result.rows };
1573
+ },
1574
+ reopen: async () => {
1575
+ await database.close();
1576
+ databases.delete(database);
1577
+ release(store);
1578
+ ({ database, store } = await open());
1579
+ },
1580
+ maintain: async (table) => {
1581
+ await database.compactTable(table);
1582
+ await database.collectGarbage();
1583
+ }
1584
+ };
1585
+ },
1586
+ close: async () => {
1587
+ await Promise.allSettled([...databases].map((database) => database.close()));
1588
+ for (const store of owned)
1589
+ store.close();
1590
+ owned.clear();
1591
+ }
1592
+ };
1593
+ }
1594
+ class InjectedFaultController {
1595
+ #point;
1596
+ #occurrence = 0;
1597
+ #seen = 0;
1598
+ #fired = false;
1599
+ arm(point, occurrence) {
1600
+ this.#point = point;
1601
+ this.#occurrence = occurrence;
1602
+ this.#seen = 0;
1603
+ this.#fired = false;
1604
+ }
1605
+ disarm() {
1606
+ this.#point = void 0;
1607
+ }
1608
+ fired() {
1609
+ return this.#fired;
1610
+ }
1611
+ inject(point) {
1612
+ if (point !== this.#point || this.#fired)
1613
+ return;
1614
+ this.#seen++;
1615
+ if (this.#seen !== this.#occurrence)
1616
+ return;
1617
+ this.#fired = true;
1618
+ throw new Error(`injected ${point} #${String(this.#occurrence)}`);
1619
+ }
1620
+ }
1621
+ export {
1622
+ InteractionFailure,
1623
+ createDatabaseDriver,
1624
+ describeError,
1625
+ evaluate,
1626
+ generateInteractionPlan,
1627
+ parseInteractionPlan,
1628
+ renderLiteral,
1629
+ renderPredicate,
1630
+ runInteractionPlan
1631
+ };