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/src/plan.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Views of plans that derive, filter or group (docs/spec.md §4.1): derive, where, aggregate, having, then
|
|
3
|
+
* sort. The engine loads this module with the first such plan, so that pages whose plans only sort do not load
|
|
4
|
+
* it, and gives it the functions of order that those pages load anyway.
|
|
5
|
+
*/
|
|
6
|
+
import type { CheckedPlan, Column, JsonValue } from "vastgrid-spec";
|
|
7
|
+
import { groupRows } from "./aggregate.js";
|
|
8
|
+
import { type ColumnReader, evaluate, filter } from "./evaluate.js";
|
|
9
|
+
import type { ColumnRanks, RankedKey } from "./order.js";
|
|
10
|
+
import { type Vector, vectorOf } from "./table.js";
|
|
11
|
+
|
|
12
|
+
/** A column that a view computes: a derived column by row ID, or a column of groups by group number. */
|
|
13
|
+
export interface ViewColumn {
|
|
14
|
+
column: Column;
|
|
15
|
+
values: JsonValue[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What a view reads of its table, and the engine's functions of order (§3.3). */
|
|
19
|
+
export interface ViewContext {
|
|
20
|
+
/** The rows of the table. */
|
|
21
|
+
count: number;
|
|
22
|
+
/** A column of the table: its values by row ID, and its ranks. */
|
|
23
|
+
values(name: string): Vector;
|
|
24
|
+
ranks(name: string): ColumnRanks;
|
|
25
|
+
rankColumn(column: Column, values: Vector): ColumnRanks;
|
|
26
|
+
sortRows(count: number, keys: readonly RankedKey[], rows: Uint32Array | null): Uint32Array | null;
|
|
27
|
+
/** The plan's sort keys, by the ranks of the view's columns. */
|
|
28
|
+
sortKeys(ranks: (name: string) => ColumnRanks): RankedKey[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A view of a plan: its columns, the columns it computes, and its rows (row IDs or group numbers) in order. */
|
|
32
|
+
export interface ViewShape {
|
|
33
|
+
columns: Column[];
|
|
34
|
+
derived: Map<string, ViewColumn>;
|
|
35
|
+
grouped: boolean;
|
|
36
|
+
/** The rows of the table, or the groups. */
|
|
37
|
+
tableRows: number;
|
|
38
|
+
rowCount: number;
|
|
39
|
+
order: Uint32Array | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function planView(checked: CheckedPlan, context: ViewContext): ViewShape {
|
|
43
|
+
const { count } = context;
|
|
44
|
+
// Expressions read the table's columns and the derived ones before them.
|
|
45
|
+
const derived = new Map<string, ViewColumn>();
|
|
46
|
+
const valuesOf = (name: string): Vector => {
|
|
47
|
+
const made = derived.get(name);
|
|
48
|
+
return made ? vectorOf(made.values) : context.values(name);
|
|
49
|
+
};
|
|
50
|
+
const read: ColumnReader = (name) => {
|
|
51
|
+
const values = valuesOf(name);
|
|
52
|
+
return (row) => values.at(row);
|
|
53
|
+
};
|
|
54
|
+
for (const item of checked.derive) {
|
|
55
|
+
derived.set(item.name, { column: item.column, values: evaluate(item.expr, read, count) });
|
|
56
|
+
}
|
|
57
|
+
const filtered = checked.where ? filter(checked.where, read, count) : null;
|
|
58
|
+
const ranksOf = (columns: Map<string, ViewColumn>, name: string) => {
|
|
59
|
+
const made = columns.get(name);
|
|
60
|
+
return made ? context.rankColumn(made.column, vectorOf(made.values)) : context.ranks(name);
|
|
61
|
+
};
|
|
62
|
+
if (!checked.aggregate) {
|
|
63
|
+
return {
|
|
64
|
+
columns: checked.columns,
|
|
65
|
+
derived,
|
|
66
|
+
grouped: false,
|
|
67
|
+
tableRows: count,
|
|
68
|
+
rowCount: filtered ? filtered.length : count,
|
|
69
|
+
order: context.sortRows(
|
|
70
|
+
count,
|
|
71
|
+
context.sortKeys((name) => ranksOf(derived, name)),
|
|
72
|
+
filtered,
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const groups = groupRows(
|
|
77
|
+
{
|
|
78
|
+
aggregate: checked.aggregate,
|
|
79
|
+
columns: checked.columns,
|
|
80
|
+
rows: filtered,
|
|
81
|
+
count,
|
|
82
|
+
values: valuesOf,
|
|
83
|
+
ranks: (name) => ranksOf(derived, name),
|
|
84
|
+
},
|
|
85
|
+
checked.having,
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
columns: groups.columns,
|
|
89
|
+
derived: groups.values,
|
|
90
|
+
grouped: true,
|
|
91
|
+
tableRows: groups.count,
|
|
92
|
+
rowCount: groups.kept ? groups.kept.length : groups.count,
|
|
93
|
+
order: context.sortRows(
|
|
94
|
+
groups.count,
|
|
95
|
+
context.sortKeys((name) => ranksOf(groups.values, name)),
|
|
96
|
+
groups.kept,
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/summary.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The summary of cells that `summarize` answers (docs/spec.md §8.2): the cells that are not blank, and the
|
|
3
|
+
* sum, minimum, maximum and average of the finite numbers among them, as floats.
|
|
4
|
+
*
|
|
5
|
+
* Exact numbers (integers, bigints, decimals) are summed exactly and rounded once; floats are summed with
|
|
6
|
+
* compensation (Neumaier), and also scaled down by 2^64 so that an average stays finite when the sum is
|
|
7
|
+
* beyond the largest float.
|
|
8
|
+
*/
|
|
9
|
+
import { type Column, floatJson, floatOf, type JsonValue } from "vastgrid-spec";
|
|
10
|
+
import { decimalText } from "./evaluate.js";
|
|
11
|
+
import type { Vector } from "./table.js";
|
|
12
|
+
|
|
13
|
+
/** What `summarize` answers (§8.2). */
|
|
14
|
+
export interface SummaryResult {
|
|
15
|
+
count: number;
|
|
16
|
+
numericCount: number;
|
|
17
|
+
sum: JsonValue;
|
|
18
|
+
min: JsonValue;
|
|
19
|
+
max: JsonValue;
|
|
20
|
+
average: JsonValue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SCALE_DOWN = 2 ** -64;
|
|
24
|
+
const SCALE_UP = 2 ** 64;
|
|
25
|
+
|
|
26
|
+
/** A sum of floats with Neumaier's compensation. */
|
|
27
|
+
class FloatSum {
|
|
28
|
+
private sum = 0;
|
|
29
|
+
private compensation = 0;
|
|
30
|
+
private overflow = false;
|
|
31
|
+
|
|
32
|
+
add(value: number) {
|
|
33
|
+
const total = this.sum + value;
|
|
34
|
+
if (!Number.isFinite(total)) this.overflow = true;
|
|
35
|
+
else if (Math.abs(this.sum) >= Math.abs(value)) this.compensation += this.sum - total + value;
|
|
36
|
+
else this.compensation += value - total + this.sum;
|
|
37
|
+
this.sum = total;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The sum, or null when it went beyond the largest float on the way. */
|
|
41
|
+
value(): number | null {
|
|
42
|
+
return this.overflow ? null : this.sum + this.compensation;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A sum of exact numbers at one scale: safe integers while they stay safe, then a bigint. */
|
|
47
|
+
class ExactSum {
|
|
48
|
+
private small = 0;
|
|
49
|
+
private big = 0n;
|
|
50
|
+
|
|
51
|
+
add(value: number) {
|
|
52
|
+
if (Math.abs(this.small) + Math.abs(value) <= Number.MAX_SAFE_INTEGER) this.small += value;
|
|
53
|
+
else {
|
|
54
|
+
this.big += BigInt(this.small);
|
|
55
|
+
this.small = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
addBig(value: bigint) {
|
|
60
|
+
this.big += value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
total(): bigint {
|
|
64
|
+
return this.big + BigInt(this.small);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Counts and sums the values of cells (§8.2). */
|
|
69
|
+
export class Summary {
|
|
70
|
+
private count = 0;
|
|
71
|
+
private numericCount = 0;
|
|
72
|
+
private min = Number.POSITIVE_INFINITY;
|
|
73
|
+
private max = Number.NEGATIVE_INFINITY;
|
|
74
|
+
/** Exact sums by scale. */
|
|
75
|
+
private readonly exact = new Map<number, ExactSum>();
|
|
76
|
+
private readonly floats = new FloatSum();
|
|
77
|
+
private readonly scaled = new FloatSum();
|
|
78
|
+
|
|
79
|
+
private exactAt(scale: number): ExactSum {
|
|
80
|
+
let sum = this.exact.get(scale);
|
|
81
|
+
if (!sum) {
|
|
82
|
+
sum = new ExactSum();
|
|
83
|
+
this.exact.set(scale, sum);
|
|
84
|
+
}
|
|
85
|
+
return sum;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private number(value: number) {
|
|
89
|
+
this.numericCount++;
|
|
90
|
+
if (value < this.min) this.min = value;
|
|
91
|
+
if (value > this.max) this.max = value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Adds the values of one column, by row ID, at the rows of `rows` (row IDs). */
|
|
95
|
+
add(column: Column, values: Vector, rows: Uint32Array) {
|
|
96
|
+
switch (column.type) {
|
|
97
|
+
case "string":
|
|
98
|
+
// The empty string is blank (§3.2).
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
const value = values.at(row);
|
|
101
|
+
if (value !== null && value !== "") this.count++;
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
case "integer":
|
|
105
|
+
case "bigint": {
|
|
106
|
+
const sum = this.exactAt(0);
|
|
107
|
+
for (const row of rows) {
|
|
108
|
+
const value = values.at(row);
|
|
109
|
+
if (value === null) continue;
|
|
110
|
+
this.count++;
|
|
111
|
+
if (typeof value === "number") {
|
|
112
|
+
sum.add(value);
|
|
113
|
+
this.number(value);
|
|
114
|
+
} else {
|
|
115
|
+
sum.addBig(BigInt(value as string));
|
|
116
|
+
this.number(Number(value));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case "decimal": {
|
|
122
|
+
const scale = column.scale ?? 0;
|
|
123
|
+
const sum = this.exactAt(scale);
|
|
124
|
+
for (const row of rows) {
|
|
125
|
+
const value = values.at(row);
|
|
126
|
+
if (value === null) continue;
|
|
127
|
+
this.count++;
|
|
128
|
+
const digits = (value as string).replace(".", "");
|
|
129
|
+
if (digits.length <= 15) sum.add(Number(digits));
|
|
130
|
+
else sum.addBig(BigInt(digits));
|
|
131
|
+
this.number(Number(value));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
case "float": {
|
|
136
|
+
const single = column.bits === 32;
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
const value = values.at(row);
|
|
139
|
+
if (value === null) continue;
|
|
140
|
+
this.count++;
|
|
141
|
+
const number = single ? Math.fround(floatOf(value)) : floatOf(value);
|
|
142
|
+
// NaN and the infinities count as cells but not as numbers.
|
|
143
|
+
if (!Number.isFinite(number)) continue;
|
|
144
|
+
this.floats.add(number);
|
|
145
|
+
this.scaled.add(number * SCALE_DOWN);
|
|
146
|
+
this.number(number);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
for (const row of rows) if (values.at(row) !== null) this.count++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The summary (§8.2): floats as §2.2 writes them, a sum beyond the largest float as `"Infinity"`. */
|
|
156
|
+
result(): SummaryResult {
|
|
157
|
+
const { count, numericCount } = this;
|
|
158
|
+
if (numericCount === 0) return { count, numericCount, sum: null, min: null, max: null, average: null };
|
|
159
|
+
// The exact sums at the finest of their scales, rounded once.
|
|
160
|
+
const scale = Math.max(...this.exact.keys(), 0);
|
|
161
|
+
let unscaled = 0n;
|
|
162
|
+
for (const [at, sum] of this.exact) unscaled += sum.total() * 10n ** BigInt(scale - at);
|
|
163
|
+
const exact = Number(decimalText(unscaled, scale));
|
|
164
|
+
const floats = this.floats.value();
|
|
165
|
+
const scaled = this.scaled.value() ?? 0;
|
|
166
|
+
const sum = exact + (floats ?? scaled * SCALE_UP);
|
|
167
|
+
const average = Number.isFinite(sum)
|
|
168
|
+
? sum / numericCount
|
|
169
|
+
: ((exact * SCALE_DOWN + scaled) / numericCount) * SCALE_UP;
|
|
170
|
+
// −0.0 equals 0.0 (§3.1); the answer writes 0.
|
|
171
|
+
return {
|
|
172
|
+
count,
|
|
173
|
+
numericCount,
|
|
174
|
+
sum: floatJson(sum + 0),
|
|
175
|
+
min: this.min + 0,
|
|
176
|
+
max: this.max + 0,
|
|
177
|
+
average: floatJson(average + 0),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
package/src/table.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tables in memory: what a host gives the engine, and the checks that make them tables of docs/spec.md
|
|
3
|
+
* §2: columns of §2.1 with unique names, and rows of typed JSON values (§2.2).
|
|
4
|
+
*/
|
|
5
|
+
import { type Column, isValueOf, type JsonValue, LOGICAL_TYPES, typeName } from "vastgrid-spec";
|
|
6
|
+
import type { ColumnNumbers } from "vastgrid-spec/columns";
|
|
7
|
+
import type { SheetInfo } from "vastgrid-spec/workbook";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A column's values by row ID as typed JSON (§2.2): an array of them, or a column of an Arrow table
|
|
11
|
+
* (`ColumnValues` of vastgrid-spec), whose numbers a sort reads without making JSON.
|
|
12
|
+
*/
|
|
13
|
+
export interface Vector {
|
|
14
|
+
readonly length: number;
|
|
15
|
+
at(row: number): JsonValue;
|
|
16
|
+
readonly numbers?: ColumnNumbers | null;
|
|
17
|
+
readonly validity?: Uint8Array | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** An array of typed JSON as a vector, which arrays are: they have `length` and `at`. */
|
|
21
|
+
export function vectorOf(values: readonly JsonValue[]): Vector {
|
|
22
|
+
return values as unknown as Vector;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A table in memory: its columns (§2.1), and its rows of typed JSON values (§2.2) in row ID order. */
|
|
26
|
+
export interface MemoryTable {
|
|
27
|
+
columns: Column[];
|
|
28
|
+
rows: JsonValue[][];
|
|
29
|
+
/**
|
|
30
|
+
* Whether `write` writes the table's cells (§7). Written values are kept apart from `rows`, which stay as the
|
|
31
|
+
* host gave them, and every later request sees them.
|
|
32
|
+
*/
|
|
33
|
+
editable?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A table as Apache Arrow (§2.3): the bytes of an IPC stream or file, of any number of record batches. Its
|
|
38
|
+
* columns have the logical types of their Arrow types, and are kept as Arrow holds them: values of fixed width
|
|
39
|
+
* as typed arrays, which sorts read as they are, and the others read when they are asked for.
|
|
40
|
+
*/
|
|
41
|
+
export interface ArrowTable {
|
|
42
|
+
arrow: Uint8Array;
|
|
43
|
+
/** Whether `write` writes the table's cells (§7), as for a `MemoryTable`. */
|
|
44
|
+
editable?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What a text file is read from: a `Blob` or a `File`, or anything that gives its bytes in slices as they do. */
|
|
48
|
+
export interface TextFile {
|
|
49
|
+
readonly size: number;
|
|
50
|
+
/** The file's name, whose extension tells its format when the table does not. */
|
|
51
|
+
readonly name?: string;
|
|
52
|
+
slice(start?: number, end?: number): { arrayBuffer(): Promise<ArrayBuffer> };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The formats of text files: CSV, TSV and JSON Lines; and `xlsx`, of Excel workbooks. */
|
|
56
|
+
export type TextFormat = "csv" | "tsv" | "jsonl" | "xlsx";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A table as a text file: CSV, TSV or JSON Lines in UTF-8, read whole into typed columns when it is first opened.
|
|
60
|
+
* Each column has the first of the types boolean, integer, bigint, float, time, date, timestamp and timestamptz
|
|
61
|
+
* that every value of it fits, and otherwise is a string. Or a sheet of an Excel workbook (`.xlsx`, `.xlsm`), which
|
|
62
|
+
* `open` chooses with `read` (§8.2): its columns have the types that the DuckDB engine reads them as.
|
|
63
|
+
*/
|
|
64
|
+
export interface FileTable {
|
|
65
|
+
/** The file, a `Blob` or a `File`: in a Web Worker or the page, read in slices without a copy of it whole. */
|
|
66
|
+
file: TextFile;
|
|
67
|
+
/**
|
|
68
|
+
* `csv`, `tsv`, `jsonl` or `xlsx`; by default from the file's name (`.csv`, `.tsv` or `.tab`, `.jsonl` or
|
|
69
|
+
* `.ndjson`, `.xlsx` or `.xlsm`).
|
|
70
|
+
*/
|
|
71
|
+
format?: TextFormat;
|
|
72
|
+
/**
|
|
73
|
+
* The field separator of a CSV file, one character; by default the one of `,`, tab, `;` and `|` that splits its
|
|
74
|
+
* first line into the most fields.
|
|
75
|
+
*/
|
|
76
|
+
delimiter?: string;
|
|
77
|
+
/** Whether the first line of a CSV or TSV file names the columns (default true). */
|
|
78
|
+
header?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The most memory, in bytes, that the table's columns may take (default 1 GiB). A file whose columns would take
|
|
81
|
+
* more is not kept: its rows are read by blocks, and its columns whole as requests need them, within this.
|
|
82
|
+
*/
|
|
83
|
+
maxBytes?: number;
|
|
84
|
+
/** Whether `write` writes the table's cells (§7), as for a `MemoryTable`. */
|
|
85
|
+
editable?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** An answer of `fetch`, as the page, Web Workers and Node.js give it. */
|
|
89
|
+
export interface HttpResponse {
|
|
90
|
+
readonly status: number;
|
|
91
|
+
readonly ok: boolean;
|
|
92
|
+
readonly headers: { get(name: string): string | null };
|
|
93
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `fetch`, or a function that answers as it does. */
|
|
97
|
+
export type HttpFetch = (
|
|
98
|
+
url: string,
|
|
99
|
+
init: { method?: string; headers: Record<string, string> },
|
|
100
|
+
) => Promise<HttpResponse>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A table as a Parquet file, read by byte ranges: its footer when it is opened, then the row groups of the rows that
|
|
104
|
+
* requests read, and whole only the columns that a view sorts, filters or groups by. Columns have the types that
|
|
105
|
+
* DuckDB reads them as.
|
|
106
|
+
*/
|
|
107
|
+
export interface ParquetTable {
|
|
108
|
+
/**
|
|
109
|
+
* The file: a `Blob` or a `File`, or the URL of a file whose server answers range requests (HTTP `Range`), with
|
|
110
|
+
* CORS that exposes `Content-Length`, `Content-Range` and `ETag` on another origin.
|
|
111
|
+
*/
|
|
112
|
+
parquet: TextFile | string;
|
|
113
|
+
/** Headers of every request for a URL, such as `Authorization`. */
|
|
114
|
+
headers?: Record<string, string>;
|
|
115
|
+
/** A `fetch` to use instead of the global one. */
|
|
116
|
+
fetch?: HttpFetch;
|
|
117
|
+
/**
|
|
118
|
+
* About the most memory, in bytes, that the columns read whole may take (default 1 GiB). Those that no request
|
|
119
|
+
* reads are forgotten when another needs their room; a request that would read more at once fails with
|
|
120
|
+
* `load_failed`.
|
|
121
|
+
*/
|
|
122
|
+
maxBytes?: number;
|
|
123
|
+
/** Whether `write` writes the table's cells (§7), as for a `MemoryTable`. */
|
|
124
|
+
editable?: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A table that does not follow §2, or a file that cannot be read, which the engine answers with `load_failed`
|
|
129
|
+
* (§8.7); a file that changed while it was open with `file_changed`.
|
|
130
|
+
*/
|
|
131
|
+
export class TableProblem extends Error {
|
|
132
|
+
constructor(
|
|
133
|
+
readonly path: string,
|
|
134
|
+
message: string,
|
|
135
|
+
readonly code: "load_failed" | "file_changed" = "load_failed",
|
|
136
|
+
) {
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = "TableProblem";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const COLUMN_MEMBERS = ["name", "type", "precision", "scale", "bits", "unit", "sourceType", "readOnly"];
|
|
143
|
+
const UNITS = ["s", "ms", "us", "ns"];
|
|
144
|
+
|
|
145
|
+
export function isObject(value: unknown): value is Record<string, unknown> {
|
|
146
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isInteger(value: unknown, min: number, max: number): value is number {
|
|
150
|
+
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A value as a problem shows it: its JSON, cut to a length that reads. */
|
|
154
|
+
export function shown(value: unknown): string {
|
|
155
|
+
const text = value === undefined ? "undefined" : (JSON.stringify(value) ?? String(value));
|
|
156
|
+
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Checks a column of a table (§2.1), and gives a copy of it. */
|
|
160
|
+
function checkColumn(value: unknown, path: string): Column {
|
|
161
|
+
if (!isObject(value)) throw new TableProblem(path, "A column is an object: { name, type, … } (§2.1).");
|
|
162
|
+
for (const key of Object.keys(value)) {
|
|
163
|
+
if (!COLUMN_MEMBERS.includes(key))
|
|
164
|
+
throw new TableProblem(`${path}/${key}`, `"${key}" is not a member of a column (§2.1).`);
|
|
165
|
+
}
|
|
166
|
+
const { name, type } = value;
|
|
167
|
+
if (typeof name !== "string") throw new TableProblem(`${path}/name`, "A column's name is a string.");
|
|
168
|
+
if (typeof type !== "string" || !(LOGICAL_TYPES as readonly string[]).includes(type)) {
|
|
169
|
+
throw new TableProblem(`${path}/type`, `${shown(type)} is not a type of §2.1.`);
|
|
170
|
+
}
|
|
171
|
+
if (type === "decimal") {
|
|
172
|
+
if (!isInteger(value.precision, 1, 38))
|
|
173
|
+
throw new TableProblem(`${path}/precision`, "A decimal's precision is from 1 to 38.");
|
|
174
|
+
if (!isInteger(value.scale, 0, value.precision))
|
|
175
|
+
throw new TableProblem(`${path}/scale`, "A decimal's scale is from 0 to its precision.");
|
|
176
|
+
} else {
|
|
177
|
+
for (const member of ["precision", "scale"]) {
|
|
178
|
+
if (value[member] !== undefined)
|
|
179
|
+
throw new TableProblem(`${path}/${member}`, "Only decimals have a precision and a scale.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (value.bits !== undefined && (type !== "float" || (value.bits !== 32 && value.bits !== 64))) {
|
|
183
|
+
throw new TableProblem(`${path}/bits`, "bits is 32 or 64, for floats.");
|
|
184
|
+
}
|
|
185
|
+
if (value.unit !== undefined && (type !== "timestamp" || !UNITS.includes(value.unit as string))) {
|
|
186
|
+
throw new TableProblem(`${path}/unit`, "unit is s, ms, us or ns, for timestamps.");
|
|
187
|
+
}
|
|
188
|
+
if (value.sourceType !== undefined && typeof value.sourceType !== "string") {
|
|
189
|
+
throw new TableProblem(`${path}/sourceType`, "sourceType is a string.");
|
|
190
|
+
}
|
|
191
|
+
if (value.readOnly !== undefined && typeof value.readOnly !== "boolean") {
|
|
192
|
+
throw new TableProblem(`${path}/readOnly`, "readOnly is a boolean.");
|
|
193
|
+
}
|
|
194
|
+
return { ...value } as unknown as Column;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A table that the engine reads: its columns, and its host's rows or its Arrow columns. */
|
|
198
|
+
export interface CheckedTable {
|
|
199
|
+
columns: Column[];
|
|
200
|
+
rows: readonly (readonly JsonValue[])[] | null;
|
|
201
|
+
vectors: Vector[] | null;
|
|
202
|
+
rowCount: number;
|
|
203
|
+
/** The file of a table whose columns are read when requests need them: a Parquet file, or a large text file. */
|
|
204
|
+
reader?: import("./lazy.js").FileColumns;
|
|
205
|
+
/** The sheets of a workbook, the one read, and what its reading changed (§8.2). */
|
|
206
|
+
workbook?: { sheets: SheetInfo[]; sheet: string; notes: string[] };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Throws a `TableProblem` at `path(index)` for the first column whose name an earlier column has. */
|
|
210
|
+
export function checkNames(columns: readonly Column[], path: (index: number) => string) {
|
|
211
|
+
const names = new Set<string>();
|
|
212
|
+
columns.forEach((column, index) => {
|
|
213
|
+
if (names.has(column.name)) throw new TableProblem(path(index), `"${column.name}" names two columns.`);
|
|
214
|
+
names.add(column.name);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Whether a host's table is given as Arrow (`ArrowTable`), which `checkArrow` of arrow.ts checks. */
|
|
219
|
+
export function isArrowTable(table: unknown): table is ArrowTable {
|
|
220
|
+
return isObject(table) && "arrow" in table;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Whether a host's table is given as a Parquet file (`ParquetTable`), which parquet.ts reads. */
|
|
224
|
+
export function isParquetTable(table: unknown): table is ParquetTable {
|
|
225
|
+
return isObject(table) && "parquet" in table;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Whether a host's table is given as a text file (`FileTable`), which `readFile` of text.ts reads. */
|
|
229
|
+
export function isFileTable(table: unknown): table is FileTable {
|
|
230
|
+
return isObject(table) && "file" in table;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Whether a table given as a file is an Excel workbook, which workbook.ts reads. */
|
|
234
|
+
export function isWorkbook(table: FileTable): boolean {
|
|
235
|
+
if (table.format !== undefined) return table.format === "xlsx";
|
|
236
|
+
const name = isObject(table.file) ? table.file.name : undefined;
|
|
237
|
+
return typeof name === "string" && /\.xls[xm]$/i.test(name);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Checks the form of a table given as rows against §2: its columns, with unique names, and rows of one value per
|
|
242
|
+
* column. Gives copies of the columns. Throws a `TableProblem` with the JSON Pointer of the first member at fault.
|
|
243
|
+
* The values are checked as they are read (`valueChecker`), so that opening a table of millions of rows costs
|
|
244
|
+
* nothing per value.
|
|
245
|
+
*/
|
|
246
|
+
export function checkTable(table: unknown): CheckedTable {
|
|
247
|
+
if (!isObject(table))
|
|
248
|
+
throw new TableProblem("", "A table is an object: { columns, rows }, { arrow }, { file } or { parquet }.");
|
|
249
|
+
if (!Array.isArray(table.columns)) throw new TableProblem("/columns", "columns is a list of columns (§2.1).");
|
|
250
|
+
const columns = table.columns.map((column, index) => checkColumn(column, `/columns/${index}`));
|
|
251
|
+
checkNames(columns, (index) => `/columns/${index}/name`);
|
|
252
|
+
const rows = table.rows;
|
|
253
|
+
if (!Array.isArray(rows)) throw new TableProblem("/rows", "rows is a list of rows.");
|
|
254
|
+
for (let row = 0; row < rows.length; row++) {
|
|
255
|
+
const values = rows[row];
|
|
256
|
+
if (!Array.isArray(values) || values.length !== columns.length) {
|
|
257
|
+
throw new TableProblem(`/rows/${row}`, `A row is a list of ${columns.length} values, one per column.`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { columns, rows, vectors: null, rowCount: rows.length };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Checks the values of a column of a table (§2.2): a function of a value and its row that throws a
|
|
265
|
+
* `TableProblem` at `/rows/{row}/{index}` for a value that is not of the column's type.
|
|
266
|
+
*/
|
|
267
|
+
export function valueChecker(column: Column, index: number): (value: unknown, row: number) => void {
|
|
268
|
+
return (value, row) => {
|
|
269
|
+
if (
|
|
270
|
+
value !== null &&
|
|
271
|
+
!(
|
|
272
|
+
(typeof value === "string" || typeof value === "number" || typeof value === "boolean") &&
|
|
273
|
+
isValueOf(value, column)
|
|
274
|
+
)
|
|
275
|
+
) {
|
|
276
|
+
throw new TableProblem(
|
|
277
|
+
`/rows/${row}/${index}`,
|
|
278
|
+
`${shown(value)} is not a ${typeName(column)} value of column "${column.name}" (§2.2).`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The value of a column of an Arrow table at a row. A value that has no typed JSON, such as text that is not
|
|
286
|
+
* UTF-8 or a time beyond a day, throws a `TableProblem` at `/rows/{row}/{index}`.
|
|
287
|
+
*/
|
|
288
|
+
export function vectorValue(vector: Vector, column: Column, index: number, row: number): JsonValue {
|
|
289
|
+
try {
|
|
290
|
+
return vector.at(row);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new TableProblem(
|
|
293
|
+
`/rows/${row}/${index}`,
|
|
294
|
+
`the value is not a ${typeName(column)} value of column "${column.name}" (§2.2): ${(error as Error).message}`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|