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/text.js
ADDED
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tables given as text files (CSV, TSV and JSON Lines in UTF-8), read by a module of their own that the engine
|
|
3
|
+
* loads with the first such table, so that pages whose tables are rows do not load it. A file is read in slices,
|
|
4
|
+
* first to find its columns, the types that all their values fit, the memory they take and where every 1,024th row
|
|
5
|
+
* starts. When its columns fit `maxBytes`, it is read again to keep its values in typed columns, numbers in typed
|
|
6
|
+
* arrays and text as UTF-8, without making rows of JSON; otherwise its rows are read by blocks of 1,024 rows as
|
|
7
|
+
* requests need them, and columns whole only when a request goes through every row of them (lazy.ts).
|
|
8
|
+
*/
|
|
9
|
+
import { dateText, daysFromCivil, floatJson, timestampText, timeText, } from "vastgrid-spec";
|
|
10
|
+
import { FileColumns } from "./lazy.js";
|
|
11
|
+
import { HIGH, LOW, TWO_32 } from "./order.js";
|
|
12
|
+
import { shown, TableProblem, } from "./table.js";
|
|
13
|
+
/** Bytes read at a time. */
|
|
14
|
+
const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
/** Rows of a block, the part of a file that rows are read by when its columns are not kept. */
|
|
16
|
+
const BLOCK_ROWS = 1024;
|
|
17
|
+
/** The bytes of the blocks read for rows that are kept, the most recently used. */
|
|
18
|
+
const CACHE_BYTES = 32 * 1024 * 1024;
|
|
19
|
+
/** The memory that a table's columns may take by default (`FileTable.maxBytes`). */
|
|
20
|
+
const MAX_BYTES = 2 ** 30;
|
|
21
|
+
const TAB = 0x09;
|
|
22
|
+
const LF = 0x0a;
|
|
23
|
+
const CR = 0x0d;
|
|
24
|
+
const QUOTE = 0x22;
|
|
25
|
+
const PLUS = 0x2b;
|
|
26
|
+
const MINUS = 0x2d;
|
|
27
|
+
const DOT = 0x2e;
|
|
28
|
+
const ZERO = 0x30;
|
|
29
|
+
const NINE = 0x39;
|
|
30
|
+
const COLON = 0x3a;
|
|
31
|
+
/** `e`, which a byte ORed with 0x20 is when it is `e` or `E`. */
|
|
32
|
+
const LOWER_E = 0x65;
|
|
33
|
+
/** The types that a value fits, as bits. */
|
|
34
|
+
const BOOLEAN = 1;
|
|
35
|
+
const INTEGER = 2;
|
|
36
|
+
const BIGINT = 4;
|
|
37
|
+
const FLOAT = 8;
|
|
38
|
+
const TIME = 16;
|
|
39
|
+
const DATE = 32;
|
|
40
|
+
const TIMESTAMP = 64;
|
|
41
|
+
const TIMESTAMPTZ = 128;
|
|
42
|
+
const TEMPORAL = TIME | DATE | TIMESTAMP | TIMESTAMPTZ;
|
|
43
|
+
const EVERY = 255;
|
|
44
|
+
/** The types of a column in the order they are tried: the first that every value of the column fits. */
|
|
45
|
+
const TYPES = [
|
|
46
|
+
[BOOLEAN, "boolean"],
|
|
47
|
+
[INTEGER, "integer"],
|
|
48
|
+
[BIGINT, "bigint"],
|
|
49
|
+
[FLOAT, "float"],
|
|
50
|
+
[TIME, "time"],
|
|
51
|
+
[DATE, "date"],
|
|
52
|
+
[TIMESTAMP, "timestamp"],
|
|
53
|
+
[TIMESTAMPTZ, "timestamptz"],
|
|
54
|
+
];
|
|
55
|
+
const FORMATS = new Map([
|
|
56
|
+
["csv", "csv"],
|
|
57
|
+
["tsv", "tsv"],
|
|
58
|
+
["tab", "tsv"],
|
|
59
|
+
["jsonl", "jsonl"],
|
|
60
|
+
["ndjson", "jsonl"],
|
|
61
|
+
]);
|
|
62
|
+
/** The words of floats that are not numbers, as DuckDB reads them. */
|
|
63
|
+
const FLOAT_WORDS = new Map(["", "+", "-"].flatMap((sign) => [
|
|
64
|
+
[`${sign}nan`, Number.NaN],
|
|
65
|
+
[`${sign}inf`, sign === "-" ? -Infinity : Infinity],
|
|
66
|
+
[`${sign}infinity`, sign === "-" ? -Infinity : Infinity],
|
|
67
|
+
]));
|
|
68
|
+
const INT64_MIN = -(2n ** 63n);
|
|
69
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
70
|
+
const DAY_MICROS = 86_400_000_000;
|
|
71
|
+
const FIRST_DAY = daysFromCivil(1, 1, 1);
|
|
72
|
+
const LAST_DAY = daysFromCivil(9999, 12, 31);
|
|
73
|
+
/** Text of fields and lines, which the first reading has checked as UTF-8; it keeps a byte order mark. */
|
|
74
|
+
const decoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true });
|
|
75
|
+
const encoder = new TextEncoder();
|
|
76
|
+
function found(name) {
|
|
77
|
+
return { name, fits: EVERY, values: 0, empty: 0, bytes: 0, nested: 0 };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reads a table given as a text file into typed columns. Throws a `TableProblem` at the member of the table at
|
|
81
|
+
* fault, or at `/rows/{row}/{column}` (`/rows/{row}` for a line of JSON Lines) for text that is not UTF-8 or a
|
|
82
|
+
* line that is not JSON. `fields` gives the column of each field of a CSV file, whose values fit its type, or null
|
|
83
|
+
* for a field that is left out (workbook.ts); by default each field is a column of the type its values fit.
|
|
84
|
+
*/
|
|
85
|
+
export async function readFile(table, chunkBytes = CHUNK_BYTES, fields) {
|
|
86
|
+
const { file } = table;
|
|
87
|
+
if (file === null ||
|
|
88
|
+
typeof file !== "object" ||
|
|
89
|
+
typeof file.size !== "number" ||
|
|
90
|
+
typeof file.slice !== "function") {
|
|
91
|
+
throw new TableProblem("/file", "file is a Blob or a File.");
|
|
92
|
+
}
|
|
93
|
+
const format = table.format ?? FORMATS.get(/\.([^.]+)$/.exec(String(file.name ?? ""))?.[1].toLowerCase() ?? "");
|
|
94
|
+
if (format !== "csv" && format !== "tsv" && format !== "jsonl") {
|
|
95
|
+
throw new TableProblem("/format", table.format === undefined
|
|
96
|
+
? `The file's name (${shown(file.name ?? null)}) does not tell its format: give format, csv, tsv, jsonl or xlsx.`
|
|
97
|
+
: "format is csv, tsv, jsonl or xlsx.");
|
|
98
|
+
}
|
|
99
|
+
const { delimiter, header = true, maxBytes = MAX_BYTES } = table;
|
|
100
|
+
if (delimiter !== undefined) {
|
|
101
|
+
const code = typeof delimiter === "string" && delimiter.length === 1 ? delimiter.charCodeAt(0) : -1;
|
|
102
|
+
if (format !== "csv" || code < 0 || code >= 0x80 || code === QUOTE || code === CR || code === LF) {
|
|
103
|
+
throw new TableProblem("/delimiter", "delimiter is one ASCII character other than a quote or a line break, for CSV.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (typeof header !== "boolean")
|
|
107
|
+
throw new TableProblem("/header", "header is a boolean.");
|
|
108
|
+
if (typeof maxBytes !== "number" || !(maxBytes > 0)) {
|
|
109
|
+
throw new TableProblem("/maxBytes", "maxBytes is a number of bytes.");
|
|
110
|
+
}
|
|
111
|
+
const csv = format === "jsonl" ? null : { separator: format === "tsv" ? TAB : (delimiter?.charCodeAt(0) ?? null), header };
|
|
112
|
+
const survey = csv ? await surveyCsv(file, csv, chunkBytes) : await surveyJson(file, chunkBytes);
|
|
113
|
+
const { rows } = survey;
|
|
114
|
+
const fieldOf = fields
|
|
115
|
+
? fields.flatMap((column, field) => (column ? [field] : []))
|
|
116
|
+
: survey.found.map((_, field) => field);
|
|
117
|
+
const kept = fieldOf.map((field) => survey.found[field] ?? found(""));
|
|
118
|
+
const columns = fields ? fieldOf.map((field) => ({ ...fields[field] })) : kept.map(columnOf);
|
|
119
|
+
const total = columns.reduce((sum, column, index) => sum + sizeOf(column, kept[index], rows), 0);
|
|
120
|
+
if (total > maxBytes) {
|
|
121
|
+
const text = csv ? { separator: survey.separator, header: csv.header } : null;
|
|
122
|
+
return new TextColumns(file, text, columns, { ...survey, found: kept }, fieldOf, maxBytes, chunkBytes).checked();
|
|
123
|
+
}
|
|
124
|
+
const built = columns.map((column, index) => build(column, kept[index], rows));
|
|
125
|
+
const read = csv
|
|
126
|
+
? await keepCsv(file, survey.separator, csv.header, byField(built, fieldOf), rows, chunkBytes)
|
|
127
|
+
: await keepJson(file, built, rows, chunkBytes);
|
|
128
|
+
if (read !== rows)
|
|
129
|
+
throw changed();
|
|
130
|
+
return { columns, rows: null, vectors: built.map((column) => vectorOf(column, rows)), rowCount: rows };
|
|
131
|
+
}
|
|
132
|
+
/** The columns of a CSV file's fields, by field: null for the fields that are not columns. */
|
|
133
|
+
function byField(columns, fieldOf) {
|
|
134
|
+
const fields = new Array(fieldOf.length === 0 ? 0 : Math.max(...fieldOf) + 1).fill(null);
|
|
135
|
+
fieldOf.forEach((field, index) => {
|
|
136
|
+
fields[field] = columns[index];
|
|
137
|
+
});
|
|
138
|
+
return fields;
|
|
139
|
+
}
|
|
140
|
+
function changed() {
|
|
141
|
+
return new TableProblem("/file", "The file changed while it was read.");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A text file whose columns would take more than `maxBytes`: its rows are read by blocks of `BLOCK_ROWS` rows, from
|
|
145
|
+
* where the first reading found that each starts, and its columns whole when a request goes through every row of
|
|
146
|
+
* them, within `maxBytes`.
|
|
147
|
+
*/
|
|
148
|
+
class TextColumns extends FileColumns {
|
|
149
|
+
file;
|
|
150
|
+
text;
|
|
151
|
+
fieldOf;
|
|
152
|
+
chunkBytes;
|
|
153
|
+
found;
|
|
154
|
+
starts;
|
|
155
|
+
/** Blocks of rows read, the most recently used last, and the bytes of the file that they hold. */
|
|
156
|
+
cache = new Map();
|
|
157
|
+
cached = 0;
|
|
158
|
+
constructor(file, text, columns, survey,
|
|
159
|
+
/** The field of a CSV file that each column is. */
|
|
160
|
+
fieldOf, maxBytes, chunkBytes) {
|
|
161
|
+
super(columns, survey.rows, maxBytes);
|
|
162
|
+
this.file = file;
|
|
163
|
+
this.text = text;
|
|
164
|
+
this.fieldOf = fieldOf;
|
|
165
|
+
this.chunkBytes = chunkBytes;
|
|
166
|
+
this.found = survey.found;
|
|
167
|
+
this.starts = survey.starts;
|
|
168
|
+
}
|
|
169
|
+
partOf(row) {
|
|
170
|
+
return Math.floor(row / BLOCK_ROWS);
|
|
171
|
+
}
|
|
172
|
+
/** The rows of a view are read by blocks wherever they lie: a block is small, and reading a column whole reads the file. */
|
|
173
|
+
get spread() {
|
|
174
|
+
return Number.POSITIVE_INFINITY;
|
|
175
|
+
}
|
|
176
|
+
sizeOf(index) {
|
|
177
|
+
return sizeOf(this.columns[index], this.found[index], this.rowCount);
|
|
178
|
+
}
|
|
179
|
+
/** The bytes of the file that a block's rows lie in. */
|
|
180
|
+
spanOf(block) {
|
|
181
|
+
return { start: this.starts[block], end: this.starts[block + 1] ?? this.file.size };
|
|
182
|
+
}
|
|
183
|
+
/** A block's columns, read once and kept while they fit the cache. */
|
|
184
|
+
block(block) {
|
|
185
|
+
const span = this.spanOf(block);
|
|
186
|
+
const size = span.end - span.start;
|
|
187
|
+
let entry = this.cache.get(block);
|
|
188
|
+
if (entry) {
|
|
189
|
+
this.cache.delete(block);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const rows = Math.min(BLOCK_ROWS, this.rowCount - block * BLOCK_ROWS);
|
|
193
|
+
// A column's text takes the block's bytes at most, and is cut to its length once read.
|
|
194
|
+
const built = this.columns.map((column, index) => build(column, { ...this.found[index], values: 0, empty: 0, bytes: size }, rows));
|
|
195
|
+
entry = this.keep(built, rows, span).then(() => built.map((column) => {
|
|
196
|
+
if (column.text)
|
|
197
|
+
column.text = column.text.slice(0, column.cursor);
|
|
198
|
+
return vectorOf(column, rows);
|
|
199
|
+
}));
|
|
200
|
+
this.cached += size;
|
|
201
|
+
// A failed read is not kept.
|
|
202
|
+
entry.catch(() => {
|
|
203
|
+
if (this.cache.get(block) === entry) {
|
|
204
|
+
this.cache.delete(block);
|
|
205
|
+
this.cached -= size;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
this.cache.set(block, entry);
|
|
210
|
+
for (const [old] of this.cache) {
|
|
211
|
+
if (this.cached <= CACHE_BYTES || old === block)
|
|
212
|
+
break;
|
|
213
|
+
const { start, end } = this.spanOf(old);
|
|
214
|
+
this.cache.delete(old);
|
|
215
|
+
this.cached -= end - start;
|
|
216
|
+
}
|
|
217
|
+
return entry;
|
|
218
|
+
}
|
|
219
|
+
/** Reads rows of the file into the columns that are not null, and fails when they are not the rows it had. */
|
|
220
|
+
async keep(built, rows, span) {
|
|
221
|
+
const { file, text, chunkBytes } = this;
|
|
222
|
+
const read = text
|
|
223
|
+
? await keepCsv(file, text.separator, text.header && span.start === undefined, byField(built, this.fieldOf), rows, chunkBytes, span)
|
|
224
|
+
: await keepJson(file, built, rows, chunkBytes, span);
|
|
225
|
+
if (read !== rows)
|
|
226
|
+
throw changed();
|
|
227
|
+
}
|
|
228
|
+
async readCells(index, rows) {
|
|
229
|
+
const blocks = new Map();
|
|
230
|
+
for (let at = 0; at < rows.length; at++) {
|
|
231
|
+
const block = this.partOf(rows[at]);
|
|
232
|
+
if (!blocks.has(block))
|
|
233
|
+
blocks.set(block, this.block(block));
|
|
234
|
+
}
|
|
235
|
+
const read = new Map();
|
|
236
|
+
for (const [block, columns] of blocks)
|
|
237
|
+
read.set(block, (await columns)[index]);
|
|
238
|
+
return Array.from(rows, (row) => read.get(this.partOf(row)).at(row % BLOCK_ROWS));
|
|
239
|
+
}
|
|
240
|
+
async readWhole(indexes, pause) {
|
|
241
|
+
const built = this.columns.map(() => null);
|
|
242
|
+
for (const index of indexes)
|
|
243
|
+
built[index] = build(this.columns[index], this.found[index], this.rowCount);
|
|
244
|
+
await this.keep(built, this.rowCount, { pause });
|
|
245
|
+
return indexes.map((index) => vectorOf(built[index], this.rowCount));
|
|
246
|
+
}
|
|
247
|
+
close() {
|
|
248
|
+
this.cache.clear();
|
|
249
|
+
this.cached = 0;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** A column of the values that the first reading found. */
|
|
253
|
+
function columnOf(column) {
|
|
254
|
+
const { name } = column;
|
|
255
|
+
if (column.values > 0) {
|
|
256
|
+
for (const [bit, type] of TYPES)
|
|
257
|
+
if (column.fits & bit)
|
|
258
|
+
return { name, type };
|
|
259
|
+
// Objects and lists of JSON, and only they, make a column of their JSON text (§2.1).
|
|
260
|
+
if (column.nested === column.values)
|
|
261
|
+
return { name, type: "string", sourceType: "JSON", readOnly: true };
|
|
262
|
+
}
|
|
263
|
+
return { name, type: "string" };
|
|
264
|
+
}
|
|
265
|
+
/** The values of a column that are not null. */
|
|
266
|
+
function present(column, found) {
|
|
267
|
+
return column.type === "string" ? found.values + found.empty : found.values;
|
|
268
|
+
}
|
|
269
|
+
/** The bytes that a column's values take in memory, `Infinity` for text of more than 4 GiB, which is not kept. */
|
|
270
|
+
function sizeOf(column, found, rows) {
|
|
271
|
+
return column.type === "string" && found.bytes > 0xffff_ffff ? Infinity : memoryOf(column, found, rows);
|
|
272
|
+
}
|
|
273
|
+
/** The bytes that a column's values take in memory. */
|
|
274
|
+
function memoryOf(column, found, rows) {
|
|
275
|
+
const validity = present(column, found) < rows ? Math.ceil(rows / 8) : 0;
|
|
276
|
+
switch (column.type) {
|
|
277
|
+
case "boolean":
|
|
278
|
+
return rows + validity;
|
|
279
|
+
case "integer":
|
|
280
|
+
case "date":
|
|
281
|
+
return 4 * rows + validity;
|
|
282
|
+
case "string":
|
|
283
|
+
return 4 * (rows + 1) + found.bytes + validity;
|
|
284
|
+
default:
|
|
285
|
+
return 8 * rows + validity;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function build(column, found, rows) {
|
|
289
|
+
const validity = present(column, found) < rows ? new Uint8Array(Math.ceil(rows / 8)) : null;
|
|
290
|
+
const built = {
|
|
291
|
+
column,
|
|
292
|
+
validity,
|
|
293
|
+
numbers: null,
|
|
294
|
+
halves: null,
|
|
295
|
+
flags: null,
|
|
296
|
+
offsets: null,
|
|
297
|
+
text: null,
|
|
298
|
+
cursor: 0,
|
|
299
|
+
};
|
|
300
|
+
switch (column.type) {
|
|
301
|
+
case "boolean":
|
|
302
|
+
built.flags = new Uint8Array(rows);
|
|
303
|
+
break;
|
|
304
|
+
case "integer":
|
|
305
|
+
case "date":
|
|
306
|
+
built.numbers = new Int32Array(rows);
|
|
307
|
+
break;
|
|
308
|
+
case "float":
|
|
309
|
+
built.numbers = new Float64Array(rows);
|
|
310
|
+
break;
|
|
311
|
+
case "string":
|
|
312
|
+
built.offsets = new Uint32Array(rows + 1);
|
|
313
|
+
built.text = new Uint8Array(found.bytes);
|
|
314
|
+
break;
|
|
315
|
+
default:
|
|
316
|
+
built.numbers = new BigInt64Array(rows);
|
|
317
|
+
built.halves = new Uint32Array(built.numbers.buffer);
|
|
318
|
+
}
|
|
319
|
+
return built;
|
|
320
|
+
}
|
|
321
|
+
/** The JSON (§2.2) of a value of a logical type from the number that holds it. */
|
|
322
|
+
function numberJson(type) {
|
|
323
|
+
switch (type) {
|
|
324
|
+
case "integer":
|
|
325
|
+
return (value) => value;
|
|
326
|
+
case "float":
|
|
327
|
+
return (value) => floatJson(value);
|
|
328
|
+
case "date":
|
|
329
|
+
return (value) => dateText(value);
|
|
330
|
+
case "bigint":
|
|
331
|
+
return (value) => (Number.isSafeInteger(Number(value)) ? Number(value) : value.toString());
|
|
332
|
+
case "time":
|
|
333
|
+
return (value) => timeText(value);
|
|
334
|
+
case "timestamp":
|
|
335
|
+
return (value) => timestampText(value);
|
|
336
|
+
default:
|
|
337
|
+
return (value) => `${timestampText(value)}+00`;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** The text of UTF-8 bytes; short text of ASCII without a decoder. */
|
|
341
|
+
function textOf(bytes, start, end) {
|
|
342
|
+
if (end - start <= 32) {
|
|
343
|
+
let text = "";
|
|
344
|
+
for (let index = start; index < end; index++) {
|
|
345
|
+
const code = bytes[index];
|
|
346
|
+
if (code >= 0x80)
|
|
347
|
+
return decoder.decode(bytes.subarray(start, end));
|
|
348
|
+
text += String.fromCharCode(code);
|
|
349
|
+
}
|
|
350
|
+
return text;
|
|
351
|
+
}
|
|
352
|
+
return decoder.decode(bytes.subarray(start, end));
|
|
353
|
+
}
|
|
354
|
+
function vectorOf(built, rows) {
|
|
355
|
+
const { validity } = built;
|
|
356
|
+
const valid = (row) => validity === null || ((validity[row >> 3] >> (row & 7)) & 1) === 1;
|
|
357
|
+
const { numbers, flags, offsets, text } = built;
|
|
358
|
+
let at;
|
|
359
|
+
if (numbers) {
|
|
360
|
+
const json = numberJson(built.column.type);
|
|
361
|
+
at = (row) => (valid(row) ? json(numbers[row]) : null);
|
|
362
|
+
}
|
|
363
|
+
else if (flags) {
|
|
364
|
+
at = (row) => (valid(row) ? flags[row] === 1 : null);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
const ends = offsets;
|
|
368
|
+
const bytes = text;
|
|
369
|
+
at = (row) => (valid(row) ? textOf(bytes, ends[row], ends[row + 1]) : null);
|
|
370
|
+
}
|
|
371
|
+
return { length: rows, numbers, validity, at };
|
|
372
|
+
}
|
|
373
|
+
/** Sets a row's bit of validity. */
|
|
374
|
+
function mark(built, row) {
|
|
375
|
+
if (built.validity)
|
|
376
|
+
built.validity[row >> 3] |= 1 << (row & 7);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Reads a file in slices from after its byte order mark, giving each to `read` after the bytes that the last one
|
|
380
|
+
* left, with where they start in the file: `read` gives how many of them it used. A row longer than a slice makes
|
|
381
|
+
* the next slice as long, so that its bytes are not read again and again.
|
|
382
|
+
*/
|
|
383
|
+
async function eachSlice(file, chunkBytes, read, span = {}) {
|
|
384
|
+
const size = span.end ?? file.size;
|
|
385
|
+
const bytesOf = async (start, end) => {
|
|
386
|
+
try {
|
|
387
|
+
return new Uint8Array(await file.slice(start, end).arrayBuffer());
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
throw new TableProblem("/file", `The file cannot be read: ${error.message}`);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
let offset = span.start ?? 0;
|
|
394
|
+
if (span.start === undefined) {
|
|
395
|
+
const head = await bytesOf(0, Math.min(size, 3));
|
|
396
|
+
if (head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf)
|
|
397
|
+
offset = 3;
|
|
398
|
+
else if ((head[0] === 0xff && head[1] === 0xfe) || (head[0] === 0xfe && head[1] === 0xff)) {
|
|
399
|
+
throw new TableProblem("/file", "The file is UTF-16 text; save it as UTF-8.");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
let rest = null;
|
|
403
|
+
for (let first = true;; first = false) {
|
|
404
|
+
if (!first)
|
|
405
|
+
await span.pause?.();
|
|
406
|
+
const end = Math.min(size, offset + Math.max(chunkBytes, rest?.length ?? 0));
|
|
407
|
+
let bytes = await bytesOf(offset, end);
|
|
408
|
+
if (rest) {
|
|
409
|
+
const joined = new Uint8Array(rest.length + bytes.length);
|
|
410
|
+
joined.set(rest);
|
|
411
|
+
joined.set(bytes, rest.length);
|
|
412
|
+
bytes = joined;
|
|
413
|
+
}
|
|
414
|
+
const last = end >= size;
|
|
415
|
+
const used = read(bytes, last, end - bytes.length);
|
|
416
|
+
if (last)
|
|
417
|
+
return;
|
|
418
|
+
rest = used < bytes.length ? bytes.subarray(used) : null;
|
|
419
|
+
offset = end;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The length of the UTF-8 sequence that starts with a byte of 0x80 or more at `index`: 2 to 4 bytes, 0 when the
|
|
424
|
+
* bytes are not UTF-8 (overlong forms, surrogates and code points beyond U+10FFFF are not), −1 when `end` cuts it.
|
|
425
|
+
*/
|
|
426
|
+
function sequenceLength(bytes, index, end) {
|
|
427
|
+
const lead = bytes[index];
|
|
428
|
+
let length;
|
|
429
|
+
let low = 0x80;
|
|
430
|
+
let high = 0xbf;
|
|
431
|
+
if (lead < 0xc2)
|
|
432
|
+
return 0;
|
|
433
|
+
if (lead < 0xe0)
|
|
434
|
+
length = 2;
|
|
435
|
+
else if (lead < 0xf0) {
|
|
436
|
+
length = 3;
|
|
437
|
+
if (lead === 0xe0)
|
|
438
|
+
low = 0xa0;
|
|
439
|
+
else if (lead === 0xed)
|
|
440
|
+
high = 0x9f;
|
|
441
|
+
}
|
|
442
|
+
else if (lead < 0xf5) {
|
|
443
|
+
length = 4;
|
|
444
|
+
if (lead === 0xf0)
|
|
445
|
+
low = 0x90;
|
|
446
|
+
else if (lead === 0xf4)
|
|
447
|
+
high = 0x8f;
|
|
448
|
+
}
|
|
449
|
+
else
|
|
450
|
+
return 0;
|
|
451
|
+
for (let next = 1; next < length; next++) {
|
|
452
|
+
if (index + next >= end)
|
|
453
|
+
return -1;
|
|
454
|
+
const byte = bytes[index + next];
|
|
455
|
+
if (byte < low || byte > high)
|
|
456
|
+
return 0;
|
|
457
|
+
low = 0x80;
|
|
458
|
+
high = 0xbf;
|
|
459
|
+
}
|
|
460
|
+
return length;
|
|
461
|
+
}
|
|
462
|
+
/** The fields of a row of CSV: where each starts and ends in the bytes, and whether it holds a quote. */
|
|
463
|
+
class Fields {
|
|
464
|
+
starts = new Int32Array(16);
|
|
465
|
+
ends = new Int32Array(16);
|
|
466
|
+
quoted = new Uint8Array(16);
|
|
467
|
+
count = 0;
|
|
468
|
+
add(start, end, quoted) {
|
|
469
|
+
if (this.count === this.starts.length) {
|
|
470
|
+
const grow = (array, make) => {
|
|
471
|
+
const grown = make(array.length * 2);
|
|
472
|
+
grown.set(array);
|
|
473
|
+
return grown;
|
|
474
|
+
};
|
|
475
|
+
this.starts = grow(this.starts, (length) => new Int32Array(length));
|
|
476
|
+
this.ends = grow(this.ends, (length) => new Int32Array(length));
|
|
477
|
+
this.quoted = grow(this.quoted, (length) => new Uint8Array(length));
|
|
478
|
+
}
|
|
479
|
+
this.starts[this.count] = start;
|
|
480
|
+
this.ends[this.count] = end;
|
|
481
|
+
this.quoted[this.count] = quoted;
|
|
482
|
+
this.count++;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/** Bytes that are not UTF-8, in the row being read, in its field `field`. */
|
|
486
|
+
class NotUtf8 {
|
|
487
|
+
field;
|
|
488
|
+
constructor(field) {
|
|
489
|
+
this.field = field;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Reads the rows of CSV bytes, calling `row` with each row's fields, and gives where the first row that is not
|
|
494
|
+
* whole starts (all of the bytes when they are the last). A quote switches quoting on and off wherever it is, so
|
|
495
|
+
* that a doubled quote within quotes is a quote; a line break outside quotes ends a row, without a carriage return
|
|
496
|
+
* before it, and an empty line is no row. Throws `NotUtf8` for bytes that are not UTF-8.
|
|
497
|
+
*/
|
|
498
|
+
function scanCsv(bytes, separator, last, fields, row) {
|
|
499
|
+
const end = bytes.length;
|
|
500
|
+
let rowStart = 0;
|
|
501
|
+
let fieldStart = 0;
|
|
502
|
+
let inQuotes = false;
|
|
503
|
+
let quoted = 0;
|
|
504
|
+
const endRow = (fieldEnd) => {
|
|
505
|
+
fields.add(fieldStart, fieldEnd > fieldStart && bytes[fieldEnd - 1] === CR ? fieldEnd - 1 : fieldEnd, quoted);
|
|
506
|
+
if (fields.count > 1 || fields.ends[0] > fields.starts[0] || quoted)
|
|
507
|
+
row();
|
|
508
|
+
fields.count = 0;
|
|
509
|
+
quoted = 0;
|
|
510
|
+
};
|
|
511
|
+
fields.count = 0;
|
|
512
|
+
for (let index = 0; index < end; index++) {
|
|
513
|
+
const code = bytes[index];
|
|
514
|
+
if (code === QUOTE) {
|
|
515
|
+
inQuotes = !inQuotes;
|
|
516
|
+
quoted = 1;
|
|
517
|
+
}
|
|
518
|
+
else if (code >= 0x80) {
|
|
519
|
+
const length = sequenceLength(bytes, index, end);
|
|
520
|
+
if (length > 0)
|
|
521
|
+
index += length - 1;
|
|
522
|
+
else if (length < 0 && !last)
|
|
523
|
+
return rowStart;
|
|
524
|
+
else
|
|
525
|
+
throw new NotUtf8(fields.count);
|
|
526
|
+
}
|
|
527
|
+
else if (!inQuotes) {
|
|
528
|
+
if (code === separator) {
|
|
529
|
+
fields.add(fieldStart, index, quoted);
|
|
530
|
+
fieldStart = index + 1;
|
|
531
|
+
quoted = 0;
|
|
532
|
+
}
|
|
533
|
+
else if (code === LF) {
|
|
534
|
+
endRow(index);
|
|
535
|
+
fieldStart = rowStart = index + 1;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (!last)
|
|
540
|
+
return rowStart;
|
|
541
|
+
if (fieldStart < end || fields.count > 0 || quoted)
|
|
542
|
+
endRow(end);
|
|
543
|
+
return end;
|
|
544
|
+
}
|
|
545
|
+
/** Where `unquote` writes the text of a field. */
|
|
546
|
+
let unquoted = new Uint8Array(256);
|
|
547
|
+
/**
|
|
548
|
+
* Writes the text of a field that holds a quote into `unquoted`, and gives its length: quotes switch quoting, and
|
|
549
|
+
* a doubled quote within quotes is a quote.
|
|
550
|
+
*/
|
|
551
|
+
function unquote(bytes, start, end) {
|
|
552
|
+
if (unquoted.length < end - start)
|
|
553
|
+
unquoted = new Uint8Array(Math.max(end - start, unquoted.length * 2));
|
|
554
|
+
let length = 0;
|
|
555
|
+
let inQuotes = false;
|
|
556
|
+
for (let index = start; index < end; index++) {
|
|
557
|
+
const code = bytes[index];
|
|
558
|
+
if (code !== QUOTE)
|
|
559
|
+
unquoted[length++] = code;
|
|
560
|
+
else if (inQuotes && index + 1 < end && bytes[index + 1] === QUOTE) {
|
|
561
|
+
unquoted[length++] = QUOTE;
|
|
562
|
+
index++;
|
|
563
|
+
}
|
|
564
|
+
else
|
|
565
|
+
inQuotes = !inQuotes;
|
|
566
|
+
}
|
|
567
|
+
return length;
|
|
568
|
+
}
|
|
569
|
+
/** The separator of `,`, tab, `;` and `|` that splits the first line into the most fields; `,` when none does. */
|
|
570
|
+
function detectSeparator(bytes) {
|
|
571
|
+
const candidates = [0x2c, TAB, 0x3b, 0x7c];
|
|
572
|
+
const counts = [0, 0, 0, 0];
|
|
573
|
+
let inQuotes = false;
|
|
574
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
575
|
+
const code = bytes[index];
|
|
576
|
+
if (code === QUOTE)
|
|
577
|
+
inQuotes = !inQuotes;
|
|
578
|
+
else if (!inQuotes) {
|
|
579
|
+
if (code === LF)
|
|
580
|
+
break;
|
|
581
|
+
const at = candidates.indexOf(code);
|
|
582
|
+
if (at >= 0)
|
|
583
|
+
counts[at]++;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
let best = 0;
|
|
587
|
+
for (let at = 1; at < counts.length; at++)
|
|
588
|
+
if (counts[at] > counts[best])
|
|
589
|
+
best = at;
|
|
590
|
+
return candidates[best];
|
|
591
|
+
}
|
|
592
|
+
/** ASCII bytes as text. */
|
|
593
|
+
function ascii(bytes, start, end) {
|
|
594
|
+
return end - start > 64 ? decoder.decode(bytes.subarray(start, end)) : textOf(bytes, start, end);
|
|
595
|
+
}
|
|
596
|
+
function isDigit(code) {
|
|
597
|
+
return code >= ZERO && code <= NINE;
|
|
598
|
+
}
|
|
599
|
+
/** The value of the text of an integer of no more than 15 digits. */
|
|
600
|
+
function integerValue(bytes, start, end) {
|
|
601
|
+
let index = start;
|
|
602
|
+
const negative = bytes[index] === MINUS;
|
|
603
|
+
if (negative || bytes[index] === PLUS)
|
|
604
|
+
index++;
|
|
605
|
+
let value = 0;
|
|
606
|
+
for (; index < end; index++)
|
|
607
|
+
value = value * 10 + (bytes[index] - ZERO);
|
|
608
|
+
return negative ? -value : value;
|
|
609
|
+
}
|
|
610
|
+
/** Powers of ten that are floats exactly. */
|
|
611
|
+
const POWERS = Array.from({ length: 23 }, (_, power) => Number(`1e${power}`));
|
|
612
|
+
/**
|
|
613
|
+
* The float of the text of a number: a decimal of up to 15 digits without an exponent is its digits divided by a
|
|
614
|
+
* power of ten, both exact, which rounds once as `Number` does (Clinger's fast path); others are read by `Number`.
|
|
615
|
+
*/
|
|
616
|
+
function floatValue(bytes, start, end) {
|
|
617
|
+
let index = start;
|
|
618
|
+
const negative = bytes[index] === MINUS;
|
|
619
|
+
if (negative || bytes[index] === PLUS)
|
|
620
|
+
index++;
|
|
621
|
+
let mantissa = 0;
|
|
622
|
+
let count = 0;
|
|
623
|
+
let scale = 0;
|
|
624
|
+
for (; index < end && isDigit(bytes[index]); index++, count++)
|
|
625
|
+
mantissa = mantissa * 10 + bytes[index] - ZERO;
|
|
626
|
+
if (index < end && bytes[index] === DOT) {
|
|
627
|
+
for (index++; index < end && isDigit(bytes[index]); index++, count++, scale++) {
|
|
628
|
+
mantissa = mantissa * 10 + bytes[index] - ZERO;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (index < end || count > 15) {
|
|
632
|
+
const text = ascii(bytes, start, end);
|
|
633
|
+
const value = Number(text);
|
|
634
|
+
return Number.isNaN(value) ? (FLOAT_WORDS.get(text.toLowerCase()) ?? value) : value;
|
|
635
|
+
}
|
|
636
|
+
const value = mantissa / POWERS[scale];
|
|
637
|
+
return negative ? -value : value;
|
|
638
|
+
}
|
|
639
|
+
/** The types that the text of an integer fits, with its digits from `digits`. */
|
|
640
|
+
function integerFits(bytes, start, digits, end) {
|
|
641
|
+
let first = digits;
|
|
642
|
+
while (first < end - 1 && bytes[first] === ZERO)
|
|
643
|
+
first++;
|
|
644
|
+
const count = end - first;
|
|
645
|
+
if (count <= 9)
|
|
646
|
+
return INTEGER | BIGINT | FLOAT;
|
|
647
|
+
if (count <= 15) {
|
|
648
|
+
const value = integerValue(bytes, start, end);
|
|
649
|
+
return value >= -0x8000_0000 && value <= 0x7fff_ffff ? INTEGER | BIGINT | FLOAT : BIGINT | FLOAT;
|
|
650
|
+
}
|
|
651
|
+
if (count <= 19) {
|
|
652
|
+
const value = BigInt(ascii(bytes, start, end));
|
|
653
|
+
return value >= INT64_MIN && value <= INT64_MAX ? BIGINT | FLOAT : FLOAT;
|
|
654
|
+
}
|
|
655
|
+
return FLOAT;
|
|
656
|
+
}
|
|
657
|
+
/** Whether the text from `index`, after the whole digits of a number, ends it as a float: a fraction, an exponent. */
|
|
658
|
+
function floatRest(bytes, index, end, whole) {
|
|
659
|
+
let at = index;
|
|
660
|
+
let fraction = 0;
|
|
661
|
+
if (bytes[at] === DOT) {
|
|
662
|
+
const from = ++at;
|
|
663
|
+
while (at < end && isDigit(bytes[at]))
|
|
664
|
+
at++;
|
|
665
|
+
fraction = at - from;
|
|
666
|
+
}
|
|
667
|
+
if (!whole && fraction === 0)
|
|
668
|
+
return false;
|
|
669
|
+
if (at < end && (bytes[at] | 0x20) === LOWER_E) {
|
|
670
|
+
at++;
|
|
671
|
+
if (at < end && (bytes[at] === PLUS || bytes[at] === MINUS))
|
|
672
|
+
at++;
|
|
673
|
+
const from = at;
|
|
674
|
+
while (at < end && isDigit(bytes[at]))
|
|
675
|
+
at++;
|
|
676
|
+
if (at === from)
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
return at === end;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* The types that the text of a field fits, of those in `wanted`: booleans (`true`, `false` in any case), integers
|
|
683
|
+
* by their range, floats (with `nan`, `inf` and `infinity`), and times, dates and timestamps (`temporalFits`).
|
|
684
|
+
*/
|
|
685
|
+
function textFits(bytes, start, end, wanted) {
|
|
686
|
+
const first = bytes[start];
|
|
687
|
+
const signed = first === PLUS || first === MINUS;
|
|
688
|
+
let index = signed ? start + 1 : start;
|
|
689
|
+
const digits = index;
|
|
690
|
+
while (index < end && isDigit(bytes[index]))
|
|
691
|
+
index++;
|
|
692
|
+
const whole = index - digits;
|
|
693
|
+
if (index === end)
|
|
694
|
+
return whole > 0 ? wanted & integerFits(bytes, start, digits, end) : 0;
|
|
695
|
+
const next = bytes[index];
|
|
696
|
+
if (wanted & FLOAT && (next === DOT || (next | 0x20) === LOWER_E) && floatRest(bytes, index, end, whole > 0)) {
|
|
697
|
+
return FLOAT;
|
|
698
|
+
}
|
|
699
|
+
if (wanted & TEMPORAL &&
|
|
700
|
+
!signed &&
|
|
701
|
+
((whole === 2 && next === COLON) || (whole === 4 && next === MINUS)) &&
|
|
702
|
+
end - start <= 40) {
|
|
703
|
+
return wanted & temporalFits(bytes, start, end);
|
|
704
|
+
}
|
|
705
|
+
if (wanted & (BOOLEAN | FLOAT) && whole === 0 && end - start <= 9) {
|
|
706
|
+
const word = ascii(bytes, start, end).toLowerCase();
|
|
707
|
+
if (word === "true" || word === "false")
|
|
708
|
+
return wanted & BOOLEAN;
|
|
709
|
+
if (FLOAT_WORDS.has(word))
|
|
710
|
+
return wanted & FLOAT;
|
|
711
|
+
}
|
|
712
|
+
return 0;
|
|
713
|
+
}
|
|
714
|
+
const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
715
|
+
const FRACTION_SCALE = [100_000, 10_000, 1000, 100, 10, 1];
|
|
716
|
+
const SPACE = 0x20;
|
|
717
|
+
const UPPER_T = 0x54;
|
|
718
|
+
const UPPER_Z = 0x5a;
|
|
719
|
+
/** The days and the microseconds of the day of the text that `temporalFits` last read, and where its clock ended. */
|
|
720
|
+
let readDays = 0;
|
|
721
|
+
let readMicros = 0;
|
|
722
|
+
let clockEnd = 0;
|
|
723
|
+
/** The number of two digits at `at`, or −1. */
|
|
724
|
+
function twoDigits(bytes, at) {
|
|
725
|
+
const tens = bytes[at] - ZERO;
|
|
726
|
+
const ones = bytes[at + 1] - ZERO;
|
|
727
|
+
return tens >= 0 && tens <= 9 && ones >= 0 && ones <= 9 ? tens * 10 + ones : -1;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Microseconds since midnight of `HH:MM:SS` with up to 6 fraction digits at `at`, before `end`, or −1; sets
|
|
731
|
+
* `clockEnd` to where it ends.
|
|
732
|
+
*/
|
|
733
|
+
function clockAt(bytes, at, end) {
|
|
734
|
+
if (end - at < 8 || bytes[at + 2] !== COLON || bytes[at + 5] !== COLON)
|
|
735
|
+
return -1;
|
|
736
|
+
const hours = twoDigits(bytes, at);
|
|
737
|
+
const minutes = twoDigits(bytes, at + 3);
|
|
738
|
+
const seconds = twoDigits(bytes, at + 6);
|
|
739
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59)
|
|
740
|
+
return -1;
|
|
741
|
+
let micros = (hours * 3600 + minutes * 60 + seconds) * 1_000_000;
|
|
742
|
+
let index = at + 8;
|
|
743
|
+
if (index < end && bytes[index] === DOT) {
|
|
744
|
+
const from = ++index;
|
|
745
|
+
while (index < end && isDigit(bytes[index])) {
|
|
746
|
+
if (index - from === 6)
|
|
747
|
+
return -1;
|
|
748
|
+
micros += (bytes[index] - ZERO) * FRACTION_SCALE[index - from];
|
|
749
|
+
index++;
|
|
750
|
+
}
|
|
751
|
+
if (index === from)
|
|
752
|
+
return -1;
|
|
753
|
+
}
|
|
754
|
+
clockEnd = index;
|
|
755
|
+
return micros;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The type of the text of a time (`HH:MM:SS` with up to 6 fraction digits), a date (`YYYY-MM-DD` of the years 0001
|
|
759
|
+
* to 9999, which also fits timestamps), a timestamp (a date, a space or `T`, and a time) or a timestamp with `Z` or
|
|
760
|
+
* an offset (`+HH`, `+HH:MM` or `+HHMM`), which is an instant; 0 for other text. Keeps the value in `readDays` and
|
|
761
|
+
* `readMicros`, in UTC for an instant.
|
|
762
|
+
*/
|
|
763
|
+
function temporalFits(bytes, start, end) {
|
|
764
|
+
if (bytes[start + 2] === COLON) {
|
|
765
|
+
const micros = clockAt(bytes, start, end);
|
|
766
|
+
if (micros < 0 || clockEnd !== end)
|
|
767
|
+
return 0;
|
|
768
|
+
readMicros = micros;
|
|
769
|
+
return TIME;
|
|
770
|
+
}
|
|
771
|
+
if (end - start < 10 || bytes[start + 4] !== MINUS || bytes[start + 7] !== MINUS)
|
|
772
|
+
return 0;
|
|
773
|
+
const century = twoDigits(bytes, start);
|
|
774
|
+
const decade = twoDigits(bytes, start + 2);
|
|
775
|
+
const month = twoDigits(bytes, start + 5);
|
|
776
|
+
const day = twoDigits(bytes, start + 8);
|
|
777
|
+
if (century < 0 || decade < 0 || month < 1 || month > 12 || day < 1)
|
|
778
|
+
return 0;
|
|
779
|
+
const year = century * 100 + decade;
|
|
780
|
+
const leap = month === 2 && year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
781
|
+
if (year < 1 || day > MONTH_DAYS[month - 1] + (leap ? 1 : 0))
|
|
782
|
+
return 0;
|
|
783
|
+
const days = daysFromCivil(year, month, day);
|
|
784
|
+
if (end - start === 10) {
|
|
785
|
+
readDays = days;
|
|
786
|
+
readMicros = 0;
|
|
787
|
+
return DATE | TIMESTAMP;
|
|
788
|
+
}
|
|
789
|
+
const separator = bytes[start + 10];
|
|
790
|
+
if (separator !== SPACE && separator !== UPPER_T)
|
|
791
|
+
return 0;
|
|
792
|
+
let micros = clockAt(bytes, start + 11, end);
|
|
793
|
+
if (micros < 0)
|
|
794
|
+
return 0;
|
|
795
|
+
let index = clockEnd;
|
|
796
|
+
if (index === end) {
|
|
797
|
+
readDays = days;
|
|
798
|
+
readMicros = micros;
|
|
799
|
+
return TIMESTAMP;
|
|
800
|
+
}
|
|
801
|
+
const sign = bytes[index];
|
|
802
|
+
if (sign === UPPER_Z) {
|
|
803
|
+
if (index + 1 !== end)
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
if ((sign !== PLUS && sign !== MINUS) || end - index < 3)
|
|
808
|
+
return 0;
|
|
809
|
+
const hours = twoDigits(bytes, index + 1);
|
|
810
|
+
let minutes = 0;
|
|
811
|
+
index += 3;
|
|
812
|
+
if (index < end) {
|
|
813
|
+
if (bytes[index] === COLON)
|
|
814
|
+
index++;
|
|
815
|
+
minutes = end - index === 2 ? twoDigits(bytes, index) : -1;
|
|
816
|
+
}
|
|
817
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59)
|
|
818
|
+
return 0;
|
|
819
|
+
micros -= (sign === MINUS ? -1 : 1) * (hours * 3600 + minutes * 60) * 1_000_000;
|
|
820
|
+
}
|
|
821
|
+
const shift = Math.floor(micros / DAY_MICROS);
|
|
822
|
+
readDays = days + shift;
|
|
823
|
+
readMicros = micros - shift * DAY_MICROS;
|
|
824
|
+
return readDays >= FIRST_DAY && readDays <= LAST_DAY ? TIMESTAMPTZ : 0;
|
|
825
|
+
}
|
|
826
|
+
/** Where `temporalText` writes a string. */
|
|
827
|
+
const temporal = new Uint8Array(40);
|
|
828
|
+
/** `temporalFits` of a string of JSON. */
|
|
829
|
+
function temporalText(text) {
|
|
830
|
+
if (text.length > temporal.length)
|
|
831
|
+
return 0;
|
|
832
|
+
for (let index = 0; index < text.length; index++) {
|
|
833
|
+
const code = text.charCodeAt(index);
|
|
834
|
+
if (code >= 0x80)
|
|
835
|
+
return 0;
|
|
836
|
+
temporal[index] = code;
|
|
837
|
+
}
|
|
838
|
+
return temporalFits(temporal, 0, text.length);
|
|
839
|
+
}
|
|
840
|
+
/** Reads a CSV or TSV file the first time: its separator, its columns with what their values fit, and its rows. */
|
|
841
|
+
async function surveyCsv(file, options, chunkBytes) {
|
|
842
|
+
const fields = new Fields();
|
|
843
|
+
const columns = [];
|
|
844
|
+
const starts = [];
|
|
845
|
+
let header = options.header ? null : [];
|
|
846
|
+
let separator = options.separator;
|
|
847
|
+
let rows = 0;
|
|
848
|
+
let bytes = new Uint8Array(0);
|
|
849
|
+
let base = 0;
|
|
850
|
+
const row = () => {
|
|
851
|
+
if (header === null) {
|
|
852
|
+
header = [];
|
|
853
|
+
for (let index = 0; index < fields.count; index++) {
|
|
854
|
+
const start = fields.starts[index];
|
|
855
|
+
const end = fields.ends[index];
|
|
856
|
+
header.push(fields.quoted[index]
|
|
857
|
+
? decoder.decode(unquoted.subarray(0, unquote(bytes, start, end)))
|
|
858
|
+
: decoder.decode(bytes.subarray(start, end)));
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
// A row's first field starts where the row does.
|
|
863
|
+
if (rows % BLOCK_ROWS === 0)
|
|
864
|
+
starts.push(base + fields.starts[0]);
|
|
865
|
+
for (let index = 0; index < fields.count; index++) {
|
|
866
|
+
columns[index] ??= found("");
|
|
867
|
+
const column = columns[index];
|
|
868
|
+
let source = bytes;
|
|
869
|
+
let start = fields.starts[index];
|
|
870
|
+
let end = fields.ends[index];
|
|
871
|
+
if (fields.quoted[index]) {
|
|
872
|
+
end = unquote(bytes, start, end);
|
|
873
|
+
start = 0;
|
|
874
|
+
source = unquoted;
|
|
875
|
+
if (end === 0) {
|
|
876
|
+
column.empty++;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
else if (start === end)
|
|
881
|
+
continue;
|
|
882
|
+
column.values++;
|
|
883
|
+
column.bytes += end - start;
|
|
884
|
+
if (column.fits !== 0)
|
|
885
|
+
column.fits &= textFits(source, start, end, column.fits);
|
|
886
|
+
}
|
|
887
|
+
rows++;
|
|
888
|
+
};
|
|
889
|
+
await eachSlice(file, chunkBytes, (slice, last, at) => {
|
|
890
|
+
bytes = slice;
|
|
891
|
+
base = at;
|
|
892
|
+
separator ??= detectSeparator(slice);
|
|
893
|
+
try {
|
|
894
|
+
return scanCsv(slice, separator, last, fields, row);
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
if (!(error instanceof NotUtf8))
|
|
898
|
+
throw error;
|
|
899
|
+
throw header === null
|
|
900
|
+
? new TableProblem("/file", "The first line, which names the columns, is not UTF-8 text.")
|
|
901
|
+
: new TableProblem(`/rows/${rows}/${error.field}`, "The text is not UTF-8 (§2.2).");
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
const names = (header ?? []);
|
|
905
|
+
const width = Math.max(names.length, columns.length);
|
|
906
|
+
const used = new Set();
|
|
907
|
+
for (let index = 0; index < width; index++) {
|
|
908
|
+
const base = names[index] || `column${index}`;
|
|
909
|
+
let name = base;
|
|
910
|
+
for (let suffix = 1; used.has(name); suffix++)
|
|
911
|
+
name = `${base}_${suffix}`;
|
|
912
|
+
used.add(name);
|
|
913
|
+
columns[index] = columns[index] ?? found("");
|
|
914
|
+
columns[index].name = name;
|
|
915
|
+
}
|
|
916
|
+
return { separator: separator ?? 0x2c, found: columns, rows, starts };
|
|
917
|
+
}
|
|
918
|
+
/** Writes the text of a field into a column as its type, from the first reading's finding that it fits. */
|
|
919
|
+
function putText(built, bytes, start, end, row) {
|
|
920
|
+
switch (built.column.type) {
|
|
921
|
+
case "boolean":
|
|
922
|
+
built.flags[row] = (bytes[start] | 0x20) === 0x74 ? 1 : 0;
|
|
923
|
+
break;
|
|
924
|
+
case "integer":
|
|
925
|
+
built.numbers[row] = integerValue(bytes, start, end);
|
|
926
|
+
break;
|
|
927
|
+
case "bigint":
|
|
928
|
+
if (end - start <= 15)
|
|
929
|
+
putInt64(built, row, integerValue(bytes, start, end));
|
|
930
|
+
else
|
|
931
|
+
built.numbers[row] = BigInt(ascii(bytes, start, end));
|
|
932
|
+
break;
|
|
933
|
+
case "float":
|
|
934
|
+
built.numbers[row] = floatValue(bytes, start, end);
|
|
935
|
+
break;
|
|
936
|
+
case "string":
|
|
937
|
+
built.text.set(bytes.subarray(start, end), built.cursor);
|
|
938
|
+
built.cursor += end - start;
|
|
939
|
+
break;
|
|
940
|
+
default:
|
|
941
|
+
if (!temporalFits(bytes, start, end))
|
|
942
|
+
throw changed();
|
|
943
|
+
putTemporal(built, row);
|
|
944
|
+
}
|
|
945
|
+
mark(built, row);
|
|
946
|
+
}
|
|
947
|
+
/** Writes what `temporalFits` last read into a column of a time, a date or a timestamp. */
|
|
948
|
+
function putTemporal(built, row) {
|
|
949
|
+
if (built.column.type === "date")
|
|
950
|
+
built.numbers[row] = readDays;
|
|
951
|
+
else if (built.column.type === "time")
|
|
952
|
+
putInt64(built, row, readMicros);
|
|
953
|
+
else {
|
|
954
|
+
// Microseconds since 1970-01-01 are safe integers for about 285 years either side of it.
|
|
955
|
+
const micros = readDays * DAY_MICROS + readMicros;
|
|
956
|
+
if (Number.isSafeInteger(micros))
|
|
957
|
+
putInt64(built, row, micros);
|
|
958
|
+
else
|
|
959
|
+
built.numbers[row] = BigInt(readDays) * BigInt(DAY_MICROS) + BigInt(readMicros);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/** Writes a safe integer into a column of 64-bit integers, as its halves. */
|
|
963
|
+
function putInt64(built, row, value) {
|
|
964
|
+
const halves = built.halves;
|
|
965
|
+
const high = Math.floor(value / TWO_32);
|
|
966
|
+
halves[row * 2 + HIGH] = high;
|
|
967
|
+
halves[row * 2 + LOW] = value - high * TWO_32;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Reads a CSV or TSV file the second time, or a span of it, keeping its values in the columns that are not null;
|
|
971
|
+
* gives the rows read.
|
|
972
|
+
*/
|
|
973
|
+
async function keepCsv(file, separator, header, built, rowCount, chunkBytes, span) {
|
|
974
|
+
const fields = new Fields();
|
|
975
|
+
const strings = built.filter((column) => column?.offsets);
|
|
976
|
+
let skip = header;
|
|
977
|
+
let rows = 0;
|
|
978
|
+
let bytes = new Uint8Array(0);
|
|
979
|
+
const row = () => {
|
|
980
|
+
if (skip) {
|
|
981
|
+
skip = false;
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (rows === rowCount)
|
|
985
|
+
throw changed();
|
|
986
|
+
const count = Math.min(fields.count, built.length);
|
|
987
|
+
for (let index = 0; index < count; index++) {
|
|
988
|
+
const column = built[index];
|
|
989
|
+
if (!column)
|
|
990
|
+
continue;
|
|
991
|
+
let source = bytes;
|
|
992
|
+
let start = fields.starts[index];
|
|
993
|
+
let end = fields.ends[index];
|
|
994
|
+
if (fields.quoted[index]) {
|
|
995
|
+
end = unquote(bytes, start, end);
|
|
996
|
+
start = 0;
|
|
997
|
+
source = unquoted;
|
|
998
|
+
// A quoted empty field is an empty string, and null in a column of another type.
|
|
999
|
+
if (end === 0 && !column.offsets)
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
else if (start === end)
|
|
1003
|
+
continue;
|
|
1004
|
+
putText(column, source, start, end, rows);
|
|
1005
|
+
}
|
|
1006
|
+
for (const column of strings)
|
|
1007
|
+
column.offsets[rows + 1] = column.cursor;
|
|
1008
|
+
rows++;
|
|
1009
|
+
};
|
|
1010
|
+
await eachSlice(file, chunkBytes, (slice, last) => {
|
|
1011
|
+
bytes = slice;
|
|
1012
|
+
return scanCsv(slice, separator, last, fields, row);
|
|
1013
|
+
}, span);
|
|
1014
|
+
return rows;
|
|
1015
|
+
}
|
|
1016
|
+
/** The bytes of a string as UTF-8, or −1 when it holds a lone surrogate. */
|
|
1017
|
+
function utf8Length(text) {
|
|
1018
|
+
let length = text.length;
|
|
1019
|
+
for (let index = 0; index < text.length; index++) {
|
|
1020
|
+
const code = text.charCodeAt(index);
|
|
1021
|
+
if (code < 0x80)
|
|
1022
|
+
continue;
|
|
1023
|
+
if (code < 0x800)
|
|
1024
|
+
length += 1;
|
|
1025
|
+
else if (code < 0xd800 || code > 0xdfff)
|
|
1026
|
+
length += 2;
|
|
1027
|
+
else if (code < 0xdc00 && (text.charCodeAt(index + 1) & 0xfc00) === 0xdc00) {
|
|
1028
|
+
length += 2;
|
|
1029
|
+
index++;
|
|
1030
|
+
}
|
|
1031
|
+
else
|
|
1032
|
+
return -1;
|
|
1033
|
+
}
|
|
1034
|
+
return length;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Calls `line` with each whole line of the bytes, without its line break, and gives where the first line that is
|
|
1038
|
+
* not whole starts (all of the bytes when they are the last).
|
|
1039
|
+
*/
|
|
1040
|
+
function eachLine(bytes, last, line) {
|
|
1041
|
+
let start = 0;
|
|
1042
|
+
for (;;) {
|
|
1043
|
+
const newline = bytes.indexOf(LF, start);
|
|
1044
|
+
const end = newline < 0 ? bytes.length : newline;
|
|
1045
|
+
if (newline < 0 && !last)
|
|
1046
|
+
return start;
|
|
1047
|
+
if (newline >= 0 || start < end)
|
|
1048
|
+
line(start, end > start && bytes[end - 1] === CR ? end - 1 : end);
|
|
1049
|
+
if (newline < 0)
|
|
1050
|
+
return bytes.length;
|
|
1051
|
+
start = newline + 1;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/** The object of a line of JSON Lines, or null for a line of white space. */
|
|
1055
|
+
function parseLine(bytes, start, end, row) {
|
|
1056
|
+
let text;
|
|
1057
|
+
try {
|
|
1058
|
+
text = decoder.decode(bytes.subarray(start, end));
|
|
1059
|
+
}
|
|
1060
|
+
catch {
|
|
1061
|
+
throw new TableProblem(`/rows/${row}`, "The line is not UTF-8 text (§2.2).");
|
|
1062
|
+
}
|
|
1063
|
+
let value;
|
|
1064
|
+
try {
|
|
1065
|
+
value = JSON.parse(text);
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
if (text.trim() === "")
|
|
1069
|
+
return null;
|
|
1070
|
+
throw new TableProblem(`/rows/${row}`, `The line is not JSON: ${error.message}`);
|
|
1071
|
+
}
|
|
1072
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1073
|
+
throw new TableProblem(`/rows/${row}`, "A line of JSON Lines is an object of the row's values by column name.");
|
|
1074
|
+
}
|
|
1075
|
+
return value;
|
|
1076
|
+
}
|
|
1077
|
+
/** The types that a number of JSON fits. */
|
|
1078
|
+
function numberFits(value) {
|
|
1079
|
+
if (!Number.isInteger(value))
|
|
1080
|
+
return FLOAT;
|
|
1081
|
+
if (value >= -0x8000_0000 && value <= 0x7fff_ffff)
|
|
1082
|
+
return INTEGER | BIGINT | FLOAT;
|
|
1083
|
+
return Number.isSafeInteger(value) ? BIGINT | FLOAT : FLOAT;
|
|
1084
|
+
}
|
|
1085
|
+
/** The text of a value of JSON in a string column: strings as they are, other values as JSON. */
|
|
1086
|
+
function jsonText(value) {
|
|
1087
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1088
|
+
}
|
|
1089
|
+
/** Reads a file of JSON Lines the first time: its columns by first appearance, with what their values fit, and its rows. */
|
|
1090
|
+
async function surveyJson(file, chunkBytes) {
|
|
1091
|
+
const columns = [];
|
|
1092
|
+
const index = new Map();
|
|
1093
|
+
const starts = [];
|
|
1094
|
+
let rows = 0;
|
|
1095
|
+
let bytes = new Uint8Array(0);
|
|
1096
|
+
let base = 0;
|
|
1097
|
+
const line = (start, end) => {
|
|
1098
|
+
if (start === end)
|
|
1099
|
+
return;
|
|
1100
|
+
const object = parseLine(bytes, start, end, rows);
|
|
1101
|
+
if (!object)
|
|
1102
|
+
return;
|
|
1103
|
+
if (rows % BLOCK_ROWS === 0)
|
|
1104
|
+
starts.push(base + start);
|
|
1105
|
+
for (const key in object) {
|
|
1106
|
+
let at = index.get(key);
|
|
1107
|
+
if (at === undefined) {
|
|
1108
|
+
if (utf8Length(key) < 0)
|
|
1109
|
+
throw new TableProblem(`/rows/${rows}`, "A name holds a lone surrogate (§2.2).");
|
|
1110
|
+
at = columns.length;
|
|
1111
|
+
index.set(key, at);
|
|
1112
|
+
columns.push(found(key));
|
|
1113
|
+
}
|
|
1114
|
+
const value = object[key];
|
|
1115
|
+
if (value === null)
|
|
1116
|
+
continue;
|
|
1117
|
+
const column = columns[at];
|
|
1118
|
+
column.values++;
|
|
1119
|
+
if (typeof value === "object") {
|
|
1120
|
+
column.fits = 0;
|
|
1121
|
+
column.nested++;
|
|
1122
|
+
column.bytes += utf8Length(JSON.stringify(value));
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof value === "string") {
|
|
1126
|
+
const length = utf8Length(value);
|
|
1127
|
+
if (length < 0) {
|
|
1128
|
+
throw new TableProblem(`/rows/${rows}/${at}`, "The string holds a lone surrogate, which is not Unicode text (§2.2).");
|
|
1129
|
+
}
|
|
1130
|
+
column.bytes += length;
|
|
1131
|
+
if (column.fits !== 0)
|
|
1132
|
+
column.fits &= temporalText(value);
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
column.bytes += jsonText(value).length;
|
|
1136
|
+
column.fits &= typeof value === "boolean" ? BOOLEAN : numberFits(value);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
rows++;
|
|
1140
|
+
};
|
|
1141
|
+
await eachSlice(file, chunkBytes, (slice, last, at) => {
|
|
1142
|
+
bytes = slice;
|
|
1143
|
+
base = at;
|
|
1144
|
+
return eachLine(slice, last, line);
|
|
1145
|
+
});
|
|
1146
|
+
return { separator: 0, found: columns, rows, starts };
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Reads a file of JSON Lines the second time, or a span of it, keeping its values in the columns that are not null;
|
|
1150
|
+
* gives the rows read.
|
|
1151
|
+
*/
|
|
1152
|
+
async function keepJson(file, built, rowCount, chunkBytes, span) {
|
|
1153
|
+
const index = new Map(built.flatMap((column, at) => (column ? [[column.column.name, at]] : [])));
|
|
1154
|
+
const strings = built.filter((column) => column?.offsets);
|
|
1155
|
+
let rows = 0;
|
|
1156
|
+
let bytes = new Uint8Array(0);
|
|
1157
|
+
const line = (start, end) => {
|
|
1158
|
+
if (start === end)
|
|
1159
|
+
return;
|
|
1160
|
+
const object = parseLine(bytes, start, end, rows);
|
|
1161
|
+
if (!object)
|
|
1162
|
+
return;
|
|
1163
|
+
if (rows === rowCount)
|
|
1164
|
+
throw changed();
|
|
1165
|
+
for (const key in object) {
|
|
1166
|
+
const value = object[key];
|
|
1167
|
+
const at = index.get(key);
|
|
1168
|
+
if (value === null || at === undefined)
|
|
1169
|
+
continue;
|
|
1170
|
+
const column = built[at];
|
|
1171
|
+
switch (column.column.type) {
|
|
1172
|
+
case "boolean":
|
|
1173
|
+
column.flags[rows] = value ? 1 : 0;
|
|
1174
|
+
break;
|
|
1175
|
+
case "integer":
|
|
1176
|
+
case "float":
|
|
1177
|
+
column.numbers[rows] = value;
|
|
1178
|
+
break;
|
|
1179
|
+
case "bigint":
|
|
1180
|
+
putInt64(column, rows, value);
|
|
1181
|
+
break;
|
|
1182
|
+
case "string": {
|
|
1183
|
+
const { written } = encoder.encodeInto(jsonText(value), column.text.subarray(column.cursor));
|
|
1184
|
+
column.cursor += written;
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
default:
|
|
1188
|
+
if (!temporalText(value))
|
|
1189
|
+
throw changed();
|
|
1190
|
+
putTemporal(column, rows);
|
|
1191
|
+
}
|
|
1192
|
+
mark(column, rows);
|
|
1193
|
+
}
|
|
1194
|
+
for (const column of strings)
|
|
1195
|
+
column.offsets[rows + 1] = column.cursor;
|
|
1196
|
+
rows++;
|
|
1197
|
+
};
|
|
1198
|
+
await eachSlice(file, chunkBytes, (slice, last) => {
|
|
1199
|
+
bytes = slice;
|
|
1200
|
+
return eachLine(slice, last, line);
|
|
1201
|
+
}, span);
|
|
1202
|
+
return rows;
|
|
1203
|
+
}
|
|
1204
|
+
//# sourceMappingURL=text.js.map
|