@minnowdb/core 0.10.1 → 0.10.2
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.js +2 -1
- package/dist/engine/database.js +140 -822
- package/dist/engine/index-terms.js +627 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +2 -1
- package/dist/engine/live-maintenance.js +220 -0
- package/dist/engine/schema.js +2 -1
- package/dist/engine/sql-functions.js +3 -2
- package/dist/engine/sql-quote.js +6 -0
- package/dist/engine/vector.js +1 -3
- package/dist/storage/indexeddb.d.ts +18 -0
- package/dist/storage/indexeddb.js +186 -42
- package/dist/storage/opfs/rpc.js +2 -1
- package/dist/storage/types.d.ts +33 -2
- package/dist/storage/types.js +17 -4
- package/dist/testing/block-store-conformance.js +40 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/interaction-simulator.d.ts +319 -0
- package/dist/testing/interaction-simulator.js +1631 -0
- package/package.json +4 -1
- package/dist/engine/client-audit-harness.js +0 -123
- package/dist/storage/indexeddb-audit-helpers.js +0 -269
- package/dist/storage/opfs/power-loss-model.js +0 -62
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { dateMilliseconds } from "../date-value.js";
|
|
2
|
+
import { estimateValuesBytes } from "./byte-estimates.js";
|
|
3
|
+
import { planMemoKey, sameQueryRow as sameLiveRow } from "./query-cache.js";
|
|
4
|
+
import { externalizeQueryResult } from "./query.js";
|
|
5
|
+
import { compareSqlValues } from "./sql-semantics.js";
|
|
6
|
+
const LIVE_HIDDEN_PREFIX = "__minnow_live_";
|
|
7
|
+
const LIVE_KEY_ALIAS = `${LIVE_HIDDEN_PREFIX}key`;
|
|
8
|
+
const LIVE_ORDER_ALIAS = `${LIVE_HIDDEN_PREFIX}order_`;
|
|
9
|
+
const LIVE_MAINTENANCE_MAX_DELTA_ROWS = 2048;
|
|
10
|
+
const LIVE_WINDOW_MARGIN_MIN = 16;
|
|
11
|
+
const LIVE_WINDOW_MARGIN_MAX = 64;
|
|
12
|
+
function liveKeyToken(value) {
|
|
13
|
+
if (typeof value === "number")
|
|
14
|
+
return `n:${String(value)}`;
|
|
15
|
+
if (typeof value === "string")
|
|
16
|
+
return `s:${value}`;
|
|
17
|
+
if (typeof value === "boolean")
|
|
18
|
+
return value ? "b:1" : "b:0";
|
|
19
|
+
if (value instanceof Date)
|
|
20
|
+
return `d:${String(dateMilliseconds(value))}`;
|
|
21
|
+
return "z";
|
|
22
|
+
}
|
|
23
|
+
function splitLiveHiddenColumns(executed, state) {
|
|
24
|
+
const publicCount = state.publicColumns.length;
|
|
25
|
+
const hidden = executed.columns.slice(publicCount);
|
|
26
|
+
const count = executed.rows.length;
|
|
27
|
+
const keys = new Array(count);
|
|
28
|
+
const order = state.orderTerms.map(() => new Array(count));
|
|
29
|
+
for (let index = 0; index < count; index += 1) {
|
|
30
|
+
const row = executed.rows[index] ?? {};
|
|
31
|
+
keys[index] = liveKeyToken(row[LIVE_KEY_ALIAS] ?? null);
|
|
32
|
+
for (const [term, { alias }] of state.orderTerms.entries()) {
|
|
33
|
+
const values = order[term];
|
|
34
|
+
if (values !== void 0)
|
|
35
|
+
values[index] = row[alias] ?? null;
|
|
36
|
+
}
|
|
37
|
+
for (const column of hidden)
|
|
38
|
+
Reflect.deleteProperty(row, column);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
result: externalizeQueryResult({
|
|
42
|
+
columns: executed.columns.slice(0, publicCount),
|
|
43
|
+
columnDomains: executed.columnDomains.slice(0, publicCount),
|
|
44
|
+
rows: executed.rows
|
|
45
|
+
}),
|
|
46
|
+
keys,
|
|
47
|
+
order
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function liveMaintainedOutcome(state, previous, previousIndex) {
|
|
51
|
+
const positions = /* @__PURE__ */ new Map();
|
|
52
|
+
let retainedBytes = state.retainedBytes ?? 128 + planMemoKey(state.fullPlan).length * 2 + planMemoKey(state.deltaPlan).length * 2;
|
|
53
|
+
for (const [index, key] of state.keys.entries()) {
|
|
54
|
+
if (positions.has(key))
|
|
55
|
+
throw new TypeError("Duplicate live input key");
|
|
56
|
+
positions.set(key, index);
|
|
57
|
+
if (state.retainedBytes === void 0)
|
|
58
|
+
retainedBytes += liveRowStateBytes(state, index);
|
|
59
|
+
}
|
|
60
|
+
state = { ...state, positions, retainedBytes };
|
|
61
|
+
const visibleCount = state.limit === void 0 ? state.rows.length : Math.min(state.rows.length, state.limit);
|
|
62
|
+
const visible = state.rows.slice(0, visibleCount);
|
|
63
|
+
if (previous === void 0) {
|
|
64
|
+
return {
|
|
65
|
+
result: {
|
|
66
|
+
columns: [...state.publicColumns],
|
|
67
|
+
columnDomains: [...state.columnDomains],
|
|
68
|
+
rows: visible
|
|
69
|
+
},
|
|
70
|
+
state,
|
|
71
|
+
retainedBytes,
|
|
72
|
+
changed: true
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const previousCount = previous.rows.length;
|
|
76
|
+
const retained = new Int32Array(visibleCount);
|
|
77
|
+
let same = visibleCount === previousCount;
|
|
78
|
+
for (let index = 0; index < visibleCount; index += 1) {
|
|
79
|
+
const was = previousIndex?.[index] ?? -1;
|
|
80
|
+
if (was >= 0 && was < previousCount) {
|
|
81
|
+
retained[index] = was;
|
|
82
|
+
if (was !== index)
|
|
83
|
+
same = false;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
retained[index] = -1;
|
|
87
|
+
const before = previous.rows[index];
|
|
88
|
+
const now = visible[index];
|
|
89
|
+
if (before !== void 0 && now !== void 0 && sameLiveRow(before, now, previous.columns)) {
|
|
90
|
+
visible[index] = before;
|
|
91
|
+
retained[index] = index;
|
|
92
|
+
} else
|
|
93
|
+
same = false;
|
|
94
|
+
}
|
|
95
|
+
if (same)
|
|
96
|
+
return { result: previous, state, retainedBytes, changed: false };
|
|
97
|
+
return {
|
|
98
|
+
result: {
|
|
99
|
+
columns: [...state.publicColumns],
|
|
100
|
+
columnDomains: [...state.columnDomains],
|
|
101
|
+
rows: visible
|
|
102
|
+
},
|
|
103
|
+
state,
|
|
104
|
+
retainedBytes,
|
|
105
|
+
changed: true,
|
|
106
|
+
retained
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function liveRowStateBytes(state, index) {
|
|
110
|
+
let bytes = 112 + (state.keys[index]?.length ?? 0) * 2 + estimateValuesBytes(Object.values(state.rows[index] ?? {})) * 2;
|
|
111
|
+
for (const values of state.order)
|
|
112
|
+
bytes += 8 + estimateValuesBytes([values[index]]) * 2;
|
|
113
|
+
return bytes;
|
|
114
|
+
}
|
|
115
|
+
function filterLiveRows(state, keep) {
|
|
116
|
+
const rows = [];
|
|
117
|
+
const keys = [];
|
|
118
|
+
const order = state.order.map(() => new Array());
|
|
119
|
+
const previous = [];
|
|
120
|
+
for (let index = 0; index < state.rows.length; index += 1) {
|
|
121
|
+
const row = state.rows[index];
|
|
122
|
+
if (row === void 0 || !keep(index))
|
|
123
|
+
continue;
|
|
124
|
+
rows.push(row);
|
|
125
|
+
keys.push(state.keys[index] ?? "z");
|
|
126
|
+
for (const [term, values] of state.order.entries()) {
|
|
127
|
+
order[term]?.push(values[index] ?? null);
|
|
128
|
+
}
|
|
129
|
+
previous.push(index);
|
|
130
|
+
}
|
|
131
|
+
return { rows, keys, order, previousIndex: Int32Array.from(previous) };
|
|
132
|
+
}
|
|
133
|
+
function trimLiveRows(rows, count) {
|
|
134
|
+
return {
|
|
135
|
+
rows: rows.rows.slice(0, count),
|
|
136
|
+
keys: rows.keys.slice(0, count),
|
|
137
|
+
order: rows.order.map((values) => values.slice(0, count)),
|
|
138
|
+
previousIndex: rows.previousIndex.slice(0, count)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function liveOrderComparator(terms) {
|
|
142
|
+
return (leftOrder, leftIndex, rightOrder, rightIndex) => {
|
|
143
|
+
for (const [term, { descending, nulls }] of terms.entries()) {
|
|
144
|
+
const a = leftOrder[term]?.[leftIndex] ?? null;
|
|
145
|
+
const b = rightOrder[term]?.[rightIndex] ?? null;
|
|
146
|
+
if (a === null || b === null) {
|
|
147
|
+
if (a === null && b === null)
|
|
148
|
+
continue;
|
|
149
|
+
const nullsFirst = nulls === "first" || nulls === void 0 && descending;
|
|
150
|
+
return a === null ? nullsFirst ? -1 : 1 : nullsFirst ? 1 : -1;
|
|
151
|
+
}
|
|
152
|
+
let comparison = compareSqlValues(a, b);
|
|
153
|
+
if (descending)
|
|
154
|
+
comparison = -comparison;
|
|
155
|
+
if (comparison !== 0)
|
|
156
|
+
return comparison;
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function mergeLiveRows(kept, added, compare, ordered) {
|
|
162
|
+
if (added.rows.length === 0)
|
|
163
|
+
return kept;
|
|
164
|
+
const terms = kept.order.length;
|
|
165
|
+
const total = kept.rows.length + added.rows.length;
|
|
166
|
+
const rows = new Array(total);
|
|
167
|
+
const keys = new Array(total);
|
|
168
|
+
const order = Array.from({ length: terms }, () => new Array(total));
|
|
169
|
+
const previousIndex = new Int32Array(total);
|
|
170
|
+
const take = (source, from, to) => {
|
|
171
|
+
rows[to] = source.rows[from] ?? {};
|
|
172
|
+
keys[to] = source.keys[from] ?? "z";
|
|
173
|
+
for (let term = 0; term < terms; term += 1) {
|
|
174
|
+
const values = order[term];
|
|
175
|
+
if (values !== void 0)
|
|
176
|
+
values[to] = source.order[term]?.[from] ?? null;
|
|
177
|
+
}
|
|
178
|
+
previousIndex[to] = source.previousIndex[from] ?? -1;
|
|
179
|
+
};
|
|
180
|
+
if (!ordered) {
|
|
181
|
+
for (let index = 0; index < kept.rows.length; index += 1)
|
|
182
|
+
take(kept, index, index);
|
|
183
|
+
for (let index = 0; index < added.rows.length; index += 1) {
|
|
184
|
+
take(added, index, kept.rows.length + index);
|
|
185
|
+
}
|
|
186
|
+
return { rows, keys, order, previousIndex };
|
|
187
|
+
}
|
|
188
|
+
const addedIndexes = added.rows.map((_, index) => index);
|
|
189
|
+
addedIndexes.sort((left, right) => compare(added.order, left, added.order, right));
|
|
190
|
+
let keptIndex = 0;
|
|
191
|
+
let addedPosition = 0;
|
|
192
|
+
for (let to = 0; to < total; to += 1) {
|
|
193
|
+
const addedIndex = addedIndexes[addedPosition];
|
|
194
|
+
const takeAdded = addedIndex !== void 0 && (keptIndex >= kept.rows.length || compare(added.order, addedIndex, kept.order, keptIndex) < 0);
|
|
195
|
+
if (takeAdded) {
|
|
196
|
+
take(added, addedIndex, to);
|
|
197
|
+
addedPosition += 1;
|
|
198
|
+
} else {
|
|
199
|
+
take(kept, keptIndex, to);
|
|
200
|
+
keptIndex += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { rows, keys, order, previousIndex };
|
|
204
|
+
}
|
|
205
|
+
export {
|
|
206
|
+
LIVE_HIDDEN_PREFIX,
|
|
207
|
+
LIVE_KEY_ALIAS,
|
|
208
|
+
LIVE_MAINTENANCE_MAX_DELTA_ROWS,
|
|
209
|
+
LIVE_ORDER_ALIAS,
|
|
210
|
+
LIVE_WINDOW_MARGIN_MAX,
|
|
211
|
+
LIVE_WINDOW_MARGIN_MIN,
|
|
212
|
+
filterLiveRows,
|
|
213
|
+
liveKeyToken,
|
|
214
|
+
liveMaintainedOutcome,
|
|
215
|
+
liveOrderComparator,
|
|
216
|
+
liveRowStateBytes,
|
|
217
|
+
mergeLiveRows,
|
|
218
|
+
splitLiveHiddenColumns,
|
|
219
|
+
trimLiveRows
|
|
220
|
+
};
|
package/dist/engine/schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { copyDate, dateMilliseconds } from "../date-value.js";
|
|
2
|
+
import { quoteSqlIdentifier } from "./sql-quote.js";
|
|
2
3
|
import { validateColumnDefault, validateEnumValues, validateSqlDomain } from "../storage/types.js";
|
|
3
4
|
import { childExpressions, compileCheckExpression, expressionColumns, hasAggregate, validateDefaultExpression } from "./query.js";
|
|
4
5
|
import { externalSqlDomainValue, normalizeSqlDomainValue } from "./sql-domains.js";
|
|
@@ -927,7 +928,7 @@ function planMigration(catalog, definition, options = {}) {
|
|
|
927
928
|
}
|
|
928
929
|
function typedTable(database, definition) {
|
|
929
930
|
const columnNames = Object.keys(definition.columns);
|
|
930
|
-
const quote =
|
|
931
|
+
const quote = quoteSqlIdentifier;
|
|
931
932
|
const scalarUniqueKey = Object.entries(definition.columns).find(([, columnDefinition]) => columnDefinition.isUnique)?.[0];
|
|
932
933
|
const keyColumns = definition.primaryKey.length > 0 ? [...definition.primaryKey] : scalarUniqueKey === void 0 ? [] : [scalarUniqueKey];
|
|
933
934
|
const normalizedRows = (rows) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { quoteSqlIdentifier } from "./sql-quote.js";
|
|
1
2
|
import { dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds } from "../date-value.js";
|
|
2
3
|
import { MAX_SQL_SCALAR_RESULT_CHARACTERS } from "./cache-limits.js";
|
|
3
4
|
import { dateDomainValue, exactNumericRounded, exactNumericUnary, externalSqlDomainValue, intervalDomainValue, isDateDomainValue, isExactNumeric, protectedSqlTextValue } from "./sql-domains.js";
|
|
@@ -467,7 +468,7 @@ function formatText(template, values) {
|
|
|
467
468
|
if (value === null || value === void 0)
|
|
468
469
|
throw new TypeError("FORMAT %I does not accept NULL");
|
|
469
470
|
const name = rendered(value);
|
|
470
|
-
return /^[a-z_][a-z0-9_]*$/.test(name) ? name :
|
|
471
|
+
return /^[a-z_][a-z0-9_]*$/.test(name) ? name : quoteSqlIdentifier(name);
|
|
471
472
|
}
|
|
472
473
|
if (value === null || value === void 0)
|
|
473
474
|
return "NULL";
|
|
@@ -902,7 +903,7 @@ const simpleScalarFunctions = /* @__PURE__ */ new Map([
|
|
|
902
903
|
returns: "string",
|
|
903
904
|
evaluate: (values) => {
|
|
904
905
|
const name = text("QUOTE_IDENT", values[0]);
|
|
905
|
-
return /^[a-z_][a-z0-9_]*$/.test(name) ? name : bounded(
|
|
906
|
+
return /^[a-z_][a-z0-9_]*$/.test(name) ? name : bounded(quoteSqlIdentifier(name), "QUOTE_IDENT");
|
|
906
907
|
}
|
|
907
908
|
}
|
|
908
909
|
],
|
package/dist/engine/vector.js
CHANGED
|
@@ -1308,7 +1308,6 @@ async function executeBoundPlanAsync(plan, memory, options) {
|
|
|
1308
1308
|
const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
|
|
1309
1309
|
return finishResult(plan, rows, memory);
|
|
1310
1310
|
}
|
|
1311
|
-
const SELECTED_ROW_COALESCE_GAP = 32;
|
|
1312
1311
|
async function scanSelectedRows(plan, selection, groups, output, memory, options) {
|
|
1313
1312
|
const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
|
|
1314
1313
|
let index = 0;
|
|
@@ -1329,9 +1328,8 @@ async function scanSelectedRows(plan, selection, groups, output, memory, options
|
|
|
1329
1328
|
index += 1;
|
|
1330
1329
|
while (index < selection.length) {
|
|
1331
1330
|
const next = selection[index] ?? 0;
|
|
1332
|
-
if (next
|
|
1331
|
+
if (next !== end || next >= windowEnd || next >= begin + DEFAULT_BATCH_ROWS)
|
|
1333
1332
|
break;
|
|
1334
|
-
}
|
|
1335
1333
|
end = next + 1;
|
|
1336
1334
|
index += 1;
|
|
1337
1335
|
}
|
|
@@ -9,6 +9,12 @@ export interface IndexedDbBlockStoreOptions {
|
|
|
9
9
|
* 8 MiB so a table's row count cannot silently become an unbounded resident-memory cost.
|
|
10
10
|
*/
|
|
11
11
|
uniqueKeyCacheBytes?: number;
|
|
12
|
+
/**
|
|
13
|
+
* How long the connection may answer nothing at all, while it has work outstanding, before
|
|
14
|
+
* every waiting call fails with `StorageUnresponsiveError` and the store refuses new work.
|
|
15
|
+
* Defaults to `INDEXEDDB_UNRESPONSIVE_AFTER_MS`.
|
|
16
|
+
*/
|
|
17
|
+
unresponsiveAfterMs?: number;
|
|
12
18
|
}
|
|
13
19
|
export declare class IndexedDbBlockStore implements BlockStore {
|
|
14
20
|
#private;
|
|
@@ -189,3 +195,15 @@ export declare class IndexedDbBlockStore implements BlockStore {
|
|
|
189
195
|
snapshotPeakRetainedBytes: number;
|
|
190
196
|
};
|
|
191
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* How long a connection may go without a single IndexedDB event, while it has work outstanding,
|
|
200
|
+
* before the adapter calls it unresponsive. Nothing in IndexedDB can be cancelled and nothing
|
|
201
|
+
* reports progress, so this is the only way for a store to tell a wedged browser from a busy
|
|
202
|
+
* one: any event anywhere on the connection resets the deadline, which means a transaction
|
|
203
|
+
* queued behind a genuinely long one is never mistaken for a wedge — only total silence is.
|
|
204
|
+
*
|
|
205
|
+
* See `StorageUnresponsiveError` for the wedge this bounds. Thirty seconds is far longer than
|
|
206
|
+
* any single request takes and comfortably inside the worker client's own request deadline, so
|
|
207
|
+
* the typed error is what a caller sees rather than a lost worker.
|
|
208
|
+
*/
|
|
209
|
+
export declare const INDEXEDDB_UNRESPONSIVE_AFTER_MS = 30000;
|