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/dates.mjs ADDED
@@ -0,0 +1,139 @@
1
+ // Dates and times. A date column holds milliseconds since 1970-01-01 UTC
2
+ // (float64, exact for any date people use); missing dates are NaN. This file
3
+ // parses ISO 8601 text (from strings or straight from CSV bytes) and expands
4
+ // the date functions of expressions into plain arithmetic, so they run in the
5
+ // same fused kernels as everything else (no new kernel code):
6
+ // year(ts) month(ts) day(ts) hour(ts) minute(ts) second(ts)
7
+ // weekday(ts) (1 = Monday ... 7 = Sunday, ISO)
8
+ // dayStart(ts) hourStart(ts) weekStart(ts) (Monday) monthStart(ts) yearStart(ts) -> ms
9
+ // date('2024-03-01') / date('2024-03-01T12:00:00Z') -> ms
10
+ // Times are UTC; text with an offset (+02:00) is converted to UTC.
11
+
12
+ export const DAY = 86_400_000, HOUR = 3_600_000, MINUTE = 60_000;
13
+
14
+ // Days since 1970-01-01 of a civil date (proleptic Gregorian, any year).
15
+ export function daysFromCivil(y, m, d) {
16
+ y -= m <= 2 ? 1 : 0;
17
+ const era = Math.floor(y / 400), yoe = y - era * 400;
18
+ const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
19
+ return era * 146097 + yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy - 719468;
20
+ }
21
+
22
+ // ISO 8601 date or date-time from bytes [s, e): YYYY-MM-DD, then optionally
23
+ // T or space, HH:MM, :SS, .fraction, and Z or ±HH:MM / ±HHMM. NaN if it isn't one.
24
+ export function parseDateBytes(b, s, e) {
25
+ while (s < e && (b[s] === 32 || b[s] === 34)) s++; // spaces, quotes
26
+ while (e > s && (b[e - 1] === 32 || b[e - 1] === 34 || b[e - 1] === 13)) e--;
27
+ let i = s, neg = false;
28
+ if (b[i] === 45 || b[i] === 43) { neg = b[i] === 45; i++; } // -YYYY / +YYYYY
29
+ const num = (n) => { let v = 0; for (let k = 0; k < n; k++, i++) { const c = b[i] - 48; if (!(c >= 0 && c <= 9) || i >= e) return -1; v = v * 10 + c; } return v; };
30
+ let y = 0, digits = 0;
31
+ while (i < e && b[i] >= 48 && b[i] <= 57) { y = y * 10 + (b[i] - 48); i++; digits++; }
32
+ if (digits < 4 || b[i] !== 45) return NaN;
33
+ i++;
34
+ const mo = num(2);
35
+ if (mo < 1 || mo > 12 || b[i] !== 45) return NaN;
36
+ i++;
37
+ const d = num(2);
38
+ if (d < 1 || d > 31) return NaN;
39
+ let ms = daysFromCivil(neg ? -y : y, mo, d) * DAY;
40
+ if (i === e) return ms;
41
+ if (b[i] !== 84 && b[i] !== 116 && b[i] !== 32) return NaN; // T, t or space
42
+ i++;
43
+ const h = num(2);
44
+ if (h < 0 || h > 24 || b[i] !== 58) return NaN;
45
+ i++;
46
+ const mi = num(2);
47
+ if (mi < 0 || mi > 59) return NaN;
48
+ ms += h * HOUR + mi * MINUTE;
49
+ if (i < e && b[i] === 58) {
50
+ i++;
51
+ const sec = num(2);
52
+ if (sec < 0 || sec > 60) return NaN;
53
+ ms += sec * 1000;
54
+ if (i < e && (b[i] === 46 || b[i] === 44)) { // fraction of a second
55
+ i++;
56
+ let scale = 100, any = false;
57
+ while (i < e && b[i] >= 48 && b[i] <= 57) { ms += (b[i] - 48) * scale; scale /= 10; i++; any = true; }
58
+ if (!any) return NaN;
59
+ }
60
+ }
61
+ if (i === e) return ms;
62
+ if (b[i] === 90 || b[i] === 122) return i + 1 === e ? ms : NaN; // Z
63
+ if (b[i] === 43 || b[i] === 45) {
64
+ const sign = b[i] === 45 ? -1 : 1;
65
+ i++;
66
+ const oh = num(2);
67
+ if (oh < 0) return NaN;
68
+ if (i < e && b[i] === 58) i++;
69
+ const om = i < e ? num(2) : 0;
70
+ if (om < 0 || i !== e) return NaN;
71
+ return ms - sign * (oh * HOUR + om * MINUTE);
72
+ }
73
+ return NaN;
74
+ }
75
+
76
+ const enc = new TextEncoder();
77
+ export function parseDateText(s) {
78
+ const b = enc.encode(String(s));
79
+ return parseDateBytes(b, 0, b.length);
80
+ }
81
+
82
+ // Does this text look like an ISO date (for guessing CSV column types)?
83
+ export const looksLikeDate = (s) => /^\s*"?[+-]?\d{4,}-\d\d-\d\d([Tt ]\d\d:\d\d(:\d\d([.,]\d+)?)?([Zz]|[+-]\d\d:?\d\d)?)?"?\s*$/.test(s) && !Number.isNaN(parseDateText(s));
84
+
85
+ // A value given for a date: Date object, ISO text or milliseconds.
86
+ export function toMillis(v) {
87
+ if (v instanceof Date) return v.getTime();
88
+ if (typeof v === 'number') return v;
89
+ if (v == null || v === '') return NaN;
90
+ return parseDateText(v);
91
+ }
92
+
93
+ // ---- expression functions (AST in, AST out) ----------------------------------
94
+ const num = (v) => ({ t: 'num', v });
95
+ const bin = (op, l, r) => ({ t: 'bin', op, l, r });
96
+ const add = (l, r) => bin('+', l, r), sub = (l, r) => bin('-', l, r), mul = (l, r) => bin('*', l, r), div = (l, r) => bin('/', l, r);
97
+ const floor = (x) => ({ t: 'call', fn: 'floor', args: [x] });
98
+ const cond = (c, x, y) => ({ t: 'cond', c, x, y });
99
+ const fdiv = (x, k) => floor(div(x, num(k)));
100
+ const mod = (x, k) => sub(x, mul(fdiv(x, k), num(k)));
101
+
102
+ // Civil date parts of ms (Howard Hinnant's days -> civil algorithm).
103
+ function civil(ts) {
104
+ const z = add(fdiv(ts, DAY), num(719468));
105
+ const era = fdiv(z, 146097);
106
+ const doe = sub(z, mul(era, num(146097)));
107
+ const yoe = fdiv(sub(add(sub(doe, fdiv(doe, 1460)), fdiv(doe, 36524)), fdiv(doe, 146096)), 365);
108
+ const doy = sub(doe, sub(add(mul(num(365), yoe), fdiv(yoe, 4)), fdiv(yoe, 100)));
109
+ const mp = fdiv(add(mul(num(5), doy), num(2)), 153);
110
+ const d = add(sub(doy, fdiv(add(mul(num(153), mp), num(2)), 5)), num(1));
111
+ const m = cond(bin('<', mp, num(10)), add(mp, num(3)), sub(mp, num(9)));
112
+ const y = add(add(yoe, mul(era, num(400))), cond(bin('<=', m, num(2)), num(1), num(0)));
113
+ return { y, m, d };
114
+ }
115
+
116
+ export const DATE_FNS = {
117
+ year: (ts) => civil(ts).y,
118
+ month: (ts) => civil(ts).m,
119
+ day: (ts) => civil(ts).d,
120
+ hour: (ts) => mod(fdiv(ts, HOUR), 24),
121
+ minute: (ts) => mod(fdiv(ts, MINUTE), 60),
122
+ second: (ts) => mod(fdiv(ts, 1000), 60),
123
+ weekday: (ts) => add(mod(add(fdiv(ts, DAY), num(3)), 7), num(1)), // 1970-01-01 was a Thursday
124
+ dayStart: (ts) => mul(fdiv(ts, DAY), num(DAY)),
125
+ hourStart: (ts) => mul(fdiv(ts, HOUR), num(HOUR)),
126
+ weekStart: (ts) => mul(sub(fdiv(ts, DAY), mod(add(fdiv(ts, DAY), num(3)), 7)), num(DAY)),
127
+ monthStart: (ts) => sub(mul(fdiv(ts, DAY), num(DAY)), mul(sub(civil(ts).d, num(1)), num(DAY))),
128
+ yearStart: (ts) => { // January 1 of the year: days from civil (y, 1, 1)
129
+ const y = sub(civil(ts).y, num(1)), era = fdiv(y, 400), yoe = sub(y, mul(era, num(400)));
130
+ const doe = add(sub(add(mul(yoe, num(365)), fdiv(yoe, 4)), fdiv(yoe, 100)), num(306));
131
+ return mul(sub(add(mul(era, num(146097)), doe), num(719468)), num(DAY));
132
+ },
133
+ };
134
+ // functions whose result is itself a date
135
+ export const DATE_RESULT = new Set(['dayStart', 'hourStart', 'weekStart', 'monthStart', 'yearStart', 'date']);
136
+
137
+ // Times in milliseconds need about 41 bits; float32 keeps 24, so a date column
138
+ // in a Float32Array table would be off by seconds to minutes: refused.
139
+ export const f32Dates = (name) => `vectro: "${name}" is a date column, and a Float32Array table can't hold times exactly (they'd be off by seconds); use the default Float64Array, or store the dates as numbers you choose (days since a start, say)`;
@@ -0,0 +1,109 @@
1
+ // Emitter: the sending side of binary Vectro streams (Node). Collectors
2
+ // connect to it, typically one connection per collector worker, and every
3
+ // batch you write goes out as column frames, dealt round-robin to the
4
+ // connections that aren't backed up.
5
+ //
6
+ // const feed = await Emitter.listen({ port: 9000, columns: { ts: 'number', price: 'number', region: 'category' } });
7
+ // await feed.write({ ts, price, region }); // arrays of one batch (any length)
8
+ // await feed.close();
9
+ //
10
+ // On the collector: engine.streamAsync({ format: 'vectro', columns }) and
11
+ // stream.connect(worker, { host, port }) for each worker.
12
+ //
13
+ // format: 'arrow' sends an Arrow IPC stream per connection instead (numbers
14
+ // as float64, categories as dictionary-encoded strings with delta
15
+ // dictionaries), which pyarrow, Polars and other Arrow readers understand.
16
+ import { createServer } from 'node:net';
17
+ import { encodeSchema, encodeEnd, BatchEncoder } from './frames.mjs';
18
+ import { ArrowEncoder } from './arrow.mjs';
19
+
20
+ const ENCODERS = {
21
+ vectro(columns) {
22
+ const e = new BatchEncoder(columns);
23
+ return { schema: () => encodeSchema(columns), encode: (batch) => e.encode(batch), end: encodeEnd };
24
+ },
25
+ arrow: (columns) => new ArrowEncoder(columns),
26
+ };
27
+
28
+ const MAX_FRAME_ROWS = 65_536; // larger batches are split into frames this size
29
+ const HIGH_WATER = 8 * 1024 * 1024; // bytes queued on a connection before it counts as full
30
+
31
+ export class Emitter {
32
+ constructor(server, columns, format) {
33
+ this.server = server;
34
+ this.columns = columns;
35
+ this.format = format;
36
+ this.conns = []; // { socket, encoder }
37
+ this.next = 0; // round-robin position
38
+ this.waiting = []; // writers waiting for room or a connection
39
+ this.rowsSent = 0;
40
+ }
41
+
42
+ static listen({ port = 0, host = '127.0.0.1', columns, format = 'vectro' }) {
43
+ if (!columns || !Object.keys(columns).length) throw new Error('vectro: Emitter.listen needs columns, e.g. { ts: "number", region: "category" }');
44
+ if (!ENCODERS[format]) throw new Error(`vectro: Emitter format must be 'vectro' or 'arrow', got "${format}"`);
45
+ ENCODERS[format](columns); // check the column kinds now
46
+ const server = createServer();
47
+ const em = new Emitter(server, columns, format);
48
+ server.on('connection', (socket) => em.#add(socket));
49
+ return new Promise((resolve, reject) => {
50
+ server.once('error', reject);
51
+ server.listen(port, host, () => resolve(em));
52
+ });
53
+ }
54
+
55
+ get port() { return this.server.address().port; }
56
+ get connections() { return this.conns.length; }
57
+
58
+ #add(socket) {
59
+ socket.setNoDelay(true);
60
+ const conn = { socket, encoder: ENCODERS[this.format](this.columns) };
61
+ socket.write(conn.encoder.schema());
62
+ socket.on('drain', () => this.#wake());
63
+ const drop = () => { this.conns = this.conns.filter((c) => c !== conn); };
64
+ socket.on('close', drop);
65
+ socket.on('error', drop);
66
+ this.conns.push(conn);
67
+ this.#wake();
68
+ }
69
+
70
+ #wake() { for (const resolve of this.waiting.splice(0)) resolve(); }
71
+
72
+ // Resolves once `n` collector connections are open.
73
+ async waitForConnections(n) {
74
+ while (this.conns.length < n) await new Promise((r) => this.waiting.push(r));
75
+ }
76
+
77
+ // A connection with room, round-robin; waits when all are full (backpressure).
78
+ async #pick() {
79
+ for (;;) {
80
+ for (let tries = 0; tries < this.conns.length; tries++) {
81
+ const c = this.conns[this.next++ % this.conns.length];
82
+ if (c.socket.writableLength < HIGH_WATER) return c;
83
+ }
84
+ await new Promise((r) => this.waiting.push(r));
85
+ }
86
+ }
87
+
88
+ // batch: { column: Float64Array | number[] | string[] }, all the same length.
89
+ async write(batch) {
90
+ const names = Object.keys(this.columns);
91
+ const first = batch[names[0]];
92
+ const rows = (first?.codes ?? first)?.length ?? 0;
93
+ const cut = (v, a, b) => (v.codes ? { codes: v.codes.subarray ? v.codes.subarray(a, b) : v.codes.slice(a, b), labels: v.labels } : v.subarray ? v.subarray(a, b) : v.slice(a, b));
94
+ for (let at = 0; at < rows; at += MAX_FRAME_ROWS) {
95
+ const end = Math.min(rows, at + MAX_FRAME_ROWS);
96
+ const part = at === 0 && end === rows ? batch : Object.fromEntries(names.map((n) => [n, cut(batch[n], at, end)]));
97
+ const conn = await this.#pick();
98
+ conn.socket.write(conn.encoder.encode(part));
99
+ this.rowsSent += end - at;
100
+ }
101
+ }
102
+
103
+ // Ends every connection cleanly and stops listening.
104
+ async close() {
105
+ for (const { socket, encoder } of this.conns) socket.end(encoder.end());
106
+ await new Promise((r) => this.server.close(r));
107
+ this.#wake();
108
+ }
109
+ }