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.
Files changed (64) hide show
  1. package/README.md +511 -111
  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 +366 -23
  8. package/dist/index.js +5 -6734
  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 +95 -60
  28. package/dist/core/column.d.ts +0 -55
  29. package/dist/core/column.d.ts.map +0 -1
  30. package/dist/core/dataframe.d.ts +0 -148
  31. package/dist/core/dataframe.d.ts.map +0 -1
  32. package/dist/core/index-cache.d.ts +0 -44
  33. package/dist/core/index-cache.d.ts.map +0 -1
  34. package/dist/core/index-manager.d.ts +0 -97
  35. package/dist/core/index-manager.d.ts.map +0 -1
  36. package/dist/core/index.d.ts +0 -75
  37. package/dist/core/index.d.ts.map +0 -1
  38. package/dist/index.d.ts.map +0 -1
  39. package/dist/index.esm.js +0 -6716
  40. package/dist/index.esm.js.map +0 -1
  41. package/dist/io/csv.d.ts +0 -23
  42. package/dist/io/csv.d.ts.map +0 -1
  43. package/dist/operations/aggregation.d.ts +0 -23
  44. package/dist/operations/aggregation.d.ts.map +0 -1
  45. package/dist/operations/derive.d.ts +0 -38
  46. package/dist/operations/derive.d.ts.map +0 -1
  47. package/dist/operations/groupby.d.ts +0 -36
  48. package/dist/operations/groupby.d.ts.map +0 -1
  49. package/dist/operations/join.d.ts +0 -58
  50. package/dist/operations/join.d.ts.map +0 -1
  51. package/dist/operations/reshape.d.ts +0 -17
  52. package/dist/operations/reshape.d.ts.map +0 -1
  53. package/dist/utils/aggregation-engine.d.ts +0 -84
  54. package/dist/utils/aggregation-engine.d.ts.map +0 -1
  55. package/dist/utils/bitset.d.ts +0 -30
  56. package/dist/utils/bitset.d.ts.map +0 -1
  57. package/dist/utils/hash.d.ts +0 -79
  58. package/dist/utils/hash.d.ts.map +0 -1
  59. package/dist/utils/performance.d.ts +0 -44
  60. package/dist/utils/performance.d.ts.map +0 -1
  61. package/dist/utils/types.d.ts +0 -7
  62. package/dist/utils/types.d.ts.map +0 -1
  63. package/dist/validation/schema.d.ts +0 -73
  64. package/dist/validation/schema.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,161 +1,561 @@
1
- # Databonk.js
1
+ # databonk
2
2
 
3
- A lightweight, fast data frame library for JavaScript and TypeScript with built-in schema validation.
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
- ## Features
5
+ ```
6
+ npm install databonk
7
+ ```
6
8
 
7
- - **Lightweight**: Minimal dependencies, tree-shakeable modules
8
- - **Fast**: Columnar storage using TypedArrays for performance
9
- - **Simple**: Clean API for common data operations
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
- ## Installation
13
+ ---
15
14
 
16
- ```bash
17
- npm install databonk zod
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
- ## Quick Start
51
+ Peer-required: **Node.js ≥ 18** (or a modern browser with WebAssembly support).
21
52
 
22
- ```javascript
23
- import { DataFrame, SchemaValidator, CommonSchemas } from 'databonk';
53
+ ---
24
54
 
25
- // Create a DataFrame
26
- const df = DataFrame.from({
27
- name: ['Alice', 'Bob', 'Charlie'],
28
- age: [25, 30, 35],
29
- city: ['NYC', 'LA', 'Chicago']
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
- // Basic operations
33
- const adults = df.filter(row => row.age >= 30);
34
- const avgAge = df.column('age').mean();
35
- const grouped = df.groupBy(['city']).agg({ avgAge: 'mean' });
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
- // Schema validation
38
- const result = df.validate(CommonSchemas.person);
39
- console.log(`Valid rows: ${result.validRows}/${result.totalRows}`);
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
- ## Core Features
121
+ ---
43
122
 
44
- ### Data Operations
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
- ### Schema Validation
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
- ### I/O Support
58
- - **CSV**: Read/write CSV files with automatic type inference
59
- - **Apache Arrow**: Optional integration for columnar data exchange
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
- ## Examples
131
+ ### Construction
63
132
 
64
- ### Schema Validation
133
+ ```typescript
134
+ import { init, DataFrame, col, lit } from 'databonk';
65
135
 
66
- ```javascript
67
- import { DataFrame, SchemaValidator } from 'databonk';
68
- import { z } from 'zod';
136
+ await init();
69
137
 
70
- // Define a custom schema
71
- const userSchema = SchemaValidator.define({
72
- name: z.string().min(1),
73
- age: z.number().int().min(0).max(150),
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
- const userData = [
79
- { name: 'Alice', age: 25, email: 'alice@example.com', role: 'admin' },
80
- { name: '', age: -5, email: 'invalid', role: 'unknown' } // Invalid
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
- const df = DataFrame.fromRows(userData);
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
- // Validate data
86
- const validation = df.validate(userSchema);
87
- console.log(`Errors: ${validation.errors.length}`);
161
+ ### Arithmetic, reductions, and precision caveat
88
162
 
89
- // Filter valid rows
90
- const validUsers = df.filterValid(userSchema);
163
+ ```typescript
164
+ import { init, DataFrame, col, lit } from 'databonk';
91
165
 
92
- // Transform data with type coercion
93
- const cleanData = df.validateAndTransform(userSchema);
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
- ### Advanced Data Operations
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
- ```javascript
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
- const products = DataFrame.fromRows([
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
- const joined = sales.join(products, 'product_id', 'inner');
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
- // Group by with multiple aggregations
113
- const summary = joined
114
- .groupBy(['region'])
115
- .agg({
116
- quantity: ['sum', 'mean'],
117
- price: 'mean'
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
- // Add calculated columns
121
- const withRevenue = joined.withColumn('revenue',
122
- row => row.quantity * row.price
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
- // Pivot tables
126
- const pivot = sales.pivot(['region'], 'product_id', 'quantity', 'sum');
253
+ dates.dispose();
254
+ ts.dispose();
255
+ tsWithTz.dispose();
127
256
  ```
128
257
 
129
- ## Docker Development
258
+ ### `dt` accessor proxy
130
259
 
131
- ```bash
132
- # Build and start development environment
133
- make docker-dev
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
- # Run tests in Docker
136
- make docker-test
268
+ const tsData = DataFrame.fromColumns(
269
+ { created: BigInt64Array.from([1717243200000n]) },
270
+ { dtypes: { created: 'timestamp' }, tzs: { created: 'America/Chicago' } },
271
+ );
137
272
 
138
- # Open shell in container
139
- make docker-shell
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
- # Local development
146
- npm install
147
- npm run build
148
- npm test
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
- # With Docker
151
- make setup
152
- make dev
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
- ## Performance
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
- Databonk.js is designed for small to medium datasets (up to ~1M rows) with:
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