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
@@ -0,0 +1,104 @@
1
+ // Browser files for datasets (see dataset.mjs): writing to the origin's
2
+ // private file system (OPFS: navigator.storage.getDirectory()), or to a
3
+ // file the user chose (a FileSystemFileHandle from showSaveFilePicker), and
4
+ // reading those back. Writes go to the file's writable stream in order, in
5
+ // the background; flush() / done() wait for them (the dataset writer's
6
+ // appendAsync / closeAsync do). Nothing is committed until the file closes,
7
+ // and an abort discards it.
8
+
9
+ export const storageAvailable = () => typeof navigator !== 'undefined' && typeof navigator.storage?.getDirectory === 'function';
10
+
11
+ async function storageDir() {
12
+ if (!storageAvailable()) throw new Error("vectro: this browser has no file storage for datasets (navigator.storage.getDirectory); use memoryFile() and download the result");
13
+ return navigator.storage.getDirectory();
14
+ }
15
+
16
+ // A writable file: a name in browser storage, or a FileSystemFileHandle.
17
+ export async function writableFile(target, { append = false } = {}) {
18
+ const handle = typeof target === 'string' ? await (await storageDir()).getFileHandle(target, { create: true }) : target;
19
+ if (typeof handle.createWritable !== 'function') throw new Error('vectro: this browser cannot write files here (no createWritable); use memoryFile() and download the result');
20
+ const before = await handle.getFile();
21
+ const existed = append && before.size > 0;
22
+ const stream = await handle.createWritable({ keepExistingData: existed });
23
+ let chain = Promise.resolve(), failed = null;
24
+ const then = (fn) => { chain = chain.then(() => (failed ? undefined : fn())).catch((err) => { failed ??= err; }); };
25
+ const check = async () => { await chain; if (failed) throw failed; };
26
+ return {
27
+ name: handle.name,
28
+ existed,
29
+ size: before.size,
30
+ // (the footer of the file as it was, when appending)
31
+ async read(u8, pos) { u8.set(new Uint8Array(await before.slice(pos, pos + u8.length).arrayBuffer())); },
32
+ write(u8, pos) {
33
+ if (failed) throw failed;
34
+ const data = u8.slice(); // (engine memory changes; the stream writes later)
35
+ then(() => stream.write({ type: 'write', position: pos, data }));
36
+ },
37
+ truncate(size) { then(() => stream.truncate(size)); },
38
+ close() { then(() => stream.close()); },
39
+ abort() { chain = chain.then(() => stream.abort()).catch(() => {}); },
40
+ flush: check,
41
+ done: check,
42
+ };
43
+ }
44
+
45
+ // A dataset kept in memory instead of a file: blob() gives it, e.g. to
46
+ // download or upload. For browsers without file storage.
47
+ export function memoryFile(name = 'dataset.vectro') {
48
+ const parts = []; // [position, bytes]
49
+ let size = 0;
50
+ return {
51
+ name,
52
+ existed: false,
53
+ size: 0,
54
+ write(u8, pos) { parts.push([pos, u8.slice()]); size = Math.max(size, pos + u8.length); },
55
+ truncate(n) { size = n; },
56
+ close() {},
57
+ blob() {
58
+ const out = new Uint8Array(size);
59
+ for (const [pos, data] of parts) out.set(data.subarray(0, Math.max(0, Math.min(data.length, size - pos))), pos);
60
+ return new Blob([out], { type: 'application/octet-stream' });
61
+ },
62
+ };
63
+ }
64
+
65
+ // Datasets in browser storage: [{ name, size }].
66
+ export async function storedFiles() {
67
+ if (!storageAvailable()) return [];
68
+ const out = [];
69
+ for await (const [name, handle] of (await storageDir()).entries()) {
70
+ if (handle.kind === 'file') out.push({ name, size: (await handle.getFile()).size });
71
+ }
72
+ return out;
73
+ }
74
+
75
+ // A stored dataset as a File (to open, or to download).
76
+ export async function storedFile(name) { return (await (await storageDir()).getFileHandle(name)).getFile(); }
77
+
78
+ export async function removeStored(name) { await (await storageDir()).removeEntry(name); }
79
+
80
+ // Temporary files for spilling (see dataset.mjs setTempStorage): a folder
81
+ // per spill in browser storage, removed when the query ends.
82
+ export const opfsTemp = {
83
+ async dir(prefix) {
84
+ const name = `${prefix}${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
85
+ await (await storageDir()).getDirectoryHandle(name, { create: true });
86
+ return name;
87
+ },
88
+ join: (dir, name) => `${dir}/${name}`,
89
+ async openWrite(path) { return writableFile(await fileHandle(path, true)); },
90
+ async openRead(path) {
91
+ const file = await (await fileHandle(path, false)).getFile();
92
+ return { name: path, size: file.size, async read(u8, pos) { u8.set(new Uint8Array(await file.slice(pos, pos + u8.length).arrayBuffer())); }, close() {} };
93
+ },
94
+ async remove(path) {
95
+ const [dir, name] = path.split('/');
96
+ const root = await storageDir();
97
+ if (name) await (await root.getDirectoryHandle(dir)).removeEntry(name); else await root.removeEntry(dir, { recursive: true });
98
+ },
99
+ };
100
+
101
+ async function fileHandle(path, create) {
102
+ const [dir, name] = path.split('/');
103
+ return (await (await storageDir()).getDirectoryHandle(dir, { create })).getFileHandle(name, { create });
104
+ }
@@ -0,0 +1,59 @@
1
+ // Node files for datasets (see dataset.mjs): reading, writing and temporary
2
+ // folders. dataset.mjs itself uses no Node APIs, so datasets also open in
3
+ // browsers (from a File or a URL; see blobFile and urlFile there).
4
+ import { openSync, readSync, writeSync, closeSync, fstatSync, ftruncateSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
5
+ import { open } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+
9
+ function readFully(fd, u8, pos) {
10
+ for (let done = 0; done < u8.length; ) {
11
+ const got = readSync(fd, u8, done, u8.length - done, pos + done);
12
+ if (got === 0) throw new Error('vectro: dataset file ended early');
13
+ done += got;
14
+ }
15
+ }
16
+
17
+ // A readable file: sync and async reads, and the descriptor (worker threads
18
+ // read chunks with it straight into engine memory).
19
+ function reader(fd, path) {
20
+ let fh = null;
21
+ return {
22
+ fd,
23
+ name: path,
24
+ size: fstatSync(fd).size,
25
+ readSync: (u8, pos) => readFully(fd, u8, pos),
26
+ async read(u8, pos) {
27
+ fh ??= open(path, 'r');
28
+ const h = await fh;
29
+ for (let done = 0; done < u8.length; ) {
30
+ const { bytesRead } = await h.read(u8, done, u8.length - done, pos + done);
31
+ if (bytesRead === 0) throw new Error('vectro: dataset file ended early');
32
+ done += bytesRead;
33
+ }
34
+ },
35
+ close() {
36
+ closeSync(fd);
37
+ if (fh) { const h = fh; fh = null; h.then((x) => x.close(), () => {}); }
38
+ },
39
+ };
40
+ }
41
+
42
+ export const nodeFiles = {
43
+ openRead: (path) => reader(openSync(path, 'r'), path),
44
+ // A writable file; with append, an existing file is opened as it is
45
+ // (existed: true, and readable, for its footer).
46
+ openWrite(path, { append = false } = {}) {
47
+ const existed = append && existsSync(path);
48
+ const fd = openSync(path, existed ? 'r+' : 'w');
49
+ return {
50
+ ...reader(fd, path),
51
+ existed,
52
+ write(u8, pos) { for (let done = 0; done < u8.length; ) done += writeSync(fd, u8, done, u8.length - done, pos + done); },
53
+ truncate: (size) => ftruncateSync(fd, size),
54
+ };
55
+ },
56
+ tempDir: (prefix) => mkdtempSync(join(tmpdir(), prefix)),
57
+ join,
58
+ remove: (path) => rmSync(path, { recursive: true, force: true }),
59
+ };
package/src/frames.mjs ADDED
@@ -0,0 +1,191 @@
1
+ // Vectro frames: a binary format for streaming column batches. The receiver
2
+ // copies number columns straight into memory: no text, no parsing.
3
+ //
4
+ // A connection carries frames, each one:
5
+ // u32 length of the rest of the frame (little-endian)
6
+ // u8 kind: 1 = schema, 2 = batch, 3 = end
7
+ // u8 0 (reserved) u16 0 (reserved)
8
+ // schema: u32 byte length + UTF-8 JSON { v: 1, columns: [[name, 'number' | 'category'], ...] }
9
+ // batch: u32 rows, u32 0, then for each column, in schema order, padded to 8 bytes:
10
+ // number: rows x float64
11
+ // category: u32 count of new labels, then each label as u32 byte length +
12
+ // UTF-8 bytes, padded to 8; then rows x u32 codes. Codes number
13
+ // the labels in order of first appearance on this connection, so
14
+ // a label is sent once per connection.
15
+ // end: nothing more.
16
+ // Every section starts at a multiple of 8 from the frame start, so a frame
17
+ // copied to an aligned buffer can be read through typed-array views.
18
+ export const SCHEMA = 1, BATCH = 2, END = 3;
19
+ const enc = new TextEncoder(), dec = new TextDecoder();
20
+ const pad8 = (n) => (n + 7) & ~7;
21
+
22
+ export function encodeSchema(columns) {
23
+ const json = enc.encode(JSON.stringify({ v: 1, columns: Object.entries(columns) }));
24
+ const out = new Uint8Array(8 + pad8(4 + json.length));
25
+ const dv = new DataView(out.buffer);
26
+ dv.setUint32(0, out.length - 4, true);
27
+ out[4] = SCHEMA;
28
+ dv.setUint32(8, json.length, true);
29
+ out.set(json, 12);
30
+ return out;
31
+ }
32
+
33
+ export function encodeEnd() {
34
+ const out = new Uint8Array(8);
35
+ new DataView(out.buffer).setUint32(0, 4, true);
36
+ out[4] = END;
37
+ return out;
38
+ }
39
+
40
+ // Builds batch frames for one connection (it remembers which labels that
41
+ // connection has already received).
42
+ export class BatchEncoder {
43
+ constructor(columns) {
44
+ this.columns = Object.entries(columns);
45
+ for (const [name, kind] of this.columns) if (kind !== 'number' && kind !== 'category') throw new Error(`vectro: emitter column "${name}" must be 'number' or 'category', got "${kind}"`);
46
+ this.codes = this.columns.map(([, kind]) => (kind === 'category' ? new Map() : null));
47
+ }
48
+
49
+ // batch: { name: Float64Array | number[] | string[] | { codes, labels } }
50
+ // (every column the same length). A category column can be given as codes
51
+ // (Uint32Array, Int32Array or number[]) into a labels array, which skips
52
+ // the per-row label lookup.
53
+ encode(batch) {
54
+ let rows = -1;
55
+ const parts = this.columns.map(([name, kind], k) => {
56
+ const values = batch[name];
57
+ if (!values) throw new Error(`vectro: batch is missing column "${name}"`);
58
+ const len = values.codes ? values.codes.length : values.length;
59
+ if (kind === 'number' && values.codes) throw new Error(`vectro: column "${name}" is a number column; codes are for category columns`);
60
+ if (rows === -1) rows = len;
61
+ else if (len !== rows) throw new RangeError(`vectro: column "${name}" has ${len} values, expected ${rows}`);
62
+ if (kind === 'number') {
63
+ const f = values instanceof Float64Array ? values : Float64Array.from(values);
64
+ return { bytes: new Uint8Array(f.buffer, f.byteOffset, f.byteLength) };
65
+ }
66
+ // category: codes on this connection, new labels sent once
67
+ const map = this.codes[k], fresh = [];
68
+ const code = (label) => {
69
+ let c = map.get(label);
70
+ if (c === undefined) { c = map.size; map.set(label, c); fresh.push(enc.encode(String(label))); }
71
+ return c;
72
+ };
73
+ let codes;
74
+ if (values.codes) { // codes + labels: map each label once, then the codes
75
+ const lut = Uint32Array.from(values.labels, code), src = values.codes;
76
+ if (src instanceof Uint32Array && lut.every((c, j) => c === j)) codes = src; // same numbering: no copy
77
+ else { codes = new Uint32Array(rows); for (let i = 0; i < rows; i++) codes[i] = lut[src[i]]; }
78
+ } else {
79
+ codes = new Uint32Array(rows);
80
+ for (let i = 0; i < rows; i++) codes[i] = code(values[i]);
81
+ }
82
+ return { fresh, bytes: new Uint8Array(codes.buffer, codes.byteOffset, codes.byteLength) };
83
+ });
84
+ let size = 16; // header + rows + reserved
85
+ for (const p of parts) size += (p.fresh ? 8 + p.fresh.reduce((a, l) => a + pad8(4 + l.length), 0) : 0) + pad8(p.bytes.length);
86
+ const out = alloc(size); // not zero-filled: every byte is written below
87
+ const dv = new DataView(out.buffer, out.byteOffset, size);
88
+ dv.setUint32(0, size - 4, true);
89
+ dv.setUint32(4, BATCH, true); // kind + 3 reserved zero bytes
90
+ dv.setUint32(8, rows, true);
91
+ dv.setUint32(12, 0, true);
92
+ let at = 16;
93
+ const put = (bytes) => { out.set(bytes, at); const end = at + pad8(bytes.length); out.fill(0, at + bytes.length, end); at = end; };
94
+ for (const p of parts) {
95
+ if (p.fresh) {
96
+ dv.setUint32(at, p.fresh.length, true);
97
+ dv.setUint32(at + 4, 0, true);
98
+ at += 8;
99
+ for (const l of p.fresh) {
100
+ const end = at + pad8(4 + l.length);
101
+ dv.setUint32(at, l.length, true);
102
+ out.set(l, at + 4);
103
+ out.fill(0, at + 4 + l.length, end);
104
+ at = end;
105
+ }
106
+ }
107
+ put(p.bytes);
108
+ }
109
+ return out;
110
+ }
111
+ }
112
+
113
+ // Frame memory: Node's Buffer.allocUnsafeSlow skips zero-filling (the
114
+ // encoder writes every byte, padding included); elsewhere a Uint8Array.
115
+ const alloc = globalThis.Buffer?.allocUnsafeSlow ? (n) => globalThis.Buffer.allocUnsafeSlow(n) : (n) => new Uint8Array(n);
116
+
117
+ // Reads frames from bytes that arrive in pieces cut anywhere. Calls
118
+ // onSchema(columns), onBatch(rows, columns) with columns[k] =
119
+ // { values: Float64Array } or { codes: Uint32Array, labels: string[] (all
120
+ // labels of this connection so far) }, and onEnd(). Views point into an
121
+ // internal buffer that is reused: copy what you keep before returning.
122
+ export class FrameReader {
123
+ constructor({ onSchema, onBatch, onEnd = () => {} }) {
124
+ this.on = { onSchema, onBatch, onEnd };
125
+ this.buf = new Uint8Array(1 << 16); // aligned (offset 0 of its own ArrayBuffer)
126
+ this.len = 0;
127
+ this.columns = null;
128
+ this.labels = null;
129
+ }
130
+
131
+ push(chunk) {
132
+ if (this.len + chunk.length > this.buf.length) {
133
+ const bigger = new Uint8Array(Math.max(this.buf.length * 2, this.len + chunk.length));
134
+ bigger.set(this.buf.subarray(0, this.len));
135
+ this.buf = bigger;
136
+ }
137
+ this.buf.set(chunk, this.len);
138
+ this.len += chunk.length;
139
+ let at = 0;
140
+ while (this.len - at >= 4) {
141
+ const size = this.buf[at] | (this.buf[at + 1] << 8) | (this.buf[at + 2] << 16) | (this.buf[at + 3] * 0x1000000);
142
+ if (this.len - at < 4 + size) break;
143
+ if (at % 8) { // keep frames aligned: move the rest to the front first
144
+ this.buf.copyWithin(0, at, this.len);
145
+ this.len -= at;
146
+ at = 0;
147
+ }
148
+ this.#frame(at, 4 + size);
149
+ at += 4 + size;
150
+ }
151
+ if (at) { this.buf.copyWithin(0, at, this.len); this.len -= at; }
152
+ }
153
+
154
+ #frame(at, size) {
155
+ const dv = new DataView(this.buf.buffer, at, size);
156
+ const kind = this.buf[at + 4];
157
+ if (kind === SCHEMA) {
158
+ const n = dv.getUint32(8, true);
159
+ const { v, columns } = JSON.parse(dec.decode(this.buf.subarray(at + 12, at + 12 + n)));
160
+ if (v !== 1) throw new Error(`vectro: unsupported frame version ${v}`);
161
+ this.columns = columns;
162
+ this.labels = columns.map(([, kind]) => (kind === 'category' ? [] : null));
163
+ this.on.onSchema(Object.fromEntries(columns));
164
+ return;
165
+ }
166
+ if (kind === END) { this.on.onEnd(); return; }
167
+ if (kind !== BATCH) throw new Error(`vectro: unknown frame kind ${kind}`);
168
+ if (!this.columns) throw new Error('vectro: a batch arrived before the schema');
169
+ const rows = dv.getUint32(8, true);
170
+ let p = 16;
171
+ const out = this.columns.map(([, kind], k) => {
172
+ if (kind === 'number') {
173
+ const values = new Float64Array(this.buf.buffer, at + p, rows);
174
+ p += pad8(rows * 8);
175
+ return { values };
176
+ }
177
+ const fresh = dv.getUint32(p, true);
178
+ p += 8;
179
+ const labels = this.labels[k];
180
+ for (let i = 0; i < fresh; i++) {
181
+ const len = dv.getUint32(p, true);
182
+ labels.push(dec.decode(this.buf.subarray(at + p + 4, at + p + 4 + len)));
183
+ p += pad8(4 + len);
184
+ }
185
+ const codes = new Uint32Array(this.buf.buffer, at + p, rows);
186
+ p += pad8(rows * 4);
187
+ return { codes, labels };
188
+ });
189
+ this.on.onBatch(rows, out);
190
+ }
191
+ }