@minnowdb/core 0.7.10 → 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 +3 -0
- package/dist/engine/client.js +8 -0
- package/dist/engine/database.js +189 -35
- package/dist/engine/errors.d.ts +2 -2
- package/dist/engine/errors.js +1 -1
- 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 +12 -0
- package/dist/engine/live.js +52 -49
- 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.js +28 -20
- package/dist/engine/windows.d.ts +12 -0
- package/dist/engine/windows.js +386 -0
- package/dist/plan/model.d.ts +1 -1
- package/package.json +1 -1
- package/postgres-feature-profile.json +6 -1
- package/sql-feature-matrix.json +20 -27
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { childExpressions, hasAggregate, mapChildExpressions, executeRowQueryInternal, inferResultColumnDomains, externalizeQueryResult } from "./query.js";
|
|
2
|
+
import { encodeQueryIdentity } from "./query-identity.js";
|
|
3
|
+
import { exactNumericBinary, exactNumericValue, externalSqlDomainValue, isExactNumeric } from "./sql-domains.js";
|
|
4
|
+
import { encodeSqlEqualityValue } from "./sql-semantics.js";
|
|
5
|
+
class LiveAggregate {
|
|
6
|
+
inputPlan;
|
|
7
|
+
#outputPlan;
|
|
8
|
+
#aggregates;
|
|
9
|
+
#groupAliases;
|
|
10
|
+
#rows;
|
|
11
|
+
#groups;
|
|
12
|
+
#domains;
|
|
13
|
+
keyAlias;
|
|
14
|
+
#schema;
|
|
15
|
+
constructor(inputPlan, outputPlan, aggregates, groupAliases, keyAlias, rows = /* @__PURE__ */ new Map(), groups = /* @__PURE__ */ new Map(), domains = [], schema = []) {
|
|
16
|
+
this.inputPlan = inputPlan;
|
|
17
|
+
this.#outputPlan = outputPlan;
|
|
18
|
+
this.#aggregates = aggregates;
|
|
19
|
+
this.#groupAliases = groupAliases;
|
|
20
|
+
this.keyAlias = keyAlias;
|
|
21
|
+
this.#rows = rows;
|
|
22
|
+
this.#groups = groups;
|
|
23
|
+
this.#domains = domains;
|
|
24
|
+
this.#schema = schema;
|
|
25
|
+
}
|
|
26
|
+
static plan(plan, qualifiedKey) {
|
|
27
|
+
const aggregates = [];
|
|
28
|
+
const groupAliases = plan.groupBy.map((_, index) => `__minnow_live_group_${String(index)}`);
|
|
29
|
+
const groupKeys = new Map(plan.groupBy.map((expression, index) => [
|
|
30
|
+
encodeQueryIdentity(expression),
|
|
31
|
+
groupAliases[index] ?? ""
|
|
32
|
+
]));
|
|
33
|
+
const rowLocal = (expression) => !hasAggregate(expression) && !["subquery", "exists", "window", "parameter", "wildcard"].includes(expression.kind) && childExpressions(expression).every(rowLocal);
|
|
34
|
+
if (!plan.groupBy.every(rowLocal))
|
|
35
|
+
return void 0;
|
|
36
|
+
const rewrite = (expression) => {
|
|
37
|
+
const grouped = groupKeys.get(encodeQueryIdentity(expression));
|
|
38
|
+
if (grouped !== void 0)
|
|
39
|
+
return { kind: "column", reference: grouped };
|
|
40
|
+
if (expression.kind === "call" && hasAggregate(expression)) {
|
|
41
|
+
if (!["COUNT", "SUM", "AVG"].includes(expression.name) || expression.arguments.length !== 1)
|
|
42
|
+
throw new TypeError("Not an additive live aggregate");
|
|
43
|
+
const argument = expression.arguments[0];
|
|
44
|
+
if (argument === void 0 || !(rowLocal(argument) || expression.name === "COUNT" && argument.kind === "wildcard"))
|
|
45
|
+
throw new TypeError("Not a row-local aggregate argument");
|
|
46
|
+
const alias = `__minnow_live_value_${String(aggregates.length)}`;
|
|
47
|
+
aggregates.push({
|
|
48
|
+
name: expression.name,
|
|
49
|
+
alias,
|
|
50
|
+
argument: argument.kind === "wildcard" ? { kind: "literal", value: 1 } : argument
|
|
51
|
+
});
|
|
52
|
+
return { kind: "column", reference: alias };
|
|
53
|
+
}
|
|
54
|
+
if (expression.kind === "column" || expression.kind === "window" || expression.kind === "subquery" || expression.kind === "exists")
|
|
55
|
+
throw new TypeError("Not a grouped live expression");
|
|
56
|
+
return mapChildExpressions(expression, rewrite);
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const select = plan.select.map((item) => ({ ...item, expression: rewrite(item.expression) }));
|
|
60
|
+
const having = plan.having.map((predicate) => ({
|
|
61
|
+
...predicate,
|
|
62
|
+
left: rewrite(predicate.left),
|
|
63
|
+
right: rewrite(predicate.right)
|
|
64
|
+
}));
|
|
65
|
+
const outputAliases = new Set(select.map((item) => item.alias));
|
|
66
|
+
const orderBy = plan.orderBy.map((term) => ({
|
|
67
|
+
...term,
|
|
68
|
+
expression: term.expression.kind === "column" && outputAliases.has(term.expression.reference) ? term.expression : rewrite(term.expression)
|
|
69
|
+
}));
|
|
70
|
+
if (aggregates.length === 0)
|
|
71
|
+
return void 0;
|
|
72
|
+
const keyAlias = "__minnow_live_aggregate_key";
|
|
73
|
+
const inputPlan = {
|
|
74
|
+
...plan,
|
|
75
|
+
select: [
|
|
76
|
+
{ alias: keyAlias, expression: { kind: "column", reference: qualifiedKey } },
|
|
77
|
+
...plan.groupBy.map((expression, index) => ({
|
|
78
|
+
alias: groupAliases[index] ?? "",
|
|
79
|
+
expression
|
|
80
|
+
})),
|
|
81
|
+
...aggregates.map(({ alias, argument }) => ({ alias, expression: argument }))
|
|
82
|
+
],
|
|
83
|
+
groupBy: [],
|
|
84
|
+
having: [],
|
|
85
|
+
orderBy: []
|
|
86
|
+
};
|
|
87
|
+
delete inputPlan.limit;
|
|
88
|
+
delete inputPlan.offset;
|
|
89
|
+
const outputPlan = {
|
|
90
|
+
...plan,
|
|
91
|
+
base: { table: "__minnow_live_groups", alias: "__minnow_live_groups" },
|
|
92
|
+
joins: [],
|
|
93
|
+
select,
|
|
94
|
+
predicates: having,
|
|
95
|
+
groupBy: [],
|
|
96
|
+
having: [],
|
|
97
|
+
orderBy
|
|
98
|
+
};
|
|
99
|
+
return new LiveAggregate(inputPlan, outputPlan, aggregates, groupAliases, keyAlias);
|
|
100
|
+
} catch {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
patch(result, changed, token) {
|
|
105
|
+
const rows = new Map(this.#rows);
|
|
106
|
+
const groups = new Map(this.#groups);
|
|
107
|
+
const touched = /* @__PURE__ */ new Set();
|
|
108
|
+
const groupFor = (key, keys = []) => {
|
|
109
|
+
let group = groups.get(key);
|
|
110
|
+
if (group === void 0) {
|
|
111
|
+
group = {
|
|
112
|
+
keys,
|
|
113
|
+
members: 0,
|
|
114
|
+
counts: this.#aggregates.map(() => 0),
|
|
115
|
+
sums: this.#aggregates.map(() => exactNumericValue(0) ?? ""),
|
|
116
|
+
absolute: this.#aggregates.map(() => 0)
|
|
117
|
+
};
|
|
118
|
+
groups.set(key, group);
|
|
119
|
+
touched.add(key);
|
|
120
|
+
} else if (!touched.has(key)) {
|
|
121
|
+
group = {
|
|
122
|
+
...group,
|
|
123
|
+
counts: [...group.counts],
|
|
124
|
+
sums: [...group.sums],
|
|
125
|
+
absolute: [...group.absolute]
|
|
126
|
+
};
|
|
127
|
+
groups.set(key, group);
|
|
128
|
+
touched.add(key);
|
|
129
|
+
}
|
|
130
|
+
return group;
|
|
131
|
+
};
|
|
132
|
+
const apply = (group, values, sign) => {
|
|
133
|
+
group.members += sign;
|
|
134
|
+
for (const [index, aggregate] of this.#aggregates.entries()) {
|
|
135
|
+
const value = values[index] ?? null;
|
|
136
|
+
if (value === null)
|
|
137
|
+
continue;
|
|
138
|
+
group.counts[index] = (group.counts[index] ?? 0) + sign;
|
|
139
|
+
if (aggregate.name !== "COUNT") {
|
|
140
|
+
if (typeof value !== "number" && !isExactNumeric(value))
|
|
141
|
+
throw new TypeError("Live SUM requires numeric values");
|
|
142
|
+
if (typeof value === "number") {
|
|
143
|
+
if (!Number.isSafeInteger(value))
|
|
144
|
+
throw new TypeError("Floating-point aggregates require full execution");
|
|
145
|
+
group.absolute[index] = (group.absolute[index] ?? 0) + sign * Math.abs(value);
|
|
146
|
+
if (!Number.isSafeInteger(group.absolute[index]))
|
|
147
|
+
throw new TypeError("Aggregate sum may round; requires full execution");
|
|
148
|
+
}
|
|
149
|
+
const sum = exactNumericBinary(sign === 1 ? "+" : "-", group.sums[index] ?? exactNumericValue(0), exactNumericValue(value));
|
|
150
|
+
if (sum === null || sum === void 0)
|
|
151
|
+
throw new TypeError("Invalid numeric aggregate contribution");
|
|
152
|
+
group.sums[index] = sum;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
for (const key of changed) {
|
|
157
|
+
const old = rows.get(key);
|
|
158
|
+
if (old === void 0)
|
|
159
|
+
continue;
|
|
160
|
+
apply(groupFor(old.group), old.values, -1);
|
|
161
|
+
rows.delete(key);
|
|
162
|
+
}
|
|
163
|
+
for (const row of result.rows) {
|
|
164
|
+
const key = token(row[this.keyAlias] ?? null);
|
|
165
|
+
const keys = this.#groupAliases.map((alias) => row[alias] ?? null);
|
|
166
|
+
const group = JSON.stringify(keys.map(encodeSqlEqualityValue));
|
|
167
|
+
const values = this.#aggregates.map(({ alias }) => row[alias] ?? null);
|
|
168
|
+
apply(groupFor(group, keys), values, 1);
|
|
169
|
+
rows.set(key, { group, values });
|
|
170
|
+
}
|
|
171
|
+
if (this.#groupAliases.length === 0)
|
|
172
|
+
groupFor("[]");
|
|
173
|
+
else
|
|
174
|
+
for (const key of touched)
|
|
175
|
+
if (groups.get(key)?.members === 0)
|
|
176
|
+
groups.delete(key);
|
|
177
|
+
const domains = this.#aggregates.map(({ alias }, index) => result.columnDomains[result.columns.indexOf(alias)] ?? this.#domains[index] ?? null);
|
|
178
|
+
return new LiveAggregate(this.inputPlan, this.#outputPlan, this.#aggregates, this.#groupAliases, this.keyAlias, rows, groups, domains, result.columns.map((name, index) => {
|
|
179
|
+
const domain = result.columnDomains[index];
|
|
180
|
+
const value = result.rows.find((row) => row[name] !== null)?.[name];
|
|
181
|
+
const aggregate = this.#aggregates.find((item) => item.alias === name);
|
|
182
|
+
return {
|
|
183
|
+
name,
|
|
184
|
+
type: aggregate?.name === "COUNT" ? "number" : domain !== null && domain !== void 0 ? "string" : typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : value instanceof Date ? "datetime" : "string",
|
|
185
|
+
...domain === null || domain === void 0 || aggregate?.name === "COUNT" ? {} : { sqlDomain: domain }
|
|
186
|
+
};
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
result() {
|
|
190
|
+
const rows = [];
|
|
191
|
+
for (const group of this.#groups.values()) {
|
|
192
|
+
const row = {};
|
|
193
|
+
for (const [index, alias] of this.#groupAliases.entries())
|
|
194
|
+
row[alias] = group.keys[index] ?? null;
|
|
195
|
+
for (const [index, aggregate] of this.#aggregates.entries()) {
|
|
196
|
+
const count = group.counts[index] ?? 0;
|
|
197
|
+
const domain = this.#domains[index];
|
|
198
|
+
let value = aggregate.name === "COUNT" ? count : count === 0 ? null : group.sums[index] ?? null;
|
|
199
|
+
if (value !== null && aggregate.name !== "COUNT") {
|
|
200
|
+
if (domain?.kind !== "numeric") {
|
|
201
|
+
value = Number(externalSqlDomainValue(value));
|
|
202
|
+
if (aggregate.name === "AVG")
|
|
203
|
+
value /= count;
|
|
204
|
+
} else if (aggregate.name === "AVG")
|
|
205
|
+
value = exactNumericBinary("/", value, count, domain.scale) ?? null;
|
|
206
|
+
}
|
|
207
|
+
row[aggregate.alias] = value;
|
|
208
|
+
}
|
|
209
|
+
rows.push(row);
|
|
210
|
+
}
|
|
211
|
+
const result = executeRowQueryInternal(this.#outputPlan, /* @__PURE__ */ new Map([["__minnow_live_groups", rows]]));
|
|
212
|
+
result.columnDomains = inferResultColumnDomains(this.#outputPlan, /* @__PURE__ */ new Map([["__minnow_live_groups", this.#schema]]));
|
|
213
|
+
return externalizeQueryResult(result);
|
|
214
|
+
}
|
|
215
|
+
get retainedBytes() {
|
|
216
|
+
let bytes = 256 + encodeQueryIdentity(this.inputPlan).length * 2 + encodeQueryIdentity(this.#outputPlan).length * 2;
|
|
217
|
+
for (const [key, row] of this.#rows)
|
|
218
|
+
bytes += 64 + key.length * 2 + row.group.length * 2 + row.values.reduce((sum, value) => sum + (typeof value === "string" ? value.length * 2 + 16 : 16), 0);
|
|
219
|
+
for (const [key, group] of this.#groups)
|
|
220
|
+
bytes += 96 + key.length * 2 + group.sums.reduce((sum, value) => sum + value.length * 2 + 24, 0);
|
|
221
|
+
return bytes;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export {
|
|
225
|
+
LiveAggregate
|
|
226
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { QueryResult, QueryRow } from "../plan/model.js";
|
|
2
|
+
import type { LiveQueryDelivery } from "./live.js";
|
|
3
|
+
export type LiveQueryPatch = {
|
|
4
|
+
readonly type: "reset";
|
|
5
|
+
readonly result: QueryResult;
|
|
6
|
+
} | {
|
|
7
|
+
readonly type: "patch";
|
|
8
|
+
/** Each next position's previous position, or -1 for a changed/new row. */
|
|
9
|
+
readonly retained: Int32Array;
|
|
10
|
+
readonly changedRows: ReadonlyArray<{
|
|
11
|
+
readonly index: number;
|
|
12
|
+
readonly row: QueryRow;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
export interface LiveQueryPatchOptions {
|
|
16
|
+
onPatch(patch: LiveQueryPatch, delivery: LiveQueryDelivery): void;
|
|
17
|
+
onError?(error: unknown): void;
|
|
18
|
+
onComplete?(): void;
|
|
19
|
+
}
|
|
20
|
+
/** Copy only changed payloads when provenance is available; resets establish a fresh baseline. */
|
|
21
|
+
export declare function createLiveQueryPatch(result: QueryResult, delivery: LiveQueryDelivery): LiveQueryPatch;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { copyDate } from "../date-value.js";
|
|
2
|
+
function copyRow(row) {
|
|
3
|
+
const copy = { ...row };
|
|
4
|
+
for (const key of Object.keys(copy)) {
|
|
5
|
+
const value = copy[key];
|
|
6
|
+
if (value instanceof Date)
|
|
7
|
+
copy[key] = copyDate(value);
|
|
8
|
+
}
|
|
9
|
+
return copy;
|
|
10
|
+
}
|
|
11
|
+
function createLiveQueryPatch(result, delivery) {
|
|
12
|
+
if (delivery.retained === void 0 || delivery.initial)
|
|
13
|
+
return {
|
|
14
|
+
type: "reset",
|
|
15
|
+
result: {
|
|
16
|
+
columns: [...result.columns],
|
|
17
|
+
columnDomains: structuredClone(result.columnDomains),
|
|
18
|
+
rows: result.rows.map(copyRow)
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const changedRows = [];
|
|
22
|
+
for (let index = 0; index < result.rows.length; index += 1) {
|
|
23
|
+
const row = result.rows[index];
|
|
24
|
+
if (row !== void 0 && (delivery.retained[index] ?? -1) < 0)
|
|
25
|
+
changedRows.push({ index, row: copyRow(row) });
|
|
26
|
+
}
|
|
27
|
+
return { type: "patch", retained: new Int32Array(delivery.retained), changedRows };
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
createLiveQueryPatch
|
|
31
|
+
};
|
package/dist/engine/live.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type LiveQueryPatchOptions } from "./live-patch.js";
|
|
2
|
+
export type { LiveQueryPatch, LiveQueryPatchOptions } from "./live-patch.js";
|
|
1
3
|
import { type CatalogProbe, type Manifest, type StoragePage } from "../storage/types.js";
|
|
2
4
|
import { type CompiledQuery, type QueryResult, type QueryValue } from "./query.js";
|
|
3
5
|
/**
|
|
@@ -27,6 +29,8 @@ export interface LiveQuerySetOptions {
|
|
|
27
29
|
readonly maxGroups?: number;
|
|
28
30
|
/** Maximum result/observer subscriptions retained by this set. Defaults to 1,024. */
|
|
29
31
|
readonly maxSubscriptions?: number;
|
|
32
|
+
/** Maximum modeled resident result and maintenance bytes per set. Defaults to 64 MiB. */
|
|
33
|
+
readonly maxRetainedBytes?: number;
|
|
30
34
|
/**
|
|
31
35
|
* Hand `onChange` the set's retained result instead of a private copy. The result is shared
|
|
32
36
|
* with every equal subscription and with the next change comparison, so a subscriber must
|
|
@@ -64,6 +68,8 @@ export interface LiveQueryDelivery {
|
|
|
64
68
|
readonly retained?: Int32Array;
|
|
65
69
|
}
|
|
66
70
|
export interface LiveQuerySubscribeOptions {
|
|
71
|
+
/** Borrow the read-only retained result for this subscription instead of copying it. */
|
|
72
|
+
readonly sharedResults?: boolean;
|
|
67
73
|
onChange(result: QueryResult, delivery: LiveQueryDelivery): void;
|
|
68
74
|
onError?(error: unknown): void;
|
|
69
75
|
/** Called once when the subscription ends because the subscription or its set closed. */
|
|
@@ -114,6 +120,8 @@ export interface LiveQueryStats {
|
|
|
114
120
|
* bounded by the engine at 64 rows and is not included.
|
|
115
121
|
*/
|
|
116
122
|
retainedRows: number;
|
|
123
|
+
/** Modeled resident result and incremental maintenance bytes, counted once per group. */
|
|
124
|
+
retainedBytes: number;
|
|
117
125
|
/** Work avoided because equal statements shared one query group or in-flight execution. */
|
|
118
126
|
sharedExecutions: number;
|
|
119
127
|
lastSweepMs: number;
|
|
@@ -146,6 +154,8 @@ export interface LiveQueryExecuteContext {
|
|
|
146
154
|
export interface LiveMaintainedExecution {
|
|
147
155
|
readonly result: QueryResult;
|
|
148
156
|
readonly state: unknown;
|
|
157
|
+
/** Modeled total bytes retained by the result and opaque maintenance state. */
|
|
158
|
+
readonly retainedBytes?: number;
|
|
149
159
|
}
|
|
150
160
|
export interface LiveMaintainedChange extends LiveMaintainedExecution {
|
|
151
161
|
readonly changed: boolean;
|
|
@@ -178,6 +188,8 @@ export declare class LiveQuerySet {
|
|
|
178
188
|
get stats(): LiveQueryStats;
|
|
179
189
|
/** Registers a query, delivers its current result, and shares work with equal statements. */
|
|
180
190
|
subscribe(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<LiveQuerySubscription>;
|
|
191
|
+
/** Delivers resets or changed row payloads without constructing a private full row array per patch. */
|
|
192
|
+
subscribePatches(query: LiveQueryInput, options: LiveQueryPatchOptions): Promise<LiveQuerySubscription>;
|
|
181
193
|
/** Observes invalidation; the statement executes inside the set only when asked to compare. */
|
|
182
194
|
observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<LiveQuerySubscription>;
|
|
183
195
|
/** Called by the owning database after each local write commit; also hints other tabs. */
|
package/dist/engine/live.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createLiveQueryPatch } from "./live-patch.js";
|
|
2
|
+
import { queryResultRetainedBytes } from "./query-cache.js";
|
|
3
|
+
import { encodeQueryIdentity } from "./query-identity.js";
|
|
1
4
|
import { LiveQueryLimitError } from "./errors.js";
|
|
2
5
|
const DEFAULT_LIVE_QUERY_MAX_GROUPS = 256;
|
|
3
6
|
const DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS = 1024;
|
|
@@ -63,41 +66,6 @@ function cloneResult(result) {
|
|
|
63
66
|
rows: result.rows.map((row) => cloneRow(row, columns))
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
|
-
function encodeQueryIdentity(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
67
|
-
if (value === null)
|
|
68
|
-
return "z";
|
|
69
|
-
if (typeof value === "undefined")
|
|
70
|
-
return "u";
|
|
71
|
-
if (typeof value === "boolean")
|
|
72
|
-
return value ? "b1" : "b0";
|
|
73
|
-
if (typeof value === "number") {
|
|
74
|
-
if (Number.isNaN(value))
|
|
75
|
-
return "nNaN;";
|
|
76
|
-
if (Object.is(value, -0))
|
|
77
|
-
return "n-0;";
|
|
78
|
-
return `n${String(value)};`;
|
|
79
|
-
}
|
|
80
|
-
if (typeof value === "string")
|
|
81
|
-
return `s${String(value.length)}:${value}`;
|
|
82
|
-
if (value instanceof Date)
|
|
83
|
-
return `d${String(dateMilliseconds(value))};`;
|
|
84
|
-
if (typeof value !== "object") {
|
|
85
|
-
throw new TypeError(`Unsupported live-query identity value: ${typeof value}`);
|
|
86
|
-
}
|
|
87
|
-
if (ancestors.has(value))
|
|
88
|
-
throw new TypeError("Live-query identity contains a cycle");
|
|
89
|
-
ancestors.add(value);
|
|
90
|
-
let encoded;
|
|
91
|
-
if (Array.isArray(value)) {
|
|
92
|
-
encoded = `a${String(value.length)}[${value.map((item) => encodeQueryIdentity(item, ancestors)).join("")}]`;
|
|
93
|
-
} else {
|
|
94
|
-
const record = value;
|
|
95
|
-
const keys = Object.keys(record).sort();
|
|
96
|
-
encoded = `o${String(keys.length)}{${keys.map((key) => `${encodeQueryIdentity(key, ancestors)}${encodeQueryIdentity(record[key], ancestors)}`).join("")}}`;
|
|
97
|
-
}
|
|
98
|
-
ancestors.delete(value);
|
|
99
|
-
return encoded;
|
|
100
|
-
}
|
|
101
69
|
function queryKey(query) {
|
|
102
70
|
return encodeQueryIdentity(typeof query === "string" ? ["sql", query] : query.kind === "sql-query" ? ["sql-query", query.sql, query.params] : ["typed-query", query.plan]);
|
|
103
71
|
}
|
|
@@ -141,6 +109,12 @@ function groupMemoizes(group) {
|
|
|
141
109
|
}
|
|
142
110
|
return false;
|
|
143
111
|
}
|
|
112
|
+
function callLiveCallback(callback) {
|
|
113
|
+
try {
|
|
114
|
+
callback?.();
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
144
118
|
class LiveQuerySet {
|
|
145
119
|
#host;
|
|
146
120
|
#channel;
|
|
@@ -153,6 +127,7 @@ class LiveQuerySet {
|
|
|
153
127
|
#lagging = /* @__PURE__ */ new Set();
|
|
154
128
|
#maxGroups;
|
|
155
129
|
#maxSubscriptions;
|
|
130
|
+
#maxRetainedBytes;
|
|
156
131
|
#sharedResults;
|
|
157
132
|
#incremental;
|
|
158
133
|
#stats = {
|
|
@@ -167,6 +142,7 @@ class LiveQuerySet {
|
|
|
167
142
|
maintained: 0,
|
|
168
143
|
groupsVisited: 0,
|
|
169
144
|
retainedRows: 0,
|
|
145
|
+
retainedBytes: 0,
|
|
170
146
|
sharedExecutions: 0,
|
|
171
147
|
lastSweepMs: 0
|
|
172
148
|
};
|
|
@@ -187,6 +163,9 @@ class LiveQuerySet {
|
|
|
187
163
|
}
|
|
188
164
|
this.#maxGroups = boundedLiveLimit(options.maxGroups ?? DEFAULT_LIVE_QUERY_MAX_GROUPS, MAX_LIVE_QUERY_GROUPS, "group");
|
|
189
165
|
this.#maxSubscriptions = boundedLiveLimit(options.maxSubscriptions ?? DEFAULT_LIVE_QUERY_MAX_SUBSCRIPTIONS, MAX_LIVE_QUERY_SUBSCRIPTIONS, "subscription");
|
|
166
|
+
this.#maxRetainedBytes = options.maxRetainedBytes ?? 64 * 1024 * 1024;
|
|
167
|
+
if (!Number.isSafeInteger(this.#maxRetainedBytes) || this.#maxRetainedBytes < 0)
|
|
168
|
+
throw new RangeError("Live query retained byte limit must be a non-negative safe integer");
|
|
190
169
|
this.#sharedResults = options.sharedResults === true;
|
|
191
170
|
this.#incremental = options.incremental !== false;
|
|
192
171
|
this.#host = host;
|
|
@@ -212,7 +191,10 @@ class LiveQuerySet {
|
|
|
212
191
|
let retainedRows = 0;
|
|
213
192
|
for (const group of this.#groups.values())
|
|
214
193
|
retainedRows += group.result?.rows.length ?? 0;
|
|
215
|
-
|
|
194
|
+
let retainedBytes = 0;
|
|
195
|
+
for (const group of this.#groups.values())
|
|
196
|
+
retainedBytes += group.retainedBytes;
|
|
197
|
+
return { ...this.#stats, retainedRows, retainedBytes };
|
|
216
198
|
}
|
|
217
199
|
#freshProbe() {
|
|
218
200
|
let pending = this.#pendingProbe;
|
|
@@ -274,6 +256,14 @@ class LiveQuerySet {
|
|
|
274
256
|
}
|
|
275
257
|
return this.#subscriptionHandle(group, subscriber);
|
|
276
258
|
}
|
|
259
|
+
subscribePatches(query, options) {
|
|
260
|
+
return this.subscribe(query, {
|
|
261
|
+
sharedResults: true,
|
|
262
|
+
onChange: (result, delivery) => options.onPatch(createLiveQueryPatch(result, delivery), delivery),
|
|
263
|
+
...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
|
|
264
|
+
...options.onComplete === void 0 ? {} : { onComplete: options.onComplete.bind(options) }
|
|
265
|
+
});
|
|
266
|
+
}
|
|
277
267
|
async observe(query, options) {
|
|
278
268
|
this.#throwIfClosed();
|
|
279
269
|
this.#reserveSubscription();
|
|
@@ -338,7 +328,7 @@ class LiveQuerySet {
|
|
|
338
328
|
this.#subscriptionCount -= 1;
|
|
339
329
|
group.subscribers.delete(subscriber);
|
|
340
330
|
if (complete)
|
|
341
|
-
subscriber.options.onComplete?.();
|
|
331
|
+
callLiveCallback(() => subscriber.options.onComplete?.());
|
|
342
332
|
this.#removeEmptyGroup(group);
|
|
343
333
|
}
|
|
344
334
|
#removeEmptyGroup(group) {
|
|
@@ -382,6 +372,7 @@ class LiveQuerySet {
|
|
|
382
372
|
subscribers: /* @__PURE__ */ new Set(),
|
|
383
373
|
seenProbe: after,
|
|
384
374
|
result: void 0,
|
|
375
|
+
retainedBytes: 0,
|
|
385
376
|
execution: void 0,
|
|
386
377
|
deliveries: 0,
|
|
387
378
|
maintenance: void 0,
|
|
@@ -445,6 +436,7 @@ class LiveQuerySet {
|
|
|
445
436
|
await this.#acquireExecutionSlot();
|
|
446
437
|
let executed;
|
|
447
438
|
let maintenance;
|
|
439
|
+
let retainedBytes;
|
|
448
440
|
try {
|
|
449
441
|
if (this.#incremental && this.#host.executeMaintainable !== void 0 && !group.unmaintainable) {
|
|
450
442
|
const maintained = await this.#host.executeMaintainable(group.query, context);
|
|
@@ -453,6 +445,7 @@ class LiveQuerySet {
|
|
|
453
445
|
else {
|
|
454
446
|
executed = maintained.result;
|
|
455
447
|
maintenance = maintained.state;
|
|
448
|
+
retainedBytes = maintained.retainedBytes;
|
|
456
449
|
}
|
|
457
450
|
}
|
|
458
451
|
executed ??= await this.#host.execute(group.query, context);
|
|
@@ -461,8 +454,7 @@ class LiveQuerySet {
|
|
|
461
454
|
}
|
|
462
455
|
const previous = group.result;
|
|
463
456
|
const changed = previous === void 0 || !sameResult(previous, executed);
|
|
464
|
-
group
|
|
465
|
-
group.maintenance = maintenance;
|
|
457
|
+
this.#retain(group, executed, maintenance, retainedBytes);
|
|
466
458
|
return { result: executed, changed };
|
|
467
459
|
})();
|
|
468
460
|
group.execution = execution;
|
|
@@ -473,6 +465,18 @@ class LiveQuerySet {
|
|
|
473
465
|
group.execution = void 0;
|
|
474
466
|
}
|
|
475
467
|
}
|
|
468
|
+
#retain(group, result, state, hint) {
|
|
469
|
+
const bytes = Math.max(queryResultRetainedBytes(result) + result.rows.length * 48, hint ?? (state === group.maintenance ? group.retainedBytes : 0));
|
|
470
|
+
let total = bytes;
|
|
471
|
+
for (const other of this.#groups.values())
|
|
472
|
+
if (other !== group)
|
|
473
|
+
total += other.retainedBytes;
|
|
474
|
+
if (!Number.isSafeInteger(total) || total > this.#maxRetainedBytes)
|
|
475
|
+
throw new LiveQueryLimitError("byte", this.#maxRetainedBytes);
|
|
476
|
+
group.result = result;
|
|
477
|
+
group.maintenance = state;
|
|
478
|
+
group.retainedBytes = bytes;
|
|
479
|
+
}
|
|
476
480
|
async #acquireExecutionSlot() {
|
|
477
481
|
if (this.#executing < LIVE_QUERY_EXECUTION_CONCURRENCY) {
|
|
478
482
|
this.#executing += 1;
|
|
@@ -509,8 +513,7 @@ class LiveQuerySet {
|
|
|
509
513
|
verdict.declined = true;
|
|
510
514
|
return { result: retained, changed: false };
|
|
511
515
|
}
|
|
512
|
-
group.result
|
|
513
|
-
group.maintenance = maintained.state;
|
|
516
|
+
this.#retain(group, maintained.result, maintained.state, maintained.retainedBytes);
|
|
514
517
|
return {
|
|
515
518
|
result: maintained.result,
|
|
516
519
|
changed: maintained.changed,
|
|
@@ -531,7 +534,7 @@ class LiveQuerySet {
|
|
|
531
534
|
return;
|
|
532
535
|
subscriber.delivered = true;
|
|
533
536
|
const consecutive = subscriber.seenDelivery === group.deliveries - 1;
|
|
534
|
-
subscriber.options.onChange(this.#sharedResults ? result : cloneResult(result), retained !== void 0 && consecutive && !delivery.initial ? { ...delivery, retained } : delivery);
|
|
537
|
+
subscriber.options.onChange(this.#sharedResults || subscriber.options.sharedResults === true ? result : cloneResult(result), retained !== void 0 && consecutive && !delivery.initial ? { ...delivery, retained } : delivery);
|
|
535
538
|
subscriber.seenDelivery = group.deliveries;
|
|
536
539
|
}
|
|
537
540
|
#deliverInvalidation(subscriber, invalidation) {
|
|
@@ -569,10 +572,10 @@ class LiveQuerySet {
|
|
|
569
572
|
continue;
|
|
570
573
|
subscriber.closed = true;
|
|
571
574
|
this.#subscriptionCount -= 1;
|
|
572
|
-
subscriber.options.onComplete?.();
|
|
575
|
+
callLiveCallback(() => subscriber.options.onComplete?.());
|
|
573
576
|
}
|
|
574
577
|
}
|
|
575
|
-
this.#onClosed
|
|
578
|
+
callLiveCallback(this.#onClosed);
|
|
576
579
|
}
|
|
577
580
|
#hint() {
|
|
578
581
|
if (this.#closed)
|
|
@@ -735,23 +738,23 @@ class LiveQuerySet {
|
|
|
735
738
|
if (isObserver(subscriber)) {
|
|
736
739
|
if (execution !== void 0 && !execution.changed && subscriber.options.suppressUnchanged === true) {
|
|
737
740
|
this.#stats.notificationsSuppressed += 1;
|
|
738
|
-
|
|
741
|
+
continue;
|
|
739
742
|
}
|
|
740
743
|
try {
|
|
741
744
|
this.#deliverInvalidation(subscriber, invalidation);
|
|
742
745
|
this.#stats.invalidations += 1;
|
|
743
746
|
} catch (error) {
|
|
744
|
-
subscriber.options.onError?.(error);
|
|
747
|
+
callLiveCallback(() => subscriber.options.onError?.(error));
|
|
745
748
|
}
|
|
746
749
|
} else if (isResultSubscriber(subscriber) && execution !== void 0) {
|
|
747
750
|
if (!execution.changed) {
|
|
748
751
|
this.#stats.notificationsSuppressed += 1;
|
|
749
|
-
|
|
752
|
+
continue;
|
|
750
753
|
}
|
|
751
754
|
try {
|
|
752
755
|
this.#deliverResult(group, subscriber, execution.result, invalidation, execution.retained);
|
|
753
756
|
} catch (error) {
|
|
754
|
-
subscriber.options.onError?.(error);
|
|
757
|
+
callLiveCallback(() => subscriber.options.onError?.(error));
|
|
755
758
|
}
|
|
756
759
|
}
|
|
757
760
|
}
|
|
@@ -769,7 +772,7 @@ class LiveQuerySet {
|
|
|
769
772
|
for (const subscriber of group.subscribers) {
|
|
770
773
|
if (subscriber.closed)
|
|
771
774
|
continue;
|
|
772
|
-
subscriber.options.onError?.(error);
|
|
775
|
+
callLiveCallback(() => subscriber.options.onError?.(error));
|
|
773
776
|
}
|
|
774
777
|
}
|
|
775
778
|
async #changedTablesSince(after, until) {
|
package/dist/engine/optimizer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeQueryIdentity } from "./query-identity.js";
|
|
1
2
|
import { dateIsoString, dateMilliseconds } from "../date-value.js";
|
|
2
3
|
import { crossJoinPlan } from "../plan/model.js";
|
|
3
4
|
import { blockHasRowWindow, blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, statementDatetimeNames, volatileScalarFunctionNames, transparentProjectionSource, dateTruncValue, integerQuotient } from "./query.js";
|
|
@@ -557,7 +558,7 @@ function propagateJoinKeyConstants(block) {
|
|
|
557
558
|
}
|
|
558
559
|
if (pairs.length === 0)
|
|
559
560
|
return;
|
|
560
|
-
const signature = (predicate) =>
|
|
561
|
+
const signature = (predicate) => encodeQueryIdentity(predicate);
|
|
561
562
|
const present = new Set(block.predicates.map(signature));
|
|
562
563
|
const implied = [];
|
|
563
564
|
const constantSide = (predicate) => {
|
|
@@ -785,9 +786,9 @@ function decorrelateBlock(block, nextAlias) {
|
|
|
785
786
|
if (!expressionHasCorrelatedSubquery(item.expression))
|
|
786
787
|
continue;
|
|
787
788
|
if (grouped) {
|
|
788
|
-
const groupedExpressions = new Set(block.groupBy.map((group) =>
|
|
789
|
+
const groupedExpressions = new Set(block.groupBy.map((group) => encodeQueryIdentity(group)));
|
|
789
790
|
const outerReferences = correlatedOuterReferences(item.expression);
|
|
790
|
-
if (containsAggregateCall(item.expression) || outerReferences.some((reference) => !groupedExpressions.has(
|
|
791
|
+
if (containsAggregateCall(item.expression) || outerReferences.some((reference) => !groupedExpressions.has(encodeQueryIdentity({ kind: "column", reference })))) {
|
|
791
792
|
throw new TypeError("A grouped correlated select-list subquery must reference only GROUP BY columns and cannot be nested inside an aggregate");
|
|
792
793
|
}
|
|
793
794
|
}
|
|
@@ -2428,7 +2429,11 @@ function foldExpression(expression) {
|
|
|
2428
2429
|
if (folded === null || typeof folded === "string" || typeof folded === "boolean" || folded instanceof Date || typeof folded === "number" && Number.isFinite(folded)) {
|
|
2429
2430
|
const target = foldedArguments[1];
|
|
2430
2431
|
const targetWord = expression.name === "CAST" && target?.kind === "literal" && typeof target.value === "string" ? target.value : void 0;
|
|
2431
|
-
const
|
|
2432
|
+
const first = literalValues[0];
|
|
2433
|
+
const sqlDomain = expression.name === "ARRAY" ? {
|
|
2434
|
+
kind: "array",
|
|
2435
|
+
element: typeof first === "number" ? "DOUBLE" : typeof first === "boolean" ? "BOOLEAN" : first instanceof Date ? "TIMESTAMP" : "TEXT"
|
|
2436
|
+
} : targetWord?.startsWith("numeric:") === true ? (() => {
|
|
2432
2437
|
const [, precisionWord = "", scaleWord = ""] = targetWord.split(":");
|
|
2433
2438
|
return {
|
|
2434
2439
|
kind: "numeric",
|
|
@@ -2565,8 +2570,8 @@ function rewriteForInner(expression, source, derived, singleSource) {
|
|
|
2565
2570
|
if (containsAggregateOrWindow(item.expression))
|
|
2566
2571
|
return void 0;
|
|
2567
2572
|
if (derived.groupBy.length > 0) {
|
|
2568
|
-
const signature =
|
|
2569
|
-
if (!derived.groupBy.some((group) =>
|
|
2573
|
+
const signature = encodeQueryIdentity(item.expression);
|
|
2574
|
+
if (!derived.groupBy.some((group) => encodeQueryIdentity(group) === signature)) {
|
|
2570
2575
|
return void 0;
|
|
2571
2576
|
}
|
|
2572
2577
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeQueryIdentity } from "./query-identity.js";
|
|
1
2
|
import { copyDate, dateMilliseconds } from "../date-value.js";
|
|
2
3
|
import { estimateValuesBytes } from "./byte-estimates.js";
|
|
3
4
|
import { copyQueryResultExternalization } from "./query.js";
|
|
@@ -7,19 +8,7 @@ function queryResultMemoKey(sql, params) {
|
|
|
7
8
|
return JSON.stringify([sql, params.map(encodeParameter)]);
|
|
8
9
|
}
|
|
9
10
|
function planMemoKey(plan) {
|
|
10
|
-
return
|
|
11
|
-
if (value instanceof Date)
|
|
12
|
-
return { $date: dateMilliseconds(value) };
|
|
13
|
-
if (typeof value === "bigint")
|
|
14
|
-
return { $bigint: value.toString() };
|
|
15
|
-
if (typeof value === "number") {
|
|
16
|
-
if (Object.is(value, -0))
|
|
17
|
-
return { $number: "-0" };
|
|
18
|
-
if (!Number.isFinite(value))
|
|
19
|
-
return { $number: String(value) };
|
|
20
|
-
}
|
|
21
|
-
return value;
|
|
22
|
-
});
|
|
11
|
+
return encodeQueryIdentity(plan);
|
|
23
12
|
}
|
|
24
13
|
function copyQueryResult(result) {
|
|
25
14
|
const columns = result.columns;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Manifest, StoragePage } from "../storage/types.js";
|
|
2
|
+
/** Bounded, process-local table generations proved by a contiguous durable commit history. */
|
|
3
|
+
export declare class QueryGenerations {
|
|
4
|
+
#private;
|
|
5
|
+
readonly page: (after: number | null, limit: number) => Promise<StoragePage<Manifest, number>>;
|
|
6
|
+
constructor(page: (after: number | null, limit: number) => Promise<StoragePage<Manifest, number>>);
|
|
7
|
+
key(tableIds: readonly string[], version: number | null): Promise<string>;
|
|
8
|
+
}
|