@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,663 @@
1
+ /**
2
+ * Remap and gather helpers for raw typed arrays, columns and whole tables (design section 5.11):
3
+ * the propagation of attribute data through compaction (freeze step 10), derived graphs (section
4
+ * 7.3) and consumer re-attachment after a builder mutation. One name per shape:
5
+ *
6
+ * - `remapArray(data, remap, newLength, fill, components)` and `remapColumn(column, remap,
7
+ * newLength)` take an OLD -> NEW map (`INVALID_INDEX` drops the row);
8
+ * - `gatherArray(data, indexMap, components)` and `gatherColumn(column, indexMap)` take a NEW ->
9
+ * OLD map (`out[i] = source[indexMap[i]]`);
10
+ * - `scatterArray(out, values, indexMap, components)` writes results computed on a derived graph
11
+ * back into the parent's index space (`out[indexMap[i]] = values[i]`).
12
+ *
13
+ * Index-valued columns (`meta.refersTo`, a u32 column or a list of u32) have their VALUES rewritten
14
+ * through the remap of the space they reference: an in-range value maps to its new index, a
15
+ * dangling reference becomes `INVALID_INDEX` with the row unset (invariant I12; for a list, the
16
+ * dangling items are dropped and a row whose every item dangled becomes unset and empty).
17
+ */
18
+
19
+ import { INVALID_INDEX } from "../constants.js";
20
+ import { GraphFormatError } from "../errors.js";
21
+ import { type Column, type ColumnMeta, type F32, type TypedArrayData, type U32 } from "../types/index.js";
22
+ import { type MutableColumnParts } from "../types/internal.js";
23
+ import { bitmapClear, bitmapGet, bitmapSet, bitmapWordCount, makeBitmap } from "./bitmap.js";
24
+ import { allocU8, columnOfValues, createColumn, partsOf } from "./column.js";
25
+ import { AttributeTable } from "./table.js";
26
+
27
+ // ============================================================ raw arrays
28
+
29
+ /**
30
+ * A zeroed array of the same class as `like` (a padded store for u8).
31
+ * @param like - the array whose class to match
32
+ * @param length - the element count
33
+ * @returns the new array
34
+ */
35
+ function allocLike<T extends TypedArrayData>(like: T, length: number): T {
36
+ if (like instanceof Uint8Array) {
37
+ return allocU8(length) as T;
38
+ }
39
+ if (like instanceof Uint32Array) {
40
+ return new Uint32Array(length) as T;
41
+ }
42
+ if (like instanceof Int32Array) {
43
+ return new Int32Array(length) as T;
44
+ }
45
+ if (like instanceof Float32Array) {
46
+ return new Float32Array(length) as T;
47
+ }
48
+ return new Float64Array(length) as T;
49
+ }
50
+
51
+ function checkComponents(components: number, data: ArrayLike<unknown>): void {
52
+ if (!(Number.isInteger(components) && components >= 1)) {
53
+ throw new GraphFormatError("E_COLUMN_TYPE", `components must be a positive integer, found ${components}`, {
54
+ field: "components",
55
+ found: components,
56
+ });
57
+ }
58
+ if (data.length % components !== 0) {
59
+ throw new GraphFormatError(
60
+ "E_COLUMN_LENGTH",
61
+ `array length ${data.length} is not a multiple of ${components}`,
62
+ {
63
+ expected: components,
64
+ found: data.length,
65
+ },
66
+ );
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Move rows of an index-aligned array from an old index space to a new one through an OLD -> NEW
72
+ * remap (`FreezeReport.nodeRemap` / `edgeRemap`, `DerivedGraph.nodeRemap` / `edgeRemap`): row `i`
73
+ * lands at `remap[i]`, rows mapped to `INVALID_INDEX` are dropped, and rows of the new space no old
74
+ * row maps to hold `fill`.
75
+ * @param data - the old array, `oldRows * components` long
76
+ * @param remap - old index -> new index or INVALID_INDEX; rows beyond `remap.length` are dropped
77
+ * @param newLength - the row count of the new space
78
+ * @param fill - the value of rows no old row maps to
79
+ * @param components - values per row (default 1)
80
+ * @returns a new array of `newLength * components` values
81
+ */
82
+ export function remapArray<T extends TypedArrayData>(
83
+ data: T,
84
+ remap: U32,
85
+ newLength: number,
86
+ fill: number,
87
+ components = 1,
88
+ ): T {
89
+ checkComponents(components, data);
90
+ const out = allocLike(data, newLength * components);
91
+ if (fill !== 0) {
92
+ out.fill(fill);
93
+ }
94
+ const rows = Math.min(remap.length, data.length / components);
95
+ for (let i = 0; i < rows; i++) {
96
+ const target = remap[i];
97
+ if (target === INVALID_INDEX) {
98
+ continue;
99
+ }
100
+ if (target >= newLength) {
101
+ throw new GraphFormatError(
102
+ "E_INDEX_RANGE",
103
+ `remap[${i}] = ${target} is outside the new space of ${newLength} rows`,
104
+ {
105
+ index: i,
106
+ found: target,
107
+ length: newLength,
108
+ },
109
+ );
110
+ }
111
+ if (components === 1) {
112
+ out[target] = data[i];
113
+ } else {
114
+ for (let k = 0; k < components; k++) {
115
+ out[target * components + k] = data[i * components + k];
116
+ }
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /**
123
+ * Gather rows through a NEW -> OLD index map: `out[i] = data[indexMap[i]]` (`DerivedGraph.nodeOrigin`
124
+ * / `edgeOrigin`).
125
+ * @param data - the source array, `oldRows * components` long
126
+ * @param indexMap - new index -> old index, every entry in range
127
+ * @param components - values per row (default 1)
128
+ * @returns a new array of `indexMap.length * components` values
129
+ */
130
+ export function gatherArray<T extends TypedArrayData>(data: T, indexMap: U32, components = 1): T {
131
+ checkComponents(components, data);
132
+ const out = allocLike(data, indexMap.length * components);
133
+ const rows = data.length / components;
134
+ for (let i = 0; i < indexMap.length; i++) {
135
+ const source = indexMap[i];
136
+ if (source >= rows) {
137
+ throw new GraphFormatError(
138
+ "E_INDEX_RANGE",
139
+ `indexMap[${i}] = ${source} is outside the source of ${rows} rows`,
140
+ {
141
+ index: i,
142
+ found: source,
143
+ length: rows,
144
+ },
145
+ );
146
+ }
147
+ if (components === 1) {
148
+ out[i] = data[source];
149
+ } else {
150
+ for (let k = 0; k < components; k++) {
151
+ out[i * components + k] = data[source * components + k];
152
+ }
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /**
159
+ * Scatter values computed on a derived graph back into the parent's index space: `out[indexMap[i]]
160
+ * = values[i]` (through `nodeOrigin` / `edgeOrigin`). Rows of `out` no value maps to are untouched.
161
+ * @param out - the destination in the parent's index space
162
+ * @param values - the values in the derived index space, `indexMap.length * components` long
163
+ * @param indexMap - derived index -> parent index, every entry in range
164
+ * @param components - values per row (default 1)
165
+ * @returns `out`
166
+ */
167
+ export function scatterArray<T extends TypedArrayData>(out: T, values: T, indexMap: U32, components = 1): T {
168
+ checkComponents(components, values);
169
+ if (values.length !== indexMap.length * components) {
170
+ throw new GraphFormatError(
171
+ "E_COLUMN_LENGTH",
172
+ `values has ${values.length} entries, expected ${indexMap.length * components}`,
173
+ {
174
+ expected: indexMap.length * components,
175
+ found: values.length,
176
+ },
177
+ );
178
+ }
179
+ const rows = out.length / components;
180
+ for (let i = 0; i < indexMap.length; i++) {
181
+ const target = indexMap[i];
182
+ if (target >= rows) {
183
+ throw new GraphFormatError(
184
+ "E_INDEX_RANGE",
185
+ `indexMap[${i}] = ${target} is outside the destination of ${rows} rows`,
186
+ {
187
+ index: i,
188
+ found: target,
189
+ length: rows,
190
+ },
191
+ );
192
+ }
193
+ if (components === 1) {
194
+ out[target] = values[i];
195
+ } else {
196
+ for (let k = 0; k < components; k++) {
197
+ out[target * components + k] = values[i * components + k];
198
+ }
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+
204
+ // ============================================================ columns
205
+
206
+ /**
207
+ * Gather the validity bits of a column through a NEW -> OLD index map; entries equal to
208
+ * INVALID_INDEX are unset rows of the result.
209
+ * @param column - the source column
210
+ * @param indexMap - new index -> old index or INVALID_INDEX
211
+ * @returns the validity bitmap of the result (null when every row is set) and its null count
212
+ */
213
+ function gatherValidity(column: Column, indexMap: U32): { validity: U32 | null; nullCount: number } {
214
+ const source = column.validity;
215
+ let validity: U32 | null = null;
216
+ let nullCount = 0;
217
+ for (let i = 0; i < indexMap.length; i++) {
218
+ const from = indexMap[i];
219
+ const set = from !== INVALID_INDEX && (source === null || bitmapGet(source, from));
220
+ if (!set) {
221
+ validity ??= makeBitmap(indexMap.length, true);
222
+ bitmapClear(validity, i);
223
+ nullCount++;
224
+ }
225
+ }
226
+ return { validity, nullCount };
227
+ }
228
+
229
+ /**
230
+ * The metadata of a gathered column: the source metadata, made nullable when the gather produced
231
+ * unset rows that the source could not have had.
232
+ * @param meta - the source metadata
233
+ * @param nullCount - the result's null count
234
+ * @returns the metadata to use
235
+ */
236
+ function gatheredMeta(meta: ColumnMeta, nullCount: number): ColumnMeta {
237
+ if (meta.nullable || nullCount === 0) {
238
+ return meta;
239
+ }
240
+ return Object.freeze({ ...meta, nullable: true });
241
+ }
242
+
243
+ /**
244
+ * Gather the rows of a column through a NEW -> OLD index map: `out[i] = column[indexMap[i]]`
245
+ * (design section 5.11, `DerivedGraph.nodeOrigin` / `edgeOrigin`). An INVALID_INDEX entry yields an
246
+ * unset row holding the fill (the column becomes nullable if it was not). Values of a refersTo
247
+ * column are NOT rewritten here; compose with remapReferences() when the referenced space changed.
248
+ * @param column - the source column
249
+ * @param indexMap - new index -> old index (or INVALID_INDEX for a row with no source)
250
+ * @returns a new column of `indexMap.length` rows
251
+ */
252
+ export function gatherColumn(column: Column, indexMap: U32): Column {
253
+ const { length } = indexMap;
254
+ for (let i = 0; i < length; i++) {
255
+ const from = indexMap[i];
256
+ if (from !== INVALID_INDEX && from >= column.length) {
257
+ throw new GraphFormatError(
258
+ "E_INDEX_RANGE",
259
+ `indexMap[${i}] = ${from} is outside the column of ${column.length} rows`,
260
+ {
261
+ index: i,
262
+ found: from,
263
+ length: column.length,
264
+ column: column.meta.name,
265
+ },
266
+ );
267
+ }
268
+ }
269
+ const { validity, nullCount } = gatherValidity(column, indexMap);
270
+ const meta = gatheredMeta(column.meta, nullCount);
271
+ const parts: MutableColumnParts = {
272
+ ...partsOf(column),
273
+ meta,
274
+ length,
275
+ validity,
276
+ nullCount,
277
+ };
278
+ const { dtype } = column;
279
+ switch (dtype) {
280
+ case "f32":
281
+ case "f64":
282
+ case "i32":
283
+ case "u32":
284
+ case "u8": {
285
+ const { components, fill } = meta;
286
+ const out = allocLike(column.data, length * components);
287
+ if (typeof fill === "number" && fill !== 0) {
288
+ out.fill(fill);
289
+ }
290
+ for (let i = 0; i < length; i++) {
291
+ const from = indexMap[i];
292
+ if (from === INVALID_INDEX) {
293
+ continue;
294
+ }
295
+ for (let k = 0; k < components; k++) {
296
+ out[i * components + k] = column.data[from * components + k];
297
+ }
298
+ }
299
+ parts.data = out;
300
+ break;
301
+ }
302
+ case "bool": {
303
+ const out = new Uint32Array(bitmapWordCount(length)).fill(meta.fill === true ? 0xffffffff : 0);
304
+ for (let i = 0; i < length; i++) {
305
+ const from = indexMap[i];
306
+ if (from === INVALID_INDEX) {
307
+ continue;
308
+ }
309
+ if (bitmapGet(column.data, from)) {
310
+ bitmapSet(out, i);
311
+ } else {
312
+ bitmapClear(out, i);
313
+ }
314
+ }
315
+ parts.data = out;
316
+ break;
317
+ }
318
+ case "dict": {
319
+ const code = typeof meta.fill === "string" ? column.codeOf(meta.fill) : INVALID_INDEX;
320
+ const fillCode = code === INVALID_INDEX ? 0 : code;
321
+ const out = new Uint32Array(length);
322
+ for (let i = 0; i < length; i++) {
323
+ const from = indexMap[i];
324
+ out[i] = from === INVALID_INDEX ? fillCode : column.codes[from];
325
+ }
326
+ parts.data = out;
327
+ break;
328
+ }
329
+ case "string": {
330
+ const fillText = typeof meta.fill === "string" ? meta.fill : "";
331
+ const strings = new Array<string>(length);
332
+ for (let i = 0; i < length; i++) {
333
+ const from = indexMap[i];
334
+ strings[i] = from === INVALID_INDEX ? fillText : column.valueAt(from);
335
+ }
336
+ parts.offsets = null;
337
+ parts.utf8 = null;
338
+ parts.strings = strings;
339
+ break;
340
+ }
341
+ case "list": {
342
+ const { offsets, child } = column;
343
+ const newOffsets = new Uint32Array(length + 1);
344
+ let total = 0;
345
+ for (let i = 0; i < length; i++) {
346
+ const from = indexMap[i];
347
+ if (from !== INVALID_INDEX) {
348
+ total += offsets[from + 1] - offsets[from];
349
+ }
350
+ newOffsets[i + 1] = total;
351
+ }
352
+ const itemMap = new Uint32Array(total);
353
+ let at = 0;
354
+ for (let i = 0; i < length; i++) {
355
+ const from = indexMap[i];
356
+ if (from === INVALID_INDEX) {
357
+ continue;
358
+ }
359
+ for (let j = offsets[from]; j < offsets[from + 1]; j++) {
360
+ itemMap[at++] = j;
361
+ }
362
+ }
363
+ parts.offsets = newOffsets;
364
+ parts.child = gatherColumn(child, itemMap);
365
+ break;
366
+ }
367
+ case "json": {
368
+ const out = new Array<unknown>(length);
369
+ for (let i = 0; i < length; i++) {
370
+ const from = indexMap[i];
371
+ out[i] = from === INVALID_INDEX ? undefined : column.values[from];
372
+ }
373
+ parts.values = out;
374
+ break;
375
+ }
376
+ default: {
377
+ const name: string = dtype;
378
+ throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
379
+ }
380
+ }
381
+ return createColumn(parts);
382
+ }
383
+
384
+ /**
385
+ * The NEW -> OLD index map equivalent to an OLD -> NEW remap: entries of the new space no old row
386
+ * maps to are INVALID_INDEX.
387
+ * @param remap - old index -> new index or INVALID_INDEX
388
+ * @param oldLength - the number of old rows to consider (entries beyond are ignored)
389
+ * @param newLength - the row count of the new space
390
+ * @returns the inverse map
391
+ */
392
+ function invertRemap(remap: U32, oldLength: number, newLength: number): U32 {
393
+ const indexMap = new Uint32Array(newLength).fill(INVALID_INDEX);
394
+ const rows = Math.min(remap.length, oldLength);
395
+ for (let i = 0; i < rows; i++) {
396
+ const target = remap[i];
397
+ if (target === INVALID_INDEX) {
398
+ continue;
399
+ }
400
+ if (target >= newLength) {
401
+ throw new GraphFormatError(
402
+ "E_INDEX_RANGE",
403
+ `remap[${i}] = ${target} is outside the new space of ${newLength} rows`,
404
+ {
405
+ index: i,
406
+ found: target,
407
+ length: newLength,
408
+ },
409
+ );
410
+ }
411
+ indexMap[target] = i;
412
+ }
413
+ return indexMap;
414
+ }
415
+
416
+ /**
417
+ * Rewrite the values of an index-valued column through the remap of the space it references
418
+ * (design section 5.11): an in-range value maps to its new index; a dangling reference (mapped to
419
+ * INVALID_INDEX or out of range) becomes INVALID_INDEX with the row unset. For a list of u32 the
420
+ * dangling items are dropped and a row whose every item dangled becomes unset and empty. A column
421
+ * without refersTo is returned as it is.
422
+ * @param column - the column
423
+ * @param valueRemap - old index -> new index or INVALID_INDEX, over the referenced space
424
+ * @returns a new column when anything was rewritten; the same object otherwise
425
+ */
426
+ export function remapReferences(column: Column, valueRemap: U32): Column {
427
+ if (column.meta.refersTo === null) {
428
+ return column;
429
+ }
430
+ const mapValue = (value: number): number => {
431
+ if (value === INVALID_INDEX || value >= valueRemap.length) {
432
+ return INVALID_INDEX;
433
+ }
434
+ return valueRemap[value];
435
+ };
436
+ if (column.dtype === "u32") {
437
+ const { length } = column;
438
+ const data = new Uint32Array(length);
439
+ let validity = column.validity === null ? null : column.validity.slice();
440
+ let { nullCount } = column;
441
+ for (let row = 0; row < length; row++) {
442
+ const mapped = mapValue(column.data[row]);
443
+ data[row] = mapped;
444
+ if (mapped === INVALID_INDEX && (validity === null || bitmapGet(validity, row))) {
445
+ validity ??= makeBitmap(length, true);
446
+ bitmapClear(validity, row);
447
+ nullCount++;
448
+ }
449
+ }
450
+ const parts: MutableColumnParts = {
451
+ ...partsOf(column),
452
+ meta: gatheredMeta(column.meta, nullCount),
453
+ data,
454
+ validity,
455
+ nullCount,
456
+ };
457
+ return createColumn(parts);
458
+ }
459
+ if (column.dtype === "list" && column.child.dtype === "u32") {
460
+ const { length, offsets, child } = column;
461
+ const rows = new Array<number[]>(length);
462
+ let validity = column.validity === null ? null : column.validity.slice();
463
+ let { nullCount } = column;
464
+ for (let row = 0; row < length; row++) {
465
+ const items: number[] = [];
466
+ const start = offsets[row];
467
+ const end = offsets[row + 1];
468
+ for (let j = start; j < end; j++) {
469
+ const mapped = mapValue(child.data[j]);
470
+ if (mapped !== INVALID_INDEX) {
471
+ items.push(mapped);
472
+ }
473
+ }
474
+ rows[row] = items;
475
+ if (end > start && items.length === 0 && (validity === null || bitmapGet(validity, row))) {
476
+ validity ??= makeBitmap(length, true);
477
+ bitmapClear(validity, row);
478
+ nullCount++;
479
+ }
480
+ }
481
+ const meta = gatheredMeta(column.meta, nullCount);
482
+ const rebuilt = columnOfValues(Object.freeze({ ...meta, nullable: true }), rows);
483
+ const parts: MutableColumnParts = {
484
+ ...partsOf(rebuilt),
485
+ meta,
486
+ validity,
487
+ nullCount,
488
+ };
489
+ return createColumn(parts);
490
+ }
491
+ throw new GraphFormatError(
492
+ "E_COLUMN_TYPE",
493
+ `column "${column.meta.name}" has refersTo but is not u32 or a list of u32`,
494
+ {
495
+ column: column.meta.name,
496
+ field: "refersTo",
497
+ },
498
+ );
499
+ }
500
+
501
+ /**
502
+ * Move a column's rows from an old index space to a new one through an OLD -> NEW remap, rewriting
503
+ * its values through `valueRemap` when it is an index-valued column (design section 5.11): rows
504
+ * mapped to INVALID_INDEX are dropped, rows of the new space no old row maps to are unset and hold
505
+ * the fill (the column becomes nullable if it was not), and dangling references become
506
+ * INVALID_INDEX with the row unset.
507
+ * @param column - the source column
508
+ * @param remap - old row -> new row or INVALID_INDEX
509
+ * @param newLength - the row count of the new space
510
+ * @param valueRemap - the remap of the space the column's values reference, or null to leave values alone
511
+ * @returns a new column of `newLength` rows
512
+ */
513
+ export function remapColumnWith(column: Column, remap: U32, newLength: number, valueRemap: U32 | null): Column {
514
+ if (remap.length !== column.length) {
515
+ throw new GraphFormatError(
516
+ "E_COLUMN_LENGTH",
517
+ `remap has ${remap.length} entries for a column of ${column.length} rows`,
518
+ { column: column.meta.name, expected: column.length, found: remap.length },
519
+ );
520
+ }
521
+ const gathered = gatherColumn(column, invertRemap(remap, column.length, newLength));
522
+ return valueRemap === null ? gathered : remapReferences(gathered, valueRemap);
523
+ }
524
+
525
+ /**
526
+ * Move a column's rows through an OLD -> NEW remap and, when the column refers to its OWN index
527
+ * space (a node column with `refersTo: "node"` such as `parent`, an edge column with `refersTo:
528
+ * "edge"` such as `pair`), rewrite its values through the SAME remap (design sections 5.11 and
529
+ * 12.2). A column whose values reference the OTHER space (a node column referring to edges) has its
530
+ * rows moved and its values left untouched, because `remap` says nothing about that space; the
531
+ * internal remapColumnWith() / remapTable() take a second remap for it.
532
+ * @param column - the source column
533
+ * @param remap - old row -> new row or INVALID_INDEX
534
+ * @param newLength - the row count of the new space
535
+ * @returns a new column of `newLength` rows
536
+ */
537
+ export function remapColumn(column: Column, remap: U32, newLength: number): Column {
538
+ const { refersTo, domain } = column.meta;
539
+ const ownSpace = refersTo !== null && refersTo === domain;
540
+ return remapColumnWith(column, remap, newLength, ownSpace ? remap : null);
541
+ }
542
+
543
+ /**
544
+ * Change the stride of an interleaved f32 array (design section 5.2, the generic stride helper):
545
+ * every row of `from` values becomes a row of `to` values, extra lanes filled with `fill` and
546
+ * surplus lanes dropped. A 2D layout result expands to the 3-component position column with
547
+ * `withComponents(xy, 2, 3, 0)`; the inverse drops `z`. Returns `data` itself when `from === to`.
548
+ * @param data - the interleaved values, `rows * from` long
549
+ * @param from - values per row of `data`, 1..16
550
+ * @param to - values per row of the result, 1..16
551
+ * @param fill - the value of the lanes `to` adds beyond `from`
552
+ * @returns a new array of `rows * to` values (the input when the stride is unchanged)
553
+ */
554
+ export function withComponents(data: F32, from: number, to: number, fill: number): F32 {
555
+ for (const [field, value] of [
556
+ ["from", from],
557
+ ["to", to],
558
+ ] as const) {
559
+ if (!Number.isInteger(value) || value < 1 || value > 16) {
560
+ throw new GraphFormatError("E_COLUMN_TYPE", `${field} must be an integer in 1..16, found ${value}`, {
561
+ field,
562
+ found: value,
563
+ });
564
+ }
565
+ }
566
+ checkComponents(from, data);
567
+ if (from === to) {
568
+ return data;
569
+ }
570
+ const rows = data.length / from;
571
+ const out = new Float32Array(rows * to);
572
+ const keep = Math.min(from, to);
573
+ if (to > from && fill !== 0) {
574
+ out.fill(fill);
575
+ }
576
+ for (let row = 0; row < rows; row++) {
577
+ const src = row * from;
578
+ const dst = row * to;
579
+ for (let k = 0; k < keep; k++) {
580
+ out[dst + k] = data[src + k];
581
+ }
582
+ }
583
+ return out;
584
+ }
585
+
586
+ // ============================================================ tables
587
+
588
+ /**
589
+ * The remaps of the two index spaces a column's values may reference; null when that space was not
590
+ * renumbered.
591
+ */
592
+ interface ReferenceRemaps {
593
+ /** Old node index -> new node index or INVALID_INDEX; null when nodes were not renumbered. */
594
+ readonly node: U32 | null;
595
+ /** Old edge index -> new edge index or INVALID_INDEX; null when edges were not renumbered. */
596
+ readonly edge: U32 | null;
597
+ }
598
+
599
+ /**
600
+ * The value remap that applies to a column, if any.
601
+ * @param meta - the column metadata
602
+ * @param refs - the reference remaps
603
+ * @returns the remap of the referenced space, or null
604
+ */
605
+ function valueRemapFor(meta: ColumnMeta, refs: ReferenceRemaps): U32 | null {
606
+ if (meta.refersTo === "node") {
607
+ return refs.node;
608
+ }
609
+ if (meta.refersTo === "edge") {
610
+ return refs.edge;
611
+ }
612
+ return null;
613
+ }
614
+
615
+ /**
616
+ * Remap every column of a table (freeze step 10, the compaction case; extension tables with
617
+ * `rowRemap` null): rows move through `rowRemap` when given, and every refersTo column's values are
618
+ * rewritten through the remap of the space it references. The result is a new table; the source is
619
+ * untouched.
620
+ * @param table - the source table
621
+ * @param rowRemap - old row -> new row or INVALID_INDEX; null when the table's rows are unchanged
622
+ * @param newLength - the row count of the new table (ignored, and equal to table.rowCount, when rowRemap is null)
623
+ * @param refs - the node and edge remaps for index-valued columns
624
+ * @returns the remapped table
625
+ */
626
+ export function remapTable(
627
+ table: AttributeTable,
628
+ rowRemap: U32 | null,
629
+ newLength: number,
630
+ refs: ReferenceRemaps,
631
+ ): AttributeTable {
632
+ const rowCount = rowRemap === null ? table.rowCount : newLength;
633
+ const columns: Column[] = [];
634
+ for (const column of table) {
635
+ const valueRemap = valueRemapFor(column.meta, refs);
636
+ let out = column;
637
+ if (rowRemap !== null) {
638
+ out = remapColumnWith(column, rowRemap, newLength, valueRemap);
639
+ } else if (valueRemap !== null) {
640
+ out = remapReferences(column, valueRemap);
641
+ }
642
+ columns.push(out);
643
+ }
644
+ return new AttributeTable({ domain: table.domain, rowCount, columns });
645
+ }
646
+
647
+ /**
648
+ * Gather every column of a table through a NEW -> OLD index map (derived graphs: `nodeOrigin` /
649
+ * `edgeOrigin`), rewriting refersTo values through the remaps of the spaces they reference.
650
+ * @param table - the source table
651
+ * @param indexMap - new row -> old row
652
+ * @param refs - the node and edge remaps for index-valued columns
653
+ * @returns the gathered table of `indexMap.length` rows
654
+ */
655
+ export function gatherTable(table: AttributeTable, indexMap: U32, refs: ReferenceRemaps): AttributeTable {
656
+ const columns: Column[] = [];
657
+ for (const column of table) {
658
+ const gathered = gatherColumn(column, indexMap);
659
+ const valueRemap = valueRemapFor(column.meta, refs);
660
+ columns.push(valueRemap === null ? gathered : remapReferences(gathered, valueRemap));
661
+ }
662
+ return new AttributeTable({ domain: table.domain, rowCount: indexMap.length, columns });
663
+ }