databonk 0.0.3 → 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 +511 -111
- 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 +366 -23
- package/dist/index.js +5 -6734
- 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 +95 -60
- package/dist/core/column.d.ts +0 -55
- package/dist/core/column.d.ts.map +0 -1
- package/dist/core/dataframe.d.ts +0 -148
- package/dist/core/dataframe.d.ts.map +0 -1
- package/dist/core/index-cache.d.ts +0 -44
- package/dist/core/index-cache.d.ts.map +0 -1
- package/dist/core/index-manager.d.ts +0 -97
- package/dist/core/index-manager.d.ts.map +0 -1
- package/dist/core/index.d.ts +0 -75
- package/dist/core/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -6716
- package/dist/index.esm.js.map +0 -1
- package/dist/io/csv.d.ts +0 -23
- package/dist/io/csv.d.ts.map +0 -1
- package/dist/operations/aggregation.d.ts +0 -23
- package/dist/operations/aggregation.d.ts.map +0 -1
- package/dist/operations/derive.d.ts +0 -38
- package/dist/operations/derive.d.ts.map +0 -1
- package/dist/operations/groupby.d.ts +0 -36
- package/dist/operations/groupby.d.ts.map +0 -1
- package/dist/operations/join.d.ts +0 -58
- package/dist/operations/join.d.ts.map +0 -1
- package/dist/operations/reshape.d.ts +0 -17
- package/dist/operations/reshape.d.ts.map +0 -1
- package/dist/utils/aggregation-engine.d.ts +0 -84
- package/dist/utils/aggregation-engine.d.ts.map +0 -1
- package/dist/utils/bitset.d.ts +0 -30
- package/dist/utils/bitset.d.ts.map +0 -1
- package/dist/utils/hash.d.ts +0 -79
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/performance.d.ts +0 -44
- package/dist/utils/performance.d.ts.map +0 -1
- package/dist/utils/types.d.ts +0 -7
- package/dist/utils/types.d.ts.map +0 -1
- package/dist/validation/schema.d.ts +0 -73
- package/dist/validation/schema.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,161 +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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **Flexible**: Works with regular arrays, TypedArrays, or Apache Arrow
|
|
11
|
-
- **Schema Validation**: Built-in Zod integration for data validation
|
|
12
|
-
- **Type Safe**: Full TypeScript support with inferred types
|
|
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)).
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
---
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
## Why databonk?
|
|
16
|
+
|
|
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
|
+
```
|
|
48
|
+
npm install databonk
|
|
18
49
|
```
|
|
19
50
|
|
|
20
|
-
|
|
51
|
+
Peer-required: **Node.js ≥ 18** (or a modern browser with WebAssembly support).
|
|
21
52
|
|
|
22
|
-
|
|
23
|
-
import { DataFrame, SchemaValidator, CommonSchemas } from 'databonk';
|
|
53
|
+
---
|
|
24
54
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { init, DataFrame, col } from 'databonk';
|
|
59
|
+
|
|
60
|
+
// Load the wasm runtime once at startup (auto-detects SIMD)
|
|
61
|
+
await init();
|
|
62
|
+
|
|
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'],
|
|
30
68
|
});
|
|
31
69
|
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
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'] });
|
|
36
75
|
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
console.log(
|
|
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
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { scope } from 'databonk';
|
|
92
|
+
|
|
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
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### I/O
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { init, DataFrame, fromCSV, fromArrow, toArrow, fromJSON, toJSON } from 'databonk';
|
|
105
|
+
|
|
106
|
+
// Load the wasm runtime once at startup (needed for fromArrow)
|
|
107
|
+
const rt = await init();
|
|
108
|
+
|
|
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);
|
|
40
119
|
```
|
|
41
120
|
|
|
42
|
-
|
|
121
|
+
---
|
|
43
122
|
|
|
44
|
-
|
|
45
|
-
- **Filtering & Selection**: Powerful row/column filtering with predicate functions
|
|
46
|
-
- **Joins**: Inner, left, right, and outer joins with multiple keys
|
|
47
|
-
- **Aggregations**: Sum, mean, count, min, max, std, variance with group-by support
|
|
48
|
-
- **Reshaping**: Pivot, melt, transpose operations for data transformation
|
|
49
|
-
- **Sorting**: Multi-column sorting with custom comparators
|
|
123
|
+
## i64 / BigInt columns
|
|
50
124
|
|
|
51
|
-
|
|
52
|
-
- **Built-in Schemas**: Common patterns for users, products, transactions, coordinates
|
|
53
|
-
- **Custom Validation**: Define your own schemas with Zod
|
|
54
|
-
- **Data Cleaning**: Filter valid/invalid rows, transform data types
|
|
55
|
-
- **Error Reporting**: Detailed validation errors with row/column information
|
|
125
|
+
> **ADR-009** — reverses the v1 non-goal "i64 / BigInt columns".
|
|
56
126
|
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
- **Streaming**: Memory-efficient processing of large datasets
|
|
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).
|
|
61
130
|
|
|
62
|
-
|
|
131
|
+
### Construction
|
|
63
132
|
|
|
64
|
-
|
|
133
|
+
```typescript
|
|
134
|
+
import { init, DataFrame, col, lit } from 'databonk';
|
|
65
135
|
|
|
66
|
-
|
|
67
|
-
import { DataFrame, SchemaValidator } from 'databonk';
|
|
68
|
-
import { z } from 'zod';
|
|
136
|
+
await init();
|
|
69
137
|
|
|
70
|
-
//
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
email: z.string().email(),
|
|
75
|
-
role: z.enum(['admin', 'user', 'guest'])
|
|
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]),
|
|
76
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' } });
|
|
77
149
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
150
|
+
// Explicit dtype forces i64 from safe-integer numbers
|
|
151
|
+
const df3 = DataFrame.fromColumns(
|
|
152
|
+
{ count: [1, 2, 3] },
|
|
153
|
+
{ dtypes: { count: 'i64' } },
|
|
154
|
+
);
|
|
155
|
+
```
|
|
82
156
|
|
|
83
|
-
|
|
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.
|
|
84
160
|
|
|
85
|
-
|
|
86
|
-
const validation = df.validate(userSchema);
|
|
87
|
-
console.log(`Errors: ${validation.errors.length}`);
|
|
161
|
+
### Arithmetic, reductions, and precision caveat
|
|
88
162
|
|
|
89
|
-
|
|
90
|
-
|
|
163
|
+
```typescript
|
|
164
|
+
import { init, DataFrame, col, lit } from 'databonk';
|
|
91
165
|
|
|
92
|
-
|
|
93
|
-
|
|
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();
|
|
94
192
|
```
|
|
95
193
|
|
|
96
|
-
|
|
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)
|
|
97
207
|
|
|
98
|
-
|
|
99
|
-
// Join operations
|
|
100
|
-
const sales = DataFrame.fromRows([
|
|
101
|
-
{ product_id: 1, quantity: 100, region: 'North' },
|
|
102
|
-
{ product_id: 2, quantity: 150, region: 'South' }
|
|
103
|
-
]);
|
|
208
|
+
---
|
|
104
209
|
|
|
105
|
-
|
|
106
|
-
{ product_id: 1, name: 'Widget', price: 10.99 },
|
|
107
|
-
{ product_id: 2, name: 'Gadget', price: 15.99 }
|
|
108
|
-
]);
|
|
210
|
+
## Temporal dtypes
|
|
109
211
|
|
|
110
|
-
|
|
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
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
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
|
+
);
|
|
111
239
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
});
|
|
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
|
+
);
|
|
119
246
|
|
|
120
|
-
//
|
|
121
|
-
const
|
|
122
|
-
|
|
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' } },
|
|
123
251
|
);
|
|
124
252
|
|
|
125
|
-
|
|
126
|
-
|
|
253
|
+
dates.dispose();
|
|
254
|
+
ts.dispose();
|
|
255
|
+
tsWithTz.dispose();
|
|
127
256
|
```
|
|
128
257
|
|
|
129
|
-
|
|
258
|
+
### `dt` accessor proxy
|
|
130
259
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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`.
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { init, DataFrame, col } from 'databonk';
|
|
265
|
+
|
|
266
|
+
await init();
|
|
134
267
|
|
|
135
|
-
|
|
136
|
-
|
|
268
|
+
const tsData = DataFrame.fromColumns(
|
|
269
|
+
{ created: BigInt64Array.from([1717243200000n]) },
|
|
270
|
+
{ dtypes: { created: 'timestamp' }, tzs: { created: 'America/Chicago' } },
|
|
271
|
+
);
|
|
137
272
|
|
|
138
|
-
|
|
139
|
-
|
|
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();
|
|
140
290
|
```
|
|
141
291
|
|
|
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.
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
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();
|
|
342
|
+
```
|
|
343
|
+
|
|
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)
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
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());
|
|
385
|
+
```
|
|
386
|
+
|
|
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.
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
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')));
|
|
403
|
+
```
|
|
404
|
+
|
|
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).
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
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();
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Full docs: [docs/threads.md](docs/threads.md)
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Bundler / WASM loading
|
|
523
|
+
|
|
524
|
+
See [docs/bundlers.md](docs/bundlers.md) for Vite, webpack, and inline-base64
|
|
525
|
+
fallback instructions.
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## API docs
|
|
530
|
+
|
|
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
|
+
---
|
|
535
|
+
|
|
142
536
|
## Development
|
|
143
537
|
|
|
538
|
+
The build toolchain runs inside Docker (Rust + wasm-opt + Node):
|
|
539
|
+
|
|
144
540
|
```bash
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
npm run
|
|
148
|
-
|
|
541
|
+
# Build wasm + JS
|
|
542
|
+
docker run --rm -v "$PWD":/work -w /work dataframe-dev \
|
|
543
|
+
bash -lc 'npm ci && npm run gate'
|
|
544
|
+
|
|
545
|
+
# Run tests only
|
|
546
|
+
docker run --rm -v "$PWD":/work -w /work dataframe-dev \
|
|
547
|
+
bash -lc 'npm ci && npm run test'
|
|
149
548
|
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
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'
|
|
153
552
|
```
|
|
154
553
|
|
|
155
|
-
|
|
554
|
+
Issue tracking: **bd (beads)** — `bd ready` to find available work, `bd prime`
|
|
555
|
+
for full workflow context.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## License
|
|
156
560
|
|
|
157
|
-
|
|
158
|
-
- **Memory efficient**: Columnar storage with TypedArrays
|
|
159
|
-
- **Fast operations**: Optimized algorithms for joins, aggregations
|
|
160
|
-
- **Minimal overhead**: Zero-copy operations where possible
|
|
161
|
-
- **Tree-shakeable**: Only import what you use
|
|
561
|
+
MIT
|