vastgrid-js 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +211 -2
- package/dist/aggregate.d.ts +45 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +305 -0
- package/dist/aggregate.js.map +1 -0
- package/dist/arrow.d.ts +7 -0
- package/dist/arrow.d.ts.map +1 -0
- package/dist/arrow.js +25 -0
- package/dist/arrow.js.map +1 -0
- package/dist/engine.d.ts +40 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1022 -0
- package/dist/engine.js.map +1 -0
- package/dist/evaluate.d.ts +37 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +774 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/export.d.ts +23 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +48 -0
- package/dist/export.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lazy.d.ts +65 -0
- package/dist/lazy.d.ts.map +1 -0
- package/dist/lazy.js +177 -0
- package/dist/lazy.js.map +1 -0
- package/dist/numbers.d.ts +20 -0
- package/dist/numbers.d.ts.map +1 -0
- package/dist/numbers.js +129 -0
- package/dist/numbers.js.map +1 -0
- package/dist/order.d.ts +67 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +561 -0
- package/dist/order.js.map +1 -0
- package/dist/parquet.d.ts +38 -0
- package/dist/parquet.d.ts.map +1 -0
- package/dist/parquet.js +673 -0
- package/dist/parquet.js.map +1 -0
- package/dist/plan.d.ts +37 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +51 -0
- package/dist/plan.js.map +1 -0
- package/dist/summary.d.ts +37 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +175 -0
- package/dist/summary.js.map +1 -0
- package/dist/table.d.ts +172 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +154 -0
- package/dist/table.js.map +1 -0
- package/dist/text.d.ts +18 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +1204 -0
- package/dist/text.js.map +1 -0
- package/dist/workbook.d.ts +6 -0
- package/dist/workbook.d.ts.map +1 -0
- package/dist/workbook.js +35 -0
- package/dist/workbook.js.map +1 -0
- package/package.json +46 -4
- package/src/aggregate.ts +358 -0
- package/src/arrow.ts +24 -0
- package/src/engine.ts +1163 -0
- package/src/evaluate.ts +764 -0
- package/src/export.ts +73 -0
- package/src/index.ts +2 -0
- package/src/lazy.ts +185 -0
- package/src/numbers.ts +128 -0
- package/src/order.ts +594 -0
- package/src/parquet.ts +801 -0
- package/src/plan.ts +99 -0
- package/src/summary.ts +180 -0
- package/src/table.ts +297 -0
- package/src/text.ts +1285 -0
- package/src/workbook.ts +48 -0
package/dist/engine.js
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An engine over tables in memory (docs/spec.md §8): it answers the requests of §8.2 about tables that
|
|
3
|
+
* its host gives it as arrays of typed JSON values (§2.2), as Arrow, as text files or as Parquet files, with views in
|
|
4
|
+
* the order of §3.3.
|
|
5
|
+
*/
|
|
6
|
+
import { answerId, checkPlan, checkRequest, encodeBlock, errorResponse, FUNCTIONS, keyText, MEASURES, MESSAGE_LIMITS, PlanError, ProtocolError, planHash, planOptions, SpecError, sameCellValue, textOf, } from "vastgrid-spec";
|
|
7
|
+
import { rankColumn, rankColumnPausing, sortRows } from "./order.js";
|
|
8
|
+
import { Summary } from "./summary.js";
|
|
9
|
+
import { checkTable, isArrowTable, isFileTable, isObject, isParquetTable, isWorkbook, shown, TableProblem, valueChecker, vectorOf, vectorValue, } from "./table.js";
|
|
10
|
+
/** The formats that `export` writes (§8.2), answered as the file's bytes. */
|
|
11
|
+
const EXPORT_FORMATS = Object.freeze(["csv", "tsv", "json", "xlsx"]);
|
|
12
|
+
/**
|
|
13
|
+
* What the engine evaluates (§4.10): every plan of §4.1, with every function of §4.4 and every measure of §4.5;
|
|
14
|
+
* `summarize`, `find`, `locate` and `export` of CSV, TSV, JSON and xlsx as data.
|
|
15
|
+
*/
|
|
16
|
+
export const MEMORY_CAPABILITIES = Object.freeze({
|
|
17
|
+
plan: Object.freeze({
|
|
18
|
+
derive: true,
|
|
19
|
+
where: true,
|
|
20
|
+
aggregate: true,
|
|
21
|
+
pivot: true,
|
|
22
|
+
having: true,
|
|
23
|
+
sort: true,
|
|
24
|
+
search: true,
|
|
25
|
+
}),
|
|
26
|
+
functions: Object.freeze([...FUNCTIONS]),
|
|
27
|
+
measures: Object.freeze([...MEASURES]),
|
|
28
|
+
summarize: true,
|
|
29
|
+
find: true,
|
|
30
|
+
locate: true,
|
|
31
|
+
write: false,
|
|
32
|
+
save: false,
|
|
33
|
+
export: Object.freeze({ formats: EXPORT_FORMATS, delivery: "data" }),
|
|
34
|
+
preview: false,
|
|
35
|
+
partialViews: false,
|
|
36
|
+
changes: "poll",
|
|
37
|
+
});
|
|
38
|
+
/** A copy of the capabilities, for a message; tables that the host makes editable are written (§7). */
|
|
39
|
+
function capabilities(write = false) {
|
|
40
|
+
return {
|
|
41
|
+
...MEMORY_CAPABILITIES,
|
|
42
|
+
plan: { ...MEMORY_CAPABILITIES.plan },
|
|
43
|
+
export: { formats: [...EXPORT_FORMATS], delivery: "data" },
|
|
44
|
+
functions: [...FUNCTIONS],
|
|
45
|
+
measures: [...MEASURES],
|
|
46
|
+
write,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const PLAN_OPTIONS = planOptions(capabilities());
|
|
50
|
+
/** Views that a table keeps, the least recently used forgotten first (§8.3). */
|
|
51
|
+
const KEPT_VIEWS = 8;
|
|
52
|
+
/** An error of §8.7 that a request is answered with. */
|
|
53
|
+
class Failure extends Error {
|
|
54
|
+
code;
|
|
55
|
+
extra;
|
|
56
|
+
constructor(code, message, extra = {}) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.extra = extra;
|
|
60
|
+
this.name = "Failure";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** A typed column with the values written to it (§7.2) over it; it reads as JSON, as the host's rows do. */
|
|
64
|
+
function writtenVector(base, written) {
|
|
65
|
+
return {
|
|
66
|
+
length: base.length,
|
|
67
|
+
at: (row) => {
|
|
68
|
+
const value = written.get(row);
|
|
69
|
+
return value === undefined ? base.at(row) : value;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Adds the names of columns that a plan names (§4.1) to `names`: in expressions, keys, measures, pivots and sorts.
|
|
75
|
+
* True when it searches (§3.5), which reads every column.
|
|
76
|
+
*/
|
|
77
|
+
function planNames(value, names) {
|
|
78
|
+
if (Array.isArray(value))
|
|
79
|
+
return value.reduce((search, item) => planNames(item, names) || search, false);
|
|
80
|
+
if (!isObject(value))
|
|
81
|
+
return false;
|
|
82
|
+
let search = typeof value.search === "string";
|
|
83
|
+
for (const [key, item] of Object.entries(value)) {
|
|
84
|
+
if ((key === "col" || key === "column") && typeof item === "string")
|
|
85
|
+
names.add(item);
|
|
86
|
+
else if ((key === "keys" || key === "columns") && Array.isArray(item)) {
|
|
87
|
+
for (const name of item)
|
|
88
|
+
if (typeof name === "string")
|
|
89
|
+
names.add(name);
|
|
90
|
+
}
|
|
91
|
+
else if (planNames(item, names))
|
|
92
|
+
search = true;
|
|
93
|
+
}
|
|
94
|
+
return search;
|
|
95
|
+
}
|
|
96
|
+
/** The position of a row that a view leaves out, in `View.positions`. */
|
|
97
|
+
const NO_POSITION = 0xffff_ffff;
|
|
98
|
+
/** The work that a request does before it lets other messages in, such as a `cancel` of it (§8.6), in ms. */
|
|
99
|
+
const SLICE = 40;
|
|
100
|
+
/**
|
|
101
|
+
* Lets other messages in when the request has worked for a slice since it last did, and stops it when it was
|
|
102
|
+
* cancelled (§8.6): a cancel that came in while the request waited for a file is seen at once. Requests that go
|
|
103
|
+
* through many rows call it between batches of them.
|
|
104
|
+
*/
|
|
105
|
+
async function pause(running) {
|
|
106
|
+
if (running.cancelled)
|
|
107
|
+
throw new Failure("cancelled", "The request was cancelled (§8.6).");
|
|
108
|
+
if (performance.now() - running.yielded < SLICE)
|
|
109
|
+
return;
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
111
|
+
running.yielded = performance.now();
|
|
112
|
+
if (running.cancelled)
|
|
113
|
+
throw new Failure("cancelled", "The request was cancelled (§8.6).");
|
|
114
|
+
}
|
|
115
|
+
/** The names that a plan sorts or groups by, or counts the distinct values of; derived columns are left out. */
|
|
116
|
+
function rankedNames(plan) {
|
|
117
|
+
const { aggregate } = plan;
|
|
118
|
+
const names = aggregate
|
|
119
|
+
? [
|
|
120
|
+
...aggregate.keys,
|
|
121
|
+
...(aggregate.pivot?.columns ?? []),
|
|
122
|
+
...aggregate.measures.flatMap((measure) => measure.fn === "count_distinct" && measure.column ? [measure.column] : []),
|
|
123
|
+
]
|
|
124
|
+
: (plan.sort ?? []).map((key) => key.column);
|
|
125
|
+
const derived = new Set((plan.derive ?? []).map((item) => item.name));
|
|
126
|
+
return names.filter((name) => !derived.has(name));
|
|
127
|
+
}
|
|
128
|
+
// The revision of each table that hosts give: its data never changes, so neither does its revision.
|
|
129
|
+
const revisions = new WeakMap();
|
|
130
|
+
let revisionCount = 0;
|
|
131
|
+
function revisionOf(table) {
|
|
132
|
+
let revision = revisions.get(table);
|
|
133
|
+
if (revision === undefined) {
|
|
134
|
+
revision = `memory-${++revisionCount}`;
|
|
135
|
+
revisions.set(table, revision);
|
|
136
|
+
}
|
|
137
|
+
return revision;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The string cut to `limit` code points, or null when it has no more than that (§8.4). A string of no more
|
|
141
|
+
* UTF-16 units than the limit has no more code points either.
|
|
142
|
+
*/
|
|
143
|
+
function cut(text, limit) {
|
|
144
|
+
if (text.length <= limit)
|
|
145
|
+
return null;
|
|
146
|
+
let units = 0;
|
|
147
|
+
for (let points = 0; points < limit && units < text.length; points++) {
|
|
148
|
+
units += text.codePointAt(units) > 0xffff ? 2 : 1;
|
|
149
|
+
}
|
|
150
|
+
return units < text.length ? text.slice(0, units) : null;
|
|
151
|
+
}
|
|
152
|
+
/** `[start, end)` spans within `[0, count)`, sorted, with those that overlap or touch merged. */
|
|
153
|
+
function mergeRanges(ranges, count) {
|
|
154
|
+
const spans = ranges
|
|
155
|
+
.map(([start, end]) => [Math.min(start, count), Math.min(end, count)])
|
|
156
|
+
.filter(([start, end]) => start < end)
|
|
157
|
+
.sort((a, b) => a[0] - b[0]);
|
|
158
|
+
const merged = [];
|
|
159
|
+
for (const span of spans) {
|
|
160
|
+
const last = merged[merged.length - 1];
|
|
161
|
+
if (last && span[0] <= last[1])
|
|
162
|
+
last[1] = Math.max(last[1], span[1]);
|
|
163
|
+
else
|
|
164
|
+
merged.push([span[0], span[1]]);
|
|
165
|
+
}
|
|
166
|
+
return merged;
|
|
167
|
+
}
|
|
168
|
+
class Engine {
|
|
169
|
+
sources;
|
|
170
|
+
/** Tables by ref, while a table ID is open on them. */
|
|
171
|
+
prepared = new Map();
|
|
172
|
+
/** Files being read by ref, which every open of the file waits for. */
|
|
173
|
+
reading = new Map();
|
|
174
|
+
/** Tables by table ID. */
|
|
175
|
+
tables = new Map();
|
|
176
|
+
running = new Map();
|
|
177
|
+
listeners = new Set();
|
|
178
|
+
nextTable = 1;
|
|
179
|
+
constructor(options) {
|
|
180
|
+
if (!isObject(options) || !isObject(options.tables)) {
|
|
181
|
+
throw new TypeError("memoryEngine takes { tables }, the tables that open opens by ref.");
|
|
182
|
+
}
|
|
183
|
+
this.sources = new Map(Object.entries(options.tables));
|
|
184
|
+
}
|
|
185
|
+
subscribe(listener) {
|
|
186
|
+
this.listeners.add(listener);
|
|
187
|
+
return () => {
|
|
188
|
+
this.listeners.delete(listener);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async handle(message) {
|
|
192
|
+
let request;
|
|
193
|
+
try {
|
|
194
|
+
request = checkRequest(message);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof ProtocolError) {
|
|
198
|
+
return errorResponse(answerId(message), "invalid_request", error.message, {
|
|
199
|
+
path: error.path,
|
|
200
|
+
reason: error.reason,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return errorResponse(answerId(message), "internal", String(error));
|
|
204
|
+
}
|
|
205
|
+
if (request.op === "cancel") {
|
|
206
|
+
const running = this.running.get(request.request);
|
|
207
|
+
if (running)
|
|
208
|
+
running.cancelled = true;
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const running = { cancelled: false, yielded: performance.now(), releases: [] };
|
|
212
|
+
this.running.set(request.id, running);
|
|
213
|
+
try {
|
|
214
|
+
return { v: 2, id: request.id, result: await this.answer(request, running) };
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error instanceof Failure)
|
|
218
|
+
return errorResponse(request.id, error.code, error.message, error.extra);
|
|
219
|
+
if (error instanceof PlanError) {
|
|
220
|
+
return errorResponse(request.id, "invalid_plan", error.message, { path: error.path, reason: error.reason });
|
|
221
|
+
}
|
|
222
|
+
if (error instanceof ProtocolError) {
|
|
223
|
+
return errorResponse(request.id, "invalid_request", error.message, { path: error.path, reason: error.reason });
|
|
224
|
+
}
|
|
225
|
+
if (error instanceof SpecError)
|
|
226
|
+
return errorResponse(request.id, error.code, error.message);
|
|
227
|
+
return errorResponse(request.id, "internal", error instanceof Error ? error.message : String(error));
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (this.running.get(request.id) === running)
|
|
231
|
+
this.running.delete(request.id);
|
|
232
|
+
for (const release of running.releases)
|
|
233
|
+
release();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async answer(request, running) {
|
|
237
|
+
switch (request.op) {
|
|
238
|
+
case "open":
|
|
239
|
+
return this.open(request.ref, request.read);
|
|
240
|
+
case "info": {
|
|
241
|
+
const id = request.table;
|
|
242
|
+
return this.info(id, this.table(request));
|
|
243
|
+
}
|
|
244
|
+
case "changes":
|
|
245
|
+
return { revision: this.table(request).revision, edits: { ...this.table(request).edits } };
|
|
246
|
+
case "reload": {
|
|
247
|
+
// Tables in memory have no file that changes (§7.6): the table is as it is.
|
|
248
|
+
const id = request.table;
|
|
249
|
+
return this.info(id, this.table(request));
|
|
250
|
+
}
|
|
251
|
+
case "close": {
|
|
252
|
+
const table = this.table(request);
|
|
253
|
+
this.tables.delete(request.table);
|
|
254
|
+
// A table that no ID names any more forgets its ranks and views, unless it holds written values, which
|
|
255
|
+
// every later request sees (§7.2); opening it again checks it again.
|
|
256
|
+
if (--table.open === 0 && table.written.size === 0) {
|
|
257
|
+
this.prepared.delete(table.key);
|
|
258
|
+
table.reader?.close();
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
case "rows":
|
|
263
|
+
return this.rows(request, running);
|
|
264
|
+
case "summarize":
|
|
265
|
+
return this.summarize(request, running);
|
|
266
|
+
case "find":
|
|
267
|
+
return this.find(request, running);
|
|
268
|
+
case "locate":
|
|
269
|
+
return this.locate(request, running);
|
|
270
|
+
case "export":
|
|
271
|
+
return this.export(request, running);
|
|
272
|
+
case "write":
|
|
273
|
+
return this.write(request, running);
|
|
274
|
+
case "save":
|
|
275
|
+
// A table in memory has no file to write back (§7.5).
|
|
276
|
+
this.table(request);
|
|
277
|
+
throw new Failure("save_unsupported", "This engine keeps tables in memory and does not save them (capabilities.save is false).");
|
|
278
|
+
default:
|
|
279
|
+
throw new Failure("invalid_request", `This engine does not offer ${request.op} (§4.10).`, {
|
|
280
|
+
path: "/op",
|
|
281
|
+
reason: "unknown_op",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async open(ref, read) {
|
|
286
|
+
if (!this.sources.has(ref))
|
|
287
|
+
throw new Failure("not_allowed", `The host does not open ${shown(ref)}.`);
|
|
288
|
+
const source = this.sources.get(ref);
|
|
289
|
+
// Arrow tables and files are read by modules of their own, loaded before the table is looked up so that two
|
|
290
|
+
// opens of one table do not both make it.
|
|
291
|
+
const arrow = isArrowTable(source) ? await import("./arrow.js") : null;
|
|
292
|
+
const file = isFileTable(source);
|
|
293
|
+
const parquet = isParquetTable(source);
|
|
294
|
+
const typed = arrow || file || parquet ? await import("./numbers.js") : null;
|
|
295
|
+
// A workbook is read once for each set of options of `read`; other tables ignore them.
|
|
296
|
+
const workbook = file && isWorkbook(source) ? await import("./workbook.js") : null;
|
|
297
|
+
const options = workbook ? workbook.readOptionsOf(read) : null;
|
|
298
|
+
const key = options ? `${ref}\u0000${JSON.stringify(options)}` : ref;
|
|
299
|
+
let table = this.prepared.get(key);
|
|
300
|
+
if (!table) {
|
|
301
|
+
let checked;
|
|
302
|
+
try {
|
|
303
|
+
checked = arrow
|
|
304
|
+
? arrow.checkArrow(source.arrow)
|
|
305
|
+
: workbook && options
|
|
306
|
+
? await this.reads(key, () => workbook.readSheet(source, options))
|
|
307
|
+
: file
|
|
308
|
+
? await this.reads(key, async () => (await import("./text.js")).readFile(source))
|
|
309
|
+
: parquet
|
|
310
|
+
? await this.reads(key, async () => (await import("./parquet.js")).ParquetFile.open(source).then((file) => file.checked()))
|
|
311
|
+
: checkTable(source);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (!(error instanceof TableProblem))
|
|
315
|
+
throw error;
|
|
316
|
+
const at = error.path === "" ? "" : `at ${error.path}, `;
|
|
317
|
+
throw new Failure(error.code, `The table ${shown(ref)} cannot be read: ${at}${error.message}`);
|
|
318
|
+
}
|
|
319
|
+
// Another open of the file may have made the table while the file was read.
|
|
320
|
+
table = this.prepared.get(key) ?? this.prepare(ref, key, source, checked, typed);
|
|
321
|
+
}
|
|
322
|
+
const id = `t${this.nextTable++}`;
|
|
323
|
+
this.tables.set(id, table);
|
|
324
|
+
table.open++;
|
|
325
|
+
return this.info(id, table);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Reads a table given as a file, once for every open that waits for it: a text file (text.ts), a sheet of a
|
|
329
|
+
* workbook (workbook.ts), or the footer of a Parquet file (parquet.ts).
|
|
330
|
+
*/
|
|
331
|
+
reads(key, read) {
|
|
332
|
+
let reading = this.reading.get(key);
|
|
333
|
+
if (!reading) {
|
|
334
|
+
reading = read().finally(() => this.reading.delete(key));
|
|
335
|
+
this.reading.set(key, reading);
|
|
336
|
+
}
|
|
337
|
+
return reading;
|
|
338
|
+
}
|
|
339
|
+
/** A table that is opened from its host's table, checked or read. */
|
|
340
|
+
prepare(ref, key, source, checked, typed) {
|
|
341
|
+
const { columns } = checked;
|
|
342
|
+
const table = {
|
|
343
|
+
ref,
|
|
344
|
+
key,
|
|
345
|
+
columns,
|
|
346
|
+
index: new Map(columns.map((column, index) => [column.name, index])),
|
|
347
|
+
rows: checked.rows,
|
|
348
|
+
vectors: checked.vectors,
|
|
349
|
+
reader: checked.reader ?? null,
|
|
350
|
+
typed,
|
|
351
|
+
rowCount: checked.rowCount,
|
|
352
|
+
revision: revisionOf(source),
|
|
353
|
+
editable: source.editable === true,
|
|
354
|
+
edits: { version: 0, cells: 0 },
|
|
355
|
+
written: new Set(),
|
|
356
|
+
keys: new Map(),
|
|
357
|
+
ranks: columns.map(() => undefined),
|
|
358
|
+
checkers: columns.map((column, index) => valueChecker(column, index)),
|
|
359
|
+
values: columns.map(() => undefined),
|
|
360
|
+
overlays: columns.map(() => undefined),
|
|
361
|
+
views: new Map(),
|
|
362
|
+
open: 0,
|
|
363
|
+
workbook: checked.workbook ?? null,
|
|
364
|
+
};
|
|
365
|
+
// What the engine made of a column of a file that is forgotten, it makes again when a request reads it whole.
|
|
366
|
+
if (table.reader) {
|
|
367
|
+
table.reader.forgotten = (index) => {
|
|
368
|
+
table.values[index] = undefined;
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
this.prepared.set(key, table);
|
|
372
|
+
return table;
|
|
373
|
+
}
|
|
374
|
+
/** TableInfo (§8.2). */
|
|
375
|
+
info(id, table) {
|
|
376
|
+
const info = {
|
|
377
|
+
table: id,
|
|
378
|
+
revision: table.revision,
|
|
379
|
+
edits: { ...table.edits },
|
|
380
|
+
status: "ready",
|
|
381
|
+
progress: 1,
|
|
382
|
+
rowCount: table.rowCount,
|
|
383
|
+
readableRows: table.rowCount,
|
|
384
|
+
columns: table.columns.map((column) => ({ ...column })),
|
|
385
|
+
capabilities: capabilities(table.editable),
|
|
386
|
+
};
|
|
387
|
+
const { workbook } = table;
|
|
388
|
+
if (workbook) {
|
|
389
|
+
if (workbook.notes.length > 0)
|
|
390
|
+
info.notes = [...workbook.notes];
|
|
391
|
+
info.sheets = workbook.sheets.map((sheet) => ({ ...sheet }));
|
|
392
|
+
info.sheet = workbook.sheet;
|
|
393
|
+
}
|
|
394
|
+
return info;
|
|
395
|
+
}
|
|
396
|
+
/** The table a request names, at the revision it names when it names one (§8.3). */
|
|
397
|
+
table(request) {
|
|
398
|
+
const table = this.tables.get(request.table);
|
|
399
|
+
if (!table)
|
|
400
|
+
throw new Failure("table_not_found", `There is no open table ${shown(request.table)}.`);
|
|
401
|
+
if ("revision" in request && request.revision !== table.revision) {
|
|
402
|
+
throw new Failure("revision_changed", `The table's revision is ${table.revision}; read info again.`);
|
|
403
|
+
}
|
|
404
|
+
return table;
|
|
405
|
+
}
|
|
406
|
+
/** Answers `load_failed` for a value that does not follow §2.2, at its JSON Pointer in the table. */
|
|
407
|
+
loadFailed(table, error) {
|
|
408
|
+
throw new Failure(error.code, `The table ${shown(table.ref)} cannot be read: at ${error.path}, ${error.message}`);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Checks every value of a column, once, before something reads all of it, and gives its values by row ID: of a
|
|
412
|
+
* table given as rows, an array of the engine's own; of one given as Arrow or as a file, its column.
|
|
413
|
+
*/
|
|
414
|
+
valuesOf(table, index) {
|
|
415
|
+
const made = table.values[index];
|
|
416
|
+
if (made)
|
|
417
|
+
return made;
|
|
418
|
+
const check = table.checkers[index];
|
|
419
|
+
let values;
|
|
420
|
+
try {
|
|
421
|
+
if (table.vectors) {
|
|
422
|
+
values = table.vectors[index];
|
|
423
|
+
const column = table.columns[index];
|
|
424
|
+
// Numbers are checked as numbers; a value found wrong is then read, which fails as its JSON would.
|
|
425
|
+
if (values.numbers && table.typed) {
|
|
426
|
+
const row = table.typed.firstInvalid(column, values.numbers, values.validity ?? null);
|
|
427
|
+
if (row >= 0)
|
|
428
|
+
check(vectorValue(values, column, index, row), row);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
for (let row = 0; row < table.rowCount; row++)
|
|
432
|
+
check(vectorValue(values, column, index, row), row);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
const rows = table.rows;
|
|
437
|
+
const array = new Array(rows.length);
|
|
438
|
+
for (let row = 0; row < rows.length; row++) {
|
|
439
|
+
const value = rows[row][index];
|
|
440
|
+
check(value, row);
|
|
441
|
+
array[row] = value;
|
|
442
|
+
}
|
|
443
|
+
values = vectorOf(array);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
if (error instanceof TableProblem)
|
|
448
|
+
this.loadFailed(table, error);
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
const written = table.overlays[index];
|
|
452
|
+
if (written)
|
|
453
|
+
values = writtenVector(values, written);
|
|
454
|
+
table.values[index] = values;
|
|
455
|
+
return values;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Reads whole the columns that a request reads whole, before it reads them, of a table whose file is read as
|
|
459
|
+
* requests need it (lazy.ts); the other tables hold all their columns.
|
|
460
|
+
*/
|
|
461
|
+
async readColumns(table, indexes, running) {
|
|
462
|
+
if (!table.reader)
|
|
463
|
+
return;
|
|
464
|
+
try {
|
|
465
|
+
running.releases.push(await table.reader.read(indexes, () => pause(running)));
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (error instanceof TableProblem)
|
|
469
|
+
this.loadFailed(table, error);
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* The values of a table's column at rows, in their order, checked when its column is not (§2.2): of a table whose
|
|
475
|
+
* file is read as requests need it, read by the parts of the file that hold them unless the column was read whole.
|
|
476
|
+
*/
|
|
477
|
+
async cellsOf(table, index, rows) {
|
|
478
|
+
if (table.values[index] || !table.reader)
|
|
479
|
+
return Array.from(rows, (row) => this.cell(table, row, index));
|
|
480
|
+
let read;
|
|
481
|
+
try {
|
|
482
|
+
read = await table.reader.cells(index, rows);
|
|
483
|
+
const written = table.overlays[index];
|
|
484
|
+
const check = table.checkers[index];
|
|
485
|
+
return Array.from(rows, (row, at) => {
|
|
486
|
+
const value = written?.get(row);
|
|
487
|
+
if (value !== undefined)
|
|
488
|
+
return value;
|
|
489
|
+
check(read[at], row);
|
|
490
|
+
return read[at];
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
if (error instanceof TableProblem)
|
|
495
|
+
this.loadFailed(table, error);
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** The value of a table's column at a row as the table holds it, checked when its column is not (§2.2). */
|
|
500
|
+
cell(table, row, index) {
|
|
501
|
+
const values = table.values[index];
|
|
502
|
+
if (values)
|
|
503
|
+
return values.at(row);
|
|
504
|
+
const written = table.overlays[index]?.get(row);
|
|
505
|
+
if (written !== undefined)
|
|
506
|
+
return written;
|
|
507
|
+
try {
|
|
508
|
+
const value = table.vectors
|
|
509
|
+
? vectorValue(table.vectors[index], table.columns[index], index, row)
|
|
510
|
+
: table.rows[row][index];
|
|
511
|
+
table.checkers[index](value, row);
|
|
512
|
+
return value;
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
if (error instanceof TableProblem)
|
|
516
|
+
this.loadFailed(table, error);
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Writes a cell: into the engine's own array of a column of a table given as rows, or over a typed column, which
|
|
522
|
+
* stays as it was and is not copied.
|
|
523
|
+
*/
|
|
524
|
+
setCell(table, row, index, value) {
|
|
525
|
+
if (!table.vectors) {
|
|
526
|
+
this.valuesOf(table, index)[row] = value;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
let written = table.overlays[index];
|
|
530
|
+
if (!written) {
|
|
531
|
+
written = new Map();
|
|
532
|
+
table.overlays[index] = written;
|
|
533
|
+
const made = table.values[index];
|
|
534
|
+
if (made)
|
|
535
|
+
table.values[index] = writtenVector(made, written);
|
|
536
|
+
}
|
|
537
|
+
written.set(row, value);
|
|
538
|
+
}
|
|
539
|
+
ranksOf(table, index) {
|
|
540
|
+
let ranks = table.ranks[index];
|
|
541
|
+
if (!ranks) {
|
|
542
|
+
const column = table.columns[index];
|
|
543
|
+
const values = this.valuesOf(table, index);
|
|
544
|
+
// A typed column is ranked from its numbers, until a write makes it an array.
|
|
545
|
+
const write = values.numbers && table.typed ? table.typed.numberWriter(column, values.numbers) : null;
|
|
546
|
+
ranks = rankColumn(column, values, write);
|
|
547
|
+
table.ranks[index] = ranks;
|
|
548
|
+
}
|
|
549
|
+
return ranks;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Ranks a column as `ranksOf` does, with pauses while text is ranked: the ranks of a write's column made while the
|
|
553
|
+
* write came are not kept.
|
|
554
|
+
*/
|
|
555
|
+
async rankPausing(table, index, running) {
|
|
556
|
+
if (table.ranks[index])
|
|
557
|
+
return;
|
|
558
|
+
const { version } = table.edits;
|
|
559
|
+
const column = table.columns[index];
|
|
560
|
+
const values = this.valuesOf(table, index);
|
|
561
|
+
const write = values.numbers && table.typed ? table.typed.numberWriter(column, values.numbers) : null;
|
|
562
|
+
const ranks = await rankColumnPausing(column, values, write, () => pause(running));
|
|
563
|
+
if (table.edits.version === version)
|
|
564
|
+
table.ranks[index] ??= ranks;
|
|
565
|
+
}
|
|
566
|
+
/** The view of a request's plan (§4.7, §8.3): checked against the table, kept by its hash. */
|
|
567
|
+
async view(table, plan, running) {
|
|
568
|
+
const checked = checkPlan(plan, table.columns, PLAN_OPTIONS);
|
|
569
|
+
const hash = await planHash(checked.plan);
|
|
570
|
+
if (running.cancelled)
|
|
571
|
+
throw new Failure("cancelled", "The request was cancelled (§8.6).");
|
|
572
|
+
const kept = table.views.get(hash);
|
|
573
|
+
if (kept) {
|
|
574
|
+
table.views.delete(hash);
|
|
575
|
+
table.views.set(hash, kept);
|
|
576
|
+
return kept;
|
|
577
|
+
}
|
|
578
|
+
if (table.reader) {
|
|
579
|
+
const names = new Set();
|
|
580
|
+
const every = planNames(checked.plan, names);
|
|
581
|
+
const named = [...names].filter((name) => table.index.has(name));
|
|
582
|
+
await this.readColumns(table, every ? table.columns.keys() : named.map((name) => table.index.get(name)), running);
|
|
583
|
+
}
|
|
584
|
+
// The columns that the view sorts or groups by are ranked first, which a cancel stops.
|
|
585
|
+
for (const name of rankedNames(checked.plan)) {
|
|
586
|
+
const index = table.index.get(name);
|
|
587
|
+
if (index !== undefined)
|
|
588
|
+
await this.rankPausing(table, index, running);
|
|
589
|
+
}
|
|
590
|
+
const count = table.rowCount;
|
|
591
|
+
const ranks = (name) => this.ranksOf(table, table.index.get(name));
|
|
592
|
+
let shape;
|
|
593
|
+
if (checked.derive.length > 0 || checked.where || checked.aggregate) {
|
|
594
|
+
const { planView } = await import("./plan.js");
|
|
595
|
+
shape = planView(checked, {
|
|
596
|
+
count,
|
|
597
|
+
values: (name) => this.valuesOf(table, table.index.get(name)),
|
|
598
|
+
ranks,
|
|
599
|
+
rankColumn,
|
|
600
|
+
sortRows,
|
|
601
|
+
sortKeys: (ranksOf) => this.sortKeys(checked, ranksOf),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
shape = {
|
|
606
|
+
columns: checked.columns,
|
|
607
|
+
derived: new Map(),
|
|
608
|
+
grouped: false,
|
|
609
|
+
tableRows: count,
|
|
610
|
+
rowCount: count,
|
|
611
|
+
order: sortRows(count, this.sortKeys(checked, ranks), null),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const view = { hash, ...shape, firsts: new Map() };
|
|
615
|
+
table.views.set(hash, view);
|
|
616
|
+
if (table.views.size > KEPT_VIEWS)
|
|
617
|
+
table.views.delete(table.views.keys().next().value);
|
|
618
|
+
// A view is made to its end, and kept; a cancel that came while it was made stops the request after it.
|
|
619
|
+
await pause(running);
|
|
620
|
+
return view;
|
|
621
|
+
}
|
|
622
|
+
/** The sort keys of a plan (§3.3); a key on a column that an earlier key sorted by cannot break a tie. */
|
|
623
|
+
sortKeys(checked, ranksOf) {
|
|
624
|
+
const used = new Set();
|
|
625
|
+
const keys = [];
|
|
626
|
+
for (const key of checked.sort) {
|
|
627
|
+
const name = key.column.name;
|
|
628
|
+
if (used.has(name))
|
|
629
|
+
continue;
|
|
630
|
+
used.add(name);
|
|
631
|
+
keys.push({ ranks: ranksOf(name), descending: key.direction === "desc" });
|
|
632
|
+
}
|
|
633
|
+
return keys;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* The columns that a request names, which must be columns of the view: the index of a table's column, or the
|
|
637
|
+
* values of a derived one or of a column of groups.
|
|
638
|
+
*/
|
|
639
|
+
columnsOf(table, view, names) {
|
|
640
|
+
return names.map((name, at) => {
|
|
641
|
+
const derived = view.derived.get(name);
|
|
642
|
+
if (derived)
|
|
643
|
+
return { name, column: derived.column, index: -1, values: vectorOf(derived.values) };
|
|
644
|
+
const index = table.index.get(name);
|
|
645
|
+
if (index === undefined || !view.columns.some((column) => column.name === name)) {
|
|
646
|
+
throw new Failure("invalid_request", `The view has no column ${shown(name)}.`, {
|
|
647
|
+
path: `/columns/${at}`,
|
|
648
|
+
reason: "syntax",
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return { name, column: table.columns[index], index, values: null };
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
async rows(request, running) {
|
|
655
|
+
const table = this.table(request);
|
|
656
|
+
const view = await this.view(table, request.plan, running);
|
|
657
|
+
const names = request.columns;
|
|
658
|
+
const sources = this.columnsOf(table, view, names);
|
|
659
|
+
const start = request.start;
|
|
660
|
+
const end = Math.max(start, Math.min(request.end, view.rowCount));
|
|
661
|
+
const limit = request.textLimit ?? MESSAGE_LIMITS.textLimitDefault;
|
|
662
|
+
const columns = sources.map((source) => source.column);
|
|
663
|
+
const rowIds = [];
|
|
664
|
+
for (let position = start; position < end; position++)
|
|
665
|
+
rowIds.push(view.order ? view.order[position] : position);
|
|
666
|
+
if (table.reader) {
|
|
667
|
+
try {
|
|
668
|
+
await table.reader.prepare(sources.flatMap((source) => (source.values ? [] : [source.index])), rowIds, () => pause(running));
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
if (error instanceof TableProblem)
|
|
672
|
+
this.loadFailed(table, error);
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// The values read are checked, of the table's columns not checked whole (§2.2).
|
|
677
|
+
const cells = await Promise.all(sources.map((source) => (source.values ? null : this.cellsOf(table, source.index, rowIds))));
|
|
678
|
+
const values = [];
|
|
679
|
+
// By name in a Map: a column may be named "__proto__".
|
|
680
|
+
const truncated = new Map();
|
|
681
|
+
for (let offset = 0; offset < rowIds.length; offset++) {
|
|
682
|
+
const row = rowIds[offset];
|
|
683
|
+
values.push(sources.map(({ values }, at) => {
|
|
684
|
+
const value = values ? values.at(row) : cells[at][offset];
|
|
685
|
+
if (typeof value !== "string" || columns[at].type !== "string")
|
|
686
|
+
return value;
|
|
687
|
+
const shorter = cut(value, limit);
|
|
688
|
+
if (shorter === null)
|
|
689
|
+
return value;
|
|
690
|
+
const offsets = truncated.get(names[at]);
|
|
691
|
+
if (!offsets)
|
|
692
|
+
truncated.set(names[at], [offset]);
|
|
693
|
+
else if (offsets[offsets.length - 1] !== offset)
|
|
694
|
+
offsets.push(offset);
|
|
695
|
+
return shorter;
|
|
696
|
+
}));
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
view: {
|
|
700
|
+
hash: view.hash,
|
|
701
|
+
status: "ready",
|
|
702
|
+
rowCount: view.rowCount,
|
|
703
|
+
rowCountExact: true,
|
|
704
|
+
readableRows: view.rowCount,
|
|
705
|
+
columns: view.columns.map((column) => ({ ...column })),
|
|
706
|
+
edits: { ...table.edits },
|
|
707
|
+
},
|
|
708
|
+
start,
|
|
709
|
+
end,
|
|
710
|
+
rowIds: view.grouped ? null : rowIds,
|
|
711
|
+
block: { bytes: encodeBlock(columns, values) },
|
|
712
|
+
...(truncated.size > 0 ? { truncated: Object.fromEntries(truncated) } : {}),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async summarize(request, running) {
|
|
716
|
+
const table = this.table(request);
|
|
717
|
+
const view = await this.view(table, request.plan, running);
|
|
718
|
+
// The cells of ranges × columns: each cell once, however the ranges overlap or a column repeats.
|
|
719
|
+
const sources = new Map(this.columnsOf(table, view, request.columns).map((item) => [item.name, item]));
|
|
720
|
+
const spans = mergeRanges(request.ranges, view.rowCount);
|
|
721
|
+
const rows = new Uint32Array(spans.reduce((total, [start, end]) => total + end - start, 0));
|
|
722
|
+
let at = 0;
|
|
723
|
+
for (const [start, end] of spans) {
|
|
724
|
+
for (let position = start; position < end; position++)
|
|
725
|
+
rows[at++] = view.order ? view.order[position] : position;
|
|
726
|
+
}
|
|
727
|
+
await this.readColumns(table, [...sources.values()].filter((source) => !source.values).map((source) => source.index), running);
|
|
728
|
+
const summary = new Summary();
|
|
729
|
+
for (const { column, index, values } of sources.values()) {
|
|
730
|
+
summary.add(column, values ?? this.valuesOf(table, index), rows);
|
|
731
|
+
}
|
|
732
|
+
return summary.result();
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* The next cell of the view whose text form contains `text` (§8.2): row by row, in a row in the order of
|
|
736
|
+
* `columns`, from the cell after `from` (before it, with −1), wrapping around and ending with `from` itself.
|
|
737
|
+
*/
|
|
738
|
+
async find(request, running) {
|
|
739
|
+
const table = this.table(request);
|
|
740
|
+
const view = await this.view(table, request.plan, running);
|
|
741
|
+
const sources = this.columnsOf(table, view, request.columns);
|
|
742
|
+
const text = request.text;
|
|
743
|
+
if (text === "" || view.rowCount === 0 || sources.length === 0)
|
|
744
|
+
return null;
|
|
745
|
+
const sensitive = request.caseSensitive === true;
|
|
746
|
+
const { mapCase } = await import("./evaluate.js");
|
|
747
|
+
const needle = sensitive ? text : mapCase(text, false);
|
|
748
|
+
await this.readColumns(table, sources.filter((source) => !source.values).map((source) => source.index), running);
|
|
749
|
+
const values = sources.map((source) => source.values ?? this.valuesOf(table, source.index));
|
|
750
|
+
const matches = (position, at) => {
|
|
751
|
+
const value = values[at].at(view.order ? view.order[position] : position);
|
|
752
|
+
if (value === null)
|
|
753
|
+
return false;
|
|
754
|
+
const column = sources[at].column;
|
|
755
|
+
const form = column.type === "string" ? value : textOf(value, column);
|
|
756
|
+
return (sensitive ? form : mapCase(form, false)).includes(needle);
|
|
757
|
+
};
|
|
758
|
+
const count = view.rowCount;
|
|
759
|
+
const width = sources.length;
|
|
760
|
+
const forward = request.direction === 1;
|
|
761
|
+
const from = request.from;
|
|
762
|
+
const row = Math.min(from.row, count - 1);
|
|
763
|
+
const column = Math.min(from.column, width);
|
|
764
|
+
// The cells of the row of `from` after it (before it, with −1).
|
|
765
|
+
if (forward) {
|
|
766
|
+
for (let at = column + 1; at < width; at++)
|
|
767
|
+
if (matches(row, at))
|
|
768
|
+
return { row, column: at };
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
for (let at = column - 1; at >= 0; at--)
|
|
772
|
+
if (matches(row, at))
|
|
773
|
+
return { row, column: at };
|
|
774
|
+
}
|
|
775
|
+
// Then the other rows, in order, wrapping around at the end.
|
|
776
|
+
for (let step = 1; step < count; step++) {
|
|
777
|
+
if (step % 1_024 === 0)
|
|
778
|
+
await pause(running);
|
|
779
|
+
const position = forward ? (row + step) % count : (row - step + count) % count;
|
|
780
|
+
if (forward) {
|
|
781
|
+
for (let at = 0; at < width; at++)
|
|
782
|
+
if (matches(position, at))
|
|
783
|
+
return { row: position, column: at };
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
for (let at = width - 1; at >= 0; at--)
|
|
787
|
+
if (matches(position, at))
|
|
788
|
+
return { row: position, column: at };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// Last, the cells of the row of `from` up to `from` itself.
|
|
792
|
+
if (forward) {
|
|
793
|
+
for (let at = 0; at <= Math.min(column, width - 1); at++)
|
|
794
|
+
if (matches(row, at))
|
|
795
|
+
return { row, column: at };
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
for (let at = width - 1; at >= column; at--)
|
|
799
|
+
if (matches(row, at))
|
|
800
|
+
return { row, column: at };
|
|
801
|
+
}
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* The view's rows as a file (§8.2), answered as its bytes (`delivery` `data`): CSV, TSV and JSON as the DuckDB
|
|
806
|
+
* engine writes them, and xlsx. The engine has no files, so a `target` is not allowed.
|
|
807
|
+
*/
|
|
808
|
+
async export(request, running) {
|
|
809
|
+
const table = this.table(request);
|
|
810
|
+
const format = request.format;
|
|
811
|
+
if (!EXPORT_FORMATS.includes(format)) {
|
|
812
|
+
throw new Failure("invalid_request", `This engine does not export ${format} (§4.10).`, {
|
|
813
|
+
path: "/format",
|
|
814
|
+
reason: "syntax",
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (request.target !== undefined) {
|
|
818
|
+
throw new Failure("not_allowed", "This engine answers exports as data and writes no files: leave out target.");
|
|
819
|
+
}
|
|
820
|
+
const view = await this.view(table, request.plan, running);
|
|
821
|
+
const names = request.columns ?? view.columns.map((column) => column.name);
|
|
822
|
+
const sources = this.columnsOf(table, view, names);
|
|
823
|
+
await this.readColumns(table, sources.filter((source) => !source.values).map((source) => source.index), running);
|
|
824
|
+
const values = sources.map((source) => source.values ?? this.valuesOf(table, source.index));
|
|
825
|
+
const { writeExport } = await import("./export.js");
|
|
826
|
+
const bytes = await writeExport({
|
|
827
|
+
format: format,
|
|
828
|
+
columns: sources.map((source) => source.column),
|
|
829
|
+
headers: request.headers ?? names,
|
|
830
|
+
numberFormats: request.numberFormats ?? null,
|
|
831
|
+
rowCount: view.rowCount,
|
|
832
|
+
row: (position) => {
|
|
833
|
+
const row = view.order ? view.order[position] : position;
|
|
834
|
+
return values.map((vector) => vector.at(row));
|
|
835
|
+
},
|
|
836
|
+
pause: () => pause(running),
|
|
837
|
+
});
|
|
838
|
+
return { target: null, rows: view.rowCount, data: { bytes } };
|
|
839
|
+
}
|
|
840
|
+
async locate(request, running) {
|
|
841
|
+
const table = this.table(request);
|
|
842
|
+
const view = await this.view(table, request.plan, running);
|
|
843
|
+
const keys = request.keys;
|
|
844
|
+
let column;
|
|
845
|
+
if (request.rowKey !== undefined) {
|
|
846
|
+
column = table.index.get(request.rowKey);
|
|
847
|
+
if (column === undefined) {
|
|
848
|
+
throw new Failure("invalid_request", `The table has no column ${shown(request.rowKey)}.`, {
|
|
849
|
+
path: "/rowKey",
|
|
850
|
+
reason: "syntax",
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
else if (keys.some((key) => "value" in key)) {
|
|
855
|
+
throw new Failure("invalid_request", "Keys by value need rowKey, the column of their values (§5.3).", {
|
|
856
|
+
path: "/rowKey",
|
|
857
|
+
reason: "syntax",
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
const name = request.rowKey;
|
|
861
|
+
// In a view of groups, values are looked up among the groups' keys; groups have no row IDs.
|
|
862
|
+
const group = view.grouped ? view.derived.get(name) : undefined;
|
|
863
|
+
if (column !== undefined && !group && keys.some((key) => "value" in key)) {
|
|
864
|
+
await this.readColumns(table, [column], running);
|
|
865
|
+
}
|
|
866
|
+
let ranks;
|
|
867
|
+
return {
|
|
868
|
+
rows: keys.map((key) => {
|
|
869
|
+
if ("rowId" in key)
|
|
870
|
+
return view.grouped ? null : this.positionOf(view, key.rowId);
|
|
871
|
+
if (view.grouped && !group)
|
|
872
|
+
return null;
|
|
873
|
+
// The first position in the view of a row whose value equals the key's (§3.1).
|
|
874
|
+
ranks ??= group ? rankColumn(group.column, vectorOf(group.values)) : this.ranksOf(table, column);
|
|
875
|
+
const rank = ranks.rankOf(key.value);
|
|
876
|
+
if (rank < 0)
|
|
877
|
+
return null;
|
|
878
|
+
const first = this.firstsOf(view, name, ranks)[rank];
|
|
879
|
+
return first < 0 ? null : first;
|
|
880
|
+
}),
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/** The row ID of a row key (§5.3): `{ rowId }`, or `{ value }` of `rowKey`'s column; null when no row has it. */
|
|
884
|
+
rowOf(table, key, column) {
|
|
885
|
+
if ("rowId" in key)
|
|
886
|
+
return Number.isSafeInteger(key.rowId) && key.rowId < table.rowCount ? key.rowId : null;
|
|
887
|
+
if (column === undefined)
|
|
888
|
+
return null;
|
|
889
|
+
let byText = table.keys.get(column);
|
|
890
|
+
if (!byText) {
|
|
891
|
+
const values = this.valuesOf(table, column);
|
|
892
|
+
byText = new Map();
|
|
893
|
+
for (let row = values.length - 1; row >= 0; row--) {
|
|
894
|
+
const value = values.at(row);
|
|
895
|
+
if (value !== null)
|
|
896
|
+
byText.set(keyText({ value }), row);
|
|
897
|
+
}
|
|
898
|
+
table.keys.set(column, byText);
|
|
899
|
+
}
|
|
900
|
+
return byText.get(keyText(key)) ?? null;
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Writes cells (§7.2): every update or none. Values are checked as their column's type, `previous` against the
|
|
904
|
+
* cell's value now, and the written rows replace the host's rows in the engine's own list of rows.
|
|
905
|
+
*/
|
|
906
|
+
async write(request, running) {
|
|
907
|
+
const table = this.table(request);
|
|
908
|
+
const updates = request.updates;
|
|
909
|
+
let keyColumn;
|
|
910
|
+
if (request.rowKey !== undefined) {
|
|
911
|
+
keyColumn = table.index.get(request.rowKey);
|
|
912
|
+
if (keyColumn === undefined) {
|
|
913
|
+
throw new Failure("invalid_request", `The table has no column ${shown(request.rowKey)}.`, {
|
|
914
|
+
path: "/rowKey",
|
|
915
|
+
reason: "syntax",
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (keyColumn !== undefined && updates.some((update) => "value" in update.key)) {
|
|
920
|
+
await this.readColumns(table, [keyColumn], running);
|
|
921
|
+
}
|
|
922
|
+
const width = table.columns.length;
|
|
923
|
+
const changes = [];
|
|
924
|
+
for (const [at, update] of updates.entries()) {
|
|
925
|
+
const index = table.index.get(update.column);
|
|
926
|
+
const column = index === undefined ? undefined : table.columns[index];
|
|
927
|
+
if (index === undefined ||
|
|
928
|
+
!column ||
|
|
929
|
+
!table.editable ||
|
|
930
|
+
column.readOnly ||
|
|
931
|
+
column.type === "interval" ||
|
|
932
|
+
column.type === "binary") {
|
|
933
|
+
throw new Failure("read_only", `The cells of ${shown(update.column)} are not written (§7.1).`, {
|
|
934
|
+
details: { column: update.column },
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const row = this.rowOf(table, update.key, keyColumn);
|
|
938
|
+
if (row === null) {
|
|
939
|
+
throw new Failure("edit_conflict", "No row has this key now.", { details: { update: at } });
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
table.checkers[index](update.value, row);
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (!(error instanceof TableProblem))
|
|
946
|
+
throw error;
|
|
947
|
+
throw new Failure("invalid_value", `${shown(update.value)} is not a value of ${shown(update.column)}.`, {
|
|
948
|
+
path: `/updates/${at}/value`,
|
|
949
|
+
reason: "type",
|
|
950
|
+
details: { update: at },
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
const [now] = await this.cellsOf(table, index, [row]);
|
|
954
|
+
if (update.previous !== undefined && !sameCellValue(now, update.previous, column)) {
|
|
955
|
+
throw new Failure("edit_conflict", "The cell holds another value now.", { details: { update: at } });
|
|
956
|
+
}
|
|
957
|
+
changes.push({ row, index, value: update.value });
|
|
958
|
+
}
|
|
959
|
+
if (changes.length === 0)
|
|
960
|
+
return { updated: 0, edits: { ...table.edits } };
|
|
961
|
+
// The host's rows, Arrow columns and files stay as they were: writes change the engine's own values.
|
|
962
|
+
for (const { row, index, value } of changes) {
|
|
963
|
+
this.setCell(table, row, index, value);
|
|
964
|
+
table.written.add(row * width + index);
|
|
965
|
+
table.ranks[index] = undefined;
|
|
966
|
+
table.keys.delete(index);
|
|
967
|
+
}
|
|
968
|
+
table.views.clear();
|
|
969
|
+
table.edits = { version: table.edits.version + 1, cells: table.written.size };
|
|
970
|
+
for (const [id, open] of this.tables) {
|
|
971
|
+
if (open !== table)
|
|
972
|
+
continue;
|
|
973
|
+
this.emit({ v: 2, event: "changed", table: id, revision: table.revision, edits: { ...table.edits } });
|
|
974
|
+
}
|
|
975
|
+
return { updated: changes.length, edits: { ...table.edits } };
|
|
976
|
+
}
|
|
977
|
+
emit(event) {
|
|
978
|
+
for (const listener of this.listeners)
|
|
979
|
+
listener(event);
|
|
980
|
+
}
|
|
981
|
+
/** The position of a row in a view, or null when the view does not have it. */
|
|
982
|
+
positionOf(view, rowId) {
|
|
983
|
+
if (rowId >= view.tableRows)
|
|
984
|
+
return null;
|
|
985
|
+
if (!view.order)
|
|
986
|
+
return rowId;
|
|
987
|
+
if (!view.positions) {
|
|
988
|
+
// Rows that the view leaves out have no position.
|
|
989
|
+
const positions = new Uint32Array(view.tableRows).fill(NO_POSITION);
|
|
990
|
+
view.order.forEach((row, position) => {
|
|
991
|
+
positions[row] = position;
|
|
992
|
+
});
|
|
993
|
+
view.positions = positions;
|
|
994
|
+
}
|
|
995
|
+
const position = view.positions[rowId];
|
|
996
|
+
return position === NO_POSITION ? null : position;
|
|
997
|
+
}
|
|
998
|
+
/** The first position in a view of each rank of a column, −1 for ranks that no row of the view has. */
|
|
999
|
+
firstsOf(view, column, ranks) {
|
|
1000
|
+
let firsts = view.firsts.get(column);
|
|
1001
|
+
if (!firsts) {
|
|
1002
|
+
firsts = new Int32Array(ranks.distinct + 2).fill(-1);
|
|
1003
|
+
for (let position = 0; position < view.rowCount; position++) {
|
|
1004
|
+
const rank = ranks.ranks[view.order ? view.order[position] : position];
|
|
1005
|
+
if (firsts[rank] < 0)
|
|
1006
|
+
firsts[rank] = position;
|
|
1007
|
+
}
|
|
1008
|
+
view.firsts.set(column, firsts);
|
|
1009
|
+
}
|
|
1010
|
+
return firsts;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* An engine over tables in memory. The host gives the tables by the `ref` that `open` takes; the engine checks
|
|
1015
|
+
* the form of a table against §2 when it is first opened, and its values as it reads them (a column whole before
|
|
1016
|
+
* it sorts or sums it), answering `load_failed` with the JSON Pointer of a value that does not follow §2.2. It
|
|
1017
|
+
* reads the rows as they are, without copying them, so they must not change afterwards.
|
|
1018
|
+
*/
|
|
1019
|
+
export function memoryEngine(options) {
|
|
1020
|
+
return new Engine(options);
|
|
1021
|
+
}
|
|
1022
|
+
//# sourceMappingURL=engine.js.map
|