@graphty/graph-format 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -43
- package/dist/graph-format.d.ts +1 -0
- package/dist/graph-format.d.ts.map +1 -0
- package/dist/graph-format.js +15798 -0
- package/dist/graph-format.js.map +1 -0
- package/dist/src/builder/arena.d.ts +85 -0
- package/dist/src/builder/arena.d.ts.map +1 -0
- package/dist/src/builder/arena.js +154 -0
- package/dist/src/builder/arena.js.map +1 -0
- package/dist/src/builder/compact.d.ts +515 -0
- package/dist/src/builder/compact.d.ts.map +1 -0
- package/dist/src/builder/compact.js +1553 -0
- package/dist/src/builder/compact.js.map +1 -0
- package/dist/src/builder/counting-sort.d.ts +96 -0
- package/dist/src/builder/counting-sort.d.ts.map +1 -0
- package/dist/src/builder/counting-sort.js +430 -0
- package/dist/src/builder/counting-sort.js.map +1 -0
- package/dist/src/builder/freeze.d.ts +72 -0
- package/dist/src/builder/freeze.d.ts.map +1 -0
- package/dist/src/builder/freeze.js +548 -0
- package/dist/src/builder/freeze.js.map +1 -0
- package/dist/src/builder/graph-builder.d.ts +602 -0
- package/dist/src/builder/graph-builder.d.ts.map +1 -0
- package/dist/src/builder/graph-builder.js +1940 -0
- package/dist/src/builder/graph-builder.js.map +1 -0
- package/dist/src/builder/options.d.ts +16 -0
- package/dist/src/builder/options.d.ts.map +1 -0
- package/dist/src/builder/options.js +21 -0
- package/dist/src/builder/options.js.map +1 -0
- package/dist/src/columns/bitmap.d.ts +96 -0
- package/dist/src/columns/bitmap.d.ts.map +1 -0
- package/dist/src/columns/bitmap.js +184 -0
- package/dist/src/columns/bitmap.js.map +1 -0
- package/dist/src/columns/column.d.ts +220 -0
- package/dist/src/columns/column.d.ts.map +1 -0
- package/dist/src/columns/column.js +2109 -0
- package/dist/src/columns/column.js.map +1 -0
- package/dist/src/columns/dictionary.d.ts +60 -0
- package/dist/src/columns/dictionary.d.ts.map +1 -0
- package/dist/src/columns/dictionary.js +102 -0
- package/dist/src/columns/dictionary.js.map +1 -0
- package/dist/src/columns/growable.d.ts +198 -0
- package/dist/src/columns/growable.d.ts.map +1 -0
- package/dist/src/columns/growable.js +296 -0
- package/dist/src/columns/growable.js.map +1 -0
- package/dist/src/columns/infer.d.ts +121 -0
- package/dist/src/columns/infer.d.ts.map +1 -0
- package/dist/src/columns/infer.js +268 -0
- package/dist/src/columns/infer.js.map +1 -0
- package/dist/src/columns/remap.d.ts +143 -0
- package/dist/src/columns/remap.d.ts.map +1 -0
- package/dist/src/columns/remap.js +588 -0
- package/dist/src/columns/remap.js.map +1 -0
- package/dist/src/columns/table.d.ts +184 -0
- package/dist/src/columns/table.d.ts.map +1 -0
- package/dist/src/columns/table.js +485 -0
- package/dist/src/columns/table.js.map +1 -0
- package/dist/src/constants.d.ts +34 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +79 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/errors.d.ts +76 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +32 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/ids/edge-id-index.d.ts +77 -0
- package/dist/src/ids/edge-id-index.d.ts.map +1 -0
- package/dist/src/ids/edge-id-index.js +155 -0
- package/dist/src/ids/edge-id-index.js.map +1 -0
- package/dist/src/ids/node-id-map.d.ts +280 -0
- package/dist/src/ids/node-id-map.d.ts.map +1 -0
- package/dist/src/ids/node-id-map.js +1139 -0
- package/dist/src/ids/node-id-map.js.map +1 -0
- package/dist/src/ids/string-store.d.ts +219 -0
- package/dist/src/ids/string-store.d.ts.map +1 -0
- package/dist/src/ids/string-store.js +526 -0
- package/dist/src/ids/string-store.js.map +1 -0
- package/dist/src/index.d.ts +30 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +29 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/populate/from-csr.d.ts +61 -0
- package/dist/src/populate/from-csr.d.ts.map +1 -0
- package/dist/src/populate/from-csr.js +706 -0
- package/dist/src/populate/from-csr.js.map +1 -0
- package/dist/src/populate/from-edge-arrays.d.ts +51 -0
- package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
- package/dist/src/populate/from-edge-arrays.js +217 -0
- package/dist/src/populate/from-edge-arrays.js.map +1 -0
- package/dist/src/populate/from-records.d.ts +51 -0
- package/dist/src/populate/from-records.d.ts.map +1 -0
- package/dist/src/populate/from-records.js +447 -0
- package/dist/src/populate/from-records.js.map +1 -0
- package/dist/src/snapshot/derived.d.ts +211 -0
- package/dist/src/snapshot/derived.d.ts.map +1 -0
- package/dist/src/snapshot/derived.js +1254 -0
- package/dist/src/snapshot/derived.js.map +1 -0
- package/dist/src/snapshot/graph-meta.d.ts +23 -0
- package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
- package/dist/src/snapshot/graph-meta.js +159 -0
- package/dist/src/snapshot/graph-meta.js.map +1 -0
- package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
- package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
- package/dist/src/snapshot/graph-snapshot.js +1247 -0
- package/dist/src/snapshot/graph-snapshot.js.map +1 -0
- package/dist/src/snapshot/hash.d.ts +90 -0
- package/dist/src/snapshot/hash.d.ts.map +1 -0
- package/dist/src/snapshot/hash.js +228 -0
- package/dist/src/snapshot/hash.js.map +1 -0
- package/dist/src/snapshot/queries.d.ts +74 -0
- package/dist/src/snapshot/queries.d.ts.map +1 -0
- package/dist/src/snapshot/queries.js +131 -0
- package/dist/src/snapshot/queries.js.map +1 -0
- package/dist/src/snapshot/validate.d.ts +192 -0
- package/dist/src/snapshot/validate.d.ts.map +1 -0
- package/dist/src/snapshot/validate.js +990 -0
- package/dist/src/snapshot/validate.js.map +1 -0
- package/dist/src/snapshot/views.d.ts +274 -0
- package/dist/src/snapshot/views.d.ts.map +1 -0
- package/dist/src/snapshot/views.js +856 -0
- package/dist/src/snapshot/views.js.map +1 -0
- package/dist/src/types/builder.d.ts +456 -0
- package/dist/src/types/builder.d.ts.map +1 -0
- package/dist/src/types/builder.js +11 -0
- package/dist/src/types/builder.js.map +1 -0
- package/dist/src/types/columns.d.ts +615 -0
- package/dist/src/types/columns.d.ts.map +1 -0
- package/dist/src/types/columns.js +14 -0
- package/dist/src/types/columns.js.map +1 -0
- package/dist/src/types/index.d.ts +14 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +11 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/internal.d.ts +172 -0
- package/dist/src/types/internal.d.ts.map +1 -0
- package/dist/src/types/internal.js +13 -0
- package/dist/src/types/internal.js.map +1 -0
- package/dist/src/types/snapshot.d.ts +801 -0
- package/dist/src/types/snapshot.d.ts.map +1 -0
- package/dist/src/types/snapshot.js +14 -0
- package/dist/src/types/snapshot.js.map +1 -0
- package/dist/src/types/wire.d.ts +196 -0
- package/dist/src/types/wire.d.ts.map +1 -0
- package/dist/src/types/wire.js +8 -0
- package/dist/src/types/wire.js.map +1 -0
- package/dist/src/util/mask.d.ts +55 -0
- package/dist/src/util/mask.d.ts.map +1 -0
- package/dist/src/util/mask.js +71 -0
- package/dist/src/util/mask.js.map +1 -0
- package/dist/src/util/options.d.ts +22 -0
- package/dist/src/util/options.d.ts.map +1 -0
- package/dist/src/util/options.js +49 -0
- package/dist/src/util/options.js.map +1 -0
- package/dist/src/util/shared-buffers.d.ts +42 -0
- package/dist/src/util/shared-buffers.d.ts.map +1 -0
- package/dist/src/util/shared-buffers.js +52 -0
- package/dist/src/util/shared-buffers.js.map +1 -0
- package/dist/src/util/typed-array.d.ts +205 -0
- package/dist/src/util/typed-array.d.ts.map +1 -0
- package/dist/src/util/typed-array.js +289 -0
- package/dist/src/util/typed-array.js.map +1 -0
- package/dist/src/wire/bytes.d.ts +69 -0
- package/dist/src/wire/bytes.d.ts.map +1 -0
- package/dist/src/wire/bytes.js +361 -0
- package/dist/src/wire/bytes.js.map +1 -0
- package/dist/src/wire/carried-views.d.ts +111 -0
- package/dist/src/wire/carried-views.d.ts.map +1 -0
- package/dist/src/wire/carried-views.js +286 -0
- package/dist/src/wire/carried-views.js.map +1 -0
- package/dist/src/wire/from-wire.d.ts +144 -0
- package/dist/src/wire/from-wire.d.ts.map +1 -0
- package/dist/src/wire/from-wire.js +1425 -0
- package/dist/src/wire/from-wire.js.map +1 -0
- package/dist/src/wire/to-wire.d.ts +171 -0
- package/dist/src/wire/to-wire.d.ts.map +1 -0
- package/dist/src/wire/to-wire.js +652 -0
- package/dist/src/wire/to-wire.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +72 -7
- package/src/builder/arena.ts +192 -0
- package/src/builder/compact.ts +1811 -0
- package/src/builder/counting-sort.ts +506 -0
- package/src/builder/freeze.ts +689 -0
- package/src/builder/graph-builder.ts +2264 -0
- package/src/builder/options.ts +29 -0
- package/src/columns/bitmap.ts +196 -0
- package/src/columns/column.ts +2574 -0
- package/src/columns/dictionary.ts +114 -0
- package/src/columns/growable.ts +350 -0
- package/src/columns/infer.ts +285 -0
- package/src/columns/remap.ts +663 -0
- package/src/columns/table.ts +568 -0
- package/src/constants.ts +89 -0
- package/src/errors.ts +115 -0
- package/src/ids/edge-id-index.ts +179 -0
- package/src/ids/node-id-map.ts +1340 -0
- package/src/ids/string-store.ts +589 -0
- package/src/index.ts +139 -0
- package/src/lib-resizable-array-buffer.d.ts +34 -0
- package/src/populate/from-csr.ts +802 -0
- package/src/populate/from-edge-arrays.ts +257 -0
- package/src/populate/from-records.ts +536 -0
- package/src/snapshot/derived.ts +1486 -0
- package/src/snapshot/graph-meta.ts +167 -0
- package/src/snapshot/graph-snapshot.ts +1503 -0
- package/src/snapshot/hash.ts +241 -0
- package/src/snapshot/queries.ts +136 -0
- package/src/snapshot/validate.ts +1108 -0
- package/src/snapshot/views.ts +943 -0
- package/src/types/builder.ts +480 -0
- package/src/types/columns.ts +727 -0
- package/src/types/index.ts +14 -0
- package/src/types/internal.ts +195 -0
- package/src/types/snapshot.ts +886 -0
- package/src/types/wire.ts +207 -0
- package/src/util/mask.ts +89 -0
- package/src/util/options.ts +57 -0
- package/src/util/shared-buffers.ts +56 -0
- package/src/util/typed-array.ts +371 -0
- package/src/wire/bytes.ts +440 -0
- package/src/wire/carried-views.ts +327 -0
- package/src/wire/from-wire.ts +1690 -0
- package/src/wire/to-wire.ts +755 -0
|
@@ -0,0 +1,2109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column implementations for every dtype of design section 5.1, the column factory that wraps raw
|
|
3
|
+
* storage (MutableColumnParts) into a Column, the declaration resolver (ColumnDecl -> ColumnMeta,
|
|
4
|
+
* design sections 5.3 and 5.5), the constructors the table uses for typed arrays, JS arrays and
|
|
5
|
+
* empty declared columns. Bitmap and padding arithmetic comes from bitmap.ts and util/typed-array.ts.
|
|
6
|
+
*
|
|
7
|
+
* Storage follows Apache Arrow's layout without the dependency: a typed values buffer, an optional
|
|
8
|
+
* validity bitmap (u32 words, LSB-first, ceil(rows / 32) words -- the one bitmap layout of the
|
|
9
|
+
* package, decision C13), an optional dictionary, offsets or child, and per-column metadata. No
|
|
10
|
+
* per-row objects exist anywhere. Column contents are frozen by contract unless meta.mutable is true
|
|
11
|
+
* (design section 5.8); the mutable gating throws E_COLUMN_IMMUTABLE.
|
|
12
|
+
*/
|
|
13
|
+
import { INVALID_INDEX } from "../constants.js";
|
|
14
|
+
import { GraphFormatError } from "../errors.js";
|
|
15
|
+
import { encodeUtf8Rows } from "../ids/string-store.js";
|
|
16
|
+
import { claimHolder, noteShared } from "../util/shared-buffers.js";
|
|
17
|
+
import { canViewAsPaddedU32, isOverPlainBuffer, paddedU32View, padTo4 } from "../util/typed-array.js";
|
|
18
|
+
import { bitmapClear, bitmapCount, bitmapGet, bitmapSet, bitmapSlice, bitmapWordCount, makeBitmap } from "./bitmap.js";
|
|
19
|
+
import { assertWellFormedString, buildCodeMap, DictionaryBuilder } from "./dictionary.js";
|
|
20
|
+
import { coerceValue, inferValuesDtype } from "./infer.js";
|
|
21
|
+
const ALL_DTYPES = new Set([
|
|
22
|
+
"f32",
|
|
23
|
+
"f64",
|
|
24
|
+
"i32",
|
|
25
|
+
"u32",
|
|
26
|
+
"u8",
|
|
27
|
+
"bool",
|
|
28
|
+
"dict",
|
|
29
|
+
"string",
|
|
30
|
+
"list",
|
|
31
|
+
"json",
|
|
32
|
+
]);
|
|
33
|
+
const NUMERIC_DTYPES = new Set(["f32", "f64", "i32", "u32", "u8"]);
|
|
34
|
+
const MAX_COMPONENTS = 16;
|
|
35
|
+
/** The deepest nesting a JSON value (a json cell, a default, options or extra) may have. */
|
|
36
|
+
const MAX_JSON_DEPTH = 256;
|
|
37
|
+
const I32_MIN = -2147483648;
|
|
38
|
+
const I32_MAX = 2147483647;
|
|
39
|
+
const U32_MAX = 0xffffffff;
|
|
40
|
+
const U8_MAX = 0xff;
|
|
41
|
+
const decoder = new TextDecoder();
|
|
42
|
+
/** Cached f32 copies of f64 columns for gpuView() (design section 10.4); dropped by markDirty(). */
|
|
43
|
+
const f32Cache = new WeakMap();
|
|
44
|
+
/**
|
|
45
|
+
* Allocate a u8 store from which a zero-copy padded u32 view is constructible (design section 5.7):
|
|
46
|
+
* the backing buffer is a multiple of 4 bytes even when the length is not.
|
|
47
|
+
* @param length - the number of bytes
|
|
48
|
+
* @returns a fresh Uint8Array over a padded buffer
|
|
49
|
+
*/
|
|
50
|
+
export function allocU8(length) {
|
|
51
|
+
return new Uint8Array(new ArrayBuffer(padTo4(length)), 0, length);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* GPU eligibility of a dtype (design section 10.4).
|
|
55
|
+
* @param dtype - the column dtype
|
|
56
|
+
* @returns "direct" for u32 / i32 / f32 / dict, "packed" for u8 / bool, "convert" for f64, "none" otherwise
|
|
57
|
+
*/
|
|
58
|
+
export function gpuEligibility(dtype) {
|
|
59
|
+
switch (dtype) {
|
|
60
|
+
case "f32":
|
|
61
|
+
case "i32":
|
|
62
|
+
case "u32":
|
|
63
|
+
case "dict":
|
|
64
|
+
return "direct";
|
|
65
|
+
case "u8":
|
|
66
|
+
case "bool":
|
|
67
|
+
return "packed";
|
|
68
|
+
case "f64":
|
|
69
|
+
return "convert";
|
|
70
|
+
case "string":
|
|
71
|
+
case "list":
|
|
72
|
+
case "json":
|
|
73
|
+
return "none";
|
|
74
|
+
default: {
|
|
75
|
+
const name = dtype;
|
|
76
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether a dtype name is one a list child may have (any dtype but list).
|
|
82
|
+
* @param value - the dtype name
|
|
83
|
+
* @returns true for a scalar dtype
|
|
84
|
+
*/
|
|
85
|
+
function isScalarDtype(value) {
|
|
86
|
+
return value !== "list" && ALL_DTYPES.has(value);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether a value is a plain object (prototype Object.prototype or null), the only object shape JSON
|
|
90
|
+
* carries.
|
|
91
|
+
* @param value - the value to test
|
|
92
|
+
* @returns true for a plain object
|
|
93
|
+
*/
|
|
94
|
+
export function isPlainObject(value) {
|
|
95
|
+
if (typeof value !== "object" || value === null) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const proto = Object.getPrototypeOf(value);
|
|
99
|
+
return proto === Object.prototype || proto === null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reject a metadata value that JSON cannot carry (design section 5.9): only null, booleans, numbers
|
|
103
|
+
* (non-finite and -0 included; the wire tags them), well-formed strings, arrays and plain objects of
|
|
104
|
+
* such values are accepted. E_COLUMN_TYPE with details.field otherwise.
|
|
105
|
+
* @param value - the value to check
|
|
106
|
+
* @param field - the metadata field name for the error details
|
|
107
|
+
* @param depth - the nesting depth of `value` (E_COLUMN_TYPE beyond MAX_JSON_DEPTH)
|
|
108
|
+
*/
|
|
109
|
+
export function assertJsonValue(value, field, depth = 0) {
|
|
110
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (typeof value === "string") {
|
|
114
|
+
assertWellFormedString(value, { field });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (depth > MAX_JSON_DEPTH) {
|
|
118
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `${field} is nested deeper than ${MAX_JSON_DEPTH} levels`, {
|
|
119
|
+
field,
|
|
120
|
+
reason: "nesting",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
for (const item of value) {
|
|
125
|
+
assertJsonValue(item, field, depth + 1);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (isPlainObject(value)) {
|
|
130
|
+
for (const key of Object.keys(value)) {
|
|
131
|
+
assertJsonValue(value[key], field, depth + 1);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `${field} is not a JSON value`, {
|
|
136
|
+
field,
|
|
137
|
+
found: value === undefined ? "undefined" : typeof value,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A deep copy of a JSON value (already checked by assertJsonValue): arrays and plain objects are
|
|
142
|
+
* copied at every level so a caller's live object never reaches a column's metadata, where a later
|
|
143
|
+
* mutation of the caller's object would change what `value()` returns for unset rows (design
|
|
144
|
+
* section 5.8). The copy is the column's own; like a typed array's contents it is immutable by
|
|
145
|
+
* contract.
|
|
146
|
+
* @param value - the JSON value
|
|
147
|
+
* @returns the copy (primitives are returned as they are)
|
|
148
|
+
*/
|
|
149
|
+
function cloneJson(value) {
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
return value.map((item) => cloneJson(item));
|
|
152
|
+
}
|
|
153
|
+
if (isPlainObject(value)) {
|
|
154
|
+
const out = {};
|
|
155
|
+
for (const key of Object.keys(value)) {
|
|
156
|
+
out[key] = cloneJson(value[key]);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
// ============================================================ declaration resolution
|
|
163
|
+
/**
|
|
164
|
+
* Whether a number is exactly representable in a numeric dtype: any number for f32 / f64, an integer
|
|
165
|
+
* in range for i32 / u32 / u8.
|
|
166
|
+
* @param dtype - the numeric dtype
|
|
167
|
+
* @param value - the number
|
|
168
|
+
* @returns true when the dtype stores the value without change
|
|
169
|
+
*/
|
|
170
|
+
export function representableNumber(dtype, value) {
|
|
171
|
+
switch (dtype) {
|
|
172
|
+
case "f32":
|
|
173
|
+
case "f64":
|
|
174
|
+
return true;
|
|
175
|
+
case "i32":
|
|
176
|
+
return Number.isInteger(value) && value >= I32_MIN && value <= I32_MAX;
|
|
177
|
+
case "u32":
|
|
178
|
+
return Number.isInteger(value) && value >= 0 && value <= U32_MAX;
|
|
179
|
+
case "u8":
|
|
180
|
+
return Number.isInteger(value) && value >= 0 && value <= U8_MAX;
|
|
181
|
+
default: {
|
|
182
|
+
const name = dtype;
|
|
183
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The single number a declared numeric default reduces to, when it is representable: the number
|
|
189
|
+
* itself for components 1, or the common value of an array of `components` equal numbers.
|
|
190
|
+
* @param dtype - the numeric dtype
|
|
191
|
+
* @param components - the column stride
|
|
192
|
+
* @param value - the declared default
|
|
193
|
+
* @returns the fill number, or null when the default is not representable as one fill value
|
|
194
|
+
*/
|
|
195
|
+
function numericDefaultFill(dtype, components, value) {
|
|
196
|
+
if (typeof value === "number") {
|
|
197
|
+
return representableNumber(dtype, value) ? value : null;
|
|
198
|
+
}
|
|
199
|
+
if (components > 1 && Array.isArray(value) && value.length === components) {
|
|
200
|
+
const first = value[0];
|
|
201
|
+
if (typeof first !== "number" || !representableNumber(dtype, first)) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
for (let k = 1; k < components; k++) {
|
|
205
|
+
if (!Object.is(value[k], first)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return first;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function typeError(field, message, extra) {
|
|
214
|
+
return new GraphFormatError("E_COLUMN_TYPE", message, { ...extra, field });
|
|
215
|
+
}
|
|
216
|
+
function resolveOrigin(origin) {
|
|
217
|
+
if (origin === undefined) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (!isPlainObject(origin)) {
|
|
221
|
+
throw typeError("origin", "origin must be a plain object");
|
|
222
|
+
}
|
|
223
|
+
const pick = (key) => {
|
|
224
|
+
const value = origin[key];
|
|
225
|
+
if (value === undefined || value === null) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (typeof value !== "string") {
|
|
229
|
+
throw typeError("origin", `origin.${key} must be a string or null`, { found: typeof value });
|
|
230
|
+
}
|
|
231
|
+
return value;
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
format: pick("format"),
|
|
235
|
+
id: pick("id"),
|
|
236
|
+
title: pick("title"),
|
|
237
|
+
type: pick("type"),
|
|
238
|
+
namespace: pick("namespace"),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function resolveComponents(field, value, dtype) {
|
|
242
|
+
if (value === undefined) {
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_COMPONENTS) {
|
|
246
|
+
throw typeError(field, `${field} must be an integer in 1..${MAX_COMPONENTS}, found ${value}`, { found: value });
|
|
247
|
+
}
|
|
248
|
+
if (value > 1 && !NUMERIC_DTYPES.has(dtype)) {
|
|
249
|
+
throw typeError(field, `${field} > 1 is only allowed for f32 / f64 / i32 / u32 / u8, not ${dtype}`, {
|
|
250
|
+
found: value,
|
|
251
|
+
dtype,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Resolve the fill value of design section 5.3: the explicit fill when given (type-checked against
|
|
258
|
+
* the dtype), else the declared default when it is representable in the dtype, else the dtype's zero
|
|
259
|
+
* (0 / false / "" ; INVALID_INDEX for a refersTo u32 column so unset references are never in range).
|
|
260
|
+
* @param dtype - the column dtype
|
|
261
|
+
* @param components - the stride
|
|
262
|
+
* @param explicit - the declared fill, if any
|
|
263
|
+
* @param defaultValue - the declared default, if any
|
|
264
|
+
* @param options - the declared options (the initial dictionary of a dict column)
|
|
265
|
+
* @param refersTo - the referenced index space, if any
|
|
266
|
+
* @returns the resolved fill
|
|
267
|
+
*/
|
|
268
|
+
function resolveFill(dtype, components, explicit, defaultValue, options, refersTo) {
|
|
269
|
+
switch (dtype) {
|
|
270
|
+
case "f32":
|
|
271
|
+
case "f64":
|
|
272
|
+
case "i32":
|
|
273
|
+
case "u32":
|
|
274
|
+
case "u8": {
|
|
275
|
+
if (explicit !== undefined) {
|
|
276
|
+
if (typeof explicit !== "number") {
|
|
277
|
+
throw typeError("fill", `fill of a ${dtype} column must be a number`, { found: typeof explicit });
|
|
278
|
+
}
|
|
279
|
+
return explicit;
|
|
280
|
+
}
|
|
281
|
+
if (refersTo !== null) {
|
|
282
|
+
return INVALID_INDEX;
|
|
283
|
+
}
|
|
284
|
+
const fromDefault = numericDefaultFill(dtype, components, defaultValue);
|
|
285
|
+
return fromDefault ?? 0;
|
|
286
|
+
}
|
|
287
|
+
case "bool": {
|
|
288
|
+
if (explicit !== undefined) {
|
|
289
|
+
if (typeof explicit !== "boolean") {
|
|
290
|
+
throw typeError("fill", "fill of a bool column must be a boolean", { found: typeof explicit });
|
|
291
|
+
}
|
|
292
|
+
return explicit;
|
|
293
|
+
}
|
|
294
|
+
return typeof defaultValue === "boolean" ? defaultValue : false;
|
|
295
|
+
}
|
|
296
|
+
case "dict": {
|
|
297
|
+
if (explicit !== undefined) {
|
|
298
|
+
if (typeof explicit !== "string") {
|
|
299
|
+
throw typeError("fill", "fill of a dict column must be a string", { found: typeof explicit });
|
|
300
|
+
}
|
|
301
|
+
return explicit;
|
|
302
|
+
}
|
|
303
|
+
if (typeof defaultValue === "string" && (options === null || options.includes(defaultValue))) {
|
|
304
|
+
return defaultValue;
|
|
305
|
+
}
|
|
306
|
+
if (options !== null && options.length > 0 && typeof options[0] === "string") {
|
|
307
|
+
return options[0];
|
|
308
|
+
}
|
|
309
|
+
return "";
|
|
310
|
+
}
|
|
311
|
+
case "string": {
|
|
312
|
+
if (explicit !== undefined) {
|
|
313
|
+
if (typeof explicit !== "string") {
|
|
314
|
+
throw typeError("fill", "fill of a string column must be a string", { found: typeof explicit });
|
|
315
|
+
}
|
|
316
|
+
return explicit;
|
|
317
|
+
}
|
|
318
|
+
return "";
|
|
319
|
+
}
|
|
320
|
+
case "list":
|
|
321
|
+
case "json": {
|
|
322
|
+
// the nominal fill of a list / json column is ""; unset rows physically hold an empty list / undefined
|
|
323
|
+
if (explicit !== undefined && explicit !== "") {
|
|
324
|
+
throw typeError("fill", `a ${dtype} column takes no fill value`, { found: explicit });
|
|
325
|
+
}
|
|
326
|
+
return "";
|
|
327
|
+
}
|
|
328
|
+
default: {
|
|
329
|
+
const name = dtype;
|
|
330
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Resolve a column declaration into the complete metadata of design section 5.5: every field
|
|
336
|
+
* present, null for none, defaults applied, every rule checked (components, list child, refersTo,
|
|
337
|
+
* JSON-ness of default / options / extra, fill typing). E_COLUMN_TYPE with details.field on every
|
|
338
|
+
* violation.
|
|
339
|
+
* @param name - the column name (overrides decl.name)
|
|
340
|
+
* @param domain - the table the column belongs to
|
|
341
|
+
* @param decl - the declaration; dtype is required
|
|
342
|
+
* @returns the resolved metadata, frozen
|
|
343
|
+
*/
|
|
344
|
+
export function resolveColumnMeta(name, domain, decl) {
|
|
345
|
+
const { dtype } = decl;
|
|
346
|
+
if (dtype === undefined || !ALL_DTYPES.has(dtype)) {
|
|
347
|
+
throw typeError("dtype", `column "${name}" has no valid dtype`, { name, found: dtype });
|
|
348
|
+
}
|
|
349
|
+
const components = resolveComponents("components", decl.components, dtype);
|
|
350
|
+
let itemDtype = null;
|
|
351
|
+
let itemComponents = null;
|
|
352
|
+
if (dtype === "list") {
|
|
353
|
+
const { itemDtype: declaredItem } = decl;
|
|
354
|
+
if (declaredItem === undefined || !isScalarDtype(declaredItem)) {
|
|
355
|
+
throw typeError("itemDtype", `list column "${name}" needs a scalar itemDtype`, {
|
|
356
|
+
name,
|
|
357
|
+
found: declaredItem,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
itemDtype = declaredItem;
|
|
361
|
+
itemComponents = resolveComponents("itemComponents", decl.itemComponents, declaredItem);
|
|
362
|
+
}
|
|
363
|
+
else if (decl.itemDtype !== undefined || decl.itemComponents !== undefined) {
|
|
364
|
+
throw typeError("itemDtype", `itemDtype / itemComponents are only allowed on list columns, not ${dtype}`, {
|
|
365
|
+
name,
|
|
366
|
+
dtype,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const refersTo = decl.refersTo ?? null;
|
|
370
|
+
if (refersTo !== null) {
|
|
371
|
+
if (refersTo !== "node" && refersTo !== "edge") {
|
|
372
|
+
throw typeError("refersTo", `refersTo must be "node" or "edge"`, { found: refersTo });
|
|
373
|
+
}
|
|
374
|
+
const indexColumn = (dtype === "u32" && components === 1) || (dtype === "list" && itemDtype === "u32" && itemComponents === 1);
|
|
375
|
+
if (!indexColumn) {
|
|
376
|
+
throw typeError("refersTo", `refersTo requires a u32 column or a list of u32, not ${dtype}`, {
|
|
377
|
+
name,
|
|
378
|
+
dtype,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const defaultValue = decl.default;
|
|
383
|
+
if (defaultValue !== undefined) {
|
|
384
|
+
assertJsonValue(defaultValue, "default");
|
|
385
|
+
}
|
|
386
|
+
let options = null;
|
|
387
|
+
if (decl.options !== undefined) {
|
|
388
|
+
if (!Array.isArray(decl.options)) {
|
|
389
|
+
throw typeError("options", "options must be an array");
|
|
390
|
+
}
|
|
391
|
+
assertJsonValue(decl.options, "options");
|
|
392
|
+
if (dtype === "dict") {
|
|
393
|
+
for (const option of decl.options) {
|
|
394
|
+
if (typeof option !== "string") {
|
|
395
|
+
throw typeError("options", "options of a dict column must be strings", { found: typeof option });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
options = Object.freeze(cloneJson([...decl.options]));
|
|
400
|
+
}
|
|
401
|
+
const extra = decl.extra ?? {};
|
|
402
|
+
if (!isPlainObject(extra)) {
|
|
403
|
+
throw typeError("extra", "extra must be a plain object");
|
|
404
|
+
}
|
|
405
|
+
assertJsonValue(extra, "extra");
|
|
406
|
+
const role = decl.role ?? null;
|
|
407
|
+
if (role !== null && typeof role !== "string") {
|
|
408
|
+
throw typeError("role", "role must be a string", { found: typeof role });
|
|
409
|
+
}
|
|
410
|
+
const fill = resolveFill(dtype, components, decl.fill, defaultValue, options, refersTo);
|
|
411
|
+
return Object.freeze({
|
|
412
|
+
name,
|
|
413
|
+
domain,
|
|
414
|
+
dtype,
|
|
415
|
+
components,
|
|
416
|
+
itemDtype,
|
|
417
|
+
itemComponents,
|
|
418
|
+
nullable: decl.nullable ?? true,
|
|
419
|
+
mutable: decl.mutable ?? false,
|
|
420
|
+
role,
|
|
421
|
+
refersTo,
|
|
422
|
+
unique: decl.unique ?? false,
|
|
423
|
+
default: cloneJson(defaultValue),
|
|
424
|
+
fill,
|
|
425
|
+
options,
|
|
426
|
+
origin: resolveOrigin(decl.origin),
|
|
427
|
+
dynamic: decl.dynamic ?? false,
|
|
428
|
+
extra: Object.freeze(cloneJson({ ...extra })),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* The declaration that reproduces a column's metadata, for patching an existing column's meta
|
|
433
|
+
* (rename, move between tables, set() with a decl patch).
|
|
434
|
+
* @param meta - the metadata to convert
|
|
435
|
+
* @returns an equivalent declaration patch
|
|
436
|
+
*/
|
|
437
|
+
export function metaToDecl(meta) {
|
|
438
|
+
return {
|
|
439
|
+
name: meta.name,
|
|
440
|
+
dtype: meta.dtype,
|
|
441
|
+
components: meta.components,
|
|
442
|
+
itemDtype: meta.itemDtype ?? undefined,
|
|
443
|
+
itemComponents: meta.itemComponents ?? undefined,
|
|
444
|
+
nullable: meta.nullable,
|
|
445
|
+
mutable: meta.mutable,
|
|
446
|
+
role: meta.role ?? undefined,
|
|
447
|
+
refersTo: meta.refersTo ?? undefined,
|
|
448
|
+
unique: meta.unique,
|
|
449
|
+
default: meta.default,
|
|
450
|
+
fill: meta.fill,
|
|
451
|
+
options: meta.options ?? undefined,
|
|
452
|
+
origin: meta.origin ?? undefined,
|
|
453
|
+
dynamic: meta.dynamic,
|
|
454
|
+
extra: meta.extra,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
// ============================================================ column base
|
|
458
|
+
/**
|
|
459
|
+
* Members shared by every column implementation (design sections 5.3, 5.7 and 5.8).
|
|
460
|
+
*/
|
|
461
|
+
class ColumnImpl {
|
|
462
|
+
constructor(dtype, meta, length, validity, nullCount) {
|
|
463
|
+
this.versionValue = 0;
|
|
464
|
+
this.defaultCache = null;
|
|
465
|
+
this.dtype = dtype;
|
|
466
|
+
this.meta = meta;
|
|
467
|
+
this.length = length;
|
|
468
|
+
this.gpu = gpuEligibility(dtype);
|
|
469
|
+
this.validityWords = validity;
|
|
470
|
+
this.nullCountValue = nullCount;
|
|
471
|
+
this.defaultValue = cloneJson(meta.default);
|
|
472
|
+
}
|
|
473
|
+
get validity() {
|
|
474
|
+
return this.validityWords;
|
|
475
|
+
}
|
|
476
|
+
get nullCount() {
|
|
477
|
+
return this.nullCountValue;
|
|
478
|
+
}
|
|
479
|
+
get version() {
|
|
480
|
+
return this.versionValue;
|
|
481
|
+
}
|
|
482
|
+
get paddedByteLength() {
|
|
483
|
+
return this.byteLength;
|
|
484
|
+
}
|
|
485
|
+
get validityByteLength() {
|
|
486
|
+
return this.validityWords === null ? 0 : this.validityWords.byteLength;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Whether the column's storage was transferred away (design sections 9.1 and 11.3): derived from
|
|
490
|
+
* the array state, since a transferred ArrayBuffer leaves zero-length views behind.
|
|
491
|
+
* @returns true when a typed array the column needs is empty although the column has rows
|
|
492
|
+
*/
|
|
493
|
+
get detached() {
|
|
494
|
+
if (this.length === 0) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const words = this.validityWords;
|
|
498
|
+
return (words !== null && words.length === 0) || this.storageDetached();
|
|
499
|
+
}
|
|
500
|
+
isSet(row) {
|
|
501
|
+
if (!(row >= 0 && row < this.length)) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const words = this.validityWords;
|
|
505
|
+
if (words === null) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
if (words.length === 0) {
|
|
509
|
+
throw this.detachedError();
|
|
510
|
+
}
|
|
511
|
+
return bitmapGet(words, row);
|
|
512
|
+
}
|
|
513
|
+
value(row) {
|
|
514
|
+
this.assertAttached();
|
|
515
|
+
if (!(Number.isInteger(row) && row >= 0 && row < this.length)) {
|
|
516
|
+
throw new GraphFormatError("E_INDEX_RANGE", `row ${row} is out of range for a column of ${this.length} rows`, {
|
|
517
|
+
row,
|
|
518
|
+
length: this.length,
|
|
519
|
+
column: this.meta.name,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const words = this.validityWords;
|
|
523
|
+
if (words !== null && !bitmapGet(words, row)) {
|
|
524
|
+
// an unset row reads the column's own copy of the declared default; a structured default is
|
|
525
|
+
// handed out as a fresh copy so no caller can change what later reads see (design section 5.8)
|
|
526
|
+
const { defaultValue } = this;
|
|
527
|
+
return defaultValue === undefined ? undefined : cloneJson(defaultValue);
|
|
528
|
+
}
|
|
529
|
+
return this.readValue(row);
|
|
530
|
+
}
|
|
531
|
+
materializeDefault() {
|
|
532
|
+
this.assertAttached();
|
|
533
|
+
if (this.meta.default === undefined) {
|
|
534
|
+
throw new GraphFormatError("E_NO_DEFAULT", `column "${this.meta.name}" declares no default`, {
|
|
535
|
+
column: this.meta.name,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
if (this.nullCountValue === 0 || this.fillIsDefault()) {
|
|
539
|
+
return this.self();
|
|
540
|
+
}
|
|
541
|
+
this.defaultCache ?? (this.defaultCache = this.withDefaults());
|
|
542
|
+
return this.defaultCache;
|
|
543
|
+
}
|
|
544
|
+
paddedU32View() {
|
|
545
|
+
throw new GraphFormatError("E_GPU_INELIGIBLE", `a ${this.dtype} column has no u32 view`, {
|
|
546
|
+
column: this.meta.name,
|
|
547
|
+
dtype: this.dtype,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
markDirty() {
|
|
551
|
+
this.assertMutable("markDirty");
|
|
552
|
+
this.assertAttached();
|
|
553
|
+
this.versionValue++;
|
|
554
|
+
this.defaultCache = null;
|
|
555
|
+
f32Cache.delete(this);
|
|
556
|
+
const words = this.validityWords;
|
|
557
|
+
if (words !== null) {
|
|
558
|
+
this.nullCountValue = this.length - bitmapCount(words, this.length);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
mutableValidity() {
|
|
562
|
+
this.assertMutable("mutableValidity");
|
|
563
|
+
return this.validityWords;
|
|
564
|
+
}
|
|
565
|
+
setAll() {
|
|
566
|
+
this.assertMutable("setAll");
|
|
567
|
+
this.validityWords = null;
|
|
568
|
+
this.nullCountValue = 0;
|
|
569
|
+
this.versionValue++;
|
|
570
|
+
this.defaultCache = null;
|
|
571
|
+
}
|
|
572
|
+
assertAttached() {
|
|
573
|
+
if (this.detached) {
|
|
574
|
+
throw this.detachedError();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
detachedError() {
|
|
578
|
+
return new GraphFormatError("E_DETACHED", `column "${this.meta.name}" was transferred away`, {
|
|
579
|
+
column: this.meta.name,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
assertMutable(operation) {
|
|
583
|
+
if (!this.meta.mutable) {
|
|
584
|
+
throw new GraphFormatError("E_COLUMN_IMMUTABLE", `${operation}() on immutable column "${this.meta.name}"`, {
|
|
585
|
+
column: this.meta.name,
|
|
586
|
+
operation,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
checkRange(start, end) {
|
|
591
|
+
this.assertAttached();
|
|
592
|
+
if (!(Number.isInteger(start) && Number.isInteger(end) && start >= 0 && start <= end && end <= this.length)) {
|
|
593
|
+
throw new GraphFormatError("E_INDEX_RANGE", `row range [${start}, ${end}) is invalid for ${this.length} rows`, {
|
|
594
|
+
start,
|
|
595
|
+
end,
|
|
596
|
+
length: this.length,
|
|
597
|
+
column: this.meta.name,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* The validity bitmap and null count of the row range [start, end); zero-copy at word boundaries.
|
|
603
|
+
* @param start - first row
|
|
604
|
+
* @param end - one past the last row
|
|
605
|
+
* @returns the bitmap over the range (null when every row is set) and its null count
|
|
606
|
+
*/
|
|
607
|
+
sliceValidity(start, end) {
|
|
608
|
+
const words = this.validityWords;
|
|
609
|
+
if (words === null) {
|
|
610
|
+
return { validity: null, nullCount: 0 };
|
|
611
|
+
}
|
|
612
|
+
const rows = end - start;
|
|
613
|
+
const validity = start % 32 === 0
|
|
614
|
+
? shareView(words.subarray(start >>> 5, (start >>> 5) + bitmapWordCount(rows)))
|
|
615
|
+
: bitmapSlice(words, start, end);
|
|
616
|
+
return { validity, nullCount: rows - bitmapCount(validity, rows) };
|
|
617
|
+
}
|
|
618
|
+
cloneValidity() {
|
|
619
|
+
this.assertAttached();
|
|
620
|
+
return this.validityWords === null ? null : this.validityWords.slice();
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Record that a zero-copy view aliases a buffer another column still holds (the owner count of
|
|
625
|
+
* design section 9.1): a slice attached to a second snapshot must never let a transfer detach the
|
|
626
|
+
* source, so the shared buffer is copied by `toWire({ transfer: true })` and `transferables()`.
|
|
627
|
+
* @param view - a subarray of a column's storage
|
|
628
|
+
* @returns the view
|
|
629
|
+
*/
|
|
630
|
+
function shareView(view) {
|
|
631
|
+
noteShared(view.buffer);
|
|
632
|
+
return view;
|
|
633
|
+
}
|
|
634
|
+
// ============================================================ fixed-width numeric columns
|
|
635
|
+
class FixedColumnImpl extends ColumnImpl {
|
|
636
|
+
constructor(dtype, meta, length, data, validity, nullCount) {
|
|
637
|
+
super(dtype, meta, length, validity, nullCount);
|
|
638
|
+
this.data = data;
|
|
639
|
+
}
|
|
640
|
+
get byteLength() {
|
|
641
|
+
return this.data.byteLength + this.validityByteLength;
|
|
642
|
+
}
|
|
643
|
+
mutableData() {
|
|
644
|
+
this.assertMutable("mutableData");
|
|
645
|
+
this.assertAttached();
|
|
646
|
+
return this.data;
|
|
647
|
+
}
|
|
648
|
+
storageDetached() {
|
|
649
|
+
return this.data.length === 0 && this.length * this.meta.components > 0;
|
|
650
|
+
}
|
|
651
|
+
slice(start, end) {
|
|
652
|
+
this.checkRange(start, end);
|
|
653
|
+
const { components } = this.meta;
|
|
654
|
+
const data = this.sliceData(start * components, end * components);
|
|
655
|
+
const { validity, nullCount } = this.sliceValidity(start, end);
|
|
656
|
+
return this.wrap(this.meta, end - start, data, validity, nullCount);
|
|
657
|
+
}
|
|
658
|
+
clone() {
|
|
659
|
+
this.assertAttached();
|
|
660
|
+
return this.wrap(this.meta, this.length, this.copyData(), this.cloneValidity(), this.nullCountValue);
|
|
661
|
+
}
|
|
662
|
+
readValue(row) {
|
|
663
|
+
const { components } = this.meta;
|
|
664
|
+
if (components === 1) {
|
|
665
|
+
return this.data[row];
|
|
666
|
+
}
|
|
667
|
+
return this.data.subarray(row * components, (row + 1) * components);
|
|
668
|
+
}
|
|
669
|
+
fillIsDefault() {
|
|
670
|
+
const { fill, default: defaultValue, components } = this.meta;
|
|
671
|
+
if (typeof fill !== "number") {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
return numericDefaultFill(this.dtype, components, defaultValue) === fill;
|
|
675
|
+
}
|
|
676
|
+
withDefaults() {
|
|
677
|
+
const { components, default: defaultValue } = this.meta;
|
|
678
|
+
const data = this.copyData();
|
|
679
|
+
const words = this.validityWords;
|
|
680
|
+
if (words !== null) {
|
|
681
|
+
for (let row = 0; row < this.length; row++) {
|
|
682
|
+
if (!bitmapGet(words, row)) {
|
|
683
|
+
writeNumeric(this.dtype, data, row * components, components, defaultValue, this.meta.name, row);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return this.wrap(this.meta, this.length, data, this.cloneValidity(), this.nullCountValue);
|
|
688
|
+
}
|
|
689
|
+
sliceData(start, end) {
|
|
690
|
+
return shareView(this.data.subarray(start, end));
|
|
691
|
+
}
|
|
692
|
+
copyData() {
|
|
693
|
+
return this.data.slice();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Write one row's numeric value (a number, or an array of `components` numbers) into a typed array,
|
|
698
|
+
* rejecting values the dtype cannot represent (a non-integer or out-of-range number for i32 / u32 /
|
|
699
|
+
* u8) with E_COLUMN_TYPE instead of letting the typed array wrap them.
|
|
700
|
+
* @param dtype - the numeric dtype
|
|
701
|
+
* @param data - the destination
|
|
702
|
+
* @param at - the element offset of the row
|
|
703
|
+
* @param components - the stride
|
|
704
|
+
* @param value - the value to write
|
|
705
|
+
* @param column - the column name for error details
|
|
706
|
+
* @param row - the row for error details
|
|
707
|
+
*/
|
|
708
|
+
function writeNumeric(dtype, data, at, components, value, column, row) {
|
|
709
|
+
if (typeof value === "number") {
|
|
710
|
+
assertRepresentable(dtype, value, column, row);
|
|
711
|
+
if (components === 1) {
|
|
712
|
+
data[at] = value;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
data.fill(value, at, at + components);
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (components === 1 || !isArrayLikeNumbers(value, components)) {
|
|
720
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `row ${row} of ${dtype} column "${column}" is not ${components} number(s)`, { column, row, components, found: typeof value });
|
|
721
|
+
}
|
|
722
|
+
for (let k = 0; k < components; k++) {
|
|
723
|
+
assertRepresentable(dtype, value[k], column, row);
|
|
724
|
+
}
|
|
725
|
+
for (let k = 0; k < components; k++) {
|
|
726
|
+
data[at + k] = value[k];
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function assertRepresentable(dtype, value, column, row) {
|
|
730
|
+
if (!representableNumber(dtype, value)) {
|
|
731
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `row ${row} of ${dtype} column "${column}": ${value} is not representable`, { column, row, value });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Whether a value is an array-like of exactly `length` numbers (a multi-component cell).
|
|
736
|
+
* @param value - the value
|
|
737
|
+
* @param length - the required length
|
|
738
|
+
* @returns true for a numeric vector of that length
|
|
739
|
+
*/
|
|
740
|
+
export function isArrayLikeNumbers(value, length) {
|
|
741
|
+
if (typeof value !== "object" || value === null) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
const arrayLike = value;
|
|
745
|
+
if (arrayLike.length !== length) {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
for (let k = 0; k < length; k++) {
|
|
749
|
+
if (typeof arrayLike[k] !== "number") {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
class F32ColumnImpl extends FixedColumnImpl {
|
|
756
|
+
constructor(meta, length, data, validity, nullCount) {
|
|
757
|
+
super("f32", meta, length, data, validity, nullCount);
|
|
758
|
+
}
|
|
759
|
+
self() {
|
|
760
|
+
return this;
|
|
761
|
+
}
|
|
762
|
+
wrap(meta, length, data, validity, nullCount) {
|
|
763
|
+
return new F32ColumnImpl(meta, length, data, validity, nullCount);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
class F64ColumnImpl extends FixedColumnImpl {
|
|
767
|
+
constructor(meta, length, data, validity, nullCount) {
|
|
768
|
+
super("f64", meta, length, data, validity, nullCount);
|
|
769
|
+
}
|
|
770
|
+
self() {
|
|
771
|
+
return this;
|
|
772
|
+
}
|
|
773
|
+
wrap(meta, length, data, validity, nullCount) {
|
|
774
|
+
return new F64ColumnImpl(meta, length, data, validity, nullCount);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
class I32ColumnImpl extends FixedColumnImpl {
|
|
778
|
+
constructor(meta, length, data, validity, nullCount) {
|
|
779
|
+
super("i32", meta, length, data, validity, nullCount);
|
|
780
|
+
}
|
|
781
|
+
self() {
|
|
782
|
+
return this;
|
|
783
|
+
}
|
|
784
|
+
wrap(meta, length, data, validity, nullCount) {
|
|
785
|
+
return new I32ColumnImpl(meta, length, data, validity, nullCount);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
class U32ColumnImpl extends FixedColumnImpl {
|
|
789
|
+
constructor(meta, length, data, validity, nullCount) {
|
|
790
|
+
super("u32", meta, length, data, validity, nullCount);
|
|
791
|
+
}
|
|
792
|
+
paddedU32View() {
|
|
793
|
+
return this.data;
|
|
794
|
+
}
|
|
795
|
+
self() {
|
|
796
|
+
return this;
|
|
797
|
+
}
|
|
798
|
+
wrap(meta, length, data, validity, nullCount) {
|
|
799
|
+
return new U32ColumnImpl(meta, length, data, validity, nullCount);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
class U8ColumnImpl extends FixedColumnImpl {
|
|
803
|
+
constructor(meta, length, data, validity, nullCount) {
|
|
804
|
+
super("u8", meta, length, data, validity, nullCount);
|
|
805
|
+
}
|
|
806
|
+
get paddedByteLength() {
|
|
807
|
+
return padTo4(this.data.byteLength) + this.validityByteLength;
|
|
808
|
+
}
|
|
809
|
+
paddedU32View() {
|
|
810
|
+
return paddedU32View(this.data);
|
|
811
|
+
}
|
|
812
|
+
self() {
|
|
813
|
+
return this;
|
|
814
|
+
}
|
|
815
|
+
wrap(meta, length, data, validity, nullCount) {
|
|
816
|
+
return new U8ColumnImpl(meta, length, data, validity, nullCount);
|
|
817
|
+
}
|
|
818
|
+
sliceData(start, end) {
|
|
819
|
+
if (start % 4 === 0) {
|
|
820
|
+
return shareView(this.data.subarray(start, end));
|
|
821
|
+
}
|
|
822
|
+
const out = allocU8(end - start);
|
|
823
|
+
out.set(this.data.subarray(start, end));
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
copyData() {
|
|
827
|
+
const out = allocU8(this.data.length);
|
|
828
|
+
out.set(this.data);
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// ============================================================ bool column
|
|
833
|
+
class BoolColumnImpl extends ColumnImpl {
|
|
834
|
+
constructor(meta, length, data, validity, nullCount) {
|
|
835
|
+
super("bool", meta, length, validity, nullCount);
|
|
836
|
+
this.data = data;
|
|
837
|
+
}
|
|
838
|
+
get byteLength() {
|
|
839
|
+
return this.data.byteLength + this.validityByteLength;
|
|
840
|
+
}
|
|
841
|
+
mutableData() {
|
|
842
|
+
this.assertMutable("mutableData");
|
|
843
|
+
this.assertAttached();
|
|
844
|
+
return this.data;
|
|
845
|
+
}
|
|
846
|
+
paddedU32View() {
|
|
847
|
+
this.assertAttached();
|
|
848
|
+
return this.data;
|
|
849
|
+
}
|
|
850
|
+
slice(start, end) {
|
|
851
|
+
this.checkRange(start, end);
|
|
852
|
+
const rows = end - start;
|
|
853
|
+
const data = start % 32 === 0
|
|
854
|
+
? shareView(this.data.subarray(start >>> 5, (start >>> 5) + bitmapWordCount(rows)))
|
|
855
|
+
: bitmapSlice(this.data, start, end);
|
|
856
|
+
const { validity, nullCount } = this.sliceValidity(start, end);
|
|
857
|
+
return new BoolColumnImpl(this.meta, rows, data, validity, nullCount);
|
|
858
|
+
}
|
|
859
|
+
clone() {
|
|
860
|
+
this.assertAttached();
|
|
861
|
+
return new BoolColumnImpl(this.meta, this.length, this.data.slice(), this.cloneValidity(), this.nullCountValue);
|
|
862
|
+
}
|
|
863
|
+
storageDetached() {
|
|
864
|
+
return this.data.length === 0;
|
|
865
|
+
}
|
|
866
|
+
self() {
|
|
867
|
+
return this;
|
|
868
|
+
}
|
|
869
|
+
readValue(row) {
|
|
870
|
+
return bitmapGet(this.data, row);
|
|
871
|
+
}
|
|
872
|
+
fillIsDefault() {
|
|
873
|
+
return this.meta.fill === this.meta.default;
|
|
874
|
+
}
|
|
875
|
+
withDefaults() {
|
|
876
|
+
const { default: defaultValue, name } = this.meta;
|
|
877
|
+
if (typeof defaultValue !== "boolean") {
|
|
878
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `default of bool column "${name}" is not a boolean`, {
|
|
879
|
+
column: name,
|
|
880
|
+
field: "default",
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const data = this.data.slice();
|
|
884
|
+
const words = this.validityWords;
|
|
885
|
+
if (words !== null) {
|
|
886
|
+
for (let row = 0; row < this.length; row++) {
|
|
887
|
+
if (!bitmapGet(words, row)) {
|
|
888
|
+
if (defaultValue) {
|
|
889
|
+
bitmapSet(data, row);
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
bitmapClear(data, row);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return new BoolColumnImpl(this.meta, this.length, data, this.cloneValidity(), this.nullCountValue);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
// ============================================================ dict column
|
|
901
|
+
class DictColumnImpl extends ColumnImpl {
|
|
902
|
+
constructor(meta, length, codes, dictionary, validity, nullCount) {
|
|
903
|
+
super("dict", meta, length, validity, nullCount);
|
|
904
|
+
this.codeMap = null;
|
|
905
|
+
this.codes = codes;
|
|
906
|
+
this.dictionary = dictionary;
|
|
907
|
+
}
|
|
908
|
+
get byteLength() {
|
|
909
|
+
return this.codes.byteLength + this.validityByteLength;
|
|
910
|
+
}
|
|
911
|
+
codeOf(value) {
|
|
912
|
+
this.codeMap ?? (this.codeMap = buildCodeMap(this.dictionary));
|
|
913
|
+
const code = this.codeMap.get(value);
|
|
914
|
+
return code === undefined ? INVALID_INDEX : code;
|
|
915
|
+
}
|
|
916
|
+
mutableData() {
|
|
917
|
+
this.assertMutable("mutableData");
|
|
918
|
+
this.assertAttached();
|
|
919
|
+
return this.codes;
|
|
920
|
+
}
|
|
921
|
+
paddedU32View() {
|
|
922
|
+
this.assertAttached();
|
|
923
|
+
return this.codes;
|
|
924
|
+
}
|
|
925
|
+
slice(start, end) {
|
|
926
|
+
this.checkRange(start, end);
|
|
927
|
+
const { validity, nullCount } = this.sliceValidity(start, end);
|
|
928
|
+
return new DictColumnImpl(this.meta, end - start, shareView(this.codes.subarray(start, end)), this.dictionary, validity, nullCount);
|
|
929
|
+
}
|
|
930
|
+
storageDetached() {
|
|
931
|
+
return this.codes.length === 0;
|
|
932
|
+
}
|
|
933
|
+
clone() {
|
|
934
|
+
this.assertAttached();
|
|
935
|
+
return new DictColumnImpl(this.meta, this.length, this.codes.slice(), [...this.dictionary], this.cloneValidity(), this.nullCountValue);
|
|
936
|
+
}
|
|
937
|
+
self() {
|
|
938
|
+
return this;
|
|
939
|
+
}
|
|
940
|
+
readValue(row) {
|
|
941
|
+
return this.dictionary[this.codes[row]];
|
|
942
|
+
}
|
|
943
|
+
fillIsDefault() {
|
|
944
|
+
return this.meta.fill === this.meta.default;
|
|
945
|
+
}
|
|
946
|
+
withDefaults() {
|
|
947
|
+
const { default: defaultValue, name } = this.meta;
|
|
948
|
+
if (typeof defaultValue !== "string") {
|
|
949
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `default of dict column "${name}" is not a string`, {
|
|
950
|
+
column: name,
|
|
951
|
+
field: "default",
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
let dict = this.dictionary;
|
|
955
|
+
let code = this.codeOf(defaultValue);
|
|
956
|
+
if (code === INVALID_INDEX) {
|
|
957
|
+
code = dict.length;
|
|
958
|
+
dict = [...dict, defaultValue];
|
|
959
|
+
}
|
|
960
|
+
const codes = this.codes.slice();
|
|
961
|
+
const words = this.validityWords;
|
|
962
|
+
if (words !== null) {
|
|
963
|
+
for (let row = 0; row < this.length; row++) {
|
|
964
|
+
if (!bitmapGet(words, row)) {
|
|
965
|
+
codes[row] = code;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return new DictColumnImpl(this.meta, this.length, codes, dict, this.cloneValidity(), this.nullCountValue);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// ============================================================ string column
|
|
973
|
+
/**
|
|
974
|
+
* Encode strings into an Arrow Utf8 store: rows + 1 offsets and the concatenated bytes in a padded
|
|
975
|
+
* u8 store.
|
|
976
|
+
* @param strings - the rows
|
|
977
|
+
* @returns the offsets and bytes
|
|
978
|
+
*/
|
|
979
|
+
function encodeStrings(strings) {
|
|
980
|
+
const encoded = encodeUtf8Rows(strings, strings.length);
|
|
981
|
+
const utf8 = allocU8(encoded.utf8.length);
|
|
982
|
+
utf8.set(encoded.utf8);
|
|
983
|
+
return { offsets: encoded.offsets, utf8 };
|
|
984
|
+
}
|
|
985
|
+
class StringColumnImpl extends ColumnImpl {
|
|
986
|
+
constructor(meta, length, offsets, utf8, strings, validity, nullCount) {
|
|
987
|
+
super("string", meta, length, validity, nullCount);
|
|
988
|
+
this.offsetsStore = offsets;
|
|
989
|
+
this.utf8Store = utf8;
|
|
990
|
+
this.strings = strings;
|
|
991
|
+
}
|
|
992
|
+
get offsets() {
|
|
993
|
+
this.store();
|
|
994
|
+
return this.offsetsStore;
|
|
995
|
+
}
|
|
996
|
+
get utf8() {
|
|
997
|
+
this.store();
|
|
998
|
+
return this.utf8Store;
|
|
999
|
+
}
|
|
1000
|
+
get byteLength() {
|
|
1001
|
+
this.store();
|
|
1002
|
+
return this.offsetsStore.byteLength + this.utf8Store.byteLength + this.validityByteLength;
|
|
1003
|
+
}
|
|
1004
|
+
valueAt(row) {
|
|
1005
|
+
const cached = this.strings?.[row];
|
|
1006
|
+
if (cached !== undefined) {
|
|
1007
|
+
return cached;
|
|
1008
|
+
}
|
|
1009
|
+
this.assertAttached();
|
|
1010
|
+
this.store();
|
|
1011
|
+
const offsets = this.offsetsStore;
|
|
1012
|
+
const utf8 = this.utf8Store;
|
|
1013
|
+
const text = decoder.decode(utf8.subarray(offsets[row], offsets[row + 1]));
|
|
1014
|
+
this.strings ?? (this.strings = new Array(this.length));
|
|
1015
|
+
this.strings[row] = text;
|
|
1016
|
+
return text;
|
|
1017
|
+
}
|
|
1018
|
+
decodeAll() {
|
|
1019
|
+
const out = new Array(this.length);
|
|
1020
|
+
for (let row = 0; row < this.length; row++) {
|
|
1021
|
+
out[row] = this.valueAt(row);
|
|
1022
|
+
}
|
|
1023
|
+
return out;
|
|
1024
|
+
}
|
|
1025
|
+
slice(start, end) {
|
|
1026
|
+
this.checkRange(start, end);
|
|
1027
|
+
const rows = end - start;
|
|
1028
|
+
const { validity, nullCount } = this.sliceValidity(start, end);
|
|
1029
|
+
const strings = this.strings === null ? null : this.strings.slice(start, end);
|
|
1030
|
+
if (this.offsetsStore === null || this.utf8Store === null) {
|
|
1031
|
+
return new StringColumnImpl(this.meta, rows, null, null, strings, validity, nullCount);
|
|
1032
|
+
}
|
|
1033
|
+
const base = this.offsetsStore[start];
|
|
1034
|
+
const offsets = new Uint32Array(rows + 1);
|
|
1035
|
+
for (let i = 0; i <= rows; i++) {
|
|
1036
|
+
offsets[i] = this.offsetsStore[start + i] - base;
|
|
1037
|
+
}
|
|
1038
|
+
const utf8 = shareView(this.utf8Store.subarray(base, this.offsetsStore[end]));
|
|
1039
|
+
return new StringColumnImpl(this.meta, rows, offsets, utf8, strings, validity, nullCount);
|
|
1040
|
+
}
|
|
1041
|
+
clone() {
|
|
1042
|
+
this.assertAttached();
|
|
1043
|
+
const strings = this.strings === null ? null : [...this.strings];
|
|
1044
|
+
if (this.offsetsStore === null || this.utf8Store === null) {
|
|
1045
|
+
return new StringColumnImpl(this.meta, this.length, null, null, strings, this.cloneValidity(), this.nullCountValue);
|
|
1046
|
+
}
|
|
1047
|
+
const utf8 = allocU8(this.utf8Store.length);
|
|
1048
|
+
utf8.set(this.utf8Store);
|
|
1049
|
+
return new StringColumnImpl(this.meta, this.length, this.offsetsStore.slice(), utf8, strings, this.cloneValidity(), this.nullCountValue);
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Whether the Utf8 store has been materialised (the wire writer asks before encoding).
|
|
1053
|
+
* @returns true when offsets and utf8 exist without being computed
|
|
1054
|
+
*/
|
|
1055
|
+
hasStore() {
|
|
1056
|
+
return this.offsetsStore !== null;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* The decoded cache as it stands (sparse), for zero-copy re-wrapping.
|
|
1060
|
+
* @returns the cache, or null when no row has been decoded
|
|
1061
|
+
*/
|
|
1062
|
+
decodedCache() {
|
|
1063
|
+
return this.strings;
|
|
1064
|
+
}
|
|
1065
|
+
self() {
|
|
1066
|
+
return this;
|
|
1067
|
+
}
|
|
1068
|
+
storageDetached() {
|
|
1069
|
+
// a materialised store is detached when its offsets vanished; undecoded rows then cannot be read
|
|
1070
|
+
return this.offsetsStore !== null && this.offsetsStore.length === 0;
|
|
1071
|
+
}
|
|
1072
|
+
readValue(row) {
|
|
1073
|
+
return this.valueAt(row);
|
|
1074
|
+
}
|
|
1075
|
+
fillIsDefault() {
|
|
1076
|
+
return this.meta.fill === this.meta.default;
|
|
1077
|
+
}
|
|
1078
|
+
withDefaults() {
|
|
1079
|
+
const { default: defaultValue, name } = this.meta;
|
|
1080
|
+
if (typeof defaultValue !== "string") {
|
|
1081
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `default of string column "${name}" is not a string`, {
|
|
1082
|
+
column: name,
|
|
1083
|
+
field: "default",
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
const strings = this.decodeAll();
|
|
1087
|
+
const words = this.validityWords;
|
|
1088
|
+
if (words !== null) {
|
|
1089
|
+
for (let row = 0; row < this.length; row++) {
|
|
1090
|
+
if (!bitmapGet(words, row)) {
|
|
1091
|
+
strings[row] = defaultValue;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return new StringColumnImpl(this.meta, this.length, null, null, strings, this.cloneValidity(), this.nullCountValue);
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Materialise the Utf8 store from the decoded strings on first need (design section 5.7); a
|
|
1099
|
+
* store that was transferred away is E_DETACHED.
|
|
1100
|
+
*/
|
|
1101
|
+
store() {
|
|
1102
|
+
if (this.offsetsStore !== null && this.utf8Store !== null) {
|
|
1103
|
+
this.assertAttached();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const strings = this.strings ?? [];
|
|
1107
|
+
const dense = new Array(this.length);
|
|
1108
|
+
for (let row = 0; row < this.length; row++) {
|
|
1109
|
+
dense[row] = strings[row] ?? "";
|
|
1110
|
+
}
|
|
1111
|
+
const encoded = encodeStrings(dense);
|
|
1112
|
+
this.offsetsStore = encoded.offsets;
|
|
1113
|
+
this.utf8Store = encoded.utf8;
|
|
1114
|
+
// the column is the first holder of the store it materialises (design section 9.1)
|
|
1115
|
+
claimHolder(encoded.offsets.buffer);
|
|
1116
|
+
claimHolder(encoded.utf8.buffer);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
class ListColumnImpl extends ColumnImpl {
|
|
1120
|
+
constructor(meta, length, offsets, child, validity, nullCount) {
|
|
1121
|
+
super("list", meta, length, validity, nullCount);
|
|
1122
|
+
this.offsets = offsets;
|
|
1123
|
+
this.child = child;
|
|
1124
|
+
}
|
|
1125
|
+
get byteLength() {
|
|
1126
|
+
return this.offsets.byteLength + this.child.byteLength + this.validityByteLength;
|
|
1127
|
+
}
|
|
1128
|
+
sliceOf(row) {
|
|
1129
|
+
this.assertAttached();
|
|
1130
|
+
const start = this.offsets[row];
|
|
1131
|
+
const end = this.offsets[row + 1];
|
|
1132
|
+
const out = new Array(end - start);
|
|
1133
|
+
for (let i = start; i < end; i++) {
|
|
1134
|
+
out[i - start] = this.child.value(i);
|
|
1135
|
+
}
|
|
1136
|
+
return out;
|
|
1137
|
+
}
|
|
1138
|
+
slice(start, end) {
|
|
1139
|
+
this.checkRange(start, end);
|
|
1140
|
+
const rows = end - start;
|
|
1141
|
+
const base = this.offsets[start];
|
|
1142
|
+
const offsets = new Uint32Array(rows + 1);
|
|
1143
|
+
for (let i = 0; i <= rows; i++) {
|
|
1144
|
+
offsets[i] = this.offsets[start + i] - base;
|
|
1145
|
+
}
|
|
1146
|
+
const { validity, nullCount } = this.sliceValidity(start, end);
|
|
1147
|
+
return new ListColumnImpl(this.meta, rows, offsets, this.child.slice(base, this.offsets[end]), validity, nullCount);
|
|
1148
|
+
}
|
|
1149
|
+
clone() {
|
|
1150
|
+
this.assertAttached();
|
|
1151
|
+
return new ListColumnImpl(this.meta, this.length, this.offsets.slice(), this.child.clone(), this.cloneValidity(), this.nullCountValue);
|
|
1152
|
+
}
|
|
1153
|
+
storageDetached() {
|
|
1154
|
+
return this.offsets.length === 0 || isColumnDetached(this.child);
|
|
1155
|
+
}
|
|
1156
|
+
self() {
|
|
1157
|
+
return this;
|
|
1158
|
+
}
|
|
1159
|
+
readValue(row) {
|
|
1160
|
+
return this.sliceOf(row);
|
|
1161
|
+
}
|
|
1162
|
+
fillIsDefault() {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
withDefaults() {
|
|
1166
|
+
const { default: defaultValue, name } = this.meta;
|
|
1167
|
+
if (!Array.isArray(defaultValue)) {
|
|
1168
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `default of list column "${name}" is not an array`, {
|
|
1169
|
+
column: name,
|
|
1170
|
+
field: "default",
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
const rows = new Array(this.length);
|
|
1174
|
+
const words = this.validityWords;
|
|
1175
|
+
for (let row = 0; row < this.length; row++) {
|
|
1176
|
+
rows[row] = words !== null && !bitmapGet(words, row) ? defaultValue : this.sliceOf(row);
|
|
1177
|
+
}
|
|
1178
|
+
const parts = listPartsFromValues(this.meta, rows);
|
|
1179
|
+
return new ListColumnImpl(this.meta, this.length, parts.offsets, parts.child, this.cloneValidity(), this.nullCountValue);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
// ============================================================ json column
|
|
1183
|
+
class JsonColumnImpl extends ColumnImpl {
|
|
1184
|
+
constructor(meta, length, values, validity, nullCount) {
|
|
1185
|
+
super("json", meta, length, validity, nullCount);
|
|
1186
|
+
this.values = values;
|
|
1187
|
+
}
|
|
1188
|
+
get byteLength() {
|
|
1189
|
+
return this.validityByteLength;
|
|
1190
|
+
}
|
|
1191
|
+
slice(start, end) {
|
|
1192
|
+
this.checkRange(start, end);
|
|
1193
|
+
const { validity, nullCount } = this.sliceValidity(start, end);
|
|
1194
|
+
return new JsonColumnImpl(this.meta, end - start, this.values.slice(start, end), validity, nullCount);
|
|
1195
|
+
}
|
|
1196
|
+
clone() {
|
|
1197
|
+
this.assertAttached();
|
|
1198
|
+
return new JsonColumnImpl(this.meta, this.length, [...this.values], this.cloneValidity(), this.nullCountValue);
|
|
1199
|
+
}
|
|
1200
|
+
storageDetached() {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
self() {
|
|
1204
|
+
return this;
|
|
1205
|
+
}
|
|
1206
|
+
readValue(row) {
|
|
1207
|
+
return this.values[row];
|
|
1208
|
+
}
|
|
1209
|
+
fillIsDefault() {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
withDefaults() {
|
|
1213
|
+
const { default: defaultValue } = this.meta;
|
|
1214
|
+
const values = [...this.values];
|
|
1215
|
+
const words = this.validityWords;
|
|
1216
|
+
if (words !== null) {
|
|
1217
|
+
for (let row = 0; row < this.length; row++) {
|
|
1218
|
+
if (!bitmapGet(words, row)) {
|
|
1219
|
+
values[row] = defaultValue;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return new JsonColumnImpl(this.meta, this.length, values, this.cloneValidity(), this.nullCountValue);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Whether a value is a Column built by this package's factory (the structural Column type also
|
|
1228
|
+
* admits a foreign object with the right members, which the table refuses with E_COLUMN_TYPE).
|
|
1229
|
+
* @param value - the candidate
|
|
1230
|
+
* @returns true for an instance of a column implementation
|
|
1231
|
+
*/
|
|
1232
|
+
export function isPackageColumn(value) {
|
|
1233
|
+
return value instanceof ColumnImpl;
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Whether a column's storage was transferred away (design section 9.1): derived from the array
|
|
1237
|
+
* state. Every accessor of such a column throws E_DETACHED; the wire module and the checksum
|
|
1238
|
+
* comparison ask first.
|
|
1239
|
+
* @param column - any column
|
|
1240
|
+
* @returns true when detached
|
|
1241
|
+
*/
|
|
1242
|
+
export function isColumnDetached(column) {
|
|
1243
|
+
return column.detached;
|
|
1244
|
+
}
|
|
1245
|
+
// ============================================================ factory
|
|
1246
|
+
function lengthError(meta, what, expected, found) {
|
|
1247
|
+
return new GraphFormatError("E_COLUMN_LENGTH", `column "${meta.name}": ${what} has length ${found}, expected ${expected}`, {
|
|
1248
|
+
column: meta.name,
|
|
1249
|
+
what,
|
|
1250
|
+
expected,
|
|
1251
|
+
found,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
function slotError(meta, message) {
|
|
1255
|
+
return new GraphFormatError("E_COLUMN_TYPE", `column "${meta.name}": ${message}`, { column: meta.name });
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Wrap raw column storage into a Column of the dtype named by parts.meta (the column factory of
|
|
1259
|
+
* design section 5.7). Checks the slot use and the length rules of invariant I12 (E_COLUMN_LENGTH /
|
|
1260
|
+
* E_COLUMN_TYPE / E_COLUMN_ALIGNMENT), recomputes nullCount from the validity bitmap, and adopts
|
|
1261
|
+
* every buffer by reference.
|
|
1262
|
+
* @param parts - the storage slots; every slot the dtype does not use must be null
|
|
1263
|
+
* @returns the column
|
|
1264
|
+
*/
|
|
1265
|
+
export function createColumn(parts) {
|
|
1266
|
+
const { meta, length } = parts;
|
|
1267
|
+
if (!(Number.isInteger(length) && length >= 0)) {
|
|
1268
|
+
throw new GraphFormatError("E_COLUMN_LENGTH", `column "${meta.name}": invalid row count ${length}`, {
|
|
1269
|
+
column: meta.name,
|
|
1270
|
+
found: length,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
const { validity: given } = parts;
|
|
1274
|
+
let validity = null;
|
|
1275
|
+
if (given !== null) {
|
|
1276
|
+
if (!meta.nullable) {
|
|
1277
|
+
throw slotError(meta, "a non-nullable column cannot carry a validity bitmap");
|
|
1278
|
+
}
|
|
1279
|
+
if (!(given instanceof Uint32Array)) {
|
|
1280
|
+
throw slotError(meta, "validity must be a Uint32Array");
|
|
1281
|
+
}
|
|
1282
|
+
if (given.length !== bitmapWordCount(length)) {
|
|
1283
|
+
throw lengthError(meta, "validity", bitmapWordCount(length), given.length);
|
|
1284
|
+
}
|
|
1285
|
+
validity = given;
|
|
1286
|
+
}
|
|
1287
|
+
const nullCount = validity === null ? 0 : length - bitmapCount(validity, length);
|
|
1288
|
+
const { dtype } = meta;
|
|
1289
|
+
switch (dtype) {
|
|
1290
|
+
case "f32":
|
|
1291
|
+
return new F32ColumnImpl(meta, length, checkNumeric(parts, Float32Array), validity, nullCount);
|
|
1292
|
+
case "f64":
|
|
1293
|
+
return new F64ColumnImpl(meta, length, checkNumeric(parts, Float64Array), validity, nullCount);
|
|
1294
|
+
case "i32":
|
|
1295
|
+
return new I32ColumnImpl(meta, length, checkNumeric(parts, Int32Array), validity, nullCount);
|
|
1296
|
+
case "u32":
|
|
1297
|
+
return new U32ColumnImpl(meta, length, checkNumeric(parts, Uint32Array), validity, nullCount);
|
|
1298
|
+
case "u8": {
|
|
1299
|
+
const data = checkNumeric(parts, Uint8Array);
|
|
1300
|
+
if (!canViewAsPaddedU32(data)) {
|
|
1301
|
+
throw new GraphFormatError("E_COLUMN_ALIGNMENT", `column "${meta.name}": no padded u32 view over the u8 data`, {
|
|
1302
|
+
column: meta.name,
|
|
1303
|
+
byteOffset: data.byteOffset,
|
|
1304
|
+
byteLength: data.byteLength,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
return new U8ColumnImpl(meta, length, data, validity, nullCount);
|
|
1308
|
+
}
|
|
1309
|
+
case "bool": {
|
|
1310
|
+
if (!(parts.data instanceof Uint32Array)) {
|
|
1311
|
+
throw slotError(meta, "bool data must be packed Uint32Array words");
|
|
1312
|
+
}
|
|
1313
|
+
if (parts.data.length !== bitmapWordCount(length)) {
|
|
1314
|
+
throw lengthError(meta, "data", bitmapWordCount(length), parts.data.length);
|
|
1315
|
+
}
|
|
1316
|
+
return new BoolColumnImpl(meta, length, parts.data, validity, nullCount);
|
|
1317
|
+
}
|
|
1318
|
+
case "dict": {
|
|
1319
|
+
if (!(parts.data instanceof Uint32Array)) {
|
|
1320
|
+
throw slotError(meta, "dict codes must be a Uint32Array");
|
|
1321
|
+
}
|
|
1322
|
+
if (parts.data.length !== length) {
|
|
1323
|
+
throw lengthError(meta, "codes", length, parts.data.length);
|
|
1324
|
+
}
|
|
1325
|
+
if (parts.dictionary === null) {
|
|
1326
|
+
throw slotError(meta, "dict column needs a dictionary");
|
|
1327
|
+
}
|
|
1328
|
+
return new DictColumnImpl(meta, length, parts.data, parts.dictionary, validity, nullCount);
|
|
1329
|
+
}
|
|
1330
|
+
case "string": {
|
|
1331
|
+
if (parts.offsets === null || parts.utf8 === null) {
|
|
1332
|
+
const { strings } = parts;
|
|
1333
|
+
if (strings === null || strings.length !== length) {
|
|
1334
|
+
throw lengthError(meta, "strings", length, strings === null ? -1 : strings.length);
|
|
1335
|
+
}
|
|
1336
|
+
for (let row = 0; row < length; row++) {
|
|
1337
|
+
if (typeof strings[row] !== "string") {
|
|
1338
|
+
throw slotError(meta, `row ${row} is not a decoded string and no Utf8 store is present`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return new StringColumnImpl(meta, length, null, null, strings, validity, nullCount);
|
|
1342
|
+
}
|
|
1343
|
+
if (parts.offsets.length !== length + 1) {
|
|
1344
|
+
throw lengthError(meta, "offsets", length + 1, parts.offsets.length);
|
|
1345
|
+
}
|
|
1346
|
+
if (parts.offsets[length] !== parts.utf8.length) {
|
|
1347
|
+
throw lengthError(meta, "utf8", parts.offsets[length], parts.utf8.length);
|
|
1348
|
+
}
|
|
1349
|
+
return new StringColumnImpl(meta, length, parts.offsets, parts.utf8, parts.strings, validity, nullCount);
|
|
1350
|
+
}
|
|
1351
|
+
case "list": {
|
|
1352
|
+
if (parts.offsets === null || parts.offsets.length !== length + 1) {
|
|
1353
|
+
throw lengthError(meta, "offsets", length + 1, parts.offsets === null ? -1 : parts.offsets.length);
|
|
1354
|
+
}
|
|
1355
|
+
const { child } = parts;
|
|
1356
|
+
if (child === null || child.dtype === "list") {
|
|
1357
|
+
throw slotError(meta, "list column needs a non-list child column");
|
|
1358
|
+
}
|
|
1359
|
+
if (child.validity !== null || child.meta.nullable) {
|
|
1360
|
+
throw slotError(meta, "list child must be non-nullable");
|
|
1361
|
+
}
|
|
1362
|
+
if (child.dtype !== meta.itemDtype || child.meta.components !== meta.itemComponents) {
|
|
1363
|
+
throw slotError(meta, `list child is ${child.dtype} x${child.meta.components}, declared ${String(meta.itemDtype)} x${String(meta.itemComponents)}`);
|
|
1364
|
+
}
|
|
1365
|
+
if (parts.offsets[length] !== child.length) {
|
|
1366
|
+
throw lengthError(meta, "child", parts.offsets[length], child.length);
|
|
1367
|
+
}
|
|
1368
|
+
return new ListColumnImpl(meta, length, parts.offsets, child, validity, nullCount);
|
|
1369
|
+
}
|
|
1370
|
+
case "json": {
|
|
1371
|
+
if (parts.values === null || parts.values.length !== length) {
|
|
1372
|
+
throw lengthError(meta, "values", length, parts.values === null ? -1 : parts.values.length);
|
|
1373
|
+
}
|
|
1374
|
+
return new JsonColumnImpl(meta, length, parts.values, validity, nullCount);
|
|
1375
|
+
}
|
|
1376
|
+
default: {
|
|
1377
|
+
const name = dtype;
|
|
1378
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function checkNumeric(parts, ctor) {
|
|
1383
|
+
const { meta, length } = parts;
|
|
1384
|
+
if (!(parts.data instanceof ctor)) {
|
|
1385
|
+
throw slotError(meta, `${meta.dtype} data must be a ${ctor.name}`);
|
|
1386
|
+
}
|
|
1387
|
+
const expected = length * meta.components;
|
|
1388
|
+
if (parts.data.length !== expected) {
|
|
1389
|
+
throw lengthError(meta, "data", expected, parts.data.length);
|
|
1390
|
+
}
|
|
1391
|
+
return parts.data;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* The raw storage of a column, sharing every buffer by reference (the inverse of createColumn): used
|
|
1395
|
+
* to re-wrap a column under new metadata and by the remap helpers.
|
|
1396
|
+
* @param column - the column to unwrap
|
|
1397
|
+
* @returns its storage slots
|
|
1398
|
+
*/
|
|
1399
|
+
export function partsOf(column) {
|
|
1400
|
+
const base = {
|
|
1401
|
+
meta: column.meta,
|
|
1402
|
+
length: column.length,
|
|
1403
|
+
data: null,
|
|
1404
|
+
validity: column.validity,
|
|
1405
|
+
nullCount: column.nullCount,
|
|
1406
|
+
dictionary: null,
|
|
1407
|
+
offsets: null,
|
|
1408
|
+
utf8: null,
|
|
1409
|
+
strings: null,
|
|
1410
|
+
child: null,
|
|
1411
|
+
values: null,
|
|
1412
|
+
};
|
|
1413
|
+
const { dtype } = column;
|
|
1414
|
+
switch (dtype) {
|
|
1415
|
+
case "f32":
|
|
1416
|
+
case "f64":
|
|
1417
|
+
case "i32":
|
|
1418
|
+
case "u32":
|
|
1419
|
+
case "u8":
|
|
1420
|
+
case "bool":
|
|
1421
|
+
base.data = column.data;
|
|
1422
|
+
return base;
|
|
1423
|
+
case "dict":
|
|
1424
|
+
base.data = column.codes;
|
|
1425
|
+
base.dictionary = column.dictionary;
|
|
1426
|
+
return base;
|
|
1427
|
+
case "string": {
|
|
1428
|
+
if (column instanceof StringColumnImpl) {
|
|
1429
|
+
base.strings = column.decodedCache();
|
|
1430
|
+
if (column.hasStore()) {
|
|
1431
|
+
base.offsets = column.offsets;
|
|
1432
|
+
base.utf8 = column.utf8;
|
|
1433
|
+
}
|
|
1434
|
+
return base;
|
|
1435
|
+
}
|
|
1436
|
+
base.offsets = column.offsets;
|
|
1437
|
+
base.utf8 = column.utf8;
|
|
1438
|
+
return base;
|
|
1439
|
+
}
|
|
1440
|
+
case "list":
|
|
1441
|
+
base.offsets = column.offsets;
|
|
1442
|
+
base.child = column.child;
|
|
1443
|
+
return base;
|
|
1444
|
+
case "json":
|
|
1445
|
+
base.values = column.values;
|
|
1446
|
+
return base;
|
|
1447
|
+
default: {
|
|
1448
|
+
const name = dtype;
|
|
1449
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* The distinct backing buffers of a column's materialised typed storage (data or codes, validity,
|
|
1455
|
+
* a materialised Utf8 store, a list's offsets and child), for the owner count of design section
|
|
1456
|
+
* 9.1: a table claims them when it attaches the column, so a buffer viewed from two tables (a
|
|
1457
|
+
* zero-copy slice, a same-realm wire receiver) is recognised as shared. A string store that has not
|
|
1458
|
+
* been materialised is not encoded by asking.
|
|
1459
|
+
* @param column - the column
|
|
1460
|
+
* @returns the buffers, each once
|
|
1461
|
+
*/
|
|
1462
|
+
export function columnBuffers(column) {
|
|
1463
|
+
const out = new Set();
|
|
1464
|
+
const add = (view) => {
|
|
1465
|
+
if (view !== null) {
|
|
1466
|
+
out.add(view.buffer);
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
add(column.validity);
|
|
1470
|
+
switch (column.dtype) {
|
|
1471
|
+
case "f32":
|
|
1472
|
+
case "f64":
|
|
1473
|
+
case "i32":
|
|
1474
|
+
case "u32":
|
|
1475
|
+
case "u8":
|
|
1476
|
+
case "bool":
|
|
1477
|
+
add(column.data);
|
|
1478
|
+
break;
|
|
1479
|
+
case "dict":
|
|
1480
|
+
add(column.codes);
|
|
1481
|
+
break;
|
|
1482
|
+
case "string":
|
|
1483
|
+
if (!(column instanceof StringColumnImpl) || column.hasStore()) {
|
|
1484
|
+
add(column.offsets);
|
|
1485
|
+
add(column.utf8);
|
|
1486
|
+
}
|
|
1487
|
+
break;
|
|
1488
|
+
case "list":
|
|
1489
|
+
add(column.offsets);
|
|
1490
|
+
for (const buffer of columnBuffers(column.child)) {
|
|
1491
|
+
out.add(buffer);
|
|
1492
|
+
}
|
|
1493
|
+
break;
|
|
1494
|
+
case "json":
|
|
1495
|
+
break;
|
|
1496
|
+
default: {
|
|
1497
|
+
const unknown = column;
|
|
1498
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${unknown.dtype}`, {});
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return [...out];
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* The same storage under different metadata (a rename, a move to another table, a declaration
|
|
1505
|
+
* patch); the buffers are shared, the Column object is new. The dtype, components and list child
|
|
1506
|
+
* shape must be unchanged (E_COLUMN_TYPE otherwise); the fill is kept because the unset rows
|
|
1507
|
+
* physically hold it; a nullable -> non-nullable change requires no unset rows.
|
|
1508
|
+
* @param column - the column to re-wrap
|
|
1509
|
+
* @param meta - the new metadata
|
|
1510
|
+
* @returns a new column sharing the storage
|
|
1511
|
+
*/
|
|
1512
|
+
export function rewrapColumn(column, meta) {
|
|
1513
|
+
const old = column.meta;
|
|
1514
|
+
if (meta.dtype !== old.dtype ||
|
|
1515
|
+
meta.components !== old.components ||
|
|
1516
|
+
meta.itemDtype !== old.itemDtype ||
|
|
1517
|
+
meta.itemComponents !== old.itemComponents) {
|
|
1518
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `column "${old.name}" is ${old.dtype} x${old.components}; a patch cannot change its dtype`, {
|
|
1519
|
+
column: old.name,
|
|
1520
|
+
field: "dtype",
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
if (meta.fill !== old.fill) {
|
|
1524
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `the fill of existing column "${old.name}" cannot change`, {
|
|
1525
|
+
column: old.name,
|
|
1526
|
+
field: "fill",
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
const parts = partsOf(column);
|
|
1530
|
+
if (!meta.nullable && column.validity !== null) {
|
|
1531
|
+
if (column.nullCount > 0) {
|
|
1532
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `column "${old.name}" has ${column.nullCount} unset rows and cannot become non-nullable`, {
|
|
1533
|
+
column: old.name,
|
|
1534
|
+
field: "nullable",
|
|
1535
|
+
nullCount: column.nullCount,
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
parts.validity = null;
|
|
1539
|
+
}
|
|
1540
|
+
parts.meta = meta;
|
|
1541
|
+
return createColumn(parts);
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Release the cached f32 copy a `gpuView()` of an f64 column keeps (design section 7.2:
|
|
1545
|
+
* `dropCaches()` releases every cached gpuView copy). A no-op for every other dtype.
|
|
1546
|
+
* @param column - the column
|
|
1547
|
+
*/
|
|
1548
|
+
export function dropGpuViewCache(column) {
|
|
1549
|
+
f32Cache.delete(column);
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* The array a GPU binds for a column (design section 10.4): its own data for u32 / i32 / f32, the
|
|
1553
|
+
* padded view for u8, the packed words for bool, the codes for dict, and a cached f32 copy for f64
|
|
1554
|
+
* (dropped by markDirty() and by GraphSnapshot.dropCaches()).
|
|
1555
|
+
* @param column - the column
|
|
1556
|
+
* @returns the bindable array; E_GPU_INELIGIBLE for string / list / json
|
|
1557
|
+
*/
|
|
1558
|
+
export function gpuViewOf(column) {
|
|
1559
|
+
const { dtype } = column;
|
|
1560
|
+
switch (dtype) {
|
|
1561
|
+
case "f32":
|
|
1562
|
+
case "i32":
|
|
1563
|
+
case "u32":
|
|
1564
|
+
case "bool":
|
|
1565
|
+
return column.data;
|
|
1566
|
+
case "u8":
|
|
1567
|
+
return column.paddedU32View();
|
|
1568
|
+
case "dict":
|
|
1569
|
+
return column.codes;
|
|
1570
|
+
case "f64": {
|
|
1571
|
+
let cached = f32Cache.get(column);
|
|
1572
|
+
if (cached === undefined) {
|
|
1573
|
+
cached = new Float32Array(column.data);
|
|
1574
|
+
f32Cache.set(column, cached);
|
|
1575
|
+
}
|
|
1576
|
+
return cached;
|
|
1577
|
+
}
|
|
1578
|
+
case "string":
|
|
1579
|
+
case "list":
|
|
1580
|
+
case "json":
|
|
1581
|
+
throw new GraphFormatError("E_GPU_INELIGIBLE", `a ${dtype} column cannot be bound by the GPU`, {
|
|
1582
|
+
column: column.meta.name,
|
|
1583
|
+
dtype,
|
|
1584
|
+
});
|
|
1585
|
+
default: {
|
|
1586
|
+
const name = dtype;
|
|
1587
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
// ============================================================ construction from declarations and values
|
|
1592
|
+
/**
|
|
1593
|
+
* The physical dict code stored in rows that hold the fill: the code of the fill string, interned
|
|
1594
|
+
* on demand when those rows are SET (a non-nullable column), or 0 when the fill is not a member and
|
|
1595
|
+
* the rows are unset (their code is never read).
|
|
1596
|
+
* @param dictionary - the dictionary
|
|
1597
|
+
* @param fill - the column's fill
|
|
1598
|
+
* @param set - whether the rows holding the fill are set
|
|
1599
|
+
* @returns the code
|
|
1600
|
+
*/
|
|
1601
|
+
function fillCode(dictionary, fill, set) {
|
|
1602
|
+
const text = typeof fill === "string" ? fill : "";
|
|
1603
|
+
if (set) {
|
|
1604
|
+
// a SET row holding the fill (a non-nullable column) needs a code that names a member: the
|
|
1605
|
+
// nominal "" is interned on demand rather than borrowing code 0, which may belong to another
|
|
1606
|
+
// value or to nothing
|
|
1607
|
+
return dictionary.intern(text);
|
|
1608
|
+
}
|
|
1609
|
+
// an unset row's code is never read; 0 when the fill is not a member
|
|
1610
|
+
const code = dictionary.codeOf(text);
|
|
1611
|
+
return code === INVALID_INDEX ? 0 : code;
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* The initial dictionary of a dict column: the declared options in order, then the fill when it is
|
|
1615
|
+
* a string not among them (so the fill code names a member).
|
|
1616
|
+
* @param meta - the column metadata
|
|
1617
|
+
* @returns the seeded dictionary builder
|
|
1618
|
+
*/
|
|
1619
|
+
export function seedDictionary(meta) {
|
|
1620
|
+
const dictionary = new DictionaryBuilder(meta.options === null ? undefined : meta.options);
|
|
1621
|
+
if (typeof meta.fill === "string" && meta.fill !== "") {
|
|
1622
|
+
dictionary.intern(meta.fill);
|
|
1623
|
+
}
|
|
1624
|
+
return dictionary;
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* A MutableColumnParts record with every storage slot null, for producers that fill one slot.
|
|
1628
|
+
* @param meta - the column metadata
|
|
1629
|
+
* @param length - the row count
|
|
1630
|
+
* @param validity - the validity bitmap, or null
|
|
1631
|
+
* @returns the parts
|
|
1632
|
+
*/
|
|
1633
|
+
export function emptyParts(meta, length, validity) {
|
|
1634
|
+
return {
|
|
1635
|
+
meta,
|
|
1636
|
+
length,
|
|
1637
|
+
data: null,
|
|
1638
|
+
validity,
|
|
1639
|
+
nullCount: validity === null ? 0 : length,
|
|
1640
|
+
dictionary: null,
|
|
1641
|
+
offsets: null,
|
|
1642
|
+
utf8: null,
|
|
1643
|
+
strings: null,
|
|
1644
|
+
child: null,
|
|
1645
|
+
values: null,
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
function childMeta(meta) {
|
|
1649
|
+
if (meta.itemDtype === null || meta.itemComponents === null) {
|
|
1650
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `column "${meta.name}" is not a list`, {
|
|
1651
|
+
column: meta.name,
|
|
1652
|
+
field: "itemDtype",
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
return resolveColumnMeta(`${meta.name}.item`, meta.domain, {
|
|
1656
|
+
dtype: meta.itemDtype,
|
|
1657
|
+
components: meta.itemComponents,
|
|
1658
|
+
nullable: false,
|
|
1659
|
+
refersTo: meta.refersTo ?? undefined,
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* A column of `length` rows with no value set: every row unset (when nullable) and the data holding
|
|
1664
|
+
* the fill (design section 5.3). This is what a declared column looks like before any value is
|
|
1665
|
+
* written, and what remap uses for rows no source row maps to.
|
|
1666
|
+
* @param domain - the table the column belongs to
|
|
1667
|
+
* @param length - the number of rows
|
|
1668
|
+
* @param decl - the declaration; dtype required
|
|
1669
|
+
* @returns the empty column
|
|
1670
|
+
*/
|
|
1671
|
+
export function createEmptyColumn(domain, length, decl) {
|
|
1672
|
+
const name = decl.name ?? "";
|
|
1673
|
+
const meta = resolveColumnMeta(name, domain, decl);
|
|
1674
|
+
return emptyColumnOf(meta, length);
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* An empty column from resolved metadata; see createEmptyColumn.
|
|
1678
|
+
* @param meta - the resolved metadata
|
|
1679
|
+
* @param length - the number of rows
|
|
1680
|
+
* @returns the empty column
|
|
1681
|
+
*/
|
|
1682
|
+
function emptyColumnOf(meta, length) {
|
|
1683
|
+
const validity = meta.nullable ? new Uint32Array(bitmapWordCount(length)) : null;
|
|
1684
|
+
const parts = emptyParts(meta, length, validity);
|
|
1685
|
+
const { dtype, components, fill } = meta;
|
|
1686
|
+
switch (dtype) {
|
|
1687
|
+
case "f32":
|
|
1688
|
+
parts.data = new Float32Array(length * components).fill(fill);
|
|
1689
|
+
break;
|
|
1690
|
+
case "f64":
|
|
1691
|
+
parts.data = new Float64Array(length * components).fill(fill);
|
|
1692
|
+
break;
|
|
1693
|
+
case "i32":
|
|
1694
|
+
parts.data = new Int32Array(length * components).fill(fill);
|
|
1695
|
+
break;
|
|
1696
|
+
case "u32":
|
|
1697
|
+
parts.data = new Uint32Array(length * components).fill(fill);
|
|
1698
|
+
break;
|
|
1699
|
+
case "u8":
|
|
1700
|
+
parts.data = allocU8(length * components).fill(fill);
|
|
1701
|
+
break;
|
|
1702
|
+
case "bool":
|
|
1703
|
+
parts.data = makeBitmap(length, fill === true);
|
|
1704
|
+
break;
|
|
1705
|
+
case "dict": {
|
|
1706
|
+
const dictionary = seedDictionary(meta);
|
|
1707
|
+
parts.data = new Uint32Array(length).fill(fillCode(dictionary, fill, validity === null && length > 0));
|
|
1708
|
+
parts.dictionary = dictionary.values;
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
case "string": {
|
|
1712
|
+
const fillText = typeof fill === "string" ? fill : "";
|
|
1713
|
+
parts.strings = new Array(length).fill(fillText);
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
case "list":
|
|
1717
|
+
parts.offsets = new Uint32Array(length + 1);
|
|
1718
|
+
parts.child = emptyColumnOf(childMeta(meta), 0);
|
|
1719
|
+
break;
|
|
1720
|
+
case "json":
|
|
1721
|
+
parts.values = new Array(length).fill(undefined);
|
|
1722
|
+
break;
|
|
1723
|
+
default: {
|
|
1724
|
+
const dtypeName = dtype;
|
|
1725
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${dtypeName}`, { dtype: dtypeName });
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return createColumn(parts);
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Whether a JS array entry is an unset cell: undefined for every dtype, null too except for json,
|
|
1732
|
+
* where null is a value (design section 12.1).
|
|
1733
|
+
* @param value - the entry
|
|
1734
|
+
* @param dtype - the column dtype
|
|
1735
|
+
* @returns true when unset
|
|
1736
|
+
*/
|
|
1737
|
+
function isUnsetEntry(value, dtype) {
|
|
1738
|
+
return value === undefined || (value === null && dtype !== "json");
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Build the offsets and child of a list column from an array of rows (each an array of items).
|
|
1742
|
+
* @param meta - the list column's metadata
|
|
1743
|
+
* @param rows - one array per row (an empty array for unset rows)
|
|
1744
|
+
* @returns the offsets and the wrapped child
|
|
1745
|
+
*/
|
|
1746
|
+
function listPartsFromValues(meta, rows) {
|
|
1747
|
+
const offsets = new Uint32Array(rows.length + 1);
|
|
1748
|
+
const items = [];
|
|
1749
|
+
for (let row = 0; row < rows.length; row++) {
|
|
1750
|
+
for (const item of rows[row]) {
|
|
1751
|
+
items.push(item);
|
|
1752
|
+
}
|
|
1753
|
+
offsets[row + 1] = items.length;
|
|
1754
|
+
}
|
|
1755
|
+
const child = columnOfValues(childMeta(meta), items);
|
|
1756
|
+
return { offsets, child };
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* A column from a JS array of values under resolved metadata (one entry per row; undefined, and null
|
|
1760
|
+
* except for json, is an unset row). Values are coerced per design section 5.1 (a boolean into a
|
|
1761
|
+
* number column becomes 1 / 0, a number or boolean into a string column its canonical text) and
|
|
1762
|
+
* rejected with E_COLUMN_TYPE otherwise.
|
|
1763
|
+
* @param meta - the resolved metadata
|
|
1764
|
+
* @param values - the entries, `length` of them
|
|
1765
|
+
* @returns the column
|
|
1766
|
+
*/
|
|
1767
|
+
export function columnOfValues(meta, values) {
|
|
1768
|
+
const { length } = values;
|
|
1769
|
+
const { dtype, components, fill, name } = meta;
|
|
1770
|
+
let validity = null;
|
|
1771
|
+
let nullCount = 0;
|
|
1772
|
+
for (let row = 0; row < length; row++) {
|
|
1773
|
+
if (isUnsetEntry(values[row], dtype)) {
|
|
1774
|
+
if (!meta.nullable) {
|
|
1775
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `row ${row} of non-nullable column "${name}" is unset`, {
|
|
1776
|
+
column: name,
|
|
1777
|
+
row,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
validity ?? (validity = makeBitmap(length, true));
|
|
1781
|
+
bitmapClear(validity, row);
|
|
1782
|
+
nullCount++;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
const parts = emptyParts(meta, length, validity);
|
|
1786
|
+
parts.nullCount = nullCount;
|
|
1787
|
+
const isSet = (row) => validity === null || bitmapGet(validity, row);
|
|
1788
|
+
switch (dtype) {
|
|
1789
|
+
case "f32":
|
|
1790
|
+
case "f64":
|
|
1791
|
+
case "i32":
|
|
1792
|
+
case "u32":
|
|
1793
|
+
case "u8": {
|
|
1794
|
+
const data = allocNumeric(dtype, length * components);
|
|
1795
|
+
if (fill !== 0) {
|
|
1796
|
+
data.fill(fill);
|
|
1797
|
+
}
|
|
1798
|
+
for (let row = 0; row < length; row++) {
|
|
1799
|
+
if (!isSet(row)) {
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
const raw = values[row];
|
|
1803
|
+
const value = typeof raw === "boolean" ? Number(raw) : raw;
|
|
1804
|
+
writeNumeric(dtype, data, row * components, components, value, name, row);
|
|
1805
|
+
}
|
|
1806
|
+
parts.data = data;
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
case "bool": {
|
|
1810
|
+
const data = makeBitmap(length, fill === true);
|
|
1811
|
+
for (let row = 0; row < length; row++) {
|
|
1812
|
+
if (!isSet(row)) {
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
const value = coerceValue(values[row], "bool");
|
|
1816
|
+
if (value === true) {
|
|
1817
|
+
bitmapSet(data, row);
|
|
1818
|
+
}
|
|
1819
|
+
else {
|
|
1820
|
+
bitmapClear(data, row);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
parts.data = data;
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
case "dict": {
|
|
1827
|
+
const dictionary = seedDictionary(meta);
|
|
1828
|
+
const codes = new Uint32Array(length);
|
|
1829
|
+
if (validity !== null) {
|
|
1830
|
+
codes.fill(fillCode(dictionary, fill, false));
|
|
1831
|
+
}
|
|
1832
|
+
for (let row = 0; row < length; row++) {
|
|
1833
|
+
if (!isSet(row)) {
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
const value = coerceValue(values[row], "string");
|
|
1837
|
+
if (typeof value !== "string") {
|
|
1838
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `row ${row} of dict column "${name}" is not a string`, {
|
|
1839
|
+
column: name,
|
|
1840
|
+
row,
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
codes[row] = dictionary.intern(value);
|
|
1844
|
+
}
|
|
1845
|
+
parts.data = codes;
|
|
1846
|
+
parts.dictionary = dictionary.values;
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
case "string": {
|
|
1850
|
+
const fillText = typeof fill === "string" ? fill : "";
|
|
1851
|
+
const strings = new Array(length);
|
|
1852
|
+
for (let row = 0; row < length; row++) {
|
|
1853
|
+
if (!isSet(row)) {
|
|
1854
|
+
strings[row] = fillText;
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
const value = coerceValue(values[row], "string");
|
|
1858
|
+
if (typeof value !== "string") {
|
|
1859
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `row ${row} of string column "${name}" is not a string`, { column: name, row });
|
|
1860
|
+
}
|
|
1861
|
+
assertWellFormedString(value, { column: name, row });
|
|
1862
|
+
strings[row] = value;
|
|
1863
|
+
}
|
|
1864
|
+
parts.strings = strings;
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
case "list": {
|
|
1868
|
+
const rows = new Array(length);
|
|
1869
|
+
for (let row = 0; row < length; row++) {
|
|
1870
|
+
if (!isSet(row)) {
|
|
1871
|
+
rows[row] = [];
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
const value = values[row];
|
|
1875
|
+
if (!Array.isArray(value)) {
|
|
1876
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `row ${row} of list column "${name}" is not an array`, {
|
|
1877
|
+
column: name,
|
|
1878
|
+
row,
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
rows[row] = value;
|
|
1882
|
+
}
|
|
1883
|
+
const listParts = listPartsFromValues(meta, rows);
|
|
1884
|
+
parts.offsets = listParts.offsets;
|
|
1885
|
+
parts.child = listParts.child;
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
case "json": {
|
|
1889
|
+
const out = new Array(length);
|
|
1890
|
+
for (let row = 0; row < length; row++) {
|
|
1891
|
+
if (!isSet(row)) {
|
|
1892
|
+
out[row] = undefined;
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
assertJsonValue(values[row], `row ${row}`);
|
|
1896
|
+
out[row] = values[row];
|
|
1897
|
+
}
|
|
1898
|
+
parts.values = out;
|
|
1899
|
+
break;
|
|
1900
|
+
}
|
|
1901
|
+
default: {
|
|
1902
|
+
const dtypeName = dtype;
|
|
1903
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${dtypeName}`, { dtype: dtypeName });
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return createColumn(parts);
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Allocate a numeric buffer of a dtype (a padded store for u8).
|
|
1910
|
+
* @param dtype - the numeric dtype
|
|
1911
|
+
* @param length - the element count
|
|
1912
|
+
* @returns the zeroed array
|
|
1913
|
+
*/
|
|
1914
|
+
export function allocNumeric(dtype, length) {
|
|
1915
|
+
switch (dtype) {
|
|
1916
|
+
case "f32":
|
|
1917
|
+
return new Float32Array(length);
|
|
1918
|
+
case "f64":
|
|
1919
|
+
return new Float64Array(length);
|
|
1920
|
+
case "i32":
|
|
1921
|
+
return new Int32Array(length);
|
|
1922
|
+
case "u32":
|
|
1923
|
+
return new Uint32Array(length);
|
|
1924
|
+
case "u8":
|
|
1925
|
+
return allocU8(length);
|
|
1926
|
+
default: {
|
|
1927
|
+
const name = dtype;
|
|
1928
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Infer the declaration of a column from a JS array of values when the caller gave no dtype: the
|
|
1934
|
+
* widening rules of design section 5.1 over the entries (json when every entry is unset), and for a
|
|
1935
|
+
* list the same rules over the items.
|
|
1936
|
+
* @param values - the entries
|
|
1937
|
+
* @returns the dtype and, for a list, the item dtype
|
|
1938
|
+
*/
|
|
1939
|
+
function inferDeclFromValues(values) {
|
|
1940
|
+
let sawArray = false;
|
|
1941
|
+
let sawOther = false;
|
|
1942
|
+
for (const value of values) {
|
|
1943
|
+
if (value === undefined || value === null) {
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
if (Array.isArray(value)) {
|
|
1947
|
+
sawArray = true;
|
|
1948
|
+
}
|
|
1949
|
+
else {
|
|
1950
|
+
sawOther = true;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
if (sawArray && !sawOther) {
|
|
1954
|
+
const items = [];
|
|
1955
|
+
for (const value of values) {
|
|
1956
|
+
if (Array.isArray(value)) {
|
|
1957
|
+
for (const item of value) {
|
|
1958
|
+
items.push(item);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return { dtype: "list", itemDtype: inferValuesDtype(items) ?? "json" };
|
|
1963
|
+
}
|
|
1964
|
+
return { dtype: inferValuesDtype(values) ?? "json", itemDtype: undefined };
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* A column from a JS array of values with a declaration patch (the set() path for string / list /
|
|
1968
|
+
* json and for inferred dtypes). The dtype is inferred when the patch has none; nullable defaults to
|
|
1969
|
+
* true.
|
|
1970
|
+
* @param domain - the table the column belongs to
|
|
1971
|
+
* @param length - the table's row count; E_COLUMN_LENGTH when values.length differs
|
|
1972
|
+
* @param name - the column name
|
|
1973
|
+
* @param values - the entries, one per row
|
|
1974
|
+
* @param decl - the declaration patch
|
|
1975
|
+
* @returns the column
|
|
1976
|
+
*/
|
|
1977
|
+
export function columnFromValues(domain, length, name, values, decl) {
|
|
1978
|
+
if (values.length !== length) {
|
|
1979
|
+
throw new GraphFormatError("E_COLUMN_LENGTH", `column "${name}": ${values.length} values for ${length} rows`, {
|
|
1980
|
+
column: name,
|
|
1981
|
+
expected: length,
|
|
1982
|
+
found: values.length,
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
let patch = { nullable: true, ...decl };
|
|
1986
|
+
if (patch.dtype === undefined) {
|
|
1987
|
+
const inferred = inferDeclFromValues(values);
|
|
1988
|
+
patch = { ...patch, dtype: inferred.dtype };
|
|
1989
|
+
if (inferred.dtype === "list" && patch.itemDtype === undefined) {
|
|
1990
|
+
patch = { ...patch, itemDtype: inferred.itemDtype };
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
const meta = resolveColumnMeta(name, domain, patch);
|
|
1994
|
+
return columnOfValues(meta, values);
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* The dtype a typed array class implies when the caller gave none.
|
|
1998
|
+
* @param data - the array
|
|
1999
|
+
* @returns f32 / f64 / i32 / u32 / u8
|
|
2000
|
+
*/
|
|
2001
|
+
export function dtypeOfArray(data) {
|
|
2002
|
+
if (data instanceof Float32Array) {
|
|
2003
|
+
return "f32";
|
|
2004
|
+
}
|
|
2005
|
+
if (data instanceof Float64Array) {
|
|
2006
|
+
return "f64";
|
|
2007
|
+
}
|
|
2008
|
+
if (data instanceof Int32Array) {
|
|
2009
|
+
return "i32";
|
|
2010
|
+
}
|
|
2011
|
+
if (data instanceof Uint32Array) {
|
|
2012
|
+
return "u32";
|
|
2013
|
+
}
|
|
2014
|
+
return "u8";
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* A column adopting a typed array by reference (design section 5.7): the dtype comes from the array
|
|
2018
|
+
* class unless the patch names one (bool and dict over a Uint32Array); the length must be rowCount *
|
|
2019
|
+
* components (bool: ceil(rowCount / 32)) or E_COLUMN_LENGTH; a u8 array from which no padded u32 view
|
|
2020
|
+
* is constructible is copied, or refused with E_COLUMN_ALIGNMENT under adopt "strict". nullable
|
|
2021
|
+
* defaults to false (every row is set).
|
|
2022
|
+
* @param domain - the table the column belongs to
|
|
2023
|
+
* @param length - the table's row count
|
|
2024
|
+
* @param name - the column name
|
|
2025
|
+
* @param data - the typed array
|
|
2026
|
+
* @param decl - the declaration patch
|
|
2027
|
+
* @param adopt - "copy" (default) or "strict"
|
|
2028
|
+
* @returns the column (`column.data !== data` only when a u8 array was copied)
|
|
2029
|
+
*/
|
|
2030
|
+
export function columnFromTypedArray(domain, length, name, data, decl, adopt = "copy") {
|
|
2031
|
+
const arrayDtype = dtypeOfArray(data);
|
|
2032
|
+
const dtype = decl.dtype ?? arrayDtype;
|
|
2033
|
+
const compatible = dtype === arrayDtype || (arrayDtype === "u32" && (dtype === "bool" || dtype === "dict"));
|
|
2034
|
+
if (!compatible) {
|
|
2035
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": a ${arrayDtype} array cannot back a ${dtype} column`, {
|
|
2036
|
+
column: name,
|
|
2037
|
+
field: "dtype",
|
|
2038
|
+
found: arrayDtype,
|
|
2039
|
+
expected: dtype,
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
const meta = resolveColumnMeta(name, domain, { nullable: false, ...decl, dtype });
|
|
2043
|
+
const parts = emptyParts(meta, length, null);
|
|
2044
|
+
let expected;
|
|
2045
|
+
switch (dtype) {
|
|
2046
|
+
case "bool":
|
|
2047
|
+
expected = bitmapWordCount(length);
|
|
2048
|
+
break;
|
|
2049
|
+
case "dict":
|
|
2050
|
+
expected = length;
|
|
2051
|
+
break;
|
|
2052
|
+
case "f32":
|
|
2053
|
+
case "f64":
|
|
2054
|
+
case "i32":
|
|
2055
|
+
case "u32":
|
|
2056
|
+
case "u8":
|
|
2057
|
+
expected = length * meta.components;
|
|
2058
|
+
break;
|
|
2059
|
+
default: {
|
|
2060
|
+
const dtypeName = dtype;
|
|
2061
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": a ${dtypeName} column cannot adopt a typed array`, {
|
|
2062
|
+
column: name,
|
|
2063
|
+
field: "dtype",
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (data.length !== expected) {
|
|
2068
|
+
throw new GraphFormatError("E_COLUMN_LENGTH", `column "${name}": array length ${data.length}, expected ${expected}`, {
|
|
2069
|
+
column: name,
|
|
2070
|
+
expected,
|
|
2071
|
+
found: data.length,
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
if (!isOverPlainBuffer(data)) {
|
|
2075
|
+
// decision D-SAB: no SharedArrayBuffer in v1; a resizable buffer's view can change length (I17)
|
|
2076
|
+
const reason = data.buffer instanceof ArrayBuffer ? "resizable ArrayBuffer" : "SharedArrayBuffer";
|
|
2077
|
+
throw new GraphFormatError("E_UNSUPPORTED", `column "${name}": a view over a ${reason} cannot be adopted`, {
|
|
2078
|
+
column: name,
|
|
2079
|
+
reason,
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
let stored = data;
|
|
2083
|
+
if (data instanceof Uint8Array && !canViewAsPaddedU32(data)) {
|
|
2084
|
+
if (adopt === "strict") {
|
|
2085
|
+
throw new GraphFormatError("E_COLUMN_ALIGNMENT", `column "${name}": no zero-copy padded u32 view over the u8 array`, {
|
|
2086
|
+
column: name,
|
|
2087
|
+
byteOffset: data.byteOffset,
|
|
2088
|
+
byteLength: data.byteLength,
|
|
2089
|
+
bufferByteLength: data.buffer.byteLength,
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
const copy = allocU8(data.length);
|
|
2093
|
+
copy.set(data);
|
|
2094
|
+
stored = copy;
|
|
2095
|
+
}
|
|
2096
|
+
parts.data = stored;
|
|
2097
|
+
if (dtype === "dict") {
|
|
2098
|
+
const dictionary = seedDictionary(meta);
|
|
2099
|
+
for (let row = 0; row < length; row++) {
|
|
2100
|
+
const code = data[row];
|
|
2101
|
+
if (code >= dictionary.size) {
|
|
2102
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": code ${code} at row ${row} is outside the ${dictionary.size} declared options`, { column: name, row, code });
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
parts.dictionary = dictionary.values;
|
|
2106
|
+
}
|
|
2107
|
+
return createColumn(parts);
|
|
2108
|
+
}
|
|
2109
|
+
//# sourceMappingURL=column.js.map
|