databonk 0.0.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +491 -96
  2. package/dist/dataframe-BggBYXYm.d.cts +886 -0
  3. package/dist/dataframe-BggBYXYm.d.ts +886 -0
  4. package/dist/index.cjs +5 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +366 -0
  7. package/dist/index.d.ts +354 -31
  8. package/dist/index.js +4 -40
  9. package/dist/index.js.map +1 -1
  10. package/dist/parallel-fv5h4BkA.d.cts +152 -0
  11. package/dist/parallel-fv5h4BkA.d.ts +152 -0
  12. package/dist/parquet.cjs +3 -0
  13. package/dist/parquet.cjs.map +1 -0
  14. package/dist/parquet.d.cts +57 -0
  15. package/dist/parquet.d.ts +57 -0
  16. package/dist/parquet.js +3 -0
  17. package/dist/parquet.js.map +1 -0
  18. package/dist/scalar.wasm +0 -0
  19. package/dist/simd-threads.wasm +0 -0
  20. package/dist/simd.wasm +0 -0
  21. package/dist/workers.cjs +102 -0
  22. package/dist/workers.cjs.map +1 -0
  23. package/dist/workers.d.cts +74 -0
  24. package/dist/workers.d.ts +74 -0
  25. package/dist/workers.js +102 -0
  26. package/dist/workers.js.map +1 -0
  27. package/package.json +93 -32
  28. package/build/release.d.ts +0 -719
  29. package/build/release.js +0 -774
  30. package/build/release.wasm +0 -0
  31. package/build/release.wasm.map +0 -1
  32. package/build/release.wat +0 -22633
  33. package/dist/dataframe.d.ts +0 -82
  34. package/dist/dataframe.d.ts.map +0 -1
  35. package/dist/dataframe.js +0 -318
  36. package/dist/dataframe.js.map +0 -1
  37. package/dist/index.d.ts.map +0 -1
  38. package/dist/loader.d.ts +0 -86
  39. package/dist/loader.d.ts.map +0 -1
  40. package/dist/loader.js +0 -147
  41. package/dist/loader.js.map +0 -1
  42. package/dist/shared-memory.d.ts +0 -64
  43. package/dist/shared-memory.d.ts.map +0 -1
  44. package/dist/shared-memory.js +0 -113
  45. package/dist/shared-memory.js.map +0 -1
package/README.md CHANGED
@@ -1,166 +1,561 @@
1
- # Databonk
1
+ # databonk
2
2
 
3
- **WASM-powered DataFrame library with SIMD acceleration**
3
+ Columnar dataframe library for JavaScript — pandas-familiar API, WebAssembly-accelerated kernels, zero-copy typed-array views, dual ESM/CJS, TypeScript types.
4
4
 
5
- Databonk is a high-performance columnar DataFrame library built with AssemblyScript and WebAssembly, featuring SIMD-optimized operations and optional SharedArrayBuffer support for zero-copy data access.
5
+ ```
6
+ npm install databonk
7
+ ```
6
8
 
7
- ## Key Features
9
+ Works in **Node.js ≥ 18**, **Vite**, and **webpack** without configuration gymnastics.
10
+ Each `.wasm` binary is ≤ 23 KB gzipped; the JS entry is **27.0 KB gzipped** (0.2.0,
11
+ v2 surface including i64 + temporals; see [Bundle sizes](#bundle-sizes) and [ADR-012](docs/adr/ADR-012-entry-size-budget-v2.md)).
8
12
 
9
- - **14x faster** than JavaScript for aggregations (sum, mean, min, max)
10
- - **SIMD acceleration** with 4-way parallel computation
11
- - **Zero-copy access** to column data via SharedArrayBuffer
12
- - **Full TypeScript support** with comprehensive type definitions
13
- - **Memory efficient** columnar storage design
14
- - **Fluent API** for method chaining
13
+ ---
15
14
 
16
- ## Installation
15
+ ## Why databonk?
17
16
 
18
- ```bash
17
+ Most JS "dataframe" libraries either wrap pandas in WASM (giant binary) or operate
18
+ row-by-row over plain objects (slow). databonk takes a different path:
19
+
20
+ - **Columns live in WASM linear memory.** JS holds zero-copy `TypedArray` views over
21
+ those buffers — no marshalling overhead on the hot path.
22
+ - **SIMD kernels** (auto-detected; scalar fallback for older Safari). Core
23
+ operations are 3–4× faster than Arquero on 1M-row pipelines.
24
+ - **pandas-shaped API** (`filter`, `groupby().agg()`, `sortValues`, `join`) without
25
+ pandas' index-alignment surprises.
26
+ - **Opt-in threads** via `enableThreads()` (COOP/COEP required in browser; always
27
+ available in Node). 3.3–3.5× speedup on 4 workers for 10M-row reductions.
28
+ - **I/O** — CSV, JSON records, Arrow IPC, and now Parquet (`databonk/parquet` subpath).
29
+ - **v2 dtypes** — `i64`/BigInt columns, `date32`, `timestamp` with timezone metadata,
30
+ and `dt` accessor proxy (`.year()`, `.month()`, `.weekday()`, …).
31
+
32
+ ### v1 non-goals (honest)
33
+
34
+ The following remain deferred or out of scope:
35
+
36
+ - `pandas.Index` / automatic alignment (row position is identity)
37
+ - Chunked / out-of-core columns; lazy query optimizer
38
+ - Write-side CSV formatting; mutation-in-place API (all ops return new frames)
39
+ - wasm64 (> 4 GB) — under consideration
40
+
41
+ i64, dates/timestamps/timezones, and Parquet I/O **shipped in v0.2.0**.
42
+
43
+ ---
44
+
45
+ ## Install
46
+
47
+ ```
19
48
  npm install databonk
20
49
  ```
21
50
 
22
- ## Quick Start
51
+ Peer-required: **Node.js ≥ 18** (or a modern browser with WebAssembly support).
52
+
53
+ ---
54
+
55
+ ## Quickstart
23
56
 
24
57
  ```typescript
25
- import { loadDatabonk, DatabonkDataFrame } from 'databonk';
58
+ import { init, DataFrame, col } from 'databonk';
26
59
 
27
- // Load the WASM module
28
- const module = await loadDatabonk();
60
+ // Load the wasm runtime once at startup (auto-detects SIMD)
61
+ await init();
29
62
 
30
- // Create a DataFrame from typed arrays
31
- const df = await DatabonkDataFrame.fromTypedArrays(module, [
32
- { name: 'id', data: new Int32Array([1, 2, 3, 4, 5]) },
33
- { name: 'value', data: new Float32Array([10.5, 20.5, 30.5, 40.5, 50.5]) },
34
- ]);
63
+ // Build a frame from typed arrays or JS arrays
64
+ const df = DataFrame.fromColumns({
65
+ id: new Int32Array([1, 2, 3, 4, 5]),
66
+ value: new Float64Array([10.5, 3.2, 8.1, 5.9, 2.7]),
67
+ group: ['a', 'b', 'a', 'b', 'a'],
68
+ });
69
+
70
+ // Filter (expression path — fast, WASM-compiled)
71
+ const filtered = df.filter(col('value').gt(5));
72
+
73
+ // Group by + aggregate
74
+ const summary = filtered.groupby(['group']).agg({ value: ['sum', 'mean'] });
75
+
76
+ // Export to JS objects
77
+ const records = summary.toRecords();
78
+ console.log(records);
79
+ // [ { group: 'a', value_sum: 18.6, value_mean: 9.3 },
80
+ // { group: 'b', value_sum: 5.9, value_mean: 5.9 } ]
81
+
82
+ // Clean up WASM memory when done
83
+ filtered.dispose();
84
+ summary.dispose();
85
+ df.dispose();
86
+ ```
87
+
88
+ ### scope() — automatic cleanup
35
89
 
36
- // Aggregations
37
- console.log('Sum:', df.sum('value')); // 152.5
38
- console.log('Mean:', df.mean('value')); // 30.5
39
- console.log('Min:', df.min('value')); // 10.5
40
- console.log('Max:', df.max('value')); // 50.5
41
- console.log('Rows:', df.rowCount); // 5
90
+ ```typescript
91
+ import { scope } from 'databonk';
42
92
 
43
- // Clean up when done
44
- df.free();
93
+ const result = scope(() => {
94
+ const filtered = df.filter(col('value').gt(5));
95
+ const grouped = filtered.groupby(['group']).agg({ value: 'sum' });
96
+ return grouped.toRecords(); // primitive — safe to return outside scope
97
+ });
98
+ // all intermediate frames disposed automatically
45
99
  ```
46
100
 
47
- ## Performance
101
+ ### I/O
102
+
103
+ ```typescript
104
+ import { init, DataFrame, fromCSV, fromArrow, toArrow, fromJSON, toJSON } from 'databonk';
48
105
 
49
- Benchmarks on 1 million rows (Float32):
106
+ // Load the wasm runtime once at startup (needed for fromArrow)
107
+ const rt = await init();
50
108
 
51
- | Operation | WASM SIMD | JavaScript | Speedup |
52
- |-----------|-----------|------------|---------|
53
- | Sum | ~0.3ms | ~4.2ms | **14x** |
54
- | Min | ~0.4ms | ~4.8ms | **12x** |
55
- | Max | ~0.4ms | ~4.8ms | **12x** |
56
- | Mean | ~0.3ms | ~5.0ms | **16x** |
109
+ // CSV (auto-infers dtypes)
110
+ const df = fromCSV(csvText, { delimiter: ',', header: true });
111
+
112
+ // Arrow IPC (compatible with Apache Arrow; no runtime arrow dep needed)
113
+ const buf = toArrow(df);
114
+ const df2 = fromArrow(buf, rt);
115
+
116
+ // JSON records
117
+ const df3 = DataFrame.fromRecords([{ x: 1, y: 'a' }, { x: 2, y: 'b' }]);
118
+ const json = toJSON(df3);
119
+ ```
57
120
 
58
- ## API Overview
121
+ ---
59
122
 
60
- ### Module Loading
123
+ ## i64 / BigInt columns
124
+
125
+ > **ADR-009** — reverses the v1 non-goal "i64 / BigInt columns".
126
+
127
+ `i64` columns store signed 64-bit integers in WASM linear memory as a contiguous
128
+ `BigInt64Array` — the same zero-copy `viewOf` model as every other dtype. BigInt
129
+ cost appears only at scalar crossings (literals, reduction returns, row-proxy access).
130
+
131
+ ### Construction
61
132
 
62
133
  ```typescript
63
- const module = await loadDatabonk({
64
- wasmPath: './build/release.wasm', // Optional: custom WASM path
65
- sharedMemory: true, // Optional: enable SharedArrayBuffer
66
- initialMemory: 256, // Optional: initial memory pages (16MB default)
67
- maximumMemory: 16384, // Optional: max memory pages (1GB default)
134
+ import { init, DataFrame, col, lit } from 'databonk';
135
+
136
+ await init();
137
+
138
+ // BigInt64Array fast path zero-copy into WASM memory
139
+ const df = DataFrame.fromColumns({
140
+ id: BigInt64Array.from([1n, 2n, 9007199254740993n]),
141
+ score: new Float64Array([0.1, 0.9, 0.5]),
68
142
  });
143
+ // dtypes: { id: 'i64', score: 'f64' }
144
+
145
+ // Plain arrays auto-detect i64 when bigint values are present
146
+ const df2 = DataFrame.fromColumns({
147
+ big: [1n, 2n, null, -3n],
148
+ }, { dtypes: { big: 'i64' } });
149
+
150
+ // Explicit dtype forces i64 from safe-integer numbers
151
+ const df3 = DataFrame.fromColumns(
152
+ { count: [1, 2, 3] },
153
+ { dtypes: { count: 'i64' } },
154
+ );
69
155
  ```
70
156
 
71
- ### DataFrame Creation
157
+ **Safe-int throw**: passing a `number` outside `Number.isSafeInteger` range
158
+ (`|x| > 2^53 − 1`) to an i64 column throws a descriptive `RangeError`.
159
+ Use a `bigint` literal for values near or above 2^53.
160
+
161
+ ### Arithmetic, reductions, and precision caveat
72
162
 
73
163
  ```typescript
74
- const df = await DatabonkDataFrame.fromTypedArrays(module, [
75
- { name: 'int_col', data: new Int32Array([1, 2, 3]) },
76
- { name: 'float_col', data: new Float32Array([1.5, 2.5, 3.5]) },
77
- { name: 'double_col', data: new Float64Array([1.1, 2.2, 3.3]) },
78
- ]);
164
+ import { init, DataFrame, col, lit } from 'databonk';
165
+
166
+ await init();
167
+
168
+ const df = DataFrame.fromColumns({
169
+ a: BigInt64Array.from([10n, 20n, 30n]),
170
+ b: BigInt64Array.from([ 1n, 2n, 3n]),
171
+ });
172
+
173
+ // Arithmetic wraps on overflow (two's complement mod 2^64)
174
+ const added = df.withColumn('c', col('a').add(col('b')));
175
+
176
+ // Literal bigint in expressions
177
+ const scaled = df.withColumn('d', col('a').mul(lit(1_000_000n)));
178
+
179
+ // Groupby + agg: sum/min/max return bigint; mean returns number (f64)
180
+ const byGroup = DataFrame.fromColumns({
181
+ g: ['x', 'y', 'x'],
182
+ v: BigInt64Array.from([10n, 20n, 30n]),
183
+ });
184
+ const grouped = byGroup.groupby('g').agg({ v: ['sum', 'mean', 'min'] });
185
+ // v_sum: bigint, v_mean: number, v_min: bigint
186
+
187
+ added.dispose();
188
+ scaled.dispose();
189
+ grouped.dispose();
190
+ byGroup.dispose();
191
+ df.dispose();
79
192
  ```
80
193
 
81
- ### Aggregations
194
+ > **Precision caveat (ADR-009):** `mean`, `std`, and `var` on an `i64` column convert
195
+ > each value to `f64` first. For magnitudes above 2^53 the conversion rounds to the
196
+ > nearest representable float; the result loses precision but never throws. This is the
197
+ > same trade-off v1 already makes for `mean_i32`. Use `sum` (returns `bigint`) when
198
+ > exact 64-bit integer results are required.
199
+
200
+ ### Widening and casting
201
+
202
+ - `i32`/`u32` ⊕ `i64` → `i64` (exact widen)
203
+ - `i64` ⊕ `f64` → `f64` (rounds if |x| > 2^53)
204
+ - `i64` ⊕ `f32` → `f64` (both widen; f32 cannot hold an i64)
205
+ - `f64 → i64`: truncates toward zero; out-of-range or NaN → **null** (never traps)
206
+ - `i64 → i32`/`u32`: wraps to the low 32 bits (two's complement)
207
+
208
+ ---
209
+
210
+ ## Temporal dtypes
211
+
212
+ > **ADR-010** — reverses the v1 non-goal "dates, timestamps, timezones".
213
+
214
+ Two logical temporal dtypes, both following the Apache Arrow model:
215
+
216
+ | logical dtype | physical | unit / epoch |
217
+ |---|---|---|
218
+ | `date32` | `i32` | days since Unix epoch (1970-01-01), proleptic Gregorian |
219
+ | `timestamp` | `i64` | milliseconds since Unix epoch, **always UTC** |
220
+
221
+ `timestamp` values are stored in UTC; timezone is column-level metadata only and never
222
+ changes the stored value (Arrow model). Null semantics are unchanged: null is the
223
+ validity bit, never a sentinel day/ms.
224
+
225
+ ### Construction
82
226
 
83
227
  ```typescript
84
- df.sum('column'); // Sum of values
85
- df.mean('column'); // Average
86
- df.min('column'); // Minimum
87
- df.max('column'); // Maximum
88
- df.count('column'); // Count of values
228
+ import { init, DataFrame, col } from 'databonk';
229
+
230
+ await init();
231
+
232
+ // date32: days since 1970-01-01 stored as i32.
233
+ // Convert JS Date to day count: Math.floor(date.getTime() / 86_400_000)
234
+ // 19737 = 2024-01-15, 19783 = 2024-03-01
235
+ const dates = DataFrame.fromColumns(
236
+ { d: new Int32Array([19737, 19783, 0]) },
237
+ { dtypes: { d: 'date32' } },
238
+ );
239
+
240
+ // timestamp: BigInt64Array of UTC milliseconds since epoch
241
+ // 1717243200000n = 2024-06-01T12:00:00Z (new Date(...).getTime())
242
+ const ts = DataFrame.fromColumns(
243
+ { ts: BigInt64Array.from([1717243200000n, 1718409600000n]) },
244
+ { dtypes: { ts: 'timestamp' } },
245
+ );
246
+
247
+ // Attach IANA timezone metadata (display / dt accessors only; stored value stays UTC)
248
+ const tsWithTz = DataFrame.fromColumns(
249
+ { ts: BigInt64Array.from([1717243200000n, 1718409600000n]) },
250
+ { dtypes: { ts: 'timestamp' }, tzs: { ts: 'America/Chicago' } },
251
+ );
252
+
253
+ dates.dispose();
254
+ ts.dispose();
255
+ tsWithTz.dispose();
89
256
  ```
90
257
 
91
- ### Column Arithmetic
258
+ ### `dt` accessor proxy
259
+
260
+ Field extraction runs entirely in JS over the typed-array view — no `Date` object per row.
261
+ UTC path uses integer civil-from-days math; tz-aware path uses a cached `Intl.DateTimeFormat`.
92
262
 
93
263
  ```typescript
94
- df.add('a', 'b', 'sum') // sum = a + b
95
- .sub('a', 'b', 'diff') // diff = a - b
96
- .scalarMul('a', 2.5, 'scaled'); // scaled = a * 2.5
264
+ import { init, DataFrame, col } from 'databonk';
265
+
266
+ await init();
267
+
268
+ const tsData = DataFrame.fromColumns(
269
+ { created: BigInt64Array.from([1717243200000n]) },
270
+ { dtypes: { created: 'timestamp' }, tzs: { created: 'America/Chicago' } },
271
+ );
272
+
273
+ // Expression path: col('created').dt.year() → Expr producing i32 Series
274
+ const years = tsData.withColumn('yr', col('created').dt.year());
275
+ const months = tsData.withColumn('mo', col('created').dt.month());
276
+ const wkdays = tsData.withColumn('wd', col('created').dt.weekday()); // ISO: Mon=1…Sun=7
277
+
278
+ // Series path (Series.dt returns SeriesDtProxy)
279
+ const series = tsData.col('created');
280
+ if (series) {
281
+ const yearSeries = series.dt.year(); // new Series, dtype=i32
282
+ // yearSeries is a view; no separate dispose() needed
283
+ void yearSeries;
284
+ }
285
+
286
+ years.dispose();
287
+ months.dispose();
288
+ wkdays.dispose();
289
+ tsData.dispose();
97
290
  ```
98
291
 
99
- ### GroupBy
292
+ Available accessors (all return i32 values): `year`, `month`, `day`, `hour`, `minute`,
293
+ `second`, `millisecond`, `weekday` (ISO 8601: Mon=1…Sun=7), `dayOfYear`, `quarter`.
294
+
295
+ `hour`, `minute`, `second`, and `millisecond` are not available on `date32` columns
296
+ (calendar date has no time-of-day component) and throw a `TypeError` if attempted.
297
+
298
+ tz-aware accessors (`ts.dt.*` with a tz-tagged `timestamp` column) use the platform
299
+ `Intl` timezone database — correct across DST and offset history. The UTC path (no tz
300
+ or `tz='UTC'`) is faster (integer math, branch-light, suitable for 1M+ rows).
301
+
302
+ ### Restricted temporal arithmetic
303
+
304
+ Only these forms are legal; everything else is a typed `FrameError`:
305
+
306
+ | operation | result dtype | notes |
307
+ |---|---|---|
308
+ | `timestamp − timestamp` | `i64` (ms) | duration; reuses `sub_i64` |
309
+ | `timestamp + integer` | `timestamp` | offset is i64 ms; wraps mod 2^64 |
310
+ | `timestamp − integer` | `timestamp` | same |
311
+ | `date32 − date32` | `i32` (days) | reuses `sub_i32` |
312
+ | `date32 + integer` | `date32` | offset is i32 days |
313
+ | `date32 − integer` | `date32` | same |
314
+
315
+ `mul`, `div`, `mod` on any temporal; `timestamp + date32`; `timestamp + timestamp` (addition);
316
+ and any temporal ⊕ float all throw a descriptive error.
317
+
318
+ ---
319
+
320
+ ## Parquet I/O
321
+
322
+ > **ADR-011** — reverses the v1 non-goal "Parquet I/O (Arrow IPC only)".
323
+
324
+ Parquet support ships as a **separate subpath** so the main entry stays dep-free
325
+ and under 30 KB. Only importing `databonk/parquet` pulls in the runtime dependencies.
100
326
 
101
327
  ```typescript
102
- const grouped = df.groupBy('category', 256) // maxKey parameter
103
- .sum('value'); // or .mean('value')
328
+ import { init, defaultRuntime } from 'databonk';
329
+ import { readParquet, writeParquet } from 'databonk/parquet';
330
+
331
+ await init();
332
+ const rt = defaultRuntime();
333
+
334
+ // Read a Parquet file (async — hyparquet reader is Promise-based)
335
+ const bytes: Uint8Array = new Uint8Array(0); // replace with actual file bytes
336
+ const dfP = await readParquet(bytes, rt);
337
+
338
+ // Write a Parquet file (sync; default uncompressed, or 'snappy')
339
+ const out: Uint8Array = writeParquet(dfP, { compression: 'snappy' });
340
+
341
+ dfP.dispose();
104
342
  ```
105
343
 
106
- ### Inner Join
344
+ ### Supported dtype profile (ADR-011)
345
+
346
+ | databonk dtype | Parquet physical / logical |
347
+ |---|---|
348
+ | `f64` | `DOUBLE` |
349
+ | `f32` | `FLOAT` |
350
+ | `i32` | `INT32` |
351
+ | `u32` | `INT32` + `UINT_32` converted type |
352
+ | `i64` | `INT64` |
353
+ | `bool` | `BOOLEAN` |
354
+ | `utf8` | `BYTE_ARRAY` + `STRING(UTF8)`, dictionary-encoded |
355
+ | `date32` | `INT32` + `DATE` logical |
356
+ | `timestamp` | `INT64` + `TIMESTAMP(MILLIS, isAdjustedToUTC)`; tz round-trips in `key_value_metadata` |
357
+
358
+ Compression: **Snappy** and **uncompressed** (both native to `hyparquet`).
359
+
360
+ Anything outside the profile raises a clear, specific `Error` naming what was
361
+ unsupported — never a silent wrong result. Explicitly out of profile: gzip/brotli/zstd/lz4
362
+ compression; `INT96` timestamps; nested/repeated (`LIST`/`MAP`/`STRUCT`) columns;
363
+ `FIXED_LEN_BYTE_ARRAY`; `DECIMAL`.
364
+
365
+ ### Subpath packaging
366
+
367
+ Runtime dependencies (`hyparquet 1.26.2` + `hyparquet-writer 0.16.1`) are declared in
368
+ `package.json` `"dependencies"` and imported as external modules at runtime — they are
369
+ **not bundled into the tarball**. They install alongside databonk automatically. The main
370
+ entry (`.`) has zero runtime dependencies and is not affected.
371
+
372
+ ---
373
+
374
+ ## Expression API vs lambda escape hatch
375
+
376
+ databonk has two filter/map styles:
377
+
378
+ ### Expression path (fast — WASM-compiled)
107
379
 
108
380
  ```typescript
109
- const result = left.innerJoin(right, 'left_key', 'right_key');
381
+ // col() builds an AST; the compiler fuses compare→filter into one kernel call.
382
+ df.filter(col('value').gt(5).and(col('group').eq('a')));
383
+ df.withColumn('doubled', col('value').mul(2));
384
+ df.filter(col('value').isNull().not());
110
385
  ```
111
386
 
112
- ### Zero-Copy Column Access
387
+ Expressions support: `add sub mul div mod neg`, `gt ge lt le eq ne`,
388
+ `and or not`, `isNull fillNull`, `cast`, and aggregations
389
+ `sum mean min max count nunique std var first last`.
390
+
391
+ ### Lambda escape hatch (SLOW PATH — scalar JS speed)
392
+
393
+ > **Warning:** `filterFn` and `mapFn` iterate rows via a JS row-proxy. They avoid
394
+ > data copies but run at scalar JS speed, not WASM speed. Use expressions above
395
+ > whenever possible.
113
396
 
114
397
  ```typescript
115
- const view = df.getColumnView('value');
116
- if (view) {
117
- console.log(view.get(0)); // First value
118
- console.log([...view]); // Iterate
119
- console.log(view.toArray()); // Copy to regular array
120
- }
398
+ // SLOW PATH — use only when an expression cannot express the logic
399
+ df.filterFn(r => r.value > 5 && r.group === 'a');
400
+
401
+ // Expression equivalent (fast):
402
+ df.filter(col('value').gt(5).and(col('group').eq('a')));
121
403
  ```
122
404
 
123
- ### Memory Management
405
+ ADR-003 requires showing the expression equivalent alongside every lambda example —
406
+ this is not just documentation style; it's a reminder that the fast path exists.
407
+
408
+ ---
409
+
410
+ ## Benchmark table
411
+
412
+ Measured in **Docker (Debian bookworm), Node v22.23.1, Linux** (single thread, SIMD build).
413
+ Numbers are medians of 3 independent fresh runs at 1M rows.
414
+ Source: [`bench/baselines/e2e-v1.json`](bench/baselines/e2e-v1.json).
415
+
416
+ | Operation | databonk (ms) | Arquero (ms) | Ratio |
417
+ |---|---:|---:|---:|
418
+ | filter → groupby → sum (pipeline) | 12.3 | 45.3 | **3.7× faster** |
419
+ | join (inner, string key) | 68.6 | 120.6 | **1.8× faster** |
420
+ | sortValues (f64, 1M rows) | 145.3 | 248.9 | **1.7× faster** |
421
+
422
+ > **Caveats:** Arquero times include `.objects()` materialisation (that's what its
423
+ > pipeline naturally produces); databonk times do not materialise to JS objects.
424
+ > Results vary by machine, Node version, dataset shape, and JIT warm-up.
425
+ > Run `node bench/e2e/pipeline.mjs` after `npm run build` to reproduce.
426
+
427
+ Danfo.js is tracked in `/bench/baselines/danfo.json` for informational comparison
428
+ but is not shown here (it uses TensorFlow.js under the hood, making size
429
+ comparisons misleading).
430
+
431
+ ### i64 kernel performance (v2.3)
432
+
433
+ Measured in Docker, Node v22.23.1, 1M elements, SIMD build.
434
+ Source: recorded run in [`bench/baselines/i64-threads-v1.txt`](bench/baselines/i64-threads-v1.txt) (`bench/kernels/i64.mjs`).
435
+
436
+ | Operation | WASM (ops/s) | JS BigInt64Array (ops/s) | Ratio |
437
+ |---|---:|---:|---:|
438
+ | `add_i64` @1M (gate ≥ 1.5×) | 4 447 | 1 555 | **2.9× faster** |
439
+ | `mul_i64` @1M | 4 175 | 1 534 | **2.7× faster** |
440
+ | `hash_i64` @1M | 1 724 | 50 | **34.6× faster** |
441
+
442
+ ### Parallel mode (4 workers, 10M f64 elements)
443
+
444
+ Source: recorded run in [`bench/baselines/i64-threads-v1.txt`](bench/baselines/i64-threads-v1.txt) (`bench/workers/threads-bench.mjs`).
445
+
446
+ | Op | 1 thread (ms) | 4 workers (ms) | Speedup |
447
+ |---|---:|---:|---:|
448
+ | sum | 2.72 | 0.81 | 3.3× |
449
+ | mean | 2.75 | 0.80 | 3.4× |
450
+ | min | 4.48 | 1.24 | 3.6× |
451
+
452
+ See [docs/threads.md](docs/threads.md) for setup.
453
+
454
+ ---
455
+
456
+ ## Bundle sizes
457
+
458
+ Gzipped, 0.2.0 build (Docker, Debian bookworm, Node v22.23.1, 2026-07-03):
459
+
460
+ | Asset | Gzipped | Notes |
461
+ |---|---:|---|
462
+ | `dist/index.js` (ESM) | 27.0 KB | v2 surface (i64 + temporals); ADR-012 budget 30 KB |
463
+ | `dist/index.cjs` (CJS) | 27.3 KB | |
464
+ | `dist/simd.wasm` | 22.1 KB | |
465
+ | `dist/scalar.wasm` (fallback) | 17.4 KB | |
466
+ | `dist/simd-threads.wasm` (threads opt-in) | 21.9 KB | |
467
+ | `dist/parquet.js` (`databonk/parquet` ESM) | 20.9 KB | hyparquet + hyparquet-writer as external imports; not bundled |
468
+
469
+ The ADR-012 budget for the main entry is **30 KB gz**. The 27.0 KB reading is with the full
470
+ v2 surface imported. Consumers who tree-shake to a v1-profile import (no i64/temporal/parquet)
471
+ pay approximately the v1 entry cost via their bundler's dead-code elimination.
472
+
473
+ ---
474
+
475
+ ## Feature matrix
476
+
477
+ | Feature | v0.1.0 | v0.2.0 |
478
+ |---|---|---|
479
+ | f64 / f32 / i32 / u32 / bool / utf8 columns | yes | yes |
480
+ | i64 / BigInt columns | no | **yes (ADR-009)** |
481
+ | Date / timestamp / timezone | no | **yes (ADR-010)** |
482
+ | `dt` accessor proxy (year/month/…/weekday) | no | **yes (ADR-010, ADR-012)** |
483
+ | Null semantics (pandas-flavored) | yes | yes |
484
+ | `filter` / `groupby` / `join` / `sortValues` | yes | yes |
485
+ | `withColumn` / `select` / `drop` / `head` / `tail` | yes | yes |
486
+ | `describe()` | yes | yes |
487
+ | Null-propagating arithmetic + Kleene boolean | yes | yes |
488
+ | Expression compiler + kernel fusion | yes | yes |
489
+ | Lambda escape hatch (`filterFn` / `mapFn`) | yes | yes |
490
+ | SIMD kernels (auto-detected) | yes | yes |
491
+ | Opt-in worker threads (`enableThreads()`) | yes | yes |
492
+ | CSV / JSON / Arrow IPC I/O | yes | yes |
493
+ | Parquet I/O (`databonk/parquet` subpath) | no | **yes (ADR-011)** |
494
+ | Chunked / out-of-core columns | no | planned |
495
+ | Lazy query optimizer | no | planned |
496
+ | `pandas.Index` / alignment | no | not planned |
497
+ | Mutation-in-place API | no | not planned |
498
+ | wasm64 (> 4 GB) | no | consideration |
499
+
500
+ ---
501
+
502
+ ## Parallel / threaded mode
503
+
504
+ Requires `SharedArrayBuffer`. In Node ≥ 18 this is always available. In the
505
+ browser you must enable cross-origin isolation (COOP/COEP headers).
124
506
 
125
507
  ```typescript
126
- df.free(); // Always free DataFrames when done
508
+ import { enableThreads } from 'databonk/workers';
509
+
510
+ const th = await enableThreads({ workers: 4 });
511
+ if (!th) throw new Error('threads unavailable — check COOP/COEP headers');
512
+
513
+ // th.sumF64 / th.meanF64 / th.minF64 / th.maxF64 dispatch in parallel
514
+ const sum = await th.sumF64(dataPtr, vpPtr, len);
515
+ th.terminate();
127
516
  ```
128
517
 
129
- ## Documentation
518
+ Full docs: [docs/threads.md](docs/threads.md)
519
+
520
+ ---
130
521
 
131
- - [API Reference](./docs/api.md) - Full API documentation
132
- - [Examples](./docs/examples.md) - Detailed code examples
522
+ ## Bundler / WASM loading
133
523
 
134
- ## Supported Column Types
524
+ See [docs/bundlers.md](docs/bundlers.md) for Vite, webpack, and inline-base64
525
+ fallback instructions.
135
526
 
136
- | Type | TypedArray | Use Case |
137
- |------|------------|----------|
138
- | Int32 | `Int32Array` | Integer keys, IDs, counts |
139
- | Float32 | `Float32Array` | Standard floating-point values |
140
- | Float64 | `Float64Array` | High-precision values |
527
+ ---
141
528
 
142
- ## Current Limitations
529
+ ## API docs
143
530
 
144
- - GroupBy currently supports single value column aggregation
145
- - Join keys must be Int32 values
146
- - String columns are supported for storage but not for operations
531
+ Generated with [TypeDoc](https://typedoc.org). Run `npm run docs` to build
532
+ `docs/api/` from source. The generated output is not committed to the repository.
533
+
534
+ ---
147
535
 
148
536
  ## Development
149
537
 
150
- ```bash
151
- # Install dependencies
152
- npm install
538
+ The build toolchain runs inside Docker (Rust + wasm-opt + Node):
153
539
 
154
- # Build WASM module
155
- npm run asbuild
540
+ ```bash
541
+ # Build wasm + JS
542
+ docker run --rm -v "$PWD":/work -w /work dataframe-dev \
543
+ bash -lc 'npm ci && npm run gate'
156
544
 
157
- # Run tests
158
- npm test
545
+ # Run tests only
546
+ docker run --rm -v "$PWD":/work -w /work dataframe-dev \
547
+ bash -lc 'npm ci && npm run test'
159
548
 
160
- # Run benchmarks
161
- npm run benchmark
549
+ # Run E2E benchmarks
550
+ docker run --rm -v "$PWD":/work -w /work dataframe-dev \
551
+ bash -lc 'npm ci && npm run build && node bench/e2e/pipeline.mjs'
162
552
  ```
163
553
 
554
+ Issue tracking: **bd (beads)** — `bd ready` to find available work, `bd prime`
555
+ for full workflow context.
556
+
557
+ ---
558
+
164
559
  ## License
165
560
 
166
561
  MIT