@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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -43
  3. package/dist/graph-format.d.ts +1 -0
  4. package/dist/graph-format.d.ts.map +1 -0
  5. package/dist/graph-format.js +15798 -0
  6. package/dist/graph-format.js.map +1 -0
  7. package/dist/src/builder/arena.d.ts +85 -0
  8. package/dist/src/builder/arena.d.ts.map +1 -0
  9. package/dist/src/builder/arena.js +154 -0
  10. package/dist/src/builder/arena.js.map +1 -0
  11. package/dist/src/builder/compact.d.ts +515 -0
  12. package/dist/src/builder/compact.d.ts.map +1 -0
  13. package/dist/src/builder/compact.js +1553 -0
  14. package/dist/src/builder/compact.js.map +1 -0
  15. package/dist/src/builder/counting-sort.d.ts +96 -0
  16. package/dist/src/builder/counting-sort.d.ts.map +1 -0
  17. package/dist/src/builder/counting-sort.js +430 -0
  18. package/dist/src/builder/counting-sort.js.map +1 -0
  19. package/dist/src/builder/freeze.d.ts +72 -0
  20. package/dist/src/builder/freeze.d.ts.map +1 -0
  21. package/dist/src/builder/freeze.js +548 -0
  22. package/dist/src/builder/freeze.js.map +1 -0
  23. package/dist/src/builder/graph-builder.d.ts +602 -0
  24. package/dist/src/builder/graph-builder.d.ts.map +1 -0
  25. package/dist/src/builder/graph-builder.js +1940 -0
  26. package/dist/src/builder/graph-builder.js.map +1 -0
  27. package/dist/src/builder/options.d.ts +16 -0
  28. package/dist/src/builder/options.d.ts.map +1 -0
  29. package/dist/src/builder/options.js +21 -0
  30. package/dist/src/builder/options.js.map +1 -0
  31. package/dist/src/columns/bitmap.d.ts +96 -0
  32. package/dist/src/columns/bitmap.d.ts.map +1 -0
  33. package/dist/src/columns/bitmap.js +184 -0
  34. package/dist/src/columns/bitmap.js.map +1 -0
  35. package/dist/src/columns/column.d.ts +220 -0
  36. package/dist/src/columns/column.d.ts.map +1 -0
  37. package/dist/src/columns/column.js +2109 -0
  38. package/dist/src/columns/column.js.map +1 -0
  39. package/dist/src/columns/dictionary.d.ts +60 -0
  40. package/dist/src/columns/dictionary.d.ts.map +1 -0
  41. package/dist/src/columns/dictionary.js +102 -0
  42. package/dist/src/columns/dictionary.js.map +1 -0
  43. package/dist/src/columns/growable.d.ts +198 -0
  44. package/dist/src/columns/growable.d.ts.map +1 -0
  45. package/dist/src/columns/growable.js +296 -0
  46. package/dist/src/columns/growable.js.map +1 -0
  47. package/dist/src/columns/infer.d.ts +121 -0
  48. package/dist/src/columns/infer.d.ts.map +1 -0
  49. package/dist/src/columns/infer.js +268 -0
  50. package/dist/src/columns/infer.js.map +1 -0
  51. package/dist/src/columns/remap.d.ts +143 -0
  52. package/dist/src/columns/remap.d.ts.map +1 -0
  53. package/dist/src/columns/remap.js +588 -0
  54. package/dist/src/columns/remap.js.map +1 -0
  55. package/dist/src/columns/table.d.ts +184 -0
  56. package/dist/src/columns/table.d.ts.map +1 -0
  57. package/dist/src/columns/table.js +485 -0
  58. package/dist/src/columns/table.js.map +1 -0
  59. package/dist/src/constants.d.ts +34 -0
  60. package/dist/src/constants.d.ts.map +1 -0
  61. package/dist/src/constants.js +79 -0
  62. package/dist/src/constants.js.map +1 -0
  63. package/dist/src/errors.d.ts +76 -0
  64. package/dist/src/errors.d.ts.map +1 -0
  65. package/dist/src/errors.js +32 -0
  66. package/dist/src/errors.js.map +1 -0
  67. package/dist/src/ids/edge-id-index.d.ts +77 -0
  68. package/dist/src/ids/edge-id-index.d.ts.map +1 -0
  69. package/dist/src/ids/edge-id-index.js +155 -0
  70. package/dist/src/ids/edge-id-index.js.map +1 -0
  71. package/dist/src/ids/node-id-map.d.ts +280 -0
  72. package/dist/src/ids/node-id-map.d.ts.map +1 -0
  73. package/dist/src/ids/node-id-map.js +1139 -0
  74. package/dist/src/ids/node-id-map.js.map +1 -0
  75. package/dist/src/ids/string-store.d.ts +219 -0
  76. package/dist/src/ids/string-store.d.ts.map +1 -0
  77. package/dist/src/ids/string-store.js +526 -0
  78. package/dist/src/ids/string-store.js.map +1 -0
  79. package/dist/src/index.d.ts +30 -0
  80. package/dist/src/index.d.ts.map +1 -0
  81. package/dist/src/index.js +29 -0
  82. package/dist/src/index.js.map +1 -0
  83. package/dist/src/populate/from-csr.d.ts +61 -0
  84. package/dist/src/populate/from-csr.d.ts.map +1 -0
  85. package/dist/src/populate/from-csr.js +706 -0
  86. package/dist/src/populate/from-csr.js.map +1 -0
  87. package/dist/src/populate/from-edge-arrays.d.ts +51 -0
  88. package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
  89. package/dist/src/populate/from-edge-arrays.js +217 -0
  90. package/dist/src/populate/from-edge-arrays.js.map +1 -0
  91. package/dist/src/populate/from-records.d.ts +51 -0
  92. package/dist/src/populate/from-records.d.ts.map +1 -0
  93. package/dist/src/populate/from-records.js +447 -0
  94. package/dist/src/populate/from-records.js.map +1 -0
  95. package/dist/src/snapshot/derived.d.ts +211 -0
  96. package/dist/src/snapshot/derived.d.ts.map +1 -0
  97. package/dist/src/snapshot/derived.js +1254 -0
  98. package/dist/src/snapshot/derived.js.map +1 -0
  99. package/dist/src/snapshot/graph-meta.d.ts +23 -0
  100. package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
  101. package/dist/src/snapshot/graph-meta.js +159 -0
  102. package/dist/src/snapshot/graph-meta.js.map +1 -0
  103. package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
  104. package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
  105. package/dist/src/snapshot/graph-snapshot.js +1247 -0
  106. package/dist/src/snapshot/graph-snapshot.js.map +1 -0
  107. package/dist/src/snapshot/hash.d.ts +90 -0
  108. package/dist/src/snapshot/hash.d.ts.map +1 -0
  109. package/dist/src/snapshot/hash.js +228 -0
  110. package/dist/src/snapshot/hash.js.map +1 -0
  111. package/dist/src/snapshot/queries.d.ts +74 -0
  112. package/dist/src/snapshot/queries.d.ts.map +1 -0
  113. package/dist/src/snapshot/queries.js +131 -0
  114. package/dist/src/snapshot/queries.js.map +1 -0
  115. package/dist/src/snapshot/validate.d.ts +192 -0
  116. package/dist/src/snapshot/validate.d.ts.map +1 -0
  117. package/dist/src/snapshot/validate.js +990 -0
  118. package/dist/src/snapshot/validate.js.map +1 -0
  119. package/dist/src/snapshot/views.d.ts +274 -0
  120. package/dist/src/snapshot/views.d.ts.map +1 -0
  121. package/dist/src/snapshot/views.js +856 -0
  122. package/dist/src/snapshot/views.js.map +1 -0
  123. package/dist/src/types/builder.d.ts +456 -0
  124. package/dist/src/types/builder.d.ts.map +1 -0
  125. package/dist/src/types/builder.js +11 -0
  126. package/dist/src/types/builder.js.map +1 -0
  127. package/dist/src/types/columns.d.ts +615 -0
  128. package/dist/src/types/columns.d.ts.map +1 -0
  129. package/dist/src/types/columns.js +14 -0
  130. package/dist/src/types/columns.js.map +1 -0
  131. package/dist/src/types/index.d.ts +14 -0
  132. package/dist/src/types/index.d.ts.map +1 -0
  133. package/dist/src/types/index.js +11 -0
  134. package/dist/src/types/index.js.map +1 -0
  135. package/dist/src/types/internal.d.ts +172 -0
  136. package/dist/src/types/internal.d.ts.map +1 -0
  137. package/dist/src/types/internal.js +13 -0
  138. package/dist/src/types/internal.js.map +1 -0
  139. package/dist/src/types/snapshot.d.ts +801 -0
  140. package/dist/src/types/snapshot.d.ts.map +1 -0
  141. package/dist/src/types/snapshot.js +14 -0
  142. package/dist/src/types/snapshot.js.map +1 -0
  143. package/dist/src/types/wire.d.ts +196 -0
  144. package/dist/src/types/wire.d.ts.map +1 -0
  145. package/dist/src/types/wire.js +8 -0
  146. package/dist/src/types/wire.js.map +1 -0
  147. package/dist/src/util/mask.d.ts +55 -0
  148. package/dist/src/util/mask.d.ts.map +1 -0
  149. package/dist/src/util/mask.js +71 -0
  150. package/dist/src/util/mask.js.map +1 -0
  151. package/dist/src/util/options.d.ts +22 -0
  152. package/dist/src/util/options.d.ts.map +1 -0
  153. package/dist/src/util/options.js +49 -0
  154. package/dist/src/util/options.js.map +1 -0
  155. package/dist/src/util/shared-buffers.d.ts +42 -0
  156. package/dist/src/util/shared-buffers.d.ts.map +1 -0
  157. package/dist/src/util/shared-buffers.js +52 -0
  158. package/dist/src/util/shared-buffers.js.map +1 -0
  159. package/dist/src/util/typed-array.d.ts +205 -0
  160. package/dist/src/util/typed-array.d.ts.map +1 -0
  161. package/dist/src/util/typed-array.js +289 -0
  162. package/dist/src/util/typed-array.js.map +1 -0
  163. package/dist/src/wire/bytes.d.ts +69 -0
  164. package/dist/src/wire/bytes.d.ts.map +1 -0
  165. package/dist/src/wire/bytes.js +361 -0
  166. package/dist/src/wire/bytes.js.map +1 -0
  167. package/dist/src/wire/carried-views.d.ts +111 -0
  168. package/dist/src/wire/carried-views.d.ts.map +1 -0
  169. package/dist/src/wire/carried-views.js +286 -0
  170. package/dist/src/wire/carried-views.js.map +1 -0
  171. package/dist/src/wire/from-wire.d.ts +144 -0
  172. package/dist/src/wire/from-wire.d.ts.map +1 -0
  173. package/dist/src/wire/from-wire.js +1425 -0
  174. package/dist/src/wire/from-wire.js.map +1 -0
  175. package/dist/src/wire/to-wire.d.ts +171 -0
  176. package/dist/src/wire/to-wire.d.ts.map +1 -0
  177. package/dist/src/wire/to-wire.js +652 -0
  178. package/dist/src/wire/to-wire.js.map +1 -0
  179. package/dist/tsconfig.build.tsbuildinfo +1 -0
  180. package/package.json +72 -7
  181. package/src/builder/arena.ts +192 -0
  182. package/src/builder/compact.ts +1811 -0
  183. package/src/builder/counting-sort.ts +506 -0
  184. package/src/builder/freeze.ts +689 -0
  185. package/src/builder/graph-builder.ts +2264 -0
  186. package/src/builder/options.ts +29 -0
  187. package/src/columns/bitmap.ts +196 -0
  188. package/src/columns/column.ts +2574 -0
  189. package/src/columns/dictionary.ts +114 -0
  190. package/src/columns/growable.ts +350 -0
  191. package/src/columns/infer.ts +285 -0
  192. package/src/columns/remap.ts +663 -0
  193. package/src/columns/table.ts +568 -0
  194. package/src/constants.ts +89 -0
  195. package/src/errors.ts +115 -0
  196. package/src/ids/edge-id-index.ts +179 -0
  197. package/src/ids/node-id-map.ts +1340 -0
  198. package/src/ids/string-store.ts +589 -0
  199. package/src/index.ts +139 -0
  200. package/src/lib-resizable-array-buffer.d.ts +34 -0
  201. package/src/populate/from-csr.ts +802 -0
  202. package/src/populate/from-edge-arrays.ts +257 -0
  203. package/src/populate/from-records.ts +536 -0
  204. package/src/snapshot/derived.ts +1486 -0
  205. package/src/snapshot/graph-meta.ts +167 -0
  206. package/src/snapshot/graph-snapshot.ts +1503 -0
  207. package/src/snapshot/hash.ts +241 -0
  208. package/src/snapshot/queries.ts +136 -0
  209. package/src/snapshot/validate.ts +1108 -0
  210. package/src/snapshot/views.ts +943 -0
  211. package/src/types/builder.ts +480 -0
  212. package/src/types/columns.ts +727 -0
  213. package/src/types/index.ts +14 -0
  214. package/src/types/internal.ts +195 -0
  215. package/src/types/snapshot.ts +886 -0
  216. package/src/types/wire.ts +207 -0
  217. package/src/util/mask.ts +89 -0
  218. package/src/util/options.ts +57 -0
  219. package/src/util/shared-buffers.ts +56 -0
  220. package/src/util/typed-array.ts +371 -0
  221. package/src/wire/bytes.ts +440 -0
  222. package/src/wire/carried-views.ts +327 -0
  223. package/src/wire/from-wire.ts +1690 -0
  224. 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
+ }