jsonstat-io 0.1.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/LICENSE +201 -0
- package/README.md +200 -0
- package/dist/arrow/index.cjs +60 -0
- package/dist/arrow/index.cjs.map +1 -0
- package/dist/arrow/index.d.cts +125 -0
- package/dist/arrow/index.d.ts +125 -0
- package/dist/arrow/index.js +3 -0
- package/dist/arrow/index.js.map +1 -0
- package/dist/arrowToCube-BdRWZ5oG.d.cts +29 -0
- package/dist/arrowToCube-Mjr79ZHM.d.ts +29 -0
- package/dist/chunk-2HY7NRKX.js +259 -0
- package/dist/chunk-2HY7NRKX.js.map +1 -0
- package/dist/chunk-62NIY7ML.js +191 -0
- package/dist/chunk-62NIY7ML.js.map +1 -0
- package/dist/chunk-AXUZF6O5.cjs +352 -0
- package/dist/chunk-AXUZF6O5.cjs.map +1 -0
- package/dist/chunk-EFRDFQOT.js +335 -0
- package/dist/chunk-EFRDFQOT.js.map +1 -0
- package/dist/chunk-GDKDYLZ4.cjs +263 -0
- package/dist/chunk-GDKDYLZ4.cjs.map +1 -0
- package/dist/chunk-IDTQZG4B.js +432 -0
- package/dist/chunk-IDTQZG4B.js.map +1 -0
- package/dist/chunk-KFIKGBIJ.cjs +200 -0
- package/dist/chunk-KFIKGBIJ.cjs.map +1 -0
- package/dist/chunk-L65CRXTV.js +64 -0
- package/dist/chunk-L65CRXTV.js.map +1 -0
- package/dist/chunk-PHLCWTHB.cjs +446 -0
- package/dist/chunk-PHLCWTHB.cjs.map +1 -0
- package/dist/chunk-VQCS4LVD.cjs +166 -0
- package/dist/chunk-VQCS4LVD.cjs.map +1 -0
- package/dist/chunk-VYS7DPB3.js +162 -0
- package/dist/chunk-VYS7DPB3.js.map +1 -0
- package/dist/chunk-ZEDTWRDN.cjs +70 -0
- package/dist/chunk-ZEDTWRDN.cjs.map +1 -0
- package/dist/cli/index.cjs +337 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +90 -0
- package/dist/cli/index.d.ts +90 -0
- package/dist/cli/index.js +333 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cubeBuilder-BEGA4C5Y.js +4 -0
- package/dist/cubeBuilder-BEGA4C5Y.js.map +1 -0
- package/dist/cubeBuilder-ZU76VVEX.cjs +25 -0
- package/dist/cubeBuilder-ZU76VVEX.cjs.map +1 -0
- package/dist/cubeReader-7QVUF3TI.js +4 -0
- package/dist/cubeReader-7QVUF3TI.js.map +1 -0
- package/dist/cubeReader-UN2BHQWK.cjs +21 -0
- package/dist/cubeReader-UN2BHQWK.cjs.map +1 -0
- package/dist/index.cjs +172 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +445 -0
- package/dist/index.d.ts +445 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/ir-BArfNxo-.d.cts +143 -0
- package/dist/ir-Jae2ymov.d.ts +143 -0
- package/dist/jsonstat-BX4mBLci.d.cts +179 -0
- package/dist/jsonstat-BX4mBLci.d.ts +179 -0
- package/dist/sources/csv.cjs +40 -0
- package/dist/sources/csv.cjs.map +1 -0
- package/dist/sources/csv.d.cts +112 -0
- package/dist/sources/csv.d.ts +112 -0
- package/dist/sources/csv.js +3 -0
- package/dist/sources/csv.js.map +1 -0
- package/dist/sources/csvw.cjs +236 -0
- package/dist/sources/csvw.cjs.map +1 -0
- package/dist/sources/csvw.d.cts +143 -0
- package/dist/sources/csvw.d.ts +143 -0
- package/dist/sources/csvw.js +230 -0
- package/dist/sources/csvw.js.map +1 -0
- package/dist/sources/duckdb.cjs +121 -0
- package/dist/sources/duckdb.cjs.map +1 -0
- package/dist/sources/duckdb.d.cts +111 -0
- package/dist/sources/duckdb.d.ts +111 -0
- package/dist/sources/duckdb.js +115 -0
- package/dist/sources/duckdb.js.map +1 -0
- package/dist/sources/parquet.cjs +117 -0
- package/dist/sources/parquet.cjs.map +1 -0
- package/dist/sources/parquet.d.cts +83 -0
- package/dist/sources/parquet.d.ts +83 -0
- package/dist/sources/parquet.js +111 -0
- package/dist/sources/parquet.js.map +1 -0
- package/dist/sources/polars.cjs +94 -0
- package/dist/sources/polars.cjs.map +1 -0
- package/dist/sources/polars.d.cts +91 -0
- package/dist/sources/polars.d.ts +91 -0
- package/dist/sources/polars.js +87 -0
- package/dist/sources/polars.js.map +1 -0
- package/docs/api.md +322 -0
- package/docs/architecture.md +140 -0
- package/docs/cli.md +159 -0
- package/docs/formats/arrow.md +111 -0
- package/docs/formats/csv.md +115 -0
- package/docs/formats/csvw.md +152 -0
- package/docs/formats/duckdb.md +151 -0
- package/docs/formats/parquet.md +132 -0
- package/docs/formats/polars.md +111 -0
- package/docs/mapping.md +136 -0
- package/package.json +119 -0
package/docs/api.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
The main entry `jsonstat-io` re-exports the always-loaded layers: model types, core engine, Arrow hub, sink, and isomorphic utils. Format adapters that need optional peers live behind subpath exports. Every subpath is **bidirectional** — it exports both an import function (`xToCube` / `xToDataset`) and an export function (`cubeToX`).
|
|
4
|
+
|
|
5
|
+
## High-level dispatchers
|
|
6
|
+
|
|
7
|
+
### `importToCube(source, options?)`
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { importToCube } from "jsonstat-io";
|
|
11
|
+
|
|
12
|
+
const obs = await importToCube("./sales.parquet");
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Loads a source (file path, URL, `"-"` for stdin, `Uint8Array`, or `Blob`), auto-detects the format, and converts it to the [`Observations`](./mapping.md#observations-ir) IR. Dispatches to the right source adapter and funnels through the Arrow hub.
|
|
16
|
+
|
|
17
|
+
**Parameters:**
|
|
18
|
+
- `source: string | Uint8Array | Blob` — the input.
|
|
19
|
+
- `options?: ImportOptions` — detection and conversion hints.
|
|
20
|
+
|
|
21
|
+
**Returns:** `Promise<Observations>`
|
|
22
|
+
|
|
23
|
+
### `importToDataset(source, options?)`
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { importToDataset } from "jsonstat-io";
|
|
27
|
+
|
|
28
|
+
const dataset = await importToDataset("./sales.parquet", {
|
|
29
|
+
measure: "amount",
|
|
30
|
+
roles: { time: ["year"], geo: ["country"] },
|
|
31
|
+
build: { valueForm: "sparse" },
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Like `importToCube` but also builds the final [`JsonStatDataset`](./mapping.md) via `buildDataset`.
|
|
36
|
+
|
|
37
|
+
**Returns:** `Promise<JsonStatDataset>`
|
|
38
|
+
|
|
39
|
+
### `ImportOptions`
|
|
40
|
+
|
|
41
|
+
Extends `ArrowToCubeOptions`:
|
|
42
|
+
|
|
43
|
+
| Field | Type | Default | Description |
|
|
44
|
+
|-------------------|-----------------------------------|-------------|-------------|
|
|
45
|
+
| `from` | `SourceFormat \| "auto"` | `"auto"` | Force a format instead of auto-detecting |
|
|
46
|
+
| `build` | `BuildOptions` | — | Passed to `buildDataset` |
|
|
47
|
+
| `csvwMetadata` | `unknown` | — | CSVW metadata object (when `from: "csvw"`) |
|
|
48
|
+
| `delimiter` | `string` | `","` | CSV delimiter |
|
|
49
|
+
| `measure` | `string` | *(detected)*| Measure column name |
|
|
50
|
+
| `dimensions` | `string[]` | *(detected)*| Dimension column names |
|
|
51
|
+
| `status` | `string` | *(detected)*| Status column name |
|
|
52
|
+
| `roles` | `RoleMap` | *(inferred)*| Role assignments |
|
|
53
|
+
| `valueForm` | `"auto" \| "dense" \| "sparse"` | `"auto"` | Value form hint |
|
|
54
|
+
|
|
55
|
+
### `importToCube`
|
|
56
|
+
|
|
57
|
+
Thrown by the dispatchers when a format cannot be detected, a required peer dependency is missing, or the source produces no data.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Export dispatchers
|
|
62
|
+
|
|
63
|
+
### `exportDataset(dataset, options)`
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { exportDataset } from "jsonstat-io";
|
|
67
|
+
|
|
68
|
+
// JSON-stat dataset → Arrow Table
|
|
69
|
+
const table = await exportDataset(dataset, { to: "arrow" });
|
|
70
|
+
|
|
71
|
+
// → Parquet bytes (needs parquet-wasm)
|
|
72
|
+
const bytes = await exportDataset(dataset, { to: "parquet" });
|
|
73
|
+
|
|
74
|
+
// → CSV text
|
|
75
|
+
const csv = await exportDataset(dataset, { to: "csv" });
|
|
76
|
+
|
|
77
|
+
// → CSV text + CSVW metadata object
|
|
78
|
+
const { csv, metadata } = await exportDataset(dataset, { to: "csvw" });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Flattens a JSON-stat dataset to the Observations IR via `readDataset`, then converts to the requested target format. Arrow-native targets (parquet) funnel through `cubeToArrow`; text targets (csv, csvw) serialize directly from the IR.
|
|
82
|
+
|
|
83
|
+
**Parameters:**
|
|
84
|
+
- `dataset: JsonStatDataset` — the input JSON-stat dataset.
|
|
85
|
+
- `options: ExportOptions` — `{ to, ...format-specific options }`.
|
|
86
|
+
|
|
87
|
+
**Returns:** `Promise<ExportResult>` — an Arrow `Table` (`to: "arrow"`), a `Uint8Array` (`to: "parquet"`), a `string` (`to: "csv"`), or a `{ csv, metadata }` object (`to: "csvw"`).
|
|
88
|
+
|
|
89
|
+
### `ExportOptions`
|
|
90
|
+
|
|
91
|
+
| Field | Type | Description |
|
|
92
|
+
|----------|-------------------------------------|-------------|
|
|
93
|
+
| `to` | `ExportTarget` | `"arrow"` \| `"parquet"` \| `"csv"` \| `"csvw"` |
|
|
94
|
+
| *(format-specific)* | varies | Passed through to the target adapter (e.g. `compression` for parquet, `delimiter` for csv) |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Core engine
|
|
99
|
+
|
|
100
|
+
### `buildDataset(obs, options?)`
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { buildDataset } from "jsonstat-io";
|
|
104
|
+
|
|
105
|
+
const { dataset, size, valueForm } = buildDataset(obs, { valueForm: "sparse" });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Converts the `Observations` IR into a `JsonStatDataset`. Resolves dimension categories, scatters values into row-major positions, decides dense/sparse, and emits status.
|
|
109
|
+
|
|
110
|
+
**Returns:** `BuildResult { dataset, size, valueForm }`
|
|
111
|
+
|
|
112
|
+
### `toDataset(obs)`
|
|
113
|
+
|
|
114
|
+
Convenience wrapper: `buildDataset(obs).dataset`.
|
|
115
|
+
|
|
116
|
+
### `readDataset(dataset, options?)`
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { readDataset } from "jsonstat-io";
|
|
120
|
+
|
|
121
|
+
const obs = readDataset(jsonStatDataset, { dropNulls: false });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The inverse of `buildDataset`: flattens a JSON-stat cube back into the `Observations` IR. Powers round-trip tests, the JSON-stat-as-input path, and **all export** operations (`exportDataset` calls `readDataset` first).
|
|
125
|
+
|
|
126
|
+
### `BuildOptions`
|
|
127
|
+
|
|
128
|
+
| Field | Type | Default | Description |
|
|
129
|
+
|--------------------|---------------------------------------------------|---------|-------------|
|
|
130
|
+
| `valueForm` | `"auto" \| "dense" \| "sparse"` | `"auto"`| Override value form |
|
|
131
|
+
| `sparseThreshold` | `number` | `0.5` | Null ratio threshold for auto sparse |
|
|
132
|
+
| `statusForm` | `"auto" \| "array" \| "string" \| "object" \| "none"` | `"auto"`| Status emission form |
|
|
133
|
+
| `meta` | `DatasetMeta` | — | Dataset label/source/updated |
|
|
134
|
+
|
|
135
|
+
### Stride utilities
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { strides, totalCells, flatPosition, multiIndex, enumerateCells } from "jsonstat-io";
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `strides(size: number[]): number[]` — stride of each dimension.
|
|
142
|
+
- `totalCells(size: number[]): number` — product of all sizes.
|
|
143
|
+
- `flatPosition(indices: number[], size: number[]): number` — multi-index → flat position.
|
|
144
|
+
- `multiIndex(pos: number, size: number[]): number[]` — flat position → multi-index.
|
|
145
|
+
- `enumerateCells(size: number[]): number[][]` — all cell addresses.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Arrow hub
|
|
150
|
+
|
|
151
|
+
### `arrowToCube(table, options?)`
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { arrowToCube } from "jsonstat-io";
|
|
155
|
+
import { tableFromIPC } from "apache-arrow";
|
|
156
|
+
|
|
157
|
+
const table = tableFromIPC(bytes);
|
|
158
|
+
const obs = arrowToCube(table, { measure: "amount" });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Converts an Apache Arrow `Table` to the `Observations` IR. Detects dimensions (dictionary columns), measures (numeric columns), and status from the schema, resolving roles from `jsonstat.*` metadata or heuristics.
|
|
162
|
+
|
|
163
|
+
### `arrowToDataset(table, options?)`
|
|
164
|
+
|
|
165
|
+
Async convenience: `buildDataset(arrowToCube(table, options)).dataset`.
|
|
166
|
+
|
|
167
|
+
### `cubeToArrow(obs)`
|
|
168
|
+
|
|
169
|
+
The export hub: converts `Observations` IR → Arrow `Table` with dictionary-encoded dimensions and a Float64 measure, annotating fields with `jsonstat.*` metadata. This is what `exportDataset({ to: "arrow" })`, `cubeToParquet`, `cubeToDuckdb`, and `cubeToPolars` all build upon.
|
|
170
|
+
|
|
171
|
+
### Schema metadata helpers
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { buildFieldMeta, readSchemaMeta, buildSchemaMeta, getFieldRole } from "jsonstat-io";
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- `buildFieldMeta(hints)` — construct `Field.metadata` entries from role/label/category hints.
|
|
178
|
+
- `readSchemaMeta(schema)` — read `jsonstat.*` schema-level metadata.
|
|
179
|
+
- `buildSchemaMeta(hints)` — construct `Schema.metadata` entries.
|
|
180
|
+
- `getFieldRole(field)` — extract the role from a field's metadata.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Source adapters (subpath exports)
|
|
185
|
+
|
|
186
|
+
Each subpath is bidirectional — it exports import (`xToCube` / `xToDataset`) and export (`cubeToX`) functions.
|
|
187
|
+
|
|
188
|
+
### `jsonstat-io/parquet`
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { parquetToCube, parquetToDataset, cubeToParquet } from "jsonstat-io/parquet";
|
|
192
|
+
|
|
193
|
+
const obs = await parquetToCube(bytes, { init: () => parquetWasm.init() });
|
|
194
|
+
|
|
195
|
+
// Export: IR → Arrow → Parquet bytes
|
|
196
|
+
const parquetBytes = await cubeToParquet(obs, { init: () => parquetWasm.init() });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Requires `parquet-wasm`. See [`formats/parquet.md`](./formats/parquet.md).
|
|
200
|
+
|
|
201
|
+
### `jsonstat-io/duckdb`
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { duckdbToCube, openDuckdbNode, cubeToDuckdb } from "jsonstat-io/duckdb";
|
|
205
|
+
|
|
206
|
+
const conn = await openDuckdbNode("./data.duckdb");
|
|
207
|
+
const obs = await duckdbToCube(conn, "SELECT year, country, amount FROM sales");
|
|
208
|
+
|
|
209
|
+
// Export: register an Arrow-backed view (or table) in the connection
|
|
210
|
+
await cubeToDuckdb(conn, obs, { tableName: "sales", mode: "view" });
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Requires `duckdb-async` (Node) or `@duckdb/duckdb-wasm` (browser). See [`formats/duckdb.md`](./formats/duckdb.md).
|
|
214
|
+
|
|
215
|
+
### `jsonstat-io/polars`
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { polarsToCube, loadPolars, cubeToPolars } from "jsonstat-io/polars";
|
|
219
|
+
|
|
220
|
+
const pl = await loadPolars();
|
|
221
|
+
const df = pl.readCSV("data.csv");
|
|
222
|
+
const obs = await polarsToCube(df);
|
|
223
|
+
|
|
224
|
+
// Export: IR → Arrow → Polars DataFrame
|
|
225
|
+
const dfOut = await cubeToPolars(obs);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Requires `nodejs-polars`. Node only. See [`formats/polars.md`](./formats/polars.md).
|
|
229
|
+
|
|
230
|
+
### `jsonstat-io/csvw`
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { csvwToCube, parseCsvwMetadata, cubeToCsvw } from "jsonstat-io/csvw";
|
|
234
|
+
|
|
235
|
+
const meta = parseCsvwMetadata(metadataJson);
|
|
236
|
+
const obs = csvwToCube(csvText, meta, { dimensions: ["year", "country"] });
|
|
237
|
+
|
|
238
|
+
// Export: IR → CSV text + CSVW metadata object
|
|
239
|
+
const { csv, metadata } = cubeToCsvw(obs);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
No dependencies. See [`formats/csvw.md`](./formats/csvw.md).
|
|
243
|
+
|
|
244
|
+
### `jsonstat-io/csv`
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { csvToCube, cubeToCsv } from "jsonstat-io/csv";
|
|
248
|
+
|
|
249
|
+
const obs = csvToCube(csvText, { measure: "amount", delimiter: ";" });
|
|
250
|
+
|
|
251
|
+
// Export: IR → CSV text (dimensions + measure + optional status)
|
|
252
|
+
const csv = cubeToCsv(obs);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
No dependencies. Heuristic measure/dimension inference on import.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Sink
|
|
260
|
+
|
|
261
|
+
### `serialize(dataset, options?)`
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { serialize } from "jsonstat-io";
|
|
265
|
+
|
|
266
|
+
const json = serialize(dataset, { pretty: true, canonicalKeys: true });
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Produces a canonical JSON string. Optionally reorders top-level keys for diff-stable output.
|
|
270
|
+
|
|
271
|
+
### `serializeToBytes(dataset, options?)`
|
|
272
|
+
|
|
273
|
+
Returns `Uint8Array` instead of a string.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Utilities
|
|
278
|
+
|
|
279
|
+
### Format detection
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { detectFormat, detectFromBytes, detectFromExtension } from "jsonstat-io";
|
|
283
|
+
|
|
284
|
+
detectFromBytes(bytes); // "parquet" | "arrow" | "jsonstat" | "unknown"
|
|
285
|
+
detectFromExtension("parquet"); // "parquet"
|
|
286
|
+
detectFormat("data.csv", bytes); // tries extension first, then bytes
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Input loading
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { loadInput } from "jsonstat-io";
|
|
293
|
+
|
|
294
|
+
const { bytes, source } = await loadInput("./sales.parquet"); // Node: reads file
|
|
295
|
+
const { bytes } = await loadInput(blob); // Browser: reads Blob
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Density decision
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { decideDensity } from "jsonstat-io";
|
|
302
|
+
|
|
303
|
+
decideDensity(0.6, 0.5); // { form: "sparse", nullRatio: 0.6, threshold: 0.5 }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Error classes
|
|
309
|
+
|
|
310
|
+
| Class | Thrown by |
|
|
311
|
+
|-----------------------|-----------|
|
|
312
|
+
| `ImporterError` | `importToCube`, `importToDataset`, `exportDataset` |
|
|
313
|
+
| `ArrowConversionError`| `arrowToCube`, `cubeToArrow` |
|
|
314
|
+
| `CubeBuilderError` | `buildDataset` |
|
|
315
|
+
| `CubeReaderError` | `readDataset` |
|
|
316
|
+
| `ParquetSourceError` | `parquetToCube` |
|
|
317
|
+
| `DuckdbSourceError` | `duckdbToCube` |
|
|
318
|
+
| `PolarsSourceError` | `polarsToCube` |
|
|
319
|
+
| `CsvwSourceError` | `csvwToCube` |
|
|
320
|
+
| `CsvSourceError` | `csvToCube` |
|
|
321
|
+
|
|
322
|
+
All extend `Error` and set `this.name` to the class name.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This document explains the layered design of `jsonstat-io` and the rationale behind the two key decisions: the **Arrow hub** and the **Observations IR**. Both import (columnar → JSON-stat) and export (JSON-stat → columnar) are fully supported and share the same IR.
|
|
4
|
+
|
|
5
|
+
## The core problem
|
|
6
|
+
|
|
7
|
+
JSON-stat 2.0 is a *dense or sparse* multidimensional cube model: values are laid out in **row-major order** ("what does not change first") across the cross-product of dimension categories. Every statistical office, every data portal, and increasingly every lakehouse produces *tabular* data instead — Parquet, Arrow, CSV, DataFrames. These two worlds speak different shapes:
|
|
8
|
+
|
|
9
|
+
| JSON-stat cube | Columnar / tabular |
|
|
10
|
+
|-----------------------|-------------------------|
|
|
11
|
+
| N-dimensional array | 2D table (rows × cols) |
|
|
12
|
+
| Row-major value order | Columnar or row order |
|
|
13
|
+
| Dimensions × size | Column names × values |
|
|
14
|
+
| Implicit cell address | Explicit row |
|
|
15
|
+
|
|
16
|
+
`jsonstat-io` bridges this gap in both directions. The challenge is not just *reading* a format — it's **faithfully reconstructing the full JSON-stat model** (roles, labels, units, coordinates, hierarchies, status, sparse/dense) from a source that carries only raw values unless you annotate it, and reversing that reconstruction losslessly on export.
|
|
17
|
+
|
|
18
|
+
## The Arrow-hub insight
|
|
19
|
+
|
|
20
|
+
Apache Arrow is the *de facto* in-memory standard of the columnar data stack. Critically, the major lakehouse engines all emit Arrow tables natively:
|
|
21
|
+
|
|
22
|
+
- **Parquet** → Arrow via `parquet-wasm` (or DuckDB/Polars, which read Parquet)
|
|
23
|
+
- **DuckDB** → Arrow via `.arrow()` / `.arrowResult()`
|
|
24
|
+
- **Polars** → Arrow via `toArrow()` / `toIPC()`
|
|
25
|
+
- **Arrow IPC** → Arrow via `tableFromIPC()` (it already *is* Arrow)
|
|
26
|
+
|
|
27
|
+
This means a single converter pair — `arrowToCube` (import) and `cubeToArrow` (export) — handles four of the five major formats in both directions. Adding a new Arrow-native source is a ~30-line adapter that calls `arrowToCube` on import and `cubeToArrow` on export. The mapping logic (dimension detection, role resolution, value scattering, status emission) lives in exactly one place per direction and is tested once.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
IMPORT
|
|
31
|
+
N sources ──▶ Arrow Table ──▶ arrowToCube ──▶ Observations IR ──▶ buildDataset ──▶ JSON-stat
|
|
32
|
+
(one shape) (one converter) (one IR) (one builder) (one format)
|
|
33
|
+
|
|
34
|
+
EXPORT
|
|
35
|
+
JSON-stat ──▶ readDataset ──▶ Observations IR ──▶ cubeToArrow ──▶ Arrow Table ──▶ N targets
|
|
36
|
+
(one IR) (one converter) (one shape)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
CSVW and plain CSV do not produce Arrow natively in this package, so they build the IR directly on import and serialize from the IR directly on export — but they share the *same* IR type and the *same* builder/reader, so the downstream path is identical.
|
|
40
|
+
|
|
41
|
+
## Layered architecture
|
|
42
|
+
|
|
43
|
+
The package is organized in strict layers with one-way dependencies:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
┌─────────────────────────────────────────────────────────┐
|
|
47
|
+
│ Public API (index.ts) + CLI (cli/) │ ← what consumers call
|
|
48
|
+
├─────────────────────────────────────────────────────────┤
|
|
49
|
+
│ Sources (parquet, duckdb, polars, csvw, csv) │ ← format adapters
|
|
50
|
+
├─────────────────────────────────────────────────────────┤
|
|
51
|
+
│ Arrow hub (arrowToCube, arrowFromCube, schemaMeta) │ ← the hub
|
|
52
|
+
├─────────────────────────────────────────────────────────┤
|
|
53
|
+
│ Core engine (strides, cubeBuilder, cubeReader) │ ← the math
|
|
54
|
+
├─────────────────────────────────────────────────────────┤
|
|
55
|
+
│ Model (jsonstat types, Observations IR) │ ← pure types
|
|
56
|
+
├─────────────────────────────────────────────────────────┤
|
|
57
|
+
│ Sink (serialize) + Utils (detect, fetch, density) │ ← isomorphic helpers
|
|
58
|
+
└─────────────────────────────────────────────────────────┘
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each layer depends only on the layers below it. The model layer is pure TypeScript types with zero runtime cost. The core engine is pure logic with no I/O. The Arrow hub depends on `apache-arrow` (a hard dependency). Source adapters depend on optional peers, imported lazily.
|
|
62
|
+
|
|
63
|
+
### Layer 1: Model (`src/model/`)
|
|
64
|
+
|
|
65
|
+
Two parallel type definitions:
|
|
66
|
+
|
|
67
|
+
- **`jsonstat.ts`** — the JSON-stat 2.0 wire types: `JsonStatDataset`, `JsonStatDimension`, `JsonStatCategory`, `JsonStatRole`, `JsonStatUnit`, etc. Plus type guards (`isDataset`, `isCollection`).
|
|
68
|
+
- **`ir.ts`** — the internal `Observations` IR: a tidy long table with `DimensionColumn[]`, one `MeasureColumn`, an optional `StatusColumn`, a `RoleMap`, `DatasetMeta`, and a `valueForm` hint.
|
|
69
|
+
|
|
70
|
+
No runtime logic here except `observationCount()`. This is the contract every layer agrees on.
|
|
71
|
+
|
|
72
|
+
### Layer 2: Core engine (`src/core/`)
|
|
73
|
+
|
|
74
|
+
The mathematical heart:
|
|
75
|
+
|
|
76
|
+
- **`strides.ts`** — row-major stride arithmetic. `strides(size)` computes the stride of each dimension; `flatPosition(indices, size)` maps a multi-index to a flat array position; `multiIndex` is the inverse; `enumerateCells` iterates all cell addresses.
|
|
77
|
+
- **`cubeBuilder.ts`** — `buildDataset(obs, options)` resolves dimension categories (explicit order or first-seen), scatters the measure values into their row-major positions, decides dense vs sparse, and emits status. This is the "long → wide" pivot.
|
|
78
|
+
- **`cubeReader.ts`** — `readDataset(dataset)` is the inverse: it takes a JSON-stat cube and *flattens* it back into the Observations IR. This powers round-trip tests and the JSON-stat-as-input path.
|
|
79
|
+
|
|
80
|
+
The core has **zero I/O dependencies** — it operates on plain arrays and objects. This is what makes it isomorphic and testable.
|
|
81
|
+
|
|
82
|
+
### Layer 3: Arrow hub (`src/arrow/`)
|
|
83
|
+
|
|
84
|
+
- **`schemaMeta.ts`** — defines the `jsonstat.*` metadata key contract on Arrow `Field` and `Schema` objects. This is how producers annotate Arrow tables with JSON-stat semantics (roles, labels, units, coordinates, hierarchies). Without metadata, `arrowToCube` falls back to heuristics.
|
|
85
|
+
- **`arrowToCube.ts`** — `arrowToCube(table, options)` reads an Arrow `Table`, detects dimensions (dictionary columns), measures (numeric columns), and status, resolves roles from metadata + options, and produces the `Observations` IR. This is the single converter that Parquet/DuckDB/Polars all funnel through.
|
|
86
|
+
- **`arrowFromCube.ts`** — `cubeToArrow(obs)` is the Phase 2 seam: IR → Arrow Table. It builds dictionary-encoded dimension columns and a Float64 measure column, annotating fields with `jsonstat.*` metadata for lossless round-trips.
|
|
87
|
+
|
|
88
|
+
### Layer 4: Source adapters (`src/sources/`)
|
|
89
|
+
|
|
90
|
+
Each adapter is bidirectional — a thin wrapper that, on import, gets bytes (or a live connection/DataFrame) into an Arrow Table then calls `arrowToCube`, and on export, takes the Arrow Table (or IR) from `cubeToArrow` and writes it to the target format:
|
|
91
|
+
|
|
92
|
+
- **`parquet.ts`** — `parquetToCube(bytes)` / `cubeToParquet(obs)` via `parquet-wasm` (lazy import). Browser + Node.
|
|
93
|
+
- **`duckdb.ts`** — `duckdbToCube(conn, query)` / `cubeToDuckdb(conn, obs)` runs SQL on import, registers an Arrow view/table on export. Supports `duckdb-async` (Node) and `@duckdb/duckdb-wasm` (browser).
|
|
94
|
+
- **`polars.ts`** — `polarsToCube(df)` / `cubeToPolars(obs)` via `toArrow()`/`fromArrow()`. Node only.
|
|
95
|
+
- **`csvw.ts`** — `csvwToCube(text, metadata)` / `cubeToCsvw(obs)` uses CSVW metadata for lossless mapping. No deps.
|
|
96
|
+
- **`csv.ts`** — `csvToCube(text)` / `cubeToCsv(obs)` infers measure/dimensions heuristically on import; serializes dimensions+measure+status columns on export. No deps.
|
|
97
|
+
|
|
98
|
+
All optional peer imports are **lazy** (`await import(...)`) so they never enter a bundle that doesn't use them.
|
|
99
|
+
|
|
100
|
+
### Layer 5: Sink + Utils (`src/sink/`, `src/util/`)
|
|
101
|
+
|
|
102
|
+
- **`serialize.ts`** — `serialize(dataset, options)` produces canonical JSON. Optionally reorders top-level keys for diff-stable output. `serializeToBytes` returns `Uint8Array`.
|
|
103
|
+
- **`detect.ts`** — `detectFormat(source, bytes)` sniffs the format from magic bytes (Parquet `PAR1`, Arrow `ARROW1`) and file extension. Used by the high-level dispatchers.
|
|
104
|
+
- **`fetch.ts`** — `loadInput(source)` normalizes file paths, URLs, stdin, `Uint8Array`, and `Blob` into a uniform `LoadedInput { bytes, source }`. Uses `fetch` in the browser, `node:fs` in Node.
|
|
105
|
+
- **`density.ts`** — `decideDensity(nullRatio, threshold)` implements the sparse/dense auto-decision.
|
|
106
|
+
|
|
107
|
+
## The Observations IR
|
|
108
|
+
|
|
109
|
+
The `Observations` IR is the keystone. It is a **tidy long table**:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
interface Observations {
|
|
113
|
+
dimensions: Record<string, DimensionColumn>; // { values: string[] }
|
|
114
|
+
measure: MeasureColumn; // { values: (number|null)[] }
|
|
115
|
+
status?: StatusColumn; // { values: string[] }
|
|
116
|
+
roles?: RoleMap; // { time?: string[], geo?: string[], metric?: string[] }
|
|
117
|
+
meta?: DatasetMeta; // { label?, source?, updated? }
|
|
118
|
+
valueForm?: "auto" | "dense" | "sparse";
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Every source produces this shape. `buildDataset` is the only consumer that pivots it into the row-major cube. This separation means:
|
|
123
|
+
|
|
124
|
+
1. **Adding a source** = producing `Observations`. You never touch the cube math.
|
|
125
|
+
2. **Testing the cube math** = construct `Observations` directly, no file I/O.
|
|
126
|
+
3. **Round-tripping** = `readDataset` (cube → IR) + `buildDataset` (IR → cube).
|
|
127
|
+
|
|
128
|
+
## Bidirectional scope
|
|
129
|
+
|
|
130
|
+
Both directions are fully implemented and tested:
|
|
131
|
+
|
|
132
|
+
- **Import** (columnar → JSON-stat): `arrowToCube` handles Arrow-native sources; `csvToCube` / `csvwToCube` handle text formats; `buildDataset` scatters into the cube. Exposed via [`importToDataset`](./api.md#importtodatasetsource-options).
|
|
133
|
+
- **Export** (JSON-stat → columnar): `readDataset` flattens the cube to the IR; `cubeToArrow` handles Arrow-native targets (Parquet, DuckDB, Polars); `cubeToCsv` / `cubeToCsvw` handle text targets. Exposed via [`exportDataset`](./api.md#exportdatasetdataset-options).
|
|
134
|
+
- **CLI:** the `--to` flag drives direction — `jsonstat` (default) imports; `arrow|parquet|csv|csvw` exports.
|
|
135
|
+
|
|
136
|
+
The same Observations IR sits at the center of both directions, guaranteeing that `export → import` round-trips losslessly.
|
|
137
|
+
|
|
138
|
+
## The Rust/Wasm accelerator seam
|
|
139
|
+
|
|
140
|
+
The performance-critical path is `buildDataset`'s value scattering and `cubeToArrow`'s vector building — the nested-loop stride multiplication. The public API signatures (`buildDataset`, `arrowToCube`, `cubeToArrow`, `exportDataset`) are stable and framework-agnostic. An accelerator can replace the internals behind the same types, yielding a drop-in speedup with no consumer changes.
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# CLI Reference
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
npx jsonstat-io [input] [options]
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Synopsis
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
jsonstat-io [input] [options]
|
|
11
|
+
|
|
12
|
+
Convert between columnar data (Arrow, Parquet, DuckDB, Polars, CSVW, CSV) and JSON-stat 2.0 cubes.
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
input Input file path, URL, or "-" for stdin (default: "-").
|
|
16
|
+
Supports .parquet, .arrow/.ipc, .csv, .csvw, .json/.jsonstat.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
-V, --version Print version and exit.
|
|
20
|
+
-h, --help Print this help and exit.
|
|
21
|
+
|
|
22
|
+
Format:
|
|
23
|
+
-f, --from <format> Source format override. One of:
|
|
24
|
+
auto (default) — sniff from magic bytes + extension
|
|
25
|
+
parquet — needs parquet-wasm
|
|
26
|
+
arrow — Arrow IPC stream/file
|
|
27
|
+
csv — plain CSV (heuristic mapping)
|
|
28
|
+
csvw — CSV with metadata
|
|
29
|
+
jsonstat, json — JSON-stat input (round-trip)
|
|
30
|
+
-t, --to <format> Output format / direction. One of:
|
|
31
|
+
jsonstat (default) — IMPORT: columnar → JSON-stat
|
|
32
|
+
arrow — EXPORT: JSON-stat → Arrow IPC
|
|
33
|
+
parquet — EXPORT: JSON-stat → Parquet (needs parquet-wasm)
|
|
34
|
+
csv — EXPORT: JSON-stat → CSV
|
|
35
|
+
csvw — EXPORT: JSON-stat → CSV + CSVW metadata
|
|
36
|
+
|
|
37
|
+
Column mapping:
|
|
38
|
+
--measure <column> Name of the measure column (overrides detection).
|
|
39
|
+
--dimensions <a,b,c> Comma-separated dimension column names, in order.
|
|
40
|
+
--role <assigns> Role assignments: time=<col>,geo=<col>,metric=<col>
|
|
41
|
+
(comma-separated). Example: --role time=year,geo=country
|
|
42
|
+
--status <column> Name of the status column.
|
|
43
|
+
|
|
44
|
+
Value form:
|
|
45
|
+
--sparse Force sparse (object) value form.
|
|
46
|
+
--dense Force dense (array) value form.
|
|
47
|
+
--auto Auto-decide value form by null ratio (default).
|
|
48
|
+
--threshold <n> Sparse threshold: null ratio 0–1 (default 0.5).
|
|
49
|
+
|
|
50
|
+
Status form:
|
|
51
|
+
--status-form <form> Status emission. One of:
|
|
52
|
+
auto (default) — string if uniform, else array
|
|
53
|
+
array — always per-cell array
|
|
54
|
+
string — always a single string
|
|
55
|
+
object — explicit {position: code}
|
|
56
|
+
none — omit status entirely
|
|
57
|
+
|
|
58
|
+
Dataset metadata:
|
|
59
|
+
--label <text> Dataset label.
|
|
60
|
+
--source <text> Dataset source.
|
|
61
|
+
--updated <date> Dataset last-updated date (ISO 8601).
|
|
62
|
+
|
|
63
|
+
Output:
|
|
64
|
+
-o, --output <file> Write to file instead of stdout.
|
|
65
|
+
--pretty Pretty-print JSON (default: true).
|
|
66
|
+
--no-pretty Compact JSON output (single line).
|
|
67
|
+
--canonical-keys Reorder top-level keys canonically (default: true).
|
|
68
|
+
--no-canonical-keys Preserve insertion key order.
|
|
69
|
+
--validate Validate output with jsonstat-validator (if installed).
|
|
70
|
+
|
|
71
|
+
CSV/CSVW:
|
|
72
|
+
--csvw-metadata <json> Inline CSVW metadata as a JSON string.
|
|
73
|
+
--delimiter <char> CSV delimiter (default: ",").
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Exit codes
|
|
77
|
+
|
|
78
|
+
| Code | Meaning |
|
|
79
|
+
|------|---------|
|
|
80
|
+
| 0 | Success |
|
|
81
|
+
| 1 | Import, validation, or write error |
|
|
82
|
+
| 2 | Invalid CLI arguments |
|
|
83
|
+
|
|
84
|
+
## Examples
|
|
85
|
+
|
|
86
|
+
### IMPORT: Parquet → JSON-stat file
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npx jsonstat-io ./sales.parquet -o sales.jsonstat.json --label "Sales 2024"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### IMPORT: CSV on stdin with explicit roles
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
cat data.csv | npx jsonstat-io - --measure amount --role time=year,geo=country
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### IMPORT: Arrow IPC from a URL, forced sparse
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
npx jsonstat-io https://example.com/data.arrow --sparse --threshold 0.3
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### IMPORT: CSVW with inline metadata
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
npx jsonstat-io ./data.csv --from csvw \
|
|
108
|
+
--csvw-metadata '{"tableSchema":{"columns":[{"titles":"year","datatype":"string"},{"titles":"value","datatype":"decimal"}],"primaryKey":["year"]}}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### IMPORT: JSON-stat round-trip (re-emit with canonical keys)
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
npx jsonstat-io ./input.jsonstat --from jsonstat --canonical-keys -o canonical.jsonstat.json
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### EXPORT: JSON-stat → Parquet
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
npx jsonstat-io ./sales.jsonstat.json --to parquet -o sales.parquet
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### EXPORT: JSON-stat → Arrow IPC
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
npx jsonstat-io ./sales.jsonstat.json --to arrow -o sales.arrow
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### EXPORT: JSON-stat → CSV (+ sibling CSVW metadata)
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
npx jsonstat-io ./sales.jsonstat.json --to csv -o sales.csv
|
|
133
|
+
# writes sales.csv and sales-metadata.json
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### EXPORT: JSON-stat → CSVW (with explicit CSVW metadata)
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
npx jsonstat-io ./sales.jsonstat.json --to csvw -o sales.csvw
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Validate the output
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
npm i -g jsonstat-validator
|
|
146
|
+
npx jsonstat-io ./sales.parquet --validate
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If `jsonstat-validator` is not installed, `--validate` prints a hint instead of failing.
|
|
150
|
+
|
|
151
|
+
## How detection works
|
|
152
|
+
|
|
153
|
+
When `--from` is omitted (or `auto`), the CLI:
|
|
154
|
+
|
|
155
|
+
1. Reads magic bytes: `PAR1` → Parquet, `ARROW1` → Arrow IPC.
|
|
156
|
+
2. Falls back to the file extension: `.parquet`, `.arrow`/`.ipc`, `.csv`, `.json`/`.jsonstat`.
|
|
157
|
+
3. For `.csv`, tries to load a sibling `*-metadata.json` (CSVW convention). If found, uses CSVW; otherwise plain CSV.
|
|
158
|
+
|
|
159
|
+
DuckDB and Polars require a live connection or DataFrame object and are **not** available via the CLI — use the programmatic API ([`docs/formats/duckdb.md`](./formats/duckdb.md), [`docs/formats/polars.md`](./formats/polars.md)).
|