vectrojs 0.0.1 → 0.1.1

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 +89 -0
  2. package/README.md +828 -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 +2247 -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/table.mjs ADDED
@@ -0,0 +1,2247 @@
1
+ // Columnar tables on top of the engine. Each column is one array in engine
2
+ // memory; text columns are dictionary-encoded (each distinct string gets a
3
+ // number code), so `region == 'EU'` becomes a numeric comparison.
4
+ //
5
+ // Filters are lazy: t.filter(p).sum(e) compiles to one fused pass computing
6
+ // sum(p ? e : 0). No filtered copy is made unless you ask with collect().
7
+
8
+ import { parseKeys, labelRanks, sortIndex } from './sort.mjs';
9
+ import { ZONE_ROWS, computeZones, zoneRange } from './zones.mjs';
10
+ import { parse, toSource, walk, analyze, isMask } from './fuse.mjs';
11
+ import { generateGroupBy, generateCompact, generateHashGroups, generateMarkPairs, generateStridedKeys } from './table-kernels.mjs';
12
+ import { decodeString } from './csv-kernel.mjs';
13
+ import { maxUnits } from './kernels.mjs';
14
+ import { topModule, markModule } from './rank-kernels.mjs';
15
+ import { sortPermG, gatherG } from './parallel-sort.mjs';
16
+ import { partitionedGroupsG, estimateGroups, PARTITION_ROWS, PARTITION_GROUPS, DIRECT_SLOTS } from './partition.mjs';
17
+ import { joinTablesG, joinViewG, lookupGroupsG } from './join.mjs';
18
+ import { Query } from './plan.mjs';
19
+ import { bounds, windowValues, readsPeers } from './window.mjs';
20
+ import { copyRows } from './rows.mjs';
21
+ import { textmapRows } from './textmap.mjs';
22
+ import { writeArrow } from './arrow.mjs';
23
+ import { interpretGroup, interpretCompact, interpretFilteredReduce } from './interp.mjs';
24
+ import { DATE_FNS, DATE_RESULT, parseDateText, toMillis, f32Dates } from './dates.mjs';
25
+ import { chooseEncoding, encodingCandidates, packInto, decodeOne, KINDS } from './pack.mjs';
26
+
27
+ const IDENT = /^[A-Za-z_]\w*$/;
28
+ const AGG_FNS = new Set(['sum', 'mean', 'min', 'max', 'count', 'countDistinct', 'approxDistinct']);
29
+ const DISTINCT = new Set(['countDistinct', 'approxDistinct']); // exact on a table (see #distinctCountsG)
30
+ const IDENTITY = { sum: 0, count: 0, mean: 0, min: Infinity, max: -Infinity };
31
+ const MAX_GROUP_SLOTS = 2 ** 28; // threads * aggregations * groups (2GB of float64)
32
+ const PARALLEL_SORT_ROWS = 50_000; // smaller tables sort on the calling thread
33
+ const TIME_STEPS = [86_400_000, 3_600_000, 60_000, 1_000]; // a day, an hour, a minute, a second (ms): see #directGroups
34
+ // Column facts worked out once and remembered with the column's array
35
+ // (arrays are never written once in a table):
36
+ const MISSING = new WeakMap(); // { n: rows, missing: how many missing values }
37
+ const KEY_RANGE = new WeakMap(); // { n: rows, range: [min, max, how many aren't whole] } (see #directGroups)
38
+
39
+ export class Table {
40
+ // cols: Map name -> { data, dict, codes, strings }
41
+ // category text: dict (code -> string) and codes (string -> code)
42
+ // free text: strings = bytes the values point into (see csv-kernel)
43
+ constructor(engine, cols, n, { pred = null, params = {}, owned = [], ownedBuffers = [], zones = true } = {}) {
44
+ this.engine = engine;
45
+ this.cols = cols;
46
+ this.n = n;
47
+ this.pred = pred; // filter, as bound expression source (or null)
48
+ this.params = params; // scalar values the filter refers to
49
+ this.owned = owned; // columns this table allocated (freed by dispose)
50
+ this.ownedBuffers = ownedBuffers; // e.g. CSV bytes that free-text columns point into
51
+ this.zones = zones; // may zone maps narrow filtered queries (see zones.mjs)
52
+ }
53
+
54
+ // With a filter, the rows that can match it according to the zone maps
55
+ // (min and max per block, kept on each column and computed on first use),
56
+ // as a table over just those rows; otherwise this table.
57
+ #narrow() {
58
+ if (!this.pred || !this.zones || this.n < 4 * ZONE_ROWS) return this;
59
+ const zonesOf = (name) => {
60
+ const c = this.cols.get(name);
61
+ if (c.zones) return c.zones;
62
+ if (!c.enc) return (c.zones = computeZones(this.engine.live(c.data), this.n));
63
+ const codes = this.engine.live(c.data), plain = new Float64Array(this.n); // packed: zones of the values
64
+ for (let i = 0; i < this.n; i++) plain[i] = decodeOne(c.enc, codes[i], this.type === Float32Array);
65
+ return (c.zones = computeZones(plain, this.n));
66
+ };
67
+ const range = zoneRange(this.pred, this.cols, this.params, this.n, zonesOf, this.type === Float32Array ? Math.fround : undefined);
68
+ if (!range) return this;
69
+ const [lo, hi] = range;
70
+ this.engine._zoneSkips = (this.engine._zoneSkips ?? 0) + (this.n - (hi - lo)); // rows skipped (tests, benchmarks)
71
+ const cols = new Map();
72
+ for (const [name, c] of this.cols) cols.set(name, { ...c, data: this.engine.live(c.data).subarray(lo, hi), zones: undefined });
73
+ return new Table(this.engine, cols, hi - lo, { pred: this.pred, params: this.params, zones: false });
74
+ }
75
+
76
+ // ---- building tables --------------------------------------------------------
77
+
78
+ // dates: names of columns holding dates as ms or ISO text (arrays of Date
79
+ // objects are dates anyway).
80
+ static fromColumns(engine, columns, { type = Float64Array, dates = [], compress = false } = {}) {
81
+ for (const [name, values] of Object.entries(columns)) {
82
+ if (dates.includes(name) || isDates(values)) {
83
+ if (type === Float32Array) throw new Error(f32Dates(name));
84
+ columns = { ...columns, [name]: { dates: Float64Array.from(values, toMillis) } };
85
+ }
86
+ }
87
+ const entries = Object.entries(columns);
88
+ if (!entries.length) throw new TypeError('vectro: a table needs at least one column');
89
+ const len = (v) => (isCoded(v) ? v.codes.length : v.dates ? v.dates.length : v.length);
90
+ const n = len(entries[0][1]);
91
+ const cols = new Map();
92
+ for (const [name, values] of entries) {
93
+ checkName(name);
94
+ if (len(values) !== n) throw new RangeError(`vectro: column "${name}" has ${len(values)} rows, expected ${n}`);
95
+ cols.set(name, values.dates ? { data: copyNumbers(engine, type, values.dates), dict: null, codes: null, date: true }
96
+ : isCoded(values) ? codedText(engine, type, values) : isText(values) ? encodeText(engine, type, values) : { data: copyNumbers(engine, type, values), dict: null, codes: null });
97
+ }
98
+ const t = new Table(engine, cols, n, { owned: [...cols.keys()] });
99
+ return compress ? t.compress() : t;
100
+ }
101
+
102
+ // Convert an array of objects. Column types come from the first row.
103
+ static fromRows(engine, rows, { type = Float64Array } = {}) {
104
+ if (!rows.length) throw new TypeError('vectro: no rows');
105
+ const n = rows.length;
106
+ const cols = new Map();
107
+ for (const name of Object.keys(rows[0])) {
108
+ checkName(name);
109
+ const data = engine.alloc(type, n);
110
+ if (typeof rows[0][name] === 'string') {
111
+ const codes = new Map(), dict = [];
112
+ for (let i = 0; i < n; i++) {
113
+ const s = rows[i][name];
114
+ let c = codes.get(s);
115
+ if (c === undefined) { c = dict.length; dict.push(s); codes.set(s, c); }
116
+ data[i] = c;
117
+ }
118
+ cols.set(name, { data, dict, codes });
119
+ } else {
120
+ const date = rows[0][name] instanceof Date;
121
+ if (date && type === Float32Array) throw new Error(f32Dates(name));
122
+ for (let i = 0; i < n; i++) { const v = rows[i][name]; data[i] = v === null || v === undefined ? NaN : date ? toMillis(v) : v; } // (null: missing)
123
+ cols.set(name, { data, dict: null, codes: null, ...(date ? { date: true } : {}) });
124
+ }
125
+ }
126
+ return new Table(engine, cols, n, { owned: [...cols.keys()] });
127
+ }
128
+
129
+ // ---- shape ----------------------------------------------------------------------
130
+
131
+ get length() { return this.n; } // physical rows (ignores the filter)
132
+ get columnNames() { return [...this.cols.keys()]; }
133
+ get type() { const c = this.cols.values().next().value; return c.enc ? c.plainType : c.data.constructor; }
134
+
135
+ column(name) {
136
+ const c = this.cols.get(name);
137
+ if (!c) throw new Error(`vectro: no column "${name}"`);
138
+ return this.engine.live(c.data);
139
+ }
140
+
141
+ // ---- expressions ----------------------------------------------------------------
142
+
143
+ // Parse an expression, swap text literals for dictionary codes, and check
144
+ // every name is a column or a param. Returns { ast, src }.
145
+ #bind(src, params) {
146
+ const ast = rewriteText(parse(src), this.cols, src);
147
+ walk(ast, (m) => {
148
+ if (m.t !== 'var') return;
149
+ if (!this.cols.has(m.name) && !(m.name in params)) {
150
+ throw new Error(`vectro: unknown column "${m.name}" in "${src}" (columns: ${this.columnNames.join(', ')})`);
151
+ }
152
+ if (this.cols.get(m.name)?.strings) {
153
+ throw new Error(`vectro: "${m.name}" is a free-text column; it can be returned but not used in expressions yet (load it as types: { ${m.name}: 'category' } to filter or group on it)`);
154
+ }
155
+ });
156
+ return { ast, src: toSource(ast) };
157
+ }
158
+
159
+ #vars(params) {
160
+ const vars = { ...params };
161
+ const t = this.type === Float32Array ? 'f32' : 'f64';
162
+ for (const [name, c] of this.cols) vars[name] = c.enc ? { data: c.data, enc: c.enc, n: this.n, type: t } : c.data; // packed: the kernels decode it
163
+ return vars;
164
+ }
165
+
166
+ // ---- compressed columns (see pack.mjs) ---------------------------------------------
167
+ // Store each number and category column in 1, 2 or 4 bytes per row where
168
+ // every value decodes back exactly; others stay as they are. In place: the
169
+ // table (and its views) keep working, with less memory. Returns the table.
170
+ compress() { return this.engine._drive(this.#compressG()); }
171
+ compressAsync() { return this.engine._driveAsync(this.#compressG()); }
172
+
173
+ *#compressG() {
174
+ const { engine } = this;
175
+ const f32 = this.type === Float32Array;
176
+ for (const c of new Set(this.cols.values())) {
177
+ if (c.enc || c.strings) continue;
178
+ const values = engine.live(c.data);
179
+ const n = values.length;
180
+ const enc = engine.jit ? yield* this.#chooseEncodingG(c.data, n, f32) : chooseEncoding(values, n, f32);
181
+ if (!enc) continue;
182
+ const plainType = values.constructor, old = c.data;
183
+ c.data = engine.jit ? yield* this.#packG(old, n, enc) : packInto(engine, old, n, enc);
184
+ c.enc = enc;
185
+ c.plainType = plainType;
186
+ c.rows = n;
187
+ engine.free(old);
188
+ }
189
+ for (const c of this.cols.values()) c.data = engine.live(c.data);
190
+ return this;
191
+ }
192
+
193
+ // On every thread: statistics in one pass, then each candidate checked
194
+ // value by value with the kernels' own arithmetic (count of values that
195
+ // wouldn't come back exactly).
196
+ *#chooseEncodingG(data, n, f32) {
197
+ const { engine } = this;
198
+ if (!n) return null;
199
+ const [min, max, frac, nan, inf, negZero] = yield* engine.evalG([
200
+ 'min(v == v ? v : Infinity)', 'max(v == v ? v : -Infinity)', 'count(v == v && v != floor(v))',
201
+ 'count(v != v)', 'count(v == Infinity || v == -Infinity)', 'count(v == 0 && 1 / v < 0)'], { v: data });
202
+ const cands = encodingCandidates({ min, max, whole: frac === 0, nan: nan > 0, inf: inf > 0, negZero: negZero > 0 }, f32);
203
+ for (const enc of cands) {
204
+ const back = enc.div ? `floor(v * ${enc.div} + 0.5) / ${enc.div}` : `floor((v - base) / ${enc.mul} + 0.5) * ${enc.mul} + base`;
205
+ const bad = yield* engine.evalG(`count(v == v && ${back} != v)`, { v: data, base: enc.base });
206
+ if (bad === 0) return enc;
207
+ }
208
+ return null;
209
+ }
210
+
211
+ // The codes, computed on every thread, in a new packed array.
212
+ *#packG(data, n, enc) {
213
+ const { engine } = this;
214
+ const k = KINDS[enc.kind];
215
+ const codes = engine.alloc(this.type, n);
216
+ try {
217
+ const code = enc.div ? `floor(v * ${enc.div} + 0.5)` : `floor((v - base) / ${enc.mul} + 0.5)`;
218
+ yield* engine.evalG(`v == v ? ${code} : ${k.nan}`, { v: data, base: enc.base }, codes);
219
+ const out = engine.alloc(k.Type, n + 16 / k.bytes);
220
+ out.set(engine.live(codes).subarray(0, n));
221
+ return out;
222
+ } finally { engine.free(codes); }
223
+ }
224
+
225
+ // Bytes the columns take now, and without packing.
226
+ memoryBytes() {
227
+ let now = 0, plain = 0;
228
+ const bpe = this.type.BYTES_PER_ELEMENT;
229
+ for (const c of new Set(this.cols.values())) {
230
+ const rows = c.enc ? c.rows : c.data.length;
231
+ now += c.enc ? c.data.BYTES_PER_ELEMENT * rows : c.data.byteLength;
232
+ plain += rows * bpe;
233
+ }
234
+ return { bytes: now, uncompressed: plain };
235
+ }
236
+
237
+ #hasPacked() { for (const c of this.cols.values()) if (c.enc) return true; return false; }
238
+
239
+ // The same rows with packed columns decoded (new arrays, owned): all of
240
+ // them, or those in `names` (the others stay packed).
241
+ *#decodedG(names = null) {
242
+ const { engine } = this;
243
+ const t = this.type === Float32Array ? 'f32' : 'f64';
244
+ const cols = new Map(), owned = [];
245
+ for (const [name, c] of this.cols) {
246
+ if (!c.enc || (names && !names.has(name))) { cols.set(name, c); continue; }
247
+ const out = engine.alloc(c.plainType, this.n);
248
+ if (engine.jit) yield* engine.evalG('x', { x: { data: c.data, enc: c.enc, n: this.n, type: t } }, out);
249
+ else { const codes = engine.live(c.data), o = engine.live(out); for (let i = 0; i < this.n; i++) o[i] = decodeOne(c.enc, codes[i], t === 'f32'); }
250
+ const { enc, plainType, rows, ...rest } = c;
251
+ cols.set(name, { ...rest, data: out });
252
+ owned.push(name);
253
+ }
254
+ for (const c of cols.values()) c.data = engine.live(c.data);
255
+ return new Table(engine, cols, this.n, { pred: this.pred, params: this.params, owned, zones: this.zones });
256
+ }
257
+
258
+ // collect() for a table with packed columns: the filter as 1 / 0 (fused,
259
+ // reads packed columns), the matching row numbers (compacted), then every
260
+ // column gathered (packed columns stay packed).
261
+ *#collectPackedG() {
262
+ const { engine, n } = this;
263
+ const T = this.type, t = T === Float32Array ? 'f32' : 'f64';
264
+ const predAst = parse(this.pred);
265
+ const arrayNames = [];
266
+ walk(predAst, (m) => { if (m.t === 'var' && this.cols.has(m.name) && !arrayNames.includes(m.name)) arrayNames.push(m.name); });
267
+ if (!arrayNames.length) return yield* this.#viaPlainG((x) => x.#collectG()); // (a filter on no column)
268
+ // the matching row numbers, in one fused pass that reads packed columns as they are
269
+ const { parts, units } = yield* engine.compile(`count(${this.pred})`).runPartsG(this.#vars(this.params));
270
+ const offsets = engine.alloc(Float64Array, units);
271
+ let m = 0;
272
+ for (let u = 0; u < units; u++) { offsets[u] = m; m += parts[u]; }
273
+ const scalarNames = [];
274
+ walk(predAst, (x) => { if (x.t === 'var' && !this.cols.has(x.name) && !scalarNames.includes(x.name)) scalarNames.push(x.name); });
275
+ const encs = arrayNames.map((nm) => this.cols.get(nm).enc ?? null);
276
+ const cacheKey = `compactrows|${t}|${this.pred}|${arrayNames}|${scalarNames}|${JSON.stringify(encs)}`;
277
+ engine._tableKernels ??= new Map();
278
+ let v = engine._tableKernels.get(cacheKey);
279
+ if (!v) engine._tableKernels.set(cacheKey, (v = engine._register(generateCompact(predAst, t, arrayNames, [], scalarNames, engine.isShared, encs, true))));
280
+ let perm;
281
+ try {
282
+ perm = engine.alloc(Uint32Array, Math.max(1, m));
283
+ const first = engine.live(this.cols.get(arrayNames[0]).data);
284
+ yield* engine._compactG(v, n, arrayNames.map((nm) => this.cols.get(nm).data), [perm], offsets, [...scalarNames.map((nm) => this.params[nm]), first.byteOffset], units);
285
+ // every column gathered by those rows (packed columns stay packed)
286
+ const out = new Map();
287
+ for (const [name, c] of this.cols) out.set(name, { ...c, data: yield* gatherG(engine, c.data, perm, m, t, c.enc), zones: undefined, ...(c.enc ? { rows: m } : {}) });
288
+ for (const c of out.values()) c.data = engine.live(c.data);
289
+ return new Table(engine, out, m, { owned: [...out.keys()] });
290
+ } finally { engine.free(offsets); if (perm) engine.free(perm); }
291
+ }
292
+
293
+ // Run op on the decoded table (packed columns read as plain numbers).
294
+ *#viaPlainG(op, names = null) {
295
+ const t = yield* this.#decodedG(names);
296
+ let out;
297
+ try { out = yield* op(t); return out; } finally { if (out !== t) t.dispose(); }
298
+ }
299
+
300
+ #mergeParams(params) {
301
+ for (const [k, v] of Object.entries(params)) {
302
+ if (typeof v !== 'number' && !(v instanceof Date)) throw new TypeError(`vectro: param "${k}" must be a number or a Date, got ${typeof v === 'string' ? `text "${v}"` : typeof v} (text goes in the expression in quotes, e.g. region == 'EU')`);
303
+ }
304
+ params = Object.fromEntries(Object.entries(params).map(([k, v]) => [k, v instanceof Date ? v.getTime() : v])); // dates as ms
305
+ for (const k of Object.keys(params)) {
306
+ if (this.cols.has(k)) throw new Error(`vectro: param "${k}" has the same name as a column`);
307
+ if (k in this.params && this.params[k] !== params[k]) throw new Error(`vectro: param "${k}" is already set to ${this.params[k]}`);
308
+ }
309
+ return { ...this.params, ...params };
310
+ }
311
+
312
+ // ---- operations -------------------------------------------------------------------
313
+
314
+ // Lazy filter: returns a view of the same columns with a condition attached.
315
+ filter(expr, params = {}) {
316
+ const merged = this.#mergeParams(params);
317
+ const { src } = this.#bind(expr, merged);
318
+ const pred = this.pred ? `(${this.pred}) && (${src})` : src;
319
+ return new Table(this.engine, this.cols, this.n, { pred, params: merged, zones: this.zones });
320
+ }
321
+
322
+ // New column computed for every row, in one fused pass.
323
+ derive(name, expr, params = {}) { return this.engine._drive(this.#deriveG(name, expr, params)); }
324
+ deriveAsync(name, expr, params = {}) { return this.engine._driveAsync(this.#deriveG(name, expr, params)); }
325
+
326
+ *#deriveG(name, expr, params) {
327
+ checkName(name);
328
+ if (this.cols.has(name)) throw new Error(`vectro: column "${name}" already exists`);
329
+ const merged = this.#mergeParams(params);
330
+ const { src } = this.#bind(expr, merged);
331
+ const out = this.engine.alloc(this.type, this.n);
332
+ yield* this.engine.evalG(src, this.#vars(merged), out);
333
+ const top = parse(expr);
334
+ const date = (top.t === 'call' && DATE_RESULT.has(top.fn)) || (top.t === 'var' && !!this.cols.get(top.name)?.date); // dayStart(ts), monthStart(ts)...: still dates
335
+ const same = top.t === 'var' ? this.cols.get(top.name) : null; // a copy of a text column stays text
336
+ const cols = new Map(this.cols).set(name, { data: out, dict: same?.dict ?? null, codes: same?.dict ? same.codes : null, date });
337
+ return new Table(this.engine, cols, this.n, { pred: this.pred, params: this.params, owned: [name] });
338
+ }
339
+
340
+ // Several aggregations in one pass: agg({ revenue: 'sum(price * qty)', n: 'count()' })
341
+ agg(spec, params = {}) { return this.engine._drive(this.#aggG(spec, params)); }
342
+ aggAsync(spec, params = {}) { return this.engine._driveAsync(this.#aggG(spec, params)); }
343
+
344
+ *#aggG(spec, params) {
345
+ if (!this.engine.jit && this.#hasPacked()) return yield* this.#viaPlainG((t) => t.#aggG(spec, params)); // (the interpreter reads plain numbers)
346
+ const narrow = this.#narrow();
347
+ if (narrow !== this) return yield* narrow.#aggG(spec, params);
348
+ const merged = this.#mergeParams(params);
349
+ const P = this.pred;
350
+ const all = Object.entries(spec).map(([out, src]) => ({ out, ...this.#aggItem(src, merged) }));
351
+ const distinct = all.filter((it) => DISTINCT.has(it.fn));
352
+ if (distinct.length) { // distinct values of the matching rows
353
+ const t = yield* this.#collectG();
354
+ try {
355
+ for (const it of distinct) it.value = (yield* t.#distinctCountsG(it.arg, merged, null, 1))[0];
356
+ } finally {
357
+ if (t !== this) t.dispose();
358
+ }
359
+ }
360
+ const items = all.filter((it) => !DISTINCT.has(it.fn));
361
+ const done = (values) => Object.fromEntries(all.map((it) => [it.out, it.value !== undefined ? it.value : values[it.out]]));
362
+ if (P && !this.engine.jit) {
363
+ // interpreter: evaluate the filter once and aggregate only matching rows
364
+ const truthy = (ast) => (isMask(ast) ? ast : { t: 'bin', op: '!=', l: ast, r: { t: 'num', v: 0 } });
365
+ const work = items.flatMap((it) => (it.fn === 'mean' ? [{ reduce: 'sum', ast: parse(it.arg) }, { reduce: 'count', ast: truthy(parse(it.present)) }]
366
+ : it.fn === 'min' || it.fn === 'max' ? [{ reduce: it.fn, ast: parse(it.arg) }, { reduce: 'count', ast: truthy(parse(it.present)) }]
367
+ : [{ reduce: it.fn, ast: it.arg === null ? null : it.fn === 'count' ? truthy(parse(it.arg)) : parse(it.arg) }]));
368
+ const t = this.type === Float32Array ? 'f32' : 'f64';
369
+ const values = interpretFilteredReduce(this.engine, t, parse(P), work, this.#vars(merged), merged, this.n);
370
+ let j = 0;
371
+ const out = {};
372
+ for (const it of items) {
373
+ if (it.fn === 'mean') out[it.out] = values[j++] / values[j++];
374
+ else if (it.fn === 'min' || it.fn === 'max') { const v = values[j++]; out[it.out] = values[j++] ? v : NaN; }
375
+ else out[it.out] = values[j++];
376
+ }
377
+ return done(out);
378
+ }
379
+ const reductions = [];
380
+ const slot = (s) => reductions.push(s) - 1;
381
+ for (const it of items) {
382
+ const e = it.arg;
383
+ if (it.fn === 'count' && !P && !e) it.value = this.n; // plain row count: nothing to compute
384
+ else if (it.fn === 'count') it.slot = slot(P ? `count((${P}) && (${e ?? 1}))` : `count(${e ?? 1})`);
385
+ else if (it.fn === 'sum') it.slot = slot(P ? `sum((${P}) ? (${e}) : 0)` : `sum(${e})`);
386
+ else if (it.fn === 'min' || it.fn === 'max') { // NaN if no value is present
387
+ it.slot = slot(P ? `${it.fn}((${P}) ? (${e}) : ${it.fn === 'min' ? '' : '-'}Infinity)` : `${it.fn}(${e})`);
388
+ it.presentSlot = slot(P ? `count((${P}) && (${it.present}))` : `count(${it.present})`);
389
+ }
390
+ else { // mean: over the values present
391
+ it.slot = slot(P ? `sum((${P}) ? (${e}) : 0)` : `sum(${e})`);
392
+ it.countSlot = slot(P ? `count((${P}) && (${it.present}))` : `count(${it.present})`);
393
+ }
394
+ }
395
+ const r = []; // a kernel takes at most 8 reductions: run them in batches
396
+ for (let i = 0; i < reductions.length; i += 8) r.push(...[(yield* this.engine.evalG(reductions.slice(i, i + 8), this.#vars(merged)))].flat());
397
+ const value = (it) => (it.value !== undefined ? it.value : it.presentSlot !== undefined ? (r[it.presentSlot] ? r[it.slot] : NaN) : it.countSlot === undefined ? r[it.slot] : r[it.slot] / r[it.countSlot]);
398
+ return done(Object.fromEntries(items.map((it) => [it.out, value(it)])));
399
+ }
400
+
401
+ sum(expr, params) { return this.agg({ v: `sum(${expr})` }, params).v; }
402
+ mean(expr, params) { return this.agg({ v: `mean(${expr})` }, params).v; }
403
+ min(expr, params) { return this.agg({ v: `min(${expr})` }, params).v; }
404
+ max(expr, params) { return this.agg({ v: `max(${expr})` }, params).v; }
405
+ count(expr, params) { return this.agg({ v: expr ? `count(${expr})` : 'count()' }, params).v; }
406
+
407
+ async sumAsync(expr, params) { return (await this.aggAsync({ v: `sum(${expr})` }, params)).v; }
408
+ async meanAsync(expr, params) { return (await this.aggAsync({ v: `mean(${expr})` }, params)).v; }
409
+ async minAsync(expr, params) { return (await this.aggAsync({ v: `min(${expr})` }, params)).v; }
410
+ async maxAsync(expr, params) { return (await this.aggAsync({ v: `max(${expr})` }, params)).v; }
411
+ async countAsync(expr, params) { return (await this.aggAsync({ v: expr ? `count(${expr})` : 'count()' }, params)).v; }
412
+
413
+ // 'sum(price * qty)' -> { fn: 'sum', arg: '(price * qty)' } with text bound to codes.
414
+ #aggItem(src, params) {
415
+ const ast = parse(src);
416
+ if (ast.t !== 'call' || !AGG_FNS.has(ast.fn) || ast.args.length > 1 || (ast.fn !== 'count' && ast.args.length !== 1)) {
417
+ throw new SyntaxError(`vectro: an aggregation must look like sum(expr), mean(expr), min(expr), max(expr), count([expr]) or countDistinct(expr), got "${src}"`);
418
+ }
419
+ if (!ast.args.length) return { fn: ast.fn, arg: null };
420
+ // a category column: its missing values are '' (count / countDistinct skip them, as SQL skips NULL)
421
+ const a0 = ast.args[0], text = a0.t === 'var' && this.cols.get(a0.name)?.dict;
422
+ if (text && ast.fn === 'count') return missingRules('count', this.#bind(`${a0.name} != ''`, params).src);
423
+ if (text && DISTINCT.has(ast.fn)) return { fn: ast.fn, arg: this.#bind(`${a0.name} != '' ? ${a0.name} : 0 / 0`, params).src };
424
+ if (!DISTINCT.has(ast.fn)) return missingRules(ast.fn, this.#bind(toSource(ast.args[0]), params).src);
425
+ if (DISTINCT.has(ast.fn) && ast.args[0].t === 'var' && this.cols.get(ast.args[0].name)?.strings) {
426
+ throw new Error(`vectro: ${ast.fn} can't count a free-text column ("${ast.args[0].name}"); load it as a category`);
427
+ }
428
+ return { fn: ast.fn, arg: this.#bind(toSource(ast.args[0]), params).src };
429
+ }
430
+
431
+ // Group by one key or several: groupBy('region'), groupBy(['region', 'day']).
432
+ // Keys can be category columns or any numbers (IDs, prices, NaN groups).
433
+ // Groups come out sorted by key: text in dictionary order, numbers
434
+ // ascending (NaN last), earlier keys first.
435
+ groupBy(key) {
436
+ const keys = Array.isArray(key) ? key : [key];
437
+ if (!keys.length) throw new Error('vectro: groupBy needs at least one key column');
438
+ if (new Set(keys).size !== keys.length) throw new Error(`vectro: groupBy got the same key twice (${keys.join(', ')})`);
439
+ for (const k of keys) {
440
+ const c = this.cols.get(k);
441
+ if (!c) throw new Error(`vectro: no column "${k}"`);
442
+ if (c.strings) throw new Error(`vectro: can't group by free-text column "${k}" (load it as types: { ${k}: 'category' })`);
443
+ }
444
+ return {
445
+ agg: (spec, params = {}) => this.engine._drive(this.#groupAggG(keys, spec, params)),
446
+ aggAsync: (spec, params = {}) => this.engine._driveAsync(this.#groupAggG(keys, spec, params)),
447
+ };
448
+ }
449
+
450
+ // (plan.mjs) GROUP BY right-table columns of an inner join on unique right
451
+ // keys, aggregating this (left) table's columns: each matching row's join
452
+ // key is looked up straight to its group number inside the group kernel
453
+ // (filter, lookup and sums in one pass over the left rows, packed columns
454
+ // read as they are), so no joined rows are made. Null when it doesn't
455
+ // apply (the plan then joins as usual). on: { left, right } key names;
456
+ // rightKeys[k]: the right column behind output key keys[k].
457
+ _lookupGroupAgg(R, on, keys, rightKeys, spec) { return this.engine._drive(this.#lookupGroupAggG(R, on, keys, rightKeys, spec)); }
458
+ _lookupGroupAggAsync(R, on, keys, rightKeys, spec) { return this.engine._driveAsync(this.#lookupGroupAggG(R, on, keys, rightKeys, spec)); }
459
+
460
+ *#lookupGroupAggG(R, on, keys, rightKeys, spec) {
461
+ const { engine } = this;
462
+ if (!engine.jit || !(R instanceof Table) || R.engine !== engine || R.pred || R.#hasPacked() || R.type !== this.type) return null;
463
+ const c = this.cols.get(on.left);
464
+ if (!c || c.strings) return null;
465
+ const merged = this.#mergeParams({});
466
+ const items = Object.entries(spec).map(([out, src]) => ({ out, ...this.#aggItem(src, merged) }));
467
+ if (items.some((it) => DISTINCT.has(it.fn) || keys.includes(it.out))) return null;
468
+ const look = yield* lookupGroupsG(this, R, on, rightKeys);
469
+ if (!look) return null;
470
+ const groups = {
471
+ K: look.G, data: c.data, enc: c.enc ?? null, lookup: look, sorted: true, owned: false, keyMeta: look.meta,
472
+ keyColumn: (k, keep) => { const v = look.values[k], out = new Float64Array(keep.length); for (let i = 0; i < keep.length; i++) out[i] = v[keep[i]]; return out; },
473
+ };
474
+ return yield* this.#runGroupsG(keys, groups, items, merged);
475
+ }
476
+
477
+ *#groupAggG(keys, spec, params) {
478
+ if (this.#hasPacked()) {
479
+ const used = new Set(keys);
480
+ for (const src of [...Object.values(spec), this.pred ?? '1']) walk(parse(src), (m) => { if (m.t === 'var' && this.cols.has(m.name)) used.add(m.name); });
481
+ if (!this.engine.jit) { // the interpreter reads plain numbers
482
+ if ([...used].some((n) => this.cols.get(n).enc)) return yield* this.#viaPlainG((t) => t.#groupAggG(keys, spec, params), used);
483
+ } else if (keys.some((k) => this.cols.get(k).enc)) { // the group kernel reads packed columns; the other paths need plain keys
484
+ const direct = yield* this.#directGroups(keys, Object.keys(spec).length + 1);
485
+ if (direct?.owned) this.engine.free(direct.data);
486
+ if (!direct) return yield* this.#viaPlainG((t) => t.#groupAggG(keys, spec, params), new Set(keys.filter((k) => this.cols.get(k).enc)));
487
+ }
488
+ }
489
+ const narrow = this.#narrow();
490
+ if (narrow !== this) return yield* narrow.#groupAggG(keys, spec, params);
491
+ const merged = this.#mergeParams(params);
492
+ const items = Object.entries(spec).map(([out, src]) => ({ out, ...this.#aggItem(src, merged) }));
493
+ for (const it of items) if (keys.includes(it.out)) throw new Error(`vectro: aggregation "${it.out}" has the same name as a key`);
494
+ if (this.pred && items.some((it) => DISTINCT.has(it.fn))) { // distinct counts need the matching rows themselves
495
+ const t = yield* this.#collectG();
496
+ try { return yield* t.#groupAggG(keys, spec, params); } finally { t.dispose(); }
497
+ }
498
+ const { engine } = this;
499
+ const totals = 1 + items.length + items.filter((it) => it.present).length, capped = {};
500
+ // (packed keys: numbered as they are read, no copy to make; the other paths would decode them first)
501
+ const packedKey = keys.some((k) => this.cols.get(k).enc);
502
+ const direct = yield* this.#directGroups(keys, items.length + 1, packedKey || items.some((it) => DISTINCT.has(it.fn)) ? 2 ** 24 : this.#directMax(totals), capped);
503
+ if (direct) {
504
+ try { return yield* this.#runGroupsG(keys, direct, items, merged); } finally { if (direct.owned) this.engine.free(direct.data); }
505
+ }
506
+ // Otherwise keep the matching rows. Big tables: partitioned by key range
507
+ // (see partition.mjs). Else (and for distinct counts, which need each
508
+ // row's group) the hash path: number the distinct keys 0..K-1 (in order
509
+ // of first appearance), then group by that number.
510
+ const t = yield* this.#collectG();
511
+ let ids = null;
512
+ try {
513
+ let est; // (groups expected)
514
+ if (engine.jit && t.n >= (engine._partitionRows ?? PARTITION_ROWS) && !items.some((it) => DISTINCT.has(it.fn))) {
515
+ est = estimateGroups(t, keys);
516
+ const out = est >= (engine._partitionGroups ?? PARTITION_GROUPS) ? yield* t.#partitionedGroupsG(keys, items, merged, est) : null;
517
+ if (out) return out;
518
+ // not partitioned after all: keys the direct path turned down only to leave them to it,
519
+ // numbered directly when most of their range is in use (else the hash path is faster)
520
+ if (capped.K && est * 8 >= capped.K) {
521
+ ids = yield* t.#directGroups(keys, items.length + 1);
522
+ if (ids) return yield* t.#runGroupsG(keys, ids, items, merged);
523
+ }
524
+ }
525
+ ids = engine.jit && t.n >= 4096 ? yield* t.#hashGroupsParallelG(keys, est) : t.#hashGroups(keys);
526
+ return yield* t.#runGroupsG(keys, ids, items, merged);
527
+ } finally {
528
+ if (ids && ids.owned !== false) { // (direct numbers can be a key column itself)
529
+ this.engine.free(ids.data);
530
+ for (const x of ids.extra ?? []) this.engine.free(x);
531
+ }
532
+ if (t !== this) t.dispose();
533
+ }
534
+ }
535
+
536
+ // The result table from the partitioned path (see partition.mjs), or null
537
+ // when it doesn't suit these keys and aggregations. Totals as #runGroupsG
538
+ // has them: the row count first, then the items, then each mean's, min's
539
+ // and max's count of values present.
540
+ *#partitionedGroupsG(keys, items, merged, est) {
541
+ const { engine } = this;
542
+ const kernelItems = [...items, ...items.filter((it) => it.present).map((it) => ({ fn: 'count', arg: it.present, of: it }))];
543
+ const sources = ['count(1)', ...kernelItems.map((it) => `${it.fn === 'mean' ? 'sum' : it.fn}(${it.arg ?? 1})`)];
544
+ const r = yield* partitionedGroupsG(this, keys, sources, merged, est);
545
+ if (!r) return null;
546
+ const { G, keyCols, accCols, map } = r;
547
+ const pending = new Set([...keyCols, ...accCols]); // freed unless they become result columns
548
+ try {
549
+ const cols = new Map(), used = new Set();
550
+ keys.forEach((key, k) => {
551
+ const src = this.cols.get(key);
552
+ cols.set(key, { data: keyCols[k], dict: src.dict ?? null, codes: src.dict ? src.codes : null, date: src.date || undefined });
553
+ });
554
+ for (const [i, it] of items.entries()) {
555
+ let data = accCols[map[1 + i]];
556
+ if (it.present || used.has(data)) {
557
+ const out = engine.alloc(Float64Array, G);
558
+ pending.add(out);
559
+ if (!it.present) engine.live(out).set(engine.live(data)); // (a total two items share)
560
+ else if (G) { // mean: over the values present; min, max: NaN where there is none
561
+ const p = accCols[map[1 + kernelItems.findIndex((k) => k.of === it)]];
562
+ yield* engine.evalG(it.fn === 'mean' ? 'v / p' : '(p != 0) ? v : NaN', { v: data, p }, out);
563
+ }
564
+ data = out;
565
+ }
566
+ used.add(data);
567
+ cols.set(it.out, { data, dict: null, codes: null });
568
+ }
569
+ for (const c of cols.values()) { pending.delete(c.data); c.data = engine.live(c.data); }
570
+ return new Table(engine, cols, G, { owned: [...cols.keys()] });
571
+ } finally {
572
+ for (const x of pending) engine.free(x);
573
+ }
574
+ }
575
+
576
+ // The most groups to number directly with `totals` totals per group: past
577
+ // threads x totals x groups = DIRECT_SLOTS, the partitioned path is faster
578
+ // (on tables it takes).
579
+ #directMax(totals) {
580
+ const { engine } = this;
581
+ if (!engine.jit || this.n < (engine._partitionRows ?? PARTITION_ROWS)) return 2 ** 24;
582
+ return Math.floor((engine._directSlots ?? DIRECT_SLOTS) / (engine.T * totals));
583
+ }
584
+
585
+ // Is src a number column of this table with no missing values? Counted
586
+ // once per column (one pass on every thread) and remembered with its array.
587
+ *#noMissingG(src) {
588
+ const c = src && IDENT.test(src) ? this.cols.get(src) : null;
589
+ if (!c || c.dict || c.strings) return false;
590
+ let known = MISSING.get(c.data);
591
+ if (known?.n !== this.n) {
592
+ const k = c.enc ? { data: c.data, enc: c.enc, n: this.n, type: this.type === Float32Array ? 'f32' : 'f64' } : c.data;
593
+ MISSING.set(c.data, (known = { n: this.n, missing: (yield* this.engine.evalG(['count(k != k)'], { k }))[0] }));
594
+ }
595
+ return known.missing === 0;
596
+ }
597
+
598
+ // Keys small enough to be group numbers as they are: a text column's codes
599
+ // or whole numbers 0..2^24, several combined as k1 * size2 + k2..., while
600
+ // the accumulators stay small (or no larger than the rows). Else null.
601
+ *#directGroups(keys, J, maxK = 2 ** 24, info = null) { // (info.K: the groups when only maxK stood in the way)
602
+ const { engine } = this;
603
+ const sizes = [], offsets = [], steps = [];
604
+ for (const key of keys) {
605
+ const c = this.cols.get(key);
606
+ if (c.dict) { sizes.push(c.dict.length); offsets.push(0); steps.push(1); continue; }
607
+ if (!this.n) { sizes.push(0); offsets.push(0); steps.push(1); continue; }
608
+ // whole numbers in a range under 2^24 (from their minimum: 202301..202412, IDs from 1e9, negatives)
609
+ const k = c.enc ? { data: c.data, enc: c.enc, n: this.n, type: this.type === Float32Array ? 'f32' : 'f64' } : c.data;
610
+ let known = KEY_RANGE.get(c.data);
611
+ if (known?.n !== this.n) KEY_RANGE.set(c.data, (known = { n: this.n, range: yield* engine.evalG(['min(k)', 'max(k)', 'count(k != floor(k))'], { k }) }));
612
+ const [lo, hi, frac] = known.range;
613
+ if (frac > 0 || !Number.isFinite(lo) || !Number.isFinite(hi)) return null;
614
+ // or times a whole number of days (hours, minutes, seconds) apart: numbered by that unit
615
+ let step = 1;
616
+ if (!(hi - lo < 2 ** 24)) {
617
+ const units = TIME_STEPS.filter((s) => (hi - lo) / s < 2 ** 24);
618
+ const off = units.length ? yield* engine.evalG(units.map((s) => `count((k - lo) / ${s} != floor((k - lo) / ${s}))`), { k, lo }) : [];
619
+ step = units.find((_, j) => [off].flat()[j] === 0);
620
+ if (!step) return null;
621
+ }
622
+ sizes.push((hi - lo) / step + 1);
623
+ offsets.push(lo);
624
+ steps.push(step);
625
+ }
626
+ const fits = (K) => !(K > 2 ** 24 || engine.T * J * K * (engine.jit && K <= 1024 ? 4 : 1) > (engine._maxGroupSlots ?? MAX_GROUP_SLOTS) || (K > 65_536 && K > 8 * this.n));
627
+ // One key from a small positive number (IDs from 1, years from 1990):
628
+ // numbered from 0 with a few groups left empty, so the key column itself
629
+ // is the group number and needs no copy.
630
+ if (keys.length === 1 && steps[0] === 1 && offsets[0] > 0 && offsets[0] <= Math.max(1024, sizes[0]) && offsets[0] + sizes[0] <= maxK && fits(offsets[0] + sizes[0])) {
631
+ sizes[0] += offsets[0];
632
+ offsets[0] = 0;
633
+ }
634
+ const K = sizes.reduce((a, b) => a * b, 1);
635
+ if (!fits(K)) return null;
636
+ if (K > maxK) { if (info) info.K = K; return null; }
637
+ const below = sizes.map((_, k) => sizes.slice(k + 1).reduce((a, b) => a * b, 1));
638
+ const keyColumn = (k, keep) => directKeys(keep, below[k], sizes[k], steps[k], offsets[k]); // key k of each kept group (codes for text)
639
+ if (keys.length === 1 && offsets[0] === 0 && steps[0] === 1) return { K, data: this.cols.get(keys[0]).data, enc: this.cols.get(keys[0]).enc ?? null, keyColumn, owned: false, sorted: true };
640
+ const term = (k) => {
641
+ const t = offsets[k] ? `(${keys[k]} - ${offsets[k] < 0 ? `(${offsets[k]})` : offsets[k]})` : keys[k];
642
+ return steps[k] === 1 ? t : `(${t} / ${steps[k]})`;
643
+ };
644
+ let expr = term(0);
645
+ for (let k = 1; k < keys.length; k++) expr = `(${expr}) * ${sizes[k]} + ${term(k)}`;
646
+ const data = engine.alloc(this.type, this.n);
647
+ yield* engine.evalG(expr, this.#vars({}), data);
648
+ return { K, data, keyColumn, owned: true, sorted: true };
649
+ }
650
+
651
+ // Number each row's distinct key (tuple) with a hash table: { K, data
652
+ // (group number per row, in engine memory), values(g), sorted: false }.
653
+ // Each group's key values are kept side by side (no trips back to its first
654
+ // row), and keys are hashed on their bits (-0 as 0, every NaN alike).
655
+ #hashGroups(keys) {
656
+ const { engine, n } = this;
657
+ if (this.type === Float32Array && n > 2 ** 24) throw new RangeError('vectro: groupBy on a float32 table is limited to 16,777,216 rows on the hash path');
658
+ const nk = keys.length, f32 = this.type === Float32Array, W = f32 ? 1 : 2; // 32-bit words per value
659
+ const data = engine.alloc(this.type, n);
660
+ const cols = keys.map((k) => engine.live(this.cols.get(k).data));
661
+ const words = cols.map((c) => new Uint32Array(c.buffer, c.byteOffset, c.length * W));
662
+ const ids = engine.live(data);
663
+ let cap = 1024, slots = new Int32Array(cap).fill(-1), hashes = new Int32Array(cap);
664
+ let kv = new Float64Array(1024 * nk); // group g's keys: kv[g * nk + k]
665
+ let K = 0;
666
+ for (let i = 0; i < n; i++) {
667
+ let h = 0x811c9dc5;
668
+ for (let k = 0; k < nk; k++) {
669
+ const v = cols[k][i];
670
+ if (v === v && v !== 0) {
671
+ h = Math.imul(h ^ words[k][i * W], 0x01000193);
672
+ if (W === 2) h = Math.imul(h ^ words[k][i * 2 + 1], 0x01000193);
673
+ } else h = Math.imul(h ^ (v === 0 ? 0 : 0x7ff8), 0x01000193); // 0 and -0; NaN
674
+ }
675
+ h ^= h >>> 15; h = Math.imul(h, 0x2c1b3c6d); h ^= h >>> 12;
676
+ const mask = cap - 1;
677
+ let slot = h & mask, g = slots[slot];
678
+ while (g !== -1) {
679
+ if (hashes[slot] === h) {
680
+ let k = 0;
681
+ for (; k < nk; k++) { const a = kv[g * nk + k], b = cols[k][i]; if (a !== b && (a === a || b === b)) break; }
682
+ if (k === nk) break;
683
+ }
684
+ slot = (slot + 1) & mask;
685
+ g = slots[slot];
686
+ }
687
+ if (g === -1) {
688
+ g = K++;
689
+ if ((g + 1) * nk > kv.length) { const bigger = new Float64Array(kv.length * 2); bigger.set(kv); kv = bigger; }
690
+ for (let k = 0; k < nk; k++) { const v = cols[k][i]; kv[g * nk + k] = v === 0 ? 0 : v; }
691
+ slots[slot] = g;
692
+ hashes[slot] = h;
693
+ if (K * 2 > cap) { // grow: rehash every group
694
+ cap *= 2;
695
+ const s2 = new Int32Array(cap).fill(-1), h2 = new Int32Array(cap);
696
+ for (let x = 0; x < slots.length; x++) {
697
+ if (slots[x] === -1) continue;
698
+ let y = hashes[x] & (cap - 1);
699
+ while (s2[y] !== -1) y = (y + 1) & (cap - 1);
700
+ s2[y] = slots[x]; h2[y] = hashes[x];
701
+ }
702
+ slots = s2; hashes = h2;
703
+ }
704
+ }
705
+ ids[i] = g;
706
+ }
707
+ const keyColumn = (k, keep) => stridedKeys(kv, keep, nk, k); // key k of each kept group (codes for text)
708
+ return { K, data, keyColumn, owned: true, sorted: false };
709
+ }
710
+
711
+ // The same numbering on every thread at once (see generateHashGroups): one
712
+ // shared table, sized from an estimate of the number of groups (Chao1 on a
713
+ // sample of rows); if more groups turn up, again with a bigger table.
714
+ // Group numbers follow first arrival, not row order (any order is fine:
715
+ // the groups are sorted by key afterwards).
716
+ *#hashGroupsParallelG(keys, est) { // (est: groups expected, if known)
717
+ const { engine, n } = this;
718
+ const nk = keys.length, t = this.type === Float32Array ? 'f32' : 'f64';
719
+ const cols = keys.map((k) => this.cols.get(k).data);
720
+ let limit = Math.min(n, engine._hashLimit ?? (est === undefined ? this.#estimateGroups(cols) : Math.ceil(est * 1.5) + 1024)); // (_hashLimit: tests)
721
+ if (t === 'f32') limit = Math.min(limit, 2 ** 24);
722
+ const cacheKey = `hashgroups|${t}|${nk}`;
723
+ engine._tableKernels ??= new Map();
724
+ let v = engine._tableKernels.get(cacheKey);
725
+ if (!v) { v = engine._register(generateHashGroups(t, nk, engine.isShared)); engine._tableKernels.set(cacheKey, v); }
726
+ const slack = 64 * maxUnits(Math.max(1, engine.T)); // group numbers reserved but unused: < 64 per unit
727
+ for (;;) {
728
+ const room = limit + slack;
729
+ let cap = 1024;
730
+ while (cap < room * 2) cap *= 2;
731
+ const slots = engine.alloc(Int32Array, cap), keyBits = engine.alloc(Float64Array, room * nk), counter = engine.alloc(Int32Array, 4);
732
+ const data = engine.alloc(this.type, n);
733
+ engine.live(slots).fill(-1);
734
+ engine.live(counter)[0] = 0;
735
+ let overflow, K;
736
+ try {
737
+ const { parts } = yield* engine._fusedG(v, n, [...cols, data], [engine.live(slots).byteOffset, cap - 1, engine.live(keyBits).byteOffset, room, engine.live(counter).byteOffset], 1);
738
+ K = engine.live(counter)[0];
739
+ overflow = parts.some((x) => x !== 0); // (K may pass limit by numbers reserved but unused)
740
+ } catch (err) { engine.free(data); engine.free(keyBits); throw err; } finally { engine.free(slots); engine.free(counter); }
741
+ if (overflow) {
742
+ engine.free(keyBits); engine.free(data);
743
+ if (limit >= n || (t === 'f32' && limit >= 2 ** 24)) throw new Error('vectro: groupBy hash table overflowed'); // can't happen: at most n groups
744
+ limit = Math.min(n, limit * 4);
745
+ continue;
746
+ }
747
+ const keyColumn = (k, keep) => stridedKeys(engine.live(keyBits), keep, nk, k); // key k of each kept group (codes for text)
748
+ const keyArrayG = function* (k, keep, m) { // the same into a new engine array, on every thread (keep: an engine Uint32Array)
749
+ engine._tableKernels ??= new Map();
750
+ let s = engine._tableKernels.get('stridedkeys');
751
+ if (!s) { s = engine._register(generateStridedKeys(engine.isShared)); engine._tableKernels.set('stridedkeys', s); }
752
+ const dst = engine.alloc(Float64Array, m);
753
+ if (m) yield* engine._unitsG(s, m, [engine.live(keyBits).byteOffset, engine.live(keep).byteOffset, engine.live(dst).byteOffset, nk, k]);
754
+ return dst;
755
+ };
756
+ return { K, data, keyColumn, keyArrayG, owned: true, sorted: false, extra: [keyBits] };
757
+ }
758
+ }
759
+
760
+ // Estimated number of distinct keys (tuples), from up to 65,536 sampled
761
+ // rows: Chao1 = seen + once^2 / (2 * twice); plenty of room is cheap.
762
+ #estimateGroups(cols) {
763
+ const live = cols.map((c) => this.engine.live(c));
764
+ const m = Math.min(this.n, 65_536), step = this.n / m;
765
+ const counts = sampleCounts(live, this.type === Float32Array, m, step);
766
+ let once = 0, twice = 0;
767
+ for (const c of counts.values()) { if (c === 1) once++; else if (c === 2) twice++; }
768
+ const est = counts.size + (twice ? (once * once) / (2 * twice) : once * (this.n / m));
769
+ return Math.ceil(est * 1.5) + 1024;
770
+ }
771
+
772
+ // Run the group kernel over group numbers `groups.data` (0..K-1).
773
+ *#runGroupsG(keys, groups, allItems, merged) {
774
+ const { engine } = this;
775
+ const { K } = groups;
776
+ // Over a column with no missing values, count(x) is the row count (always
777
+ // counted) and sum / mean / min / max need no check for missing values:
778
+ // SQL's SUM(x) is sum(x) and count(x), so this takes a whole accumulator off.
779
+ const full = new Set();
780
+ for (const it of allItems) if (!DISTINCT.has(it.fn) && (yield* this.#noMissingG(it.raw))) full.add(it);
781
+ const rowCount = (it) => it.fn === 'count' && (!it.arg || full.has(it));
782
+ const items = allItems.filter((it) => !DISTINCT.has(it.fn) && !rowCount(it));
783
+ // means need the count of values present: one more kernel item each
784
+ const kernelItems = [...items.map((it) => (full.has(it) ? { fn: it.fn, arg: it.raw } : it)),
785
+ ...items.filter((it) => it.present && !full.has(it)).map((it) => ({ fn: 'count', arg: it.present, of: it }))]; // means, mins, maxes
786
+ const totals = yield* this.#groupTotalsG(groups, kernelItems, merged);
787
+ if (!totals) return null; // (lookup groups the kernels can't take)
788
+ const totalOf = (it) => (rowCount(it) ? totals[0] : totals[1 + items.indexOf(it)]);
789
+ const presentOf = (it) => (full.has(it) ? totals[0] : totals[1 + kernelItems.findIndex((k) => k.of === it)]);
790
+ // Keep groups that have rows, sorted by key (codes for text keys, so
791
+ // dictionary order; NaN last), and build whole columns.
792
+ let keep = keptGroups(totals[0], K);
793
+ const m = keep.length;
794
+ for (const it of allItems) { // distinct counts per group: from (group, value) pairs
795
+ if (!DISTINCT.has(it.fn)) continue;
796
+ let gData = groups.data;
797
+ if (groups.enc) { // a packed key: group numbers as plain numbers
798
+ gData = engine.alloc(this.type, this.n);
799
+ yield* engine.evalG('x', { x: { data: groups.data, enc: groups.enc, n: this.n, type: this.type === Float32Array ? 'f32' : 'f64' } }, gData);
800
+ }
801
+ try { it.perGroup = yield* this.#distinctCountsG(it.arg, merged, gData, K); } finally { if (gData !== groups.data) engine.free(gData); }
802
+ }
803
+ // each item's value for every group number
804
+ const perGroup = allItems.map((it) => {
805
+ const v = DISTINCT.has(it.fn) ? it.perGroup : totalOf(it);
806
+ return it.fn === 'mean' ? ratioOf(v, presentOf(it)) : it.fn === 'min' || it.fn === 'max' ? presentOnly(v, presentOf(it)) : v;
807
+ });
808
+ if (engine.jit && m >= PARALLEL_SORT_ROWS) return yield* this.#groupTableG(keys, groups, allItems, perGroup, keep);
809
+ let codes = keys.map((_, k) => groups.keyColumn(k, keep));
810
+ if (!groups.sorted) {
811
+ const idx = sortIndex(codes, m);
812
+ keep = gatherU32(keep, idx);
813
+ codes = codes.map((a) => gatherF64(a, idx));
814
+ }
815
+ // (text keys keep their codes as numbers: a code with no label, NaN, stays NaN)
816
+ const cols = new Map(), made = [];
817
+ const toEngine = (a) => { const x = engine.alloc(Float64Array, m); engine.live(x).set(a); made.push(x); return x; };
818
+ try {
819
+ keys.forEach((key, k) => {
820
+ const src = groups.keyMeta?.[k] ?? this.cols.get(key);
821
+ cols.set(key, { data: toEngine(codes[k]), dict: src.dict ?? null, codes: src.dict ? src.codes : null, date: src.date || undefined });
822
+ });
823
+ allItems.forEach((it, j) => cols.set(it.out, { data: toEngine(gatherF64(perGroup[j], keep)), dict: null, codes: null }));
824
+ for (const c of cols.values()) c.data = engine.live(c.data);
825
+ made.length = 0;
826
+ return new Table(engine, cols, m, { owned: [...cols.keys()] });
827
+ } finally {
828
+ for (const x of made) engine.free(x);
829
+ }
830
+ }
831
+
832
+ // (see #runGroupsG) Many groups: the result columns made in engine memory
833
+ // on every thread. keep: the group numbers kept, ascending; their keys are
834
+ // sorted (unless they are in order already), then every column gathered
835
+ // in that order.
836
+ *#groupTableG(keys, groups, allItems, perGroup, keep) {
837
+ const { engine } = this;
838
+ const m = keep.length, made = [];
839
+ const toEngine = (a, Type = Float64Array) => { const x = engine.alloc(Type, a.length); engine.live(x).set(a); made.push(x); return x; };
840
+ const owned = (x) => { made.splice(made.indexOf(x), 1); return x; };
841
+ try {
842
+ const keepE = toEngine(keep, Uint32Array), keyArrs = [];
843
+ for (let k = 0; k < keys.length; k++) {
844
+ if (groups.keyArrayG) { const a = yield* groups.keyArrayG(k, keepE, m); made.push(a); keyArrs.push(a); } else keyArrs.push(toEngine(groups.keyColumn(k, keep)));
845
+ }
846
+ const order = groups.sorted ? null : yield* groupPermG(engine, keyArrs, m); // (row r of the result: kept group order[r])
847
+ if (order) made.push(order);
848
+ const at = order ? yield* gatherG(engine, keepE, order, m, 'f32') : keepE; // (group numbers in result order; 4-byte copies)
849
+ if (order) made.push(at);
850
+ const cols = new Map();
851
+ keys.forEach((key, k) => {
852
+ const src = groups.keyMeta?.[k] ?? this.cols.get(key);
853
+ cols.set(key, { data: null, dict: src.dict ?? null, codes: src.dict ? src.codes : null, date: src.date || undefined, k });
854
+ });
855
+ for (const c of cols.values()) { c.data = order ? yield* gatherG(engine, keyArrs[c.k], order, m, 'f64') : owned(keyArrs[c.k]); delete c.k; }
856
+ for (const [j, it] of allItems.entries()) cols.set(it.out, { data: yield* gatherG(engine, toEngine(perGroup[j]), at, m, 'f64'), dict: null, codes: null });
857
+ for (const c of cols.values()) c.data = engine.live(c.data);
858
+ return new Table(engine, cols, m, { owned: [...cols.keys()] });
859
+ } finally {
860
+ for (const x of made) engine.free(x);
861
+ }
862
+ }
863
+
864
+ // (datasets, see dataset.mjs) Add the rows that match this table's filter
865
+ // into running per-group totals: state.acc holds J * K float64s for the J
866
+ // reductions in `sources` ('count(1)', 'sum(x)', 'min(x)'... at most 8,
867
+ // missing-value rules already applied), for K groups numbered 0..K-1 by
868
+ // the expression `group`. Each thread adds up its own range of groups;
869
+ // packed columns are read as they are.
870
+ _groupInto(group, K, sources, state) { return this.engine._drive(this.#groupIntoG(group, K, sources, state)); }
871
+ _groupIntoAsync(group, K, sources, state) { return this.engine._driveAsync(this.#groupIntoG(group, K, sources, state)); }
872
+
873
+ *#groupIntoG(group, K, sources, state) {
874
+ const { engine } = this;
875
+ const merged = this.params;
876
+ const bound = sources.map((src) => this.#bind(src, merged).src);
877
+ const analysis = analyze(bound);
878
+ if (analysis.items.length !== bound.length || bound.length > 8) throw new Error('vectro: internal: up to 8 different reductions per pass');
879
+ const J = bound.length;
880
+ if (!state.acc) {
881
+ state.acc = engine.alloc(Float64Array, J * K);
882
+ analysis.items.forEach((it, j) => engine.live(state.acc).fill(IDENTITY[it.reduce], j * K, (j + 1) * K));
883
+ }
884
+ if (!this.n) return;
885
+ const gData = engine.alloc(this.type, this.n);
886
+ try {
887
+ yield* engine.evalG(this.#bind(group, merged).src, this.#vars(merged), gData);
888
+ const predAst = this.pred ? parse(this.pred) : null;
889
+ const names = [...analysis.vars];
890
+ if (predAst) walk(predAst, (m) => { if (m.t === 'var' && !names.includes(m.name)) names.push(m.name); });
891
+ const arrayNames = names.filter((nm) => this.cols.has(nm)), scalarNames = names.filter((nm) => !this.cols.has(nm));
892
+ const t = this.type === Float32Array ? 'f32' : 'f64';
893
+ const encs = arrayNames.map((nm) => this.cols.get(nm).enc ?? null);
894
+ const cacheKey = `grouprange|${t}|${bound.join(';')}|${this.pred}|${arrayNames}|${scalarNames}|${JSON.stringify([encs, null])}`;
895
+ engine._tableKernels ??= new Map();
896
+ let v = engine._tableKernels.get(cacheKey);
897
+ if (!v) {
898
+ v = engine._register(generateGroupBy(analysis.items, predAst, t, arrayNames, scalarNames, 1, engine.isShared, true, encs, null));
899
+ engine._tableKernels.set(cacheKey, v);
900
+ }
901
+ const arrays = [...arrayNames.map((nm) => this.cols.get(nm).data), gData];
902
+ yield* engine._groupRangeG(v, this.n, arrays, scalarNames.map((nm) => merged[nm]), engine.live(state.acc).byteOffset, K);
903
+ } finally {
904
+ engine.free(gData);
905
+ }
906
+ }
907
+
908
+ // Per group number (0..K-1): [row counts, then one array per item]. A
909
+ // kernel takes at most 8 reductions (the row count is one): more items run
910
+ // in several passes.
911
+ *#groupTotalsG(groups, items, merged) {
912
+ if (items.length > 7) {
913
+ const totals = [];
914
+ for (let i = 0; i < items.length; i += 7) {
915
+ const part = yield* this.#groupTotalsG(groups, items.slice(i, i + 7), merged);
916
+ if (!part) return null;
917
+ totals.push(...(i ? part.slice(1) : part));
918
+ }
919
+ return totals;
920
+ }
921
+ const { engine } = this;
922
+ const { K } = groups;
923
+ const sources = ['count(1)', ...items.map((it) => `${it.fn === 'mean' ? 'sum' : it.fn}(${it.arg ?? 1})`)];
924
+ const analysis = analyze(sources);
925
+ const predAst = this.pred ? parse(this.pred) : null;
926
+ const names = [...analysis.vars];
927
+ if (predAst) walk(predAst, (m) => { if (m.t === 'var' && !names.includes(m.name)) names.push(m.name); });
928
+ const arrayNames = names.filter((nm) => this.cols.has(nm));
929
+ const scalarNames = names.filter((nm) => !this.cols.has(nm));
930
+ const t = this.type === Float32Array ? 'f32' : 'f64';
931
+
932
+ const J = analysis.items.length;
933
+ const T = engine.T;
934
+ // Few groups: 4 rotating copies per group (see generateGroupBy). The
935
+ // interpreter's precompiled scatter kernels use one slot per group.
936
+ const C = engine.jit && K <= 1024 ? 4 : 1;
937
+ let totals;
938
+ // Per-thread accumulators pay off while they stay small; past that (or
939
+ // past the memory cap) one thread adds up rows that were already
940
+ // filtered (the hash path collects them first).
941
+ const slotsNeeded = T * J * K * C;
942
+ const big = T > 1 && slotsNeeded > (engine._bigGroupSlots ?? 2 ** 23);
943
+ // packed columns (and a packed key) are decoded by the kernel as it reads them
944
+ const encs = arrayNames.map((nm) => this.cols.get(nm).enc ?? null), keyEnc = groups.enc ?? null;
945
+ const look = groups.lookup ?? null; // (join keys looked up as group numbers, see _lookupGroupAgg)
946
+ if (look && big) return null; // (many groups: every thread would scan every row; the usual join path)
947
+ const encKey = JSON.stringify([encs, keyEnc, !!look]);
948
+ const lookArgs = look ? [engine.live(look.lut).byteOffset, look.min, look.size] : [];
949
+ if (big && engine.jit && J * K <= (engine._maxGroupSlots ?? MAX_GROUP_SLOTS)) {
950
+ // many groups: each thread adds up its own range of groups, all rows
951
+ const acc = engine.alloc(Float64Array, J * K);
952
+ analysis.items.forEach((it, j) => engine.live(acc).fill(IDENTITY[it.reduce], j * K, (j + 1) * K));
953
+ const cacheKey = `grouprange|${t}|${sources.join(';')}|${this.pred}|${arrayNames}|${scalarNames}|${encKey}`;
954
+ engine._tableKernels ??= new Map();
955
+ let v = engine._tableKernels.get(cacheKey);
956
+ if (!v) {
957
+ v = engine._register(generateGroupBy(analysis.items, predAst, t, arrayNames, scalarNames, 1, engine.isShared, true, encs, keyEnc, !!look));
958
+ engine._tableKernels.set(cacheKey, v);
959
+ }
960
+ const arrays = [...arrayNames.map((nm) => this.cols.get(nm).data), groups.data];
961
+ yield* engine._groupRangeG(v, this.n, arrays, [...scalarNames.map((nm) => merged[nm]), ...lookArgs], engine.live(acc).byteOffset, K);
962
+ const a = engine.live(acc);
963
+ totals = analysis.items.map((_, j) => a.slice(j * K, (j + 1) * K));
964
+ engine.free(acc);
965
+ } else if (slotsNeeded > (engine._maxGroupSlots ?? MAX_GROUP_SLOTS) || (!this.pred && big)) {
966
+ if (look) return null; // (the scatter reads group numbers: the caller joins the usual way)
967
+ let keyData = groups.data;
968
+ if (keyEnc) { // (the scatter reads group numbers as plain numbers)
969
+ keyData = engine.alloc(this.type, this.n);
970
+ const codes = engine.live(groups.data), o = engine.live(keyData);
971
+ for (let i = 0; i < this.n; i++) o[i] = decodeOne(keyEnc, codes[i], t === 'f32');
972
+ }
973
+ try { totals = yield* this.#scatterG(keyData, K, analysis.items, sources, merged); } finally { if (keyData !== groups.data) engine.free(keyData); } // too many groups for per-thread accumulators
974
+ } else {
975
+ // Accumulators: one block per thread, padded to 256 bytes so two threads
976
+ // never update the same cache line. Every slot starts at its identity.
977
+ const block = Math.ceil((J * K * C) / 32) * 32; // in float64s
978
+ const acc = engine.alloc(Float64Array, Math.max(1, T * block));
979
+ const slots = (s, j) => [s * block + j * K * C, s * block + (j + 1) * K * C];
980
+ analysis.items.forEach((it, j) => {
981
+ for (let s = 0; s < T; s++) acc.fill(IDENTITY[it.reduce], ...slots(s, j));
982
+ });
983
+ if (engine.jit) {
984
+ const cacheKey = `group|${t}|${C}|${sources.join(';')}|${this.pred}|${arrayNames}|${scalarNames}|${encKey}`;
985
+ engine._tableKernels ??= new Map();
986
+ let v = engine._tableKernels.get(cacheKey);
987
+ if (!v) {
988
+ v = engine._register(generateGroupBy(analysis.items, predAst, t, arrayNames, scalarNames, C, engine.isShared, false, encs, keyEnc, !!look));
989
+ engine._tableKernels.set(cacheKey, v);
990
+ }
991
+ const arrays = [...arrayNames.map((nm) => this.cols.get(nm).data), groups.data];
992
+ yield* engine._groupG(v, this.n, arrays, [...scalarNames.map((nm) => merged[nm]), ...lookArgs], acc.byteOffset, block * 8, K);
993
+ } else {
994
+ if (look) { engine.free(acc); return null; } // (the interpreter has no lookup)
995
+ // the calling thread fills thread 0's block
996
+ const cols = Object.fromEntries(arrayNames.map((nm) => [nm, this.cols.get(nm).data]));
997
+ interpretGroup(engine, t, analysis.items, predAst, cols, merged, groups.data, acc, K, this.n);
998
+ }
999
+
1000
+ // Combine threads (and copies) into one value per group and aggregation:
1001
+ // the thread blocks by fused passes added into thread 0's, 15 blocks a
1002
+ // pass (kernels, so fast from the first query; small ones run on this
1003
+ // thread), then the C copies of each group by a plain loop.
1004
+ totals = [];
1005
+ for (const [j, it] of analysis.items.entries()) {
1006
+ let from = T;
1007
+ if (T > 1 && engine.jit) {
1008
+ for (let s0 = 1; s0 < T; s0 += 15) {
1009
+ const vars = { t0: acc.subarray(...slots(0, j)) };
1010
+ for (let s = s0; s < Math.min(T, s0 + 15); s++) vars[`t${s}`] = acc.subarray(...slots(s, j));
1011
+ const vnames = Object.keys(vars);
1012
+ const expr = it.reduce === 'min' || it.reduce === 'max'
1013
+ ? vnames.slice(1).reduce((e, nm) => `${it.reduce}(${e}, ${nm})`, vnames[0])
1014
+ : vnames.join(' + ');
1015
+ yield* engine.evalG(expr, vars, vars.t0);
1016
+ }
1017
+ from = 1;
1018
+ }
1019
+ const a = engine.live(acc);
1020
+ if (C === 1 && from === 1) { const [lo, hi] = slots(0, j); totals.push(a.slice(lo, hi)); continue; }
1021
+ const op = it.reduce === 'min' ? Math.min : it.reduce === 'max' ? Math.max : (x, y) => x + y;
1022
+ const out = new Float64Array(K).fill(IDENTITY[it.reduce]);
1023
+ for (let s = 0; s < from; s++) {
1024
+ const [lo] = slots(s, j);
1025
+ for (let g = 0; g < K; g++) for (let c = 0; c < C; c++) out[g] = op(out[g], a[lo + g * C + c]);
1026
+ }
1027
+ totals.push(out);
1028
+ }
1029
+ engine.free(acc);
1030
+ }
1031
+
1032
+ return totals;
1033
+ }
1034
+
1035
+ // Distinct values of `arg` (bound source) in each group: groupData holds
1036
+ // each row's group number (0..K-1), or null for one group. The table must
1037
+ // have no filter. The (group, value) pairs are numbered with the hash
1038
+ // table (every thread), and each pair adds one to its group. Missing values
1039
+ // (NaN) don't count, as in SQL's COUNT(DISTINCT x).
1040
+ *#distinctCountsG(arg, merged, groupData, K) {
1041
+ if (this.#hasPacked()) return yield* this.#viaPlainG((t) => t.#distinctCountsG(arg, merged, groupData, K));
1042
+ const { engine, n } = this;
1043
+ const counts = new Float64Array(K);
1044
+ if (!n) return counts;
1045
+ const plain = /^[A-Za-z_]\w*$/.test(arg.trim()) ? this.cols.get(arg.trim()) : null;
1046
+ let own = null;
1047
+ if (!plain) { own = engine.alloc(this.type, n); yield* engine.evalG(arg, this.#vars(merged), own); }
1048
+ if (engine.jit) { // whole numbers in a small range: a byte per (group, value) instead of the hash table
1049
+ let dense = null;
1050
+ try {
1051
+ dense = yield* this.#denseDistinctG(plain ? plain.data : own, groupData, K);
1052
+ if (!dense && n >= (engine._partitionRows ?? PARTITION_ROWS) && !engine._noPartitionedDistinct) dense = yield* this.#partitionedDistinctG(plain ? plain.data : own, groupData, K);
1053
+ if (!dense && n >= 4096) dense = yield* this.#numberedDistinctG(plain ? plain.data : own, groupData, K);
1054
+ } catch (err) { if (own) engine.free(own); throw err; }
1055
+ if (dense) { if (own) engine.free(own); return dense; }
1056
+ }
1057
+ const cols = new Map();
1058
+ if (groupData) cols.set('g', { data: groupData, dict: null, codes: null });
1059
+ cols.set('v', { data: plain ? plain.data : own, dict: null, codes: null });
1060
+ const tmp = new Table(engine, cols, n, { zones: false });
1061
+ const keys = groupData ? ['g', 'v'] : ['v'];
1062
+ let pairs = null;
1063
+ try {
1064
+ // small values: pair numbers as they are; others: the hash table
1065
+ pairs = (yield* tmp.#directGroups(keys, 1)) ?? (engine.jit && n >= 4096 ? yield* tmp.#hashGroupsParallelG(keys) : tmp.#hashGroups(keys));
1066
+ const [rowsPer] = yield* tmp.#groupTotalsG(pairs, [], merged); // which pair numbers have rows (every thread)
1067
+ let m = 0;
1068
+ for (let p = 0; p < pairs.K; p++) if (rowsPer[p] > 0) m++;
1069
+ const keep = new Float64Array(m);
1070
+ for (let p = 0, r = 0; p < pairs.K; p++) if (rowsPer[p] > 0) keep[r++] = p;
1071
+ const gs = groupData ? pairs.keyColumn(0, keep) : null, vs = pairs.keyColumn(groupData ? 1 : 0, keep);
1072
+ for (let r = 0; r < m; r++) if (vs[r] === vs[r]) counts[gs ? gs[r] : 0]++;
1073
+ return counts;
1074
+ } finally {
1075
+ if (pairs?.owned) engine.free(pairs.data);
1076
+ for (const x of pairs?.extra ?? []) engine.free(x);
1077
+ if (own) engine.free(own);
1078
+ }
1079
+ }
1080
+
1081
+ // Distinct counts per group (see #distinctCountsG) when the values are whole
1082
+ // numbers spanning a range small enough for one byte per (group, value):
1083
+ // every thread marks its rows' pairs in one byte map (generateMarkPairs),
1084
+ // then each group's marks are counted. Else null.
1085
+ *#denseDistinctG(values, groupData, K) {
1086
+ const { engine, n } = this;
1087
+ const [lo, hi, frac] = yield* engine.evalG(['min(v == v ? v : 1 / 0)', 'max(v == v ? v : -1 / 0)', 'count(v == v && v != floor(v))'], { v: values }); // (missing values skipped)
1088
+ if (!(hi >= lo) || frac > 0) return hi >= lo ? null : new Float64Array(K); // (no values: no distinct ones)
1089
+ const fits = (span) => Math.ceil((span + 1) / 8) * 8 * K <= Math.min(2 ** 26, Math.max(2 ** 20, 16 * n));
1090
+ // too wide, but maybe every value is lo plus a multiple of one step (IDs 1000, 1010, 1020...):
1091
+ // the step from a sample (it divides the real one), checked on every row
1092
+ let step = 1;
1093
+ if (!fits(hi - lo) && hi - lo < 2 ** 53 && !engine._noStride) {
1094
+ step = sampleStep(engine.live(values), n, lo);
1095
+ if (!(step > 1 && fits((hi - lo) / step))) return null;
1096
+ }
1097
+ const width = Math.ceil(((hi - lo) / step + 1) / 8) * 8; // (each group's bytes start on an 8-byte boundary)
1098
+ if (!(width * K <= Math.min(2 ** 26, Math.max(2 ** 20, 16 * n)))) return null;
1099
+ const t = this.type === Float32Array ? 'f32' : 'f64';
1100
+ engine._tableKernels ??= new Map();
1101
+ const name = `markpairs|${t}|${step > 1}`;
1102
+ let v = engine._tableKernels.get(name);
1103
+ if (!v) { v = engine._register(generateMarkPairs(t, engine.isShared, step > 1)); engine._tableKernels.set(name, v); }
1104
+ const map = engine.alloc(Uint8Array, width * K);
1105
+ try {
1106
+ engine.live(map).fill(0);
1107
+ const { parts } = yield* engine._fusedG(v, n, [groupData ?? values, values], [engine.live(map).byteOffset, width, lo, groupData ? 1 : 0, step], 1);
1108
+ if (step > 1 && parts.some((x) => x > 0)) return null; // (some value off the step: checked in f64 as it marks, so float32 tables too)
1109
+ return countMarks(engine.live(map), K, width);
1110
+ } finally {
1111
+ engine.free(map);
1112
+ }
1113
+ }
1114
+
1115
+ // Distinct counts of any values with many of them (see partition.mjs): the
1116
+ // distinct (group, value) pairs are the groups of a partitioned group-by
1117
+ // with nothing to add up. One group: only how many there are (no sort, no
1118
+ // result columns). Missing values make one pair per group, taken off.
1119
+ // Null when few distinct pairs are expected (the hash path is cheaper) or
1120
+ // the partitioned path turns them down.
1121
+ *#partitionedDistinctG(values, groupData, K) {
1122
+ const { engine, n } = this;
1123
+ const cols = new Map();
1124
+ if (groupData) cols.set('g', { data: groupData, dict: null, codes: null });
1125
+ cols.set('v', { data: values, dict: null, codes: null });
1126
+ const tmp = new Table(engine, cols, n, { zones: false });
1127
+ const keys = groupData ? ['g', 'v'] : ['v'];
1128
+ const est = estimateGroups(tmp, keys);
1129
+ if (est < (engine._partitionGroups ?? PARTITION_GROUPS)) return null;
1130
+ if (!groupData) {
1131
+ const r = yield* partitionedGroupsG(tmp, keys, [], {}, est, { countOnly: true });
1132
+ if (!r) return null;
1133
+ const [missing] = yield* engine.evalG(['count(v != v)'], { v: values });
1134
+ return Float64Array.of(r.G - (missing > 0 ? 1 : 0));
1135
+ }
1136
+ const r = yield* partitionedGroupsG(tmp, keys, [], {}, est);
1137
+ if (!r) return null;
1138
+ try { // the pairs come sorted by group, then value (NaN last): each group's run, found by binary search
1139
+ const counts = new Float64Array(K), gs = engine.live(r.keyCols[0]), vs = engine.live(r.keyCols[1]);
1140
+ // galloping from the last run's end: O(K log(G / K)), not O(K log G) (1M groups: 90 ms -> 16 ms)
1141
+ const after = (g, s) => {
1142
+ let a = s, b = s, step = 1;
1143
+ while (b < r.G && gs[b] <= g) { a = b + 1; b = s + step; step *= 2; }
1144
+ if (b > r.G) b = r.G;
1145
+ while (a < b) { const m = (a + b) >>> 1; if (gs[m] <= g) a = m + 1; else b = m; }
1146
+ return a;
1147
+ };
1148
+ for (let g = 0, s = 0; g < K; g++) {
1149
+ const e = after(g, s);
1150
+ counts[g] = e - s - (e > s && vs[e - 1] !== vs[e - 1] ? 1 : 0);
1151
+ s = e;
1152
+ }
1153
+ return counts;
1154
+ } finally {
1155
+ for (const x of [...r.keyCols, ...r.accCols]) engine.free(x);
1156
+ }
1157
+ }
1158
+
1159
+ // Distinct counts of any values: each distinct value numbered by the hash
1160
+ // table (every thread; missing values left out), then the numbers counted
1161
+ // as in #denseDistinctG. Null when that byte map would be too big.
1162
+ *#numberedDistinctG(values, groupData, K) {
1163
+ const { engine, n } = this;
1164
+ const one = new Table(engine, new Map([['v', { data: values, dict: null, codes: null }]]), n, { zones: false });
1165
+ const ids = yield* one.#hashGroupsParallelG(['v']);
1166
+ for (const x of ids.extra ?? []) engine.free(x);
1167
+ try {
1168
+ if (K * Math.ceil(ids.K / 8) * 8 > Math.min(2 ** 26, Math.max(2 ** 20, 16 * n))) return null;
1169
+ yield* engine.evalG('v == v ? id : 0 / 0', { v: values, id: ids.data }, ids.data); // (missing values: no number)
1170
+ return yield* this.#denseDistinctG(ids.data, groupData, K);
1171
+ } finally {
1172
+ engine.free(ids.data);
1173
+ }
1174
+ }
1175
+
1176
+ // Many groups (more than per-thread accumulators allow): evaluate each
1177
+ // aggregation's argument as a column (on all threads), then add up per
1178
+ // group on this thread. `groups` has no filter here (rows were collected).
1179
+ *#scatterG(groupData, K, kernelItems, sources, merged) {
1180
+ const { engine } = this;
1181
+ const totals = [];
1182
+ for (const [j, it] of kernelItems.entries()) {
1183
+ const out = new Float64Array(K).fill(IDENTITY[it.reduce]);
1184
+ const ast = parse(sources[j]);
1185
+ const arg = ast.args[0];
1186
+ let vals = null;
1187
+ if (!(arg.t === 'num' && arg.v === 1 && it.reduce === 'count')) {
1188
+ const src = it.reduce === 'count' ? `(${toSource(arg)}) ${isMask(arg) ? '' : '!= 0'} ? 1 : 0` : toSource(arg);
1189
+ const col = engine.alloc(this.type, this.n);
1190
+ yield* engine.evalG(src, this.#vars(merged), col);
1191
+ vals = Float64Array.from(engine.live(col));
1192
+ engine.free(col);
1193
+ }
1194
+ const n = this.n, ids = engine.live(groupData); // (after any allocation above)
1195
+ if (!vals) for (let i = 0; i < n; i++) out[ids[i]]++;
1196
+ else if (it.reduce === 'min') for (let i = 0; i < n; i++) out[ids[i]] = Math.min(out[ids[i]], vals[i]);
1197
+ else if (it.reduce === 'max') for (let i = 0; i < n; i++) out[ids[i]] = Math.max(out[ids[i]], vals[i]);
1198
+ else for (let i = 0; i < n; i++) out[ids[i]] += vals[i];
1199
+ totals.push(out);
1200
+ }
1201
+ return totals;
1202
+ }
1203
+
1204
+ // Materialize the filter: a new table holding only the matching rows.
1205
+ // Pass 1 counts matches per unit of work; pass 2 copies every unit's rows
1206
+ // straight to its final offset, all columns at once.
1207
+ collect() { return this.engine._drive(this.#collectG()); }
1208
+ collectAsync() { return this.engine._driveAsync(this.#collectG()); }
1209
+
1210
+ *#collectG() {
1211
+ if (this.#hasPacked() && this.pred) return yield* (this.engine.jit ? this.#collectPackedG() : this.#viaPlainG((t) => t.#collectG())); // (gathering packed codes needs compiled kernels)
1212
+ const narrow = this.#narrow();
1213
+ if (narrow !== this) return yield* narrow.#collectG();
1214
+ if (!this.pred) return this;
1215
+ const { engine } = this;
1216
+ const vars = this.#vars(this.params);
1217
+ if (!engine.jit) {
1218
+ const names = this.columnNames;
1219
+ const dst = interpretCompact(engine, this.type === Float32Array ? 'f32' : 'f64', parse(this.pred), vars, this.params, names, this.n);
1220
+ const out = new Map(names.map((nm, k) => [nm, { ...this.cols.get(nm), data: dst[k] }]));
1221
+ return new Table(engine, out, dst[0].length, { owned: names });
1222
+ }
1223
+ const { parts, units } = yield* engine.compile(`count(${this.pred})`).runPartsG(vars);
1224
+ const offsets = engine.alloc(Float64Array, units);
1225
+ let total = 0;
1226
+ for (let u = 0; u < units; u++) { offsets[u] = total; total += parts[u]; }
1227
+
1228
+ const predAst = parse(this.pred);
1229
+ const predNames = [];
1230
+ walk(predAst, (m) => { if (m.t === 'var' && !predNames.includes(m.name)) predNames.push(m.name); });
1231
+ const scalarNames = predNames.filter((nm) => !this.cols.has(nm));
1232
+ const scalars = scalarNames.map((nm) => this.params[nm]);
1233
+ const t = this.type === Float32Array ? 'f32' : 'f64';
1234
+
1235
+ const out = new Map();
1236
+ const names = this.columnNames;
1237
+ // A kernel takes at most 32 arrays, so wide tables are copied in batches.
1238
+ for (let i = 0; i < names.length; i += 12) {
1239
+ const batch = names.slice(i, i + 12);
1240
+ const srcNames = [...new Set([...predNames.filter((nm) => this.cols.has(nm)), ...batch])];
1241
+ const copyFrom = batch.map((nm) => srcNames.indexOf(nm));
1242
+ const cacheKey = `compact|${t}|${this.pred}|${srcNames}|${copyFrom}|${scalarNames}`;
1243
+ engine._tableKernels ??= new Map();
1244
+ let v = engine._tableKernels.get(cacheKey);
1245
+ if (!v) {
1246
+ v = engine._register(generateCompact(predAst, t, srcNames, copyFrom, scalarNames, engine.isShared));
1247
+ engine._tableKernels.set(cacheKey, v);
1248
+ }
1249
+ const dst = batch.map(() => engine.alloc(this.type, total));
1250
+ yield* engine._compactG(v, this.n, srcNames.map((nm) => this.cols.get(nm).data), dst, offsets, scalars, units);
1251
+ batch.forEach((nm, k) => out.set(nm, { ...this.cols.get(nm), data: dst[k], zones: undefined })); // (new rows: no zone maps yet)
1252
+ }
1253
+ engine.free(offsets);
1254
+ return new Table(engine, out, total, { owned: names });
1255
+ }
1256
+
1257
+ // A view with only some columns (no copy). Columns the filter reads stay
1258
+ // available to it.
1259
+ select(...names) {
1260
+ const keep = new Set(names.flat());
1261
+ for (const nm of keep) if (!this.cols.has(nm)) throw new Error(`vectro: no column "${nm}"`);
1262
+ if (this.pred) walk(parse(this.pred), (m) => { if (m.t === 'var' && this.cols.has(m.name)) keep.add(m.name); });
1263
+ const cols = new Map([...keep].map((nm) => [nm, this.cols.get(nm)])); // in the order asked (then the filter's columns)
1264
+ return new Table(this.engine, cols, this.n, { pred: this.pred, params: this.params, zones: this.zones });
1265
+ }
1266
+
1267
+ // ---- sorting ---------------------------------------------------------------
1268
+ // sort(by): a new table in order of `by` (a column, an expression, or a list;
1269
+ // "-" first means large to small; text sorts by label). Stable; NaN last.
1270
+ // The order is computed on the calling thread (a radix sort); key
1271
+ // expressions are computed on all threads.
1272
+ sort(by) { return this.engine._drive(this.#sortG(by)); }
1273
+ sortAsync(by) { return this.engine._driveAsync(this.#sortG(by)); }
1274
+
1275
+ // (SQL window functions, see window.mjs) The rows, sorted by the partition
1276
+ // keys and then the order keys, with one new column per item:
1277
+ // spec = { partition: [names], order: [{ name, desc }], items: [{ name,
1278
+ // fn, arg (a column name or null), frame, offset, def, k, p }] }
1279
+ // A view with columns renamed (map: old name -> new name); a column the new
1280
+ // names replace is left out. The view owns nothing (see plan.mjs deriveAllG).
1281
+ _renamed(map) {
1282
+ const taken = new Set(map.values()), cols = new Map();
1283
+ for (const [name, c] of this.cols) {
1284
+ if (map.has(name)) cols.set(map.get(name), c);
1285
+ else if (!taken.has(name)) cols.set(name, c);
1286
+ }
1287
+ return new Table(this.engine, cols, this.n, { pred: this.pred, params: this.params, zones: false });
1288
+ }
1289
+
1290
+ // (SQL text functions) a new column `name` = fn(values of the input
1291
+ // columns), run once per distinct combination of them (see textmap.mjs):
1292
+ // text answers make a category column with state's labels ({ dict, codes },
1293
+ // shared by every call with that state), else numbers (date: true marks dates).
1294
+ _textmap(name, spec) { return this.engine._drive(this.#textmapG(name, spec)); }
1295
+ _textmapAsync(name, spec) { return this.engine._driveAsync(this.#textmapG(name, spec)); }
1296
+
1297
+ *#textmapG(name, { inputs = [], fn, text = true, date = false, labels = null, state = { dict: [], codes: new Map() } }) {
1298
+ checkName(name);
1299
+ const { engine } = this;
1300
+ const t = this.pred ? yield* this.#collectG() : this;
1301
+ if (t.cols.has(name)) throw new Error(`vectro: column "${name}" already exists`);
1302
+ const ins = inputs.map((k) => t.cols.get(k) ?? (() => { throw new Error(`vectro: no column "${k}"`); })());
1303
+ if (labels) { // codes already: copied (on every thread), with the labels
1304
+ const data = engine.alloc(t.type, t.n);
1305
+ if (t.n) yield* engine.evalG('x', { x: ins[0].enc ? { data: ins[0].data, enc: ins[0].enc, n: t.n, type: t.type === Float32Array ? 'f32' : 'f64' } : ins[0].data }, data);
1306
+ const cols = new Map(t.cols), owned = t === this ? [] : [...t.owned];
1307
+ if (t !== this) t.owned = [];
1308
+ cols.set(name, { data, dict: state.dict, codes: state.codes });
1309
+ owned.push(name);
1310
+ for (const c of cols.values()) c.data = engine.live(c.data);
1311
+ return new Table(engine, cols, t.n, { owned, ownedBuffers: t === this ? [] : t.ownedBuffers.splice(0), zones: false });
1312
+ }
1313
+ const ranges = [], made = []; // number columns' [min, max, values not whole] (every thread)
1314
+ for (let k = 0; k < ins.length; k++) {
1315
+ const c = ins[k];
1316
+ if (c.dict || c.strings || c.enc || !t.n) { ranges.push(null); continue; } // (packed: read value by value)
1317
+ const r = yield* engine.evalG(['min(k)', 'max(k)', 'count(k == k && k != floor(k))'], { k: c.data });
1318
+ if (r && !r[2] && r[1] - r[0] < 2 ** 22) { ranges.push(r); continue; }
1319
+ // others: numbered by the hash group kernel (every thread), each number's value once
1320
+ const one = new Table(engine, new Map([['v', c]]), t.n, { zones: false });
1321
+ const ids = engine.jit && t.n >= 4096 ? yield* one.#hashGroupsParallelG(['v']) : one.#hashGroups(['v']);
1322
+ made.push(ids.data);
1323
+ const vals = ids.keyColumn(0, Array.from({ length: ids.K }, (_, g) => g));
1324
+ for (const x of ids.extra ?? []) engine.free(x); // (the key values: read just above)
1325
+ ins[k] = { data: ids.data, dict: null, numbered: (g) => vals[g] };
1326
+ ranges.push([0, Math.max(0, ids.K - 1), 0]);
1327
+ }
1328
+ const data = engine.alloc(t.type, t.n);
1329
+ try { textmapRows(engine, ins, t.n, t.type === Float32Array, fn, text, state, engine.live(data), ranges); } finally { for (const x of made) engine.free(x); }
1330
+ const cols = new Map(t.cols), owned = t === this ? [] : [...t.owned];
1331
+ if (t !== this) t.owned = [];
1332
+ cols.set(name, text ? { data, dict: state.dict, codes: state.codes } : { data, dict: null, codes: null, date: date || undefined });
1333
+ owned.push(name);
1334
+ for (const c of cols.values()) c.data = engine.live(c.data);
1335
+ return new Table(engine, cols, t.n, { owned, ownedBuffers: t === this ? [] : t.ownedBuffers.splice(0), zones: false });
1336
+ }
1337
+
1338
+ _window(spec) { return this.engine._drive(this.#windowG(spec)); }
1339
+ _windowAsync(spec) { return this.engine._driveAsync(this.#windowG(spec)); }
1340
+
1341
+ *#windowG({ partition = [], order = [], items, firstRanks = null }) {
1342
+ const { engine } = this;
1343
+ if (items.length && order.length === 1 && !order[0].desc && items.every((it) => (it.fn === 'percentile_cont' || it.fn === 'percentile_disc') && it.arg === order[0].name) && !this.cols.get(order[0].name)?.dict) return yield* this.#percentilesG(partition, items); // (text: by its sorted labels, below)
1344
+ if (firstRanks && order.length) { // only rows that can rank in the first k (see plan.mjs firstRanks)
1345
+ const few = yield* this.#firstRowsG(partition, order[0], firstRanks);
1346
+ if (few) { try { return yield* few.#windowG({ partition, order, items }); } finally { few.dispose(); } }
1347
+ }
1348
+ const by = [...partition, ...order.map((o) => `${o.desc ? '-' : ''}${o.name}`)];
1349
+ const t = by.length ? yield* this.#sortG(by) : yield* this.#collectG();
1350
+ const n = t.n, f32 = t.type === Float32Array;
1351
+ const values = (name) => { // a column's values (codes for text), plain
1352
+ const c = t.cols.get(name);
1353
+ if (!c) throw new Error(`vectro: no column "${name}"`);
1354
+ if (c.strings) throw new Error(`vectro: window functions can't read the free-text column "${name}"`);
1355
+ const d = engine.live(c.data), out = new Float64Array(n);
1356
+ if (c.enc) for (let i = 0; i < n; i++) out[i] = decodeOne(c.enc, d[i], f32); else out.set(d.subarray(0, n));
1357
+ return out;
1358
+ };
1359
+ const b = bounds(n, partition.map(values), order.map((o) => values(o.name)), readsPeers(items));
1360
+ const orderKey = order.length === 1 ? values(order[0].name) : null;
1361
+ const cols = new Map(t.cols), owned = t === this ? [] : [...t.owned];
1362
+ if (t !== this) t.owned = [];
1363
+ const KEEPS_KIND = new Set(['lag', 'lead', 'first_value', 'last_value', 'nth_value', 'min', 'max', 'percentile_disc']);
1364
+ for (const it of items) {
1365
+ if (cols.has(it.name)) throw new Error(`vectro: column "${it.name}" already exists`);
1366
+ const vals = windowValues(n, b, { ...it, arg: it.arg ? values(it.arg) : null }, order.length > 0, orderKey, order[0]?.desc);
1367
+ const data = engine.alloc(t.type, n);
1368
+ engine.live(data).set(vals);
1369
+ const src = it.arg && KEEPS_KIND.has(it.fn) ? t.cols.get(it.arg) : null; // (text stays text, dates stay dates)
1370
+ cols.set(it.name, { data, dict: src?.dict ?? null, codes: src?.codes ?? null, date: src?.date || (it.fn === 'percentile_cont' && t.cols.get(it.arg)?.date) || undefined });
1371
+ owned.push(it.name);
1372
+ }
1373
+ for (const c of cols.values()) c.data = engine.live(c.data);
1374
+ return new Table(engine, cols, n, { owned, zones: false });
1375
+ }
1376
+
1377
+ // The rows that can be among the first k of their partition in the window's
1378
+ // order: those whose first order key is at most the partition's k-th
1379
+ // smallest (all its ties too), as a new table; null when that isn't fewer
1380
+ // rows. Every row ordered before a kept row is kept, so row numbers and
1381
+ // ranks computed on these rows alone are the same.
1382
+ *#firstRowsG(partition, first, k) {
1383
+ const { engine } = this;
1384
+ const t = yield* this.#collectG();
1385
+ const made = []; // engine arrays to free
1386
+ try {
1387
+ const n = t.n, f32 = t.type === Float32Array, c = t.cols.get(first.name);
1388
+ if (n < 4096 || !c || c.strings || partition.some((p) => !t.cols.has(p))) return null;
1389
+ const parallel = engine.jit && !f32;
1390
+ // each row's partition number 0..K-1 (as float64 for the kernels)
1391
+ let K = 1, g = null;
1392
+ if (partition.length) {
1393
+ const ids = (yield* t.#directGroups(partition, 1)) ?? (engine.jit ? yield* t.#hashGroupsParallelG(partition) : t.#hashGroups(partition));
1394
+ for (const x of ids.extra ?? []) engine.free(x);
1395
+ K = ids.K;
1396
+ if (ids.owned) made.push(ids.data);
1397
+ if (ids.enc || !parallel) {
1398
+ const d = engine.live(ids.data), plain = parallel ? engine.alloc(Float64Array, n) : null, out = plain ? engine.live(plain) : new Int32Array(n);
1399
+ if (ids.enc) for (let i = 0; i < n; i++) out[i] = decodeOne(ids.enc, d[i], f32); else for (let i = 0; i < n; i++) out[i] = d[i];
1400
+ g = plain ?? out;
1401
+ if (plain) made.push(plain);
1402
+ } else g = ids.data;
1403
+ }
1404
+ if (K * k > 2 ** 24 || K * k * 2 > n) return null;
1405
+ let keep;
1406
+ if (parallel) {
1407
+ if (!g) { g = engine.alloc(Float64Array, n); engine.live(g).fill(0); made.push(g); }
1408
+ const plainKey = !c.dict && !c.enc;
1409
+ const key = plainKey ? c.data : engine.alloc(Float64Array, n);
1410
+ if (!plainKey) { engine.live(key).set(firstKey(engine, c, first.desc, n, false)); made.push(key); }
1411
+ keep = yield* firstRowsParallelG(engine, key, plainKey && first.desc, g, n, K, k);
1412
+ } else keep = firstRows(firstKey(engine, c, first.desc, n, f32), g, n, K, k);
1413
+ return keep.length * 2 > n ? null : t.#gather(keep, keep.length);
1414
+ } finally {
1415
+ for (const x of made) engine.free(x);
1416
+ if (t !== this) t.dispose();
1417
+ }
1418
+ }
1419
+
1420
+ // Percentiles of each partition, for every row, without sorting the table:
1421
+ // rows numbered by partition (as for groupBy), each partition's values
1422
+ // gathered (a counting sort by partition number), the ranks needed picked
1423
+ // with quickselect. Rows stay in their order.
1424
+ *#percentilesG(partition, items) {
1425
+ const { engine } = this;
1426
+ const t = yield* this.#collectG();
1427
+ const n = t.n, f32 = t.type === Float32Array;
1428
+ const plainOf = (c, data = c.data) => { const d = engine.live(data), out = new Float64Array(n); if (c.enc) for (let i = 0; i < n; i++) out[i] = decodeOne(c.enc, d[i], f32); else out.set(d.subarray(0, n)); return out; };
1429
+ let K = 1, g = null, gData = null; // each row's partition: JS int32, and in engine memory while needed
1430
+ const made = [];
1431
+ if (partition.length && n) {
1432
+ for (const k of partition) if (!t.cols.has(k)) throw new Error(`vectro: no column "${k}"`);
1433
+ const ids = (yield* t.#directGroups(partition, 1)) ?? (engine.jit && n >= 4096 ? yield* t.#hashGroupsParallelG(partition) : t.#hashGroups(partition));
1434
+ for (const x of ids.extra ?? []) engine.free(x);
1435
+ if (ids.owned) made.push(ids.data);
1436
+ K = ids.K;
1437
+ g = ids.enc ? Int32Array.from(plainOf({ enc: ids.enc }, ids.data)) : toInt32(engine.live(ids.data), n);
1438
+ if (!ids.enc && !f32) gData = ids.data;
1439
+ }
1440
+ try {
1441
+ return yield* t.#percentileColumnsG(items, K, g, gData, t !== this);
1442
+ } finally {
1443
+ for (const x of made) engine.free(x);
1444
+ }
1445
+ }
1446
+
1447
+ // (see #percentilesG) The new columns: each item's value for its row's
1448
+ // partition. temp: this table is a copy, its columns can move to the result.
1449
+ *#percentileColumnsG(items, K, g, gData, temp) {
1450
+ const { engine } = this;
1451
+ const t = this, n = t.n, f32 = t.type === Float32Array;
1452
+ const plainOf = (c, data = c.data) => { const d = engine.live(data), out = new Float64Array(n); if (c.enc) for (let i = 0; i < n; i++) out[i] = decodeOne(c.enc, d[i], f32); else out.set(d.subarray(0, n)); return out; };
1453
+ const cols = new Map(t.cols), owned = temp ? [...t.owned] : [];
1454
+ if (temp) t.owned = [];
1455
+ const valuesOf = (arg) => {
1456
+ const c = t.cols.get(arg);
1457
+ if (!c) throw new Error(`vectro: no column "${arg}"`);
1458
+ if (c.strings) throw new Error(`vectro: window functions can't read the free-text column "${arg}"`);
1459
+ return !c.enc && !f32 ? engine.live(c.data).subarray(0, n) : plainOf(c); // (read only)
1460
+ };
1461
+ // the ranks each item needs in each partition, then picked all at once per column
1462
+ const byArg = new Map();
1463
+ for (const it of items) {
1464
+ if (cols.has(it.name)) throw new Error(`vectro: column "${it.name}" already exists`);
1465
+ if (!byArg.has(it.arg)) byArg.set(it.arg, []);
1466
+ byArg.get(it.arg).push(it);
1467
+ }
1468
+ const per = new Map();
1469
+ for (const [arg, its] of byArg) {
1470
+ const c = t.cols.get(arg);
1471
+ let got = null;
1472
+ if (engine.jit && !f32 && n >= 65_536 && K <= 64 && c && !c.enc && !c.strings && (gData || K === 1)) got = yield* t.#percentilesParallelG(c.data, gData, K, its);
1473
+ got ??= percentiles(valuesOf(arg), g, K, its);
1474
+ its.forEach((it, j) => per.set(it, got[j]));
1475
+ }
1476
+ for (const it of items) {
1477
+ const per1 = per.get(it);
1478
+ const data = engine.alloc(t.type, n), d = engine.live(data);
1479
+ if (g) spread(d, per1, g, n); else d.fill(per1[0] ?? NaN, 0, n);
1480
+ const src = it.fn === 'percentile_disc' ? t.cols.get(it.arg) : null; // (text stays text, dates stay dates)
1481
+ cols.set(it.name, { data, dict: src?.dict ?? null, codes: src?.codes ?? null, date: t.cols.get(it.arg)?.date || undefined });
1482
+ owned.push(it.name);
1483
+ }
1484
+ for (const c of cols.values()) c.data = engine.live(c.data);
1485
+ return new Table(engine, cols, n, { owned, zones: false });
1486
+ }
1487
+
1488
+ // percentiles (below) for few partitions on every thread: each value's
1489
+ // bucket (4096 per partition, evenly over the values' range; missing
1490
+ // values in one more) is counted with the group kernel, the buckets
1491
+ // holding the wanted ranks found from the counts, and only their values
1492
+ // collected and selected among. values, gData: float64 engine arrays (gData
1493
+ // null: one partition). null when the values aren't all finite.
1494
+ *#percentilesParallelG(values, gData, K, items) {
1495
+ const { engine, n } = this;
1496
+ const [lo, hi] = yield* engine.evalG(['min(v == v ? v : 1 / 0)', 'max(v == v ? v : -1 / 0)'], { v: values });
1497
+ if (!(hi >= lo)) return items.map(() => new Float64Array(K).fill(NaN));
1498
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return null;
1499
+ const B = 4096, W = B + 1, vars = { v: values, lo, sc: hi > lo ? B / (hi - lo) : 0 };
1500
+ if (gData) vars.g = gData;
1501
+ const bucket = `(v == v ? (floor((v - lo) * sc) >= ${B} ? ${B - 1} : floor((v - lo) * sc)) : ${B})`;
1502
+ const keys = engine.alloc(Float64Array, n);
1503
+ try {
1504
+ yield* engine.evalG(gData ? `g * ${W} + ${bucket}` : bucket, vars, keys);
1505
+ const [counts] = yield* this.#groupTotalsG({ K: K * W, data: keys }, [], {});
1506
+ const sizes = new Int32Array(K);
1507
+ for (let k = 0; k < K; k++) { let m = 0; for (let b = k * W, e = b + B; b < e; b++) m += counts[b]; sizes[k] = m; }
1508
+ const { plan, wants } = percentileRanks(sizes, items);
1509
+ // each wanted rank's bucket and its rank there
1510
+ const chosen = new Map(), place = wants.map(([k, r]) => {
1511
+ let b = k * W, below = 0;
1512
+ while (below + counts[b] <= r) below += counts[b++];
1513
+ if (!chosen.has(b)) chosen.set(b, chosen.size);
1514
+ return [chosen.get(b), r - below];
1515
+ });
1516
+ const got = new Float64Array(wants.length);
1517
+ if (wants.length) {
1518
+ const cols = new Map([['k', { data: keys, dict: null, codes: null }], ['v', { data: values, dict: null, codes: null }]]);
1519
+ const pred = [...chosen.keys()].map((b) => `k == ${b}`).join(' || ');
1520
+ const few = yield* new Table(engine, cols, n, { pred, zones: false }).#collectG();
1521
+ try {
1522
+ const fk = engine.live(few.cols.get('k').data), fv = engine.live(few.cols.get('v').data);
1523
+ const bufs = [...chosen.keys()].map((b) => new Float64Array(counts[b])), fill = new Int32Array(bufs.length);
1524
+ for (let i = 0; i < few.n; i++) { const j = chosen.get(fk[i]); bufs[j][fill[j]++] = fv[i]; }
1525
+ place.forEach(([j, r], w) => { got[w] = select(bufs[j], 0, bufs[j].length, r); });
1526
+ } finally {
1527
+ few.dispose();
1528
+ }
1529
+ }
1530
+ return percentileValues(plan, got);
1531
+ } finally {
1532
+ engine.free(keys);
1533
+ }
1534
+ }
1535
+
1536
+ // (SQL UNION ALL) this table's rows, then each other's, as a new table;
1537
+ // columns by position, named as this table's.
1538
+ _concat(others) {
1539
+ const names = this.columnNames;
1540
+ const parts = [this, ...others].map((t) => {
1541
+ const tn = t.columnNames;
1542
+ if (tn.length !== names.length) throw new Error(`vectro: UNION needs the same number of columns on each side (${names.length} and ${tn.length})`);
1543
+ const kind = (c) => (c.strings ? 'free text' : c.dict ? 'text' : 'number');
1544
+ const cols = new Map(names.map((nm, k) => {
1545
+ const c = t.cols.get(tn[k]), want = this.cols.get(nm);
1546
+ if (kind(c) !== kind(want)) throw new Error(`vectro: UNION column ${k + 1} is ${kind(want)} on one side and ${kind(c)} on another`);
1547
+ return [nm, c];
1548
+ }));
1549
+ const v = new Table(t.engine, cols, t.n, { pred: t.pred, params: t.params });
1550
+ return v.pred ? v.collect() : v;
1551
+ });
1552
+ // rows in one part only (an outer join's unmatched rows are often none): that part as it is
1553
+ const full = parts.filter((p) => p.n > 0);
1554
+ const keep = full.length === 1 ? full[0] : null;
1555
+ try { return keep ?? copyRows(this.engine, parts.map((t) => [t, 0, t.n])); } finally { for (const p of parts) if (p !== keep && p.owned.length) p.dispose(); }
1556
+ }
1557
+ async _concatAsync(others) { return this._concat(others); }
1558
+
1559
+ // top(k, by): the first k rows of sort(by), e.g. top(10, '-revenue') for
1560
+ // the 10 largest, without sorting everything: rows are kept only if their
1561
+ // first key is at most a cutoff estimated from a sample (counted and
1562
+ // collected on all threads), and only those are sorted.
1563
+ top(k, by) { return this.engine._drive(this.#topG(k, by)); }
1564
+ topAsync(k, by) { return this.engine._driveAsync(this.#topG(k, by)); }
1565
+
1566
+ // Sort key values for every row of this (unfiltered) table, as JS arrays.
1567
+ *#keyArraysG(keys) {
1568
+ const { engine } = this;
1569
+ const out = [];
1570
+ for (const key of keys) {
1571
+ if (key.column) {
1572
+ const c = this.cols.get(key.column), rank = labelRanks(c.dict, key.desc), codes = engine.live(c.data);
1573
+ const out1 = new Float64Array(this.n);
1574
+ for (let i = 0; i < this.n; i++) out1[i] = rank[c.enc ? decodeOne(c.enc, codes[i]) : codes[i]];
1575
+ out.push(out1);
1576
+ continue;
1577
+ }
1578
+ const d = yield* this.#deriveG('__np_sortkey', key.text, {});
1579
+ try { out.push(Float64Array.from(engine.live(d.cols.get('__np_sortkey').data))); } finally { d.dispose(); }
1580
+ }
1581
+ return out;
1582
+ }
1583
+
1584
+ *#sortG(by) {
1585
+ const t = yield* this.#collectG();
1586
+ try {
1587
+ const keys = parseKeys(by, t.cols);
1588
+ if (this.engine.jit && t.n >= PARALLEL_SORT_ROWS) return yield* t.#sortParallelG(keys);
1589
+ const perm = sortIndex(yield* t.#keyArraysG(keys), t.n);
1590
+ return t.#gather(perm, perm.length);
1591
+ } finally {
1592
+ if (t !== this) t.dispose();
1593
+ }
1594
+ }
1595
+
1596
+ // Sort on every thread (see parallel-sort.mjs): keys as engine arrays of
1597
+ // the table's type (a plain number column is used as it is), then every
1598
+ // column gathered in the new order on every thread.
1599
+ *#sortParallelG(keys) {
1600
+ const { engine, n } = this;
1601
+ const t = this.type === Float32Array ? 'f32' : 'f64';
1602
+ const made = [], arrays = [];
1603
+ try {
1604
+ for (const key of keys) {
1605
+ const c = key.column ? null : this.cols.get(key.text.trim());
1606
+ if (c && !c.dict && !c.strings && !c.enc) { arrays.push(c.data); continue; } // a number column: as it is (packed: decoded below)
1607
+ const a = engine.alloc(this.type, n);
1608
+ made.push(a);
1609
+ arrays.push(a);
1610
+ if (key.column) { // text: rank of each label, A to Z (or Z to A)
1611
+ const col = this.cols.get(key.column), rank = labelRanks(col.dict, key.desc);
1612
+ const codes = engine.live(col.data), out = engine.live(a);
1613
+ for (let i = 0; i < n; i++) out[i] = rank[col.enc ? decodeOne(col.enc, codes[i]) : codes[i]];
1614
+ } else yield* engine.evalG(this.#bind(key.text, {}).src, this.#vars({}), a);
1615
+ }
1616
+ const perm = yield* sortPermG(engine, arrays, n, t);
1617
+ try {
1618
+ const out = new Map();
1619
+ for (const [name, c] of this.cols) out.set(name, { ...c, data: yield* gatherG(engine, c.data, perm, n, t, c.enc), zones: undefined, ...(c.enc ? { rows: n } : {}) }); // (packed stays packed)
1620
+ for (const c of out.values()) c.data = engine.live(c.data); // later allocations may have moved memory
1621
+ return new Table(engine, out, n, { owned: [...out.keys()] });
1622
+ } finally {
1623
+ engine.free(perm);
1624
+ }
1625
+ } finally {
1626
+ for (const a of made) engine.free(a);
1627
+ }
1628
+ }
1629
+
1630
+ // A new table with rows perm[0..count) of this one (columns in `omit` left out).
1631
+ #gather(perm, count, omit = null) {
1632
+ const { engine } = this;
1633
+ const out = new Map();
1634
+ for (const [name, c] of this.cols) {
1635
+ if (omit?.has(name)) continue;
1636
+ const data = engine.alloc(c.data.constructor, count + (c.enc ? 16 / c.data.BYTES_PER_ELEMENT : 0)); // (packed: codes copied as they are, room after the end)
1637
+ const src = engine.live(c.data);
1638
+ for (let i = 0; i < count; i++) data[i] = src[perm[i]];
1639
+ out.set(name, { ...c, data, zones: undefined, ...(c.enc ? { rows: count } : {}) }); // (new rows: no zone maps yet)
1640
+ }
1641
+ for (const c of out.values()) c.data = engine.live(c.data); // later allocations may have moved memory
1642
+ return new Table(engine, out, count, { owned: [...out.keys()] });
1643
+ }
1644
+
1645
+ *#topG(k, by) {
1646
+ const narrow = this.#narrow(); // sample and count only rows that can match the filter
1647
+ if (narrow !== this) return yield* narrow.#topG(k, by);
1648
+ const { engine } = this;
1649
+ k = Math.max(0, Math.floor(k));
1650
+ const keys = parseKeys(by, this.cols);
1651
+ const n = this.n;
1652
+ const sortAll = function* (t) {
1653
+ const sorted = yield* t.#sortG(by);
1654
+ if (k >= sorted.n) return sorted;
1655
+ const head = sorted.#gather(Uint32Array.from({ length: k }, (_, i) => i), k);
1656
+ sorted.dispose();
1657
+ return head;
1658
+ };
1659
+ // small tables, k close to n, or a text first key: just sort
1660
+ if (keys[0].column || n <= 50_000 || k * 8 >= n) return yield* sortAll(this);
1661
+
1662
+ // A cutoff from an evenly spaced sample: the first key is computed for the
1663
+ // sampled rows only. The full passes (count, then collect) are fused
1664
+ // filters "first key <= cutoff" on all threads; nothing else is stored.
1665
+ const S = Math.min(20_000, Math.max(1_024, Math.ceil(n / 64))), stride = Math.max(1, Math.floor(n / S));
1666
+ const picked = Uint32Array.from({ length: Math.ceil(n / stride) }, (_, i) => i * stride);
1667
+ // copy only the columns the first key reads (a table can be very wide)
1668
+ const used = new Set();
1669
+ walk(parse(keys[0].text), (m) => { if (m.t === 'var' && this.cols.has(m.name)) used.add(m.name); });
1670
+ const sampleTable = this.#gather(picked, picked.length, new Set(this.columnNames.filter((nm) => !used.has(nm))));
1671
+ let sample;
1672
+ try {
1673
+ const d = yield* sampleTable.#deriveG('__np_topkey', keys[0].text, {});
1674
+ try {
1675
+ sample = Float64Array.from(engine.live(d.cols.get('__np_topkey').data)).sort(); // native numeric sort; NaN goes last
1676
+ let m = sample.length;
1677
+ while (m > 0 && sample[m - 1] !== sample[m - 1]) m--;
1678
+ sample = sample.subarray(0, m);
1679
+ } finally { d.dispose(); }
1680
+ } finally {
1681
+ sampleTable.dispose();
1682
+ }
1683
+ const below = (cutoff) => this.filter(`(${keys[0].text}) <= __np_cutoff`, { __np_cutoff: cutoff });
1684
+ let q = Math.min(1, (3 * k) / n), cutoff = Infinity, count = 0;
1685
+ for (let tries = 0; tries < 8; tries++) {
1686
+ const at = Math.min(sample.length - 1, Math.ceil(q * sample.length) + 8);
1687
+ cutoff = q >= 1 || at < 0 ? Infinity : sample[at];
1688
+ ({ c: count } = yield* below(cutoff).#aggG({ c: 'count()' }, {}));
1689
+ if (count >= k || cutoff === Infinity) break;
1690
+ q = Math.min(1, q * 4);
1691
+ }
1692
+ if (count < k) return yield* sortAll(this); // fewer than k rows with a value: NaN rows are needed
1693
+ // every row that can be among the first k has a first key <= cutoff
1694
+ const candidates = yield* below(cutoff).#collectG();
1695
+ try {
1696
+ const perm = sortIndex(yield* candidates.#keyArraysG(keys), candidates.n);
1697
+ return candidates.#gather(perm, Math.min(k, perm.length));
1698
+ } finally {
1699
+ candidates.dispose();
1700
+ }
1701
+ }
1702
+
1703
+ // Plain JS objects (decodes text columns). Collects the filter first.
1704
+ // The rows (matching the filter) as plain columns: Float64Array (Float32Array
1705
+ // for float32 tables) per number column, { codes, labels } per category
1706
+ // column, string[] per free-text column. Copies, safe to keep.
1707
+ toColumns() { return this.engine._drive(this.#toColumnsG()); }
1708
+ toColumnsAsync() { return this.engine._driveAsync(this.#toColumnsG()); }
1709
+
1710
+ *#toColumnsG() {
1711
+ if (this.#hasPacked()) return yield* this.#viaPlainG((t) => t.#toColumnsG());
1712
+ const t = yield* this.#collectG();
1713
+ const out = {};
1714
+ for (const [name, c] of t.cols) {
1715
+ const data = this.engine.live(c.data).subarray(0, t.n);
1716
+ if (c.dict) { const codes = new Uint32Array(t.n); codes.set(data); out[name] = { codes, labels: c.dict.slice() }; }
1717
+ else if (c.strings) { const sbuf = this.engine.live(c.strings); out[name] = Array.from(data, (v) => decodeString(sbuf, v)); }
1718
+ else out[name] = data.slice();
1719
+ }
1720
+ if (t !== this) t.dispose();
1721
+ return out;
1722
+ }
1723
+
1724
+ // The rows (matching the filter) as Arrow IPC bytes: the stream format, or
1725
+ // with file: true the file format (.arrow / .feather for pandas, pyarrow,
1726
+ // Polars, DuckDB). Categories become dictionary-encoded strings.
1727
+ toArrow({ file = false } = {}) { return writeArrow(this.toColumns(), { file, dates: this.#dateNames() }); }
1728
+ async toArrowAsync({ file = false } = {}) { return writeArrow(await this.toColumnsAsync(), { file, dates: this.#dateNames() }); }
1729
+ #dateNames() { return [...this.cols].filter(([, c]) => c.date).map(([name]) => name); }
1730
+
1731
+ toRows(limit = Infinity) { return this.engine._drive(this.#toRowsG(limit)); }
1732
+ toRowsAsync(limit = Infinity) { return this.engine._driveAsync(this.#toRowsG(limit)); }
1733
+
1734
+ *#toRowsG(limit) {
1735
+ const t = yield* this.#collectG();
1736
+ const rows = [];
1737
+ const { engine } = this;
1738
+ const cols = [...t.cols].map(([name, c]) => [name, c, engine.live(c.data), c.strings ? engine.live(c.strings) : null]);
1739
+ const f32 = t.type === Float32Array;
1740
+ for (let i = 0; i < Math.min(t.n, limit); i++) {
1741
+ const row = {};
1742
+ for (const [name, c, data, sbuf] of cols) {
1743
+ const x = c.enc ? decodeOne(c.enc, data[i], f32) : data[i]; // (packed: decoded here, row by row)
1744
+ row[name] = c.dict ? c.dict[x] : sbuf ? decodeString(sbuf, x) : c.date ? (x === x ? new Date(x) : null) : x;
1745
+ }
1746
+ rows.push(row);
1747
+ }
1748
+ if (t !== this) t.dispose();
1749
+ return rows;
1750
+ }
1751
+
1752
+ // A lazy query over this table (see plan.mjs): steps are recorded,
1753
+ // optimized together and run by collect() / toRows() / agg() / count().
1754
+ lazy() { return new Query({ op: 'scan', table: this }); }
1755
+
1756
+ // The first n rows (matching the filter) as a new table.
1757
+ head(n) { return this.engine._drive(this.#headG(n)); }
1758
+ headAsync(n) { return this.engine._driveAsync(this.#headG(n)); }
1759
+ *#headG(n) {
1760
+ const t = yield* this.#collectG();
1761
+ if (n >= t.n) return t;
1762
+ const out = t.#gather(Uint32Array.from({ length: Math.max(0, n) }, (_, i) => i), Math.max(0, n));
1763
+ if (t !== this) t.dispose();
1764
+ return out;
1765
+ }
1766
+
1767
+ // Rows [start, end) (matching the filter) as a new table.
1768
+ slice(start, end = Infinity) { return this.engine._drive(this.#sliceG(start, end)); }
1769
+ sliceAsync(start, end = Infinity) { return this.engine._driveAsync(this.#sliceG(start, end)); }
1770
+ *#sliceG(start, end) {
1771
+ const t = yield* this.#collectG();
1772
+ const a = Math.max(0, Math.min(t.n, Math.floor(start))), b = Math.max(a, Math.min(t.n, Math.floor(end)));
1773
+ const out = t.#gather(Uint32Array.from({ length: b - a }, (_, i) => a + i), b - a);
1774
+ if (t !== this) t.dispose();
1775
+ return out;
1776
+ }
1777
+
1778
+ // Hash join: orders.join(customers, { on: 'customer_id', how: 'inner' | 'left' }).
1779
+ // on: a column both tables have, several (['region', 'day']), or
1780
+ // { left: 'cust', right: 'id' }. Put the smaller table on the right (it is
1781
+ // the one hashed). Rows come in left-table order, each with its matches in
1782
+ // right-table order; right columns named like a left column get `suffix`
1783
+ // (default '_right'). See join.mjs.
1784
+ join(other, opts) { return this.engine._drive(this.#joinOuterG(other, opts)); }
1785
+ joinAsync(other, opts) { return this.engine._driveAsync(this.#joinOuterG(other, opts)); }
1786
+
1787
+ // (SQL RIGHT / FULL JOIN) opts.unmatched: an object the join puts the right
1788
+ // rows no left row matched in, as a table (unmatched.table), from the
1789
+ // probe's own matches.
1790
+ *#joinOuterG(other, opts) {
1791
+ const u = opts?.unmatched;
1792
+ if (u) u.result = null;
1793
+ const out = yield* this.#joinG(other, opts);
1794
+ if (u) {
1795
+ if (!u.result) { out?.dispose?.(); throw new Error('vectro: join: no unmatched rows were made'); }
1796
+ const { cols, n } = u.result;
1797
+ u.result = null;
1798
+ u.table = new Table(this.engine, cols, n, { owned: [...cols.keys()] });
1799
+ }
1800
+ return out;
1801
+ }
1802
+
1803
+ *#joinG(other, opts) {
1804
+ if (opts?.view && other instanceof Table && !other.pred && !other.#hasPacked()) { // (datasets: unique right keys as a view, see joinViewG)
1805
+ let collected = null, view = null;
1806
+ try {
1807
+ view = yield* joinViewG(this, other, opts, function* () { collected = yield* this.#collectG(); return collected; }.bind(this), (cols, n, pred, left, made) => {
1808
+ const owned = [...made];
1809
+ const ownedBuffers = [];
1810
+ if (left !== this) { owned.push(...left.owned); ownedBuffers.push(...left.ownedBuffers); left.owned = []; left.ownedBuffers = []; } // (the view keeps the collected rows)
1811
+ return new Table(this.engine, cols, n, { pred, owned, ownedBuffers, zones: false });
1812
+ });
1813
+ } finally {
1814
+ if (!view && collected && collected !== this) collected.dispose();
1815
+ }
1816
+ if (view) return view;
1817
+ }
1818
+ if (this.#hasPacked()) { // (decoded first; a view join keeps the decoded columns it shows)
1819
+ const t = yield* this.#decodedG();
1820
+ let out;
1821
+ try {
1822
+ out = yield* t.#joinG(other, opts);
1823
+ const at = (c) => c?.data && this.engine.live(c.data).byteOffset;
1824
+ const kept = t.owned.filter((nm) => at(out.cols.get(nm)) === at(t.cols.get(nm)));
1825
+ if (kept.length) { out.owned.push(...kept); t.owned = t.owned.filter((nm) => !kept.includes(nm)); }
1826
+ return out;
1827
+ } finally { if (out !== t) t.dispose(); }
1828
+ }
1829
+ if (other instanceof Table && other.#hasPacked()) { const o = yield* other.#decodedG(); try { return yield* this.#joinG(o, opts); } finally { o.dispose(); } }
1830
+ if (!(other instanceof Table)) throw new TypeError('vectro: join needs another table of the same engine');
1831
+ const l = yield* this.#collectG(), r = yield* other.#collectG();
1832
+ try {
1833
+ return yield* joinTablesG(l, r, opts, (cols, n) => new Table(this.engine, cols, n, { owned: [...cols.keys()] }));
1834
+ } finally {
1835
+ if (l !== this) l.dispose();
1836
+ if (r !== other) r.dispose();
1837
+ }
1838
+ }
1839
+
1840
+ // Free the columns this table allocated (shared columns stay alive).
1841
+ dispose() {
1842
+ for (const name of this.owned) {
1843
+ const data = this.cols.get(name)?.data;
1844
+ if (data) this.engine.free(data);
1845
+ }
1846
+ for (const b of this.ownedBuffers) this.engine.free(b);
1847
+ this.owned = [];
1848
+ this.ownedBuffers = [];
1849
+ }
1850
+ }
1851
+
1852
+ // ---- helpers ---------------------------------------------------------------------
1853
+
1854
+ const RESERVED = new Set(['Infinity', 'NaN']); // (function names are fine: sum(...) is a call, sum a column)
1855
+ function checkName(name) {
1856
+ if (!IDENT.test(name) || RESERVED.has(name)) throw new Error(`vectro: "${name}" can't be a column name (use letters, digits and _)`);
1857
+ }
1858
+
1859
+ // Missing values (NaN) in aggregations, as in SQL: sum and mean skip them
1860
+ // (mean divides by the values present), min and max skip them, count(x)
1861
+ // counts the values present (count(condition) still counts where it holds).
1862
+ // arg is the bound source; returns the aggregation item.
1863
+ // (raw: the argument as given; where it is a column with no missing values,
1864
+ // the group-by uses it as it is and takes counts from the row count)
1865
+ function missingRules(fn, arg) {
1866
+ const present = `(${arg}) == (${arg})`;
1867
+ if (fn === 'count') return isMask(parse(arg)) ? { fn, arg } : { fn, arg: present, raw: arg };
1868
+ if (fn === 'min') return { fn, arg: `(${present} ? (${arg}) : Infinity)`, present, raw: arg }; // (present: NaN when nothing is)
1869
+ if (fn === 'max') return { fn, arg: `(${present} ? (${arg}) : -Infinity)`, present, raw: arg };
1870
+ return fn === 'mean' ? { fn, arg: `(${present} ? (${arg}) : 0)`, present, raw: arg } : { fn, arg: `(${present} ? (${arg}) : 0)`, raw: arg }; // sum, mean
1871
+ }
1872
+
1873
+ // a category column given as codes into a labels array: { codes, labels }
1874
+ export const isCoded = (values) => values != null && !ArrayBuffer.isView(values) && !Array.isArray(values) && values.codes != null && Array.isArray(values.labels);
1875
+
1876
+ // Codes + labels as a category column. Repeated labels share one code.
1877
+ function codedText(engine, type, { codes: src, labels }) {
1878
+ const data = engine.alloc(type, src.length);
1879
+ const codes = new Map(), dict = [], lut = new Float64Array(labels.length);
1880
+ labels.forEach((s, j) => {
1881
+ let c = codes.get(s);
1882
+ if (c === undefined) { c = dict.length; dict.push(s); codes.set(s, c); }
1883
+ lut[j] = c;
1884
+ });
1885
+ for (let i = 0; i < src.length; i++) {
1886
+ const c = src[i];
1887
+ if (!(c >= 0 && c < labels.length)) throw new RangeError(`vectro: code ${c} at row ${i} has no label (${labels.length} labels)`);
1888
+ data[i] = lut[c];
1889
+ }
1890
+ return { data, dict, codes };
1891
+ }
1892
+
1893
+ const isDates = (values) => Array.isArray(values) && values.find((v) => v != null) instanceof Date;
1894
+ const isText = (values) => !ArrayBuffer.isView(values) && values.length > 0 && typeof values[0] === 'string';
1895
+
1896
+ function copyNumbers(engine, type, values) {
1897
+ const data = engine.alloc(type, values.length);
1898
+ if (Array.isArray(values)) for (let i = 0; i < values.length; i++) { const v = values[i]; data[i] = v === null || v === undefined ? NaN : v; } // (null: missing, as in JSON)
1899
+ else data.set(values);
1900
+ return data;
1901
+ }
1902
+
1903
+ function encodeText(engine, type, values) {
1904
+ const data = engine.alloc(type, values.length);
1905
+ const codes = new Map(), dict = [];
1906
+ for (let i = 0; i < values.length; i++) {
1907
+ const s = values[i];
1908
+ let c = codes.get(s);
1909
+ if (c === undefined) { c = dict.length; dict.push(s); codes.set(s, c); }
1910
+ data[i] = c;
1911
+ }
1912
+ return { data, dict, codes };
1913
+ }
1914
+
1915
+ // Text functions, run once per distinct label of a category column, not per
1916
+ // row: the matching codes become a few range checks the fused loop can run.
1917
+ const TEXT_FNS = {
1918
+ startsWith: (label, [p]) => label.startsWith(p),
1919
+ endsWith: (label, [p]) => label.endsWith(p),
1920
+ contains: (label, [p]) => label.includes(p),
1921
+ in: (label, list) => list.includes(label),
1922
+ like: (label, [pattern], re) => re.test(label),
1923
+ ilike: (label, [pattern], re) => re.test(label), // (the pattern ignoring case)
1924
+ };
1925
+ const MAX_RANGES = 256;
1926
+
1927
+ // startsWith(region, 'E') -> (region >= 0 && region <= 1) || region == 5
1928
+ function textCall(n, cols, src) {
1929
+ const [colNode, ...rest] = n.args;
1930
+ const c = colNode?.t === 'var' ? cols.get(colNode.name) : null;
1931
+ if (c?.strings) throw new Error(`vectro: "${colNode.name}" is a free-text column; ${n.fn}() needs a category column (load it as types: { ${colNode.name}: 'category' })`);
1932
+ if (!c?.dict) throw new SyntaxError(`vectro: ${n.fn}() needs a text column first, then text in quotes, in "${src}"`);
1933
+ if (!rest.length || rest.some((a) => a.t !== 'str')) throw new SyntaxError(`vectro: ${n.fn}(${colNode.name}, …) needs text in quotes after the column, in "${src}"`);
1934
+ if (n.fn !== 'in' && rest.length !== 1) throw new SyntaxError(`vectro: ${n.fn}() takes a column and one text, in "${src}"`);
1935
+ const args = rest.map((a) => a.v);
1936
+ const re = n.fn === 'like' || n.fn === 'ilike' ? new RegExp(`^${args[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '[\\s\\S]*').replace(/_/g, '[\\s\\S]')}$`, n.fn === 'ilike' ? 'iu' : '') : null;
1937
+ const match = c.dict.map((label) => TEXT_FNS[n.fn](label, args, re));
1938
+ // runs of consecutive codes that match (or, if shorter, that don't)
1939
+ const runs = (want) => {
1940
+ const out = [];
1941
+ for (let i = 0; i < match.length; i++) {
1942
+ if (match[i] !== want) continue;
1943
+ let j = i;
1944
+ while (j + 1 < match.length && match[j + 1] === want) j++;
1945
+ out.push([i, j]);
1946
+ i = j;
1947
+ }
1948
+ return out;
1949
+ };
1950
+ const yes = runs(true), no = runs(false);
1951
+ const col = { t: 'var', name: colNode.name };
1952
+ const num = (v) => ({ t: 'num', v });
1953
+ const anyOf = (list) => list.map(([a, b]) => (a === b ? { t: 'bin', op: '==', l: col, r: num(a) } : { t: 'bin', op: '&&', l: { t: 'bin', op: '>=', l: col, r: num(a) }, r: { t: 'bin', op: '<=', l: col, r: num(b) } }))
1954
+ .reduce((x, y) => ({ t: 'bin', op: '||', l: x, r: y }));
1955
+ if (!yes.length) return { t: 'bin', op: '==', l: col, r: num(-1) }; // nothing matches
1956
+ if (!no.length) return { t: 'bin', op: '==', l: col, r: col }; // everything matches
1957
+ if (Math.min(yes.length, no.length) > MAX_RANGES) throw new Error(`vectro: ${n.fn}() matches ${match.filter(Boolean).length} labels of "${colNode.name}" spread over too many runs; group by it first or filter differently`);
1958
+ return yes.length <= no.length ? anyOf(yes) : { t: 'un', op: '!', x: anyOf(no) };
1959
+ }
1960
+
1961
+ // `region == 'EU'` -> `region == 3` (the code for 'EU', or -1 if absent).
1962
+ export function rewriteText(ast, cols, src) {
1963
+ const textCol = (n) => n.t === 'var' && cols.get(n.name)?.dict;
1964
+ const dateCol = (n) => n.t === 'var' && cols.get(n.name)?.date;
1965
+ const one = (n) => { if (n.args.length !== 1) throw new SyntaxError(`vectro: ${n.fn}() takes one value, in "${src}"`); return fix(n.args[0]); };
1966
+ const fix = (n) => {
1967
+ if (n.t === 'call' && TEXT_FNS[n.fn]) return textCall(n, cols, src);
1968
+ if (n.t === 'call' && n.fn === 'date') { // date('2024-03-01'): the time in ms
1969
+ const v = n.args.length === 1 && n.args[0].t === 'str' ? parseDateText(n.args[0].v) : NaN;
1970
+ if (Number.isNaN(v)) throw new SyntaxError(`vectro: date() needs ISO text such as date('2024-03-01') or date('2024-03-01T12:00:00Z'), in "${src}"`);
1971
+ return { t: 'num', v };
1972
+ }
1973
+ if (n.t === 'call' && DATE_FNS[n.fn]) return DATE_FNS[n.fn](one(n));
1974
+ if (n.t === 'call' && n.fn === 'isMissing') { const x = one(n); return { t: 'bin', op: '!=', l: x, r: x }; }
1975
+ if (n.t === 'call' && n.fn === 'coalesce') { // first value that isn't missing
1976
+ if (n.args.length < 2) throw new SyntaxError(`vectro: coalesce() needs two values or more, in "${src}"`);
1977
+ return n.args.map(fix).reduceRight((rest, x) => ({ t: 'cond', c: { t: 'bin', op: '==', l: x, r: x }, x, y: rest }));
1978
+ }
1979
+ if (n.t === 'bin' && (n.l.t === 'str' || n.r.t === 'str') && (dateCol(n.l) || dateCol(n.r))) { // ts >= '2024-03-01'
1980
+ const [colNode, strNode] = n.l.t === 'str' ? [n.r, n.l] : [n.l, n.r];
1981
+ const v = parseDateText(strNode.v);
1982
+ if (Number.isNaN(v)) throw new SyntaxError(`vectro: ${JSON.stringify(strNode.v)} isn't a date (use ISO text such as '2024-03-01' or '2024-03-01T12:00:00Z'), in "${src}"`);
1983
+ const lit = { t: 'num', v };
1984
+ return n.l.t === 'str' ? { ...n, l: lit, r: colNode } : { ...n, l: colNode, r: lit };
1985
+ }
1986
+ if (n.t === 'bin') {
1987
+ if ((n.op === '==' || n.op === '!=') && (n.l.t === 'str' || n.r.t === 'str')) {
1988
+ const [colNode, strNode] = n.l.t === 'str' ? [n.r, n.l] : [n.l, n.r];
1989
+ if (colNode.t === 'var' && cols.get(colNode.name)?.strings) {
1990
+ throw new Error(`vectro: "${colNode.name}" is a free-text column; it can be returned but not used in expressions yet (load it as types: { ${colNode.name}: 'category' } to filter or group on it)`);
1991
+ }
1992
+ if (!textCol(colNode)) throw new SyntaxError(`vectro: ${JSON.stringify(strNode.v)} can only be compared with a text column, in "${src}"`);
1993
+ const code = cols.get(colNode.name).codes.get(strNode.v) ?? -1;
1994
+ return { ...n, l: colNode, r: { t: 'num', v: code } };
1995
+ }
1996
+ return { ...n, l: fix(n.l), r: fix(n.r) };
1997
+ }
1998
+ if (n.t === 'un') return { ...n, x: fix(n.x) };
1999
+ if (n.t === 'call') return { ...n, args: n.args.map(fix) };
2000
+ if (n.t === 'cond') return { ...n, c: fix(n.c), x: fix(n.x), y: fix(n.y) };
2001
+ return n;
2002
+ };
2003
+ return fix(ast);
2004
+ }
2005
+
2006
+ // Percentiles of each partition (items: { fn: 'percentile_cont' |
2007
+ // 'percentile_disc', p }): x the values (NaN skipped), g each row's partition
2008
+ // 0..K-1 (null: one). Returns one Float64Array(K) per item. With few
2009
+ // partitions, a radix pick: each value's top 16 bits in sortable order are
2010
+ // counted, then only the values in the buckets holding the wanted ranks are
2011
+ // gathered and selected among (two passes over x, in order). With many
2012
+ // (small) partitions, each one's values are gathered and selected among.
2013
+ function percentiles(x, g, K, items) {
2014
+ const n = x.length, radix = K <= 64 && n >= 4096, B = 65536;
2015
+ const sizes = new Int32Array(K);
2016
+ let hist = null, hi = null;
2017
+ if (radix) {
2018
+ hist = new Int32Array(K * B);
2019
+ hi = new Int32Array(x.buffer, x.byteOffset, n * 2); // (signed: stays a small integer in V8)
2020
+ if (g) { for (let i = 0; i < n; i++) { const v = x[i]; if (v === v) { const h = hi[2 * i + 1]; hist[g[i] * B + (h < 0 ? ~h >>> 16 : (h >>> 16) | 0x8000)]++; } } } // (negatives reversed)
2021
+ else for (let i = 0; i < n; i++) { const v = x[i]; if (v === v) { const h = hi[2 * i + 1]; hist[h < 0 ? ~h >>> 16 : (h >>> 16) | 0x8000]++; } }
2022
+ for (let k = 0; k < K; k++) { let m = 0; for (let b = k * B, e = b + B; b < e; b++) m += hist[b]; sizes[k] = m; }
2023
+ } else if (g) { for (let i = 0; i < n; i++) if (x[i] === x[i]) sizes[g[i]]++; } else for (let i = 0; i < n; i++) if (x[i] === x[i]) sizes[0]++;
2024
+ const { plan, wants } = percentileRanks(sizes, items);
2025
+ const got = new Float64Array(wants.length);
2026
+ if (!radix) {
2027
+ const start = new Int32Array(K + 1);
2028
+ for (let k = 0; k < K; k++) start[k + 1] = start[k] + sizes[k];
2029
+ const at = start.slice(0, K), buf = new Float64Array(start[K]);
2030
+ for (let i = 0; i < n; i++) { const v = x[i]; if (v === v) buf[at[g ? g[i] : 0]++] = v; }
2031
+ wants.forEach(([k, r], j) => { got[j] = select(buf, start[k], start[k + 1], start[k] + r); });
2032
+ } else if (wants.length) {
2033
+ // each wanted rank's bucket, and its rank within the bucket
2034
+ const slotOf = new Map(), place = [];
2035
+ for (const [k, r] of wants) {
2036
+ let b = 0, below = 0;
2037
+ const h0 = k * B;
2038
+ while (below + hist[h0 + b] <= r) below += hist[h0 + b++];
2039
+ if (!slotOf.has(h0 + b)) slotOf.set(h0 + b, slotOf.size);
2040
+ place.push([slotOf.get(h0 + b), r - below]);
2041
+ }
2042
+ const slot = new Int32Array(K * B).fill(-1);
2043
+ const bufs = [...slotOf.keys()].map((key, j) => { slot[key] = j; return new Float64Array(hist[key]); }), fill = new Int32Array(bufs.length);
2044
+ for (let i = 0; i < n; i++) {
2045
+ const v = x[i];
2046
+ if (v !== v) continue;
2047
+ const h = hi[2 * i + 1], j = slot[(g ? g[i] * B : 0) + (h < 0 ? ~h >>> 16 : (h >>> 16) | 0x8000)];
2048
+ if (j >= 0) bufs[j][fill[j]++] = v;
2049
+ }
2050
+ place.forEach(([j, r], w) => { got[w] = select(bufs[j], 0, bufs[j].length, r); });
2051
+ }
2052
+ return percentileValues(plan, got);
2053
+ }
2054
+
2055
+ // The ranks each item needs in each partition of sizes[k] values present
2056
+ // (0 = the smallest): wants = [[partition, rank]...], plan[item][partition] =
2057
+ // { at (index into wants), frac (between it and the next) } or null.
2058
+ function percentileRanks(sizes, items) {
2059
+ const wants = [];
2060
+ const plan = items.map((it) => Array.from(sizes, (m, k) => {
2061
+ if (!m) return null;
2062
+ if (it.fn === 'percentile_disc') { wants.push([k, Math.max(0, Math.ceil(it.p * m) - 1)]); return { at: wants.length - 1 }; }
2063
+ const r = it.p * (m - 1), lo = Math.floor(r);
2064
+ wants.push([k, lo]);
2065
+ if (lo + 1 < m && r > lo) { wants.push([k, lo + 1]); return { at: wants.length - 2, frac: r - lo }; }
2066
+ return { at: wants.length - 1 };
2067
+ }));
2068
+ return { plan, wants };
2069
+ }
2070
+ // Each item's value per partition, from the values found for the wanted ranks.
2071
+ function percentileValues(plan, got) {
2072
+ return plan.map((ws) => Float64Array.from(ws, (w) => (!w ? NaN : w.frac ? got[w.at] + w.frac * (got[w.at + 1] - got[w.at]) : got[w.at])));
2073
+ }
2074
+ // Key k of each kept group: from keys stored side by side (kv[g * nk + k]),
2075
+ // or from a direct group number (g / below % size steps from the offset).
2076
+ function stridedKeys(kv, keep, nk, k) { const n = keep.length, out = new Float64Array(n); for (let r = 0; r < n; r++) out[r] = kv[keep[r] * nk + k]; return out; }
2077
+ function directKeys(keep, below, size, step, offset) { const n = keep.length, out = new Float64Array(n); for (let r = 0; r < n; r++) out[r] = (Math.floor(keep[r] / below) % size) * step + offset; return out; }
2078
+ // Group numbers 0..K-1 with rows (counts[g] > 0), in order.
2079
+ function keptGroups(counts, K) {
2080
+ let m = 0;
2081
+ for (let g = 0; g < K; g++) if (counts[g] > 0) m++;
2082
+ const keep = new Uint32Array(m);
2083
+ for (let g = 0, r = 0; g < K; g++) if (counts[g] > 0) keep[r++] = g;
2084
+ return keep;
2085
+ }
2086
+ function gatherF64(src, idx) { const n = idx.length, out = new Float64Array(n); for (let r = 0; r < n; r++) out[r] = src[idx[r]]; return out; }
2087
+ function gatherU32(src, idx) { const n = idx.length, out = new Uint32Array(n); for (let r = 0; r < n; r++) out[r] = src[idx[r]]; return out; }
2088
+
2089
+ // The order of m groups by their keys (engine arrays, one per key, first
2090
+ // most important; NaN last) on every thread, as an engine Uint32Array:
2091
+ // whole-number keys (codes, IDs) combined into one key when their ranges
2092
+ // allow (fewer passes).
2093
+ function* groupPermG(engine, keyArrs, m) {
2094
+ let sortKeys = keyArrs, one = null;
2095
+ if (keyArrs.length > 1) {
2096
+ const vars = Object.fromEntries(keyArrs.map((a, k) => [`k${k}`, a]));
2097
+ let span = 1, expr = null;
2098
+ for (let k = 0; k < keyArrs.length && span <= 2 ** 53; k++) {
2099
+ const [lo, hi, frac] = yield* engine.evalG(['min(k)', 'max(k)', 'count(k != floor(k))'], { k: keyArrs[k] });
2100
+ if (frac > 0 || !Number.isFinite(lo) || !Number.isFinite(hi)) { span = Infinity; break; }
2101
+ vars[`lo${k}`] = lo;
2102
+ span *= hi - lo + 1;
2103
+ expr = expr === null ? `(k${k} - lo${k})` : `(${expr}) * ${hi - lo + 1} + (k${k} - lo${k})`;
2104
+ }
2105
+ if (span <= 2 ** 53) { // (exact: every combination below 2^53)
2106
+ one = engine.alloc(Float64Array, m);
2107
+ yield* engine.evalG(expr, vars, one);
2108
+ sortKeys = [one];
2109
+ }
2110
+ }
2111
+ try { return yield* sortPermG(engine, sortKeys, m, 'f64'); } finally { if (one) engine.free(one); }
2112
+ }
2113
+ function ratioOf(v, p) { const n = v.length, out = new Float64Array(n); for (let g = 0; g < n; g++) out[g] = v[g] / p[g]; return out; } // (means)
2114
+ function presentOnly(v, p) { const n = v.length, out = new Float64Array(n); for (let g = 0; g < n; g++) out[g] = p[g] ? v[g] : NaN; return out; } // (mins, maxes: NaN where no value)
2115
+
2116
+ // How often each key (tuple) shows up among m rows taken every `step` rows:
2117
+ // keys as one 32-bit hash of their bits (-0 as 0); for an estimate only.
2118
+ function sampleCounts(cols, f32, m, step) {
2119
+ const W = f32 ? 1 : 2, words = cols.map((c) => new Int32Array(c.buffer, c.byteOffset, c.length * W)), counts = new Map();
2120
+ for (let j = 0; j < m; j++) {
2121
+ const i = Math.floor(j * step);
2122
+ let h = 0x811c9dc5;
2123
+ for (let k = 0; k < cols.length; k++) {
2124
+ if (cols[k][i] === 0) { h = Math.imul(h, 0x01000193); continue; }
2125
+ h = Math.imul(h ^ words[k][i * W], 0x01000193);
2126
+ if (W === 2) h = Math.imul(h ^ words[k][i * W + 1], 0x01000193);
2127
+ }
2128
+ counts.set(h, (counts.get(h) ?? 0) + 1);
2129
+ }
2130
+ return counts;
2131
+ }
2132
+
2133
+ // The greatest common divisor of (v - lo) over up to 4096 values spread
2134
+ // over the column (NaN skipped): the real step divides it. 1: no step.
2135
+ function sampleStep(d, n, lo) {
2136
+ const m = Math.min(n, 4096), at = n / m;
2137
+ let g = 0;
2138
+ for (let j = 0; j < m && g !== 1; j++) {
2139
+ const v = d[Math.floor(j * at)];
2140
+ if (v !== v) continue;
2141
+ let a = v - lo, b = g;
2142
+ while (b) { const r = a % b; a = b; b = r; }
2143
+ g = a;
2144
+ }
2145
+ return g > 1 ? g : 1;
2146
+ }
2147
+
2148
+ // The marks (0 or 1 bytes) in each group's `width` bytes, 4 at a time.
2149
+ function countMarks(map, K, width) {
2150
+ const w = new Uint32Array(map.buffer, map.byteOffset, (K * width) >> 2), per = width >> 2, out = new Float64Array(K);
2151
+ for (let g = 0, i = 0; g < K; g++) {
2152
+ let c = 0;
2153
+ for (const e = i + per; i < e; i++) c += Math.imul(w[i], 0x01010101) >>> 24;
2154
+ out[g] = c;
2155
+ }
2156
+ return out;
2157
+ }
2158
+
2159
+ // A column's values as numbers sorting ascending in its order (NaN last):
2160
+ // text by its labels' order, descending keys negated.
2161
+ function firstKey(engine, c, desc, n, f32) {
2162
+ const d = engine.live(c.data), key = new Float64Array(n);
2163
+ if (c.dict) { const rank = labelRanks(c.dict, desc); for (let i = 0; i < n; i++) key[i] = rank[c.enc ? decodeOne(c.enc, d[i]) : d[i]]; }
2164
+ else if (c.enc) for (let i = 0; i < n; i++) key[i] = (desc ? -1 : 1) * decodeOne(c.enc, d[i], f32);
2165
+ else if (desc) for (let i = 0; i < n; i++) key[i] = -d[i]; else key.set(d.subarray(0, n));
2166
+ return key;
2167
+ }
2168
+
2169
+ // firstRows (below) on every thread (see rank-kernels.mjs): key and g are
2170
+ // float64 engine arrays; neg: keys are negated as read.
2171
+ function* firstRowsParallelG(engine, key, neg, g, n, K, k) {
2172
+ engine._tableKernels ??= new Map();
2173
+ const kernel = (name, make) => {
2174
+ let v = engine._tableKernels.get(name);
2175
+ if (!v) { v = engine._register(make(engine.isShared)); engine._tableKernels.set(name, v); }
2176
+ return v;
2177
+ };
2178
+ const top = kernel('firstrows-top', topModule), mark = kernel('firstrows-mark', markModule);
2179
+ const slots = engine.alloc(Float64Array, K * (k + 1)), mask = engine.alloc(Uint8Array, n);
2180
+ try {
2181
+ engine.live(slots).fill(0);
2182
+ yield* engine._groupRangeG(top, n, [key, g], [k, neg ? 1 : 0], engine.live(slots).byteOffset, K);
2183
+ yield* engine._groupRangeG(mark, n, [key, g, mask], [k, neg ? 1 : 0], engine.live(slots).byteOffset, K);
2184
+ return maskedRows(engine.live(mask), n);
2185
+ } finally {
2186
+ engine.free(slots);
2187
+ engine.free(mask);
2188
+ }
2189
+ }
2190
+ function maskedRows(m, n) {
2191
+ let c = 0;
2192
+ for (let i = 0; i < n; i++) c += m[i];
2193
+ const out = new Uint32Array(c);
2194
+ for (let i = 0, j = 0; i < n; i++) if (m[i]) out[j++] = i;
2195
+ return out;
2196
+ }
2197
+
2198
+ // Row numbers (in order) whose key is at most the k-th smallest key of their
2199
+ // partition g[i] (0..K-1; g null: one partition), NaN sorting last; every row
2200
+ // of a partition with fewer than k rows. Each partition keeps its k smallest
2201
+ // keys seen so far, sorted, so most rows cost one comparison.
2202
+ function firstRows(key, g, n, K, k) {
2203
+ const top = new Float64Array(K * k), cnt = new Int32Array(K);
2204
+ for (let i = 0; i < n; i++) {
2205
+ const v = key[i], p = g === null ? 0 : g[i], c = cnt[p], base = p * k;
2206
+ let j;
2207
+ if (c < k) { j = base + c; cnt[p] = c + 1; } else {
2208
+ const last = top[base + k - 1];
2209
+ if (last !== last ? v !== v : v !== v || v >= last) continue; // not smaller than the k-th
2210
+ j = base + k - 1;
2211
+ }
2212
+ for (; j > base; j--) { const a = top[j - 1]; if (a !== a ? v !== v : v !== v || v >= a) break; top[j] = a; } // insert, keeping order
2213
+ top[j] = v;
2214
+ }
2215
+ let m = 0;
2216
+ for (let pass = 0, out = null; pass < 2; pass++) {
2217
+ for (let i = 0; i < n; i++) {
2218
+ const v = key[i], p = g === null ? 0 : g[i], last = top[p * k + k - 1];
2219
+ if (cnt[p] < k || last !== last || (v === v && v <= last)) { if (out) out[m] = i; m++; }
2220
+ }
2221
+ if (!pass) { out = new Uint32Array(m); m = 0; } else return out;
2222
+ }
2223
+ }
2224
+ function toInt32(a, n) { const out = new Int32Array(n); for (let i = 0; i < n; i++) out[i] = a[i]; return out; }
2225
+ function spread(d, per, g, n) { for (let i = 0; i < n; i++) d[i] = per[g[i]]; }
2226
+
2227
+ // The k-th smallest of a[lo..hi) (quickselect, in place: afterwards a[k] is
2228
+ // it, smaller values before it, larger after).
2229
+ function select(a, lo, hi, k) {
2230
+ let l = lo, r = hi - 1;
2231
+ while (r > l) {
2232
+ if (r - l < 16) { for (let i = l + 1; i <= r; i++) { const v = a[i]; let j = i - 1; while (j >= l && a[j] > v) { a[j + 1] = a[j]; j--; } a[j + 1] = v; } break; }
2233
+ const m = (l + r) >> 1; // median of three as the pivot
2234
+ if (a[m] < a[l]) { const x = a[m]; a[m] = a[l]; a[l] = x; }
2235
+ if (a[r] < a[l]) { const x = a[r]; a[r] = a[l]; a[l] = x; }
2236
+ if (a[r] < a[m]) { const x = a[r]; a[r] = a[m]; a[m] = x; }
2237
+ const pivot = a[m];
2238
+ let i = l, j = r;
2239
+ while (i <= j) {
2240
+ while (a[i] < pivot) i++;
2241
+ while (a[j] > pivot) j--;
2242
+ if (i <= j) { const x = a[i]; a[i] = a[j]; a[j] = x; i++; j--; }
2243
+ }
2244
+ if (k <= j) r = j; else if (k >= i) l = i; else break;
2245
+ }
2246
+ return a[k];
2247
+ }