@minnowdb/core 0.7.10 → 0.9.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 (75) hide show
  1. package/dist/engine/artifact-cache.js +3 -2
  2. package/dist/engine/buffered-writer.js +12 -2
  3. package/dist/engine/client.d.ts +9 -0
  4. package/dist/engine/client.js +112 -20
  5. package/dist/engine/database.d.ts +5 -3
  6. package/dist/engine/database.js +2199 -1675
  7. package/dist/engine/errors.d.ts +21 -2
  8. package/dist/engine/errors.js +30 -1
  9. package/dist/engine/index.d.ts +1 -0
  10. package/dist/engine/index.js +4 -0
  11. package/dist/engine/live-accept.js +13 -0
  12. package/dist/engine/live-aggregate.js +264 -0
  13. package/dist/engine/live-patch.d.ts +21 -0
  14. package/dist/engine/live-patch.js +31 -0
  15. package/dist/engine/live.d.ts +14 -0
  16. package/dist/engine/live.js +146 -142
  17. package/dist/engine/optimizer.js +11 -6
  18. package/dist/engine/query-cache.js +19 -15
  19. package/dist/engine/query-generations.js +61 -0
  20. package/dist/engine/query-identity.js +41 -0
  21. package/dist/engine/query.d.ts +7 -13
  22. package/dist/engine/query.js +298 -435
  23. package/dist/engine/result-state.d.ts +7 -0
  24. package/dist/engine/result-state.js +15 -0
  25. package/dist/engine/sql-domains.js +121 -0
  26. package/dist/engine/sql-semantics.js +7 -4
  27. package/dist/engine/typed-live.js +28 -20
  28. package/dist/engine/vector.d.ts +4 -0
  29. package/dist/engine/vector.js +14 -14
  30. package/dist/engine/windows.d.ts +12 -0
  31. package/dist/engine/windows.js +387 -0
  32. package/dist/engine/worker-server.d.ts +1 -1
  33. package/dist/engine/worker-server.js +28 -15
  34. package/dist/engine/write-coordinator.js +54 -0
  35. package/dist/plan/model.d.ts +1 -1
  36. package/dist/storage/indexeddb.js +134 -89
  37. package/dist/storage/opfs/index.d.ts +1 -1
  38. package/dist/storage/opfs/index.js +3 -1
  39. package/dist/storage/opfs/leader.js +20 -9
  40. package/dist/storage/opfs/rpc.js +3 -1
  41. package/dist/storage/opfs/store.d.ts +2 -0
  42. package/dist/storage/opfs/store.js +119 -43
  43. package/dist/storage/toolkit/record-core.js +2 -1
  44. package/dist/storage/types.d.ts +14 -0
  45. package/dist/storage/types.js +21 -0
  46. package/dist/transactions/index.d.ts +3 -0
  47. package/dist/transactions/index.js +4 -1
  48. package/dist/worker-protocol/index.d.ts +1 -1
  49. package/dist/worker-protocol/index.js +1 -1
  50. package/package.json +2 -2
  51. package/postgres-feature-profile.json +6 -1
  52. package/sql-feature-matrix.json +20 -27
  53. package/dist/date-value.d.ts +0 -20
  54. package/dist/engine/artifact-cache.d.ts +0 -29
  55. package/dist/engine/byte-estimates.d.ts +0 -11
  56. package/dist/engine/cancellation.d.ts +0 -2
  57. package/dist/engine/defaults.d.ts +0 -29
  58. package/dist/engine/group-index.d.ts +0 -33
  59. package/dist/engine/join-index.d.ts +0 -10
  60. package/dist/engine/live-equal.d.ts +0 -7
  61. package/dist/engine/point-read.d.ts +0 -59
  62. package/dist/engine/query-cache.d.ts +0 -21
  63. package/dist/engine/result-wire.d.ts +0 -70
  64. package/dist/engine/sort-keys.d.ts +0 -73
  65. package/dist/engine/sql-domains.d.ts +0 -93
  66. package/dist/engine/sql-functions.d.ts +0 -11
  67. package/dist/engine/sql-json.d.ts +0 -40
  68. package/dist/engine/sql-semantics.d.ts +0 -66
  69. package/dist/engine/worker-store-indexeddb.d.ts +0 -2
  70. package/dist/engine/worker-store-memory.d.ts +0 -2
  71. package/dist/engine/worker-store-opfs.d.ts +0 -2
  72. package/dist/engine/write-block-planner.d.ts +0 -19
  73. package/dist/storage/opfs/leader.d.ts +0 -460
  74. package/dist/storage/opfs/rpc.d.ts +0 -82
  75. package/dist/storage/opfs/snapshot-ledger.d.ts +0 -41
@@ -0,0 +1,387 @@
1
+ import { QueryMemoryContext } from "./memory.js";
2
+ import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
3
+ import { compareSqlValues, defineSqlResultProperty } from "./sql-semantics.js";
4
+ import { exactNumericBinary, exactNumericValue, isExactNumeric } from "./sql-domains.js";
5
+ import { throwIfAborted } from "./cancellation.js";
6
+ class FrameAggregate {
7
+ #width;
8
+ #values;
9
+ #counts;
10
+ #window;
11
+ #exact;
12
+ #prefixEnd = 0;
13
+ #prefixTotal = null;
14
+ #prefixCount = 0;
15
+ constructor(values, window, memory, prefix) {
16
+ this.#window = window;
17
+ this.#exact = values.some(isExactNumeric);
18
+ if (prefix) {
19
+ this.#width = 0;
20
+ this.#values = values;
21
+ this.#counts = new Uint32Array(0);
22
+ return;
23
+ }
24
+ const width = 2 ** Math.ceil(Math.log2(Math.max(1, values.length)));
25
+ this.#width = width;
26
+ memory.tally(width * 2 * 20, "Window aggregate tree");
27
+ this.#values = new Array(width * 2).fill(null);
28
+ this.#counts = new Uint32Array(width * 2);
29
+ for (let index = 0; index < values.length; index += 1) {
30
+ const value = values[index] ?? null;
31
+ this.#values[width + index] = value;
32
+ this.#counts[width + index] = value === null ? 0 : 1;
33
+ }
34
+ for (let index = width - 1; index > 0; index -= 1) {
35
+ this.#counts[index] = (this.#counts[index * 2] ?? 0) + (this.#counts[index * 2 + 1] ?? 0);
36
+ this.#values[index] = this.#combine(this.#values[index * 2] ?? null, this.#values[index * 2 + 1] ?? null);
37
+ }
38
+ }
39
+ #combine(left, right) {
40
+ if (left === null)
41
+ return right;
42
+ if (right === null)
43
+ return left;
44
+ if (this.#window.name === "COUNT")
45
+ return null;
46
+ if (this.#window.name === "MIN")
47
+ return compareSqlValues(left, right) <= 0 ? left : right;
48
+ if (this.#window.name === "MAX")
49
+ return compareSqlValues(left, right) >= 0 ? left : right;
50
+ if (this.#exact) {
51
+ const total = exactNumericBinary("+", left, right);
52
+ if (total === void 0)
53
+ throw new TypeError("Window SUM requires numeric values");
54
+ return total;
55
+ }
56
+ if (typeof left !== "number" || typeof right !== "number")
57
+ throw new TypeError("Window SUM requires numeric values");
58
+ return left + right;
59
+ }
60
+ prefixValue(end, scale) {
61
+ while (this.#prefixEnd < end) {
62
+ const value = this.#values[this.#prefixEnd] ?? null;
63
+ this.#prefixEnd += 1;
64
+ if (value === null)
65
+ continue;
66
+ this.#prefixCount += 1;
67
+ this.#prefixTotal = this.#combine(this.#prefixTotal, value);
68
+ }
69
+ return this.#finish(this.#prefixTotal, this.#prefixCount, scale);
70
+ }
71
+ value(ranges, scale) {
72
+ let total = null;
73
+ let count = 0;
74
+ for (const [from, to] of ranges) {
75
+ let low = from + this.#width;
76
+ let high = to + this.#width;
77
+ let left = null;
78
+ let right = null;
79
+ while (low < high) {
80
+ if (low % 2 === 1) {
81
+ count += this.#counts[low] ?? 0;
82
+ left = this.#combine(left, this.#values[low] ?? null);
83
+ low += 1;
84
+ }
85
+ if (high % 2 === 1) {
86
+ high -= 1;
87
+ count += this.#counts[high] ?? 0;
88
+ right = this.#combine(this.#values[high] ?? null, right);
89
+ }
90
+ low = Math.floor(low / 2);
91
+ high = Math.floor(high / 2);
92
+ }
93
+ total = this.#combine(total, this.#combine(left, right));
94
+ }
95
+ return this.#finish(total, count, scale);
96
+ }
97
+ #finish(total, count, scale) {
98
+ if (this.#window.name === "COUNT")
99
+ return count;
100
+ if (count === 0)
101
+ return null;
102
+ if (this.#window.name !== "AVG")
103
+ return total;
104
+ if (this.#exact)
105
+ return exactNumericBinary("/", total ?? exactNumericValue(0), count, scale) ?? null;
106
+ if (typeof total !== "number")
107
+ throw new TypeError("Window AVG requires numeric values");
108
+ return total / count;
109
+ }
110
+ }
111
+ function frameRanges(partition, window, position) {
112
+ const size = partition.indexes.length;
113
+ const frame = window.frame ?? {
114
+ unit: "range",
115
+ start: { kind: "unbounded-preceding" },
116
+ end: { kind: window.orderAliases.length === 0 ? "unbounded-following" : "current-row" }
117
+ };
118
+ const groupEdge = (group) => group < 0 ? 0 : partition.groupStarts[group] ?? size;
119
+ const bound = (edge, start) => {
120
+ switch (edge.kind) {
121
+ case "unbounded-preceding":
122
+ return 0;
123
+ case "unbounded-following":
124
+ return size;
125
+ case "current-row":
126
+ return frame.unit === "rows" ? position + (start ? 0 : 1) : start ? partition.peerStart[position] ?? 0 : partition.peerEnd[position] ?? size;
127
+ case "preceding":
128
+ case "following": {
129
+ const delta = (edge.offset ?? 0) * (edge.kind === "preceding" ? -1 : 1);
130
+ if (frame.unit === "range") {
131
+ const current = partition.orderValues[position] ?? null;
132
+ if (current === null)
133
+ return start ? partition.peerStart[position] ?? 0 : partition.peerEnd[position] ?? size;
134
+ if (typeof current !== "number" && !isExactNumeric(current))
135
+ throw new TypeError("Offset RANGE frames require numeric ORDER BY values");
136
+ const order = window.orderAliases[0];
137
+ if (window.orderAliases.length !== 1 || order === void 0)
138
+ throw new TypeError("Offset RANGE frames require exactly one ORDER BY expression");
139
+ const descending = order.direction === "desc";
140
+ const distance = descending ? -delta : delta;
141
+ const target = isExactNumeric(current) ? exactNumericBinary("+", current, distance) ?? null : current + distance;
142
+ let low2 = 0;
143
+ let high2 = size;
144
+ while (low2 < high2) {
145
+ const middle = Math.floor((low2 + high2) / 2);
146
+ const value = partition.orderValues[middle] ?? null;
147
+ const comparison = value === null ? (order.nulls ?? (descending ? "first" : "last")) === "first" ? -1 : 1 : compareSqlValues(value, target) * (descending ? -1 : 1);
148
+ if (comparison < 0 || !start && comparison === 0)
149
+ low2 = middle + 1;
150
+ else
151
+ high2 = middle;
152
+ }
153
+ return low2;
154
+ }
155
+ return frame.unit === "groups" ? groupEdge((partition.groupOrdinal[position] ?? 0) + delta + (start ? 0 : 1)) : position + delta + (start ? 0 : 1);
156
+ }
157
+ }
158
+ };
159
+ const low = Math.max(0, Math.min(size, bound(frame.start, true)));
160
+ const high = Math.max(0, Math.min(size, bound(frame.end, false)));
161
+ if (high <= low)
162
+ return [];
163
+ if (frame.exclude === void 0 || frame.exclude === "no-others")
164
+ return [[low, high]];
165
+ const from = frame.exclude === "current-row" ? position : partition.peerStart[position] ?? position;
166
+ const to = frame.exclude === "current-row" ? position + 1 : partition.peerEnd[position] ?? position + 1;
167
+ const ranges = [];
168
+ if (low < Math.min(high, from))
169
+ ranges.push([low, Math.min(high, from)]);
170
+ if (frame.exclude === "ties" && position >= low && position < high)
171
+ ranges.push([position, position + 1]);
172
+ if (Math.max(low, to) < high)
173
+ ranges.push([Math.max(low, to), high]);
174
+ return ranges;
175
+ }
176
+ function* applyPartition(rows, partition, window, result, memory) {
177
+ const { indexes, peerStart, peerEnd, groupOrdinal } = partition;
178
+ const size = indexes.length;
179
+ memory.tally(size * 8, "Window argument values");
180
+ const values = Array.from(indexes, (index) => window.argumentAlias === void 0 ? 1 : rows[index]?.[window.argumentAlias] ?? null);
181
+ const aggregates = /* @__PURE__ */ new Set(["SUM", "AVG", "COUNT", "MIN", "MAX"]);
182
+ const frame = window.frame;
183
+ const prefix = frame === void 0 || (frame.exclude === void 0 || frame.exclude === "no-others") && frame.start.kind === "unbounded-preceding" && (frame.end.kind === "current-row" || frame.end.kind === "unbounded-following");
184
+ const whole = frame?.end.kind === "unbounded-following" || frame === void 0 && window.orderAliases.length === 0;
185
+ const aggregate = aggregates.has(window.name) ? new FrameAggregate(values, window, memory, prefix) : void 0;
186
+ const domain = result.columnDomains[result.columns.indexOf(window.argumentAlias ?? "")];
187
+ for (let position = 0; position < size; position += 1) {
188
+ if (position % 2048 === 0)
189
+ yield;
190
+ let value;
191
+ switch (window.name) {
192
+ case "ROW_NUMBER":
193
+ value = position + 1;
194
+ break;
195
+ case "RANK":
196
+ value = (peerStart[position] ?? 0) + 1;
197
+ break;
198
+ case "DENSE_RANK":
199
+ value = (groupOrdinal[position] ?? 0) + 1;
200
+ break;
201
+ case "PERCENT_RANK":
202
+ value = size === 1 ? 0 : (peerStart[position] ?? 0) / (size - 1);
203
+ break;
204
+ case "CUME_DIST":
205
+ value = (peerEnd[position] ?? size) / size;
206
+ break;
207
+ case "NTILE": {
208
+ const buckets = window.offset ?? 1;
209
+ const width = Math.floor(size / buckets);
210
+ const extra = size % buckets;
211
+ const larger = (width + 1) * extra;
212
+ value = position < larger ? Math.floor(position / (width + 1)) + 1 : extra + Math.floor((position - larger) / width) + 1;
213
+ break;
214
+ }
215
+ case "LAG":
216
+ case "LEAD": {
217
+ const target = position + (window.offset ?? 1) * (window.name === "LAG" ? -1 : 1);
218
+ value = target < 0 || target >= size ? window.fallback ?? null : values[target] ?? null;
219
+ break;
220
+ }
221
+ default: {
222
+ if (aggregate !== void 0 && prefix) {
223
+ const end = whole ? size : frame?.unit === "rows" ? position + 1 : peerEnd[position] ?? size;
224
+ value = aggregate.prefixValue(end, domain?.kind === "numeric" ? domain.scale : void 0);
225
+ break;
226
+ }
227
+ const ranges = frameRanges(partition, window, position);
228
+ if (aggregate !== void 0)
229
+ value = aggregate.value(ranges, domain?.kind === "numeric" ? domain.scale : void 0);
230
+ else {
231
+ let remaining = window.name === "NTH_VALUE" ? window.offset ?? 1 : 1;
232
+ value = null;
233
+ if (window.name === "LAST_VALUE") {
234
+ const last = ranges.at(-1);
235
+ value = last === void 0 ? null : values[last[1] - 1] ?? null;
236
+ } else {
237
+ for (const [from, to] of ranges) {
238
+ if (remaining <= to - from) {
239
+ value = values[from + remaining - 1] ?? null;
240
+ break;
241
+ }
242
+ remaining -= to - from;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ const row = rows[indexes[position] ?? -1];
249
+ if (row !== void 0) {
250
+ if (window.alias in row)
251
+ defineSqlResultProperty(row, window.alias, value);
252
+ else
253
+ row[window.alias] = value;
254
+ }
255
+ }
256
+ }
257
+ function* windowSteps(result, windows, options) {
258
+ const memory = options.memoryContext ?? new QueryMemoryContext();
259
+ const owned = options.memoryContext === void 0;
260
+ const rows = [];
261
+ try {
262
+ memory.tally(result.rows.length * (24 + windows.length * 16 + (options.copyRows === false ? 0 : 48 + result.columns.length * 16)), "Window result rows");
263
+ for (let index = 0; index < result.rows.length; index += 1) {
264
+ if (index % 2048 === 0)
265
+ yield;
266
+ const row = result.rows[index] ?? {};
267
+ rows.push(options.copyRows === false ? row : { ...row });
268
+ }
269
+ const groups = /* @__PURE__ */ new Map();
270
+ for (const window of windows) {
271
+ const key = JSON.stringify([window.partitionAliases, window.orderAliases]);
272
+ const group = groups.get(key);
273
+ if (group === void 0)
274
+ groups.set(key, [window]);
275
+ else
276
+ group.push(window);
277
+ }
278
+ for (const group of groups.values()) {
279
+ const window = group[0];
280
+ if (window === void 0)
281
+ continue;
282
+ const work = memory.createChild();
283
+ try {
284
+ const aliases = [
285
+ ...window.partitionAliases,
286
+ ...window.orderAliases.map(({ alias }) => alias)
287
+ ];
288
+ const peers = group.some((member) => !["ROW_NUMBER", "NTILE", "LAG", "LEAD"].includes(member.name) && (["RANK", "DENSE_RANK", "PERCENT_RANK", "CUME_DIST"].includes(member.name) || member.frame?.unit !== "rows" || member.frame.exclude === "group" || member.frame.exclude === "ties"));
289
+ const range = group.some((member) => member.frame?.unit === "range");
290
+ work.tally(rows.length * (20 + (peers ? 20 : 0) + (range ? 8 : 0) + aliases.length * 40), "Window sort and partition buffers");
291
+ const columns = aliases.map((alias) => buildSortKeyColumn(rows.length, (index) => rows[index]?.[alias] ?? null));
292
+ yield;
293
+ const indexes = sortKeyIndexes(rows.length, columns.map((column, index) => {
294
+ const order = window.orderAliases[index - window.partitionAliases.length];
295
+ return { column, descending: order?.direction === "desc", nulls: order?.nulls };
296
+ }));
297
+ const partitions = columns.slice(0, window.partitionAliases.length);
298
+ const orders = columns.slice(window.partitionAliases.length);
299
+ let start = 0;
300
+ while (start < indexes.length) {
301
+ let end = start + 1;
302
+ while (end < indexes.length && partitions.every((column) => column.compare(indexes[start] ?? 0, indexes[end] ?? 0) === 0)) {
303
+ if (end % 2048 === 0)
304
+ yield;
305
+ end += 1;
306
+ }
307
+ const size = end - start;
308
+ const partition = {
309
+ indexes: indexes.subarray(start, end),
310
+ peerStart: new Int32Array(peers ? size : 0),
311
+ peerEnd: new Int32Array(peers ? size : 0),
312
+ groupOrdinal: new Int32Array(peers ? size : 0),
313
+ groupStarts: [],
314
+ orderValues: range ? Array.from(indexes.subarray(start, end), (index) => rows[index]?.[window.orderAliases[0]?.alias ?? ""] ?? null) : []
315
+ };
316
+ let begin = 0;
317
+ for (let position = 1; peers && position <= size; position += 1) {
318
+ if (position % 2048 === 0)
319
+ yield;
320
+ if (position === size || orders.some((column) => column.compare(indexes[start + begin] ?? 0, indexes[start + position] ?? 0) !== 0)) {
321
+ const ordinal = partition.groupStarts.length;
322
+ partition.groupStarts.push(begin);
323
+ partition.peerStart.fill(begin, begin, position);
324
+ partition.peerEnd.fill(position, begin, position);
325
+ partition.groupOrdinal.fill(ordinal, begin, position);
326
+ begin = position;
327
+ }
328
+ }
329
+ for (const member of group) {
330
+ const frameMemory = work.createChild();
331
+ try {
332
+ yield* applyPartition(rows, partition, member, result, frameMemory);
333
+ } finally {
334
+ frameMemory.close();
335
+ }
336
+ }
337
+ start = end;
338
+ }
339
+ } finally {
340
+ work.close();
341
+ }
342
+ }
343
+ return {
344
+ columns: [...result.columns, ...windows.map(({ alias }) => alias)],
345
+ columnDomains: [...result.columnDomains, ...windows.map(() => null)],
346
+ rows
347
+ };
348
+ } finally {
349
+ if (owned)
350
+ memory.close();
351
+ }
352
+ }
353
+ function applyWindowFunctions(result, windows, options = {}) {
354
+ const steps = windowSteps(result, windows, options);
355
+ try {
356
+ for (; ; ) {
357
+ throwIfAborted(options.signal);
358
+ const step = steps.next();
359
+ if (step.done)
360
+ return step.value;
361
+ }
362
+ } finally {
363
+ steps.return({ columns: [], columnDomains: [], rows: [] });
364
+ }
365
+ }
366
+ async function applyWindowFunctionsAsync(result, windows, options = {}) {
367
+ const steps = windowSteps(result, windows, options);
368
+ let yieldedAt = performance.now();
369
+ try {
370
+ for (; ; ) {
371
+ throwIfAborted(options.signal);
372
+ const step = steps.next();
373
+ if (step.done)
374
+ return step.value;
375
+ if (performance.now() - yieldedAt >= 8) {
376
+ await new Promise((resolve) => setTimeout(resolve, 0));
377
+ yieldedAt = performance.now();
378
+ }
379
+ }
380
+ } finally {
381
+ steps.return({ columns: [], columnDomains: [], rows: [] });
382
+ }
383
+ }
384
+ export {
385
+ applyWindowFunctions,
386
+ applyWindowFunctionsAsync
387
+ };
@@ -26,7 +26,7 @@ export type StoreDescriptor = {
26
26
  durability?: "relaxed" | "strict";
27
27
  };
28
28
  /** The cloneable subset of MinnowDatabaseOptions; function-valued seams stay worker-side. */
29
- export type WireDatabaseOptions = Pick<MinnowDatabaseOptions, "compression" | "targetBlockBytes" | "rowsPerBlock" | "maxCommitRetries" | "spillOwnerLeaseMs" | "transactionOwnerLeaseMs" | "transactionIdleTimeoutMs" | "bufferPoolBytes" | "executionMemoryBudgetBytes" | "autoCollectDebtLimitCommits" | "autoCollect" | "autoCompact">;
29
+ export type WireDatabaseOptions = Pick<MinnowDatabaseOptions, "compression" | "targetBlockBytes" | "rowsPerBlock" | "maxCommitRetries" | "coordinateWrites" | "spillOwnerLeaseMs" | "transactionOwnerLeaseMs" | "transactionIdleTimeoutMs" | "bufferPoolBytes" | "executionMemoryBudgetBytes" | "autoCollectDebtLimitCommits" | "autoCollect" | "autoCompact">;
30
30
  export interface DatabaseInitPayload {
31
31
  store: StoreDescriptor;
32
32
  options?: WireDatabaseOptions;
@@ -455,7 +455,9 @@ class DatabaseRpcServer {
455
455
  resolveVersion = resolve;
456
456
  rejectVersion = reject;
457
457
  });
458
+ let snapshotSession;
458
459
  const done = this.database.snapshot(async (session) => {
460
+ snapshotSession = session;
459
461
  resolveVersion(session.version);
460
462
  await new Promise((resolveRelease) => {
461
463
  release = resolveRelease;
@@ -466,7 +468,7 @@ class DatabaseRpcServer {
466
468
  const version = await versionReady;
467
469
  if (release === void 0)
468
470
  throw new Error("Snapshot scope did not install its release");
469
- this.#publishHandle(handleId, { type: "snapshot", release, done });
471
+ this.#publishHandle(handleId, { type: "snapshot", session: snapshotSession, release, done });
470
472
  return { handleId, version };
471
473
  } catch (error) {
472
474
  release?.();
@@ -494,6 +496,9 @@ class DatabaseRpcServer {
494
496
  }
495
497
  switch (handle.type) {
496
498
  case "snapshot": {
499
+ if (method === "query") {
500
+ return this.#querySession(handle.session, args, context);
501
+ }
497
502
  if (method !== "close")
498
503
  throw new Error(`Unsupported snapshot method: ${method}`);
499
504
  handle.release();
@@ -537,19 +542,21 @@ class DatabaseRpcServer {
537
542
  }
538
543
  }
539
544
  }
545
+ async #querySession(session, args, context) {
546
+ const [sql, options, reportStats = false] = args;
547
+ return new ColumnarResult(encodeQueryResult(await session.query(sql, {
548
+ ...options,
549
+ signal: context.signal,
550
+ ...reportStats ? {
551
+ onStats: (stats) => {
552
+ this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
553
+ }
554
+ } : {}
555
+ })));
556
+ }
540
557
  async #callWriteHandle(handleId, handle, method, args, context) {
541
- if (method === "query") {
542
- const [sql, options, reportStats = false] = args;
543
- return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, {
544
- ...options,
545
- signal: context.signal,
546
- ...reportStats ? {
547
- onStats: (stats) => {
548
- this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
549
- }
550
- } : {}
551
- })));
552
- }
558
+ if (method === "query")
559
+ return this.#querySession(handle.session, args, context);
553
560
  if (method === "execute") {
554
561
  const [sql, params] = args;
555
562
  return handle.session.execute(sql, params);
@@ -665,15 +672,21 @@ class DatabaseRpcServer {
665
672
  case "subscribe": {
666
673
  const subscriptionId = this.#claimHandleId(args[0]);
667
674
  const query = args[1];
675
+ const patches = args[2]?.patches === true;
668
676
  let subscription;
669
677
  try {
670
678
  subscription = await handle.set.subscribe(query, {
671
679
  onChange: (result, delivery) => {
672
- const encoded = encodeQueryResult(result);
680
+ const patch = patches && !delivery.initial && delivery.retained !== void 0;
681
+ const encoded = encodeQueryResult(patch ? {
682
+ columns: result.columns,
683
+ columnDomains: result.columnDomains,
684
+ rows: result.rows.filter((_, index) => (delivery.retained?.[index] ?? -1) < 0)
685
+ } : result);
673
686
  const retained = delivery.retained?.slice();
674
687
  if (retained !== void 0)
675
688
  encoded.transfer.push(retained.buffer);
676
- this.scope.postMessage(rpcEvent(subscriptionId, "change", {
689
+ this.scope.postMessage(rpcEvent(subscriptionId, patch ? "patch" : "change", {
677
690
  result: encoded.payload,
678
691
  delivery: retained === void 0 ? delivery : { ...delivery, retained }
679
692
  }), { transfer: encoded.transfer });
@@ -0,0 +1,54 @@
1
+ const anonymous = /* @__PURE__ */ new WeakMap();
2
+ const named = /* @__PURE__ */ new Map();
3
+ async function coordinateWrite(store, run, signal) {
4
+ signal.throwIfAborted();
5
+ const name = store.liveQueryChannelName;
6
+ let queue = name === void 0 ? anonymous.get(store) : named.get(name);
7
+ if (queue === void 0) {
8
+ queue = { tail: Promise.resolve() };
9
+ if (name === void 0)
10
+ anonymous.set(store, queue);
11
+ else
12
+ named.set(name, queue);
13
+ }
14
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
15
+ const lockController = new AbortController();
16
+ let admitted = false;
17
+ const enter = () => {
18
+ signal.throwIfAborted();
19
+ admitted = true;
20
+ return run();
21
+ };
22
+ const operation = queue.tail.then(async () => {
23
+ signal.throwIfAborted();
24
+ return name !== void 0 && locks !== void 0 ? await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter) : await enter();
25
+ });
26
+ const settled = operation.then(() => void 0, () => void 0);
27
+ queue.tail = settled;
28
+ void settled.then(() => {
29
+ if (queue.tail !== settled)
30
+ return;
31
+ if (name === void 0)
32
+ anonymous.delete(store);
33
+ else
34
+ named.delete(name);
35
+ });
36
+ let abort = () => void 0;
37
+ const cancelled = new Promise((_resolve, reject) => {
38
+ abort = () => {
39
+ if (!admitted) {
40
+ lockController.abort(signal.reason);
41
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Write admission cancelled", { cause: signal.reason }));
42
+ }
43
+ };
44
+ signal.addEventListener("abort", abort, { once: true });
45
+ });
46
+ try {
47
+ return await Promise.race([operation, cancelled]);
48
+ } finally {
49
+ signal.removeEventListener("abort", abort);
50
+ }
51
+ }
52
+ export {
53
+ coordinateWrite
54
+ };
@@ -17,7 +17,7 @@ export type ComparisonOperator = "=" | "!=" | "<>" | ">" | ">=" | "<" | "<=";
17
17
  export type AggregateName = "COUNT" | "SUM" | "AVG" | "MIN" | "MAX" | "JSON_ARRAYAGG" | "STRING_AGG"
18
18
  /** Optimizer-only aggregate that enforces scalar-subquery cardinality. */
19
19
  | "MINNOW_SINGLE_VALUE";
20
- export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "TO_JSON" | "IS_JSON" | "ARRAY"
20
+ export type ScalarFunctionName = "ROUND" | "COALESCE" | "DATE_TRUNC" | "DATE_ADD" | "UPPER" | "LOWER" | "LENGTH" | "ABS" | "TRIM" | "LTRIM" | "RTRIM" | "SUBSTR" | "REPLACE" | "INSTR" | "NULLIF" | "GREATEST" | "LEAST" | "FLOOR" | "CEIL" | "MOD" | "POWER" | "SQRT" | "EXTRACT" | "CAST" | "OCTET_LENGTH" | "LPAD" | "RPAD" | "OVERLAY" | "CURRENT_DATE" | "CURRENT_TIMESTAMP" | "LOCALTIME" | "GROUPING" | "JSON_VALUE" | "JSON_QUERY" | "JSON_EXISTS" | "JSON_OBJECT" | "JSON_ARRAY" | "TO_JSON" | "IS_JSON" | "MINNOW_ARRAY_AT" | "MINNOW_ARRAY_FROM_JSON" | "MINNOW_ARRAY_ELEMENT" | "ARRAY"
21
21
  /** Parser-produced `->` JSON member/element access returning a JSON value. */
22
22
  | "MINNOW_JSON_GET"
23
23
  /** Parser-produced `->>` JSON member/element access returning text. */