@minnowdb/core 0.10.1 → 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.
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Interaction-plan simulator: a seeded, replayable workload over the whole SQL surface an
3
+ * application uses, checked against a shadow model and a set of properties.
4
+ *
5
+ * `simulator.ts` is the storage-completion simulator: one table, keyed upserts and deletes, a
6
+ * seeded completion order for every block-store call. This one sits above it, in the spirit of
7
+ * Turso's simulator. A plan is a sequence of *interactions* -- DDL, inserts, predicate updates
8
+ * and deletes, selects, indexes, SQL transactions, concurrent rounds across several connections,
9
+ * faults, reopens, maintenance -- generated from a seed and serialized as JSON, so a failing CI
10
+ * seed replays exactly and can be checked in. Every interaction is executed through the real
11
+ * SQL entry points and judged against an in-memory shadow model that applies three-valued
12
+ * predicate logic, plus the properties a database must honour whatever its internals do:
13
+ *
14
+ * - insert-select: a row just inserted comes back exactly, and a rejected insert leaves none
15
+ * of its rows behind (statement atomicity)
16
+ * - update-count and delete-count: the reported row count is the model's matched count
17
+ * - delete-select: nothing matching a delete's predicate survives it
18
+ * - drop-select and double-create-failure: dropped tables are gone, duplicates are refused
19
+ * - select-limit: LIMIT returns exactly min(limit, matching) rows in ORDER BY order
20
+ * - where-true-false-null: COUNT(p) + COUNT(NOT p) + COUNT(p IS NULL) = COUNT(*)
21
+ * - union-all-cardinality: a UNION ALL has the sum of its members' cardinalities
22
+ * - transaction-isolation: uncommitted rows are visible on their own connection and invisible
23
+ * to another; ROLLBACK discards them and COMMIT keeps them
24
+ * - concurrent-explicability: a read taken while a round of commuting writes is in flight
25
+ * shows every untouched key exactly and every touched key at either its old or new value
26
+ * - fault-atomicity: a mutation interrupted by a storage fault or a crash lands entirely or
27
+ * not at all, and a reported success is durable
28
+ * - agreement: at every checkpoint every connection reads the same database as the model
29
+ *
30
+ * The runner is driver-agnostic. `SimulationDriver` opens connections over one shared database;
31
+ * the Node driver wraps `MinnowDatabase` over any block store, and the Playwright driver wraps
32
+ * one real browser tab per connection over IndexedDB or OPFS, so the same plan runs in both.
33
+ */
34
+ import { type MinnowDatabaseOptions } from "../engine/database.js";
35
+ import type { BlockStore } from "../storage/index.js";
36
+ export type ColumnType = "integer" | "real" | "text" | "boolean";
37
+ export interface PlanColumn {
38
+ readonly name: string;
39
+ readonly type: ColumnType;
40
+ readonly nullable: boolean;
41
+ }
42
+ export interface PlanTable {
43
+ readonly name: string;
44
+ /** Every table has `id INTEGER PRIMARY KEY`; these are the other columns. */
45
+ readonly columns: readonly PlanColumn[];
46
+ }
47
+ export type PlanValue = number | string | boolean | null;
48
+ export type PlanRow = Readonly<Record<string, PlanValue>>;
49
+ export type CompareOperator = "=" | "<>" | "<" | "<=" | ">" | ">=";
50
+ export type Predicate = {
51
+ readonly kind: "compare";
52
+ readonly column: string;
53
+ readonly op: CompareOperator;
54
+ readonly value: PlanValue;
55
+ } | {
56
+ readonly kind: "isNull";
57
+ readonly column: string;
58
+ readonly negated: boolean;
59
+ } | {
60
+ readonly kind: "and";
61
+ readonly left: Predicate;
62
+ readonly right: Predicate;
63
+ } | {
64
+ readonly kind: "or";
65
+ readonly left: Predicate;
66
+ readonly right: Predicate;
67
+ } | {
68
+ readonly kind: "not";
69
+ readonly inner: Predicate;
70
+ } | {
71
+ readonly kind: "literal";
72
+ readonly value: boolean;
73
+ };
74
+ export type Assignment = {
75
+ readonly kind: "set";
76
+ readonly column: string;
77
+ readonly value: PlanValue;
78
+ } | {
79
+ readonly kind: "increment";
80
+ readonly column: string;
81
+ readonly by: number;
82
+ };
83
+ export type KeyedMutation = {
84
+ readonly kind: "insert";
85
+ readonly row: PlanRow;
86
+ } | {
87
+ readonly kind: "upsert";
88
+ readonly row: PlanRow;
89
+ } | {
90
+ readonly kind: "updateKey";
91
+ readonly id: number;
92
+ readonly assignments: readonly Assignment[];
93
+ } | {
94
+ readonly kind: "deleteKey";
95
+ readonly id: number;
96
+ };
97
+ export type TransactionStatement = {
98
+ readonly kind: "insert";
99
+ readonly rows: readonly PlanRow[];
100
+ } | {
101
+ readonly kind: "update";
102
+ readonly predicate: Predicate;
103
+ readonly assignments: readonly Assignment[];
104
+ } | {
105
+ readonly kind: "delete";
106
+ readonly predicate: Predicate;
107
+ };
108
+ export type Interaction = {
109
+ readonly kind: "createTable";
110
+ readonly connection: number;
111
+ readonly table: PlanTable;
112
+ readonly expectExisting: boolean;
113
+ } | {
114
+ readonly kind: "insert";
115
+ readonly connection: number;
116
+ readonly table: string;
117
+ readonly rows: readonly PlanRow[];
118
+ readonly viaParameters: boolean;
119
+ } | {
120
+ readonly kind: "update";
121
+ readonly connection: number;
122
+ readonly table: string;
123
+ readonly predicate: Predicate;
124
+ readonly assignments: readonly Assignment[];
125
+ } | {
126
+ readonly kind: "delete";
127
+ readonly connection: number;
128
+ readonly table: string;
129
+ readonly predicate: Predicate;
130
+ } | {
131
+ readonly kind: "select";
132
+ readonly connection: number;
133
+ readonly table: string;
134
+ readonly predicate: Predicate;
135
+ readonly limit: number | null;
136
+ readonly descending: boolean;
137
+ } | {
138
+ readonly kind: "partition";
139
+ readonly connection: number;
140
+ readonly table: string;
141
+ readonly predicate: Predicate;
142
+ } | {
143
+ readonly kind: "unionAll";
144
+ readonly connection: number;
145
+ readonly table: string;
146
+ readonly left: Predicate;
147
+ readonly right: Predicate;
148
+ } | {
149
+ readonly kind: "createIndex";
150
+ readonly connection: number;
151
+ readonly table: string;
152
+ readonly name: string;
153
+ readonly columns: readonly string[];
154
+ } | {
155
+ readonly kind: "dropTable";
156
+ readonly connection: number;
157
+ readonly table: string;
158
+ readonly expectMissing: boolean;
159
+ } | {
160
+ readonly kind: "transaction";
161
+ readonly connection: number;
162
+ readonly observer: number;
163
+ readonly table: string;
164
+ readonly statements: readonly TransactionStatement[];
165
+ readonly outcome: "commit" | "rollback";
166
+ } | {
167
+ readonly kind: "concurrent";
168
+ readonly table: string;
169
+ readonly operations: ReadonlyArray<{
170
+ readonly connection: number;
171
+ readonly mutation: KeyedMutation;
172
+ }>;
173
+ readonly readers: readonly number[];
174
+ } | {
175
+ readonly kind: "fault";
176
+ readonly connection: number;
177
+ readonly table: string;
178
+ readonly mutation: KeyedMutation;
179
+ readonly point: FaultPointName;
180
+ } | {
181
+ readonly kind: "reopen";
182
+ readonly connection: number;
183
+ } | {
184
+ readonly kind: "maintenance";
185
+ readonly connection: number;
186
+ readonly table: string;
187
+ } | {
188
+ readonly kind: "checkpoint";
189
+ };
190
+ export type FaultPointName = "beforeBlockWrite" | "afterBlockWrite" | "beforeTransactionCommit" | "afterTransactionCommit" | "crash";
191
+ export interface InteractionPlan {
192
+ readonly version: 1;
193
+ readonly seed: number;
194
+ readonly connections: number;
195
+ readonly interactions: readonly Interaction[];
196
+ }
197
+ export interface InteractionPlanOptions {
198
+ /** Interactions to generate, checkpoints and DDL included. */
199
+ readonly length?: number;
200
+ readonly connections?: number;
201
+ /** Tables the plan may create at once. */
202
+ readonly tables?: number;
203
+ /** Primary-key space per table; small enough that inserts collide and updates hit. */
204
+ readonly keySpace?: number;
205
+ /**
206
+ * Fault points the plan may draw from. A driver without storage fault injection (a real
207
+ * browser) generates crash faults only, so every fault step is one it can exercise.
208
+ */
209
+ readonly faultPoints?: readonly FaultPointName[];
210
+ }
211
+ export interface SimulatedExecuteResult {
212
+ readonly kind: string;
213
+ readonly rowCount?: number;
214
+ }
215
+ export interface SimulatedQueryResult {
216
+ readonly columns: readonly string[];
217
+ readonly rows: ReadonlyArray<Readonly<Record<string, unknown>>>;
218
+ }
219
+ export interface SimulatedConnection {
220
+ execute(sql: string, params?: readonly PlanValue[]): Promise<SimulatedExecuteResult>;
221
+ query(sql: string, params?: readonly PlanValue[]): Promise<SimulatedQueryResult>;
222
+ /** Closes and reopens this connection over the same database. */
223
+ reopen(): Promise<void>;
224
+ /** Runs the store's maintenance (compaction and collection) for one table. */
225
+ maintain?(table: string): Promise<void>;
226
+ /**
227
+ * Kills the connection's process while a call may be in flight; the pending call must fail
228
+ * with an unknown-outcome or lost-connection error, and `reopen` must bring the connection
229
+ * back. A driver without a process boundary omits it and the plan's crash faults are skipped.
230
+ */
231
+ crash?(): Promise<void>;
232
+ }
233
+ export interface SimulatedFaults {
234
+ /** Fails the next `occurrence`-th storage call at `point` on any connection. */
235
+ arm(point: Exclude<FaultPointName, "crash">, occurrence: number): void;
236
+ disarm(): void;
237
+ fired(): boolean;
238
+ }
239
+ export interface SimulationDriver {
240
+ open(connection: number): Promise<SimulatedConnection>;
241
+ readonly faults?: SimulatedFaults;
242
+ /** Whether errors the driver reports carry `name`; a driver over a text channel may not. */
243
+ close?(): Promise<void>;
244
+ }
245
+ export interface InteractionRunOptions {
246
+ /** Bounded trace of executed SQL kept for the failure report. */
247
+ readonly traceLength?: number;
248
+ }
249
+ export interface InteractionRunResult {
250
+ readonly seed: number;
251
+ /** Interactions judged. Short of the plan's length only when `transientsAccepted` is not zero. */
252
+ readonly interactions: number;
253
+ readonly statements: number;
254
+ readonly queries: number;
255
+ readonly acceptedWrites: number;
256
+ /**
257
+ * Refusals the engine documents and the runner models rather than reports: a write that lost a
258
+ * conflict, and a transaction whose COMMIT lost the race to another connection's data commit.
259
+ * The run continues from the state they leave behind.
260
+ */
261
+ readonly rejectedConflicts: number;
262
+ /**
263
+ * Refusals the plan expected: a duplicate insert, a double create, and a transaction the engine
264
+ * rolled back on its own idle deadline, which the runner acknowledges with a `ROLLBACK`. These
265
+ * do not stop the run either.
266
+ */
267
+ readonly expectedFailures: number;
268
+ readonly faultsInjected: number;
269
+ readonly faultsSkipped: number;
270
+ readonly reopens: number;
271
+ readonly checkpoints: number;
272
+ readonly tablesAtEnd: number;
273
+ readonly rowsAtEnd: number;
274
+ /**
275
+ * The one transient that ends a run instead of being modelled: a store that stopped answering.
276
+ * A browser can wedge a whole database beyond any engine's reach (a worker terminated with a
277
+ * write in flight leaves WebKit's IndexedDB connection registered until its document goes away),
278
+ * and the engine then bounds the wait and reports `StorageUnresponsiveError` rather than hanging.
279
+ * Nothing further can be driven through that store, so the run stops at that interaction and
280
+ * `interactions` counts only the steps it judged.
281
+ */
282
+ readonly transientsAccepted: number;
283
+ /** What ended the run early, when `transientsAccepted` is not zero. */
284
+ readonly stoppedBy: string | undefined;
285
+ }
286
+ export declare class InteractionFailure extends Error {
287
+ readonly index: number;
288
+ readonly interaction: Interaction;
289
+ readonly trace: readonly string[];
290
+ constructor(message: string, index: number, interaction: Interaction, trace: readonly string[], options?: ErrorOptions);
291
+ }
292
+ /** Generates a bounded, replayable plan; the same seed and options always produce the same plan. */
293
+ export declare function generateInteractionPlan(seed: number, options?: InteractionPlanOptions): InteractionPlan;
294
+ export declare function parseInteractionPlan(source: string): InteractionPlan;
295
+ type Truth = true | false | null;
296
+ /** SQL three-valued logic over one row. */
297
+ export declare function evaluate(predicate: Predicate, row: PlanRow): Truth;
298
+ export declare function renderLiteral(value: PlanValue): string;
299
+ export declare function renderPredicate(predicate: Predicate): string;
300
+ export declare function runInteractionPlan(plan: InteractionPlan, driver: SimulationDriver, options?: InteractionRunOptions): Promise<InteractionRunResult>;
301
+ export declare function describeError(error: unknown): string;
302
+ export interface DatabaseDriverOptions {
303
+ /** Options for every `MinnowDatabase` the driver opens; `now` and `createId` stay the engine's. */
304
+ readonly databaseOptions?: MinnowDatabaseOptions;
305
+ }
306
+ /**
307
+ * Where a connection's block store comes from: one shared instance, as several engines in one
308
+ * process share it, or a factory that opens a separate instance over the same durable database
309
+ * for every connection and every reopen -- the shape of separate tabs, each with its own
310
+ * adapter caches, which is where a cross-tab cache that goes stale shows up.
311
+ */
312
+ export type DriverStoreSource = BlockStore | (() => Promise<BlockStore>);
313
+ /**
314
+ * A driver whose connections are `MinnowDatabase` instances over one database, with the storage
315
+ * fault points armed through `FaultInjectingBlockStore`. Crash faults are skipped: there is no
316
+ * process to kill.
317
+ */
318
+ export declare function createDatabaseDriver(source: DriverStoreSource, options?: DatabaseDriverOptions): SimulationDriver;
319
+ export {};