vectrojs 0.0.1 → 0.1.1
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/CHANGELOG.md +89 -0
- package/README.md +828 -4
- package/bin/vectro.mjs +82 -0
- package/dist/vectro.wasm +0 -0
- package/mcp/server.mjs +288 -0
- package/package.json +84 -6
- package/src/arrow.mjs +905 -0
- package/src/bitpack.mjs +135 -0
- package/src/browser.mjs +129 -0
- package/src/bytes.mjs +60 -0
- package/src/csv-kernel.mjs +205 -0
- package/src/csv.mjs +270 -0
- package/src/dataset.mjs +1763 -0
- package/src/dates.mjs +139 -0
- package/src/emitter.mjs +109 -0
- package/src/engine.mjs +881 -0
- package/src/files-browser.mjs +104 -0
- package/src/files-node.mjs +59 -0
- package/src/frames.mjs +191 -0
- package/src/fuse.mjs +512 -0
- package/src/interp.mjs +305 -0
- package/src/join.mjs +534 -0
- package/src/kernels.mjs +312 -0
- package/src/ndjson.mjs +136 -0
- package/src/node.mjs +120 -0
- package/src/pack.mjs +114 -0
- package/src/parallel-sort.mjs +99 -0
- package/src/partition-kernels.mjs +388 -0
- package/src/partition.mjs +395 -0
- package/src/plan.mjs +551 -0
- package/src/portable.mjs +21 -0
- package/src/rank-kernels.mjs +81 -0
- package/src/rows.mjs +91 -0
- package/src/serve/catalog.mjs +148 -0
- package/src/serve/flight.mjs +270 -0
- package/src/serve/formats.mjs +87 -0
- package/src/serve/http.mjs +182 -0
- package/src/serve/index.mjs +9 -0
- package/src/serve/odata.mjs +291 -0
- package/src/serve/pg.mjs +588 -0
- package/src/serve/pgcatalog.mjs +220 -0
- package/src/serve/pgtypes.mjs +199 -0
- package/src/sketch.mjs +75 -0
- package/src/sort-kernels.mjs +117 -0
- package/src/sort.mjs +102 -0
- package/src/split-worker-core.mjs +252 -0
- package/src/split-worker-node.mjs +8 -0
- package/src/split-worker.mjs +7 -0
- package/src/split.mjs +741 -0
- package/src/sql.mjs +1558 -0
- package/src/sqltext.mjs +132 -0
- package/src/static-kernels.mjs +164 -0
- package/src/stream-worker.mjs +553 -0
- package/src/table-kernels.mjs +323 -0
- package/src/table.mjs +2247 -0
- package/src/textmap.mjs +159 -0
- package/src/vectors.mjs +138 -0
- package/src/wasm.mjs +254 -0
- package/src/window.mjs +160 -0
- package/src/worker-browser.mjs +21 -0
- package/src/worker-loop.mjs +106 -0
- package/src/worker.mjs +22 -0
- package/src/zones.mjs +75 -0
package/src/bitpack.mjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Bit-packed codes, for datasets on disk. Packed columns (pack.mjs) keep
|
|
2
|
+
// their codes in 1, 2 or 4 bytes so the kernels can read them directly; on
|
|
3
|
+
// disk each code takes only the bits it needs (a quantity 1..9: 4 bits, a
|
|
4
|
+
// price in cents up to 999.99: 17), so a query reads fewer bytes. Reading a
|
|
5
|
+
// chunk unpacks them back to bytes on every thread.
|
|
6
|
+
//
|
|
7
|
+
// A column's codes are stored as (code - base) in `bits` bits, little-endian
|
|
8
|
+
// in 32-bit words; with missing values, the all-ones value means missing.
|
|
9
|
+
|
|
10
|
+
import { I32, Fn, buildModule, get, set, i32c, block, loop, end, br, brIf, i32add, i32geu, i32wrapI64, memarg } from './wasm.mjs';
|
|
11
|
+
|
|
12
|
+
const I64 = 0x7e;
|
|
13
|
+
const I64_EXTEND_U = [0xad], I64_MUL = [0x7e], I64_SHR_U = [0x88], I64_AND = [0x83], I64_SHL = [0x86], I64_SUB = [0x7d], I64_EQ = [0x51];
|
|
14
|
+
const I64_LOAD = [0x29, 0, 0], SELECT = [0x1b];
|
|
15
|
+
const STORE = { 1: [0x3a, 0, 0], 2: [0x3b, 1, 0], 4: [0x36, 2, 0] }; // i32.store8 / store16 / store
|
|
16
|
+
const SHIFT = { 1: 0, 2: 1, 4: 2 };
|
|
17
|
+
|
|
18
|
+
// The fewest bits that hold 0..max (1..32).
|
|
19
|
+
export function bitsFor(max) {
|
|
20
|
+
let b = 1;
|
|
21
|
+
while (b < 32 && max >= 2 ** b) b++;
|
|
22
|
+
return b;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// How a column's codes would be stored: { bits, base, missing } where
|
|
26
|
+
// `missing` is the codes' NaN code if any row has it; or null when packing to
|
|
27
|
+
// the bit saves nothing over `byteBits`.
|
|
28
|
+
export function bitPlan(codes, n, nanCode, byteBits) {
|
|
29
|
+
let lo = Infinity, hi = -Infinity, missing = false;
|
|
30
|
+
for (let i = 0; i < n; i++) {
|
|
31
|
+
const c = codes[i];
|
|
32
|
+
if (c === nanCode) { missing = true; continue; }
|
|
33
|
+
if (c < lo) lo = c;
|
|
34
|
+
if (c > hi) hi = c;
|
|
35
|
+
}
|
|
36
|
+
if (lo === Infinity) lo = hi = 0;
|
|
37
|
+
const bits = bitsFor(hi - lo + (missing ? 1 : 0));
|
|
38
|
+
return bits < byteBits ? { bits, base: lo, missing: missing ? nanCode : null } : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// codes[0..n) as bytes (whole 32-bit words).
|
|
42
|
+
export function packBits(codes, n, { bits, base, missing }) {
|
|
43
|
+
const words = new Uint32Array(Math.ceil((n * bits) / 32));
|
|
44
|
+
const ones = bits === 32 ? 0xffffffff : 2 ** bits - 1;
|
|
45
|
+
for (let i = 0, pos = 0; i < n; i++, pos += bits) {
|
|
46
|
+
const c = codes[i];
|
|
47
|
+
const v = missing !== null && c === missing ? ones : c - base;
|
|
48
|
+
const w = pos >>> 5, sh = pos & 31;
|
|
49
|
+
words[w] |= v << sh;
|
|
50
|
+
if (sh + bits > 32) words[w + 1] |= v >>> (32 - sh);
|
|
51
|
+
}
|
|
52
|
+
return new Uint8Array(words.buffer);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The same in reverse, in plain JS (engines without compiled kernels).
|
|
56
|
+
export function unpackBits(bytes, n, { bits, base, missing }, out) {
|
|
57
|
+
const words = new Uint32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >>> 2);
|
|
58
|
+
const ones = bits === 32 ? 0xffffffff : 2 ** bits - 1;
|
|
59
|
+
for (let i = 0, pos = 0; i < n; i++, pos += bits) {
|
|
60
|
+
const w = pos >>> 5, sh = pos & 31;
|
|
61
|
+
let v = words[w] >>> sh;
|
|
62
|
+
if (sh + bits > 32) v |= words[w + 1] << (32 - sh);
|
|
63
|
+
v = bits === 32 ? v >>> 0 : (v & ones) >>> 0;
|
|
64
|
+
out[i] = missing !== null && v === ones ? missing : v + base;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// unpack(start, end, unit, src, dst, base, hasMissing, missingCode):
|
|
69
|
+
// dst[i] = the code of row i, for i in [start, end), as `width`-byte
|
|
70
|
+
// integers, for codes of `bits` bits (compiled in). Eight rows always take
|
|
71
|
+
// `bits` bytes, so within a group of eight every row's byte offset and shift
|
|
72
|
+
// are constants: the body is unrolled (a 64-bit load, shift, mask, store per
|
|
73
|
+
// row). Rows before the first whole group and after the last take a general
|
|
74
|
+
// loop.
|
|
75
|
+
export function unpackModule(width, bits, shared) {
|
|
76
|
+
const f = new Fn('k', [I32, I32, I32, I32, I32, I32, I32, I32], []);
|
|
77
|
+
const [start, stop, , src, dst, base, hasMissing, missingCode] = [0, 1, 2, 3, 4, 5, 6, 7];
|
|
78
|
+
const i = f.local(I32), pos = f.local(I64), v = f.local(I64), at = f.local(I32), out = f.local(I32), limit = f.local(I32);
|
|
79
|
+
const mask = i64c(BigInt(2 ** bits - 1));
|
|
80
|
+
const store = (offset) => [STORE[width][0], STORE[width][1], ...memarg(0, offset).slice(1)];
|
|
81
|
+
// [address on the stack] value (i64, shifted) -> masked; missing mapped; stored
|
|
82
|
+
const finish = (offset) => [...mask, ...I64_AND, ...set(v),
|
|
83
|
+
...get(missingCode), ...get(v), ...i32wrapI64, ...get(base), ...i32add,
|
|
84
|
+
...get(v), ...mask, ...I64_EQ, ...get(hasMissing), 0x71, ...SELECT, ...store(offset)];
|
|
85
|
+
const general = (lim) => [...block, ...loop, ...get(i), ...get(lim), ...i32geu, ...brIf(1),
|
|
86
|
+
...get(i), ...i32c(SHIFT[width]), 0x74, ...get(dst), ...i32add, // address of dst[i]
|
|
87
|
+
...get(i), ...I64_EXTEND_U, ...i64c(BigInt(bits)), ...I64_MUL, ...set(pos), // bit position
|
|
88
|
+
...get(src), ...get(pos), 0x42, 3, ...I64_SHR_U, ...i32wrapI64, ...i32add, ...I64_LOAD,
|
|
89
|
+
...get(pos), 0x42, 7, ...I64_AND, ...I64_SHR_U, ...finish(0),
|
|
90
|
+
...get(i), ...i32c(1), ...i32add, ...set(i), ...br(0), ...end, ...end];
|
|
91
|
+
// head: rows up to the first multiple of 8 (or the end)
|
|
92
|
+
f.emit(get(start), set(i));
|
|
93
|
+
f.emit(get(start), i32c(7), i32add, i32c(-8), [0x71], set(limit));
|
|
94
|
+
f.emit(get(limit), get(stop), get(limit), get(stop), [0x49], SELECT, set(limit)); // min (i32.lt_u)
|
|
95
|
+
f.emit(general(limit));
|
|
96
|
+
// whole groups of eight: at = src + i / 8 * bits, out = dst + i * width
|
|
97
|
+
f.emit(get(stop), i32c(-8), [0x71], set(limit));
|
|
98
|
+
f.emit(get(i), i32c(3), [0x76], i32c(bits), [0x6c], get(src), i32add, set(at));
|
|
99
|
+
f.emit(get(i), i32c(SHIFT[width]), [0x74], get(dst), i32add, set(out));
|
|
100
|
+
f.emit(block, loop, get(i), get(limit), i32geu, brIf(1));
|
|
101
|
+
for (let j = 0; j < 8; j++) {
|
|
102
|
+
const b = j * bits;
|
|
103
|
+
f.emit(get(out), get(at), [0x29, ...memarg(0, b >> 3)], [0x42, b & 7], I64_SHR_U, finish(j * width));
|
|
104
|
+
}
|
|
105
|
+
f.emit(get(at), i32c(bits), i32add, set(at), get(out), i32c(8 * width), i32add, set(out), get(i), i32c(8), i32add, set(i), br(0), end, end);
|
|
106
|
+
// tail
|
|
107
|
+
f.emit(general(stop));
|
|
108
|
+
return buildModule([f], shared);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function i64c(x) { // i64.const, signed LEB128
|
|
112
|
+
const o = [0x42];
|
|
113
|
+
for (;;) {
|
|
114
|
+
const b = Number(x & 0x7fn);
|
|
115
|
+
x >>= 7n;
|
|
116
|
+
if ((x === 0n && !(b & 0x40)) || (x === -1n && b & 0x40)) { o.push(b); return o; }
|
|
117
|
+
o.push(b | 0x80);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// seq(start, end, unit, dst, first, step): dst[i] = first + i * step as
|
|
122
|
+
// `width`-byte integers (a column that counts up at a fixed step, rebuilt on
|
|
123
|
+
// every thread; first already includes the piece's first row).
|
|
124
|
+
export function seqModule(width, shared) {
|
|
125
|
+
const f = new Fn('k', [I32, I32, I32, I32, I32, I32], []);
|
|
126
|
+
const [start, stop, , dst, first, step] = [0, 1, 2, 3, 4, 5];
|
|
127
|
+
const i = f.local(I32), v = f.local(I32), out = f.local(I32);
|
|
128
|
+
f.emit(get(start), set(i));
|
|
129
|
+
f.emit(get(i), get(step), [0x6c], get(first), i32add, set(v));
|
|
130
|
+
f.emit(get(i), i32c(SHIFT[width]), [0x74], get(dst), i32add, set(out));
|
|
131
|
+
f.emit(block, loop, get(i), get(stop), i32geu, brIf(1));
|
|
132
|
+
f.emit(get(out), get(v), STORE[width]);
|
|
133
|
+
f.emit(get(v), get(step), i32add, set(v), get(out), i32c(width), i32add, set(out), get(i), i32c(1), i32add, set(i), br(0), end, end);
|
|
134
|
+
return buildModule([f], shared);
|
|
135
|
+
}
|
package/src/browser.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Browser entry. On cross-origin-isolated pages (served with
|
|
2
|
+
// Cross-Origin-Opener-Policy: same-origin
|
|
3
|
+
// Cross-Origin-Embedder-Policy: require-corp)
|
|
4
|
+
// the engine runs Web Workers on every core: the async API (aggAsync,
|
|
5
|
+
// groupBy().aggAsync, collectAsync, ...) spreads queries across them while the
|
|
6
|
+
// page stays responsive. Sync calls still work, on the page's thread only
|
|
7
|
+
// (the page's main thread is not allowed to block and wait for workers).
|
|
8
|
+
// Other pages get the single-thread engine, which needs no headers.
|
|
9
|
+
//
|
|
10
|
+
// import { Engine } from 'vectrojs/browser';
|
|
11
|
+
// const engine = new Engine();
|
|
12
|
+
// const orders = await engine.loadCSV('/orders.csv');
|
|
13
|
+
// await orders.filter("region == 'EU'").sumAsync('price * qty');
|
|
14
|
+
import { Engine as CoreEngine } from './engine.mjs';
|
|
15
|
+
import { openDatasetAsync, writeDatasetAsync, csvToDatasetAsync, createDatasetAsync } from './dataset.mjs';
|
|
16
|
+
import { writableFile, storedFiles, storedFile, removeStored, storageAvailable, opfsTemp } from './files-browser.mjs';
|
|
17
|
+
import { setTempStorage } from './dataset.mjs';
|
|
18
|
+
if (storageAvailable()) setTempStorage(opfsTemp); // (huge group-bys, sorts and joins spill to browser storage)
|
|
19
|
+
|
|
20
|
+
export const browserPlatform = {
|
|
21
|
+
canBlock: false,
|
|
22
|
+
cpus: () => navigator.hardwareConcurrency || 4,
|
|
23
|
+
spawn(workerData) {
|
|
24
|
+
const { port1, port2 } = new MessageChannel();
|
|
25
|
+
const worker = new Worker(new URL('./worker-browser.mjs', import.meta.url), { type: 'module' });
|
|
26
|
+
worker.postMessage({ ...workerData, port: port2 }, [port2]);
|
|
27
|
+
// Messages from the worker arrive asynchronously; keep them until read.
|
|
28
|
+
port1.inbox = [];
|
|
29
|
+
port1.waiters = [];
|
|
30
|
+
port1.onmessage = (ev) => {
|
|
31
|
+
port1.inbox.push(ev.data);
|
|
32
|
+
for (const w of port1.waiters.splice(0)) w();
|
|
33
|
+
};
|
|
34
|
+
return { worker, port: port1 };
|
|
35
|
+
},
|
|
36
|
+
// A message that has already arrived, or undefined.
|
|
37
|
+
receive: (port) => (port.inbox.length ? { message: port.inbox.shift() } : undefined),
|
|
38
|
+
// Resolves when any of the ports receives a message.
|
|
39
|
+
nextMessage: (ports) => new Promise((resolve) => { for (const p of ports) p.waiters.push(resolve); }),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const threadsAvailable = () => Boolean(globalThis.crossOriginIsolated) && typeof SharedArrayBuffer === 'function';
|
|
43
|
+
|
|
44
|
+
export class Engine extends CoreEngine {
|
|
45
|
+
// spin: 0, workers sleep as soon as a task is done. Spinning only pays off
|
|
46
|
+
// when the caller blocks (Node); a page awaits anyway, and in Safari seven
|
|
47
|
+
// spinning workers starved the page: 4x slower queries, 9-40 ms stalls.
|
|
48
|
+
constructor(opts = {}) {
|
|
49
|
+
super({ platform: threadsAvailable() ? browserPlatform : null, spin: 0, ...opts });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fetch a CSV and parse it (on the workers when there are any).
|
|
53
|
+
async loadCSV(url, opts) {
|
|
54
|
+
const res = await fetch(url);
|
|
55
|
+
if (!res.ok) throw new Error(`vectro: ${url}: HTTP ${res.status}`);
|
|
56
|
+
return this.parseCSVAsync(new Uint8Array(await res.arrayBuffer()), opts);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A dataset (a .vectro table of any size, see dataset.mjs) from a File the
|
|
60
|
+
// user picked or dropped, a Blob, or a URL (read with HTTP range requests):
|
|
61
|
+
// only the parts a query needs are read. Query it with the async calls.
|
|
62
|
+
// const orders = await engine.openDataset(input.files[0]);
|
|
63
|
+
// await sql('SELECT region, COUNT(*) FROM orders GROUP BY region', { orders }).toRowsAsync();
|
|
64
|
+
openDataset(source) { return openDatasetAsync(this, source); }
|
|
65
|
+
|
|
66
|
+
// Writing datasets in the browser: into its file storage (OPFS) by name,
|
|
67
|
+
// or to a file the user chose (a FileSystemFileHandle), or memoryFile().
|
|
68
|
+
// Big inputs stream: each chunk is on disk before the next is made.
|
|
69
|
+
// await engine.csvToDataset(droppedCsvFile, 'orders.vectro'); // a CSV of any size
|
|
70
|
+
// await engine.writeDataset('eu.vectro', orders.filter("region == 'EU'"));
|
|
71
|
+
// const orders = await engine.openStoredDataset('orders.vectro');
|
|
72
|
+
async writeDataset(target, source, opts) { return writeDatasetAsync(this, await target_(target, opts), source, opts); }
|
|
73
|
+
async csvToDataset(csv, target, opts) { return csvToDatasetAsync(this, csv, await target_(target, opts), opts); }
|
|
74
|
+
// A writer: appendAsync(table or columns) as often as needed, then closeAsync().
|
|
75
|
+
async createDataset(target, opts) { return createDatasetAsync(this, await target_(target, opts), opts); }
|
|
76
|
+
async storedDatasets() { return (await storedFiles()).filter((f) => f.name.endsWith('.vectro')); }
|
|
77
|
+
async openStoredDataset(name) { return openDatasetAsync(this, await storedFile(name)); }
|
|
78
|
+
storedDatasetFile(name) { return storedFile(name); } // (a File: to download, or to open)
|
|
79
|
+
removeStoredDataset(name) { return removeStored(name); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// a name in browser storage or a FileSystemFileHandle -> a writable file; file objects as they are
|
|
83
|
+
const target_ = (t, opts) => (typeof t === 'string' || typeof t?.createWritable === 'function' ? writableFile(t, { append: opts?.append }) : t);
|
|
84
|
+
|
|
85
|
+
// ---- split tables: multi-core on pages WITHOUT isolation headers ------------
|
|
86
|
+
// No shared memory: each Web Worker owns a slice of every table and answers
|
|
87
|
+
// for it; the page combines the partial results (see split.mjs). Async API only.
|
|
88
|
+
//
|
|
89
|
+
// import { SplitEngine } from 'vectrojs/browser';
|
|
90
|
+
// const engine = new SplitEngine(); // one worker per core
|
|
91
|
+
// const orders = await engine.loadCSV('/orders.csv');
|
|
92
|
+
// await orders.filter("region == 'EU'").sumAsync('price * qty');
|
|
93
|
+
import { SplitEngine as BaseSplitEngine } from './split.mjs';
|
|
94
|
+
|
|
95
|
+
export function spawnSplitWorker() {
|
|
96
|
+
const w = new Worker(new URL('./split-worker.mjs', import.meta.url), { type: 'module' });
|
|
97
|
+
return {
|
|
98
|
+
post: (msg, transfer) => w.postMessage(msg, transfer ?? []),
|
|
99
|
+
onMessage: (fn) => { w.onmessage = (ev) => fn(ev.data); },
|
|
100
|
+
onError: (fn) => {
|
|
101
|
+
w.onerror = (ev) => { ev.preventDefault?.(); fn(new Error(ev.message || 'worker error')); };
|
|
102
|
+
w.onmessageerror = () => fn(new Error('a message from the worker could not be read'));
|
|
103
|
+
},
|
|
104
|
+
terminate: () => w.terminate(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class SplitEngine extends BaseSplitEngine {
|
|
109
|
+
constructor(opts = {}) {
|
|
110
|
+
super({ workers: navigator.hardwareConcurrency || 4, spawn: spawnSplitWorker, ...opts });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async loadCSV(url, opts) {
|
|
114
|
+
const res = await fetch(url);
|
|
115
|
+
if (!res.ok) throw new Error(`vectro: ${url}: HTTP ${res.status}`);
|
|
116
|
+
return this.parseCSVAsync(new Uint8Array(await res.arrayBuffer()), opts);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { canCompileWasm } from './engine.mjs';
|
|
121
|
+
export { Table } from './table.mjs';
|
|
122
|
+
|
|
123
|
+
export { VectorIndex } from './vectors.mjs';
|
|
124
|
+
export { BatchEncoder, FrameReader, encodeSchema, encodeEnd } from './frames.mjs';
|
|
125
|
+
export { readArrow, writeArrow, ArrowReader, ArrowEncoder } from './arrow.mjs';
|
|
126
|
+
export { DistinctSketch } from './sketch.mjs';
|
|
127
|
+
export { sql } from './sql.mjs';
|
|
128
|
+
export { Dataset, openDatasetAsync, blobFile, urlFile, writeDatasetAsync, csvToDatasetAsync, createDatasetAsync } from './dataset.mjs';
|
|
129
|
+
export { memoryFile, writableFile, storageAvailable } from './files-browser.mjs';
|
package/src/bytes.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Text and byte helpers that work everywhere: Node's Buffer when it exists
|
|
2
|
+
// (fast, and accepts shared memory), standard TextDecoder otherwise.
|
|
3
|
+
|
|
4
|
+
const NodeBuffer = globalThis.Buffer;
|
|
5
|
+
const decoders = { utf8: new TextDecoder('utf-8'), latin1: new TextDecoder('latin1') };
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const isShared = (buf) => typeof SharedArrayBuffer === 'function' && buf instanceof SharedArrayBuffer;
|
|
8
|
+
|
|
9
|
+
// Decode bytes [s, e) of a Uint8Array.
|
|
10
|
+
export function decode(u8, s, e, enc = 'utf8') {
|
|
11
|
+
if (NodeBuffer) return NodeBuffer.from(u8.buffer, u8.byteOffset, u8.length).toString(enc, s, e);
|
|
12
|
+
const part = u8.subarray(s, e);
|
|
13
|
+
return decoders[enc].decode(isShared(u8.buffer) ? part.slice() : part); // TextDecoder refuses shared memory
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const encode = (str) => encoder.encode(str);
|
|
17
|
+
|
|
18
|
+
// Node's Buffer.indexOf uses memchr/memmem and is ~6x faster than
|
|
19
|
+
// TypedArray.indexOf, so it's used whenever Buffer exists.
|
|
20
|
+
const asBuffer = (u8) => NodeBuffer.from(u8.buffer, u8.byteOffset, u8.length);
|
|
21
|
+
|
|
22
|
+
// First index of `byte` in [s, e), or -1.
|
|
23
|
+
export function indexOfByte(u8, byte, s, e = u8.length) {
|
|
24
|
+
const i = NodeBuffer ? asBuffer(u8).indexOf(byte, s) : u8.indexOf(byte, s);
|
|
25
|
+
return i !== -1 && i < e ? i : -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Number of times `byte` occurs in [s, e).
|
|
29
|
+
export function countByte(u8, byte, s, e) {
|
|
30
|
+
let n = 0;
|
|
31
|
+
if (NodeBuffer) {
|
|
32
|
+
const b = asBuffer(u8);
|
|
33
|
+
for (let i = b.indexOf(byte, s); i !== -1 && i < e; i = b.indexOf(byte, i + 1)) n++;
|
|
34
|
+
} else {
|
|
35
|
+
for (let i = s; i < e; i++) if (u8[i] === byte) n++;
|
|
36
|
+
}
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Occurrences of the byte pattern starting inside [s, e) (overlapping matches count).
|
|
41
|
+
export function countPattern(u8, pat, s, e) {
|
|
42
|
+
const m = pat.length;
|
|
43
|
+
if (m === 0) return 0;
|
|
44
|
+
let n = 0;
|
|
45
|
+
if (NodeBuffer) {
|
|
46
|
+
const hi = Math.min(u8.length, e + m - 1);
|
|
47
|
+
if (hi <= s) return 0;
|
|
48
|
+
const b = NodeBuffer.from(u8.buffer, u8.byteOffset + s, hi - s), p = NodeBuffer.from(pat.buffer, pat.byteOffset, m);
|
|
49
|
+
for (let i = b.indexOf(p, 0); i !== -1 && i < e - s; i = b.indexOf(p, i + 1)) n++;
|
|
50
|
+
return n;
|
|
51
|
+
}
|
|
52
|
+
const first = pat[0], last = Math.min(e - 1, u8.length - m);
|
|
53
|
+
for (let i = s; i <= last; i++) {
|
|
54
|
+
if (u8[i] !== first) continue;
|
|
55
|
+
let k = 1;
|
|
56
|
+
while (k < m && u8[i + k] === pat[k]) k++;
|
|
57
|
+
if (k === m) n++;
|
|
58
|
+
}
|
|
59
|
+
return n;
|
|
60
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// CSV parsing that runs on every thread. The file's bytes are in shared
|
|
2
|
+
// memory; each unit of work is one chunk that starts at a row boundary.
|
|
3
|
+
|
|
4
|
+
import { decode, countByte } from './bytes.mjs';
|
|
5
|
+
|
|
6
|
+
const NL = 10, CR = 13, QUOTE = 34, SPACE = 32, MINUS = 45, PLUS = 43, DOT = 46;
|
|
7
|
+
|
|
8
|
+
// Free-text values are stored as one number: byte offset * 2^20 + length,
|
|
9
|
+
// with bit 19 marking a quoted value that contains "" escapes.
|
|
10
|
+
export const STR_SHIFT = 2 ** 20, STR_ESCAPED = 2 ** 19, STR_MAX_LEN = 2 ** 19 - 1;
|
|
11
|
+
export function decodeString(bytes, v) {
|
|
12
|
+
if (Number.isNaN(v)) return '';
|
|
13
|
+
const start = Math.floor(v / STR_SHIFT);
|
|
14
|
+
let len = v - start * STR_SHIFT;
|
|
15
|
+
const escaped = len >= STR_ESCAPED;
|
|
16
|
+
if (escaped) len -= STR_ESCAPED;
|
|
17
|
+
const s = decode(bytes, start, start + len);
|
|
18
|
+
return escaped ? s.replaceAll('""', '"') : s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Dictionary keyed by raw bytes, so repeated values never become JS strings:
|
|
22
|
+
// a string is made once per distinct value per chunk. `unescape` turns the
|
|
23
|
+
// raw bytes of an escaped value into its text (CSV: "" -> ").
|
|
24
|
+
export class ByteDict {
|
|
25
|
+
constructor(bytes, unescape = (str) => str.replaceAll('""', '"')) {
|
|
26
|
+
this.bytes = bytes;
|
|
27
|
+
this.unescape = unescape;
|
|
28
|
+
this.cap = 64;
|
|
29
|
+
this.slots = new Int32Array(this.cap).fill(-1);
|
|
30
|
+
this.hashes = new Uint32Array(this.cap);
|
|
31
|
+
this.starts = [];
|
|
32
|
+
this.lens = [];
|
|
33
|
+
this.strings = [];
|
|
34
|
+
}
|
|
35
|
+
code(s, e, escaped) {
|
|
36
|
+
const b = this.bytes;
|
|
37
|
+
let h = 2166136261;
|
|
38
|
+
for (let i = s; i < e; i++) h = Math.imul(h ^ b[i], 16777619);
|
|
39
|
+
h >>>= 0;
|
|
40
|
+
const mask = this.cap - 1;
|
|
41
|
+
for (let slot = h & mask; ; slot = (slot + 1) & mask) {
|
|
42
|
+
const c = this.slots[slot];
|
|
43
|
+
if (c === -1) return this.#insert(slot, h, s, e, escaped);
|
|
44
|
+
if (this.hashes[slot] === h && this.lens[c] === e - s && this.#same(this.starts[c], s, e - s)) return c;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
#same(a, b, len) {
|
|
48
|
+
const x = this.bytes;
|
|
49
|
+
for (let i = 0; i < len; i++) if (x[a + i] !== x[b + i]) return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
#insert(slot, h, s, e, escaped) {
|
|
53
|
+
const code = this.strings.length;
|
|
54
|
+
const str = decode(this.bytes, s, e);
|
|
55
|
+
this.strings.push(escaped ? this.unescape(str) : str);
|
|
56
|
+
this.starts.push(s);
|
|
57
|
+
this.lens.push(e - s);
|
|
58
|
+
this.slots[slot] = code;
|
|
59
|
+
this.hashes[slot] = h;
|
|
60
|
+
if (this.strings.length * 2 > this.cap) this.#grow();
|
|
61
|
+
return code;
|
|
62
|
+
}
|
|
63
|
+
#grow() {
|
|
64
|
+
const old = { slots: this.slots, hashes: this.hashes };
|
|
65
|
+
this.cap *= 2;
|
|
66
|
+
this.slots = new Int32Array(this.cap).fill(-1);
|
|
67
|
+
this.hashes = new Uint32Array(this.cap);
|
|
68
|
+
const mask = this.cap - 1;
|
|
69
|
+
for (let i = 0; i < old.slots.length; i++) {
|
|
70
|
+
const c = old.slots[i];
|
|
71
|
+
if (c === -1) continue;
|
|
72
|
+
let slot = old.hashes[i] & mask;
|
|
73
|
+
while (this.slots[slot] !== -1) slot = (slot + 1) & mask;
|
|
74
|
+
this.slots[slot] = c;
|
|
75
|
+
this.hashes[slot] = old.hashes[i];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const POW10 = Array.from({ length: 23 }, (_, i) => 10 ** i);
|
|
80
|
+
|
|
81
|
+
// Count quote characters (mode 0) or rows (mode 1) in chunk u.
|
|
82
|
+
// Rows are newlines outside quotes, plus a final line without a newline.
|
|
83
|
+
export function csvScan(u, A, sc) {
|
|
84
|
+
const bytes = A[0], bounds = A[1];
|
|
85
|
+
const start = bounds[u], end = bounds[u + 1];
|
|
86
|
+
if (end <= start) return 0;
|
|
87
|
+
const mode = sc[0], hasQuotes = sc[1];
|
|
88
|
+
if (mode === 0) return countByte(bytes, QUOTE, start, end);
|
|
89
|
+
let rows;
|
|
90
|
+
if (!hasQuotes) {
|
|
91
|
+
rows = countByte(bytes, NL, start, end); // native indexOf speed
|
|
92
|
+
} else {
|
|
93
|
+
rows = 0;
|
|
94
|
+
let inQuotes = false;
|
|
95
|
+
for (let i = start; i < end; i++) {
|
|
96
|
+
const c = bytes[i];
|
|
97
|
+
if (c === QUOTE) inQuotes = !inQuotes;
|
|
98
|
+
else if (c === NL && !inQuotes) rows++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return bytes[end - 1] === NL ? rows : rows + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Parse chunk u straight into the output columns.
|
|
105
|
+
// A = [bytes, bounds, rowStarts, fieldMap, kinds, ...columns]
|
|
106
|
+
// fieldMap[f] = output column for field f (-1 = skip)
|
|
107
|
+
// kinds[o] = 0 number, 1 category (dictionary codes), 2 free text (byte refs)
|
|
108
|
+
// sc = [delimiter, number of fields]
|
|
109
|
+
// Text columns get chunk-local codes; emit() reports each chunk's dictionary
|
|
110
|
+
// so the caller can renumber them into one shared dictionary.
|
|
111
|
+
export function csvParse(u, A, sc, emit) {
|
|
112
|
+
const bytes = A[0], bounds = A[1], rowStarts = A[2], fieldMap = A[3], kinds = A[4];
|
|
113
|
+
const cols = [];
|
|
114
|
+
for (let o = 0; o < kinds.length; o++) cols.push(A[5 + o]);
|
|
115
|
+
const delim = sc[0], nFields = sc[1];
|
|
116
|
+
const dicts = Array.from(kinds, (k) => (k === 1 ? new ByteDict(bytes) : null));
|
|
117
|
+
const end = bounds[u + 1];
|
|
118
|
+
let i = bounds[u], row = rowStarts[u];
|
|
119
|
+
|
|
120
|
+
while (i < end) {
|
|
121
|
+
let f = 0;
|
|
122
|
+
for (;;) {
|
|
123
|
+
let s, e, escaped = false;
|
|
124
|
+
if (i < end && bytes[i] === QUOTE) {
|
|
125
|
+
s = ++i;
|
|
126
|
+
for (;;) {
|
|
127
|
+
if (i >= end) { e = i; break; }
|
|
128
|
+
if (bytes[i] === QUOTE) {
|
|
129
|
+
if (bytes[i + 1] === QUOTE) { escaped = true; i += 2; continue; }
|
|
130
|
+
e = i++;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
while (i < end && bytes[i] !== delim && bytes[i] !== NL) i++;
|
|
136
|
+
} else {
|
|
137
|
+
s = i;
|
|
138
|
+
while (i < end) { const c = bytes[i]; if (c === delim || c === NL) break; i++; }
|
|
139
|
+
e = i;
|
|
140
|
+
if (e > s && bytes[e - 1] === CR) e--;
|
|
141
|
+
}
|
|
142
|
+
const o = f < nFields ? fieldMap[f] : -1;
|
|
143
|
+
if (o >= 0) {
|
|
144
|
+
const kind = kinds[o];
|
|
145
|
+
if (kind === 0) cols[o][row] = parseNumber(bytes, s, e);
|
|
146
|
+
else if (kind === 1) cols[o][row] = dicts[o].code(s, e, escaped);
|
|
147
|
+
else {
|
|
148
|
+
if (e - s > STR_MAX_LEN) throw new RangeError(`vectro: a text value in row ${row + 1} is longer than ${STR_MAX_LEN} bytes`);
|
|
149
|
+
cols[o][row] = s * STR_SHIFT + (e - s) + (escaped ? STR_ESCAPED : 0);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
f++;
|
|
153
|
+
if (i < end && bytes[i] === delim) { i++; continue; }
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
// missing trailing fields
|
|
157
|
+
for (; f < nFields; f++) {
|
|
158
|
+
const o = fieldMap[f];
|
|
159
|
+
if (o < 0) continue;
|
|
160
|
+
if (kinds[o] === 1) cols[o][row] = dicts[o].code(0, 0, false); // empty text
|
|
161
|
+
else cols[o][row] = NaN; // number, or free text (decodes to '')
|
|
162
|
+
}
|
|
163
|
+
i++; // past the newline
|
|
164
|
+
row++;
|
|
165
|
+
}
|
|
166
|
+
dicts.forEach((d, o) => { if (d) emit({ kind: 'csvdict', unit: u, col: o, strings: d.strings }); });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Exact decimal parsing for plain numbers (Clinger's fast path: an integer of
|
|
170
|
+
// at most 2^53 times or divided by an exact power of ten is correctly rounded,
|
|
171
|
+
// so the result is identical to Number()). Anything else goes to Number().
|
|
172
|
+
export function parseNumber(b, s, e) {
|
|
173
|
+
while (s < e && b[s] === SPACE) s++;
|
|
174
|
+
while (e > s && (b[e - 1] === SPACE || b[e - 1] === CR)) e--;
|
|
175
|
+
if (s === e) return NaN;
|
|
176
|
+
let i = s, neg = false;
|
|
177
|
+
if (b[i] === MINUS) { neg = true; i++; } else if (b[i] === PLUS) i++;
|
|
178
|
+
let m = 0, digits = 0, exp = 0, c;
|
|
179
|
+
while (i < e && (c = b[i] - 48) >= 0 && c <= 9) { m = m * 10 + c; digits++; i++; }
|
|
180
|
+
if (i < e && b[i] === DOT) {
|
|
181
|
+
i++;
|
|
182
|
+
while (i < e && (c = b[i] - 48) >= 0 && c <= 9) { m = m * 10 + c; digits++; exp--; i++; }
|
|
183
|
+
}
|
|
184
|
+
if (i < e && (b[i] | 32) === 101) { // e / E
|
|
185
|
+
i++;
|
|
186
|
+
let eneg = false, ev = 0, edigits = 0;
|
|
187
|
+
if (b[i] === MINUS) { eneg = true; i++; } else if (b[i] === PLUS) i++;
|
|
188
|
+
while (i < e && (c = b[i] - 48) >= 0 && c <= 9) { ev = ev * 10 + c; edigits++; i++; }
|
|
189
|
+
if (!edigits) return slowNumber(b, s, e);
|
|
190
|
+
exp += eneg ? -ev : ev;
|
|
191
|
+
}
|
|
192
|
+
if (i !== e || digits === 0 || m > 9007199254740991) return slowNumber(b, s, e);
|
|
193
|
+
let v;
|
|
194
|
+
if (exp === 0) v = m;
|
|
195
|
+
else if (exp > 0 && exp <= 22) v = m * POW10[exp];
|
|
196
|
+
else if (exp < 0 && exp >= -22) v = m / POW10[-exp];
|
|
197
|
+
else return slowNumber(b, s, e);
|
|
198
|
+
return neg ? -v : v;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function slowNumber(b, s, e) {
|
|
202
|
+
const str = decode(b, s, e, 'latin1');
|
|
203
|
+
const v = Number(str);
|
|
204
|
+
return str.trim() === '' ? NaN : v;
|
|
205
|
+
}
|