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