@minnowdb/core 0.2.1 → 0.4.0

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 (270) hide show
  1. package/README.md +23 -32
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +2 -1
  21. package/dist/engine/artifact-cache.js +5 -1
  22. package/dist/engine/batch.d.ts +11 -3
  23. package/dist/engine/batch.js +17 -7
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +24 -5
  29. package/dist/engine/catalog.js +34 -7
  30. package/dist/engine/client.d.ts +29 -7
  31. package/dist/engine/client.js +216 -40
  32. package/dist/engine/database.d.ts +224 -39
  33. package/dist/engine/database.js +7475 -1539
  34. package/dist/engine/defaults.d.ts +4 -8
  35. package/dist/engine/defaults.js +46 -22
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +41 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -10
  43. package/dist/engine/index.js +12 -9
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +504 -126
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +262 -27
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +1 -2
  59. package/dist/engine/query-cache.js +7 -5
  60. package/dist/engine/query.d.ts +134 -29
  61. package/dist/engine/query.js +1668 -372
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +6 -3
  65. package/dist/engine/schema-wire.js +40 -34
  66. package/dist/engine/schema.d.ts +156 -80
  67. package/dist/engine/schema.js +570 -108
  68. package/dist/engine/sort-keys.d.ts +4 -4
  69. package/dist/engine/sort-keys.js +44 -29
  70. package/dist/engine/sql-domains.d.ts +31 -0
  71. package/dist/engine/sql-domains.js +585 -0
  72. package/dist/engine/sql-driver.d.ts +19 -0
  73. package/dist/engine/sql-driver.js +1 -0
  74. package/dist/engine/sql-json.d.ts +7 -9
  75. package/dist/engine/sql-json.js +75 -15
  76. package/dist/engine/sql-semantics.d.ts +8 -3
  77. package/dist/engine/sql-semantics.js +458 -30
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +12 -2
  81. package/dist/engine/vector.js +635 -119
  82. package/dist/engine/worker-host.d.ts +9 -2
  83. package/dist/engine/worker-host.js +620 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +3 -4
  91. package/dist/plan/index.js +3 -4
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +99 -51
  95. package/dist/storage/indexeddb.js +13448 -2660
  96. package/dist/storage/memory.d.ts +64 -57
  97. package/dist/storage/memory.js +1042 -128
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +409 -75
  103. package/dist/storage/opfs/leader.js +4621 -650
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +224 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +33 -99
  109. package/dist/storage/opfs/store.js +453 -364
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -85
  115. package/dist/storage/snapshot.js +33 -265
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +4 -5
  119. package/dist/storage/toolkit/index.js +28 -4
  120. package/dist/storage/toolkit/record-core.d.ts +182 -55
  121. package/dist/storage/toolkit/record-core.js +6158 -1331
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +15 -2
  127. package/dist/storage/toolkit/wire.js +199 -10
  128. package/dist/storage/types.d.ts +1439 -204
  129. package/dist/storage/types.js +1745 -141
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +1009 -119
  132. package/dist/testing/index.d.ts +46 -26
  133. package/dist/testing/index.js +112 -57
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +97 -0
  137. package/dist/testing/simulator.js +591 -0
  138. package/dist/testing/sqllogictest.d.ts +89 -0
  139. package/dist/testing/sqllogictest.js +434 -0
  140. package/dist/transactions/index.d.ts +92 -16
  141. package/dist/transactions/index.js +1074 -236
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +53 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +172 -252
  147. package/dist/block-format/block.d.ts.map +0 -1
  148. package/dist/block-format/block.js.map +0 -1
  149. package/dist/block-format/checksum.d.ts.map +0 -1
  150. package/dist/block-format/checksum.js.map +0 -1
  151. package/dist/block-format/codecs.d.ts.map +0 -1
  152. package/dist/block-format/codecs.js.map +0 -1
  153. package/dist/block-format/column.d.ts.map +0 -1
  154. package/dist/block-format/column.js.map +0 -1
  155. package/dist/block-format/index.d.ts.map +0 -1
  156. package/dist/block-format/index.js.map +0 -1
  157. package/dist/block-format/physical.d.ts.map +0 -1
  158. package/dist/block-format/physical.js.map +0 -1
  159. package/dist/block-format/types.d.ts.map +0 -1
  160. package/dist/block-format/types.js.map +0 -1
  161. package/dist/engine/artifact-cache.d.ts.map +0 -1
  162. package/dist/engine/artifact-cache.js.map +0 -1
  163. package/dist/engine/batch.d.ts.map +0 -1
  164. package/dist/engine/batch.js.map +0 -1
  165. package/dist/engine/buffered-writer.d.ts.map +0 -1
  166. package/dist/engine/buffered-writer.js.map +0 -1
  167. package/dist/engine/catalog.d.ts.map +0 -1
  168. package/dist/engine/catalog.js.map +0 -1
  169. package/dist/engine/client.d.ts.map +0 -1
  170. package/dist/engine/client.js.map +0 -1
  171. package/dist/engine/coordinator.d.ts +0 -17
  172. package/dist/engine/coordinator.d.ts.map +0 -1
  173. package/dist/engine/coordinator.js +0 -60
  174. package/dist/engine/coordinator.js.map +0 -1
  175. package/dist/engine/database.d.ts.map +0 -1
  176. package/dist/engine/database.js.map +0 -1
  177. package/dist/engine/defaults.d.ts.map +0 -1
  178. package/dist/engine/defaults.js.map +0 -1
  179. package/dist/engine/errors.d.ts.map +0 -1
  180. package/dist/engine/errors.js.map +0 -1
  181. package/dist/engine/fts.d.ts.map +0 -1
  182. package/dist/engine/fts.js.map +0 -1
  183. package/dist/engine/group-index.d.ts.map +0 -1
  184. package/dist/engine/group-index.js.map +0 -1
  185. package/dist/engine/index.d.ts.map +0 -1
  186. package/dist/engine/index.js.map +0 -1
  187. package/dist/engine/join-index.d.ts.map +0 -1
  188. package/dist/engine/join-index.js.map +0 -1
  189. package/dist/engine/live.d.ts.map +0 -1
  190. package/dist/engine/live.js.map +0 -1
  191. package/dist/engine/memory.d.ts.map +0 -1
  192. package/dist/engine/memory.js.map +0 -1
  193. package/dist/engine/optimizer.d.ts.map +0 -1
  194. package/dist/engine/optimizer.js.map +0 -1
  195. package/dist/engine/query-cache.d.ts.map +0 -1
  196. package/dist/engine/query-cache.js.map +0 -1
  197. package/dist/engine/query.d.ts.map +0 -1
  198. package/dist/engine/query.js.map +0 -1
  199. package/dist/engine/result-wire.d.ts.map +0 -1
  200. package/dist/engine/result-wire.js.map +0 -1
  201. package/dist/engine/schema-wire.d.ts.map +0 -1
  202. package/dist/engine/schema-wire.js.map +0 -1
  203. package/dist/engine/schema.d.ts.map +0 -1
  204. package/dist/engine/schema.js.map +0 -1
  205. package/dist/engine/sort-keys.d.ts.map +0 -1
  206. package/dist/engine/sort-keys.js.map +0 -1
  207. package/dist/engine/sql-json.d.ts.map +0 -1
  208. package/dist/engine/sql-json.js.map +0 -1
  209. package/dist/engine/sql-semantics.d.ts.map +0 -1
  210. package/dist/engine/sql-semantics.js.map +0 -1
  211. package/dist/engine/vector.d.ts.map +0 -1
  212. package/dist/engine/vector.js.map +0 -1
  213. package/dist/engine/worker-host.d.ts.map +0 -1
  214. package/dist/engine/worker-host.js.map +0 -1
  215. package/dist/engine/worker.d.ts.map +0 -1
  216. package/dist/engine/worker.js.map +0 -1
  217. package/dist/index.d.ts.map +0 -1
  218. package/dist/index.js.map +0 -1
  219. package/dist/plan/index.d.ts.map +0 -1
  220. package/dist/plan/index.js.map +0 -1
  221. package/dist/storage/fixture-shape.d.ts +0 -42
  222. package/dist/storage/fixture-shape.d.ts.map +0 -1
  223. package/dist/storage/fixture-shape.js +0 -146
  224. package/dist/storage/fixture-shape.js.map +0 -1
  225. package/dist/storage/index.d.ts.map +0 -1
  226. package/dist/storage/index.js.map +0 -1
  227. package/dist/storage/indexeddb.d.ts.map +0 -1
  228. package/dist/storage/indexeddb.js.map +0 -1
  229. package/dist/storage/memory.d.ts.map +0 -1
  230. package/dist/storage/memory.js.map +0 -1
  231. package/dist/storage/opfs/files.d.ts.map +0 -1
  232. package/dist/storage/opfs/files.js.map +0 -1
  233. package/dist/storage/opfs/index.d.ts.map +0 -1
  234. package/dist/storage/opfs/index.js.map +0 -1
  235. package/dist/storage/opfs/leader.d.ts.map +0 -1
  236. package/dist/storage/opfs/leader.js.map +0 -1
  237. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  238. package/dist/storage/opfs/rpc.js.map +0 -1
  239. package/dist/storage/opfs/store.d.ts.map +0 -1
  240. package/dist/storage/opfs/store.js.map +0 -1
  241. package/dist/storage/snapshot.d.ts.map +0 -1
  242. package/dist/storage/snapshot.js.map +0 -1
  243. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  244. package/dist/storage/toolkit/extents.js.map +0 -1
  245. package/dist/storage/toolkit/index.d.ts.map +0 -1
  246. package/dist/storage/toolkit/index.js.map +0 -1
  247. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  248. package/dist/storage/toolkit/record-core.js.map +0 -1
  249. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  250. package/dist/storage/toolkit/sync-file.js.map +0 -1
  251. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  252. package/dist/storage/toolkit/wal.js.map +0 -1
  253. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wire.js.map +0 -1
  255. package/dist/storage/types.d.ts.map +0 -1
  256. package/dist/storage/types.js.map +0 -1
  257. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  258. package/dist/testing/block-store-conformance.js.map +0 -1
  259. package/dist/testing/index.d.ts.map +0 -1
  260. package/dist/testing/index.js.map +0 -1
  261. package/dist/testing/opfs-shim.d.ts.map +0 -1
  262. package/dist/testing/opfs-shim.js.map +0 -1
  263. package/dist/testing/seeds.d.ts +0 -11
  264. package/dist/testing/seeds.d.ts.map +0 -1
  265. package/dist/testing/seeds.js +0 -50
  266. package/dist/testing/seeds.js.map +0 -1
  267. package/dist/transactions/index.d.ts.map +0 -1
  268. package/dist/transactions/index.js.map +0 -1
  269. package/dist/worker-protocol/index.d.ts.map +0 -1
  270. package/dist/worker-protocol/index.js.map +0 -1
@@ -0,0 +1,591 @@
1
+ import { MinnowDatabase } from "../engine/database.js";
2
+ import { MemoryBlockStore } from "../storage/index.js";
3
+ import { FaultInjectingBlockStore } from "./index.js";
4
+ import { md5Hex } from "./sqllogictest.js";
5
+ async function collectStoragePages(read) {
6
+ const records = [];
7
+ let cursor = null;
8
+ for (;;) {
9
+ const page = await read(cursor);
10
+ records.push(...page.records);
11
+ if (page.nextCursor === null)
12
+ return records;
13
+ cursor = page.nextCursor;
14
+ }
15
+ }
16
+ /** Chooses every storage completion from a seeded queue and keeps only a bounded trace tail. */
17
+ export class DeterministicScheduler {
18
+ #random;
19
+ #traceLimit;
20
+ #pending = [];
21
+ #trace = [];
22
+ #sequence = 0;
23
+ #highWater = 0;
24
+ constructor(seed, traceLimit = 256) {
25
+ if (!Number.isSafeInteger(traceLimit) || traceLimit < 0) {
26
+ throw new RangeError("Simulator trace limit must be a non-negative whole number");
27
+ }
28
+ this.#random = mulberry32(seed);
29
+ this.#traceLimit = traceLimit;
30
+ }
31
+ get highWater() {
32
+ return this.#highWater;
33
+ }
34
+ get trace() {
35
+ return this.#trace;
36
+ }
37
+ schedule(label, run) {
38
+ return new Promise((resolve, reject) => {
39
+ this.#pending.push({ label, run, resolve, reject });
40
+ this.#highWater = Math.max(this.#highWater, this.#pending.length);
41
+ });
42
+ }
43
+ async settle(promises) {
44
+ const state = { complete: false };
45
+ const settled = Promise.allSettled(promises).then((results) => {
46
+ state.complete = true;
47
+ return results;
48
+ });
49
+ let emptyTurns = 0;
50
+ while (!state.complete) {
51
+ await Promise.resolve();
52
+ const pending = this.#pending.length;
53
+ if (pending === 0) {
54
+ // Production maintenance deliberately yields through a timer between bounded pages.
55
+ // A microtask-only poll can therefore declare deadlock while the next scheduled store
56
+ // call is merely waiting for its event-loop turn. Let timers advance before counting an
57
+ // empty turn; the chosen storage completion order remains seeded and deterministic.
58
+ await nextEventLoopTurn();
59
+ if (this.#pending.length > 0) {
60
+ emptyTurns = 0;
61
+ continue;
62
+ }
63
+ emptyTurns++;
64
+ if (emptyTurns > 100) {
65
+ throw new Error("Deterministic simulator deadlocked with no scheduled storage operation");
66
+ }
67
+ continue;
68
+ }
69
+ emptyTurns = 0;
70
+ const index = Math.floor(this.#random() * pending);
71
+ const operation = this.#pending.splice(index, 1)[0];
72
+ if (operation === undefined)
73
+ continue;
74
+ try {
75
+ const value = await operation.run();
76
+ operation.resolve(value);
77
+ this.#record(operation.label, "resolved");
78
+ }
79
+ catch (error) {
80
+ operation.reject(error);
81
+ this.#record(operation.label, "rejected");
82
+ }
83
+ }
84
+ return settled;
85
+ }
86
+ #record(operation, outcome) {
87
+ this.#sequence++;
88
+ if (this.#traceLimit === 0)
89
+ return;
90
+ this.#trace.push({ sequence: this.#sequence, operation, outcome });
91
+ if (this.#trace.length > this.#traceLimit)
92
+ this.#trace.shift();
93
+ }
94
+ }
95
+ function nextEventLoopTurn() {
96
+ if (typeof MessageChannel === "undefined") {
97
+ return new Promise((resolve) => setTimeout(resolve, 0));
98
+ }
99
+ return new Promise((resolve) => {
100
+ const channel = new MessageChannel();
101
+ channel.port1.onmessage = () => {
102
+ channel.port1.close();
103
+ channel.port2.close();
104
+ resolve();
105
+ };
106
+ channel.port2.postMessage(undefined);
107
+ });
108
+ }
109
+ /** Wraps every asynchronous BlockStore entry point in the deterministic completion scheduler. */
110
+ export function scheduledBlockStore(store, scheduler) {
111
+ return new Proxy(store, {
112
+ get(target, property) {
113
+ const value = Reflect.get(target, property, target);
114
+ if (typeof value !== "function")
115
+ return value;
116
+ const method = value;
117
+ if (property === "close")
118
+ return (...arguments_) => method.apply(target, arguments_);
119
+ return (...arguments_) => scheduler.schedule(String(property), async () => method.apply(target, arguments_));
120
+ },
121
+ });
122
+ }
123
+ export function generateSimulationPlan(seed, options = {}) {
124
+ if (!Number.isSafeInteger(seed)) {
125
+ throw new RangeError("Simulator seed must be a whole number");
126
+ }
127
+ const rounds = options.rounds ?? 48;
128
+ const clients = options.clients ?? 4;
129
+ const keySpace = options.keySpace ?? 32;
130
+ if (!Number.isSafeInteger(rounds) || rounds < 1 || rounds > 100_000) {
131
+ throw new RangeError("Simulator rounds must be between 1 and 100,000");
132
+ }
133
+ if (!Number.isSafeInteger(clients) || clients < 1 || clients > 32) {
134
+ throw new RangeError("Simulator clients must be between 1 and 32");
135
+ }
136
+ if (!Number.isSafeInteger(keySpace) || keySpace < 1 || keySpace > 10_000) {
137
+ throw new RangeError("Simulator key space must be between 1 and 10,000");
138
+ }
139
+ if (keySpace < clients) {
140
+ throw new RangeError("Simulator key space must be at least the client count");
141
+ }
142
+ const random = mulberry32(seed);
143
+ const steps = [];
144
+ const faultPoints = [
145
+ "beforeBlockWrite",
146
+ "afterBlockWrite",
147
+ "beforeTransactionCommit",
148
+ "afterTransactionCommit",
149
+ ];
150
+ for (let round = 0; round < rounds; round++) {
151
+ const operations = [];
152
+ const start = Math.floor(random() * keySpace);
153
+ let stride = Math.floor(random() * keySpace) + 1;
154
+ while (greatestCommonDivisor(stride, keySpace) !== 1)
155
+ stride++;
156
+ for (let client = 0; client < clients; client++) {
157
+ // A round is a genuinely concurrent commit race, but its row mutations commute. That gives
158
+ // the reference model one exact answer instead of guessing which same-key promise linearized
159
+ // last. Contention still happens at the shared manifest/transaction boundary.
160
+ const key = ((start + client * stride) % keySpace) + 1;
161
+ if (random() < 0.18)
162
+ operations.push({ kind: "delete", client, key });
163
+ else
164
+ operations.push({ kind: "put", client, key, amount: Math.floor(random() * 1_000_000) });
165
+ }
166
+ steps.push({ kind: "concurrent", operations });
167
+ if (round % 4 === 3)
168
+ steps.push({ kind: "checkpoint" });
169
+ if (round % 12 === 11)
170
+ steps.push({ kind: "maintenance" });
171
+ if (round % 20 === 19) {
172
+ const point = faultPoints[Math.floor(random() * faultPoints.length)] ?? "beforeBlockWrite";
173
+ steps.push({
174
+ kind: "fault",
175
+ point,
176
+ occurrence: 1,
177
+ operation: {
178
+ kind: "put",
179
+ client: Math.floor(random() * clients),
180
+ key: Math.floor(random() * keySpace) + 1,
181
+ amount: Math.floor(random() * 1_000_000),
182
+ },
183
+ });
184
+ }
185
+ }
186
+ steps.push({ kind: "checkpoint" }, { kind: "maintenance" }, { kind: "checkpoint" });
187
+ return { version: 1, seed, clients, keySpace, steps };
188
+ }
189
+ export function parseSimulationPlan(source) {
190
+ const value = JSON.parse(source);
191
+ if (typeof value !== "object" || value === null)
192
+ throw new TypeError("Simulation plan must be an object");
193
+ const plan = value;
194
+ if (plan.version !== 1)
195
+ throw new TypeError("Simulation plan version must be 1");
196
+ if (!Number.isSafeInteger(plan.seed))
197
+ throw new TypeError("Simulation seed must be a whole number");
198
+ if (!Number.isSafeInteger(plan.clients) || (plan.clients ?? 0) < 1 || (plan.clients ?? 0) > 32) {
199
+ throw new TypeError("Simulation client count is invalid");
200
+ }
201
+ if (!Number.isSafeInteger(plan.keySpace) ||
202
+ (plan.keySpace ?? 0) < 1 ||
203
+ (plan.keySpace ?? 0) > 10_000) {
204
+ throw new TypeError("Simulation key space is invalid");
205
+ }
206
+ if ((plan.keySpace ?? 0) < (plan.clients ?? 0)) {
207
+ throw new TypeError("Simulation key space must be at least the client count");
208
+ }
209
+ if (!Array.isArray(plan.steps) || plan.steps.length > 200_000) {
210
+ throw new TypeError("Simulation steps must be a bounded array");
211
+ }
212
+ validateSteps(plan.steps, plan.clients ?? 0, plan.keySpace ?? 0);
213
+ return plan;
214
+ }
215
+ /** Runs a replayable state-machine plan through production SQL, transactions, and block storage. */
216
+ export async function runSimulation(plan, options = {}) {
217
+ parseSimulationPlan(JSON.stringify(plan));
218
+ const base = new MemoryBlockStore();
219
+ const faults = new FaultController();
220
+ const scheduler = new DeterministicScheduler(plan.seed ^ 0x5f3759df, options.traceEvents ?? 256);
221
+ const store = scheduledBlockStore(new FaultInjectingBlockStore(base, (point) => faults.inject(point)), scheduler);
222
+ let clock = Date.parse("2026-01-01T00:00:00.000Z");
223
+ let id = 0;
224
+ const createDatabase = () => new MinnowDatabase(store, {
225
+ compression: "raw",
226
+ rowsPerBlock: 8,
227
+ maxCommitRetries: 16,
228
+ now: () => new Date(clock),
229
+ createId: () => `sim-${String(id++)}`,
230
+ bufferPoolBytes: 0,
231
+ autoCompact: false,
232
+ autoCollect: false,
233
+ });
234
+ const setup = createDatabase();
235
+ await drive(scheduler, setup.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, client INTEGER NOT NULL, amount INTEGER NOT NULL)"));
236
+ await drive(scheduler, setup.close());
237
+ const clients = Array.from({ length: plan.clients }, createDatabase);
238
+ const model = new Map();
239
+ let acceptedMutations = 0;
240
+ let rejectedConflicts = 0;
241
+ let injectedFaults = 0;
242
+ let checkpoints = 0;
243
+ for (const step of plan.steps) {
244
+ clock += 1_000;
245
+ if (step.kind === "concurrent") {
246
+ const operations = step.operations.map((operation) => executeMutation(requireClient(clients, operation.client), operation)
247
+ .then(() => {
248
+ applyMutation(model, operation);
249
+ acceptedMutations++;
250
+ })
251
+ .catch((error) => {
252
+ if (/manifest changed|conflict/iu.test(errorMessage(error))) {
253
+ rejectedConflicts++;
254
+ return;
255
+ }
256
+ throw error;
257
+ }));
258
+ assertSettled(await scheduler.settle(operations));
259
+ continue;
260
+ }
261
+ if (step.kind === "checkpoint") {
262
+ await assertModel(scheduler, clients[0] ?? setup, model);
263
+ checkpoints++;
264
+ continue;
265
+ }
266
+ if (step.kind === "maintenance") {
267
+ const database = clients[0] ?? setup;
268
+ await drive(scheduler, database.compactTable("items"));
269
+ // One pass can remove obsolete manifests/segments while their transaction journals are
270
+ // still provenance roots. The next pass can reclaim those now-unreferenced journals.
271
+ await drive(scheduler, database.collectGarbage());
272
+ await drive(scheduler, database.collectGarbage());
273
+ continue;
274
+ }
275
+ const before = new Map(model);
276
+ const after = new Map(model);
277
+ applyMutation(after, step.operation);
278
+ faults.arm(step.point, step.occurrence);
279
+ const result = await scheduler.settle([
280
+ executeMutation(requireClient(clients, step.operation.client), step.operation),
281
+ ]);
282
+ if (!faults.fired)
283
+ throw new Error(`Simulation fault was not reached: ${step.point}`);
284
+ injectedFaults++;
285
+ faults.disarm();
286
+ const failedClient = requireClient(clients, step.operation.client);
287
+ await drive(scheduler, failedClient.close());
288
+ const replacement = createDatabase();
289
+ clients[step.operation.client] = replacement;
290
+ const actual = await readRows(scheduler, replacement);
291
+ if (sameRows(actual, before)) {
292
+ if (result[0]?.status === "fulfilled") {
293
+ throw new Error(`Fault ${step.point} returned success but did not persist its mutation`);
294
+ }
295
+ replaceModel(model, before);
296
+ }
297
+ else if (sameRows(actual, after))
298
+ replaceModel(model, after);
299
+ else
300
+ throw new Error(`Fault ${step.point} left a state that was neither before nor after the mutation`);
301
+ }
302
+ await assertModel(scheduler, clients[0] ?? setup, model);
303
+ const closeResults = await scheduler.settle(clients.map((client) => client.close()));
304
+ assertSettled(closeResults);
305
+ // Measure eventual storage after the simulated tabs release their deliberately cached reader
306
+ // leases. A fresh maintenance client can then collapse history without an artificial pin from
307
+ // the workload whose final state has already been checked.
308
+ const maintenance = createDatabase();
309
+ const compacted = await compactToFixedPoint(scheduler, maintenance, plan.steps.length);
310
+ const maintenanceCompactions = compacted.passes;
311
+ const compactionStop = compacted.stop;
312
+ const collected = await collectToFixedPoint(scheduler, maintenance, plan.steps.length);
313
+ let collectionPasses = collected.passes;
314
+ const collectionTail = [...collected.tail];
315
+ await assertModel(scheduler, maintenance, model);
316
+ await drive(scheduler, maintenance.close());
317
+ const sweeper = createDatabase();
318
+ const swept = await collectToFixedPoint(scheduler, sweeper, plan.steps.length);
319
+ collectionPasses += swept.passes;
320
+ collectionTail.push(...swept.tail);
321
+ if (collectionTail.length > 8)
322
+ collectionTail.splice(0, collectionTail.length - 8);
323
+ await drive(scheduler, sweeper.close());
324
+ const rows = sortedRows(model);
325
+ const [segments, transactions, manifests, currentManifest, leases, compactions, storageBytes] = await Promise.all([
326
+ collectStoragePages((cursor) => base.listSegmentPage(cursor, 256)),
327
+ collectStoragePages((cursor) => base.listTransactionPage(cursor, 256)),
328
+ collectStoragePages((cursor) => base.listManifestPage(cursor, 256)),
329
+ base.getCurrentManifest(),
330
+ base.listLeases(),
331
+ base.listCompactionJobs(),
332
+ base.getLogicalStorageBytes(),
333
+ ]);
334
+ const blockCount = currentManifest?.liveBlockCount ?? 0;
335
+ const blockLimit = plan.keySpace * 16 + 64;
336
+ const segmentLimit = plan.keySpace + 16;
337
+ const transactionLimit = segmentLimit + 16;
338
+ if (blockCount > blockLimit ||
339
+ segments.length > segmentLimit ||
340
+ transactions.length > transactionLimit) {
341
+ const transactionStates = transactions.reduce((counts, transaction) => {
342
+ counts[transaction.status] = (counts[transaction.status] ?? 0) + 1;
343
+ return counts;
344
+ }, {});
345
+ const compactionStates = compactions.reduce((counts, compaction) => {
346
+ counts[compaction.state] = (counts[compaction.state] ?? 0) + 1;
347
+ return counts;
348
+ }, {});
349
+ throw new Error(`Storage did not compact to a bound: ${String(blockCount)}/${String(blockLimit)} live blocks, ` +
350
+ `${String(segments.length)}/${String(segmentLimit)} segments, ` +
351
+ `${String(transactions.length)}/${String(transactionLimit)} transactions; ` +
352
+ `${JSON.stringify(transactionStates)} transaction states, ` +
353
+ `${JSON.stringify(compactionStates)} compaction states, ` +
354
+ `${String(currentManifest?.liveBlockCount ?? 0)} current blocks, compaction stopped at ${compactionStop}, ` +
355
+ `${String(manifests.length)} manifests (${String(manifests.filter((manifest) => manifest.prunedAt !== undefined).length)} pruned) remain after ${String(maintenanceCompactions)} compactions ` +
356
+ `and ${String(collectionPasses)} collection passes; collection tail ${JSON.stringify(collectionTail)}`);
357
+ }
358
+ if (manifests.length > 2) {
359
+ throw new Error(`Garbage collection left ${String(manifests.length)} manifests and ${String(leases.length)} leases`);
360
+ }
361
+ if (leases.length !== 0) {
362
+ throw new Error(`Simulation clients leaked ${String(leases.length)} leases`);
363
+ }
364
+ const trace = [...scheduler.trace];
365
+ return {
366
+ seed: plan.seed,
367
+ acceptedMutations,
368
+ rejectedConflicts,
369
+ injectedFaults,
370
+ checkpoints,
371
+ maintenanceCompactions,
372
+ compactionStop,
373
+ collectionPasses,
374
+ collectionTail,
375
+ rows,
376
+ blockCount,
377
+ segmentCount: segments.length,
378
+ transactionCount: transactions.length,
379
+ manifestCount: manifests.length,
380
+ leaseCount: leases.length,
381
+ storageBytes,
382
+ schedulerHighWater: scheduler.highWater,
383
+ trace,
384
+ traceDigest: md5Hex(JSON.stringify(trace)),
385
+ };
386
+ }
387
+ class FaultController {
388
+ #point;
389
+ #occurrence = 0;
390
+ #seen = 0;
391
+ fired = false;
392
+ arm(point, occurrence) {
393
+ this.#point = point;
394
+ this.#occurrence = occurrence;
395
+ this.#seen = 0;
396
+ this.fired = false;
397
+ }
398
+ disarm() {
399
+ this.#point = undefined;
400
+ }
401
+ inject(point) {
402
+ if (point !== this.#point || this.fired)
403
+ return;
404
+ this.#seen++;
405
+ if (this.#seen !== this.#occurrence)
406
+ return;
407
+ this.fired = true;
408
+ throw new Error(`injected ${point} #${String(this.#occurrence)}`);
409
+ }
410
+ }
411
+ async function drive(scheduler, promise) {
412
+ const result = (await scheduler.settle([promise]))[0];
413
+ if (result?.status === "fulfilled")
414
+ return result.value;
415
+ throw result?.reason ?? new Error("Scheduled operation did not settle");
416
+ }
417
+ function assertSettled(results) {
418
+ const failed = results.find((result) => result.status === "rejected");
419
+ if (failed?.status === "rejected")
420
+ throw failed.reason;
421
+ }
422
+ function executeMutation(database, operation) {
423
+ if (operation.kind === "delete") {
424
+ return database.execute("DELETE FROM items WHERE id = ?", [operation.key]);
425
+ }
426
+ return database.execute("INSERT INTO items (id, client, amount) VALUES (?, ?, ?) " +
427
+ "ON CONFLICT (id) DO UPDATE SET client = excluded.client, amount = excluded.amount", [operation.key, operation.client, operation.amount]);
428
+ }
429
+ function applyMutation(model, operation) {
430
+ if (operation.kind === "delete")
431
+ model.delete(operation.key);
432
+ else
433
+ model.set(operation.key, {
434
+ id: operation.key,
435
+ client: operation.client,
436
+ amount: operation.amount,
437
+ });
438
+ }
439
+ async function assertModel(scheduler, database, model) {
440
+ const actual = await readRows(scheduler, database);
441
+ if (!sameRows(actual, model)) {
442
+ throw new Error(`Model mismatch:\nexpected ${JSON.stringify(sortedRows(model))}\nreceived ${JSON.stringify(actual)}`);
443
+ }
444
+ }
445
+ async function readRows(scheduler, database) {
446
+ const result = await drive(scheduler, database.query("SELECT id, client, amount FROM items ORDER BY id", { memoize: false }));
447
+ return result.rows;
448
+ }
449
+ function sameRows(rows, model) {
450
+ return JSON.stringify(rows) === JSON.stringify(sortedRows(model));
451
+ }
452
+ function sortedRows(model) {
453
+ return [...model.values()].sort((left, right) => left.id - right.id);
454
+ }
455
+ function replaceModel(target, source) {
456
+ target.clear();
457
+ for (const [key, value] of source)
458
+ target.set(key, value);
459
+ }
460
+ function requireClient(clients, index) {
461
+ const client = clients[index];
462
+ if (client === undefined)
463
+ throw new RangeError(`Simulation client is out of range: ${String(index)}`);
464
+ return client;
465
+ }
466
+ function validateSteps(steps, clients, keySpace) {
467
+ const validPoint = new Set([
468
+ "beforeBlockWrite",
469
+ "afterBlockWrite",
470
+ "beforeBlockRead",
471
+ "afterBlockRead",
472
+ "beforeTransactionCommit",
473
+ "afterTransactionCommit",
474
+ ]);
475
+ const validateMutation = (value) => {
476
+ if (typeof value !== "object" || value === null) {
477
+ throw new TypeError("Simulation mutation must be an object");
478
+ }
479
+ const operation = value;
480
+ if (operation.kind !== "put" && operation.kind !== "delete") {
481
+ throw new TypeError("Simulation mutation kind is invalid");
482
+ }
483
+ const client = operation.client;
484
+ if (typeof client !== "number" ||
485
+ !Number.isSafeInteger(client) ||
486
+ client < 0 ||
487
+ client >= clients) {
488
+ throw new TypeError("Simulation mutation has an invalid client");
489
+ }
490
+ const key = operation.key;
491
+ if (typeof key !== "number" || !Number.isSafeInteger(key) || key < 1 || key > keySpace) {
492
+ throw new TypeError("Simulation mutation has an invalid key");
493
+ }
494
+ if (operation.kind === "put" &&
495
+ (typeof operation.amount !== "number" || !Number.isSafeInteger(operation.amount))) {
496
+ throw new TypeError("Simulation put has an invalid amount");
497
+ }
498
+ };
499
+ for (const value of steps) {
500
+ if (typeof value !== "object" || value === null) {
501
+ throw new TypeError("Simulation step must be an object");
502
+ }
503
+ const step = value;
504
+ if (step.kind === "concurrent") {
505
+ if (!Array.isArray(step.operations) || step.operations.length > clients * 4) {
506
+ throw new TypeError("Simulation concurrent step is unbounded");
507
+ }
508
+ step.operations.forEach(validateMutation);
509
+ }
510
+ else if (step.kind === "fault") {
511
+ if (typeof step.point !== "string" || !validPoint.has(step.point)) {
512
+ throw new TypeError("Simulation fault point is invalid");
513
+ }
514
+ const occurrence = step.occurrence;
515
+ if (typeof occurrence !== "number" || !Number.isSafeInteger(occurrence) || occurrence < 1) {
516
+ throw new TypeError("Simulation fault occurrence is invalid");
517
+ }
518
+ validateMutation(step.operation);
519
+ }
520
+ else if (step.kind !== "checkpoint" && step.kind !== "maintenance") {
521
+ throw new TypeError("Simulation step kind is invalid");
522
+ }
523
+ }
524
+ }
525
+ function errorMessage(error) {
526
+ return error instanceof Error ? error.message : String(error);
527
+ }
528
+ async function compactToFixedPoint(scheduler, database, workloadSteps) {
529
+ const maximumPasses = Math.min(10_000, workloadSteps + 128);
530
+ for (let pass = 0; pass < maximumPasses; pass++) {
531
+ const result = await drive(scheduler, database.compactTable("items"));
532
+ if (!result.compacted)
533
+ return { passes: pass, stop: result.skipReason ?? "unknown" };
534
+ }
535
+ throw new Error(`Compaction did not converge within ${String(maximumPasses)} passes`);
536
+ }
537
+ async function collectToFixedPoint(scheduler, database, workloadSteps) {
538
+ const maximumPasses = Math.min(10_000, workloadSteps + 128);
539
+ let emptyPasses = 0;
540
+ const tail = [];
541
+ for (let pass = 1; pass <= maximumPasses; pass++) {
542
+ const result = await drive(scheduler, database.collectGarbage());
543
+ tail.push(summarizeCollection(result));
544
+ if (tail.length > 8)
545
+ tail.shift();
546
+ const changed = result.prunedManifestCount +
547
+ result.reclaimedSegmentCount +
548
+ result.reclaimedBlockCount +
549
+ result.reclaimedTransactionCount;
550
+ // collectGarbage also prunes finished maintenance records after returning its counters. A
551
+ // zero-artifact pass can therefore remove the final provenance root that lets the next pass
552
+ // reclaim bytes. Two consecutive empty passes prove both layers have settled.
553
+ if (changed === 0) {
554
+ emptyPasses++;
555
+ if (emptyPasses === 2)
556
+ return { passes: pass, tail };
557
+ }
558
+ else
559
+ emptyPasses = 0;
560
+ }
561
+ throw new Error(`Garbage collection did not converge within ${String(maximumPasses)} passes`);
562
+ }
563
+ function summarizeCollection(result) {
564
+ return {
565
+ prunedManifests: result.prunedManifestCount,
566
+ retainedManifests: result.retainedManifestCount,
567
+ reclaimedSegments: result.reclaimedSegmentCount,
568
+ retainedSegments: result.retainedSegmentCount,
569
+ reclaimedBlocks: result.reclaimedBlockCount,
570
+ retainedBlocks: result.retainedBlockCount,
571
+ reclaimedTransactions: result.reclaimedTransactionCount,
572
+ retainedTransactions: result.retainedTransactionCount,
573
+ };
574
+ }
575
+ function mulberry32(seed) {
576
+ let state = seed >>> 0;
577
+ return () => {
578
+ state = (state + 0x6d2b79f5) >>> 0;
579
+ let mixed = state;
580
+ mixed = Math.imul(mixed ^ (mixed >>> 15), mixed | 1);
581
+ mixed ^= mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61);
582
+ return ((mixed ^ (mixed >>> 14)) >>> 0) / 4_294_967_296;
583
+ };
584
+ }
585
+ function greatestCommonDivisor(left, right) {
586
+ let a = left;
587
+ let b = right;
588
+ while (b !== 0)
589
+ [a, b] = [b, a % b];
590
+ return a;
591
+ }
@@ -0,0 +1,89 @@
1
+ import type { QueryResult, QueryValue } from "../engine/index.js";
2
+ export type SqlLogicType = "I" | "R" | "T";
3
+ export type SqlLogicSortMode = "nosort" | "rowsort" | "valuesort";
4
+ export interface SqlLogicLocation {
5
+ readonly file: string;
6
+ readonly line: number;
7
+ }
8
+ export interface SqlLogicCondition {
9
+ readonly kind: "onlyif" | "skipif";
10
+ readonly engine: string;
11
+ readonly location: SqlLogicLocation;
12
+ }
13
+ interface LocatedRecord {
14
+ readonly location: SqlLogicLocation;
15
+ }
16
+ export interface SqlLogicStatement extends LocatedRecord {
17
+ readonly kind: "statement";
18
+ readonly expectation: "ok" | "error";
19
+ readonly sql: string;
20
+ readonly conditions: readonly SqlLogicCondition[];
21
+ }
22
+ export interface SqlLogicExpectedValues {
23
+ readonly kind: "values";
24
+ readonly values: readonly string[];
25
+ }
26
+ export interface SqlLogicExpectedHash {
27
+ readonly kind: "hash";
28
+ readonly valueCount: number;
29
+ readonly hash: string;
30
+ }
31
+ export interface SqlLogicQuery extends LocatedRecord {
32
+ readonly kind: "query";
33
+ readonly types: readonly SqlLogicType[];
34
+ readonly sortMode: SqlLogicSortMode;
35
+ readonly label?: string;
36
+ readonly sql: string;
37
+ readonly expected: SqlLogicExpectedValues | SqlLogicExpectedHash;
38
+ readonly conditions: readonly SqlLogicCondition[];
39
+ }
40
+ export interface SqlLogicHashThreshold extends LocatedRecord {
41
+ readonly kind: "hash-threshold";
42
+ readonly valueCount: number;
43
+ }
44
+ export interface SqlLogicHalt extends LocatedRecord {
45
+ readonly kind: "halt";
46
+ }
47
+ export type SqlLogicRecord = SqlLogicStatement | SqlLogicQuery | SqlLogicHashThreshold | SqlLogicHalt;
48
+ export interface SqlLogicDatabase {
49
+ readonly engineName: string;
50
+ execute(sql: string): Promise<unknown>;
51
+ query(sql: string): Promise<QueryResult>;
52
+ close(): void | Promise<void>;
53
+ }
54
+ export interface SqlLogicRunStatistics {
55
+ readonly files: number;
56
+ readonly statements: number;
57
+ readonly queries: number;
58
+ readonly values: number;
59
+ readonly skipped: number;
60
+ readonly halted: boolean;
61
+ readonly hashThreshold: number;
62
+ }
63
+ export interface SqlLogicRunOptions {
64
+ /** Audit hook. Returning "continue" records the failure externally and runs the next record. */
65
+ readonly onFailure?: (failure: SqlLogicFailure, record: SqlLogicStatement | SqlLogicQuery) => "continue" | undefined;
66
+ }
67
+ export declare class SqlLogicParseError extends Error {
68
+ readonly location: SqlLogicLocation;
69
+ constructor(message: string, location: SqlLogicLocation);
70
+ }
71
+ export declare class SqlLogicFailure extends Error {
72
+ readonly location: SqlLogicLocation;
73
+ readonly sql?: string | undefined;
74
+ constructor(message: string, location: SqlLogicLocation, sql?: string | undefined, options?: ErrorOptions);
75
+ }
76
+ /** Parses the original SQLite SQLLogicTest format, rejecting unknown extensions. */
77
+ export declare function parseSqlLogicTest(source: string, file?: string): SqlLogicRecord[];
78
+ /**
79
+ * Parses a line source one record at a time. The full upstream corpus contains very large files;
80
+ * this form bounds parser memory to the largest individual record instead of the largest file.
81
+ */
82
+ export declare function parseSqlLogicTestLines(lines: Iterable<string> | AsyncIterable<string>, file?: string): AsyncGenerator<SqlLogicRecord>;
83
+ /** Runs already-parsed records against one empty database connection. */
84
+ export declare function runSqlLogicTest(records: Iterable<SqlLogicRecord> | AsyncIterable<SqlLogicRecord>, database: SqlLogicDatabase, options?: SqlLogicRunOptions): Promise<SqlLogicRunStatistics>;
85
+ /** Renders one value with the original SQLite SQLLogicTest adapter rules. */
86
+ export declare function renderSqlLogicValue(value: QueryValue, type: SqlLogicType | undefined): string;
87
+ /** Small browser-safe MD5 implementation for compatibility with the original corpus hashes. */
88
+ export declare function md5Hex(input: string): string;
89
+ export {};