vectrojs 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +821 -4
  3. package/bin/vectro.mjs +82 -0
  4. package/dist/vectro.wasm +0 -0
  5. package/mcp/server.mjs +288 -0
  6. package/package.json +84 -6
  7. package/src/arrow.mjs +905 -0
  8. package/src/bitpack.mjs +135 -0
  9. package/src/browser.mjs +129 -0
  10. package/src/bytes.mjs +60 -0
  11. package/src/csv-kernel.mjs +205 -0
  12. package/src/csv.mjs +270 -0
  13. package/src/dataset.mjs +1763 -0
  14. package/src/dates.mjs +139 -0
  15. package/src/emitter.mjs +109 -0
  16. package/src/engine.mjs +881 -0
  17. package/src/files-browser.mjs +104 -0
  18. package/src/files-node.mjs +59 -0
  19. package/src/frames.mjs +191 -0
  20. package/src/fuse.mjs +512 -0
  21. package/src/interp.mjs +305 -0
  22. package/src/join.mjs +534 -0
  23. package/src/kernels.mjs +312 -0
  24. package/src/ndjson.mjs +136 -0
  25. package/src/node.mjs +120 -0
  26. package/src/pack.mjs +114 -0
  27. package/src/parallel-sort.mjs +99 -0
  28. package/src/partition-kernels.mjs +388 -0
  29. package/src/partition.mjs +395 -0
  30. package/src/plan.mjs +551 -0
  31. package/src/portable.mjs +21 -0
  32. package/src/rank-kernels.mjs +81 -0
  33. package/src/rows.mjs +91 -0
  34. package/src/serve/catalog.mjs +148 -0
  35. package/src/serve/flight.mjs +270 -0
  36. package/src/serve/formats.mjs +87 -0
  37. package/src/serve/http.mjs +182 -0
  38. package/src/serve/index.mjs +9 -0
  39. package/src/serve/odata.mjs +291 -0
  40. package/src/serve/pg.mjs +588 -0
  41. package/src/serve/pgcatalog.mjs +220 -0
  42. package/src/serve/pgtypes.mjs +199 -0
  43. package/src/sketch.mjs +75 -0
  44. package/src/sort-kernels.mjs +117 -0
  45. package/src/sort.mjs +102 -0
  46. package/src/split-worker-core.mjs +252 -0
  47. package/src/split-worker-node.mjs +8 -0
  48. package/src/split-worker.mjs +7 -0
  49. package/src/split.mjs +741 -0
  50. package/src/sql.mjs +1558 -0
  51. package/src/sqltext.mjs +132 -0
  52. package/src/static-kernels.mjs +164 -0
  53. package/src/stream-worker.mjs +553 -0
  54. package/src/table-kernels.mjs +323 -0
  55. package/src/table.mjs +2206 -0
  56. package/src/textmap.mjs +159 -0
  57. package/src/vectors.mjs +138 -0
  58. package/src/wasm.mjs +254 -0
  59. package/src/window.mjs +160 -0
  60. package/src/worker-browser.mjs +21 -0
  61. package/src/worker-loop.mjs +106 -0
  62. package/src/worker.mjs +22 -0
  63. package/src/zones.mjs +75 -0
package/src/sort.mjs ADDED
@@ -0,0 +1,102 @@
1
+ // Sorting helpers for tables (see Table.sort / Table.top).
2
+ //
3
+ // Sort keys are given as text: a column name or any expression, sorted from
4
+ // small to large; a leading "-" reverses it ('-price', '-(price * qty)').
5
+ // A category column sorts by its labels (A to Z, or Z to A with "-").
6
+ // Several keys: ['region', '-revenue']. Ties keep table order (stable).
7
+ // Missing values (NaN) always come last.
8
+
9
+ const IDENT = /^\s*(-?)\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/;
10
+
11
+ // [{ text: expression to compute | null, column: category column | null, desc }]
12
+ export function parseKeys(by, cols) {
13
+ const list = Array.isArray(by) ? by : [by];
14
+ if (!list.length) throw new Error('vectro: sort needs at least one key, e.g. "-price"');
15
+ return list.map((k) => {
16
+ if (typeof k !== 'string' || !k.trim()) throw new Error(`vectro: a sort key must be a column or expression, got ${JSON.stringify(k)}`);
17
+ const m = IDENT.exec(k);
18
+ if (m && cols.get(m[2])?.dict) return { column: m[2], desc: m[1] === '-', text: null };
19
+ if (m && cols.get(m[2])?.strings) throw new Error(`vectro: can't sort by free-text column "${m[2]}" (load it as a category)`);
20
+ return { column: null, desc: false, text: k };
21
+ });
22
+ }
23
+
24
+ // Ranks for a category column's codes: code -> position of its label A..Z.
25
+ export function labelRanks(dict, desc) {
26
+ const order = dict.map((_, i) => i).sort((a, b) => (dict[a] < dict[b] ? -1 : dict[a] > dict[b] ? 1 : 0));
27
+ const rank = new Float64Array(dict.length);
28
+ order.forEach((code, r) => { rank[code] = desc ? -r : r; });
29
+ return rank;
30
+ }
31
+
32
+ // Stable sort of row indices 0..n-1 by several Float64Array keys (first key
33
+ // most important): LSD radix sort, last key first. Each key becomes two
34
+ // uint32 words that sort as unsigned numbers; the words travel with the
35
+ // indices (so every pass reads in order instead of gathering at random), in
36
+ // 11-bit digits (2,048 buckets fit in cache). All digit counts are taken in
37
+ // one pass per key, and digits that are the same in every row are skipped,
38
+ // so small integer keys take one or two passes instead of six. Keys that are
39
+ // whole numbers within 2^53 of their minimum (codes, IDs, counts) sort as
40
+ // that offset, so only the digits that vary cost a pass.
41
+ const DIGITS = [[0, 0], [0, 11], [0, 22], [1, 0], [1, 11], [1, 22]]; // [word (0 = low), shift]
42
+ export function sortIndex(keys, n) {
43
+ let idx = new Uint32Array(n);
44
+ for (let i = 0; i < n; i++) idx[i] = i;
45
+ if (n <= 4096) return smallSort(keys, idx); // a comparison sort is quicker than bucket passes
46
+ let lo = new Uint32Array(n), hi = new Uint32Array(n);
47
+ let idx2 = new Uint32Array(n), lo2 = new Uint32Array(n), hi2 = new Uint32Array(n);
48
+ const hist = new Uint32Array(6 * 2048);
49
+ for (let k = keys.length - 1; k >= 0; k--) {
50
+ // float64 -> two uint32 halves that sort as unsigned numbers, in the current order
51
+ const f = keys[k];
52
+ let min = Infinity, max = -Infinity, whole = true;
53
+ for (let i = 0; i < n; i++) { const v = f[i]; if (v < min) min = v; if (v > max) max = v; if (v !== Math.floor(v)) { whole = false; break; } }
54
+ whole = whole && max - min < 2 ** 53; // (NaN and infinities fail `whole`)
55
+ const bits = new Uint32Array(f.buffer, f.byteOffset, n * 2);
56
+ hist.fill(0);
57
+ for (let i = 0; i < n; i++) {
58
+ const r = idx[i];
59
+ let h, l;
60
+ if (whole) { const o = f[r] - min; h = Math.floor(o / 4294967296); l = o - h * 4294967296; }
61
+ else {
62
+ h = bits[2 * r + 1]; l = bits[2 * r];
63
+ if (f[r] !== f[r]) { h = 0xffffffff; l = 0xffffffff; } // NaN: after everything
64
+ else if (h & 0x80000000) { h = ~h >>> 0; l = ~l >>> 0; } // negative: flip all
65
+ else h = (h | 0x80000000) >>> 0; // positive: flip the sign
66
+ }
67
+ hi[i] = h; lo[i] = l;
68
+ hist[l & 2047]++; hist[2048 + ((l >>> 11) & 2047)]++; hist[4096 + (l >>> 22)]++;
69
+ hist[6144 + (h & 2047)]++; hist[8192 + ((h >>> 11) & 2047)]++; hist[10240 + (h >>> 22)]++;
70
+ }
71
+ for (let d = 0; d < 6; d++) {
72
+ const [word, shift] = DIGITS[d], base = d * 2048;
73
+ const src = word ? hi : lo;
74
+ if (hist[base + ((src[0] >>> shift) & 2047)] === n) continue; // every row has this digit
75
+ let sum = 0;
76
+ for (let b = 0; b < 2048; b++) { const c = hist[base + b]; hist[base + b] = sum; sum += c; }
77
+ for (let i = 0; i < n; i++) {
78
+ const p = hist[base + ((src[i] >>> shift) & 2047)]++;
79
+ idx2[p] = idx[i]; lo2[p] = lo[i]; hi2[p] = hi[i];
80
+ }
81
+ [idx, idx2] = [idx2, idx]; [lo, lo2] = [lo2, lo]; [hi, hi2] = [hi2, hi];
82
+ }
83
+ }
84
+ return idx;
85
+ }
86
+
87
+ // Stable comparison sort of row indices for small inputs (same order as the
88
+ // radix sort: by each key in turn, NaN last, ties in table order).
89
+ function smallSort(keys, idx) {
90
+ const order = Array.from(idx);
91
+ order.sort((a, b) => {
92
+ for (const f of keys) {
93
+ const x = f[a], y = f[b];
94
+ if (x === y) continue;
95
+ if (x !== x) { if (y !== y) continue; return 1; }
96
+ if (y !== y) return -1;
97
+ return x < y ? -1 : 1;
98
+ }
99
+ return a - b;
100
+ });
101
+ return Uint32Array.from(order);
102
+ }
@@ -0,0 +1,252 @@
1
+ // Worker side of split tables. Each worker has its own single-thread engine
2
+ // and owns slices of tables (no shared memory). The page sends requests
3
+ // { rid, op, args } and gets back { rid, ok, value | error }.
4
+ import { Engine } from './engine.mjs';
5
+ import { Table } from './table.mjs';
6
+ import { parse } from './fuse.mjs';
7
+ import { createStreams } from './stream-worker.mjs';
8
+ import { parseKeys } from './sort.mjs';
9
+ import { searchTable } from './vectors.mjs';
10
+ import { decodeOne } from './pack.mjs';
11
+ import { SKETCH_SIZE, sketchAdd, hashNumber, hashText } from './sketch.mjs';
12
+
13
+ export function serveSplitWorker(post, listen) {
14
+ let engine;
15
+ const tables = new Map(); // table id -> Table (this worker's slice)
16
+ const streams = createStreams(() => engine); // live streams (see stream-worker.mjs)
17
+
18
+ const table = (id) => {
19
+ const t = tables.get(id) ?? streams.view(id);
20
+ if (!t) throw new Error(`vectro: no table ${id} in this worker`);
21
+ return t;
22
+ };
23
+ // A filtered view: the page keeps filters as text and sends them with each query.
24
+ const view = (id, preds = []) => preds.reduce((t, p) => t.filter(p.expr, p.params), table(id));
25
+ const dictsOf = (t) => Object.fromEntries([...t.cols].filter(([, c]) => c.dict).map(([name, c]) => [name, c.dict]));
26
+
27
+ // A worker's rows come in segments (blocks of the original table, in this
28
+ // worker's order); the page needs rows per segment to put rows from all
29
+ // workers back in table order. With filters, the compiled count(filter)
30
+ // runs on each segment's rows. With `limit`, counting stops once that many
31
+ // rows matched: later segments can't be among the first `limit` rows.
32
+ function segmentCounts(id, preds, segs, limit = Infinity) {
33
+ if (!preds?.length) return Float64Array.from(segs);
34
+ const v = view(id, preds), t = table(id);
35
+ const used = new Set();
36
+ (function walk(node) {
37
+ if (node.t === 'var') used.add(node.name);
38
+ for (const k of ['x', 'y', 'c', 'l', 'r']) if (node[k]) walk(node[k]);
39
+ if (node.args) node.args.forEach(walk);
40
+ })(parse(v.pred));
41
+ const columns = [...used].filter((name) => t.cols.has(name)).map((name) => [name, t.cols.get(name)]);
42
+ const scalars = Object.fromEntries([...used].filter((name) => !t.cols.has(name)).map((name) => [name, v.params[name]]));
43
+ const src = `count(${v.pred})`;
44
+ const out = [];
45
+ let r = 0, total = 0;
46
+ for (let k = 0; k < segs.length && total < limit; k++) {
47
+ const vars = { ...scalars };
48
+ for (const [name, c] of columns) { // (memory may have grown: live views)
49
+ const part = engine.live(c.data).subarray(r, r + segs[k]);
50
+ vars[name] = c.enc ? { data: part, enc: c.enc, n: segs[k], type: t.type === Float32Array ? 'f32' : 'f64' } : part;
51
+ }
52
+ const c = segs[k] ? engine.eval(src, vars) : 0;
53
+ out.push(c);
54
+ total += c;
55
+ r += segs[k];
56
+ }
57
+ return Float64Array.from(out);
58
+ }
59
+
60
+ const handlers = {
61
+ init({ engineOptions }) {
62
+ engine = new Engine(engineOptions);
63
+ return { jit: engine.jit };
64
+ },
65
+ // A slice of columns. Text columns get this worker's own dictionary; the
66
+ // page merges them and sends `remap` so every worker uses the same codes.
67
+ load({ id, columns }) {
68
+ const t = engine.table(columns);
69
+ tables.set(id, t);
70
+ return { n: t.length, dicts: dictsOf(t), names: t.columnNames };
71
+ },
72
+ // Columns already encoded by the page: numbers, plus text as codes into a
73
+ // shared dictionary (so nothing needs renumbering afterwards).
74
+ loadEncoded({ id, order, numbers, text, float32 = false, dates = [] }) {
75
+ const T = float32 ? Float32Array : Float64Array; // one number type per table
76
+ const base = Object.keys(numbers).length ? engine.table(numbers, { type: T }) : null;
77
+ const cols = new Map();
78
+ for (const name of order) {
79
+ if (text[name]) {
80
+ const { codes, dict } = text[name];
81
+ const data = engine.alloc(T, codes.length);
82
+ data.set(codes);
83
+ cols.set(name, { data, dict, codes: new Map(dict.map((v, i) => [v, i])), strings: null });
84
+ } else {
85
+ cols.set(name, base.cols.get(name));
86
+ }
87
+ }
88
+ for (const c of cols.values()) c.data = engine.live(c.data); // later allocations may have moved memory
89
+ for (const name of dates) cols.get(name).date = true;
90
+ const n = base ? base.length : (Object.values(text)[0]?.codes.length ?? 0);
91
+ const t = new Table(engine, cols, n, { owned: [...order] });
92
+ tables.set(id, t);
93
+ return { n: t.length, names: t.columnNames };
94
+ },
95
+ // CSV: a header plus this worker's pieces of the file, back to back.
96
+ // `pieces` are their byte lengths; returns the rows in each piece.
97
+ loadCSV({ id, bytes, opts, pieces }) {
98
+ let segs = null;
99
+ const t = engine.parseCSV(bytes, { ...opts, pieces, onPieceRows: (rows) => { segs = rows; } });
100
+ tables.set(id, t);
101
+ return { n: t.length, segs, dicts: dictsOf(t), names: t.columnNames, strings: [...t.cols].filter(([, c]) => c.strings).map(([n]) => n), dates: [...t.cols].filter(([, c]) => c.date).map(([n]) => n) };
102
+ },
103
+ // Switch text columns to the shared dictionary: code -> lut[code].
104
+ remap({ id, maps }) {
105
+ const t = table(id);
106
+ let repack = false;
107
+ for (const [name, { lut, dict }] of Object.entries(maps)) {
108
+ const c = t.cols.get(name);
109
+ if (c.enc) { // packed codes: unpack, renumber, pack again below
110
+ const plain = engine.alloc(c.plainType, t.length), codes = engine.live(c.data);
111
+ for (let i = 0; i < t.length; i++) plain[i] = decodeOne(c.enc, codes[i], c.plainType === Float32Array);
112
+ engine.free(c.data);
113
+ c.data = plain;
114
+ delete c.enc; delete c.plainType; delete c.rows;
115
+ repack = true;
116
+ }
117
+ const data = engine.live(c.data);
118
+ for (let i = 0; i < t.length; i++) data[i] = lut[data[i]];
119
+ c.dict = dict;
120
+ c.codes = new Map(dict.map((s, i) => [s, i]));
121
+ }
122
+ if (repack) t.compress();
123
+ return true;
124
+ },
125
+ agg({ id, preds, spec, params }) {
126
+ return view(id, preds).agg(spec, params);
127
+ },
128
+ // Partial group results, as columns: one per key (labels for text keys)
129
+ // + one array per aggregate.
130
+ groupBy({ id, preds, keys, spec, params }) {
131
+ const g = view(id, preds).groupBy(keys).agg(spec, params);
132
+ try {
133
+ const keyCols = keys.map((key) => {
134
+ const k = g.cols.get(key);
135
+ const codes = engine.live(k.data);
136
+ return k.dict ? Array.from(codes, (c) => k.dict[c]) : Float64Array.from(codes);
137
+ });
138
+ const cols = {};
139
+ for (const out of Object.keys(spec)) cols[out] = Float64Array.from(engine.live(g.cols.get(out).data));
140
+ return { keyCols, cols };
141
+ } finally {
142
+ g.dispose();
143
+ }
144
+ },
145
+ collect({ id, preds, newId, segs }) {
146
+ const counts = segmentCounts(id, preds, segs);
147
+ const t = view(id, preds).collect();
148
+ tables.set(newId, t);
149
+ return { n: t.length, segs: counts };
150
+ },
151
+ derive({ id, newId, name, expr, params }) {
152
+ const t = table(id).derive(name, expr, params);
153
+ tables.set(newId, t);
154
+ return t.length;
155
+ },
156
+ // This worker's first k rows by `by`, with each expression key's values
157
+ // (text keys are compared by label on the page).
158
+ top({ id, preds, k, by, columns = null }) {
159
+ const v = view(id, preds);
160
+ const t = (columns ? v.select(columns) : v).top(k, by);
161
+ try {
162
+ const keys = parseKeys(by, t.cols);
163
+ const values = keys.map((key) => {
164
+ if (key.column) return null;
165
+ const d = t.derive('__np_k', key.text);
166
+ try { return Float64Array.from(engine.live(d.cols.get('__np_k').data)); } finally { d.dispose(); }
167
+ });
168
+ return { rows: t.toRows(), keys: keys.map((key) => (key.column ? { label: key.column, desc: key.desc } : null)), values };
169
+ } finally {
170
+ t.dispose();
171
+ }
172
+ },
173
+ // Vector search over this worker's rows (see vectors.mjs).
174
+ vectorTop({ id, preds, query, opts }) {
175
+ return searchTable(view(id, preds), query, opts);
176
+ },
177
+ // Distinct (group..., value) pairs of this worker's rows, or with sketch
178
+ // one distinct-count sketch per group (the page merges them).
179
+ distinct({ id, preds, keys, arg, params, sketch }) {
180
+ const v = view(id, preds);
181
+ const plain = /^[A-Za-z_]\w*$/.test(arg.trim()) && v.cols.has(arg.trim()) && !keys.includes(arg.trim()); // (a key: count a copy)
182
+ const t = plain ? v : v.derive('__np_dv', arg, params);
183
+ const name = plain ? arg.trim() : '__np_dv';
184
+ const g = t.groupBy([...keys, name]).agg({ __np_n: 'count()' });
185
+ try {
186
+ const colOf = (k) => { const c = g.cols.get(k), d = engine.live(c.data); return c.dict ? Array.from(d, (x) => c.dict[x]) : Float64Array.from(d); };
187
+ let keyCols = keys.map(colOf), values = colOf(name);
188
+ if (Array.isArray(values) && values.includes('')) { // (missing text: not a value to count)
189
+ const keep = values.map((x) => x !== '');
190
+ keyCols = keyCols.map((c) => c.filter((_, r) => keep[r]));
191
+ values = values.filter((_, r) => keep[r]);
192
+ }
193
+ if (!sketch) return { keyCols, values };
194
+ const index = new Map(), groupCols = keys.map(() => []), regs = [];
195
+ for (let r = 0; r < values.length; r++) {
196
+ const tuple = keyCols.map((c) => c[r]);
197
+ const id = tuple.map((x) => (typeof x === 'string' ? JSON.stringify(x) : String(x))).join(',');
198
+ let at = index.get(id);
199
+ if (at === undefined) { at = index.size; index.set(id, at); tuple.forEach((x, k) => groupCols[k].push(x)); regs.push(new Uint8Array(SKETCH_SIZE)); }
200
+ const x = values[r];
201
+ if (typeof x === 'string') sketchAdd(regs[at], 0, hashText(x));
202
+ else if (x === x) sketchAdd(regs[at], 0, hashNumber(x));
203
+ }
204
+ const sketches = new Uint8Array(regs.length * SKETCH_SIZE);
205
+ regs.forEach((r, i) => sketches.set(r, i * SKETCH_SIZE));
206
+ return { keyCols: groupCols, sketches };
207
+ } finally {
208
+ g.dispose();
209
+ if (!plain) t.dispose();
210
+ }
211
+ },
212
+ columns({ id, preds, segs }) {
213
+ return { columns: view(id, preds).toColumns(), segs: segmentCounts(id, preds, segs) };
214
+ },
215
+ toRows({ id, preds, limit, segs }) {
216
+ return { rows: view(id, preds).toRows(limit), segs: segmentCounts(id, preds, segs, limit ?? Infinity) };
217
+ },
218
+ ...streams.handlers,
219
+ streamSnapshot: (args) => streams.handlers.streamSnapshot({ ...args, tables }),
220
+ dispose({ id }) {
221
+ if (streams.close(id)) return true;
222
+ tables.get(id)?.dispose();
223
+ tables.delete(id);
224
+ return true;
225
+ },
226
+ };
227
+
228
+ listen(async ({ rid, op, args }) => {
229
+ try {
230
+ const value = await handlers[op](args);
231
+ post({ rid, ok: true, value }, transferList(value));
232
+ } catch (err) {
233
+ post({ rid, ok: false, error: (globalThis.process?.env?.VECTRO_DEBUG ? err?.stack : err?.message) ?? String(err) });
234
+ }
235
+ });
236
+ }
237
+
238
+ // Typed arrays in a group result move to the page without copying.
239
+ function transferList(value) {
240
+ const out = [];
241
+ if (value && typeof value === 'object') {
242
+ if (ArrayBuffer.isView(value.keys)) out.push(value.keys.buffer);
243
+ if (value.keyCols) for (const k of value.keyCols) if (ArrayBuffer.isView(k)) out.push(k.buffer);
244
+ if (ArrayBuffer.isView(value.sketches)) out.push(value.sketches.buffer);
245
+ if (ArrayBuffer.isView(value.values)) out.push(value.values.buffer);
246
+ if (value.cols) for (const c of Object.values(value.cols)) out.push(c.buffer);
247
+ if (ArrayBuffer.isView(value.segs)) out.push(value.segs.buffer);
248
+ if (value.columns) for (const c of Object.values(value.columns)) if (ArrayBuffer.isView(c) || ArrayBuffer.isView(c.codes)) out.push((c.codes ?? c).buffer);
249
+ if (Array.isArray(value.values)) for (const v of value.values) if (ArrayBuffer.isView(v)) out.push(v.buffer);
250
+ }
251
+ return out;
252
+ }
@@ -0,0 +1,8 @@
1
+ // Node worker thread for split tables (see split-worker-core.mjs).
2
+ import { parentPort } from 'node:worker_threads';
3
+ import { serveSplitWorker } from './split-worker-core.mjs';
4
+
5
+ serveSplitWorker(
6
+ (msg, transfer) => parentPort.postMessage(msg, transfer),
7
+ (handle) => parentPort.on('message', handle),
8
+ );
@@ -0,0 +1,7 @@
1
+ // Browser Web Worker for split tables (see split-worker-core.mjs).
2
+ import { serveSplitWorker } from './split-worker-core.mjs';
3
+
4
+ serveSplitWorker(
5
+ (msg, transfer) => self.postMessage(msg, transfer),
6
+ (handle) => { self.onmessage = (ev) => handle(ev.data); },
7
+ );