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/csv.mjs ADDED
@@ -0,0 +1,270 @@
1
+ // Parallel CSV loading into a Table.
2
+ //
3
+ // 1. The file's bytes go straight into engine memory (no JS strings).
4
+ // Reading files is in node.mjs; this module works anywhere.
5
+ // 2. Cut it into chunks at row boundaries. With quotes in the file, a
6
+ // parallel pass counts quote characters per chunk so each cut can tell
7
+ // whether it lands inside a quoted field (which may contain newlines).
8
+ // 3. Count rows per chunk in parallel, so every chunk knows its first row.
9
+ // 4. Parse every chunk in parallel straight into the final columns. Text
10
+ // columns get per-chunk dictionaries that are merged at the end.
11
+
12
+ import { Table } from './table.mjs';
13
+ import { OP } from './kernels.mjs';
14
+ import { parseNumber, STR_SHIFT, STR_ESCAPED } from './csv-kernel.mjs';
15
+ import { parseDateBytes, looksLikeDate, f32Dates } from './dates.mjs';
16
+ import { decode, encode, indexOfByte } from './bytes.mjs';
17
+
18
+ const KIND = { number: 0, category: 1, string: 2, date: 2 }; // dates are read as text, then parsed (see below)
19
+
20
+ const NL = 10, CR = 13, QUOTE = 34;
21
+ const CHUNK_BYTES = 2 << 20;
22
+ const RESERVED = new Set(['Infinity', 'NaN', 'sum', 'mean', 'min', 'max', 'count', 'abs', 'sqrt', 'floor', 'ceil']);
23
+
24
+ // CSV given as a string or bytes (Uint8Array, Buffer).
25
+ export function parseCSV(engine, text, opts = {}) { return engine._drive(parseCSVG(engine, text, opts)); }
26
+ export function parseCSVAsync(engine, text, opts = {}) { return engine._driveAsync(parseCSVG(engine, text, opts)); }
27
+
28
+ function* parseCSVG(engine, text, opts) {
29
+ const src = typeof text === 'string' ? encode(text) : text;
30
+ const bytes = engine.alloc(Uint8Array, src.length);
31
+ bytes.set(src);
32
+ return yield* loadBytesG(engine, bytes, opts);
33
+ }
34
+
35
+ export function loadBytes(engine, bytes, opts) { return engine._drive(loadBytesG(engine, bytes, opts)); }
36
+ export function loadBytesAsync(engine, bytes, opts) { return engine._driveAsync(loadBytesG(engine, bytes, opts)); }
37
+
38
+ // options:
39
+ // delimiter ',' (one character)
40
+ // header true: first line has column names; false: col0, col1, ...
41
+ // types { name: 'number' | 'category' | 'string' | 'text' } to override
42
+ // the guess. category = repeating labels (filterable, groupable);
43
+ // string = free text (kept as bytes; returned, not filtered yet);
44
+ // text = pick category or string from the sample.
45
+ // columns [names] to load only some columns
46
+ // type Float64Array (default) or Float32Array for number storage
47
+ // sampleRows rows used to guess column types (default 1000)
48
+ // chunkBytes target bytes per parallel chunk (tests use small chunks)
49
+ // pieces internal (split tables): byte lengths of consecutive pieces of
50
+ // the data, each starting at a row; they become the chunks, and
51
+ // onPieceRows(rows per piece) is called before parsing
52
+ // Parse CSV bytes that are already in engine memory.
53
+ // (Plain memory moves when it grows, so views are refreshed after each alloc.)
54
+ function* loadBytesG(engine, bytes, { delimiter = ',', header = true, types = {}, columns: only = null, type = Float64Array, sampleRows = 1000, chunkBytes = CHUNK_BYTES, pieces = null, onPieceRows = null } = {}) {
55
+ const temp = [bytes];
56
+ try {
57
+ const info = inspectCSV(bytes, { delimiter, header, types, sampleRows });
58
+ const { delim, names } = info;
59
+ let { start, end } = info;
60
+ const nFields = names.length;
61
+ const want = only ? new Set(only) : null;
62
+ if (want) for (const nm of want) if (!names.includes(nm)) throw new Error(`vectro: no column "${nm}" in the CSV (columns: ${names.join(', ')})`);
63
+
64
+ const out = []; // { name, kind }
65
+ let fieldMap = engine.alloc(Float64Array, nFields);
66
+ temp.push(fieldMap);
67
+ bytes = engine.live(bytes);
68
+ names.forEach((nm, f) => {
69
+ if (want && !want.has(nm)) { fieldMap[f] = -1; return; }
70
+ fieldMap[f] = out.length;
71
+ if (info.kinds[f] === 'date' && type === Float32Array) throw new Error(f32Dates(nm));
72
+ out.push({ name: nm, kind: KIND[info.kinds[f]], date: info.kinds[f] === 'date' });
73
+ });
74
+
75
+ // Cut points at row boundaries.
76
+ const T = engine.T;
77
+ const dataLen = Math.max(0, end - start);
78
+ const chunks = pieces ? pieces.length : Math.max(1, Math.min(T * 16, Math.ceil(dataLen / chunkBytes)));
79
+ let bounds = engine.alloc(Float64Array, chunks + 1);
80
+ temp.push(bounds);
81
+ bytes = engine.live(bytes);
82
+ fieldMap = engine.live(fieldMap);
83
+ const hasQuotes = indexOfByte(bytes, QUOTE, start, end) !== -1;
84
+ if (pieces) {
85
+ bounds[0] = start;
86
+ for (let k = 0; k < chunks; k++) bounds[k + 1] = Math.min(end, bounds[k] + pieces[k]); // the end may exclude trailing blank lines
87
+ if (bounds[chunks] < end) throw new Error('vectro: CSV pieces don\'t cover the data');
88
+ } else {
89
+ for (let k = 0; k <= chunks; k++) bounds[k] = start + Math.floor((k * dataLen) / chunks);
90
+ }
91
+
92
+ if (pieces) {
93
+ // already cut at rows
94
+ } else if (hasQuotes) {
95
+ const { parts: quotes } = yield* engine._chunksG(OP.CSV_SCAN, chunks, [bytes, bounds], [0, 1]);
96
+ let before = 0;
97
+ const raw = Float64Array.from(bounds);
98
+ for (let k = 1; k < chunks; k++) {
99
+ before += quotes[k - 1];
100
+ let inQuotes = before % 2 === 1;
101
+ let j = raw[k];
102
+ for (; j < end; j++) {
103
+ const c = bytes[j];
104
+ if (c === QUOTE) inQuotes = !inQuotes;
105
+ else if (c === NL && !inQuotes) break;
106
+ }
107
+ bounds[k] = Math.max(bounds[k - 1], Math.min(end, j + 1));
108
+ }
109
+ } else {
110
+ for (let k = 1; k < chunks; k++) {
111
+ const nl = indexOfByte(bytes, NL, bounds[k], end);
112
+ bounds[k] = Math.max(bounds[k - 1], nl === -1 ? end : nl + 1);
113
+ }
114
+ }
115
+ bounds[chunks] = end;
116
+
117
+ // Rows per chunk -> first row of each chunk.
118
+ const { parts: rowsPer } = yield* engine._chunksG(OP.CSV_SCAN, chunks, [bytes, bounds], [1, hasQuotes ? 1 : 0]);
119
+ onPieceRows?.(Array.from(rowsPer));
120
+ let rowStarts = engine.alloc(Float64Array, chunks);
121
+ temp.push(rowStarts);
122
+ let total = 0;
123
+ for (let k = 0; k < chunks; k++) { rowStarts[k] = total; total += rowsPer[k]; }
124
+
125
+ // Parse.
126
+ let cols = out.map(() => engine.alloc(type, total));
127
+ const kinds = engine.alloc(Float64Array, out.length);
128
+ temp.push(kinds);
129
+ out.forEach((c, o) => { kinds[o] = c.kind; });
130
+ // one dictionary message per chunk per category column
131
+ const expect = chunks * out.filter((c) => c.kind === 1).length;
132
+ const { messages } = yield* engine._chunksG(OP.CSV_PARSE, chunks, [bytes, bounds, rowStarts, fieldMap, kinds, ...cols], [delim, nFields], expect);
133
+
134
+ // One dictionary per text column, numbered in order of first appearance.
135
+ [bytes, rowStarts] = [engine.live(bytes), engine.live(rowStarts)];
136
+ cols = cols.map((c) => engine.live(c));
137
+ const dicts = out.map((c) => (c.kind === 1 ? { dict: [], codes: new Map() } : null));
138
+ messages.sort((a, b) => a.unit - b.unit || a.col - b.col);
139
+ for (const m of messages) {
140
+ const { dict, codes } = dicts[m.col];
141
+ const lut = new Float64Array(m.strings.length);
142
+ m.strings.forEach((str, local) => {
143
+ let g = codes.get(str);
144
+ if (g === undefined) { g = dict.length; dict.push(str); codes.set(str, g); }
145
+ lut[local] = g;
146
+ });
147
+ const col = cols[m.col];
148
+ for (let r = rowStarts[m.unit], stop = r + rowsPer[m.unit]; r < stop; r++) col[r] = lut[col[r]];
149
+ }
150
+
151
+ // Date columns: each value's text (still in the file's bytes) as ms.
152
+ out.forEach((c, o) => {
153
+ if (!c.date) return;
154
+ const col = cols[o];
155
+ for (let r = 0; r < total; r++) {
156
+ const v = col[r];
157
+ if (v !== v) continue;
158
+ const s = Math.floor(v / STR_SHIFT), len = (v - s * STR_SHIFT) % STR_ESCAPED;
159
+ col[r] = parseDateBytes(bytes, s, s + len);
160
+ }
161
+ });
162
+
163
+ // Free-text columns point into the file's bytes, so the table keeps them.
164
+ const keepBytes = out.some((c) => c.kind === 2 && !c.date);
165
+ if (keepBytes) temp.shift(); // temp[0] is the bytes array
166
+ const tableCols = new Map();
167
+ out.forEach((c, o) => tableCols.set(c.name, {
168
+ data: cols[o], dict: dicts[o]?.dict ?? null, codes: dicts[o]?.codes ?? null, strings: c.kind === 2 && !c.date ? bytes : null, date: c.date,
169
+ }));
170
+ return new Table(engine, tableCols, total, { owned: out.map((c) => c.name), ownedBuffers: keepBytes ? [bytes] : [] });
171
+ } finally {
172
+ for (const t of temp) engine.free(t);
173
+ }
174
+ }
175
+
176
+ // Header, data range and column types of CSV bytes, from the header and a
177
+ // sample of rows (the rest isn't read). kinds[f] is 'number', 'category' or
178
+ // 'string' for field f, after applying the `types` overrides.
179
+ export function inspectCSV(bytes, { delimiter = ',', header = true, types = {}, sampleRows = 1000 } = {}) {
180
+ const delim = delimiter.charCodeAt(0);
181
+ if (delimiter.length !== 1 || delim === QUOTE || delim === NL) throw new Error('vectro: delimiter must be one character, not a quote or newline');
182
+
183
+ let start = bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0; // UTF-8 BOM
184
+ let end = bytes.length;
185
+ while (end > start && (bytes[end - 1] === NL || bytes[end - 1] === CR)) end--;
186
+
187
+ const first = splitRow(bytes, start, end, delim);
188
+ const headerStart = start;
189
+ let names;
190
+ if (header) {
191
+ names = cleanNames(first.fields.map((f) => fieldText(bytes, f)));
192
+ start = Math.min(first.next, end);
193
+ } else {
194
+ names = first.fields.map((_, i) => `col${i}`);
195
+ }
196
+ const nFields = names.length;
197
+
198
+ const isNumber = names.map(() => true), isDate = names.map(() => true);
199
+ const distinct = names.map(() => new Set());
200
+ const filled = names.map(() => 0);
201
+ for (let i = start, r = 0; i < end && r < sampleRows; r++) {
202
+ const row = splitRow(bytes, i, end, delim);
203
+ row.fields.forEach((f, k) => {
204
+ if (k >= nFields || f.e === f.s) return;
205
+ const text = fieldText(bytes, f);
206
+ filled[k]++;
207
+ distinct[k].add(text);
208
+ if (isNumber[k] && Number.isNaN(parseNumber(bytes, f.s, f.e)) && text.trim() !== 'NaN') isNumber[k] = false;
209
+ if (isDate[k] && !looksLikeDate(text)) isDate[k] = false;
210
+ });
211
+ i = row.next;
212
+ }
213
+ // Text where most sampled values differ (ids, notes, emails) is free text;
214
+ // text that repeats (regions, statuses) is a category.
215
+ const guess = (k) => (isNumber[k] ? 'number' : isDate[k] && filled[k] ? 'date' : filled[k] >= 50 && distinct[k].size > filled[k] / 2 ? 'string' : 'category');
216
+ const kinds = names.map((nm, f) => {
217
+ let kind = types[nm] ?? guess(f);
218
+ if (kind === 'text') kind = guess(f) === 'string' ? 'string' : 'category';
219
+ if (!(kind in KIND)) throw new Error(`vectro: type for "${nm}" must be 'number', 'category', 'string', 'text' or 'date'`);
220
+ return kind;
221
+ });
222
+ return { delim, names, kinds, headerStart, start, end };
223
+ }
224
+
225
+ // Fields of one row (quote-aware): [{ s, e, escaped }], plus where the next row starts.
226
+ function splitRow(bytes, i, end, delim) {
227
+ const fields = [];
228
+ for (;;) {
229
+ let s, e, escaped = false;
230
+ if (i < end && bytes[i] === QUOTE) {
231
+ s = ++i;
232
+ for (;;) {
233
+ if (i >= end) { e = i; break; }
234
+ if (bytes[i] === QUOTE) {
235
+ if (bytes[i + 1] === QUOTE) { escaped = true; i += 2; continue; }
236
+ e = i++;
237
+ break;
238
+ }
239
+ i++;
240
+ }
241
+ while (i < end && bytes[i] !== delim && bytes[i] !== NL) i++;
242
+ } else {
243
+ s = i;
244
+ while (i < end && bytes[i] !== delim && bytes[i] !== NL) i++;
245
+ e = i;
246
+ if (e > s && bytes[e - 1] === CR) e--;
247
+ }
248
+ fields.push({ s, e, escaped });
249
+ if (i < end && bytes[i] === delim) { i++; continue; }
250
+ return { fields, next: i + 1 };
251
+ }
252
+ }
253
+
254
+ function fieldText(bytes, f) {
255
+ const str = decode(bytes, f.s, f.e);
256
+ return f.escaped ? str.replaceAll('""', '"') : str;
257
+ }
258
+
259
+ // Column names must work in expressions: "Order Date" -> Order_Date.
260
+ function cleanNames(raw) {
261
+ const seen = new Map();
262
+ return raw.map((r, i) => {
263
+ let nm = r.trim().replace(/[^A-Za-z0-9_]/g, '_') || `col${i}`;
264
+ if (/^\d/.test(nm)) nm = `_${nm}`;
265
+ if (RESERVED.has(nm)) nm = `${nm}_`;
266
+ const k = seen.get(nm) ?? 0;
267
+ seen.set(nm, k + 1);
268
+ return k ? `${nm}_${k + 1}` : nm;
269
+ });
270
+ }