vectrojs 0.0.0-stage → 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 (64) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +822 -2
  4. package/bin/vectro.mjs +82 -0
  5. package/dist/vectro.wasm +0 -0
  6. package/mcp/server.mjs +288 -0
  7. package/package.json +91 -4
  8. package/src/arrow.mjs +905 -0
  9. package/src/bitpack.mjs +135 -0
  10. package/src/browser.mjs +129 -0
  11. package/src/bytes.mjs +60 -0
  12. package/src/csv-kernel.mjs +205 -0
  13. package/src/csv.mjs +270 -0
  14. package/src/dataset.mjs +1763 -0
  15. package/src/dates.mjs +139 -0
  16. package/src/emitter.mjs +109 -0
  17. package/src/engine.mjs +881 -0
  18. package/src/files-browser.mjs +104 -0
  19. package/src/files-node.mjs +59 -0
  20. package/src/frames.mjs +191 -0
  21. package/src/fuse.mjs +512 -0
  22. package/src/interp.mjs +305 -0
  23. package/src/join.mjs +534 -0
  24. package/src/kernels.mjs +312 -0
  25. package/src/ndjson.mjs +136 -0
  26. package/src/node.mjs +120 -0
  27. package/src/pack.mjs +114 -0
  28. package/src/parallel-sort.mjs +99 -0
  29. package/src/partition-kernels.mjs +388 -0
  30. package/src/partition.mjs +395 -0
  31. package/src/plan.mjs +551 -0
  32. package/src/portable.mjs +21 -0
  33. package/src/rank-kernels.mjs +81 -0
  34. package/src/rows.mjs +91 -0
  35. package/src/serve/catalog.mjs +148 -0
  36. package/src/serve/flight.mjs +270 -0
  37. package/src/serve/formats.mjs +87 -0
  38. package/src/serve/http.mjs +182 -0
  39. package/src/serve/index.mjs +9 -0
  40. package/src/serve/odata.mjs +291 -0
  41. package/src/serve/pg.mjs +588 -0
  42. package/src/serve/pgcatalog.mjs +220 -0
  43. package/src/serve/pgtypes.mjs +199 -0
  44. package/src/sketch.mjs +75 -0
  45. package/src/sort-kernels.mjs +117 -0
  46. package/src/sort.mjs +102 -0
  47. package/src/split-worker-core.mjs +252 -0
  48. package/src/split-worker-node.mjs +8 -0
  49. package/src/split-worker.mjs +7 -0
  50. package/src/split.mjs +741 -0
  51. package/src/sql.mjs +1558 -0
  52. package/src/sqltext.mjs +132 -0
  53. package/src/static-kernels.mjs +164 -0
  54. package/src/stream-worker.mjs +553 -0
  55. package/src/table-kernels.mjs +323 -0
  56. package/src/table.mjs +2206 -0
  57. package/src/textmap.mjs +159 -0
  58. package/src/vectors.mjs +138 -0
  59. package/src/wasm.mjs +254 -0
  60. package/src/window.mjs +160 -0
  61. package/src/worker-browser.mjs +21 -0
  62. package/src/worker-loop.mjs +106 -0
  63. package/src/worker.mjs +22 -0
  64. package/src/zones.mjs +75 -0
package/src/plan.mjs ADDED
@@ -0,0 +1,551 @@
1
+ // Lazy queries: table.lazy() records a chain, optimizes it as a whole, then
2
+ // runs it with the table methods. Nothing runs until collect(), toRows(),
3
+ // agg(), count() (or their Async versions).
4
+ //
5
+ // orders.lazy()
6
+ // .derive('revenue', 'price * qty')
7
+ // .filter("region == 'EU' && revenue > 100")
8
+ // .groupBy('customer').agg({ spend: 'sum(revenue)', orders: 'count()' })
9
+ // .sort('-spend').limit(10)
10
+ // .collect();
11
+ //
12
+ // What the optimizer does:
13
+ // - derived columns are inlined: filters, aggregations and sort keys use
14
+ // the expression itself, so `revenue` above is never built as a column;
15
+ // a derived column that is needed as a column (output, group key, join
16
+ // key) is built after the filters, on the matching rows only;
17
+ // - filters move down: below derives (by inlining), selects and sorts,
18
+ // below groupBy when they only use its keys, and into each side of a join
19
+ // when they only use that side's columns;
20
+ // - only the columns used later are carried (joins, sorts and collects copy
21
+ // nothing else), and derives nobody uses are dropped;
22
+ // - sort + limit becomes top(k), which doesn't sort everything;
23
+ // - parameters become constants, and constant parts of expressions are
24
+ // computed once (in the kernel compiler, which also computes repeated
25
+ // parts of an expression once per row).
26
+ // explain() prints the optimized plan.
27
+
28
+ import { parse, toSource, walk } from './fuse.mjs';
29
+ import { DATE_RESULT } from './dates.mjs';
30
+
31
+ export const vars = (ast) => { const out = new Set(); walk(ast, (n) => { if (n.t === 'var') out.add(n.name); }); return out; };
32
+ const and = (a, b) => (a ? { t: 'bin', op: '&&', l: a, r: b } : b);
33
+ function conjuncts(ast) { return ast.t === 'bin' && ast.op === '&&' ? [...conjuncts(ast.l), ...conjuncts(ast.r)] : [ast]; }
34
+
35
+ // Parameters as constants (dates as ms); text stays text.
36
+ function bindParams(ast, params) {
37
+ if (!params || !Object.keys(params).length) return ast;
38
+ const sub = (n) => {
39
+ if (n.t === 'var' && n.name in params) {
40
+ const v = params[n.name] instanceof Date ? params[n.name].getTime() : params[n.name];
41
+ return typeof v === 'string' ? { t: 'str', v } : { t: 'num', v };
42
+ }
43
+ if (n.t === 'bin') return { ...n, l: sub(n.l), r: sub(n.r) };
44
+ if (n.t === 'un') return { ...n, x: sub(n.x) };
45
+ if (n.t === 'call') return { ...n, args: n.args.map(sub) };
46
+ if (n.t === 'cond') return { ...n, c: sub(n.c), x: sub(n.x), y: sub(n.y) };
47
+ return n;
48
+ };
49
+ return sub(ast);
50
+ }
51
+
52
+ // Replace derived names by their expressions. A text literal compared with a
53
+ // derived date (monthStart(ts) >= '2024-01-01') becomes date('...').
54
+ function inline(ast, derived) {
55
+ if (!derived.size) return ast;
56
+ const isDerivedDate = (n) => n.t === 'var' && derived.get(n.name)?.date;
57
+ const sub = (n) => {
58
+ if (n.t === 'var' && derived.has(n.name)) return derived.get(n.name).ast;
59
+ if (n.t === 'bin') {
60
+ let { l, r } = n;
61
+ if (l.t === 'str' && isDerivedDate(r)) l = { t: 'call', fn: 'date', args: [l] };
62
+ if (r.t === 'str' && isDerivedDate(l)) r = { t: 'call', fn: 'date', args: [r] };
63
+ return { ...n, l: sub(l), r: sub(r) };
64
+ }
65
+ if (n.t === 'un') return { ...n, x: sub(n.x) };
66
+ if (n.t === 'call') return { ...n, args: n.args.map(sub) };
67
+ if (n.t === 'cond') return { ...n, c: sub(n.c), x: sub(n.x), y: sub(n.y) };
68
+ return n;
69
+ };
70
+ return sub(ast);
71
+ }
72
+
73
+ // Sort keys: '-name' or an expression.
74
+ const IDENT = /^\s*(-?)\s*([A-Za-z_]\w*)\s*$/;
75
+ const keyList = (by) => (Array.isArray(by) ? by : [by]);
76
+
77
+ export class Query {
78
+ constructor(node) { this.node = node; }
79
+
80
+ filter(expr, params = {}) { return new Query({ op: 'filter', input: this.node, ast: bindParams(parse(expr), params) }); }
81
+ derive(name, expr, params = {}) { return new Query({ op: 'derive', input: this.node, name, ast: bindParams(parse(expr), params) }); }
82
+ select(...names) { return new Query({ op: 'select', input: this.node, names: names.flat() }); }
83
+ sort(by) { return new Query({ op: 'sort', input: this.node, by: keyList(by) }); }
84
+ limit(n, offset = 0) { return new Query({ op: 'limit', input: this.node, n: Math.max(0, Math.floor(n)), offset: Math.max(0, Math.floor(offset)) }); }
85
+ top(k, by) { return this.sort(by).limit(k); }
86
+ join(other, opts) {
87
+ const right = other instanceof Query ? other.node : { op: 'scan', table: other };
88
+ return new Query({ op: 'join', left: this.node, right, opts: opts ?? {} });
89
+ }
90
+ // (SQL) a column computed by a JavaScript function of whole values, once
91
+ // per distinct combination (text functions): see Table._textmap. The spec's
92
+ // state keeps the labels, so it is made here once for this step. labels:
93
+ // the one input holds codes 0..L-1 of these labels already (no fn).
94
+ textmap(name, spec) { return new Query({ op: 'textmap', input: this.node, name, inputs: spec.inputs ?? [], fn: spec.fn, text: spec.text ?? true, date: spec.date ?? false, label: spec.label ?? 'fn', labels: spec.labels ?? null, state: spec.state ?? (spec.labels ? { dict: spec.labels.slice(), codes: new Map(spec.labels.map((l, i) => [l, i])) } : { dict: [], codes: new Map() }) }); }
95
+ // (SQL) window functions: see Table._window for the spec
96
+ window(spec) { return new Query({ op: 'window', input: this.node, partition: spec.partition ?? [], order: spec.order ?? [], items: spec.items }); }
97
+ // (SQL) the rows of several queries one after another, columns by position
98
+ static union(queries) { return new Query({ op: 'union', inputs: queries.map((q) => q.node) }); }
99
+ // (SQL RIGHT / FULL JOIN) the right rows that a join with opts.unmatched =
100
+ // state matched to no left row, columns `names` (the join's right side);
101
+ // only after that join ran (an earlier input of the same union).
102
+ static unmatched(state, names) { return new Query({ op: 'unmatched', state, names }); }
103
+ groupBy(keys) {
104
+ return { agg: (spec, params = {}) => new Query({ op: 'aggregate', input: this.node, keys: keyList(keys), spec: Object.fromEntries(Object.entries(spec).map(([k, v]) => [k, bindParams(parse(v), params)])) }) };
105
+ }
106
+
107
+ // ---- running --------------------------------------------------------------------
108
+ collect() { return runSync(this.#final()); }
109
+ collectAsync() { return runAsync(this.#final()); }
110
+ toRows(limit = Infinity) { const t = this.#limited(limit).collect(); try { return t.toRows(limit); } finally { t.dispose(); } }
111
+ async toRowsAsync(limit = Infinity) { const t = await this.#limited(limit).collectAsync(); try { return await t.toRowsAsync(limit); } finally { t.dispose(); } }
112
+ agg(spec, params = {}) { return runSync(this.#aggNode(spec, params)); }
113
+ aggAsync(spec, params = {}) { return runAsync(this.#aggNode(spec, params)); }
114
+ count() { return this.agg({ n: 'count()' }).n; }
115
+ async countAsync() { return (await this.aggAsync({ n: 'count()' })).n; }
116
+
117
+ // The optimized plan, one step per line (innermost first).
118
+ explain() { return describe(optimize(this.#final().node)); }
119
+
120
+ #final() { return new Query({ op: 'output', input: this.node }); }
121
+ // (on a dataset, only the rows asked for are read)
122
+ #limited(limit) { return Number.isFinite(limit) && findDataset(this.node) ? this.limit(limit) : this; }
123
+ #aggNode(spec, params) {
124
+ return new Query({ op: 'total', input: this.node, spec: Object.fromEntries(Object.entries(spec).map(([k, v]) => [k, bindParams(parse(v), params)])) });
125
+ }
126
+ }
127
+
128
+ // ---- optimizing: move filters down --------------------------------------------------
129
+
130
+ // Column names a node produces (null: unknown, e.g. a table we haven't seen).
131
+ export function namesOf(node) {
132
+ switch (node.op) {
133
+ case 'scan': return node.table.columnNames;
134
+ case 'filter': case 'sort': case 'limit': case 'output': return namesOf(node.input);
135
+ case 'derive': return [...new Set([...namesOf(node.input), node.name])];
136
+ case 'select': return node.names;
137
+ case 'aggregate': return [...node.keys, ...Object.keys(node.spec)];
138
+ case 'join': return joinNames(node).all;
139
+ case 'window': return [...namesOf(node.input).filter((n) => !node.items.some((it) => it.name === n)), ...node.items.map((it) => it.name)];
140
+ case 'textmap': return [...namesOf(node.input).filter((n) => n !== node.name), node.name];
141
+ case 'union': return namesOf(node.inputs[0]);
142
+ case 'unmatched': return node.names;
143
+ }
144
+ return [];
145
+ }
146
+
147
+ // Output names of a join, and where each comes from.
148
+ export function joinNames(node) {
149
+ const left = namesOf(node.left), right = namesOf(node.right);
150
+ const { on, suffix = '_right' } = node.opts;
151
+ const [lk, rk] = typeof on === 'string' || Array.isArray(on) ? [[on].flat(), [on].flat()] : [[on?.left].flat(), [on?.right].flat()];
152
+ const same = new Set(rk.filter((n, k) => lk[k] === n));
153
+ const fromRight = new Map(); // output name -> right column
154
+ const all = [...left];
155
+ for (const n of right) {
156
+ if (same.has(n)) continue;
157
+ let out = n;
158
+ while (all.includes(out)) out += suffix;
159
+ all.push(out);
160
+ fromRight.set(out, n);
161
+ }
162
+ return { all, left: new Set(left), fromRight };
163
+ }
164
+
165
+ const rename = (ast, map) => {
166
+ const sub = (n) => {
167
+ if (n.t === 'var' && map.has(n.name)) return { ...n, name: map.get(n.name) };
168
+ if (n.t === 'bin') return { ...n, l: sub(n.l), r: sub(n.r) };
169
+ if (n.t === 'un') return { ...n, x: sub(n.x) };
170
+ if (n.t === 'call') return { ...n, args: n.args.map(sub) };
171
+ if (n.t === 'cond') return { ...n, c: sub(n.c), x: sub(n.x), y: sub(n.y) };
172
+ return n;
173
+ };
174
+ return sub(ast);
175
+ };
176
+
177
+ // Place one condition as deep as it can go below `node`.
178
+ function pushFilter(node, cond) {
179
+ const used = vars(cond);
180
+ switch (node.op) {
181
+ case 'filter': return { ...node, input: pushFilter(node.input, cond) };
182
+ case 'sort': case 'select': return { ...node, input: pushFilter(node.input, cond) };
183
+ case 'derive': // below the derive, with its expression in place of its name
184
+ return { ...node, input: pushFilter(node.input, used.has(node.name) ? inline(cond, new Map([[node.name, { ast: node.ast, date: isDateExpr(node) }]])) : cond) };
185
+ case 'aggregate':
186
+ if ([...used].every((n) => node.keys.includes(n))) return { ...node, input: pushFilter(node.input, cond), pushed: (node.pushed ?? 0) + 1 };
187
+ break;
188
+ case 'textmap':
189
+ if (!used.has(node.name)) return { ...node, input: pushFilter(node.input, cond) };
190
+ break;
191
+ case 'window': { // (only a filter on the partition keys keeps every window the same)
192
+ if (node.partition.length && [...used].every((n) => node.partition.includes(n))) return { ...node, input: pushFilter(node.input, cond) };
193
+ const k = firstRanks(cond, node);
194
+ if (k !== null) return { op: 'filter', input: { ...node, firstRanks: Math.min(k, node.firstRanks ?? Infinity) }, ast: cond };
195
+ break;
196
+ }
197
+ case 'join': {
198
+ const { left, fromRight } = joinNames(node);
199
+ if ([...used].every((n) => left.has(n))) return { ...node, left: pushFilter(node.left, cond) };
200
+ if (node.opts.how !== 'left' && [...used].every((n) => fromRight.has(n))) return { ...node, right: pushFilter(node.right, rename(cond, fromRight)) };
201
+ break;
202
+ }
203
+ }
204
+ return { op: 'filter', input: node, ast: cond };
205
+ }
206
+ const isDateExpr = (node) => node.ast.t === 'call' && DATE_RESULT.has(node.ast.fn);
207
+
208
+ // A filter keeping only the first k rows (rank) of each partition, as
209
+ // `rn <= k`, `rn < k + 1`, `rn = k` or `k >= rn` on a ROW_NUMBER or RANK of
210
+ // this window: k, else null. The window then only needs the rows that can
211
+ // rank that high (see Table._window); every function of the node must give
212
+ // the same values on those rows (it reads only rows ordered before each row).
213
+ const PREFIX_FNS = new Set(['row_number', 'rank', 'dense_rank', 'lag']);
214
+ function firstRanks(cond, node) {
215
+ if (cond.t !== 'bin' || !node.order.length || !node.items.every((it) => PREFIX_FNS.has(it.fn))) return null;
216
+ const flip = { '>=': '<=', '>': '<', '==': '==' };
217
+ const [v, op, lim] = cond.l.t === 'var' && cond.r.t === 'num' ? [cond.l, cond.op, cond.r.v] : cond.r.t === 'var' && cond.l.t === 'num' ? [cond.r, flip[cond.op], cond.l.v] : [];
218
+ const item = v && node.items.find((it) => it.name === v.name);
219
+ if (!item || (item.fn !== 'row_number' && item.fn !== 'rank')) return null;
220
+ const k = op === '<=' || op === '==' ? Math.floor(lim) : op === '<' ? Math.ceil(lim) - 1 : NaN;
221
+ return k >= 1 && k <= 10_000 ? k : null;
222
+ }
223
+
224
+ export function optimize(node) {
225
+ switch (node.op) {
226
+ case 'scan': case 'unmatched': return node;
227
+ case 'filter': {
228
+ let input = optimize(node.input);
229
+ for (const c of conjuncts(node.ast)) input = pushFilter(input, c);
230
+ return input;
231
+ }
232
+ case 'join': return { ...node, left: optimize(node.left), right: optimize(node.right) };
233
+ case 'union': return { ...node, inputs: node.inputs.map(optimize) };
234
+ case 'aggregate': {
235
+ const input = optimize(node.input);
236
+ return { ...node, input };
237
+ }
238
+ default: return { ...node, input: optimize(node.input) };
239
+ }
240
+ }
241
+
242
+ // ---- running -------------------------------------------------------------------------
243
+ // The executor is a generator of table calls { table, method, args } (sync
244
+ // or async drivers run them), building on pipeline states:
245
+ // { table (a Table, maybe filtered), derived: name -> { ast, date }, visible }
246
+
247
+ function* execG(node, needed, temps) {
248
+ switch (node.op) {
249
+ case 'scan': return { table: node.table, derived: new Map(), visible: node.table.columnNames };
250
+ case 'unmatched': { // (the join that made it ran first, in this run)
251
+ const t = runState(temps).get(node.state);
252
+ if (!t) throw new Error('vectro: internal: unmatched rows before their join ran');
253
+ return { table: t, derived: new Map(), visible: node.names };
254
+ }
255
+ case 'filter': {
256
+ const s = yield* execG(node.input, union(needed, vars(node.ast)), temps);
257
+ return { ...s, table: s.table.filter(toSource(inline(node.ast, s.derived))) };
258
+ }
259
+ case 'derive': {
260
+ if (needed && !needed.has(node.name)) return yield* execG(node.input, needed, temps); // nobody uses it
261
+ const s = yield* execG(node.input, union(minus(needed, node.name), vars(node.ast)), temps);
262
+ const derived = new Map(s.derived).set(node.name, { ast: inline(node.ast, s.derived), date: isDateExpr(node) || (node.ast.t === 'var' && !!s.table.cols.get(node.ast.name)?.date) });
263
+ return { ...s, derived, visible: [...s.visible.filter((n) => n !== node.name), node.name] };
264
+ }
265
+ case 'select': {
266
+ const have = namesOf(node.input); // (only the columns used: a dataset's chunks hold just those)
267
+ for (const n of node.names) if ((!needed || needed.has(n)) && !have.includes(n)) throw new Error(`vectro: no column "${n}"`);
268
+ const s = yield* execG(node.input, needed ? new Set(node.names.filter((n) => needed.has(n))) : new Set(node.names), temps);
269
+ return { ...s, visible: node.names };
270
+ }
271
+ case 'aggregate': {
272
+ const fused = yield* lookupAggregateG(node, temps);
273
+ if (fused) return { table: fused, derived: new Map(), visible: fused.columnNames };
274
+ const specAsts = Object.values(node.spec);
275
+ const want = new Set([...node.keys, ...specAsts.flatMap((a) => [...vars(a)])]);
276
+ const s = yield* execG(node.input, want, temps);
277
+ const t = yield* materializeG(s, new Set(node.keys), temps, specAsts);
278
+ const spec = Object.fromEntries(Object.entries(node.spec).map(([k, a]) => [k, toSource(inline(a, s.derived))]));
279
+ const g = yield { table: t, method: node.keys.length ? 'groupAgg' : 'oneRow', args: [node.keys, spec] };
280
+ return { table: g, derived: new Map(), visible: g.columnNames };
281
+ }
282
+ case 'limit': case 'sort': {
283
+ const sortNode = node.op === 'sort' ? node : node.input.op === 'sort' ? node.input : null;
284
+ const k = node.op === 'limit' ? node.n + (node.offset ?? 0) : null;
285
+ const below = sortNode ? sortNode.input : node.input;
286
+ const keyAsts = (sortNode?.by ?? []).map((b) => parse(String(b).trim()));
287
+ const s = yield* execG(below, union(needed, new Set(keyAsts.flatMap((a) => [...vars(a)]))), temps);
288
+ const keep = needed ? s.visible.filter((n) => needed.has(n)) : s.visible;
289
+ // the rows to output, then derived output columns on those rows only
290
+ const later = keep.filter((n) => s.derived.has(n)).map((n) => s.derived.get(n).ast); // derived outputs: built on the rows kept
291
+ const base = yield* materializeG(s, new Set(keep.filter((n) => !s.derived.has(n))), temps, [...keyAsts, ...later], true);
292
+ let r;
293
+ if (sortNode) {
294
+ const by = sortNode.by.map((b) => {
295
+ const m = IDENT.exec(String(b));
296
+ if (m && !s.derived.has(m[2])) return String(b).trim(); // a column ('-region' stays a label sort)
297
+ return toSource(inline(parse(String(b)), s.derived));
298
+ });
299
+ r = yield { table: base, method: k === null ? 'sort' : 'top', args: k === null ? [by] : [k, by] };
300
+ } else r = yield { table: base, method: 'head', args: [k] };
301
+ if (node.offset) r = yield { table: r, method: 'slice', args: [node.offset] };
302
+ r = yield* deriveAllG(r, keep.filter((n) => s.derived.has(n)), s.derived);
303
+ return { table: r, derived: new Map(), visible: keep };
304
+ }
305
+ case 'window': {
306
+ const made = new Set(node.items.map((it) => it.name));
307
+ const inputs = [...node.partition, ...node.order.map((o) => o.name), ...node.items.map((it) => it.arg).filter(Boolean)];
308
+ const s = yield* execG(node.input, needed ? new Set([...[...needed].filter((n) => !made.has(n)), ...inputs]) : null, temps);
309
+ const keep = needed ? s.visible.filter((n) => needed.has(n) || inputs.includes(n)) : s.visible;
310
+ const t = yield* materializeG(s, new Set([...keep, ...inputs]), temps, [], true);
311
+ const w = yield { table: t.select(...new Set([...keep, ...inputs])), method: '_window', args: [{ partition: node.partition, order: node.order, items: node.items, firstRanks: node.firstRanks }] };
312
+ return { table: w, derived: new Map(), visible: [...keep, ...node.items.map((it) => it.name)] };
313
+ }
314
+ case 'textmap': {
315
+ const s = yield* execG(node.input, needed ? new Set([...[...needed].filter((n) => n !== node.name), ...node.inputs]) : null, temps);
316
+ const keep = (needed ? s.visible.filter((n) => needed.has(n) || node.inputs.includes(n)) : s.visible).filter((n) => n !== node.name);
317
+ const cols = new Set([...keep, ...node.inputs]);
318
+ const t = yield* materializeG(s, cols, temps, [], true);
319
+ const out = yield { table: cols.size ? t.select(...cols) : t, method: '_textmap', args: [node.name, node] };
320
+ return { table: out, derived: new Map(), visible: [...keep, node.name] };
321
+ }
322
+ case 'union': {
323
+ // only the columns used later (by position: each input names them its own way)
324
+ const names0 = namesOf(node.inputs[0]);
325
+ let at = names0.map((_, k) => k);
326
+ if (needed) { at = at.filter((k) => needed.has(names0[k])); if (!at.length) at = [0]; } // (the row count needs one)
327
+ const parts = [];
328
+ for (const input of node.inputs) {
329
+ const names = namesOf(input);
330
+ parts.push(yield* execG({ op: 'output', input: at.length === names.length ? input : { op: 'select', input, names: at.map((k) => names[k]) } }, null, temps));
331
+ }
332
+ const u = yield { table: parts[0], method: '_concat', args: [parts.slice(1)] };
333
+ return { table: u, derived: new Map(), visible: u.columnNames };
334
+ }
335
+ case 'join': {
336
+ const { left: leftNames, fromRight } = joinNames(node);
337
+ const { on } = node.opts;
338
+ const [lk, rk] = typeof on === 'string' || Array.isArray(on) ? [[on].flat(), [on].flat()] : [[on?.left].flat(), [on?.right].flat()];
339
+ const needL = new Set(lk), needR = new Set(rk);
340
+ for (const n of needed ?? namesOf(node)) { if (leftNames.has(n)) needL.add(n); else if (fromRight.has(n)) needR.add(fromRight.get(n)); else if (rk.includes(n)) needR.add(n); }
341
+ // (a right column renamed for a name it shares: those names come along, so the join names it the same)
342
+ const suffix = node.opts.suffix ?? '_right';
343
+ for (const [out, n] of fromRight) {
344
+ if (out === n || !needR.has(n)) continue;
345
+ for (let x = n; x !== out; x += suffix) { if (leftNames.has(x)) needL.add(x); else if (fromRight.has(x)) needR.add(fromRight.get(x)); }
346
+ }
347
+ const sl = yield* execG(node.left, needL, temps), sr = yield* execG(node.right, needR, temps);
348
+ const lt = yield* materializeG(sl, needL, temps, [], true), rt = yield* materializeG(sr, needR, temps, [], true);
349
+ // (RIGHT / FULL JOIN) the right rows it matched to none, kept for this run's 'unmatched' step
350
+ const holder = node.opts.unmatched ? {} : null;
351
+ const j = yield { table: lt, method: 'join', args: [rt, holder ? { ...node.opts, unmatched: holder } : node.opts] };
352
+ if (holder) { temps.push(holder.table); runState(temps).set(node.opts.unmatched, holder.table); }
353
+ return { table: j, derived: new Map(), visible: j.columnNames.filter((n) => n !== '__vectro_found') }; // (a view join's match flag)
354
+ }
355
+ case 'output': {
356
+ const s = yield* execG(node.input, null, temps);
357
+ const t = yield* materializeG(s, new Set(s.visible), temps, [], true);
358
+ const out = yield { table: t.select(...s.visible), method: 'collect', args: [] };
359
+ return out.columnNames.length === s.visible.length ? out : out.select(...s.visible); // (a filter's columns came along: drop them)
360
+ }
361
+ case 'groupInto': { // (datasets) add one chunk's matching rows into running group totals
362
+ const asts = [node.group, ...node.sources];
363
+ const s = yield* execG(node.input, new Set(asts.flatMap((a) => [...vars(a)])), temps);
364
+ const src = (a) => toSource(inline(a, s.derived));
365
+ return yield { table: s.table, method: '_groupInto', args: [src(node.group), node.K, node.sources.map(src), node.state] };
366
+ }
367
+ case 'total': {
368
+ const asts = Object.values(node.spec);
369
+ const s = yield* execG(node.input, new Set(asts.flatMap((a) => [...vars(a)])), temps);
370
+ const spec = Object.fromEntries(Object.entries(node.spec).map(([k, a]) => [k, toSource(inline(a, s.derived))]));
371
+ return yield { table: s.table, method: 'agg', args: [spec] };
372
+ }
373
+ }
374
+ throw new Error(`vectro: unknown plan step ${node.op}`);
375
+ }
376
+
377
+ // GROUP BY right columns of an inner join on one key, aggregating left
378
+ // columns only: the left rows (still filtered, packed columns as they are)
379
+ // go straight into groups found by looking up their key (see
380
+ // Table._lookupGroupAgg), no joined rows made. Null when that doesn't apply.
381
+ function* lookupAggregateG(node, temps) {
382
+ const J = node.input;
383
+ if (J.op !== 'join' || (J.opts.how ?? 'inner') !== 'inner' || !node.keys.length) return null;
384
+ const lazyOnly = (n) => n.op === 'scan' || ((n.op === 'filter' || n.op === 'derive' || n.op === 'select') && lazyOnly(n.input)); // (views: cheap to make again if this path declines)
385
+ if (!lazyOnly(J.left) || !lazyOnly(J.right)) return null;
386
+ const { on } = J.opts;
387
+ const [lk, rk] = typeof on === 'string' || Array.isArray(on) ? [[on].flat(), [on].flat()] : [[on?.left].flat(), [on?.right].flat()];
388
+ if (lk.length !== 1 || rk.length !== 1) return null;
389
+ const { left: leftNames, fromRight } = joinNames(J);
390
+ if (!node.keys.every((k) => fromRight.has(k))) return null;
391
+ const specAsts = Object.values(node.spec);
392
+ const used = new Set(specAsts.flatMap((a) => [...vars(a)]));
393
+ if (![...used].every((n) => leftNames.has(n))) return null;
394
+ const rightKeys = node.keys.map((k) => fromRight.get(k));
395
+ const sl = yield* execG(J.left, new Set([...used, lk[0]]), temps);
396
+ if (sl.derived.has(lk[0])) return null;
397
+ const sr = yield* execG(J.right, new Set([...rightKeys, rk[0]]), temps);
398
+ if (sr.derived.has(rk[0]) || rightKeys.some((k) => sr.derived.has(k))) return null;
399
+ const spec = Object.fromEntries(Object.entries(node.spec).map(([k, a]) => [k, toSource(inline(a, sl.derived))]));
400
+ const base = new Set([lk[0]]);
401
+ for (const a of Object.values(spec)) for (const v of vars(parse(a))) if (sl.table.cols.has(v)) base.add(v);
402
+ const lt = sl.table.select(...base), rt = sr.table.select(...new Set([rk[0], ...rightKeys]));
403
+ return yield { table: lt, method: '_lookupGroupAgg', args: [rt, { left: lk[0], right: rk[0] }, node.keys, rightKeys, spec] };
404
+ }
405
+
406
+ // A table holding `names` (base or derived) of state s, still filtered.
407
+ // Derived columns are built on the matching rows only (collect first).
408
+ // exprs: expressions that will be evaluated on it (their columns are kept).
409
+ function* materializeG(s, names, temps, exprs = [], keepVisibleOrder = false) {
410
+ const derivedNames = [...names].filter((n) => s.derived.has(n));
411
+ const base = new Set([...names].filter((n) => !s.derived.has(n)));
412
+ for (const n of derivedNames) for (const v of vars(s.derived.get(n).ast)) base.add(v);
413
+ for (const e of exprs) for (const v of vars(inline(e, s.derived))) if (s.table.cols.has(v)) base.add(v);
414
+ for (const n of base) if (!s.table.cols.has(n)) throw new Error(`vectro: no column "${n}"`);
415
+ let t = base.size ? s.table.select(...base) : s.table;
416
+ if (!derivedNames.length) return t;
417
+ if (t.pred) t = yield { table: t, method: 'collect', args: [] };
418
+ return yield* deriveAllG(t, derivedNames, s.derived);
419
+ }
420
+
421
+ // The derived columns `names` on table t. One named like a column t still
422
+ // has (shadowed: SELECT price AS cust, cust AS price) is made under a fresh
423
+ // name, then the names are swapped in a view; a derived name for that very
424
+ // column is the column itself.
425
+ function* deriveAllG(t, names, derived) {
426
+ const swap = new Map();
427
+ for (const n of names) {
428
+ const d = derived.get(n);
429
+ if (d.ast.t === 'var' && d.ast.name === n && t.cols.has(n)) continue;
430
+ const at = t.cols.has(n) ? `__vectro_d${swap.size}_${n}` : n;
431
+ if (at !== n) swap.set(at, n);
432
+ t = yield { table: t, method: 'derive', args: [at, toSource(d.ast)], date: d.date };
433
+ }
434
+ if (swap.size) t = t._renamed(swap);
435
+ return t;
436
+ }
437
+
438
+ const union = (a, b) => (a ? new Set([...a, ...b]) : null);
439
+ // what one run of a plan keeps between steps (its temps list stands for the run)
440
+ const runStates = new WeakMap();
441
+ const runState = (temps) => { let m = runStates.get(temps); if (!m) runStates.set(temps, (m = new Map())); return m; };
442
+ const minus = (a, name) => (a ? new Set([...a].filter((n) => n !== name)) : null);
443
+
444
+ // Free the intermediate tables, except columns the result still uses.
445
+ function release(result, temps) {
446
+ const used = new Set();
447
+ if (result?.cols) for (const c of result.cols.values()) used.add(c.data);
448
+ for (const t of temps) {
449
+ if (t === result) continue;
450
+ for (const name of [...t.owned]) {
451
+ const c = t.cols.get(name);
452
+ if (c && used.has(c.data)) { // the result shares it: the result owns it now
453
+ t.owned = t.owned.filter((n) => n !== name);
454
+ const outName = [...result.cols].find(([, rc]) => rc.data === c.data)?.[0];
455
+ if (outName && !result.owned.includes(outName)) result.owned.push(outName);
456
+ }
457
+ }
458
+ t.dispose();
459
+ }
460
+ }
461
+
462
+ function call(req, sync) {
463
+ const { table, method, args } = req;
464
+ const suffix = sync ? '' : 'Async';
465
+ if (method === 'groupAgg') return table.groupBy(args[0])[`agg${suffix}`](args[1]);
466
+ if (method === 'oneRow') { // totals without keys, as a one-row table
467
+ const asTable = (o) => table.engine.table(Object.fromEntries(Object.entries(o).map(([k, v]) => [k, [v]])));
468
+ return sync ? asTable(table.agg(args[1])) : table.aggAsync(args[1]).then(asTable);
469
+ }
470
+ if (method === 'derive' && req.date) { // keep the date flag of dayStart(ts) and friends
471
+ const mark = (t) => { t.cols.get(args[0]).date = true; return t; };
472
+ return sync ? mark(table.derive(...args)) : table.deriveAsync(...args).then(mark);
473
+ }
474
+ return table[`${method}${suffix}`](...args);
475
+ }
476
+
477
+ // Tables a call made (not the one it was called on) are intermediate: freed
478
+ // at the end unless the result uses their columns.
479
+ const record = (temps, req, out) => { if (out && out !== req.table && out.cols && !temps.includes(out)) temps.push(out); return out; };
480
+
481
+ // A dataset (tables on disk, see dataset.mjs) in the plan: it runs the plan
482
+ // chunk by chunk.
483
+ export function findDataset(node) {
484
+ if (node.op === 'scan') return node.table.isDataset ? node.table : null;
485
+ if (node.op === 'unmatched') return null;
486
+ if (node.op === 'union') { for (const i of node.inputs) { const d = findDataset(i); if (d) return d; } return null; }
487
+ return node.op === 'join' ? findDataset(node.left) ?? findDataset(node.right) : findDataset(node.input);
488
+ }
489
+
490
+ function runSync(q) {
491
+ const plan = optimize(q.node);
492
+ const ds = findDataset(plan);
493
+ return ds ? ds.runPlan(plan, true) : execute(plan, true);
494
+ }
495
+
496
+ function runAsync(q) {
497
+ const plan = optimize(q.node);
498
+ const ds = findDataset(plan);
499
+ return ds ? ds.runPlan(plan, false) : execute(plan, false);
500
+ }
501
+
502
+ // Run an optimized plan over tables in memory (sync: the result; else a promise).
503
+ export function execute(plan, sync) { return sync ? executeSync(plan) : executeAsync(plan); }
504
+
505
+ function executeSync(plan) {
506
+ const temps = [];
507
+ const g = execG(plan, null, temps);
508
+ let result;
509
+ try {
510
+ let step = g.next();
511
+ while (!step.done) step = g.next(record(temps, step.value, call(step.value, true)));
512
+ result = step.value;
513
+ return result;
514
+ } finally { release(result, temps); }
515
+ }
516
+
517
+ async function executeAsync(plan) {
518
+ const temps = [];
519
+ const g = execG(plan, null, temps);
520
+ let result;
521
+ try {
522
+ let step = g.next();
523
+ while (!step.done) step = g.next(record(temps, step.value, await call(step.value, false)));
524
+ result = step.value;
525
+ return result;
526
+ } finally { release(result, temps); }
527
+ }
528
+
529
+ // ---- explain -----------------------------------------------------------------------------
530
+ function describe(node, depth = 0) {
531
+ const pad = ' '.repeat(depth);
532
+ switch (node.op) {
533
+ case 'scan': return node.table.isDataset ? `${pad}scan dataset ${node.table.path} (${node.table.chunkCount} chunks, read one at a time; chunks the filters rule out are skipped) [${node.table.columnNames.join(', ')}]` : `${pad}scan [${node.table.columnNames.join(', ')}]`;
534
+ case 'filter': return `${describe(node.input, depth)}\n${pad}filter ${toSource(node.ast)}`;
535
+ case 'derive': return `${describe(node.input, depth)}\n${pad}derive ${node.name} = ${toSource(node.ast)} (inlined where used)`;
536
+ case 'select': return `${describe(node.input, depth)}\n${pad}select ${node.names.join(', ')}`;
537
+ case 'aggregate': return `${describe(node.input, depth)}\n${pad}groupBy ${node.keys.join(', ')}: ${Object.entries(node.spec).map(([k, a]) => `${k} = ${toSource(a)}`).join(', ')}${node.pushed ? ` (${node.pushed} filter(s) on keys moved below)` : ''}`;
538
+ case 'sort': return `${describe(node.input, depth)}\n${pad}sort ${node.by.join(', ')}`;
539
+ case 'limit': return node.input.op === 'sort'
540
+ ? `${describe(node.input.input, depth)}\n${pad}top ${node.n + (node.offset ?? 0)} by ${node.input.by.join(', ')} (sort + limit)${node.offset ? `, then skip ${node.offset}` : ''}`
541
+ : `${describe(node.input, depth)}\n${pad}limit ${node.n}${node.offset ? ` offset ${node.offset}` : ''}`;
542
+ case 'join': return `${pad}join ${JSON.stringify(node.opts.on)} ${node.opts.how ?? 'inner'}\n${pad} left:\n${describe(node.left, depth + 2)}\n${pad} right:\n${describe(node.right, depth + 2)}`;
543
+ case 'output': return describe(node.input, depth);
544
+ case 'textmap': return `${describe(node.input, depth)}\n${pad}${node.text ? 'text' : 'value'} ${node.name} = ${node.label}(${node.inputs.join(', ')}) (once per distinct value)`;
545
+ case 'window': return `${describe(node.input, depth)}\n${pad}window ${node.items.map((it) => `${it.name} = ${it.fn}(${it.arg ?? ''})`).join(', ')}${node.partition.length ? ` partition by ${node.partition.join(', ')}` : ''}${node.order.length ? ` order by ${node.order.map((o) => (o.desc ? `-${o.name}` : o.name)).join(', ')}` : ''}${node.firstRanks ? ` (rows that can rank in the first ${node.firstRanks} only)` : ''}`;
546
+ case 'union': return `${pad}union all\n${node.inputs.map((i) => describe(i, depth + 1)).join(`\n${pad} and\n`)}`;
547
+ case 'unmatched': return `${pad}right rows the join above matched to none (from its probe) [${node.names.join(', ')}]`;
548
+ case 'total': return `${describe(node.input, depth)}\n${pad}totals: ${Object.entries(node.spec).map(([k, a]) => `${k} = ${toSource(a)}`).join(', ')}`;
549
+ }
550
+ return `${pad}${node.op}`;
551
+ }
@@ -0,0 +1,21 @@
1
+ // Portable entry for browsers, Deno, Bun and edge runtimes: no Node APIs,
2
+ // single-threaded, plain memory (no SharedArrayBuffer needed).
3
+ //
4
+ // import { Engine } from 'vectrojs/portable';
5
+ // const engine = new Engine(); // browsers, Deno, Bun
6
+ //
7
+ // Cloudflare Workers (no compiling WebAssembly at runtime): bundle the
8
+ // precompiled module and pass it in. Expressions then run on the
9
+ // vectorized interpreter instead of compiled kernels.
10
+ // import wasmModule from 'vectrojs/dist/vectro.wasm';
11
+ // const engine = new Engine({ wasmModule });
12
+ export { Engine, canCompileWasm } from './engine.mjs';
13
+ export { Table } from './table.mjs';
14
+
15
+ export { VectorIndex } from './vectors.mjs';
16
+ export { BatchEncoder, FrameReader, encodeSchema, encodeEnd } from './frames.mjs';
17
+ export { readArrow, writeArrow, ArrowReader, ArrowEncoder } from './arrow.mjs';
18
+ export { DistinctSketch } from './sketch.mjs';
19
+ export { sql } from './sql.mjs';
20
+ export { Dataset, openDatasetAsync, blobFile, urlFile, writeDatasetAsync, csvToDatasetAsync, createDatasetAsync } from './dataset.mjs'; // (datasets from a Blob or URL, read asynchronously; written to a file object)
21
+ export { memoryFile } from './files-browser.mjs';
@@ -0,0 +1,81 @@
1
+ // Kernels for the rows that can rank in the first k of their partition (see
2
+ // Table #firstRowsG), run as ranged group kernels (OP.GROUP_RANGE): every
3
+ // thread reads all rows and handles only the partitions in its range
4
+ // [lo, hi), so each partition's slot is written by one thread.
5
+ //
6
+ // Keys and partition numbers are float64 arrays. Slot p, k + 1 float64s at
7
+ // top + p * (k + 1) * 8, holds how many keys it has, then its k smallest so
8
+ // far, ascending, NaN last. neg != 0: keys are negated as read (descending).
9
+
10
+ import { I32, F64, Fn, buildModule, get, set, i32c, block, loop, end, br, brIf, i32add, i32mul } from './wasm.mjs';
11
+
12
+ const F64_LOAD = [0x2b, 3, 0], F64_STORE = [0x39, 3, 0], I32_STORE8 = [0x3a, 0, 0];
13
+ const TRUNC_U = [0xfc, 0x03], F64_FROM_U = [0xb8], F64_NEG = [0x9a], SELECT = [0x1b];
14
+ const F64_EQ = [0x61], F64_NE = [0x62], F64_LT = [0x63], F64_LE = [0x65];
15
+ const I32_SUB = [0x6b], I32_LT_U = [0x49], I32_AND = [0x71], I32_OR = [0x72], I32_SHL = [0x74], IF = [0x04, 0x40], ELSE = [0x05];
16
+ const f64c = (v) => [0x44, ...new Uint8Array(Float64Array.of(v).buffer)];
17
+
18
+ // Shared start of both loops: p = partition of row i; when it is in range,
19
+ // v = its key and s = its slot's address; else on to the next row.
20
+ function rowStart(f, { key, g, lo, hi, top, neg }, { i, p, v, s, w8 }, skip) {
21
+ f.emit(get(i), i32c(3), I32_SHL, get(g), i32add, F64_LOAD, TRUNC_U, set(p));
22
+ f.emit(get(p), get(lo), I32_SUB, get(hi), get(lo), I32_SUB, I32_LT_U, [0x45], brIf(skip)); // (p - lo) < (hi - lo), unsigned
23
+ f.emit(get(i), i32c(3), I32_SHL, get(key), i32add, F64_LOAD, set(v));
24
+ f.emit(get(v), F64_NEG, get(v), get(neg), f64c(0), F64_NE, SELECT, set(v));
25
+ f.emit(get(p), get(w8), i32mul, get(top), i32add, set(s));
26
+ }
27
+
28
+ // v sorts before a (NaN last): (a is NaN and v isn't) or v < a
29
+ const before = (v, a) => [...get(a), ...get(a), ...F64_NE, ...get(v), ...get(v), ...F64_EQ, ...I32_AND, ...get(v), ...get(a), ...F64_LT, ...I32_OR];
30
+
31
+ // (key, g, n, top, K, lo, hi, k, neg): each partition's k smallest keys.
32
+ export function topModule(shared = true) {
33
+ const f = new Fn('k', [I32, I32, I32, I32, I32, I32, I32, F64, F64], []);
34
+ const P = { key: 0, g: 1, n: 2, top: 3, lo: 5, hi: 6, k: 7, neg: 8 };
35
+ const L = { i: f.local(I32), p: f.local(I32), s: f.local(I32), c: f.local(I32), j: f.local(I32), kk: f.local(I32), w8: f.local(I32), v: f.local(F64), a: f.local(F64) };
36
+ f.emit(get(P.k), TRUNC_U, set(L.kk), get(L.kk), i32c(1), i32add, i32c(8), i32mul, set(L.w8));
37
+ f.emit(i32c(0), set(L.i));
38
+ f.emit(block, loop, get(L.i), get(P.n), [0x4f], brIf(1)); // i >= n: done
39
+ f.emit(block); // (next row)
40
+ rowStart(f, P, L, 0);
41
+ f.emit(get(L.s), F64_LOAD, TRUNC_U, set(L.c));
42
+ f.emit(get(L.c), get(L.kk), I32_LT_U, IF); // fewer than k so far: one more
43
+ f.emit(get(L.c), i32c(1), i32add, set(L.j), get(L.s), get(L.j), F64_FROM_U, F64_STORE);
44
+ f.emit(ELSE); // else only if it sorts before the k-th
45
+ f.emit(get(L.s), get(L.kk), i32c(3), I32_SHL, i32add, F64_LOAD, set(L.a));
46
+ f.emit(before(L.v, L.a), [0x45], brIf(1), get(L.kk), set(L.j));
47
+ f.emit(end);
48
+ // insert at j, moving larger keys up one
49
+ f.emit(block, loop, get(L.j), i32c(2), I32_LT_U, brIf(1));
50
+ f.emit(get(L.s), get(L.j), i32c(1), I32_SUB, i32c(3), I32_SHL, i32add, F64_LOAD, set(L.a));
51
+ f.emit(before(L.v, L.a), [0x45], brIf(1));
52
+ f.emit(get(L.s), get(L.j), i32c(3), I32_SHL, i32add, get(L.a), F64_STORE);
53
+ f.emit(get(L.j), i32c(1), I32_SUB, set(L.j), br(0), end, end);
54
+ f.emit(get(L.s), get(L.j), i32c(3), I32_SHL, i32add, get(L.v), F64_STORE);
55
+ f.emit(end); // (next row)
56
+ f.emit(get(L.i), i32c(1), i32add, set(L.i), br(0), end, end);
57
+ return buildModule([f], shared);
58
+ }
59
+
60
+ // (key, g, mask, n, top, K, lo, hi, k, neg): mask[i] (a byte) = 1 when row i
61
+ // can rank in the first k: its partition has fewer than k keys, or its key
62
+ // is at most the k-th smallest (every key, when that is NaN).
63
+ export function markModule(shared = true) {
64
+ const f = new Fn('k', [I32, I32, I32, I32, I32, I32, I32, I32, F64, F64], []);
65
+ const P = { key: 0, g: 1, mask: 2, n: 3, top: 4, lo: 6, hi: 7, k: 8, neg: 9 };
66
+ const L = { i: f.local(I32), p: f.local(I32), s: f.local(I32), kk: f.local(I32), w8: f.local(I32), v: f.local(F64), a: f.local(F64) };
67
+ f.emit(get(P.k), TRUNC_U, set(L.kk), get(L.kk), i32c(1), i32add, i32c(8), i32mul, set(L.w8));
68
+ f.emit(i32c(0), set(L.i));
69
+ f.emit(block, loop, get(L.i), get(P.n), [0x4f], brIf(1));
70
+ f.emit(block);
71
+ rowStart(f, P, L, 0);
72
+ f.emit(get(L.s), get(L.kk), i32c(3), I32_SHL, i32add, F64_LOAD, set(L.a)); // the k-th smallest
73
+ f.emit(get(P.mask), get(L.i), i32add);
74
+ f.emit(get(L.s), F64_LOAD, TRUNC_U, get(L.kk), I32_LT_U);
75
+ f.emit(get(L.a), get(L.a), F64_NE, I32_OR);
76
+ f.emit(get(L.v), get(L.a), F64_LE, I32_OR);
77
+ f.emit(I32_STORE8);
78
+ f.emit(end);
79
+ f.emit(get(L.i), i32c(1), i32add, set(L.i), br(0), end, end);
80
+ return buildModule([f], shared);
81
+ }