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/window.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Window functions over rows already sorted by (partition keys, order keys):
|
|
2
|
+
// each function gives one value per row, from the rows of its partition in
|
|
3
|
+
// its frame. Plain loops, one pass each (running and moving sums from prefix
|
|
4
|
+
// sums, moving min / max with a monotonic queue). Missing values (NaN) follow
|
|
5
|
+
// SQL: sum / avg / min / max skip them, count(x) counts values present.
|
|
6
|
+
//
|
|
7
|
+
// spec items: { fn, arg (Float64Array | null), frame, offset, def, k, p }
|
|
8
|
+
// fn: row_number rank dense_rank percent_rank cume_dist ntile
|
|
9
|
+
// lag lead first_value last_value nth_value
|
|
10
|
+
// sum avg min max count (count with arg null: rows)
|
|
11
|
+
// percentile_cont percentile_disc (p; arg sorted within the partition)
|
|
12
|
+
// frame: { unit: 'rows' | 'range', start, end } with start / end each
|
|
13
|
+
// { kind: 'unbounded' | 'preceding' | 'following' | 'current', n }
|
|
14
|
+
// (null: SQL's default: with ORDER BY, RANGE from the partition start to
|
|
15
|
+
// the current row and its ties; without, the whole partition)
|
|
16
|
+
|
|
17
|
+
// Partition starts: part[i] = first row of row i's partition; peers likewise
|
|
18
|
+
// for rows with equal order keys inside a partition (peers: false skips them,
|
|
19
|
+
// for functions that don't look at ties).
|
|
20
|
+
export function bounds(n, partKeys, orderKeys, peers = true) {
|
|
21
|
+
const newPart = new Uint8Array(n), partStart = new Int32Array(n), partEnd = new Int32Array(n);
|
|
22
|
+
if (n) newPart[0] = 1;
|
|
23
|
+
for (const k of partKeys) markChanges(k, newPart, n);
|
|
24
|
+
spans(newPart, partStart, partEnd, n);
|
|
25
|
+
if (!peers) return { partStart, partEnd, peerStart: partStart, peerEnd: partEnd };
|
|
26
|
+
const newPeer = newPart.slice(), peerStart = new Int32Array(n), peerEnd = new Int32Array(n);
|
|
27
|
+
for (const k of orderKeys) markChanges(k, newPeer, n);
|
|
28
|
+
spans(newPeer, peerStart, peerEnd, n);
|
|
29
|
+
return { partStart, partEnd, peerStart, peerEnd };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// mark[i] = 1 where k[i] differs from k[i - 1] (NaN equals NaN here)
|
|
33
|
+
function markChanges(k, mark, n) {
|
|
34
|
+
for (let i = 1; i < n; i++) { const a = k[i], b = k[i - 1]; if (a !== b && (a === a || b === b)) mark[i] = 1; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Each row's run [start, end) from the marks of where runs begin.
|
|
38
|
+
function spans(mark, start, end, n) {
|
|
39
|
+
let s = 0;
|
|
40
|
+
for (let i = 1; i <= n; i++) {
|
|
41
|
+
if (i === n || mark[i]) {
|
|
42
|
+
for (let j = s; j < i; j++) { start[j] = s; end[j] = i; }
|
|
43
|
+
s = i;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Whether some item reads peers (rows with equal order keys): ranks, and
|
|
49
|
+
// frames by RANGE (the default with ORDER BY counts the current row's ties).
|
|
50
|
+
const NO_PEERS = new Set(['row_number', 'ntile', 'lag', 'lead', 'percentile_cont', 'percentile_disc']);
|
|
51
|
+
export const readsPeers = (items) => items.some((it) => !NO_PEERS.has(it.fn) && it.frame?.unit !== 'rows');
|
|
52
|
+
|
|
53
|
+
// Each row's frame [lo[i], hi[i]) (row indexes, within its partition).
|
|
54
|
+
// orderKey: the single ORDER BY key, for RANGE with distances (desc: it falls).
|
|
55
|
+
function frames(n, b, frame, hasOrder, orderKey, desc) {
|
|
56
|
+
const lo = new Int32Array(n), hi = new Int32Array(n);
|
|
57
|
+
const f = frame ?? (hasOrder ? { unit: 'range', start: { kind: 'unbounded' }, end: { kind: 'current' } } : { unit: 'rows', start: { kind: 'unbounded' }, end: { kind: 'unbounded' } });
|
|
58
|
+
const { partStart: ps, partEnd: pe, peerStart: qs, peerEnd: qe } = b;
|
|
59
|
+
const byValue = f.unit === 'range' && (f.start.n !== undefined || f.end.n !== undefined);
|
|
60
|
+
if (byValue && !orderKey) throw new Error('vectro: RANGE with a distance needs exactly one ORDER BY key, a number or date');
|
|
61
|
+
for (let i = 0; i < n; i++) {
|
|
62
|
+
for (const [e, isEnd] of [[f.start, false], [f.end, true]]) {
|
|
63
|
+
let at;
|
|
64
|
+
if (e.kind === 'unbounded') at = isEnd ? pe[i] : ps[i];
|
|
65
|
+
else if (f.unit === 'rows') {
|
|
66
|
+
const j = e.kind === 'current' ? i : e.kind === 'preceding' ? i - e.n : i + e.n;
|
|
67
|
+
at = Math.min(pe[i], Math.max(ps[i], isEnd ? j + 1 : j));
|
|
68
|
+
} else if (e.kind === 'current') at = isEnd ? qe[i] : qs[i]; // (RANGE: the current row and its ties)
|
|
69
|
+
else continue; // (RANGE n PRECEDING / FOLLOWING: by value, below)
|
|
70
|
+
if (isEnd) hi[i] = at; else lo[i] = at;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (byValue) { // two pointers per partition over the keys, in their order
|
|
74
|
+
const sign = desc ? -1 : 1, off = (e) => (e.kind === 'preceding' ? -e.n : e.kind === 'following' ? e.n : 0);
|
|
75
|
+
for (let s = 0; s < n; s = pe[s]) {
|
|
76
|
+
const end = pe[s];
|
|
77
|
+
let a = s, z = s;
|
|
78
|
+
for (let i = s; i < end; i++) {
|
|
79
|
+
const v = sign * orderKey[i];
|
|
80
|
+
if (v !== v) { lo[i] = qs[i]; hi[i] = qe[i]; continue; } // (missing keys: their ties)
|
|
81
|
+
if (f.start.n !== undefined) { const t = v + off(f.start); while (a < end && sign * orderKey[a] < t) a++; lo[i] = a; }
|
|
82
|
+
if (f.end.n !== undefined) { const t = v + off(f.end); while (z < end && sign * orderKey[z] <= t) z++; hi[i] = z; }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < n; i++) if (hi[i] < lo[i]) hi[i] = lo[i]; // (an empty frame)
|
|
87
|
+
return { lo, hi };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The values of one window function, for every row.
|
|
91
|
+
export function windowValues(n, b, item, hasOrder, orderKey, desc) {
|
|
92
|
+
const out = new Float64Array(n);
|
|
93
|
+
const { partStart: ps, partEnd: pe, peerStart: qs, peerEnd: qe } = b;
|
|
94
|
+
const x = item.arg;
|
|
95
|
+
switch (item.fn) {
|
|
96
|
+
case 'row_number': for (let i = 0; i < n; i++) out[i] = i - ps[i] + 1; return out;
|
|
97
|
+
case 'rank': for (let i = 0; i < n; i++) out[i] = qs[i] - ps[i] + 1; return out;
|
|
98
|
+
case 'dense_rank': for (let i = 0, r = 0; i < n; i++) { if (i === ps[i]) r = 1; else if (qs[i] === i) r++; out[i] = r; } return out;
|
|
99
|
+
case 'percent_rank': for (let i = 0; i < n; i++) { const m = pe[i] - ps[i]; out[i] = m > 1 ? (qs[i] - ps[i]) / (m - 1) : 0; } return out;
|
|
100
|
+
case 'cume_dist': for (let i = 0; i < n; i++) out[i] = (qe[i] - ps[i]) / (pe[i] - ps[i]); return out;
|
|
101
|
+
case 'ntile': for (let i = 0; i < n; i++) { // the first (m mod k) tiles get one row more
|
|
102
|
+
const m = pe[i] - ps[i], k = item.k, j = i - ps[i], small = Math.floor(m / k), big = m % k;
|
|
103
|
+
out[i] = j < big * (small + 1) ? Math.floor(j / (small + 1)) + 1 : big + Math.floor((j - big * (small + 1)) / Math.max(1, small)) + 1;
|
|
104
|
+
} return out;
|
|
105
|
+
case 'lag': case 'lead': {
|
|
106
|
+
const k = (item.fn === 'lag' ? -1 : 1) * (item.offset ?? 1);
|
|
107
|
+
for (let i = 0; i < n; i++) { const j = i + k; out[i] = j >= ps[i] && j < pe[i] ? x[j] : (item.def ?? NaN); }
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
case 'percentile_cont': case 'percentile_disc': { // over the partition's values present (sorted: the window orders by the value)
|
|
111
|
+
for (let s = 0; s < n; ) {
|
|
112
|
+
const e = pe[s];
|
|
113
|
+
let a = s, z = e; // present values: NaN sort last
|
|
114
|
+
while (z > a && x[z - 1] !== x[z - 1]) z--;
|
|
115
|
+
const m = z - a;
|
|
116
|
+
let v = NaN;
|
|
117
|
+
if (m) {
|
|
118
|
+
if (item.fn === 'percentile_disc') v = x[a + Math.max(0, Math.ceil(item.p * m) - 1)];
|
|
119
|
+
else { const r = item.p * (m - 1), lo = Math.floor(r); v = x[a + lo] + (r - lo) * ((x[a + Math.min(m - 1, lo + 1)]) - x[a + lo]); }
|
|
120
|
+
}
|
|
121
|
+
out.fill(v, s, e);
|
|
122
|
+
s = e;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// functions of the frame
|
|
128
|
+
const { lo, hi } = frames(n, b, item.frame, hasOrder, orderKey, desc);
|
|
129
|
+
switch (item.fn) {
|
|
130
|
+
case 'first_value': for (let i = 0; i < n; i++) out[i] = hi[i] > lo[i] ? x[lo[i]] : NaN; return out;
|
|
131
|
+
case 'last_value': for (let i = 0; i < n; i++) out[i] = hi[i] > lo[i] ? x[hi[i] - 1] : NaN; return out;
|
|
132
|
+
case 'nth_value': for (let i = 0; i < n; i++) { const j = lo[i] + item.k - 1; out[i] = j < hi[i] ? x[j] : NaN; } return out;
|
|
133
|
+
case 'sum': case 'avg': case 'count': {
|
|
134
|
+
// prefix sums of the values present, and of how many are present
|
|
135
|
+
const s = new Float64Array(n + 1), c = new Float64Array(n + 1);
|
|
136
|
+
for (let i = 0; i < n; i++) { const v = x ? x[i] : 1; const ok = v === v; s[i + 1] = s[i] + (ok ? v : 0); c[i + 1] = c[i] + (ok ? 1 : 0); }
|
|
137
|
+
for (let i = 0; i < n; i++) {
|
|
138
|
+
const cnt = c[hi[i]] - c[lo[i]], sum = s[hi[i]] - s[lo[i]];
|
|
139
|
+
out[i] = item.fn === 'count' ? cnt : item.fn === 'avg' ? (cnt ? sum / cnt : NaN) : cnt ? sum : NaN; // (sum of nothing: NULL)
|
|
140
|
+
}
|
|
141
|
+
if (item.fn === 'sum' && item.exactSmall !== false) { // short frames: add directly (prefix differences lose digits)
|
|
142
|
+
for (let i = 0; i < n; i++) if (hi[i] - lo[i] <= 64) { let t = 0, m = 0; for (let j = lo[i]; j < hi[i]; j++) { const v = x[j]; if (v === v) { t += v; m++; } } out[i] = m ? t : NaN; }
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
case 'min': case 'max': { // frames only move forward: a monotonic queue of candidates
|
|
147
|
+
const better = item.fn === 'min' ? (a, c) => a <= c : (a, c) => a >= c;
|
|
148
|
+
const q = new Int32Array(n);
|
|
149
|
+
let head = 0, tail = 0, added = 0, part = -1;
|
|
150
|
+
for (let i = 0; i < n; i++) {
|
|
151
|
+
if (ps[i] !== part) { part = ps[i]; head = tail = 0; added = ps[i]; }
|
|
152
|
+
while (added < hi[i]) { const v = x[added]; if (v === v) { while (tail > head && better(v, x[q[tail - 1]])) tail--; q[tail++] = added; } added++; }
|
|
153
|
+
while (tail > head && q[head] < lo[i]) head++;
|
|
154
|
+
out[i] = tail > head ? x[q[head]] : NaN;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`vectro: unknown window function ${item.fn}`);
|
|
160
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Browser Web Worker: runs the shared worker loop. Messages arrive only
|
|
2
|
+
// between event-loop turns, so they are queued here and the loop awaits them
|
|
3
|
+
// when a task needs one (pullAsync).
|
|
4
|
+
import { workerLoop } from './worker-loop.mjs';
|
|
5
|
+
|
|
6
|
+
self.onmessage = (first) => {
|
|
7
|
+
self.onmessage = null;
|
|
8
|
+
const data = first.data;
|
|
9
|
+
const { port } = data;
|
|
10
|
+
const inbox = [];
|
|
11
|
+
let wake = null;
|
|
12
|
+
port.onmessage = (ev) => {
|
|
13
|
+
inbox.push(ev.data);
|
|
14
|
+
if (wake) { const w = wake; wake = null; w(); }
|
|
15
|
+
};
|
|
16
|
+
workerLoop(data, {
|
|
17
|
+
post: (msg) => port.postMessage(msg),
|
|
18
|
+
pullSync(handle) { while (inbox.length) handle(inbox.shift()); },
|
|
19
|
+
pullAsync: () => new Promise((resolve) => { wake = resolve; }),
|
|
20
|
+
}).catch((err) => port.postMessage({ kind: 'error', message: err?.stack ?? String(err) }));
|
|
21
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// The worker side of the engine, shared by Node (worker.mjs) and browsers
|
|
2
|
+
// (worker-browser.mjs). A worker blocks on the control block between tasks
|
|
3
|
+
// (Atomics.wait is allowed in workers everywhere) and only needs messages for
|
|
4
|
+
// two things: newly compiled kernels and arrays outside engine memory.
|
|
5
|
+
//
|
|
6
|
+
// host = {
|
|
7
|
+
// post(msg) send a message to the main thread
|
|
8
|
+
// pullSync(handle) hand every message that has already arrived to handle()
|
|
9
|
+
// pullAsync() browsers: a promise for the next message (a browser
|
|
10
|
+
// worker only receives messages between event-loop turns);
|
|
11
|
+
// Node: undefined (messages can be read synchronously)
|
|
12
|
+
// }
|
|
13
|
+
import { C, OP, WASM_MEMORY, SEQ_WRITING, MAX_ARGS, TAG_SHIFT, decodeArgs, runUnits, tagOf } from './kernels.mjs';
|
|
14
|
+
|
|
15
|
+
export async function workerLoop({ ctrlBuf, scalarBuf, resultBuf, spin, module, memory, index }, host) {
|
|
16
|
+
const ctrl = new Int32Array(ctrlBuf);
|
|
17
|
+
const sc = new Float64Array(scalarBuf);
|
|
18
|
+
const res = new Float64Array(resultBuf);
|
|
19
|
+
const buffers = new Map(); // bufferId -> SharedArrayBuffer (arrays outside WASM memory)
|
|
20
|
+
const fusedModules = new Map(); // kernel id -> compiled WebAssembly.Module
|
|
21
|
+
const fusedKernels = new Map(); // kernel id -> this thread's instance of it
|
|
22
|
+
|
|
23
|
+
// Same compiled module, same shared memory as every other thread.
|
|
24
|
+
const W = new WebAssembly.Instance(module, { env: { memory } }).exports;
|
|
25
|
+
|
|
26
|
+
// memory.buffer is re-read each time so views see memory that grew since.
|
|
27
|
+
// The engine tells other threads about a grow asynchronously; grow(0) catches up.
|
|
28
|
+
function bufferFor(id, bytesNeeded) {
|
|
29
|
+
if (id !== WASM_MEMORY) return buffers.get(id);
|
|
30
|
+
if (memory.buffer.byteLength < bytesNeeded) memory.grow(0);
|
|
31
|
+
return memory.buffer;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handle = (msg) => {
|
|
35
|
+
if (msg.kind === 'kernel') fusedModules.set(msg.id, msg.module);
|
|
36
|
+
else buffers.set(msg.id, msg.buffer);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const usesKernel = (op) => op === OP.FUSED || op === OP.GROUP || op === OP.GROUP_RANGE || op === OP.UNITS || op === OP.TASKS || op === OP.COMPACT;
|
|
40
|
+
|
|
41
|
+
// Does the current task need a kernel or array we haven't received yet?
|
|
42
|
+
function missing(op) {
|
|
43
|
+
if (usesKernel(op) && !fusedModules.has(ctrl[C.KERNEL])) return true;
|
|
44
|
+
for (let k = 0; k < MAX_ARGS; k++) {
|
|
45
|
+
const id = ctrl[C.ARGS + k * 4];
|
|
46
|
+
if (id > WASM_MEMORY && !buffers.has(id)) return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fused(id) {
|
|
52
|
+
let k = fusedKernels.get(id);
|
|
53
|
+
if (!k) {
|
|
54
|
+
k = new WebAssembly.Instance(fusedModules.get(id), { env: { memory } }).exports.k;
|
|
55
|
+
fusedKernels.set(id, k);
|
|
56
|
+
}
|
|
57
|
+
return k;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let seen = 0;
|
|
61
|
+
for (;;) {
|
|
62
|
+
// Spin briefly (cheap wake-up for back-to-back calls), then sleep.
|
|
63
|
+
let s = Atomics.load(ctrl, C.SEQ);
|
|
64
|
+
for (let k = 0; (s === seen || s === SEQ_WRITING) && k < spin; k++) s = Atomics.load(ctrl, C.SEQ);
|
|
65
|
+
while (s === seen || s === SEQ_WRITING) {
|
|
66
|
+
if (s === seen) Atomics.wait(ctrl, C.SEQ, seen);
|
|
67
|
+
s = Atomics.load(ctrl, C.SEQ);
|
|
68
|
+
}
|
|
69
|
+
seen = s;
|
|
70
|
+
|
|
71
|
+
const op = ctrl[C.OP];
|
|
72
|
+
if (op === OP.EXIT) break;
|
|
73
|
+
|
|
74
|
+
// If we woke up late, the main thread may already be on a newer task and
|
|
75
|
+
// rewriting these fields. Reading them can then fail or mix two tasks, so
|
|
76
|
+
// re-check SEQ: a replaced task is skipped (claiming would fail anyway).
|
|
77
|
+
let A, F;
|
|
78
|
+
try {
|
|
79
|
+
host.pullSync(handle);
|
|
80
|
+
// A browser worker gets messages only between event-loop turns: if the
|
|
81
|
+
// task needs something that was sent but not yet received, step out of
|
|
82
|
+
// the loop until it arrives (the main thread sent it before the task).
|
|
83
|
+
while (host.pullAsync && missing(op) && Atomics.load(ctrl, C.SEQ) === s) {
|
|
84
|
+
await host.pullAsync();
|
|
85
|
+
host.pullSync(handle);
|
|
86
|
+
}
|
|
87
|
+
A = decodeArgs(ctrl, bufferFor);
|
|
88
|
+
F = usesKernel(op) ? fused(ctrl[C.KERNEL])
|
|
89
|
+
: op === OP.CSV_PARSE ? (msg) => host.post(msg) // results go back to the main thread
|
|
90
|
+
: op === OP.READ ? (jobs, u) => { // a file read straight into engine memory (Node: host.read)
|
|
91
|
+
const dst = jobs[u * 4 + 3], len = jobs[u * 4 + 2];
|
|
92
|
+
if (memory.buffer.byteLength < dst + len) memory.grow(0);
|
|
93
|
+
host.read(jobs[u * 4], new Uint8Array(memory.buffer, dst, len), jobs[u * 4 + 1]);
|
|
94
|
+
}
|
|
95
|
+
: null;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// a newer task is being written (SEQ changed), or ran without the
|
|
98
|
+
// workers (a fresh tag): these fields weren't our task's
|
|
99
|
+
if (Atomics.load(ctrl, C.SEQ) !== s || Atomics.load(ctrl, C.NEXT) >>> TAG_SHIFT !== tagOf(s)) continue;
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
if (Atomics.load(ctrl, C.SEQ) !== s) continue;
|
|
103
|
+
runUnits(op, ctrl[C.THREADS], ctrl[C.N], A, sc, ctrl, ctrl[C.WASM] ? W : null, res, tagOf(s), F, index,
|
|
104
|
+
(err) => host.post({ kind: 'error', message: err?.stack ?? String(err) }));
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/worker.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Node worker thread: runs the shared worker loop. Messages are read
|
|
2
|
+
// synchronously, so the loop never has to return to the event loop.
|
|
3
|
+
import { workerData, receiveMessageOnPort } from 'node:worker_threads';
|
|
4
|
+
import { readSync } from 'node:fs';
|
|
5
|
+
import { workerLoop } from './worker-loop.mjs';
|
|
6
|
+
|
|
7
|
+
const { port } = workerData;
|
|
8
|
+
workerLoop(workerData, {
|
|
9
|
+
post: (msg) => port.postMessage(msg),
|
|
10
|
+
pullSync(handle) {
|
|
11
|
+
let m;
|
|
12
|
+
while ((m = receiveMessageOnPort(port))) handle(m.message);
|
|
13
|
+
},
|
|
14
|
+
pullAsync: undefined,
|
|
15
|
+
read(fd, view, pos) { // (datasets: chunks are read on every thread)
|
|
16
|
+
for (let done = 0; done < view.length; ) {
|
|
17
|
+
const got = readSync(fd, view, done, view.length - done, pos + done);
|
|
18
|
+
if (got === 0) throw new Error('vectro: dataset file ended early');
|
|
19
|
+
done += got;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
});
|
package/src/zones.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Zone maps: the min and max of each block of rows, per column. A filter
|
|
2
|
+
// such as `ts >= start && price > 900` can't match a block whose max ts is
|
|
3
|
+
// below `start`, so a query only needs the rows from the first block that
|
|
4
|
+
// can match to the last one. On time-ordered data (logs, orders, streams),
|
|
5
|
+
// "the last hour" becomes a small slice of the table.
|
|
6
|
+
//
|
|
7
|
+
// Exact by construction: only `column <op> number` parts of a top-level
|
|
8
|
+
// `&&` chain are used (number columns and text codes; never free text), and
|
|
9
|
+
// the rows left out provably fail the filter.
|
|
10
|
+
import { parse } from './fuse.mjs';
|
|
11
|
+
|
|
12
|
+
export const ZONE_ROWS = 16_384;
|
|
13
|
+
const FLIP = { '<': '>', '<=': '>=', '>': '<', '>=': '<=', '==': '==' };
|
|
14
|
+
|
|
15
|
+
// Min and max per block, skipping NaN (a block of only NaN has min Infinity).
|
|
16
|
+
export function computeZones(data, n, B = ZONE_ROWS) {
|
|
17
|
+
const blocks = Math.ceil(n / B);
|
|
18
|
+
const mins = new Float64Array(blocks).fill(Infinity), maxs = new Float64Array(blocks).fill(-Infinity);
|
|
19
|
+
for (let b = 0; b < blocks; b++) {
|
|
20
|
+
let lo = Infinity, hi = -Infinity;
|
|
21
|
+
for (let i = b * B, end = Math.min(n, i + B); i < end; i++) {
|
|
22
|
+
const v = data[i];
|
|
23
|
+
if (v < lo) lo = v;
|
|
24
|
+
if (v > hi) hi = v;
|
|
25
|
+
}
|
|
26
|
+
mins[b] = lo; maxs[b] = hi;
|
|
27
|
+
}
|
|
28
|
+
return { block: B, shift: 0, mins, maxs };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// `column <op> value` conditions every matching row must meet.
|
|
32
|
+
export function conditions(pred, cols, params) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const value = (m) => {
|
|
35
|
+
if (m.t === 'num') return m.v;
|
|
36
|
+
if (m.t === 'var' && !cols.has(m.name) && typeof params[m.name] === 'number') return params[m.name];
|
|
37
|
+
if (m.t === 'un' && m.op === '-') { const v = value(m.x); return v === undefined ? undefined : -v; }
|
|
38
|
+
return undefined;
|
|
39
|
+
};
|
|
40
|
+
const column = (m) => (m.t === 'var' && cols.has(m.name) && !cols.get(m.name).strings ? m.name : null);
|
|
41
|
+
(function visit(node) {
|
|
42
|
+
if (node.t === 'bin' && node.op === '&&') { visit(node.l); visit(node.r); return; }
|
|
43
|
+
if (node.t !== 'bin' || !FLIP[node.op]) return;
|
|
44
|
+
let col = column(node.l), v = value(node.r), op = node.op;
|
|
45
|
+
if (!col) { col = column(node.r); v = value(node.l); op = FLIP[op]; }
|
|
46
|
+
if (col && typeof v === 'number' && !Number.isNaN(v)) out.push({ col, op, v });
|
|
47
|
+
})(parse(pred));
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const mayMatch = (op, lo, hi, v) =>
|
|
52
|
+
op === '>' ? hi > v : op === '>=' ? hi >= v : op === '<' ? lo < v : op === '<=' ? lo <= v : lo <= v && v <= hi;
|
|
53
|
+
|
|
54
|
+
// The rows [lo, hi) that can match `pred`, or null when zone maps can't
|
|
55
|
+
// narrow it usefully. zonesOf(name) gives a column's zones (computed lazily).
|
|
56
|
+
// round: how the compiled filter rounds numbers (Math.fround for float32
|
|
57
|
+
// tables), so the zone check compares exactly like the filter will.
|
|
58
|
+
export function zoneRange(pred, cols, params, n, zonesOf, round = (v) => v) {
|
|
59
|
+
const conds = conditions(pred, cols, params).map((c) => ({ ...c, v: round(c.v) }));
|
|
60
|
+
if (!conds.length) return null;
|
|
61
|
+
let first = Infinity, last = -Infinity, zones = null;
|
|
62
|
+
const maps = conds.map((c) => ({ ...c, z: zonesOf(c.col) }));
|
|
63
|
+
zones = maps[0].z;
|
|
64
|
+
const { block: B, shift } = zones;
|
|
65
|
+
const blocks = zones.mins.length;
|
|
66
|
+
for (let b = 0; b < blocks; b++) {
|
|
67
|
+
let ok = true;
|
|
68
|
+
for (const { op, v, z } of maps) if (!mayMatch(op, z.mins[b], z.maxs[b], v)) { ok = false; break; }
|
|
69
|
+
if (ok) { if (b < first) first = b; last = b; }
|
|
70
|
+
}
|
|
71
|
+
if (first === Infinity) return [0, 0];
|
|
72
|
+
const lo = Math.max(0, first * B - shift), hi = Math.min(n, (last + 1) * B - shift);
|
|
73
|
+
if (hi - lo > 0.9 * n) return null; // not worth a narrower run
|
|
74
|
+
return [lo, Math.max(lo, hi)];
|
|
75
|
+
}
|