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