@minnowdb/core 0.7.8 → 0.8.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.
- package/dist/engine/client.d.ts +5 -2
- package/dist/engine/client.js +14 -4
- package/dist/engine/database.js +632 -39
- package/dist/engine/errors.d.ts +2 -2
- package/dist/engine/errors.js +1 -1
- package/dist/engine/keyed-live.js +55 -18
- package/dist/engine/live-aggregate.d.ts +12 -0
- package/dist/engine/live-aggregate.js +226 -0
- package/dist/engine/live-patch.d.ts +21 -0
- package/dist/engine/live-patch.js +31 -0
- package/dist/engine/live.d.ts +107 -8
- package/dist/engine/live.js +353 -187
- package/dist/engine/optimizer.js +11 -6
- package/dist/engine/query-cache.js +2 -13
- package/dist/engine/query-generations.d.ts +8 -0
- package/dist/engine/query-generations.js +61 -0
- package/dist/engine/query-identity.d.ts +3 -0
- package/dist/engine/query-identity.js +41 -0
- package/dist/engine/query.d.ts +6 -11
- package/dist/engine/query.js +294 -428
- package/dist/engine/sql-domains.d.ts +4 -0
- package/dist/engine/sql-domains.js +121 -0
- package/dist/engine/sql-semantics.js +7 -4
- package/dist/engine/typed-live.d.ts +12 -1
- package/dist/engine/typed-live.js +110 -25
- package/dist/engine/windows.d.ts +12 -0
- package/dist/engine/windows.js +386 -0
- package/dist/engine/worker-server.js +12 -5
- package/dist/plan/model.d.ts +1 -1
- package/dist/worker-protocol/index.d.ts +1 -1
- package/dist/worker-protocol/index.js +1 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +6 -1
- package/sql-feature-matrix.json +20 -27
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { QueryMemoryContext } from "./memory.js";
|
|
2
|
+
import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
|
|
3
|
+
import { compareSqlValues } 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
|
+
Object.defineProperty(row, window.alias, {
|
|
251
|
+
value,
|
|
252
|
+
enumerable: true,
|
|
253
|
+
writable: true,
|
|
254
|
+
configurable: true
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function* windowSteps(result, windows, options) {
|
|
259
|
+
const memory = options.memoryContext ?? new QueryMemoryContext();
|
|
260
|
+
const owned = options.memoryContext === void 0;
|
|
261
|
+
const rows = [];
|
|
262
|
+
try {
|
|
263
|
+
memory.tally(result.rows.length * (24 + windows.length * 16 + (options.copyRows === false ? 0 : 48 + result.columns.length * 16)), "Window result rows");
|
|
264
|
+
for (let index = 0; index < result.rows.length; index += 1) {
|
|
265
|
+
if (index % 2048 === 0)
|
|
266
|
+
yield;
|
|
267
|
+
const row = result.rows[index] ?? {};
|
|
268
|
+
rows.push(options.copyRows === false ? row : { ...row });
|
|
269
|
+
}
|
|
270
|
+
const groups = /* @__PURE__ */ new Map();
|
|
271
|
+
for (const window of windows) {
|
|
272
|
+
const key = JSON.stringify([window.partitionAliases, window.orderAliases]);
|
|
273
|
+
const group = groups.get(key);
|
|
274
|
+
if (group === void 0)
|
|
275
|
+
groups.set(key, [window]);
|
|
276
|
+
else
|
|
277
|
+
group.push(window);
|
|
278
|
+
}
|
|
279
|
+
for (const group of groups.values()) {
|
|
280
|
+
const window = group[0];
|
|
281
|
+
if (window === void 0)
|
|
282
|
+
continue;
|
|
283
|
+
const work = memory.createChild();
|
|
284
|
+
try {
|
|
285
|
+
const aliases = [
|
|
286
|
+
...window.partitionAliases,
|
|
287
|
+
...window.orderAliases.map(({ alias }) => alias)
|
|
288
|
+
];
|
|
289
|
+
work.tally(rows.length * (48 + aliases.length * 40), "Window sort and partition buffers");
|
|
290
|
+
const columns = aliases.map((alias) => buildSortKeyColumn(rows.length, (index) => rows[index]?.[alias] ?? null));
|
|
291
|
+
yield;
|
|
292
|
+
const indexes = sortKeyIndexes(rows.length, columns.map((column, index) => {
|
|
293
|
+
const order = window.orderAliases[index - window.partitionAliases.length];
|
|
294
|
+
return { column, descending: order?.direction === "desc", nulls: order?.nulls };
|
|
295
|
+
}));
|
|
296
|
+
const partitions = columns.slice(0, window.partitionAliases.length);
|
|
297
|
+
const orders = columns.slice(window.partitionAliases.length);
|
|
298
|
+
let start = 0;
|
|
299
|
+
while (start < indexes.length) {
|
|
300
|
+
let end = start + 1;
|
|
301
|
+
while (end < indexes.length && partitions.every((column) => column.compare(indexes[start] ?? 0, indexes[end] ?? 0) === 0)) {
|
|
302
|
+
if (end % 2048 === 0)
|
|
303
|
+
yield;
|
|
304
|
+
end += 1;
|
|
305
|
+
}
|
|
306
|
+
const size = end - start;
|
|
307
|
+
const partition = {
|
|
308
|
+
indexes: indexes.subarray(start, end),
|
|
309
|
+
peerStart: new Int32Array(size),
|
|
310
|
+
peerEnd: new Int32Array(size),
|
|
311
|
+
groupOrdinal: new Int32Array(size),
|
|
312
|
+
groupStarts: [],
|
|
313
|
+
orderValues: Array.from(indexes.subarray(start, end), (index) => rows[index]?.[window.orderAliases[0]?.alias ?? ""] ?? null)
|
|
314
|
+
};
|
|
315
|
+
let begin = 0;
|
|
316
|
+
for (let position = 1; position <= size; position += 1) {
|
|
317
|
+
if (position % 2048 === 0)
|
|
318
|
+
yield;
|
|
319
|
+
if (position === size || orders.some((column) => column.compare(indexes[start + begin] ?? 0, indexes[start + position] ?? 0) !== 0)) {
|
|
320
|
+
const ordinal = partition.groupStarts.length;
|
|
321
|
+
partition.groupStarts.push(begin);
|
|
322
|
+
partition.peerStart.fill(begin, begin, position);
|
|
323
|
+
partition.peerEnd.fill(position, begin, position);
|
|
324
|
+
partition.groupOrdinal.fill(ordinal, begin, position);
|
|
325
|
+
begin = position;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const member of group) {
|
|
329
|
+
const frameMemory = work.createChild();
|
|
330
|
+
try {
|
|
331
|
+
yield* applyPartition(rows, partition, member, result, frameMemory);
|
|
332
|
+
} finally {
|
|
333
|
+
frameMemory.close();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
start = end;
|
|
337
|
+
}
|
|
338
|
+
} finally {
|
|
339
|
+
work.close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
columns: [...result.columns, ...windows.map(({ alias }) => alias)],
|
|
344
|
+
columnDomains: [...result.columnDomains, ...windows.map(() => null)],
|
|
345
|
+
rows
|
|
346
|
+
};
|
|
347
|
+
} finally {
|
|
348
|
+
if (owned)
|
|
349
|
+
memory.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function applyWindowFunctions(result, windows, options = {}) {
|
|
353
|
+
const steps = windowSteps(result, windows, options);
|
|
354
|
+
try {
|
|
355
|
+
for (; ; ) {
|
|
356
|
+
throwIfAborted(options.signal);
|
|
357
|
+
const step = steps.next();
|
|
358
|
+
if (step.done)
|
|
359
|
+
return step.value;
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
steps.return({ columns: [], columnDomains: [], rows: [] });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function applyWindowFunctionsAsync(result, windows, options = {}) {
|
|
366
|
+
const steps = windowSteps(result, windows, options);
|
|
367
|
+
let yieldedAt = performance.now();
|
|
368
|
+
try {
|
|
369
|
+
for (; ; ) {
|
|
370
|
+
throwIfAborted(options.signal);
|
|
371
|
+
const step = steps.next();
|
|
372
|
+
if (step.done)
|
|
373
|
+
return step.value;
|
|
374
|
+
if (performance.now() - yieldedAt >= 8) {
|
|
375
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
376
|
+
yieldedAt = performance.now();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} finally {
|
|
380
|
+
steps.return({ columns: [], columnDomains: [], rows: [] });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
export {
|
|
384
|
+
applyWindowFunctions,
|
|
385
|
+
applyWindowFunctionsAsync
|
|
386
|
+
};
|
|
@@ -379,7 +379,8 @@ class DatabaseRpcServer {
|
|
|
379
379
|
try {
|
|
380
380
|
set = this.database.liveQueries({
|
|
381
381
|
...channelName === void 0 ? {} : { channelName },
|
|
382
|
-
...pollIntervalMs === void 0 ? {} : { pollIntervalMs }
|
|
382
|
+
...pollIntervalMs === void 0 ? {} : { pollIntervalMs },
|
|
383
|
+
sharedResults: true
|
|
383
384
|
});
|
|
384
385
|
this.#publishHandle(handleId, { type: "live-set", set, subscriptionIds: /* @__PURE__ */ new Set() });
|
|
385
386
|
} catch (error) {
|
|
@@ -667,11 +668,15 @@ class DatabaseRpcServer {
|
|
|
667
668
|
let subscription;
|
|
668
669
|
try {
|
|
669
670
|
subscription = await handle.set.subscribe(query, {
|
|
670
|
-
onChange: (result) => {
|
|
671
|
+
onChange: (result, delivery) => {
|
|
671
672
|
const encoded = encodeQueryResult(result);
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
673
|
+
const retained = delivery.retained?.slice();
|
|
674
|
+
if (retained !== void 0)
|
|
675
|
+
encoded.transfer.push(retained.buffer);
|
|
676
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "change", {
|
|
677
|
+
result: encoded.payload,
|
|
678
|
+
delivery: retained === void 0 ? delivery : { ...delivery, retained }
|
|
679
|
+
}), { transfer: encoded.transfer });
|
|
675
680
|
},
|
|
676
681
|
onError: (error) => {
|
|
677
682
|
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
@@ -700,9 +705,11 @@ class DatabaseRpcServer {
|
|
|
700
705
|
case "observe": {
|
|
701
706
|
const subscriptionId = this.#claimHandleId(args[0]);
|
|
702
707
|
const query = args[1];
|
|
708
|
+
const { suppressUnchanged } = args[2] ?? {};
|
|
703
709
|
let subscription;
|
|
704
710
|
try {
|
|
705
711
|
subscription = await handle.set.observe(query, {
|
|
712
|
+
...suppressUnchanged === true ? { suppressUnchanged: true } : {},
|
|
706
713
|
onInvalidate: (invalidation) => {
|
|
707
714
|
this.scope.postMessage(rpcEvent(subscriptionId, "invalidate", invalidation));
|
|
708
715
|
},
|
package/dist/plan/model.d.ts
CHANGED
|
@@ -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. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minnowdb/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Wilhite",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
{
|
|
143
143
|
"id": "type.exact-numeric",
|
|
144
144
|
"classification": "different",
|
|
145
|
-
"reason": "Minnow preserves exact decimals at the JavaScript boundary as strings, as PGlite's default decoder also does. A declared scale renders at exactly that scale as PostgreSQL does; a bare NUMERIC column, a derived arithmetic result, and a value cast or concatenated to text render canonically, without the trailing fractional zeros PostgreSQL preserves. Division and AVG select their result scale the way PostgreSQL does, so quotient digits agree
|
|
145
|
+
"reason": "Minnow preserves exact decimals at the JavaScript boundary as strings, as PGlite's default decoder also does. A declared scale renders at exactly that scale as PostgreSQL does; a bare NUMERIC column, a derived arithmetic result, and a value cast or concatenated to text render canonically, without the trailing fractional zeros PostgreSQL preserves. Division and AVG select their result scale the way PostgreSQL does, so quotient digits agree \u2014 including AVG over a column whose declared scale exceeds the selection. The canonical encoding does drop a stored value's display scale, so an explicit arithmetic quotient (such as SUM(v) / COUNT(v)) over a column declared with more than about twenty fractional digits can carry fewer digits than PostgreSQL, which floors the selection at the operand's display scale. An arithmetic or comparison expression mixing a float column with a constant Float64 cannot represent stays exact, where PostgreSQL casts the constant to float8 and rounds it before evaluating."
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
"id": "type.json-jsonb",
|
|
@@ -258,6 +258,11 @@
|
|
|
258
258
|
"id": "privileges.grant",
|
|
259
259
|
"classification": "inapplicable",
|
|
260
260
|
"reason": "An embedded database has no server roles, owners, or GRANT boundary."
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"id": "aggregate.array-agg",
|
|
264
|
+
"classification": "different",
|
|
265
|
+
"reason": "Minnow returns arrays as canonical JSON text at the JavaScript boundary; PostgreSQL clients return native arrays. ARRAY_AGG supports DISTINCT and ordering, but not FILTER or window use."
|
|
261
266
|
}
|
|
262
267
|
]
|
|
263
268
|
}
|
package/sql-feature-matrix.json
CHANGED
|
@@ -653,14 +653,13 @@
|
|
|
653
653
|
"id": "window.frame",
|
|
654
654
|
"status": "supported",
|
|
655
655
|
"example": "SELECT amount, SUM(amount) OVER (ORDER BY amount, joined ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rows",
|
|
656
|
-
"notes": "ROWS frames
|
|
656
|
+
"notes": "ROWS frames use row distances; RANGE frames use peers or numeric ordering-value distances; GROUPS frames use peer-group distances. Exclusions are supported."
|
|
657
657
|
},
|
|
658
658
|
{
|
|
659
659
|
"id": "window.frame-range-offset",
|
|
660
|
-
"status": "
|
|
660
|
+
"status": "supported",
|
|
661
661
|
"example": "SELECT amount, SUM(amount) OVER (ORDER BY amount RANGE BETWEEN 1 PRECEDING AND CURRENT ROW) AS windowed FROM rows",
|
|
662
|
-
"
|
|
663
|
-
"notes": "A numeric RANGE offset bounds the frame by ordering-value distance, which the engine does not implement. ROWS frames take numeric offsets; RANGE frames take UNBOUNDED and CURRENT ROW bounds only."
|
|
662
|
+
"notes": "Numeric RANGE offsets measure ordering-value distance and require one numeric ORDER BY expression. Temporal interval offsets are not supported."
|
|
664
663
|
},
|
|
665
664
|
{
|
|
666
665
|
"id": "window.distinct-aggregate",
|
|
@@ -671,10 +670,9 @@
|
|
|
671
670
|
},
|
|
672
671
|
{
|
|
673
672
|
"id": "window.outside-select",
|
|
674
|
-
"status": "
|
|
673
|
+
"status": "supported",
|
|
675
674
|
"example": "SELECT amount FROM rows ORDER BY ROW_NUMBER() OVER (ORDER BY amount)",
|
|
676
|
-
"
|
|
677
|
-
"notes": "PostgreSQL also evaluates window functions in ORDER BY; Minnow evaluates them only as select items. Alias the window in the select list and order by the alias."
|
|
675
|
+
"notes": "Window functions work in SELECT and ORDER BY. WHERE, GROUP BY, and HAVING cannot contain windows."
|
|
678
676
|
},
|
|
679
677
|
{
|
|
680
678
|
"id": "join.right",
|
|
@@ -843,7 +841,7 @@
|
|
|
843
841
|
"id": "expression.cast",
|
|
844
842
|
"status": "supported",
|
|
845
843
|
"example": "SELECT CAST(amount AS INTEGER) AS whole, CAST(amount AS TEXT) AS label FROM rows",
|
|
846
|
-
"notes": "
|
|
844
|
+
"notes": "Integer casts round exact NUMERIC ties away from zero and floating-point ties to even. Integer text accepts signed decimal digits. Postfix :: and CAST use the same rules."
|
|
847
845
|
},
|
|
848
846
|
{
|
|
849
847
|
"id": "identifier.quoted",
|
|
@@ -928,10 +926,9 @@
|
|
|
928
926
|
},
|
|
929
927
|
{
|
|
930
928
|
"id": "join.full-grouped",
|
|
931
|
-
"status": "
|
|
929
|
+
"status": "supported",
|
|
932
930
|
"example": "SELECT r.region AS region, COUNT(*) AS matched FROM rows r FULL JOIN dims d ON d.region = r.region GROUP BY r.region",
|
|
933
|
-
"
|
|
934
|
-
"notes": "FULL JOIN desugars into a union of two left joins, and grouping, DISTINCT, and window functions do not distribute over that union yet. Put the FULL JOIN in a derived table and group, deduplicate, or window in the outer block."
|
|
931
|
+
"notes": "The sole FULL JOIN composes with grouping, DISTINCT, wildcards, and window functions by applying these operations after both unmatched sides are included."
|
|
935
932
|
},
|
|
936
933
|
{
|
|
937
934
|
"id": "order-by.ordinal",
|
|
@@ -1114,10 +1111,9 @@
|
|
|
1114
1111
|
},
|
|
1115
1112
|
{
|
|
1116
1113
|
"id": "join.full-compound-on",
|
|
1117
|
-
"status": "
|
|
1118
|
-
"example": "SELECT r.region FROM rows r FULL JOIN
|
|
1119
|
-
"
|
|
1120
|
-
"notes": "FULL JOIN takes a single equality ON condition; move the extra condition into a derived source."
|
|
1114
|
+
"status": "supported",
|
|
1115
|
+
"example": "SELECT r.region FROM rows r FULL JOIN dims d ON d.region = r.region AND d.label <> ''",
|
|
1116
|
+
"notes": "A sole FULL JOIN accepts compound ON predicates and preserves unmatched rows from both sides."
|
|
1121
1117
|
},
|
|
1122
1118
|
{
|
|
1123
1119
|
"id": "datetime.current-date",
|
|
@@ -1552,10 +1548,9 @@
|
|
|
1552
1548
|
},
|
|
1553
1549
|
{
|
|
1554
1550
|
"id": "aggregate.array-agg",
|
|
1555
|
-
"status": "
|
|
1556
|
-
"example": "SELECT region, array_agg(amount) AS amounts FROM rows GROUP BY region",
|
|
1557
|
-
"
|
|
1558
|
-
"notes": "array_agg is not supported because arrays are not; json_agg (JSON_ARRAYAGG) collects a group into a JSON array instead."
|
|
1551
|
+
"status": "supported",
|
|
1552
|
+
"example": "SELECT region, array_agg(amount ORDER BY amount) AS amounts FROM rows GROUP BY region ORDER BY region",
|
|
1553
|
+
"notes": "ARRAY_AGG supports DISTINCT and aggregate-local ORDER BY, includes NULL elements, and returns NULL for empty input. Arrays cross the JavaScript boundary as canonical JSON text. FILTER and window use are not supported."
|
|
1559
1554
|
},
|
|
1560
1555
|
{
|
|
1561
1556
|
"id": "aggregate.ordered-set",
|
|
@@ -1568,14 +1563,13 @@
|
|
|
1568
1563
|
"id": "type.array",
|
|
1569
1564
|
"status": "supported",
|
|
1570
1565
|
"example": "SELECT ARRAY[1, 2] AS pair",
|
|
1571
|
-
"notes": "Constructors and array columns use canonical JSON text at the JavaScript boundary.
|
|
1566
|
+
"notes": "Constructors and array columns use canonical JSON text at the JavaScript boundary. One-based scalar subscripts and ARRAY_AGG are supported. Array operators such as concatenation and ANY/ALL remain unsupported."
|
|
1572
1567
|
},
|
|
1573
1568
|
{
|
|
1574
1569
|
"id": "type.array-subscript",
|
|
1575
|
-
"status": "
|
|
1570
|
+
"status": "supported",
|
|
1576
1571
|
"example": "SELECT (ARRAY[1, 2, 3])[1] AS first_element",
|
|
1577
|
-
"
|
|
1578
|
-
"notes": "PostgreSQL reads array elements with one-based subscripts and slices. Minnow arrays are opaque JSON text values; element access is refused at parse time."
|
|
1572
|
+
"notes": "One-based scalar subscripts return NULL for an out-of-range position. Slices and multidimensional access are not supported."
|
|
1579
1573
|
},
|
|
1580
1574
|
{
|
|
1581
1575
|
"id": "type.array-any",
|
|
@@ -1748,7 +1742,7 @@
|
|
|
1748
1742
|
"status": "unsupported",
|
|
1749
1743
|
"example": "SELECT INTERVAL '1 day' + INTERVAL '2 hours' AS total FROM rows",
|
|
1750
1744
|
"error": "Date arithmetic requires a date or datetime value",
|
|
1751
|
-
"notes": "Interval-valued arithmetic — interval + interval, timestamp - timestamp, date - date, date + integer,
|
|
1745
|
+
"notes": "Interval-valued arithmetic — interval + interval, timestamp - timestamp, date - date, date + integer, justify_days — is not supported. A date or datetime plus or minus an INTERVAL is; subtract two EXTRACT(EPOCH …) readings for a duration in seconds."
|
|
1752
1746
|
},
|
|
1753
1747
|
{
|
|
1754
1748
|
"id": "expression.date-minus-date",
|
|
@@ -1828,10 +1822,9 @@
|
|
|
1828
1822
|
},
|
|
1829
1823
|
{
|
|
1830
1824
|
"id": "function.unnest",
|
|
1831
|
-
"status": "
|
|
1825
|
+
"status": "supported",
|
|
1832
1826
|
"example": "SELECT x FROM unnest(ARRAY[3, 1, 2]) AS x",
|
|
1833
|
-
"
|
|
1834
|
-
"notes": "Set-returning functions (unnest, generate_series, regexp_split_to_table, jsonb_array_elements) are not supported as FROM sources; use a VALUES source, a recursive CTE for a series, or JSON_TABLE for a JSON array."
|
|
1827
|
+
"notes": "UNNEST over an ARRAY constructor produces typed rows and supports WITH ORDINALITY and output column aliases. Table-correlated array inputs and arbitrary array expressions are not supported."
|
|
1835
1828
|
},
|
|
1836
1829
|
{
|
|
1837
1830
|
"id": "function.generate-series",
|