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.
- package/README.md +491 -96
- package/dist/dataframe-BggBYXYm.d.cts +886 -0
- package/dist/dataframe-BggBYXYm.d.ts +886 -0
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +366 -0
- package/dist/index.d.ts +354 -31
- package/dist/index.js +4 -40
- package/dist/index.js.map +1 -1
- package/dist/parallel-fv5h4BkA.d.cts +152 -0
- package/dist/parallel-fv5h4BkA.d.ts +152 -0
- package/dist/parquet.cjs +3 -0
- package/dist/parquet.cjs.map +1 -0
- package/dist/parquet.d.cts +57 -0
- package/dist/parquet.d.ts +57 -0
- package/dist/parquet.js +3 -0
- package/dist/parquet.js.map +1 -0
- package/dist/scalar.wasm +0 -0
- package/dist/simd-threads.wasm +0 -0
- package/dist/simd.wasm +0 -0
- package/dist/workers.cjs +102 -0
- package/dist/workers.cjs.map +1 -0
- package/dist/workers.d.cts +74 -0
- package/dist/workers.d.ts +74 -0
- package/dist/workers.js +102 -0
- package/dist/workers.js.map +1 -0
- package/package.json +93 -32
- package/build/release.d.ts +0 -719
- package/build/release.js +0 -774
- package/build/release.wasm +0 -0
- package/build/release.wasm.map +0 -1
- package/build/release.wat +0 -22633
- package/dist/dataframe.d.ts +0 -82
- package/dist/dataframe.d.ts.map +0 -1
- package/dist/dataframe.js +0 -318
- package/dist/dataframe.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/loader.d.ts +0 -86
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -147
- package/dist/loader.js.map +0 -1
- package/dist/shared-memory.d.ts +0 -64
- package/dist/shared-memory.d.ts.map +0 -1
- package/dist/shared-memory.js +0 -113
- package/dist/shared-memory.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,166 +1,561 @@
|
|
|
1
|
-
#
|
|
1
|
+
# databonk
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
```
|
|
6
|
+
npm install databonk
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
15
|
+
## Why databonk?
|
|
17
16
|
|
|
18
|
-
|
|
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
|
-
|
|
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 {
|
|
58
|
+
import { init, DataFrame, col } from 'databonk';
|
|
26
59
|
|
|
27
|
-
// Load the
|
|
28
|
-
|
|
60
|
+
// Load the wasm runtime once at startup (auto-detects SIMD)
|
|
61
|
+
await init();
|
|
29
62
|
|
|
30
|
-
//
|
|
31
|
-
const df =
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
df.
|
|
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
|
-
|
|
101
|
+
### I/O
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { init, DataFrame, fromCSV, fromArrow, toArrow, fromJSON, toJSON } from 'databonk';
|
|
48
105
|
|
|
49
|
-
|
|
106
|
+
// Load the wasm runtime once at startup (needed for fromArrow)
|
|
107
|
+
const rt = await init();
|
|
50
108
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
121
|
+
---
|
|
59
122
|
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
+
Full docs: [docs/threads.md](docs/threads.md)
|
|
519
|
+
|
|
520
|
+
---
|
|
130
521
|
|
|
131
|
-
|
|
132
|
-
- [Examples](./docs/examples.md) - Detailed code examples
|
|
522
|
+
## Bundler / WASM loading
|
|
133
523
|
|
|
134
|
-
|
|
524
|
+
See [docs/bundlers.md](docs/bundlers.md) for Vite, webpack, and inline-base64
|
|
525
|
+
fallback instructions.
|
|
135
526
|
|
|
136
|
-
|
|
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
|
-
##
|
|
529
|
+
## API docs
|
|
143
530
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
151
|
-
# Install dependencies
|
|
152
|
-
npm install
|
|
538
|
+
The build toolchain runs inside Docker (Rust + wasm-opt + Node):
|
|
153
539
|
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|