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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { makeVector, Field, Float64, vectorFromArray, Utf8, Schema, Table, Int32, Int64, Float32, Bool, Dictionary, DateDay, DateMillisecond, TimestampSecond, TimestampMillisecond } from 'apache-arrow';
|
|
2
|
+
|
|
3
|
+
// src/arrow/schemaMeta.ts
|
|
4
|
+
var META_PREFIX = "jsonstat.";
|
|
5
|
+
function getFieldMeta(field, key) {
|
|
6
|
+
return field.metadata.get(`${META_PREFIX}${key}`);
|
|
7
|
+
}
|
|
8
|
+
function getFieldMetaJson(field, key) {
|
|
9
|
+
const raw = getFieldMeta(field, key);
|
|
10
|
+
if (raw === void 0) return void 0;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
} catch {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function isMeasureField(field) {
|
|
18
|
+
return getFieldMeta(field, "measure") === "true";
|
|
19
|
+
}
|
|
20
|
+
function isStatusField(field) {
|
|
21
|
+
return getFieldMeta(field, "status") === "true";
|
|
22
|
+
}
|
|
23
|
+
function getFieldRole(field) {
|
|
24
|
+
const r = getFieldMeta(field, "role");
|
|
25
|
+
if (r === "time" || r === "geo" || r === "metric") return r;
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
function buildFieldMeta(hints) {
|
|
29
|
+
const meta = /* @__PURE__ */ new Map();
|
|
30
|
+
if (hints.role) meta.set(`${META_PREFIX}role`, hints.role);
|
|
31
|
+
if (hints.measure) meta.set(`${META_PREFIX}measure`, "true");
|
|
32
|
+
if (hints.status) meta.set(`${META_PREFIX}status`, "true");
|
|
33
|
+
if (hints.label) meta.set(`${META_PREFIX}label`, hints.label);
|
|
34
|
+
if (hints.categoryLabels)
|
|
35
|
+
meta.set(`${META_PREFIX}categoryLabels`, JSON.stringify(hints.categoryLabels));
|
|
36
|
+
if (hints.categoryUnits)
|
|
37
|
+
meta.set(`${META_PREFIX}categoryUnits`, JSON.stringify(hints.categoryUnits));
|
|
38
|
+
if (hints.categoryCoords)
|
|
39
|
+
meta.set(`${META_PREFIX}categoryCoords`, JSON.stringify(hints.categoryCoords));
|
|
40
|
+
if (hints.categoryChild)
|
|
41
|
+
meta.set(`${META_PREFIX}categoryChild`, JSON.stringify(hints.categoryChild));
|
|
42
|
+
if (hints.categoryOrder)
|
|
43
|
+
meta.set(`${META_PREFIX}categoryOrder`, JSON.stringify(hints.categoryOrder));
|
|
44
|
+
return meta;
|
|
45
|
+
}
|
|
46
|
+
function readSchemaMeta(schema) {
|
|
47
|
+
const meta = {};
|
|
48
|
+
const label = schema.metadata.get(`${META_PREFIX}label`);
|
|
49
|
+
const source = schema.metadata.get(`${META_PREFIX}source`);
|
|
50
|
+
const updated = schema.metadata.get(`${META_PREFIX}updated`);
|
|
51
|
+
const extensionRaw = schema.metadata.get(`${META_PREFIX}extension`);
|
|
52
|
+
const valueForm = schema.metadata.get(`${META_PREFIX}valueForm`);
|
|
53
|
+
if (label) meta.label = label;
|
|
54
|
+
if (source) meta.source = source;
|
|
55
|
+
if (updated) meta.updated = updated;
|
|
56
|
+
if (extensionRaw) {
|
|
57
|
+
try {
|
|
58
|
+
meta.extension = JSON.parse(extensionRaw);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let roles;
|
|
63
|
+
const rolesRaw = schema.metadata.get(`${META_PREFIX}roles`);
|
|
64
|
+
if (rolesRaw) {
|
|
65
|
+
try {
|
|
66
|
+
roles = JSON.parse(rolesRaw);
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
meta: Object.keys(meta).length ? meta : void 0,
|
|
72
|
+
roles,
|
|
73
|
+
valueForm: valueForm === "auto" || valueForm === "dense" || valueForm === "sparse" ? valueForm : void 0
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function buildSchemaMeta(hints) {
|
|
77
|
+
const meta = /* @__PURE__ */ new Map();
|
|
78
|
+
if (hints.label) meta.set(`${META_PREFIX}label`, hints.label);
|
|
79
|
+
if (hints.source) meta.set(`${META_PREFIX}source`, hints.source);
|
|
80
|
+
if (hints.updated) meta.set(`${META_PREFIX}updated`, hints.updated);
|
|
81
|
+
if (hints.extension)
|
|
82
|
+
meta.set(`${META_PREFIX}extension`, JSON.stringify(hints.extension));
|
|
83
|
+
if (hints.roles) meta.set(`${META_PREFIX}roles`, JSON.stringify(hints.roles));
|
|
84
|
+
if (hints.valueForm) meta.set(`${META_PREFIX}valueForm`, hints.valueForm);
|
|
85
|
+
return meta;
|
|
86
|
+
}
|
|
87
|
+
var ArrowConversionError = class extends Error {
|
|
88
|
+
constructor(message) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "ArrowConversionError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function isDictionaryType(t) {
|
|
94
|
+
return t instanceof Dictionary;
|
|
95
|
+
}
|
|
96
|
+
function isNumericType(t) {
|
|
97
|
+
return t instanceof Int32 || t instanceof Int64 || t instanceof Float32 || t instanceof Float64;
|
|
98
|
+
}
|
|
99
|
+
function isStringType(t) {
|
|
100
|
+
return t instanceof Utf8;
|
|
101
|
+
}
|
|
102
|
+
function isTemporalType(t) {
|
|
103
|
+
return t instanceof DateDay || t instanceof DateMillisecond || t instanceof TimestampSecond || t instanceof TimestampMillisecond;
|
|
104
|
+
}
|
|
105
|
+
function isDimensionCandidate(t) {
|
|
106
|
+
return isDictionaryType(t) || isStringType(t) || isTemporalType(t) || t instanceof Bool;
|
|
107
|
+
}
|
|
108
|
+
function extractDimension(name, vec, field, rowCount) {
|
|
109
|
+
const values = new Array(rowCount);
|
|
110
|
+
if (isDictionaryType(vec.type)) {
|
|
111
|
+
for (let i = 0; i < rowCount; i++) {
|
|
112
|
+
const v = vec.get(i);
|
|
113
|
+
if (v === null) {
|
|
114
|
+
throw new ArrowConversionError(
|
|
115
|
+
`Dimension "${name}": null value at row ${i}; dimensions cannot be null`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
values[i] = String(v);
|
|
119
|
+
}
|
|
120
|
+
} else if (isStringType(vec.type)) {
|
|
121
|
+
for (let i = 0; i < rowCount; i++) {
|
|
122
|
+
const v = vec.get(i);
|
|
123
|
+
if (v === null) {
|
|
124
|
+
throw new ArrowConversionError(
|
|
125
|
+
`Dimension "${name}": null value at row ${i}; dimensions cannot be null`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
values[i] = String(v);
|
|
129
|
+
}
|
|
130
|
+
} else if (isTemporalType(vec.type)) {
|
|
131
|
+
for (let i = 0; i < rowCount; i++) {
|
|
132
|
+
const v = vec.get(i);
|
|
133
|
+
if (v === null) {
|
|
134
|
+
throw new ArrowConversionError(
|
|
135
|
+
`Dimension "${name}": null value at row ${i}; dimensions cannot be null`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const d = v instanceof Date ? v : new Date(v);
|
|
139
|
+
values[i] = d.toISOString().slice(0, 10);
|
|
140
|
+
}
|
|
141
|
+
} else if (vec.type instanceof Bool) {
|
|
142
|
+
for (let i = 0; i < rowCount; i++) {
|
|
143
|
+
const v = vec.get(i);
|
|
144
|
+
values[i] = v === null ? "" : v ? "true" : "false";
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
for (let i = 0; i < rowCount; i++) {
|
|
148
|
+
const v = vec.get(i);
|
|
149
|
+
values[i] = v === null ? "" : String(v);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const categoryLabels = getFieldMetaJson(field, "categoryLabels");
|
|
153
|
+
const categoryUnits = getFieldMetaJson(field, "categoryUnits");
|
|
154
|
+
const categoryCoords = getFieldMetaJson(field, "categoryCoords");
|
|
155
|
+
const categoryChild = getFieldMetaJson(field, "categoryChild");
|
|
156
|
+
const categoryOrder = getFieldMetaJson(field, "categoryOrder");
|
|
157
|
+
const label = field.metadata.get("jsonstat.label") ?? void 0;
|
|
158
|
+
return {
|
|
159
|
+
id: name,
|
|
160
|
+
values,
|
|
161
|
+
categoryOrder,
|
|
162
|
+
categoryLabels,
|
|
163
|
+
categoryUnits,
|
|
164
|
+
categoryCoordinates: categoryCoords,
|
|
165
|
+
categoryChild,
|
|
166
|
+
label,
|
|
167
|
+
href: field.metadata.get("jsonstat.href") ?? void 0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function extractMeasure(name, vec, rowCount) {
|
|
171
|
+
const values = new Array(rowCount);
|
|
172
|
+
for (let i = 0; i < rowCount; i++) {
|
|
173
|
+
const v = vec.get(i);
|
|
174
|
+
values[i] = v === null ? null : Number(v);
|
|
175
|
+
}
|
|
176
|
+
return { name, values };
|
|
177
|
+
}
|
|
178
|
+
function extractStatus(vec, rowCount) {
|
|
179
|
+
const values = new Array(rowCount);
|
|
180
|
+
for (let i = 0; i < rowCount; i++) {
|
|
181
|
+
const v = vec.get(i);
|
|
182
|
+
values[i] = v === null ? "" : String(v);
|
|
183
|
+
}
|
|
184
|
+
return { values };
|
|
185
|
+
}
|
|
186
|
+
function resolveRoles(table, dimensionNames, options) {
|
|
187
|
+
const roles = {};
|
|
188
|
+
const add = (role, id) => {
|
|
189
|
+
(roles[role] ?? (roles[role] = [])).push(id);
|
|
190
|
+
};
|
|
191
|
+
for (const name of dimensionNames) {
|
|
192
|
+
const field = table.schema.fields.find((f) => f.name === name);
|
|
193
|
+
if (!field) continue;
|
|
194
|
+
const r = getFieldRole(field);
|
|
195
|
+
if (r) add(r, name);
|
|
196
|
+
}
|
|
197
|
+
const schemaRoles = readSchemaMeta(table.schema).roles;
|
|
198
|
+
if (schemaRoles) {
|
|
199
|
+
for (const role of ["time", "geo", "metric"]) {
|
|
200
|
+
for (const id of schemaRoles[role] ?? []) {
|
|
201
|
+
if (!(roles[role] ?? []).includes(id)) add(role, id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!roles.time || roles.time.length === 0) {
|
|
206
|
+
for (const name of dimensionNames) {
|
|
207
|
+
const field = table.schema.fields.find((f) => f.name === name);
|
|
208
|
+
if (field && isTemporalType(field.type)) {
|
|
209
|
+
add("time", name);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (options.roles) {
|
|
215
|
+
for (const role of ["time", "geo", "metric"]) {
|
|
216
|
+
for (const id of options.roles[role] ?? []) {
|
|
217
|
+
if (!(roles[role] ?? []).includes(id)) add(role, id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return roles;
|
|
222
|
+
}
|
|
223
|
+
function arrowToCube(table, options = {}) {
|
|
224
|
+
const rowCount = table.numRows;
|
|
225
|
+
if (rowCount === 0) {
|
|
226
|
+
throw new ArrowConversionError("Arrow table has no rows");
|
|
227
|
+
}
|
|
228
|
+
const fieldNames = table.schema.fields.map((f) => f.name);
|
|
229
|
+
if (fieldNames.length === 0) {
|
|
230
|
+
throw new ArrowConversionError("Arrow table has no columns");
|
|
231
|
+
}
|
|
232
|
+
const measureCol = options.measure ?? table.schema.fields.find((f) => isMeasureField(f))?.name ?? table.schema.fields.find((f) => isNumericType(f.type))?.name ?? (() => {
|
|
233
|
+
throw new ArrowConversionError(
|
|
234
|
+
"No measure column found: no numeric column and no jsonstat.measure marker. Pass options.measure to specify one."
|
|
235
|
+
);
|
|
236
|
+
})();
|
|
237
|
+
const measureVec = table.getChild(measureCol);
|
|
238
|
+
if (!measureVec) {
|
|
239
|
+
throw new ArrowConversionError(`Measure column "${measureCol}" not found in table`);
|
|
240
|
+
}
|
|
241
|
+
const measure = extractMeasure(measureCol, measureVec, rowCount);
|
|
242
|
+
let status;
|
|
243
|
+
let statusName = options.status;
|
|
244
|
+
if (!statusName) {
|
|
245
|
+
const marked = table.schema.fields.find((f) => isStatusField(f));
|
|
246
|
+
if (marked) statusName = marked.name;
|
|
247
|
+
}
|
|
248
|
+
if (statusName) {
|
|
249
|
+
const vec = table.getChild(statusName);
|
|
250
|
+
if (!vec) {
|
|
251
|
+
throw new ArrowConversionError(`Status column "${statusName}" not found`);
|
|
252
|
+
}
|
|
253
|
+
status = extractStatus(vec, rowCount);
|
|
254
|
+
}
|
|
255
|
+
let dimensionNames;
|
|
256
|
+
if (options.dimensions) {
|
|
257
|
+
dimensionNames = options.dimensions;
|
|
258
|
+
for (const name of dimensionNames) {
|
|
259
|
+
if (!fieldNames.includes(name)) {
|
|
260
|
+
throw new ArrowConversionError(`Dimension column "${name}" not found`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
dimensionNames = table.schema.fields.filter(
|
|
265
|
+
(f) => f.name !== measureCol && f.name !== statusName && isDimensionCandidate(f.type)
|
|
266
|
+
).map((f) => f.name);
|
|
267
|
+
if (dimensionNames.length === 0) {
|
|
268
|
+
throw new ArrowConversionError(
|
|
269
|
+
"No dimension columns found (need dictionary/string/temporal/bool columns distinct from the measure). Pass options.dimensions to specify them."
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const dimensions = {};
|
|
274
|
+
for (const name of dimensionNames) {
|
|
275
|
+
const vec = table.getChild(name);
|
|
276
|
+
if (!vec) {
|
|
277
|
+
throw new ArrowConversionError(`Dimension column "${name}" not found`);
|
|
278
|
+
}
|
|
279
|
+
const field = table.schema.fields.find((f) => f.name === name);
|
|
280
|
+
if (!field) {
|
|
281
|
+
throw new ArrowConversionError(`Dimension field "${name}" not found in schema`);
|
|
282
|
+
}
|
|
283
|
+
dimensions[name] = extractDimension(name, vec, field, rowCount);
|
|
284
|
+
}
|
|
285
|
+
const roles = resolveRoles(table, dimensionNames, options);
|
|
286
|
+
const schemaMeta = readSchemaMeta(table.schema);
|
|
287
|
+
const observations = {
|
|
288
|
+
dimensions,
|
|
289
|
+
measure,
|
|
290
|
+
status,
|
|
291
|
+
model: {
|
|
292
|
+
dimensionIds: dimensionNames.slice(),
|
|
293
|
+
roles,
|
|
294
|
+
meta: schemaMeta.meta,
|
|
295
|
+
valueForm: options.valueForm ?? schemaMeta.valueForm ?? "auto",
|
|
296
|
+
statusForm: status ? "auto" : "none"
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
return observations;
|
|
300
|
+
}
|
|
301
|
+
async function arrowToDataset(table, options) {
|
|
302
|
+
const { toDataset } = await import('./cubeBuilder-BEGA4C5Y.js');
|
|
303
|
+
return toDataset(arrowToCube(table, options));
|
|
304
|
+
}
|
|
305
|
+
function buildDimensionColumn(col) {
|
|
306
|
+
const order = resolveCategoryOrder(col);
|
|
307
|
+
const vector = vectorFromArray(
|
|
308
|
+
col.values,
|
|
309
|
+
new Dictionary(new Utf8(), new Int32())
|
|
310
|
+
);
|
|
311
|
+
const metadata = buildFieldMeta({
|
|
312
|
+
label: col.label,
|
|
313
|
+
categoryLabels: col.categoryLabels,
|
|
314
|
+
categoryUnits: col.categoryUnits,
|
|
315
|
+
categoryCoords: col.categoryCoordinates,
|
|
316
|
+
categoryChild: col.categoryChild,
|
|
317
|
+
categoryOrder: col.categoryOrder ?? (order.length > 0 ? order : void 0)
|
|
318
|
+
});
|
|
319
|
+
const field = new Field(
|
|
320
|
+
col.id,
|
|
321
|
+
new Dictionary(new Utf8(), new Int32()),
|
|
322
|
+
true,
|
|
323
|
+
metadata
|
|
324
|
+
);
|
|
325
|
+
return { name: col.id, vector, field };
|
|
326
|
+
}
|
|
327
|
+
function resolveCategoryOrder(col) {
|
|
328
|
+
if (col.categoryOrder && col.categoryOrder.length > 0) {
|
|
329
|
+
return col.categoryOrder.slice();
|
|
330
|
+
}
|
|
331
|
+
const seen = /* @__PURE__ */ new Set();
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const v of col.values) {
|
|
334
|
+
if (!seen.has(v)) {
|
|
335
|
+
seen.add(v);
|
|
336
|
+
out.push(v);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
function cubeToArrow(obs) {
|
|
342
|
+
const n = obs.measure.values.length;
|
|
343
|
+
const dimIds = obs.model.dimensionIds;
|
|
344
|
+
const fields = [];
|
|
345
|
+
const columns = {};
|
|
346
|
+
for (const id of dimIds) {
|
|
347
|
+
const col = obs.dimensions[id];
|
|
348
|
+
if (!col) {
|
|
349
|
+
throw new Error(`arrowFromCube: missing dimension column "${id}"`);
|
|
350
|
+
}
|
|
351
|
+
const built = buildDimensionColumn(col);
|
|
352
|
+
const role = roleFor(obs, id);
|
|
353
|
+
if (role) {
|
|
354
|
+
built.field.metadata.set("jsonstat.role", role);
|
|
355
|
+
}
|
|
356
|
+
fields.push(built.field);
|
|
357
|
+
columns[built.name] = built.vector;
|
|
358
|
+
}
|
|
359
|
+
const measureValues = new Float64Array(n);
|
|
360
|
+
let nullCount = 0;
|
|
361
|
+
for (let i = 0; i < n; i++) {
|
|
362
|
+
const v = obs.measure.values[i];
|
|
363
|
+
if (v === null) {
|
|
364
|
+
nullCount++;
|
|
365
|
+
} else {
|
|
366
|
+
measureValues[i] = v;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const measureVector = makeVector(measureValues);
|
|
370
|
+
const measureName = obs.measure.name ?? "value";
|
|
371
|
+
const measureMeta = buildFieldMeta({ measure: true });
|
|
372
|
+
const measureField = new Field(measureName, new Float64(), true, measureMeta);
|
|
373
|
+
fields.push(measureField);
|
|
374
|
+
columns[measureName] = measureVector;
|
|
375
|
+
if (nullCount > 0) {
|
|
376
|
+
columns[measureName] = buildNullableFloat64Vector(obs.measure.values);
|
|
377
|
+
}
|
|
378
|
+
if (obs.status) {
|
|
379
|
+
const statusName = "status";
|
|
380
|
+
const statusVector = vectorFromArray(
|
|
381
|
+
obs.status.values.map((s) => s === "" ? null : s),
|
|
382
|
+
new Utf8()
|
|
383
|
+
);
|
|
384
|
+
const statusMeta = buildFieldMeta({ status: true });
|
|
385
|
+
const statusField = new Field(statusName, new Utf8(), true, statusMeta);
|
|
386
|
+
fields.push(statusField);
|
|
387
|
+
columns[statusName] = statusVector;
|
|
388
|
+
}
|
|
389
|
+
const schemaMeta = buildSchemaMeta({
|
|
390
|
+
label: obs.model.meta?.label,
|
|
391
|
+
source: obs.model.meta?.source,
|
|
392
|
+
updated: obs.model.meta?.updated,
|
|
393
|
+
extension: obs.model.meta?.extension,
|
|
394
|
+
roles: obs.model.roles,
|
|
395
|
+
valueForm: obs.model.valueForm
|
|
396
|
+
});
|
|
397
|
+
const schema = new Schema(fields, schemaMeta);
|
|
398
|
+
return new Table(schema, columns);
|
|
399
|
+
}
|
|
400
|
+
function buildNullableFloat64Vector(values) {
|
|
401
|
+
const n = values.length;
|
|
402
|
+
const data = new Float64Array(n);
|
|
403
|
+
const nullCount = values.filter((v) => v === null).length;
|
|
404
|
+
const numBytes = Math.ceil(n / 8);
|
|
405
|
+
const nullBitmap = new Uint8Array(numBytes);
|
|
406
|
+
for (let i = 0; i < n; i++) {
|
|
407
|
+
if (values[i] !== null) {
|
|
408
|
+
data[i] = values[i];
|
|
409
|
+
nullBitmap[Math.floor(i / 8)] |= 1 << i % 8;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const vector = makeVector({
|
|
413
|
+
type: new Float64(),
|
|
414
|
+
data,
|
|
415
|
+
nullBitmap,
|
|
416
|
+
length: n,
|
|
417
|
+
nullCount
|
|
418
|
+
});
|
|
419
|
+
return vector;
|
|
420
|
+
}
|
|
421
|
+
function roleFor(obs, id) {
|
|
422
|
+
const roles = obs.model.roles;
|
|
423
|
+
if (!roles) return void 0;
|
|
424
|
+
if (roles.time?.includes(id)) return "time";
|
|
425
|
+
if (roles.geo?.includes(id)) return "geo";
|
|
426
|
+
if (roles.metric?.includes(id)) return "metric";
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export { ArrowConversionError, META_PREFIX, arrowToCube, arrowToDataset, buildFieldMeta, buildSchemaMeta, cubeToArrow, getFieldMeta, getFieldMetaJson, getFieldRole, isMeasureField, isStatusField, readSchemaMeta };
|
|
431
|
+
//# sourceMappingURL=chunk-IDTQZG4B.js.map
|
|
432
|
+
//# sourceMappingURL=chunk-IDTQZG4B.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/arrow/schemaMeta.ts","../src/arrow/arrowToCube.ts","../src/arrow/arrowFromCube.ts"],"names":["Dictionary","Utf8","Int32","Field","Float64","Table"],"mappings":"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","file":"chunk-IDTQZG4B.js","sourcesContent":["/**\n * Arrow schema metadata contract for JSON-stat.\n *\n * Producers (Parquet/DuckDB/Polars/CSVW adapters, or hand-built Arrow tables)\n * annotate Arrow [`Field`](https://arrow.apache.org/docs/js/) metadata with\n * `jsonstat.*` keys so that [`arrowToCube`](./arrowToCube.ts) can reconstruct\n * the full JSON-stat model — not just the raw values. Without these keys the\n * converter falls back to heuristic inference (dictionary columns = dimensions,\n * first numeric column = measure); with them, the mapping is exact and\n * lossless, which is what the round-trip tests require.\n *\n * ## Metadata key reference\n *\n * Field-level keys (on `Field.metadata`):\n *\n * | Key | Value | Meaning |\n * |------------------------------|----------------------------------------|---------|\n * | `jsonstat.role` | `\"time\"` / `\"geo\"` / `\"metric\"` | Dimension role |\n * | `jsonstat.measure` | `\"true\"` | Marks this column as the measure (overrides type heuristics) |\n * | `jsonstat.status` | `\"true\"` | Marks this column as the per-row status column |\n * | `jsonstat.label` | string | Dimension label |\n * | `jsonstat.categoryLabels` | JSON object: `{id: label}` | Category labels |\n * | `jsonstat.categoryUnits` | JSON object: `{id: {decimals,...}}` | Per-category unit metadata (metric role) |\n * | `jsonstat.categoryCoords` | JSON object: `{id: [lon,lat]}` | Per-category coordinates (geo role) |\n * | `jsonstat.categoryChild` | JSON object: `{parent: [child,...]}` | Hierarchy |\n * | `jsonstat.categoryOrder` | JSON array: `[id,...]` | Explicit category ordering (e.g. chronological) |\n *\n * Schema-level keys (on `Schema.metadata`):\n *\n * | Key | Value | Meaning |\n * |------------------------------|----------------------------------------|---------|\n * | `jsonstat.label` | string | Dataset label |\n * | `jsonstat.source` | string | Dataset source |\n * | `jsonstat.updated` | ISO 8601 string | Dataset update time |\n * | `jsonstat.extension` | JSON object | Provider extension data |\n * | `jsonstat.valueForm` | `\"auto\"`/`\"dense\"`/`\"sparse\"` | Value emission hint |\n *\n * See [docs/mapping.md](../../docs/mapping.md) for the full fidelity table.\n */\n\nimport type { Field, Schema } from \"apache-arrow\";\nimport type {\n DatasetMeta,\n RoleMap,\n} from \"../model/ir\";\nimport type { JsonStatRole } from \"../model/jsonstat\";\n\n/** Metadata namespace prefix used for all JSON-stat keys. */\nexport const META_PREFIX = \"jsonstat.\";\n\n// ---------------------------------------------------------------------------\n// Field metadata: reading\n// ---------------------------------------------------------------------------\n\n/** Read a single string metadata value from a field. */\nexport function getFieldMeta(field: Field, key: string): string | undefined {\n return field.metadata.get(`${META_PREFIX}${key}`);\n}\n\n/** Read and JSON-parse a field metadata value, with a fallback. */\nexport function getFieldMetaJson<T>(field: Field, key: string): T | undefined {\n const raw = getFieldMeta(field, key);\n if (raw === undefined) return undefined;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return undefined;\n }\n}\n\n/** Is this field marked as the measure column? */\nexport function isMeasureField(field: Field): boolean {\n return getFieldMeta(field, \"measure\") === \"true\";\n}\n\n/** Is this field marked as the status column? */\nexport function isStatusField(field: Field): boolean {\n return getFieldMeta(field, \"status\") === \"true\";\n}\n\n/** Read the dimension role assigned to a field, if any. */\nexport function getFieldRole(field: Field): \"time\" | \"geo\" | \"metric\" | undefined {\n const r = getFieldMeta(field, \"role\");\n if (r === \"time\" || r === \"geo\" || r === \"metric\") return r;\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Field metadata: writing\n// ---------------------------------------------------------------------------\n\n/** Build a metadata Map for a field from JSON-stat-oriented hints. */\nexport function buildFieldMeta(hints: {\n role?: \"time\" | \"geo\" | \"metric\";\n measure?: boolean;\n status?: boolean;\n label?: string;\n categoryLabels?: Record<string, string>;\n categoryUnits?: Record<string, unknown>;\n categoryCoords?: Record<string, [number, number]>;\n categoryChild?: Record<string, string[]>;\n categoryOrder?: string[];\n}): Map<string, string> {\n const meta = new Map<string, string>();\n if (hints.role) meta.set(`${META_PREFIX}role`, hints.role);\n if (hints.measure) meta.set(`${META_PREFIX}measure`, \"true\");\n if (hints.status) meta.set(`${META_PREFIX}status`, \"true\");\n if (hints.label) meta.set(`${META_PREFIX}label`, hints.label);\n if (hints.categoryLabels)\n meta.set(`${META_PREFIX}categoryLabels`, JSON.stringify(hints.categoryLabels));\n if (hints.categoryUnits)\n meta.set(`${META_PREFIX}categoryUnits`, JSON.stringify(hints.categoryUnits));\n if (hints.categoryCoords)\n meta.set(`${META_PREFIX}categoryCoords`, JSON.stringify(hints.categoryCoords));\n if (hints.categoryChild)\n meta.set(`${META_PREFIX}categoryChild`, JSON.stringify(hints.categoryChild));\n if (hints.categoryOrder)\n meta.set(`${META_PREFIX}categoryOrder`, JSON.stringify(hints.categoryOrder));\n return meta;\n}\n\n// ---------------------------------------------------------------------------\n// Schema metadata: reading\n// ---------------------------------------------------------------------------\n\n/** Read dataset-level metadata from a schema. */\nexport function readSchemaMeta(schema: Schema): {\n meta?: DatasetMeta;\n roles?: RoleMap;\n valueForm?: \"auto\" | \"dense\" | \"sparse\";\n} {\n const meta: DatasetMeta = {};\n const label = schema.metadata.get(`${META_PREFIX}label`);\n const source = schema.metadata.get(`${META_PREFIX}source`);\n const updated = schema.metadata.get(`${META_PREFIX}updated`);\n const extensionRaw = schema.metadata.get(`${META_PREFIX}extension`);\n const valueForm = schema.metadata.get(`${META_PREFIX}valueForm`);\n if (label) meta.label = label;\n if (source) meta.source = source;\n if (updated) meta.updated = updated;\n if (extensionRaw) {\n try {\n meta.extension = JSON.parse(extensionRaw) as Record<string, unknown>;\n } catch {\n /* ignore malformed extension */\n }\n }\n\n // Roles are also expressible at the schema level as a single JSON object\n // (useful when a producer wants to declare roles without per-field metadata).\n let roles: RoleMap | undefined;\n const rolesRaw = schema.metadata.get(`${META_PREFIX}roles`);\n if (rolesRaw) {\n try {\n roles = JSON.parse(rolesRaw) as JsonStatRole;\n } catch {\n /* ignore */\n }\n }\n\n return {\n meta: Object.keys(meta).length ? meta : undefined,\n roles,\n valueForm:\n valueForm === \"auto\" || valueForm === \"dense\" || valueForm === \"sparse\"\n ? valueForm\n : undefined,\n };\n}\n\n/** Build a schema-level metadata Map from dataset hints. */\nexport function buildSchemaMeta(hints: {\n label?: string;\n source?: string;\n updated?: string;\n extension?: Record<string, unknown>;\n roles?: RoleMap;\n valueForm?: \"auto\" | \"dense\" | \"sparse\";\n}): Map<string, string> {\n const meta = new Map<string, string>();\n if (hints.label) meta.set(`${META_PREFIX}label`, hints.label);\n if (hints.source) meta.set(`${META_PREFIX}source`, hints.source);\n if (hints.updated) meta.set(`${META_PREFIX}updated`, hints.updated);\n if (hints.extension)\n meta.set(`${META_PREFIX}extension`, JSON.stringify(hints.extension));\n if (hints.roles) meta.set(`${META_PREFIX}roles`, JSON.stringify(hints.roles));\n if (hints.valueForm) meta.set(`${META_PREFIX}valueForm`, hints.valueForm);\n return meta;\n}\n","/**\n * arrowToCube — the Arrow hub converter.\n *\n * Converts an Apache Arrow [`Table`](https://arrow.apache.org/docs/js/) into the\n * [`Observations`](../model/ir.ts) IR. Because Parquet, DuckDB, and Polars can\n * all emit Arrow tables natively, this single converter serves three sources —\n * the core leverage point of the architecture (see\n * [docs/architecture.md](../../docs/architecture.md) §\"The Arrow-hub insight\").\n *\n * ## Mapping rules\n *\n * 1. **Dimensions** = dictionary-encoded columns (or string columns). Their\n * dictionary values become JSON-stat categories. The per-field\n * `jsonstat.*` metadata (see [`schemaMeta`](./schemaMeta.ts)) supplies\n * labels, units, coordinates, hierarchies, ordering, and roles.\n * 2. **Measure** = the first non-dictionary numeric column, *or* the column\n * marked `jsonstat.measure=true`. If a `--measure` hint is supplied it\n * overrides detection.\n * 3. **Status** = the column marked `jsonstat.status=true`, if any.\n *\n * When metadata is absent, the converter infers conservatively and the result\n * is still a valid (if minimally-annotated) JSON-stat dataset.\n */\n\nimport {\n DataType,\n Field,\n Table,\n Vector,\n Utf8,\n Dictionary,\n Int32,\n Int64,\n Float64,\n Float32,\n Bool,\n DateDay,\n DateMillisecond,\n TimestampSecond,\n TimestampMillisecond,\n} from \"apache-arrow\";\nimport type {\n DimensionColumn,\n MeasureColumn,\n Observations,\n RoleMap,\n StatusColumn,\n} from \"../model/ir\";\nimport {\n getFieldMetaJson,\n getFieldRole,\n isMeasureField,\n isStatusField,\n readSchemaMeta,\n} from \"./schemaMeta\";\nimport type { JsonStatUnit, Coordinates } from \"../model/jsonstat\";\n\n// ---------------------------------------------------------------------------\n// Errors & options\n// ---------------------------------------------------------------------------\n\nexport class ArrowConversionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ArrowConversionError\";\n }\n}\n\nexport interface ArrowToCubeOptions {\n /** Explicit measure column name. Overrides detection + metadata. */\n measure?: string;\n /** Explicit dimension column names, in order. Overrides detection. */\n dimensions?: string[];\n /** Explicit status column name. Overrides metadata. */\n status?: string;\n /** Explicit role assignments (merge with, and take precedence over, metadata). */\n roles?: RoleMap;\n /** Value-form hint forwarded to the cube model. */\n valueForm?: \"auto\" | \"dense\" | \"sparse\";\n}\n\n// ---------------------------------------------------------------------------\n// Type predicates\n// ---------------------------------------------------------------------------\n\n/** Is this Arrow data type a dictionary (the natural dimension representation)? */\nfunction isDictionaryType(t: DataType): t is Dictionary<any, any> {\n return t instanceof Dictionary;\n}\n\n/** Is this Arrow data type numeric (suitable for a measure)? */\nfunction isNumericType(t: DataType): boolean {\n return (\n t instanceof Int32 ||\n t instanceof Int64 ||\n t instanceof Float32 ||\n t instanceof Float64\n );\n}\n\n/** Is this Arrow data type a string/utf8 (a fallback dimension type)? */\nfunction isStringType(t: DataType): boolean {\n return t instanceof Utf8;\n}\n\n/** Is this Arrow data type a temporal type (a time-dimension candidate)? */\nfunction isTemporalType(t: DataType): boolean {\n return (\n t instanceof DateDay ||\n t instanceof DateMillisecond ||\n t instanceof TimestampSecond ||\n t instanceof TimestampMillisecond\n );\n}\n\n/** Is this column a dimension candidate (dictionary, string, temporal, or bool)? */\nfunction isDimensionCandidate(t: DataType): boolean {\n return isDictionaryType(t) || isStringType(t) || isTemporalType(t) || t instanceof Bool;\n}\n\n// ---------------------------------------------------------------------------\n// Column extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract a dimension column from an Arrow vector. Reads the dictionary values\n * (for dictionary-encoded columns) or the raw string values (for utf8), plus\n * any `jsonstat.*` metadata.\n */\nfunction extractDimension(\n name: string,\n vec: Vector,\n field: Field,\n rowCount: number,\n): DimensionColumn {\n const values: string[] = new Array(rowCount);\n\n // Dictionary-encoded: Vector.get(i) returns the decoded dictionary value\n // in Arrow v17, so we can treat dictionary columns the same as strings.\n if (isDictionaryType(vec.type)) {\n for (let i = 0; i < rowCount; i++) {\n const v = vec.get(i);\n if (v === null) {\n throw new ArrowConversionError(\n `Dimension \"${name}\": null value at row ${i}; dimensions cannot be null`,\n );\n }\n values[i] = String(v);\n }\n } else if (isStringType(vec.type)) {\n for (let i = 0; i < rowCount; i++) {\n const v = vec.get(i);\n if (v === null) {\n throw new ArrowConversionError(\n `Dimension \"${name}\": null value at row ${i}; dimensions cannot be null`,\n );\n }\n values[i] = String(v);\n }\n } else if (isTemporalType(vec.type)) {\n // Normalize temporal values to ISO strings for stable category IDs.\n for (let i = 0; i < rowCount; i++) {\n const v = vec.get(i);\n if (v === null) {\n throw new ArrowConversionError(\n `Dimension \"${name}\": null value at row ${i}; dimensions cannot be null`,\n );\n }\n // DateDay/DateMillisecond return JS Date or epoch; normalize.\n const d = v instanceof Date ? v : new Date(v);\n values[i] = d.toISOString().slice(0, 10); // YYYY-MM-DD\n }\n } else if (vec.type instanceof Bool) {\n for (let i = 0; i < rowCount; i++) {\n const v = vec.get(i);\n values[i] = v === null ? \"\" : v ? \"true\" : \"false\";\n }\n } else {\n // Coerce any other type to string.\n for (let i = 0; i < rowCount; i++) {\n const v = vec.get(i);\n values[i] = v === null ? \"\" : String(v);\n }\n }\n\n // Pull JSON-stat metadata from the field (passed from the table schema).\n const categoryLabels = getFieldMetaJson<Record<string, string>>(field, \"categoryLabels\");\n const categoryUnits = getFieldMetaJson<Record<string, JsonStatUnit>>(field, \"categoryUnits\");\n const categoryCoords = getFieldMetaJson<Record<string, Coordinates>>(field, \"categoryCoords\");\n const categoryChild = getFieldMetaJson<Record<string, string[]>>(field, \"categoryChild\");\n const categoryOrder = getFieldMetaJson<string[]>(field, \"categoryOrder\");\n const label = field.metadata.get(\"jsonstat.label\") ?? undefined;\n\n return {\n id: name,\n values,\n categoryOrder,\n categoryLabels,\n categoryUnits,\n categoryCoordinates: categoryCoords,\n categoryChild,\n label,\n href: field.metadata.get(\"jsonstat.href\") ?? undefined,\n };\n}\n\n/** Extract the measure column as numeric values with nulls. */\nfunction extractMeasure(name: string, vec: Vector, rowCount: number): MeasureColumn {\n const values: (number | null)[] = new Array(rowCount);\n for (let i = 0; i < rowCount; i++) {\n const v = vec.get(i);\n values[i] = v === null ? null : Number(v);\n }\n return { name, values };\n}\n\n/** Extract the status column as string values. */\nfunction extractStatus(vec: Vector, rowCount: number): StatusColumn {\n const values: string[] = new Array(rowCount);\n for (let i = 0; i < rowCount; i++) {\n const v = vec.get(i);\n values[i] = v === null ? \"\" : String(v);\n }\n return { values };\n}\n\n// ---------------------------------------------------------------------------\n// Role resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve roles: start from field metadata, overlay schema-level roles, then\n * apply caller-supplied hints (which win). Also infer a `time` role for the\n * first temporal dimension if no role is declared.\n */\nfunction resolveRoles(\n table: Table,\n dimensionNames: string[],\n options: ArrowToCubeOptions,\n): RoleMap {\n const roles: RoleMap = {};\n const add = (role: keyof RoleMap, id: string) => {\n (roles[role] ??= []).push(id);\n };\n\n // 1. Field-level metadata roles.\n for (const name of dimensionNames) {\n const field = table.schema.fields.find((f) => f.name === name);\n if (!field) continue;\n const r = getFieldRole(field);\n if (r) add(r, name);\n }\n\n // 2. Schema-level roles (merge).\n const schemaRoles = readSchemaMeta(table.schema).roles;\n if (schemaRoles) {\n for (const role of [\"time\", \"geo\", \"metric\"] as const) {\n for (const id of schemaRoles[role] ?? []) {\n if (!(roles[role] ?? []).includes(id)) add(role, id);\n }\n }\n }\n\n // 3. Inference: if no time role, assign it to the first temporal dimension.\n if (!roles.time || roles.time.length === 0) {\n for (const name of dimensionNames) {\n const field = table.schema.fields.find((f) => f.name === name);\n if (field && isTemporalType(field.type)) {\n add(\"time\", name);\n break;\n }\n }\n }\n\n // 4. Caller hints win.\n if (options.roles) {\n for (const role of [\"time\", \"geo\", \"metric\"] as const) {\n for (const id of options.roles[role] ?? []) {\n if (!(roles[role] ?? []).includes(id)) add(role, id);\n }\n }\n }\n\n return roles;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Convert an Arrow [`Table`] into the [`Observations`] IR.\n *\n * @throws [`ArrowConversionError`](#arrowconversionerror) on mapping problems.\n */\nexport function arrowToCube(\n table: Table,\n options: ArrowToCubeOptions = {},\n): Observations {\n const rowCount = table.numRows;\n if (rowCount === 0) {\n throw new ArrowConversionError(\"Arrow table has no rows\");\n }\n\n const fieldNames = table.schema.fields.map((f) => f.name);\n if (fieldNames.length === 0) {\n throw new ArrowConversionError(\"Arrow table has no columns\");\n }\n\n // --- Measure column -----------------------------------------------------\n // Resolve the measure column name to a definite string: explicit hint >\n // jsonstat.measure marker > first numeric column; throw if none found.\n const measureCol: string =\n options.measure ??\n table.schema.fields.find((f) => isMeasureField(f))?.name ??\n table.schema.fields.find((f) => isNumericType(f.type))?.name ??\n (() => {\n throw new ArrowConversionError(\n \"No measure column found: no numeric column and no jsonstat.measure marker. \" +\n \"Pass options.measure to specify one.\",\n );\n })();\n const measureVec = table.getChild(measureCol);\n if (!measureVec) {\n throw new ArrowConversionError(`Measure column \"${measureCol}\" not found in table`);\n }\n const measure = extractMeasure(measureCol, measureVec, rowCount);\n\n // --- Status column ------------------------------------------------------\n let status: StatusColumn | undefined;\n let statusName = options.status;\n if (!statusName) {\n const marked = table.schema.fields.find((f) => isStatusField(f));\n if (marked) statusName = marked.name;\n }\n if (statusName) {\n const vec = table.getChild(statusName);\n if (!vec) {\n throw new ArrowConversionError(`Status column \"${statusName}\" not found`);\n }\n status = extractStatus(vec, rowCount);\n }\n\n // --- Dimension columns --------------------------------------------------\n let dimensionNames: string[];\n if (options.dimensions) {\n dimensionNames = options.dimensions;\n // Validate they exist.\n for (const name of dimensionNames) {\n if (!fieldNames.includes(name)) {\n throw new ArrowConversionError(`Dimension column \"${name}\" not found`);\n }\n }\n } else {\n // Every column that is not the measure or status, and is a dimension\n // candidate (dictionary/string/temporal/bool), becomes a dimension.\n dimensionNames = table.schema.fields\n .filter(\n (f) =>\n f.name !== measureCol &&\n f.name !== statusName &&\n isDimensionCandidate(f.type),\n )\n .map((f) => f.name);\n if (dimensionNames.length === 0) {\n throw new ArrowConversionError(\n \"No dimension columns found (need dictionary/string/temporal/bool columns \" +\n \"distinct from the measure). Pass options.dimensions to specify them.\",\n );\n }\n }\n\n const dimensions: Record<string, DimensionColumn> = {};\n for (const name of dimensionNames) {\n const vec = table.getChild(name);\n if (!vec) {\n throw new ArrowConversionError(`Dimension column \"${name}\" not found`);\n }\n const field = table.schema.fields.find((f) => f.name === name);\n if (!field) {\n throw new ArrowConversionError(`Dimension field \"${name}\" not found in schema`);\n }\n dimensions[name] = extractDimension(name, vec, field, rowCount);\n }\n\n // --- Roles & model ------------------------------------------------------\n const roles = resolveRoles(table, dimensionNames, options);\n const schemaMeta = readSchemaMeta(table.schema);\n\n const observations: Observations = {\n dimensions,\n measure,\n status,\n model: {\n dimensionIds: dimensionNames.slice(),\n roles,\n meta: schemaMeta.meta,\n valueForm: options.valueForm ?? schemaMeta.valueForm ?? \"auto\",\n statusForm: status ? \"auto\" : \"none\",\n },\n };\n\n return observations;\n}\n\n/** Convenience: convert an Arrow table straight to a JSON-stat dataset. */\nexport async function arrowToDataset(\n table: Table,\n options?: ArrowToCubeOptions,\n): Promise<import(\"../model/jsonstat\").JsonStatDataset> {\n // Lazy import to avoid a hard build-time dependency cycle for tree-shaking.\n const { toDataset } = await import(\"../core/cubeBuilder\");\n return toDataset(arrowToCube(table, options));\n}\n","/**\n * arrowFromCube — the inverse of [`arrowToCube`](./arrowToCube.ts): converts the\n * [`Observations`](../model/ir.ts) IR into an Apache Arrow [`Table`].\n *\n * ## Role\n *\n * This is the **Phase-2 export seam**: the forward path from JSON-stat to the\n * columnar lakehouse stack. Export = [`readDataset`](../core/cubeReader.ts)\n * (JSON-stat → IR) → `cubeToArrow` (IR → Arrow) → Arrow-to-Parquet/DuckDB/\n * Polars/CSV writers (which Arrow ecosystems provide natively).\n *\n * It is shipped in v0.1 (alongside [`cubeReader`](../core/cubeReader.ts)) for\n * two reasons: (1) the round-trip tests use it to assert that JSON-stat → IR →\n * Arrow → IR → JSON-stat is stable; (2) proving the IR is bidirectional now\n * de-risks Phase 2 to \"just write the sink writers\".\n *\n * ## Mapping\n *\n * - Each dimension → a dictionary-encoded Utf8 column (dictionary = the\n * resolved category list), annotated with `jsonstat.*` field metadata so the\n * round-trip preserves labels/units/roles/hierarchies.\n * - The measure → a Float64 column (nulls preserved), marked\n * `jsonstat.measure=true`.\n * - The status column (if any) → a Utf8 column marked `jsonstat.status=true`.\n * - Dataset-level metadata (label/source/updated/extension/roles/valueForm) is\n * written to the schema metadata.\n */\n\nimport {\n Table,\n Field,\n Schema,\n Float64,\n Utf8,\n Dictionary,\n Int32,\n vectorFromArray,\n makeVector,\n Vector,\n} from \"apache-arrow\";\nimport type {\n DimensionColumn,\n Observations,\n} from \"../model/ir\";\nimport type { JsonStatUnit, Coordinates } from \"../model/jsonstat\";\nimport { buildFieldMeta, buildSchemaMeta } from \"./schemaMeta\";\n\n// ---------------------------------------------------------------------------\n// Dimension column construction\n// ---------------------------------------------------------------------------\n\n/**\n * Build a dictionary-encoded Utf8 Arrow vector for a dimension column, plus the\n * `jsonstat.*` field metadata that preserves labels/units/roles/hierarchies.\n *\n * We use a dictionary with Utf8 values — the most natural Arrow representation\n * of a categorical dimension, and what [`arrowToCube`](./arrowToCube.ts) reads\n * back losslessly.\n */\nfunction buildDimensionColumn(\n col: DimensionColumn,\n): { name: string; vector: Vector; field: Field } {\n // Enumerate the canonical category order: explicit if given, else\n // first-seen. This is attached to the field metadata so arrowToCube can\n // restore the exact order on the round-trip.\n const order = resolveCategoryOrder(col);\n\n // Build the dictionary vector from the raw string values. arrowToCube reads\n // the dictionary values for category IDs and the metadata for ordering.\n const vector = vectorFromArray(\n col.values,\n new Dictionary(new Utf8(), new Int32()),\n );\n\n // Field metadata preserving the JSON-stat dimension model.\n const metadata = buildFieldMeta({\n label: col.label,\n categoryLabels: col.categoryLabels,\n categoryUnits: col.categoryUnits as Record<string, JsonStatUnit> | undefined,\n categoryCoords: col.categoryCoordinates as Record<string, Coordinates> | undefined,\n categoryChild: col.categoryChild,\n categoryOrder: col.categoryOrder ?? (order.length > 0 ? order : undefined),\n });\n\n const field = new Field(\n col.id,\n new Dictionary(new Utf8(), new Int32()),\n true,\n metadata,\n );\n\n return { name: col.id, vector, field };\n}\n\n/**\n * Resolve the canonical category order for a dimension: explicit order if\n * given, otherwise first-seen from the values.\n */\nfunction resolveCategoryOrder(col: DimensionColumn): string[] {\n if (col.categoryOrder && col.categoryOrder.length > 0) {\n return col.categoryOrder.slice();\n }\n const seen = new Set<string>();\n const out: string[] = [];\n for (const v of col.values) {\n if (!seen.has(v)) {\n seen.add(v);\n out.push(v);\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Convert the [`Observations`](../model/ir.ts) IR into an Arrow [`Table`].\n *\n * Each dimension becomes a dictionary column; the measure becomes a Float64\n * column; the optional status becomes a Utf8 column. All `jsonstat.*`\n * metadata is attached so the result round-trips through [`arrowToCube`](./arrowToCube.ts).\n *\n * @throws Error if a dimension column referenced by the model is missing.\n */\nexport function cubeToArrow(obs: Observations): Table {\n const n = obs.measure.values.length;\n const dimIds = obs.model.dimensionIds;\n\n const fields: Field[] = [];\n const columns: Record<string, Vector> = {};\n\n // --- Dimension columns -------------------------------------------------\n for (const id of dimIds) {\n const col = obs.dimensions[id];\n if (!col) {\n throw new Error(`arrowFromCube: missing dimension column \"${id}\"`);\n }\n const built = buildDimensionColumn(col);\n\n // Attach the role (if any) to the field metadata.\n const role = roleFor(obs, id);\n if (role) {\n built.field.metadata.set(\"jsonstat.role\", role);\n }\n\n fields.push(built.field);\n columns[built.name] = built.vector;\n }\n\n // --- Measure column (Float64, nulls preserved) -------------------------\n const measureValues = new Float64Array(n);\n let nullCount = 0;\n for (let i = 0; i < n; i++) {\n const v = obs.measure.values[i];\n if (v === null) {\n nullCount++;\n } else {\n measureValues[i] = v;\n }\n }\n const measureVector = makeVector(measureValues);\n const measureName = obs.measure.name ?? \"value\";\n const measureMeta = buildFieldMeta({ measure: true });\n const measureField = new Field(measureName, new Float64(), true, measureMeta);\n fields.push(measureField);\n columns[measureName] = measureVector;\n // Track nulls for the measure so arrowToCube reads them back as null.\n if (nullCount > 0) {\n // makeVector(Float64Array) has nullCount 0; we need to rebuild with a\n // validity bitmap so the nulls survive the round-trip. Reconstruct using\n // a Uint8Array value selector.\n columns[measureName] = buildNullableFloat64Vector(obs.measure.values);\n }\n\n // --- Status column (Utf8), if present ---------------------------------\n if (obs.status) {\n const statusName = \"status\";\n const statusVector = vectorFromArray(\n obs.status.values.map((s) => (s === \"\" ? null : s)),\n new Utf8(),\n );\n const statusMeta = buildFieldMeta({ status: true });\n const statusField = new Field(statusName, new Utf8(), true, statusMeta);\n fields.push(statusField);\n columns[statusName] = statusVector;\n }\n\n // --- Schema with dataset-level metadata --------------------------------\n const schemaMeta = buildSchemaMeta({\n label: obs.model.meta?.label,\n source: obs.model.meta?.source,\n updated: obs.model.meta?.updated,\n extension: obs.model.meta?.extension as Record<string, unknown> | undefined,\n roles: obs.model.roles,\n valueForm: obs.model.valueForm,\n });\n\n const schema = new Schema(fields, schemaMeta);\n\n return new Table(schema, columns);\n}\n\n/**\n * Build a Float64 Vector that preserves nulls via a validity bitmap.\n * `makeVector(Float64Array)` always reports nullCount 0, so when the measure\n * contains nulls we construct the Data manually with a null bitmap.\n */\nfunction buildNullableFloat64Vector(values: (number | null)[]): Vector {\n const n = values.length;\n const data = new Float64Array(n);\n const nullCount = values.filter((v) => v === null).length;\n // Arrow validity bitmap: 1 bit per row, little-endian within each byte.\n // A set bit (1) means the value is valid (non-null).\n const numBytes = Math.ceil(n / 8);\n const nullBitmap = new Uint8Array(numBytes);\n for (let i = 0; i < n; i++) {\n if (values[i] !== null) {\n data[i] = values[i] as number;\n nullBitmap[Math.floor(i / 8)] |= 1 << i % 8;\n }\n }\n const vector = makeVector({\n type: new Float64(),\n data,\n nullBitmap,\n length: n,\n nullCount,\n } as any);\n return vector;\n}\n\n/** Look up the role (if any) assigned to a dimension id in the model. */\nfunction roleFor(obs: Observations, id: string): \"time\" | \"geo\" | \"metric\" | undefined {\n const roles = obs.model.roles;\n if (!roles) return undefined;\n if (roles.time?.includes(id)) return \"time\";\n if (roles.geo?.includes(id)) return \"geo\";\n if (roles.metric?.includes(id)) return \"metric\";\n return undefined;\n}\n"]}
|