@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.
- package/dist/engine/artifact-cache.js +3 -2
- package/dist/engine/buffered-writer.js +12 -2
- package/dist/engine/client.d.ts +9 -0
- package/dist/engine/client.js +112 -20
- package/dist/engine/database.d.ts +5 -3
- package/dist/engine/database.js +2199 -1675
- package/dist/engine/errors.d.ts +21 -2
- package/dist/engine/errors.js +30 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -0
- package/dist/engine/live-accept.js +13 -0
- package/dist/engine/live-aggregate.js +264 -0
- package/dist/engine/live-patch.d.ts +21 -0
- package/dist/engine/live-patch.js +31 -0
- package/dist/engine/live.d.ts +14 -0
- package/dist/engine/live.js +146 -142
- package/dist/engine/optimizer.js +11 -6
- package/dist/engine/query-cache.js +19 -15
- package/dist/engine/query-generations.js +61 -0
- package/dist/engine/query-identity.js +41 -0
- package/dist/engine/query.d.ts +7 -13
- package/dist/engine/query.js +298 -435
- package/dist/engine/result-state.d.ts +7 -0
- package/dist/engine/result-state.js +15 -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/vector.d.ts +4 -0
- package/dist/engine/vector.js +14 -14
- package/dist/engine/windows.d.ts +12 -0
- package/dist/engine/windows.js +387 -0
- package/dist/engine/worker-server.d.ts +1 -1
- package/dist/engine/worker-server.js +28 -15
- package/dist/engine/write-coordinator.js +54 -0
- package/dist/plan/model.d.ts +1 -1
- package/dist/storage/indexeddb.js +134 -89
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +3 -1
- package/dist/storage/opfs/leader.js +20 -9
- package/dist/storage/opfs/rpc.js +3 -1
- package/dist/storage/opfs/store.d.ts +2 -0
- package/dist/storage/opfs/store.js +119 -43
- package/dist/storage/toolkit/record-core.js +2 -1
- package/dist/storage/types.d.ts +14 -0
- package/dist/storage/types.js +21 -0
- package/dist/transactions/index.d.ts +3 -0
- package/dist/transactions/index.js +4 -1
- package/dist/worker-protocol/index.d.ts +1 -1
- package/dist/worker-protocol/index.js +1 -1
- package/package.json +2 -2
- package/postgres-feature-profile.json +6 -1
- package/sql-feature-matrix.json +20 -27
- package/dist/date-value.d.ts +0 -20
- package/dist/engine/artifact-cache.d.ts +0 -29
- package/dist/engine/byte-estimates.d.ts +0 -11
- package/dist/engine/cancellation.d.ts +0 -2
- package/dist/engine/defaults.d.ts +0 -29
- package/dist/engine/group-index.d.ts +0 -33
- package/dist/engine/join-index.d.ts +0 -10
- package/dist/engine/live-equal.d.ts +0 -7
- package/dist/engine/point-read.d.ts +0 -59
- package/dist/engine/query-cache.d.ts +0 -21
- package/dist/engine/result-wire.d.ts +0 -70
- package/dist/engine/sort-keys.d.ts +0 -73
- package/dist/engine/sql-domains.d.ts +0 -93
- package/dist/engine/sql-functions.d.ts +0 -11
- package/dist/engine/sql-json.d.ts +0 -40
- package/dist/engine/sql-semantics.d.ts +0 -66
- package/dist/engine/worker-store-indexeddb.d.ts +0 -2
- package/dist/engine/worker-store-memory.d.ts +0 -2
- package/dist/engine/worker-store-opfs.d.ts +0 -2
- package/dist/engine/write-block-planner.d.ts +0 -19
- package/dist/storage/opfs/leader.d.ts +0 -460
- package/dist/storage/opfs/rpc.d.ts +0 -82
- package/dist/storage/opfs/snapshot-ledger.d.ts +0 -41
package/dist/engine/errors.d.ts
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
* them (name, fields, instanceof) without bundling the executor.
|
|
4
4
|
*/
|
|
5
5
|
type ErrorValue = boolean | number | string | Date;
|
|
6
|
+
/** An idle SQL transaction rolled back; later statements must not become autocommits. */
|
|
7
|
+
export declare class TransactionExpiredError extends Error {
|
|
8
|
+
readonly name = "TransactionExpiredError";
|
|
9
|
+
constructor();
|
|
10
|
+
}
|
|
11
|
+
/** The worker stopped responding within the configured request deadline. */
|
|
12
|
+
export declare class DatabaseWorkerTimeoutError extends Error {
|
|
13
|
+
readonly method: string;
|
|
14
|
+
readonly timeoutMs: number;
|
|
15
|
+
readonly name = "DatabaseWorkerTimeoutError";
|
|
16
|
+
constructor(method: string, timeoutMs: number);
|
|
17
|
+
}
|
|
18
|
+
/** A transport failure cannot prove whether this operation published before its reply was lost. */
|
|
19
|
+
export declare class DatabaseWorkerOutcomeUnknownError extends Error {
|
|
20
|
+
readonly method: string;
|
|
21
|
+
readonly requestId: string;
|
|
22
|
+
readonly name = "DatabaseWorkerOutcomeUnknownError";
|
|
23
|
+
constructor(method: string, requestId: string, options?: ErrorOptions);
|
|
24
|
+
}
|
|
6
25
|
/** A database operation named a table that is not present in the current catalog. */
|
|
7
26
|
export declare class UnknownTableError extends TypeError {
|
|
8
27
|
readonly tableName: string;
|
|
@@ -17,10 +36,10 @@ export declare class DatabaseReadBacklogError extends Error {
|
|
|
17
36
|
}
|
|
18
37
|
/** A live-query owner reached one of its documented resident-resource ceilings. */
|
|
19
38
|
export declare class LiveQueryLimitError extends Error {
|
|
20
|
-
readonly resource: "set" | "group" | "subscription";
|
|
39
|
+
readonly resource: "set" | "group" | "subscription" | "byte";
|
|
21
40
|
readonly limit: number;
|
|
22
41
|
readonly name = "LiveQueryLimitError";
|
|
23
|
-
constructor(resource: "set" | "group" | "subscription", limit: number);
|
|
42
|
+
constructor(resource: "set" | "group" | "subscription" | "byte", limit: number);
|
|
24
43
|
}
|
|
25
44
|
export declare class UniqueConstraintError extends Error {
|
|
26
45
|
readonly tableName: string;
|
package/dist/engine/errors.js
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
class TransactionExpiredError extends Error {
|
|
2
|
+
name = "TransactionExpiredError";
|
|
3
|
+
constructor() {
|
|
4
|
+
super("The SQL transaction expired and rolled back; issue ROLLBACK or BEGIN before continuing");
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
class DatabaseWorkerTimeoutError extends Error {
|
|
8
|
+
method;
|
|
9
|
+
timeoutMs;
|
|
10
|
+
name = "DatabaseWorkerTimeoutError";
|
|
11
|
+
constructor(method, timeoutMs) {
|
|
12
|
+
super(`Database worker ${method} did not respond within ${String(timeoutMs)}ms`);
|
|
13
|
+
this.method = method;
|
|
14
|
+
this.timeoutMs = timeoutMs;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class DatabaseWorkerOutcomeUnknownError extends Error {
|
|
18
|
+
method;
|
|
19
|
+
requestId;
|
|
20
|
+
name = "DatabaseWorkerOutcomeUnknownError";
|
|
21
|
+
constructor(method, requestId, options) {
|
|
22
|
+
super(`Database worker outcome is unknown for ${method}; reconcile durable application IDs before retrying`, options);
|
|
23
|
+
this.method = method;
|
|
24
|
+
this.requestId = requestId;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
1
27
|
function formatValue(value) {
|
|
2
28
|
return value instanceof Date ? dateIsoString(value) : String(value);
|
|
3
29
|
}
|
|
@@ -22,7 +48,7 @@ class LiveQueryLimitError extends Error {
|
|
|
22
48
|
limit;
|
|
23
49
|
name = "LiveQueryLimitError";
|
|
24
50
|
constructor(resource, limit) {
|
|
25
|
-
super(resource === "set" ? `A database cannot retain more than ${String(limit)} live-query sets` : `A live-query set cannot retain more than ${String(limit)} ${resource} records`);
|
|
51
|
+
super(resource === "set" ? `A database cannot retain more than ${String(limit)} live-query sets` : resource === "byte" ? `A live-query set cannot retain more than ${String(limit)} modeled bytes` : `A live-query set cannot retain more than ${String(limit)} ${resource} records`);
|
|
26
52
|
this.resource = resource;
|
|
27
53
|
this.limit = limit;
|
|
28
54
|
}
|
|
@@ -117,10 +143,13 @@ export {
|
|
|
117
143
|
CompactionMemoryBudgetError,
|
|
118
144
|
CompactionWriteAmplificationError,
|
|
119
145
|
DatabaseReadBacklogError,
|
|
146
|
+
DatabaseWorkerOutcomeUnknownError,
|
|
147
|
+
DatabaseWorkerTimeoutError,
|
|
120
148
|
LiveQueryLimitError,
|
|
121
149
|
MaintenanceBacklogError,
|
|
122
150
|
MissingKeyError,
|
|
123
151
|
SqlCompileError,
|
|
152
|
+
TransactionExpiredError,
|
|
124
153
|
UniqueConstraintError,
|
|
125
154
|
UnknownTableError,
|
|
126
155
|
VisibleSegmentCursorStaleError
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -17,3 +17,4 @@ export type * from "./schema.js";
|
|
|
17
17
|
export type { InsertValue, CompiledQuery, CompiledStatement, QueryResult, QueryRow, QueryExecutionOptions, QueryValue, SqlColumnSchema, SqlColumnType, } from "./query.js";
|
|
18
18
|
export type { AsyncQueryExecutionOptions, QueryBatchExecutionOptions, QuerySpillStore, } from "./vector.js";
|
|
19
19
|
export type { MinnowSqlDriver, MinnowSqlExecutor } from "./sql-driver.js";
|
|
20
|
+
export { OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError, } from "../storage/types.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./errors.js";
|
|
|
4
4
|
import { QueryMemoryBudgetError } from "./memory.js";
|
|
5
5
|
import { MAX_SQL_NESTING_DEPTH, MAX_SQL_NUMERIC_DIGITS, MAX_SQL_PARAMETERS, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_STRUCTURED_VALUE_DEPTH, MAX_SQL_STRUCTURED_VALUE_ITEMS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS } from "./cache-limits.js";
|
|
6
6
|
import { column, foreignKeyName, isDestructiveStep, planMigration, schema, table, view } from "./schema.js";
|
|
7
|
+
import { OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError } from "../storage/types.js";
|
|
7
8
|
export {
|
|
8
9
|
MAX_SQL_NESTING_DEPTH,
|
|
9
10
|
MAX_SQL_NUMERIC_DIGITS,
|
|
@@ -15,6 +16,9 @@ export {
|
|
|
15
16
|
MAX_SQL_STRUCTURED_VALUE_ITEMS,
|
|
16
17
|
MAX_SQL_TEXT_CHARACTERS,
|
|
17
18
|
MAX_SQL_TOKENS,
|
|
19
|
+
OpfsCoordinationError,
|
|
20
|
+
OpfsDatabaseInUseError,
|
|
21
|
+
OpfsUncertainOutcomeError,
|
|
18
22
|
QueryMemoryBudgetError,
|
|
19
23
|
column,
|
|
20
24
|
foreignKeyName,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const publications = /* @__PURE__ */ new WeakMap();
|
|
2
|
+
function stageLiveExecution(execution, publish) {
|
|
3
|
+
publications.set(execution, publish);
|
|
4
|
+
return execution;
|
|
5
|
+
}
|
|
6
|
+
function acceptLiveExecution(execution) {
|
|
7
|
+
publications.get(execution)?.();
|
|
8
|
+
publications.delete(execution);
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
acceptLiveExecution,
|
|
12
|
+
stageLiveExecution
|
|
13
|
+
};
|
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
function contributionBytes(key, row) {
|
|
6
|
+
return 64 + key.length * 2 + row.group.length * 2 + row.values.reduce((sum, value) => sum + (typeof value === "string" ? value.length * 2 + 16 : 16), 0);
|
|
7
|
+
}
|
|
8
|
+
function groupBytes(key, group) {
|
|
9
|
+
return 96 + key.length * 2 + group.sums.reduce((sum, value) => sum + value.length * 2 + 24, 0);
|
|
10
|
+
}
|
|
11
|
+
class LiveAggregate {
|
|
12
|
+
inputPlan;
|
|
13
|
+
#outputPlan;
|
|
14
|
+
#aggregates;
|
|
15
|
+
#groupAliases;
|
|
16
|
+
#contributions;
|
|
17
|
+
#revision;
|
|
18
|
+
#pending;
|
|
19
|
+
#bytes;
|
|
20
|
+
#groups;
|
|
21
|
+
#domains;
|
|
22
|
+
keyAlias;
|
|
23
|
+
#schema;
|
|
24
|
+
constructor(inputPlan, outputPlan, aggregates, groupAliases, keyAlias, contributions = { rows: /* @__PURE__ */ new Map(), revision: 0 }, groups = /* @__PURE__ */ new Map(), domains = [], schema = [], bytes = 256 + encodeQueryIdentity(inputPlan).length * 2 + encodeQueryIdentity(outputPlan).length * 2, pending) {
|
|
25
|
+
this.inputPlan = inputPlan;
|
|
26
|
+
this.#outputPlan = outputPlan;
|
|
27
|
+
this.#aggregates = aggregates;
|
|
28
|
+
this.#groupAliases = groupAliases;
|
|
29
|
+
this.keyAlias = keyAlias;
|
|
30
|
+
this.#contributions = contributions;
|
|
31
|
+
this.#revision = contributions.revision;
|
|
32
|
+
this.#pending = pending;
|
|
33
|
+
this.#bytes = bytes;
|
|
34
|
+
this.#groups = groups;
|
|
35
|
+
this.#domains = domains;
|
|
36
|
+
this.#schema = schema;
|
|
37
|
+
}
|
|
38
|
+
static plan(plan, qualifiedKey) {
|
|
39
|
+
const aggregates = [];
|
|
40
|
+
const groupAliases = plan.groupBy.map((_, index) => `__minnow_live_group_${String(index)}`);
|
|
41
|
+
const groupKeys = new Map(plan.groupBy.map((expression, index) => [
|
|
42
|
+
encodeQueryIdentity(expression),
|
|
43
|
+
groupAliases[index] ?? ""
|
|
44
|
+
]));
|
|
45
|
+
const rowLocal = (expression) => !hasAggregate(expression) && !["subquery", "exists", "window", "parameter", "wildcard"].includes(expression.kind) && childExpressions(expression).every(rowLocal);
|
|
46
|
+
if (!plan.groupBy.every(rowLocal))
|
|
47
|
+
return void 0;
|
|
48
|
+
const rewrite = (expression) => {
|
|
49
|
+
const grouped = groupKeys.get(encodeQueryIdentity(expression));
|
|
50
|
+
if (grouped !== void 0)
|
|
51
|
+
return { kind: "column", reference: grouped };
|
|
52
|
+
if (expression.kind === "call" && hasAggregate(expression)) {
|
|
53
|
+
if (!["COUNT", "SUM", "AVG"].includes(expression.name) || expression.arguments.length !== 1 || expression.distinct === true)
|
|
54
|
+
throw new TypeError("Not an additive live aggregate");
|
|
55
|
+
const argument = expression.arguments[0];
|
|
56
|
+
if (argument === void 0 || !(rowLocal(argument) || expression.name === "COUNT" && argument.kind === "wildcard"))
|
|
57
|
+
throw new TypeError("Not a row-local aggregate argument");
|
|
58
|
+
const alias = `__minnow_live_value_${String(aggregates.length)}`;
|
|
59
|
+
aggregates.push({
|
|
60
|
+
name: expression.name,
|
|
61
|
+
alias,
|
|
62
|
+
argument: argument.kind === "wildcard" ? { kind: "literal", value: 1 } : argument
|
|
63
|
+
});
|
|
64
|
+
return { kind: "column", reference: alias };
|
|
65
|
+
}
|
|
66
|
+
if (expression.kind === "column" || expression.kind === "window" || expression.kind === "subquery" || expression.kind === "exists")
|
|
67
|
+
throw new TypeError("Not a grouped live expression");
|
|
68
|
+
return mapChildExpressions(expression, rewrite);
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
const select = plan.select.map((item) => ({ ...item, expression: rewrite(item.expression) }));
|
|
72
|
+
const having = plan.having.map((predicate) => ({
|
|
73
|
+
...predicate,
|
|
74
|
+
left: rewrite(predicate.left),
|
|
75
|
+
right: rewrite(predicate.right)
|
|
76
|
+
}));
|
|
77
|
+
const outputAliases = new Set(select.map((item) => item.alias));
|
|
78
|
+
const orderBy = plan.orderBy.map((term) => ({
|
|
79
|
+
...term,
|
|
80
|
+
expression: term.expression.kind === "column" && outputAliases.has(term.expression.reference) ? term.expression : rewrite(term.expression)
|
|
81
|
+
}));
|
|
82
|
+
if (aggregates.length === 0)
|
|
83
|
+
return void 0;
|
|
84
|
+
const keyAlias = "__minnow_live_aggregate_key";
|
|
85
|
+
const inputPlan = {
|
|
86
|
+
...plan,
|
|
87
|
+
select: [
|
|
88
|
+
{ alias: keyAlias, expression: { kind: "column", reference: qualifiedKey } },
|
|
89
|
+
...plan.groupBy.map((expression, index) => ({
|
|
90
|
+
alias: groupAliases[index] ?? "",
|
|
91
|
+
expression
|
|
92
|
+
})),
|
|
93
|
+
...aggregates.map(({ alias, argument }) => ({ alias, expression: argument }))
|
|
94
|
+
],
|
|
95
|
+
groupBy: [],
|
|
96
|
+
having: [],
|
|
97
|
+
orderBy: []
|
|
98
|
+
};
|
|
99
|
+
delete inputPlan.limit;
|
|
100
|
+
delete inputPlan.offset;
|
|
101
|
+
const outputPlan = {
|
|
102
|
+
...plan,
|
|
103
|
+
base: { table: "__minnow_live_groups", alias: "__minnow_live_groups" },
|
|
104
|
+
joins: [],
|
|
105
|
+
select,
|
|
106
|
+
predicates: having,
|
|
107
|
+
groupBy: [],
|
|
108
|
+
having: [],
|
|
109
|
+
orderBy
|
|
110
|
+
};
|
|
111
|
+
return new LiveAggregate(inputPlan, outputPlan, aggregates, groupAliases, keyAlias);
|
|
112
|
+
} catch {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
patch(result, changed, token) {
|
|
117
|
+
if (this.#pending !== void 0 || this.#revision !== this.#contributions.revision)
|
|
118
|
+
throw new Error("Unaccepted or stale aggregate");
|
|
119
|
+
const rows = this.#contributions.rows;
|
|
120
|
+
const pending = /* @__PURE__ */ new Map();
|
|
121
|
+
let bytes = this.#bytes;
|
|
122
|
+
const groups = new Map(this.#groups);
|
|
123
|
+
const touched = /* @__PURE__ */ new Set();
|
|
124
|
+
const groupFor = (key, keys = []) => {
|
|
125
|
+
let group = groups.get(key);
|
|
126
|
+
if (group !== void 0 && !touched.has(key))
|
|
127
|
+
bytes -= groupBytes(key, group);
|
|
128
|
+
if (group === void 0) {
|
|
129
|
+
group = {
|
|
130
|
+
keys,
|
|
131
|
+
members: 0,
|
|
132
|
+
counts: this.#aggregates.map(() => 0),
|
|
133
|
+
sums: this.#aggregates.map(() => exactNumericValue(0) ?? ""),
|
|
134
|
+
absolute: this.#aggregates.map(() => 0)
|
|
135
|
+
};
|
|
136
|
+
groups.set(key, group);
|
|
137
|
+
touched.add(key);
|
|
138
|
+
} else if (!touched.has(key)) {
|
|
139
|
+
group = {
|
|
140
|
+
...group,
|
|
141
|
+
counts: [...group.counts],
|
|
142
|
+
sums: [...group.sums],
|
|
143
|
+
absolute: [...group.absolute]
|
|
144
|
+
};
|
|
145
|
+
groups.set(key, group);
|
|
146
|
+
touched.add(key);
|
|
147
|
+
}
|
|
148
|
+
return group;
|
|
149
|
+
};
|
|
150
|
+
const apply = (group, values, sign) => {
|
|
151
|
+
group.members += sign;
|
|
152
|
+
for (const [index, aggregate] of this.#aggregates.entries()) {
|
|
153
|
+
const value = values[index] ?? null;
|
|
154
|
+
if (value === null)
|
|
155
|
+
continue;
|
|
156
|
+
group.counts[index] = (group.counts[index] ?? 0) + sign;
|
|
157
|
+
if (aggregate.name !== "COUNT") {
|
|
158
|
+
if (typeof value !== "number" && !isExactNumeric(value))
|
|
159
|
+
throw new TypeError("Live SUM requires numeric values");
|
|
160
|
+
if (typeof value === "number") {
|
|
161
|
+
if (!Number.isSafeInteger(value))
|
|
162
|
+
throw new TypeError("Floating-point aggregates require full execution");
|
|
163
|
+
group.absolute[index] = (group.absolute[index] ?? 0) + sign * Math.abs(value);
|
|
164
|
+
if (!Number.isSafeInteger(group.absolute[index]))
|
|
165
|
+
throw new TypeError("Aggregate sum may round; requires full execution");
|
|
166
|
+
}
|
|
167
|
+
const sum = exactNumericBinary(sign === 1 ? "+" : "-", group.sums[index] ?? exactNumericValue(0), exactNumericValue(value));
|
|
168
|
+
if (sum === null || sum === void 0)
|
|
169
|
+
throw new TypeError("Invalid numeric aggregate contribution");
|
|
170
|
+
group.sums[index] = sum;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
for (const key of changed) {
|
|
175
|
+
const old = rows.get(key);
|
|
176
|
+
if (old === void 0)
|
|
177
|
+
continue;
|
|
178
|
+
apply(groupFor(old.group), old.values, -1);
|
|
179
|
+
pending.set(key, void 0);
|
|
180
|
+
bytes -= contributionBytes(key, old);
|
|
181
|
+
}
|
|
182
|
+
for (const row of result.rows) {
|
|
183
|
+
const key = token(row[this.keyAlias] ?? null);
|
|
184
|
+
if (pending.get(key) !== void 0 || rows.has(key) && !changed.has(key))
|
|
185
|
+
throw new TypeError("Duplicate live input key");
|
|
186
|
+
const keys = this.#groupAliases.map((alias) => row[alias] ?? null);
|
|
187
|
+
const group = JSON.stringify(keys.map(encodeSqlEqualityValue));
|
|
188
|
+
const values = this.#aggregates.map(({ alias }) => row[alias] ?? null);
|
|
189
|
+
apply(groupFor(group, keys), values, 1);
|
|
190
|
+
const contribution = { group, values };
|
|
191
|
+
pending.set(key, contribution);
|
|
192
|
+
bytes += contributionBytes(key, contribution);
|
|
193
|
+
}
|
|
194
|
+
if (this.#groupAliases.length === 0)
|
|
195
|
+
groupFor("[]");
|
|
196
|
+
else
|
|
197
|
+
for (const key of touched)
|
|
198
|
+
if (groups.get(key)?.members === 0)
|
|
199
|
+
groups.delete(key);
|
|
200
|
+
for (const key of touched) {
|
|
201
|
+
const group = groups.get(key);
|
|
202
|
+
if (group !== void 0)
|
|
203
|
+
bytes += groupBytes(key, group);
|
|
204
|
+
}
|
|
205
|
+
const domains = this.#aggregates.map(({ alias }, index) => result.columnDomains[result.columns.indexOf(alias)] ?? this.#domains[index] ?? null);
|
|
206
|
+
return new LiveAggregate(this.inputPlan, this.#outputPlan, this.#aggregates, this.#groupAliases, this.keyAlias, this.#contributions, groups, domains, result.columns.map((name, index) => {
|
|
207
|
+
const domain = result.columnDomains[index];
|
|
208
|
+
const value = result.rows.find((row) => row[name] !== null)?.[name];
|
|
209
|
+
const aggregate = this.#aggregates.find((item) => item.alias === name);
|
|
210
|
+
return {
|
|
211
|
+
name,
|
|
212
|
+
type: aggregate?.name === "COUNT" ? "number" : domain !== null && domain !== void 0 ? "string" : typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : value instanceof Date ? "datetime" : value === void 0 || value === null ? this.#schema[index]?.type ?? "string" : "string",
|
|
213
|
+
...domain === null || domain === void 0 || aggregate?.name === "COUNT" ? {} : { sqlDomain: domain }
|
|
214
|
+
};
|
|
215
|
+
}), bytes, pending);
|
|
216
|
+
}
|
|
217
|
+
accept() {
|
|
218
|
+
const pending = this.#pending;
|
|
219
|
+
if (pending === void 0)
|
|
220
|
+
return;
|
|
221
|
+
if (this.#revision !== this.#contributions.revision)
|
|
222
|
+
throw new Error("Stale aggregate");
|
|
223
|
+
for (const [key, contribution] of pending) {
|
|
224
|
+
if (contribution === void 0)
|
|
225
|
+
this.#contributions.rows.delete(key);
|
|
226
|
+
else
|
|
227
|
+
this.#contributions.rows.set(key, contribution);
|
|
228
|
+
}
|
|
229
|
+
this.#revision = ++this.#contributions.revision;
|
|
230
|
+
this.#pending = void 0;
|
|
231
|
+
}
|
|
232
|
+
result() {
|
|
233
|
+
const rows = [];
|
|
234
|
+
for (const group of this.#groups.values()) {
|
|
235
|
+
const row = {};
|
|
236
|
+
for (const [index, alias] of this.#groupAliases.entries())
|
|
237
|
+
row[alias] = group.keys[index] ?? null;
|
|
238
|
+
for (const [index, aggregate] of this.#aggregates.entries()) {
|
|
239
|
+
const count = group.counts[index] ?? 0;
|
|
240
|
+
const domain = this.#domains[index];
|
|
241
|
+
let value = aggregate.name === "COUNT" ? count : count === 0 ? null : group.sums[index] ?? null;
|
|
242
|
+
if (value !== null && aggregate.name !== "COUNT") {
|
|
243
|
+
if (domain?.kind !== "numeric") {
|
|
244
|
+
value = Number(externalSqlDomainValue(value));
|
|
245
|
+
if (aggregate.name === "AVG")
|
|
246
|
+
value /= count;
|
|
247
|
+
} else if (aggregate.name === "AVG")
|
|
248
|
+
value = exactNumericBinary("/", value, count, domain.scale) ?? null;
|
|
249
|
+
}
|
|
250
|
+
row[aggregate.alias] = value;
|
|
251
|
+
}
|
|
252
|
+
rows.push(row);
|
|
253
|
+
}
|
|
254
|
+
const result = executeRowQueryInternal(this.#outputPlan, /* @__PURE__ */ new Map([["__minnow_live_groups", rows]]));
|
|
255
|
+
result.columnDomains = inferResultColumnDomains(this.#outputPlan, /* @__PURE__ */ new Map([["__minnow_live_groups", this.#schema]]));
|
|
256
|
+
return externalizeQueryResult(result);
|
|
257
|
+
}
|
|
258
|
+
get retainedBytes() {
|
|
259
|
+
return this.#bytes;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export {
|
|
263
|
+
LiveAggregate
|
|
264
|
+
};
|
|
@@ -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;
|
|
@@ -159,6 +169,8 @@ export interface LiveQueryHost {
|
|
|
159
169
|
dependencyTableIds(query: LiveQueryInput, probe?: CatalogProbe): Promise<Set<string>>;
|
|
160
170
|
/** Executes the statement; the returned result belongs to the set and is never shared. */
|
|
161
171
|
execute(query: LiveQueryInput, context?: LiveQueryExecuteContext): Promise<QueryResult>;
|
|
172
|
+
/** Optionally publishes an accepted result for adapters that re-execute after invalidation. */
|
|
173
|
+
memoize?(query: LiveQueryInput, result: QueryResult, probe: CatalogProbe): Promise<void>;
|
|
162
174
|
/**
|
|
163
175
|
* Executes a statement the host can later maintain incrementally, or returns undefined when
|
|
164
176
|
* the statement's shape rules that out; the set then executes it in full from then on.
|
|
@@ -178,6 +190,8 @@ export declare class LiveQuerySet {
|
|
|
178
190
|
get stats(): LiveQueryStats;
|
|
179
191
|
/** Registers a query, delivers its current result, and shares work with equal statements. */
|
|
180
192
|
subscribe(query: LiveQueryInput, options: LiveQuerySubscribeOptions): Promise<LiveQuerySubscription>;
|
|
193
|
+
/** Delivers resets or changed row payloads without constructing a private full row array per patch. */
|
|
194
|
+
subscribePatches(query: LiveQueryInput, options: LiveQueryPatchOptions): Promise<LiveQuerySubscription>;
|
|
181
195
|
/** Observes invalidation; the statement executes inside the set only when asked to compare. */
|
|
182
196
|
observe(query: LiveQueryInput, options: LiveQueryObserveOptions): Promise<LiveQuerySubscription>;
|
|
183
197
|
/** Called by the owning database after each local write commit; also hints other tabs. */
|