@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,1940 @@
1
+ /**
2
+ * GraphBuilder: the only mutable object in the package and the producer of snapshots (design
3
+ * section 6). A long-lived, structure-of-arrays accumulator (compact.ts) that can be frozen
4
+ * repeatedly through the pipeline of freeze.ts. Node indices are assigned in first-seen order and
5
+ * edge indices in addEdge order (invariant I14); removal tombstones in O(degree) through the two
6
+ * incidence lists; a compacting freeze renumbers the builder's own indices to equal the new
7
+ * snapshot's (decision C7) and reports the remaps; freeze() never shares core arrays or columns with
8
+ * the snapshot (I18) while the id `Map` and `ids` array are shared by design (section 4.2).
9
+ *
10
+ * Every public method and getter throws E_BUILDER_DISPOSED after dispose(); every throw leaves the
11
+ * builder in a consistent state (design section 11.1): the failing operation is not applied (the
12
+ * record, composition and expansion methods validate everything they will write before the first
13
+ * mutation), and a freeze that fails commits nothing.
14
+ */
15
+ import { assertJsonValue, dtypeOfArray, metaToDecl, resolveColumnMeta } from "../columns/column.js";
16
+ import { assertWellFormedString } from "../columns/dictionary.js";
17
+ import { wideningRank } from "../columns/infer.js";
18
+ import { INVALID_INDEX, MAX_COUNT } from "../constants.js";
19
+ import { GraphFormatError } from "../errors.js";
20
+ import { describeId, validateNodeId } from "../ids/node-id-map.js";
21
+ import { detachString } from "../ids/string-store.js";
22
+ import { EMPTY_GRAPH_META, resolveGraphMeta } from "../snapshot/graph-meta.js";
23
+ import { createStagingColumn, inferInitialDtype, inferredEquivalent, Staging, StagingColumn, } from "./compact.js";
24
+ import { runFreeze } from "./freeze.js";
25
+ import { assertDuplicatePolicy } from "./options.js";
26
+ // ============================================================ option handling
27
+ const SELF_LOOP_POLICIES = new Set(["keep", "drop", "error"]);
28
+ const WEIGHT_DTYPES = new Set(["f32", "f64"]);
29
+ /** The name of the bool edge column written by an in-place expansion (design section 3.6). */
30
+ const DIRECTED_COLUMN = "graphty.directed";
31
+ /** The name of the u32 edge column pairing the halves of an expanded edge (design section 3.6). */
32
+ const PAIR_COLUMN = "graphty.pair";
33
+ /**
34
+ * The E_UNSUPPORTED error for an option value outside its documented set.
35
+ * @param field - the option name
36
+ * @param found - the value
37
+ * @returns the error
38
+ */
39
+ function optionError(field, found) {
40
+ return new GraphFormatError("E_UNSUPPORTED", `builder option ${field} has an unsupported value ${String(found)}`, {
41
+ field,
42
+ found,
43
+ reason: "unsupported option",
44
+ });
45
+ }
46
+ /**
47
+ * Resolve the constructor options (design section 12.2) with their defaults, checking every enum.
48
+ * @param options - the constructor options
49
+ * @returns the fixed options and the initial direction
50
+ */
51
+ function resolveOptions(options) {
52
+ if (typeof options !== "object" || options === null) {
53
+ throw optionError("options", options);
54
+ }
55
+ if (typeof options.directed !== "boolean") {
56
+ throw optionError("directed", options.directed);
57
+ }
58
+ const weighted = options.weighted ?? "auto";
59
+ if (weighted !== "auto" && typeof weighted !== "boolean") {
60
+ throw optionError("weighted", weighted);
61
+ }
62
+ const weightDtype = options.weightDtype ?? "f32";
63
+ if (!WEIGHT_DTYPES.has(weightDtype)) {
64
+ throw optionError("weightDtype", weightDtype);
65
+ }
66
+ const duplicateEdges = assertDuplicatePolicy("duplicateEdges", options.duplicateEdges ?? "keep", optionError);
67
+ const selfLoops = options.selfLoops ?? "keep";
68
+ if (!SELF_LOOP_POLICIES.has(selfLoops)) {
69
+ throw optionError("selfLoops", selfLoops);
70
+ }
71
+ const addMissingNodes = options.addMissingNodes ?? true;
72
+ if (typeof addMissingNodes !== "boolean") {
73
+ throw optionError("addMissingNodes", addMissingNodes);
74
+ }
75
+ const hint = (field) => {
76
+ const value = options[field];
77
+ if (value === undefined) {
78
+ return null;
79
+ }
80
+ if (!Number.isInteger(value) || value < 0) {
81
+ throw optionError(field, value);
82
+ }
83
+ return value;
84
+ };
85
+ return {
86
+ fixed: {
87
+ weighted,
88
+ weightDtype,
89
+ duplicateEdges,
90
+ selfLoops,
91
+ addMissingNodes,
92
+ expectedNodes: hint("expectedNodes"),
93
+ expectedEdges: hint("expectedEdges"),
94
+ },
95
+ directed: options.directed,
96
+ };
97
+ }
98
+ /**
99
+ * Drop the explicitly undefined fields of a patch so it can be spread over defaults.
100
+ * @param patch - the patch
101
+ * @returns the defined fields
102
+ */
103
+ function definedFields(patch) {
104
+ const out = {};
105
+ for (const key of Object.keys(patch)) {
106
+ const value = patch[key];
107
+ if (value !== undefined) {
108
+ out[key] = value;
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+ /**
114
+ * Whether two column declarations describe the same storage (the "same declaration" rule of design
115
+ * section 11.3 for a repeated declare*Column).
116
+ * @param a - one resolved metadata
117
+ * @param b - the other
118
+ * @returns true when dtype, components and the list child agree
119
+ */
120
+ function sameShape(a, b) {
121
+ return (a.dtype === b.dtype &&
122
+ a.components === b.components &&
123
+ a.itemDtype === b.itemDtype &&
124
+ a.itemComponents === b.itemComponents);
125
+ }
126
+ // ============================================================ the class
127
+ /**
128
+ * The mutable accumulator that produces frozen snapshots (design sections 6 and 12.2). Construct
129
+ * with `{ directed }` (required) and the policies of GraphBuilderOptions; push nodes and edges by id
130
+ * or by index; declare and write attribute columns; call `freeze()` as often as needed. Implements
131
+ * GraphSink, the subset importers program against (design section 8.3).
132
+ */
133
+ export class GraphBuilder {
134
+ /**
135
+ * Create an empty builder.
136
+ * @param options - the options; `directed` is required
137
+ */
138
+ constructor(options) {
139
+ this.locked = false;
140
+ this.optionsCache = null;
141
+ /** Column name -> index of the node staging columns; rebuilt when the column array is replaced. */
142
+ this.nodeNames = null;
143
+ /** Column name -> index of the edge staging columns; rebuilt when the column array is replaced. */
144
+ this.edgeNames = null;
145
+ this.graphValues = new Map();
146
+ this.metaValue = EMPTY_GRAPH_META;
147
+ this.widenings = [];
148
+ this.mutations = 0;
149
+ this.dirtyFlag = true;
150
+ this.disposed = false;
151
+ const resolved = resolveOptions(options);
152
+ this.fixed = resolved.fixed;
153
+ this.directedValue = resolved.directed;
154
+ this.staging = this.freshStaging();
155
+ }
156
+ /**
157
+ * Seed a builder from a snapshot in O(n + m) (design section 6.6): ids, edges in logical order
158
+ * with their declared orientation, weights (through the role-"weight" column when present, so an
159
+ * f64 shadow and the explicit / omitted record survive), columns, extension tables and meta;
160
+ * indices are preserved. The staging precision follows an f64 shadow unless the patch says
161
+ * otherwise.
162
+ * @param snapshot - the snapshot to continue
163
+ * @param options - option overrides; `directed` defaults to the snapshot's
164
+ * @returns the builder
165
+ */
166
+ static from(snapshot, options) {
167
+ const patch = definedFields(options ?? {});
168
+ const shadow = snapshot.edges.byRole("weight");
169
+ const weightDtype = patch.weightDtype ?? (shadow !== null && shadow.dtype === "f64" ? "f64" : undefined);
170
+ // a declared weight array is never dropped (design section 3.7): a weighted snapshot whose
171
+ // edges all omitted the weight seeds a `weighted: true` builder unless the patch says otherwise
172
+ const weighted = patch.weighted ?? (snapshot.flags.weighted ? true : undefined);
173
+ const builder = new GraphBuilder({
174
+ directed: snapshot.directed,
175
+ expectedNodes: snapshot.nodeCount,
176
+ expectedEdges: snapshot.edgeCount,
177
+ ...patch,
178
+ ...(weightDtype === undefined ? {} : { weightDtype }),
179
+ ...(weighted === undefined ? {} : { weighted }),
180
+ });
181
+ builder.addGraph(snapshot, { onDuplicateNode: "error" });
182
+ builder.setMeta(snapshot.meta);
183
+ return builder;
184
+ }
185
+ // ---------------------------------------------------------------- options and state
186
+ /**
187
+ * The resolved options; `directed` is the current value.
188
+ * @returns the options
189
+ */
190
+ get options() {
191
+ this.check();
192
+ this.optionsCache ?? (this.optionsCache = Object.freeze({ directed: this.directedValue, ...this.fixed }));
193
+ return this.optionsCache;
194
+ }
195
+ /**
196
+ * The current direction.
197
+ * @returns true when directed
198
+ */
199
+ get directed() {
200
+ this.check();
201
+ return this.directedValue;
202
+ }
203
+ /**
204
+ * Whether lockDirected() was called.
205
+ * @returns true when locked
206
+ */
207
+ get directedLocked() {
208
+ this.check();
209
+ return this.locked;
210
+ }
211
+ /**
212
+ * Live node count.
213
+ * @returns the count
214
+ */
215
+ get nodeCount() {
216
+ this.check();
217
+ return this.staging.liveNodeCount;
218
+ }
219
+ /**
220
+ * Live edge count (exact, from the incidence lists).
221
+ * @returns the count
222
+ */
223
+ get edgeCount() {
224
+ this.check();
225
+ return this.staging.liveEdgeCount;
226
+ }
227
+ /**
228
+ * Next node index to be assigned.
229
+ * @returns the bound
230
+ */
231
+ get nodeBound() {
232
+ this.check();
233
+ return this.staging.nodeBound;
234
+ }
235
+ /**
236
+ * Next logical edge index to be assigned.
237
+ * @returns the bound
238
+ */
239
+ get edgeBound() {
240
+ this.check();
241
+ return this.staging.edgeBound;
242
+ }
243
+ /**
244
+ * Increments on every topology or weight mutation; column writes and freeze() do not count.
245
+ * @returns the count
246
+ */
247
+ get mutationCount() {
248
+ this.check();
249
+ return this.mutations;
250
+ }
251
+ /**
252
+ * Whether the builder was mutated since the last freeze() (true for a builder never frozen).
253
+ * @returns true when a fresh freeze would differ from the last one
254
+ */
255
+ get dirty() {
256
+ this.check();
257
+ return this.dirtyFlag;
258
+ }
259
+ /**
260
+ * Change the direction (design section 6.6): a no-op when unchanged; free while no live edge
261
+ * exists; with live edges only undirected -> directed with `expand`, which appends a mirror edge
262
+ * for every live edge and writes the graphty.directed / graphty.pair columns. E_DIRECTED when
263
+ * locked or refused.
264
+ * @param directed - the new direction
265
+ * @param options - `expand` for the in-place expansion
266
+ */
267
+ setDirected(directed, options) {
268
+ this.check();
269
+ if (directed === this.directedValue) {
270
+ return;
271
+ }
272
+ if (this.locked) {
273
+ throw new GraphFormatError("E_DIRECTED", "the builder's direction is locked", {
274
+ reason: "locked",
275
+ directed: this.directedValue,
276
+ });
277
+ }
278
+ if (this.staging.liveEdgeCount === 0) {
279
+ this.directedValue = directed;
280
+ this.optionsCache = null;
281
+ this.mutated();
282
+ return;
283
+ }
284
+ if (!directed) {
285
+ throw new GraphFormatError("E_DIRECTED", "a builder with edges cannot become undirected", {
286
+ reason: "edges present",
287
+ edgeCount: this.staging.liveEdgeCount,
288
+ });
289
+ }
290
+ if (options?.expand !== true) {
291
+ throw new GraphFormatError("E_DIRECTED", "an undirected builder with edges becomes directed only with { expand: true }", { reason: "expand required", edgeCount: this.staging.liveEdgeCount });
292
+ }
293
+ this.expandToDirected();
294
+ }
295
+ /** Fix the direction: every later changing setDirected() throws E_DIRECTED. */
296
+ lockDirected() {
297
+ this.check();
298
+ this.locked = true;
299
+ }
300
+ // ---------------------------------------------------------------- nodes
301
+ /**
302
+ * Add a node, or return the index of the live node with this id; a tombstoned id is revived at
303
+ * its old index (design section 6.6).
304
+ * @param id - the node id; E_INVALID_ID when illegal
305
+ * @returns the node index
306
+ */
307
+ addNode(id) {
308
+ this.check();
309
+ return this.addValidatedNode(validateNodeId(id));
310
+ }
311
+ /**
312
+ * Add many nodes, writing every id's index (new or existing) into `out`.
313
+ * @param ids - the ids
314
+ * @param out - receives the indices; allocated when omitted; E_COLUMN_LENGTH when too short
315
+ * @returns `out`
316
+ */
317
+ addNodes(ids, out) {
318
+ this.check();
319
+ const list = Array.isArray(ids) ? ids : [...ids];
320
+ const validated = list.map((id) => validateNodeId(id));
321
+ if (out !== undefined && out.length < validated.length) {
322
+ throw new GraphFormatError("E_COLUMN_LENGTH", `out has ${out.length} entries for ${validated.length} ids`, {
323
+ expected: validated.length,
324
+ found: out.length,
325
+ });
326
+ }
327
+ const target = out ?? new Uint32Array(validated.length);
328
+ for (let i = 0; i < validated.length; i++) {
329
+ target[i] = this.addValidatedNode(validated[i]);
330
+ }
331
+ return target;
332
+ }
333
+ /**
334
+ * Append `count` nodes whose ids are their own indices, never touching the id Map while every
335
+ * node is anonymous (design section 6.6).
336
+ * @param count - how many; E_TOO_LARGE beyond MAX_COUNT; E_DUPLICATE_ID when an index is already an id
337
+ * @returns the first new index
338
+ */
339
+ addAnonymousNodes(count) {
340
+ this.check();
341
+ if (!Number.isInteger(count) || count < 0) {
342
+ throw new GraphFormatError("E_INDEX_RANGE", `node count ${count} is not a non-negative integer`, {
343
+ count,
344
+ });
345
+ }
346
+ const { staging } = this;
347
+ const first = staging.nodeBound;
348
+ this.checkNodeLimit(count);
349
+ if (staging.idToIndex !== null) {
350
+ for (let i = first; i < first + count; i++) {
351
+ if (staging.idToIndex.has(i)) {
352
+ throw new GraphFormatError("E_DUPLICATE_ID", `node id ${i} exists; anonymous node ${i} cannot use it`, {
353
+ id: i,
354
+ index: staging.idToIndex.get(i),
355
+ });
356
+ }
357
+ }
358
+ }
359
+ for (let i = 0; i < count; i++) {
360
+ staging.pushNode(null);
361
+ }
362
+ if (count > 0) {
363
+ this.mutated();
364
+ }
365
+ return first;
366
+ }
367
+ /**
368
+ * Whether a live node has this id (total: an illegal id is simply absent).
369
+ * @param id - the id
370
+ * @returns true when present and alive
371
+ */
372
+ hasNode(id) {
373
+ this.check();
374
+ return this.lookup(id) !== INVALID_INDEX;
375
+ }
376
+ /**
377
+ * Total lookup of a live node.
378
+ * @param id - the id
379
+ * @returns the index, or INVALID_INDEX
380
+ */
381
+ indexOf(id) {
382
+ this.check();
383
+ return this.lookup(id);
384
+ }
385
+ /**
386
+ * The id of a live node index.
387
+ * @param index - the index; E_INDEX_RANGE when out of range or tombstoned
388
+ * @returns the id
389
+ */
390
+ idOf(index) {
391
+ this.check();
392
+ const { staging } = this;
393
+ if (!this.isLiveNode(index)) {
394
+ throw new GraphFormatError("E_INDEX_RANGE", `node index ${index} is out of range or removed`, {
395
+ index,
396
+ bound: staging.nodeBound,
397
+ });
398
+ }
399
+ return staging.ids === null ? index : staging.ids[index];
400
+ }
401
+ /**
402
+ * Tombstone a node and every live incident edge (O(degree)); bumps mutationCount.
403
+ * @param id - the id; E_UNKNOWN_NODE when absent or already removed
404
+ * @returns the removed live incident edge indices, ascending
405
+ */
406
+ removeNode(id) {
407
+ this.check();
408
+ const index = this.lookup(id);
409
+ if (index === INVALID_INDEX) {
410
+ throw new GraphFormatError("E_UNKNOWN_NODE", `unknown node id ${describeId(id)}`, { id });
411
+ }
412
+ return this.removeLiveNode(index);
413
+ }
414
+ /**
415
+ * Tombstone a node by index and every live incident edge.
416
+ * @param index - the index; E_UNKNOWN_NODE (details.index) when out of range or removed
417
+ * @returns the removed live incident edge indices, ascending
418
+ */
419
+ removeNodeByIndex(index) {
420
+ this.check();
421
+ if (!this.isLiveNode(index)) {
422
+ throw new GraphFormatError("E_UNKNOWN_NODE", `node index ${index} is out of range or removed`, { index });
423
+ }
424
+ return this.removeLiveNode(index);
425
+ }
426
+ /**
427
+ * Grow staging capacity ahead of a bulk push.
428
+ * @param nodes - the node count to fit
429
+ * @param edges - the edge count to fit
430
+ */
431
+ reserve(nodes, edges) {
432
+ this.check();
433
+ const check = (field, value) => {
434
+ if (value === undefined) {
435
+ return 0;
436
+ }
437
+ if (!Number.isInteger(value) || value < 0 || value > MAX_COUNT) {
438
+ throw new GraphFormatError("E_TOO_LARGE", `reserve(${field}) ${value} is not an integer in [0, MAX_COUNT]`, {
439
+ field,
440
+ count: value,
441
+ max: MAX_COUNT,
442
+ });
443
+ }
444
+ return value;
445
+ };
446
+ this.staging.reserve(check("nodes", nodes), check("edges", edges));
447
+ }
448
+ // ---------------------------------------------------------------- edges
449
+ /**
450
+ * Add a logical edge by id; unknown endpoints are created when addMissingNodes is set (a
451
+ * tombstoned endpoint is revived), else E_UNKNOWN_NODE. A NaN weight is E_INVALID_WEIGHT.
452
+ * @param source - source id
453
+ * @param target - target id
454
+ * @param weight - the weight; 1 when omitted
455
+ * @returns the logical edge index
456
+ */
457
+ addEdge(source, target, weight) {
458
+ this.check();
459
+ const w = this.checkWeight(weight, undefined);
460
+ const u = this.resolveEndpoint(validateNodeId(source));
461
+ const v = this.resolveEndpoint(validateNodeId(target));
462
+ return this.pushEdge(u, v, w);
463
+ }
464
+ /**
465
+ * Add a logical edge by node index.
466
+ * @param u - source index; E_UNKNOWN_NODE (details.index) when out of range or removed
467
+ * @param v - target index
468
+ * @param weight - the weight; 1 when omitted
469
+ * @returns the logical edge index
470
+ */
471
+ addEdgeByIndex(u, v, weight) {
472
+ this.check();
473
+ const w = this.checkWeight(weight, undefined);
474
+ this.checkLiveNode(u);
475
+ this.checkLiveNode(v);
476
+ return this.pushEdge(u, v, w);
477
+ }
478
+ /**
479
+ * Bulk add edges by node index (the index-space path used with addAnonymousNodes). Everything is
480
+ * validated before the first edge is pushed.
481
+ * @param src - source indices
482
+ * @param dst - target indices; E_COLUMN_LENGTH when the lengths differ
483
+ * @param weights - per-edge weights; 1 when omitted; E_INVALID_WEIGHT for NaN
484
+ * @returns the first new logical edge index
485
+ */
486
+ addEdges(src, dst, weights) {
487
+ this.check();
488
+ const count = src.length;
489
+ if (dst.length !== count || (weights !== undefined && weights.length !== count)) {
490
+ throw new GraphFormatError("E_COLUMN_LENGTH", `addEdges: src has ${count} entries, dst ${dst.length}, weights ${weights === undefined ? "none" : weights.length}`, { src: count, dst: dst.length, weights: weights?.length ?? null });
491
+ }
492
+ for (let i = 0; i < count; i++) {
493
+ this.checkLiveNode(src[i]);
494
+ this.checkLiveNode(dst[i]);
495
+ }
496
+ let explicit = null;
497
+ if (weights !== undefined) {
498
+ for (let i = 0; i < count; i++) {
499
+ this.checkWeight(weights[i], i);
500
+ }
501
+ explicit = this.fixed.weighted === false ? null : weights;
502
+ }
503
+ this.checkEdgeLimit(count, this.arcsOf(src, dst, count));
504
+ const { staging } = this;
505
+ staging.reserve(0, staging.edgeBound + count);
506
+ const first = staging.pushEdges(src, dst, explicit);
507
+ if (count > 0) {
508
+ this.mutated();
509
+ }
510
+ return first;
511
+ }
512
+ /**
513
+ * Bulk add edges by id. Every id and weight is validated before anything is added.
514
+ * @param src - source ids
515
+ * @param dst - target ids; E_COLUMN_LENGTH when the lengths differ
516
+ * @param weights - per-edge weights; 1 when omitted
517
+ * @returns the first new logical edge index
518
+ */
519
+ addEdgesByIds(src, dst, weights) {
520
+ this.check();
521
+ const count = src.length;
522
+ if (dst.length !== count || (weights !== undefined && weights.length !== count)) {
523
+ throw new GraphFormatError("E_COLUMN_LENGTH", `addEdgesByIds: src has ${count} entries, dst ${dst.length}, weights ${weights === undefined ? "none" : weights.length}`, { src: count, dst: dst.length, weights: weights?.length ?? null });
524
+ }
525
+ const sources = new Array(count);
526
+ const targets = new Array(count);
527
+ const checked = new Array(count);
528
+ for (let i = 0; i < count; i++) {
529
+ sources[i] = validateNodeId(src[i]);
530
+ targets[i] = validateNodeId(dst[i]);
531
+ checked[i] = this.checkWeight(weights === undefined ? undefined : weights[i], i);
532
+ }
533
+ if (!this.fixed.addMissingNodes) {
534
+ for (let i = 0; i < count; i++) {
535
+ this.requireLive(sources[i]);
536
+ this.requireLive(targets[i]);
537
+ }
538
+ }
539
+ this.checkEdgeLimit(count, this.directedValue ? count : 2 * count);
540
+ const first = this.staging.edgeBound;
541
+ for (let i = 0; i < count; i++) {
542
+ const u = this.resolveEndpoint(sources[i]);
543
+ const v = this.resolveEndpoint(targets[i]);
544
+ this.staging.pushEdge(u, v, checked[i]);
545
+ }
546
+ if (count > 0) {
547
+ this.mutated();
548
+ }
549
+ return first;
550
+ }
551
+ /**
552
+ * Tombstone an edge (O(1)); bumps mutationCount when something was removed.
553
+ * @param edge - the logical edge index
554
+ * @returns true when a live edge was removed
555
+ */
556
+ removeEdge(edge) {
557
+ this.check();
558
+ if (!this.isLiveEdge(edge)) {
559
+ return false;
560
+ }
561
+ this.staging.killEdge(edge);
562
+ this.mutated();
563
+ return true;
564
+ }
565
+ /**
566
+ * Whether an edge index is live.
567
+ * @param edge - the logical edge index
568
+ * @returns true when live
569
+ */
570
+ hasEdge(edge) {
571
+ this.check();
572
+ return this.isLiveEdge(edge);
573
+ }
574
+ /**
575
+ * Read a live edge's endpoints back.
576
+ * @param edge - the logical edge index; E_INDEX_RANGE when out of range or removed
577
+ * @returns [source index, target index] in declared orientation
578
+ */
579
+ edgeEndpoints(edge) {
580
+ this.check();
581
+ this.checkEdge(edge);
582
+ return [this.staging.src.get(edge), this.staging.dst.get(edge)];
583
+ }
584
+ /**
585
+ * Read a live edge's weight back.
586
+ * @param edge - the logical edge index; E_INDEX_RANGE when out of range or removed
587
+ * @returns the weight (1 when unweighted)
588
+ */
589
+ edgeWeight(edge) {
590
+ this.check();
591
+ this.checkEdge(edge);
592
+ const { weight } = this.staging;
593
+ return weight === null ? 1 : weight.get(edge);
594
+ }
595
+ /**
596
+ * Set the weight of a live edge, allocating the weight array on first use and marking the weight
597
+ * explicit; bumps mutationCount.
598
+ * @param edge - the logical edge index; E_INDEX_RANGE when out of range or removed
599
+ * @param weight - the weight; E_INVALID_WEIGHT for NaN
600
+ */
601
+ setEdgeWeight(edge, weight) {
602
+ this.check();
603
+ this.checkEdge(edge);
604
+ const w = this.checkWeight(weight, undefined);
605
+ const { staging } = this;
606
+ if (w !== undefined) {
607
+ staging.ensureWeights().set(edge, w);
608
+ staging.trackWeight(edge, true);
609
+ }
610
+ this.mutated();
611
+ }
612
+ /**
613
+ * Live edges leaving a node, from the out-list (O(degree)). On an undirected builder every
614
+ * incident edge leaves the node (invariant I7, the snapshot's row holds both orientations), so
615
+ * both lists are walked and a self-loop is listed once.
616
+ * @param index - the node index; E_INDEX_RANGE when out of range
617
+ * @returns a fresh ascending array of live edge indices
618
+ */
619
+ outEdgesOf(index) {
620
+ this.check();
621
+ this.checkNodeIndex(index);
622
+ return this.directedValue ? this.staging.outEdges(index) : this.staging.incidentEdges(index);
623
+ }
624
+ /**
625
+ * Live edges entering a node, from the in-list (O(degree)); on an undirected builder the same
626
+ * set as outEdgesOf (the alias rule of inDegree === outDegree).
627
+ * @param index - the node index; E_INDEX_RANGE when out of range
628
+ * @returns a fresh ascending array of live edge indices
629
+ */
630
+ inEdgesOf(index) {
631
+ this.check();
632
+ this.checkNodeIndex(index);
633
+ return this.directedValue ? this.staging.inEdges(index) : this.staging.incidentEdges(index);
634
+ }
635
+ /**
636
+ * Live edges u -> v (undirected: either orientation), from the incidence lists.
637
+ * @param u - source index; E_INDEX_RANGE when out of range
638
+ * @param v - target index; E_INDEX_RANGE when out of range
639
+ * @returns a fresh ascending array of live edge indices
640
+ */
641
+ findEdges(u, v) {
642
+ this.check();
643
+ this.checkNodeIndex(u);
644
+ this.checkNodeIndex(v);
645
+ const { staging } = this;
646
+ const found = [];
647
+ for (let e = staging.firstOut.get(u); e !== INVALID_INDEX; e = staging.nextOut.get(e)) {
648
+ if (staging.edgeAlive.get(e) && staging.dst.get(e) === v) {
649
+ found.push(e);
650
+ }
651
+ }
652
+ if (!this.directedValue && u !== v) {
653
+ for (let e = staging.firstIn.get(u); e !== INVALID_INDEX; e = staging.nextIn.get(e)) {
654
+ if (staging.edgeAlive.get(e) && staging.src.get(e) === v) {
655
+ found.push(e);
656
+ }
657
+ }
658
+ }
659
+ return Uint32Array.from(found).sort();
660
+ }
661
+ // ---------------------------------------------------------------- attributes
662
+ /**
663
+ * Declare a node column; the same shape again returns the existing handle, a different shape is
664
+ * E_COLUMN_EXISTS, a taken role E_DUPLICATE_ROLE.
665
+ * @param decl - the declaration
666
+ * @returns the column handle
667
+ */
668
+ declareNodeColumn(decl) {
669
+ this.check();
670
+ return this.declare("node", decl);
671
+ }
672
+ /**
673
+ * Declare an edge column; the same shape again returns the existing handle, a different shape is
674
+ * E_COLUMN_EXISTS, a taken role E_DUPLICATE_ROLE.
675
+ * @param decl - the declaration
676
+ * @returns the column handle
677
+ */
678
+ declareEdgeColumn(decl) {
679
+ this.check();
680
+ return this.declare("edge", decl);
681
+ }
682
+ /**
683
+ * Look up a node column handle.
684
+ * @param name - the column name
685
+ * @returns the handle, or INVALID_INDEX when absent
686
+ */
687
+ nodeColumn(name) {
688
+ this.check();
689
+ return this.handleOf(this.staging.nodeColumns, name);
690
+ }
691
+ /**
692
+ * Look up an edge column handle.
693
+ * @param name - the column name
694
+ * @returns the handle, or INVALID_INDEX when absent
695
+ */
696
+ edgeColumn(name) {
697
+ this.check();
698
+ return this.handleOf(this.staging.edgeColumns, name);
699
+ }
700
+ /**
701
+ * Set one node cell; a string column name auto-declares an inferred column (design section 5.1)
702
+ * on the first set value, and an unset value for an undeclared name is a no-op.
703
+ * @param column - the handle (E_UNKNOWN_COLUMN when stale) or name
704
+ * @param index - the node index; E_INDEX_RANGE when out of range
705
+ * @param value - the value; undefined / null unset the row
706
+ */
707
+ setNodeValue(column, index, value) {
708
+ this.check();
709
+ this.checkNodeIndex(index);
710
+ this.writeCell("node", column, index, value);
711
+ }
712
+ /**
713
+ * Set one edge cell; a string column name auto-declares an inferred column.
714
+ * @param column - the handle (E_UNKNOWN_COLUMN when stale) or name
715
+ * @param edge - the logical edge index; E_INDEX_RANGE when out of range
716
+ * @param value - the value; undefined / null unset the row
717
+ */
718
+ setEdgeValue(column, edge, value) {
719
+ this.check();
720
+ this.checkEdgeIndex(edge);
721
+ this.writeCell("edge", column, edge, value);
722
+ }
723
+ /**
724
+ * Widen an inferred node column to a wider dtype of the design section 5.1 order without
725
+ * changing any value (an importer that knows from the lexical grammar that a column of `2.0`
726
+ * cells is f64 although every value so far was integral). A no-op when the column is already
727
+ * as wide; E_COLUMN_TYPE for a declared (non-inferred) column, a dtype outside the inference
728
+ * order or a narrower one; the widening is reported in FreezeReport.widened.
729
+ * @param column - the handle (E_UNKNOWN_COLUMN when stale) or name (E_UNKNOWN_COLUMN when absent)
730
+ * @param dtype - the dtype to widen to
731
+ */
732
+ widenNodeColumn(column, dtype) {
733
+ this.check();
734
+ this.widenColumn("node", column, dtype);
735
+ }
736
+ /**
737
+ * Widen an inferred edge column to a wider dtype of the design section 5.1 order without
738
+ * changing any value; see widenNodeColumn().
739
+ * @param column - the handle (E_UNKNOWN_COLUMN when stale) or name (E_UNKNOWN_COLUMN when absent)
740
+ * @param dtype - the dtype to widen to
741
+ */
742
+ widenEdgeColumn(column, dtype) {
743
+ this.check();
744
+ this.widenColumn("edge", column, dtype);
745
+ }
746
+ /**
747
+ * Bulk-set a node column from a typed array (copied into staging); replaces a column of the same
748
+ * name. The length must be nodeBound * components (bool: ceil(nodeBound / 32) words).
749
+ * @param name - the column name
750
+ * @param data - the values
751
+ * @param decl - declaration fields (dtype defaults to the array's)
752
+ */
753
+ setNodeColumn(name, data, decl) {
754
+ this.check();
755
+ this.setBulkColumn("node", this.staging.nodeBound, name, data, decl ?? {});
756
+ }
757
+ /**
758
+ * Bulk-set an edge column from a typed array (copied into staging); replaces a column of the same
759
+ * name. The length must be edgeBound * components.
760
+ * @param name - the column name
761
+ * @param data - the values
762
+ * @param decl - declaration fields (dtype defaults to the array's)
763
+ */
764
+ setEdgeColumn(name, data, decl) {
765
+ this.check();
766
+ this.setBulkColumn("edge", this.staging.edgeBound, name, data, decl ?? {});
767
+ }
768
+ /**
769
+ * Set a graph-level attribute (the graph table's single row); the value and declaration are
770
+ * checked now, the column is built at freeze.
771
+ * @param name - the column name
772
+ * @param value - the value; undefined unsets it
773
+ * @param decl - declaration fields for the column
774
+ */
775
+ setGraphValue(name, value, decl) {
776
+ this.check();
777
+ const patch = decl ?? {};
778
+ // build once now so a bad value or declaration fails at the call, not at freeze
779
+ resolveColumnMeta(name, "graph", { dtype: "json", ...patch });
780
+ if (value !== undefined) {
781
+ buildGraphColumn(name, value, patch);
782
+ }
783
+ this.graphValues.set(name, { decl: patch, value });
784
+ }
785
+ /**
786
+ * Merge fields into the graph metadata (design section 5.9); undefined fields are left alone,
787
+ * `extra` must be a JSON value (E_COLUMN_TYPE with details.field otherwise).
788
+ * @param meta - the fields to set
789
+ */
790
+ setMeta(meta) {
791
+ this.check();
792
+ this.metaValue = resolveGraphMeta(this.metaValue, meta);
793
+ }
794
+ /**
795
+ * Create an extension table (design section 5.10).
796
+ * @param name - the table name; E_COLUMN_EXISTS when taken
797
+ * @param decls - its columns; E_COLUMN_EXISTS for a repeated name, E_DUPLICATE_ROLE for a repeated role
798
+ * @returns the table handle
799
+ */
800
+ addExtensionTable(name, decls) {
801
+ this.check();
802
+ const { staging } = this;
803
+ if (staging.extensions.some((table) => table.name === name)) {
804
+ throw new GraphFormatError("E_COLUMN_EXISTS", `extension table "${name}" already exists`, { table: name });
805
+ }
806
+ const columns = [];
807
+ for (const decl of decls) {
808
+ const column = createStagingColumn(decl.name, "extension", decl, false);
809
+ if (columns.some((other) => other.meta.name === column.meta.name)) {
810
+ throw new GraphFormatError("E_COLUMN_EXISTS", `column "${decl.name}" is declared twice in "${name}"`, {
811
+ table: name,
812
+ column: decl.name,
813
+ });
814
+ }
815
+ checkRole(columns, column.meta, name);
816
+ columns.push(column);
817
+ }
818
+ staging.extensions.push({ name, columns, rowCount: 0 });
819
+ return (staging.extensions.length - 1);
820
+ }
821
+ /**
822
+ * Append a row to an extension table.
823
+ * @param table - the table handle; E_INDEX_RANGE when unknown
824
+ * @param values - one value per declared column; E_COLUMN_LENGTH otherwise
825
+ * @returns the new row index
826
+ */
827
+ addExtensionRow(table, values) {
828
+ this.check();
829
+ const target = this.extensionOf(table);
830
+ if (values.length !== target.columns.length) {
831
+ throw new GraphFormatError("E_COLUMN_LENGTH", `extension table "${target.name}" has ${target.columns.length} columns; ${values.length} values given`, { table: target.name, expected: target.columns.length, found: values.length });
832
+ }
833
+ const row = target.rowCount;
834
+ const checked = target.columns.map((column, i) => {
835
+ const value = this.checkReference(column.meta, values[i]);
836
+ column.checkValue(row, value);
837
+ return value;
838
+ });
839
+ for (let i = 0; i < checked.length; i++) {
840
+ target.columns[i].write(row, checked[i]);
841
+ }
842
+ target.rowCount = row + 1;
843
+ return row;
844
+ }
845
+ /**
846
+ * Add a node from a record: every key present overwrites that row (last-write-wins per attribute,
847
+ * design section 6.6), auto-declaring inferred columns.
848
+ * @param id - the node id
849
+ * @param attrs - attribute values keyed by column name
850
+ * @returns the node index
851
+ */
852
+ addNodeRecord(id, attrs) {
853
+ this.check();
854
+ const validated = validateNodeId(id);
855
+ const keys = Object.keys(attrs);
856
+ // every attribute is validated before the node is added (design section 11.1)
857
+ this.checkCells("node", this.prospectiveNodeIndex(validated), keys, attrs);
858
+ const index = this.addValidatedNode(validated);
859
+ for (const key of keys) {
860
+ this.writeCell("node", key, index, attrs[key]);
861
+ }
862
+ return index;
863
+ }
864
+ /**
865
+ * Add an edge from a record; always a new edge (parallels are kept). The weight comes from
866
+ * `weightKey` ("weight" by default; null = none) and must be a number (E_INVALID_WEIGHT).
867
+ * @param source - source id
868
+ * @param target - target id
869
+ * @param attrs - attribute values keyed by column name
870
+ * @param weightKey - the key holding the weight; default "weight"; null = no weight
871
+ * @returns the logical edge index
872
+ */
873
+ addEdgeRecord(source, target, attrs, weightKey) {
874
+ this.check();
875
+ const key = weightKey === undefined ? "weight" : weightKey;
876
+ let weight;
877
+ if (key !== null && Object.prototype.hasOwnProperty.call(attrs, key)) {
878
+ const raw = attrs[key];
879
+ if (raw !== undefined && raw !== null) {
880
+ if (typeof raw !== "number") {
881
+ throw new GraphFormatError("E_INVALID_WEIGHT", `edge weight "${key}" is not a number`, {
882
+ key,
883
+ found: typeof raw,
884
+ });
885
+ }
886
+ weight = raw;
887
+ }
888
+ }
889
+ const keys = Object.keys(attrs).filter((name) => name !== key);
890
+ // every attribute is validated before the edge (and any missing endpoint) is added (design 11.1)
891
+ this.checkWeight(weight, undefined);
892
+ this.checkCells("edge", this.staging.edgeBound, keys, attrs);
893
+ const edge = this.addEdge(source, target, weight);
894
+ for (const name of keys) {
895
+ this.writeCell("edge", name, edge, attrs[name]);
896
+ }
897
+ return edge;
898
+ }
899
+ // ---------------------------------------------------------------- composition
900
+ /**
901
+ * Append another snapshot (design section 6.6): nodes are merged by id ("merge": the incoming set
902
+ * rows overwrite; "error": E_DUPLICATE_ID), edges are appended in logical order with their
903
+ * declared orientation and weights, columns declared in both graphs widen to the union dtype of
904
+ * design section 5.1, dictionaries are re-interned, extension tables are appended with their
905
+ * references rewritten, and graph attributes overwrite by name.
906
+ * @param snapshot - the snapshot to append
907
+ * @param options - duplicate-id handling
908
+ * @param options.onDuplicateNode - "merge" (default) or "error"
909
+ */
910
+ addGraph(snapshot, options) {
911
+ this.check();
912
+ const onDuplicate = options?.onDuplicateNode ?? "merge";
913
+ if (onDuplicate !== "merge" && onDuplicate !== "error") {
914
+ throw new GraphFormatError("E_UNSUPPORTED", `addGraph option onDuplicateNode has an unsupported value ${String(onDuplicate)}`, { field: "onDuplicateNode", found: onDuplicate, reason: "unsupported option" });
915
+ }
916
+ if (snapshot.directed !== this.directedValue) {
917
+ throw new GraphFormatError("E_DIRECTED", `a ${snapshot.directed ? "directed" : "undirected"} snapshot cannot be appended to a ${this.directedValue ? "directed" : "undirected"} builder`, { reason: "direction mismatch", directed: this.directedValue, found: snapshot.directed });
918
+ }
919
+ const { staging } = this;
920
+ const { nodeCount, edgeCount } = snapshot;
921
+ // everything that can be refused is checked before the first mutation (design section 11.1):
922
+ // duplicate ids, the count limits, weights, roles and extension column names
923
+ const anonymous = staging.ids === null &&
924
+ staging.nodeBound === 0 &&
925
+ snapshot.ids.kind === "identity" &&
926
+ snapshot.ids.offset === 0;
927
+ const nodeMap = new Uint32Array(nodeCount);
928
+ let newNodes = nodeCount;
929
+ if (!anonymous) {
930
+ newNodes = 0;
931
+ for (let i = 0; i < nodeCount; i++) {
932
+ const id = snapshot.ids.idOf(i);
933
+ const existing = this.lookup(id);
934
+ if (existing === INVALID_INDEX) {
935
+ newNodes++;
936
+ }
937
+ else if (onDuplicate === "error") {
938
+ throw new GraphFormatError("E_DUPLICATE_ID", `node id ${describeId(id)} already exists`, {
939
+ id,
940
+ index: existing,
941
+ });
942
+ }
943
+ }
944
+ }
945
+ this.checkNodeLimit(newNodes);
946
+ const list = snapshot.edgeList();
947
+ this.checkEdgeLimit(edgeCount, this.arcsOf(list.src, list.dst, edgeCount));
948
+ const shadow = snapshot.edges.byRole("weight");
949
+ const weights = new Array(edgeCount);
950
+ for (let e = 0; e < edgeCount; e++) {
951
+ let weight;
952
+ if (shadow !== null) {
953
+ weight = shadow.isSet(e) ? shadow.value(e) : undefined;
954
+ }
955
+ else if (list.weights !== null) {
956
+ weight = list.weights[e];
957
+ }
958
+ weights[e] = this.checkWeight(weight, e);
959
+ }
960
+ for (const column of snapshot.nodes) {
961
+ this.checkIncomingColumn("node", column);
962
+ }
963
+ for (const column of snapshot.edges) {
964
+ if (column !== shadow) {
965
+ this.checkIncomingColumn("edge", column);
966
+ }
967
+ }
968
+ for (const [name, table] of snapshot.extensions) {
969
+ this.checkIncomingExtension(name, table);
970
+ }
971
+ // nodes
972
+ if (anonymous) {
973
+ const first = this.addAnonymousNodes(nodeCount);
974
+ for (let i = 0; i < nodeCount; i++) {
975
+ nodeMap[i] = first + i;
976
+ }
977
+ }
978
+ else {
979
+ for (let i = 0; i < nodeCount; i++) {
980
+ const id = snapshot.ids.idOf(i);
981
+ const existing = this.lookup(id);
982
+ nodeMap[i] = existing === INVALID_INDEX ? this.addValidatedNode(id) : existing;
983
+ }
984
+ }
985
+ // edges: declared orientation, weights through the role column when present; a weighted
986
+ // snapshot keeps its declared weight array (design section 3.7) unless the builder is unweighted
987
+ if (snapshot.flags.weighted && this.fixed.weighted !== false) {
988
+ staging.ensureWeights();
989
+ }
990
+ const edgeMap = new Uint32Array(edgeCount);
991
+ for (let e = 0; e < edgeCount; e++) {
992
+ edgeMap[e] = this.pushEdge(nodeMap[list.src[e]], nodeMap[list.dst[e]], weights[e]);
993
+ }
994
+ // columns
995
+ const refs = { node: nodeMap, edge: edgeMap };
996
+ for (const column of snapshot.nodes) {
997
+ this.copyColumn("node", column, nodeMap, refs);
998
+ }
999
+ for (const column of snapshot.edges) {
1000
+ if (column !== shadow) {
1001
+ this.copyColumn("edge", column, edgeMap, refs);
1002
+ }
1003
+ }
1004
+ for (const column of snapshot.graph) {
1005
+ if (column.isSet(0)) {
1006
+ this.setGraphValue(column.meta.name, column.value(0), metaToDecl(column.meta));
1007
+ }
1008
+ }
1009
+ for (const [name, table] of snapshot.extensions) {
1010
+ this.appendExtension(name, table, refs);
1011
+ }
1012
+ }
1013
+ // ---------------------------------------------------------------- output and lifecycle
1014
+ /**
1015
+ * Run the freeze pipeline of design section 6.3 and return a snapshot; the builder keeps its
1016
+ * staging (compacted and renumbered when tombstones existed, rewritten by a merge policy) unless
1017
+ * `release` is set.
1018
+ * @param options - the freeze options
1019
+ * @returns the snapshot
1020
+ */
1021
+ freeze(options) {
1022
+ return this.freezeWithReport(options).snapshot;
1023
+ }
1024
+ /**
1025
+ * freeze() plus the report of what was renumbered, merged, dropped and widened (design section
1026
+ * 6.6): the remaps are relative to the previous freeze (the builder's own index space on the
1027
+ * first) and null exactly when nothing was renumbered.
1028
+ * @param options - the freeze options
1029
+ * @returns the snapshot and its report
1030
+ */
1031
+ freezeWithReport(options) {
1032
+ this.check();
1033
+ const opts = options ?? {};
1034
+ const context = {
1035
+ staging: this.staging,
1036
+ directed: this.directedValue,
1037
+ options: this.options,
1038
+ graphValues: this.graphValues,
1039
+ meta: this.metaValue,
1040
+ widened: this.widenings,
1041
+ };
1042
+ const outcome = runFreeze(context, opts);
1043
+ this.staging = outcome.staging;
1044
+ this.widenings = [];
1045
+ this.dirtyFlag = false;
1046
+ if (opts.release === true) {
1047
+ this.staging = this.freshStaging();
1048
+ }
1049
+ return { snapshot: outcome.snapshot, report: outcome.report };
1050
+ }
1051
+ /** Empty the builder (nodes, edges, columns, extension tables, graph attributes, meta), keeping its options and lock. */
1052
+ clear() {
1053
+ this.check();
1054
+ this.staging = this.freshStaging();
1055
+ this.graphValues = new Map();
1056
+ this.metaValue = EMPTY_GRAPH_META;
1057
+ this.widenings = [];
1058
+ this.mutated();
1059
+ }
1060
+ /** Release everything; every further call throws E_BUILDER_DISPOSED. */
1061
+ dispose() {
1062
+ if (this.disposed) {
1063
+ return;
1064
+ }
1065
+ this.staging = new Staging({
1066
+ weightDtype: this.fixed.weightDtype,
1067
+ weighted: false,
1068
+ expectedNodes: null,
1069
+ expectedEdges: null,
1070
+ resizable: undefined,
1071
+ });
1072
+ this.graphValues = new Map();
1073
+ this.metaValue = EMPTY_GRAPH_META;
1074
+ this.widenings = [];
1075
+ this.disposed = true;
1076
+ }
1077
+ /**
1078
+ * Bytes of typed staging currently held (capacity, not length; JS arrays and the id Map excluded).
1079
+ * @returns the byte count
1080
+ */
1081
+ byteLength() {
1082
+ this.check();
1083
+ return this.staging.byteLength();
1084
+ }
1085
+ // ---------------------------------------------------------------- private: state
1086
+ check() {
1087
+ if (this.disposed) {
1088
+ throw new GraphFormatError("E_BUILDER_DISPOSED", "the builder was disposed", {});
1089
+ }
1090
+ }
1091
+ mutated() {
1092
+ this.mutations++;
1093
+ this.dirtyFlag = true;
1094
+ }
1095
+ freshStaging() {
1096
+ return new Staging({
1097
+ weightDtype: this.fixed.weightDtype,
1098
+ weighted: this.fixed.weighted === true,
1099
+ expectedNodes: this.fixed.expectedNodes,
1100
+ expectedEdges: this.fixed.expectedEdges,
1101
+ resizable: undefined,
1102
+ });
1103
+ }
1104
+ // ---------------------------------------------------------------- private: nodes
1105
+ /**
1106
+ * The index a validated id would get from addValidatedNode: its existing (live or tombstoned)
1107
+ * index, or the next one.
1108
+ * @param id - a validated id
1109
+ * @returns the prospective index
1110
+ */
1111
+ prospectiveNodeIndex(id) {
1112
+ const { staging } = this;
1113
+ if (staging.idToIndex === null) {
1114
+ return typeof id === "number" && Number.isInteger(id) && id >= 0 && id < staging.nodeBound
1115
+ ? id
1116
+ : staging.nodeBound;
1117
+ }
1118
+ return staging.idToIndex.get(typeof id === "number" && id === 0 ? 0 : id) ?? staging.nodeBound;
1119
+ }
1120
+ /**
1121
+ * The live index of an id, total.
1122
+ * @param id - any value
1123
+ * @returns the index, or INVALID_INDEX when absent, illegal or tombstoned
1124
+ */
1125
+ lookup(id) {
1126
+ const { staging } = this;
1127
+ if (staging.idToIndex === null) {
1128
+ if (typeof id !== "number" || !Number.isInteger(id) || id < 0 || id >= staging.nodeBound) {
1129
+ return INVALID_INDEX;
1130
+ }
1131
+ return staging.nodeAlive.get(id) ? id : INVALID_INDEX;
1132
+ }
1133
+ const index = staging.idToIndex.get(typeof id === "number" && id === 0 ? 0 : id);
1134
+ if (index === undefined || !staging.nodeAlive.get(index)) {
1135
+ return INVALID_INDEX;
1136
+ }
1137
+ return index;
1138
+ }
1139
+ isLiveNode(index) {
1140
+ const { staging } = this;
1141
+ return Number.isInteger(index) && index >= 0 && index < staging.nodeBound && staging.nodeAlive.get(index);
1142
+ }
1143
+ isLiveEdge(edge) {
1144
+ const { staging } = this;
1145
+ return Number.isInteger(edge) && edge >= 0 && edge < staging.edgeBound && staging.edgeAlive.get(edge);
1146
+ }
1147
+ checkNodeIndex(index) {
1148
+ if (!Number.isInteger(index) || index < 0 || index >= this.staging.nodeBound) {
1149
+ throw new GraphFormatError("E_INDEX_RANGE", `node index ${index} is out of range`, {
1150
+ index,
1151
+ bound: this.staging.nodeBound,
1152
+ });
1153
+ }
1154
+ }
1155
+ checkEdgeIndex(edge) {
1156
+ if (!Number.isInteger(edge) || edge < 0 || edge >= this.staging.edgeBound) {
1157
+ throw new GraphFormatError("E_INDEX_RANGE", `edge index ${edge} is out of range`, {
1158
+ edge,
1159
+ bound: this.staging.edgeBound,
1160
+ });
1161
+ }
1162
+ }
1163
+ checkLiveNode(index) {
1164
+ if (!this.isLiveNode(index)) {
1165
+ throw new GraphFormatError("E_UNKNOWN_NODE", `node index ${index} is out of range or removed`, { index });
1166
+ }
1167
+ }
1168
+ checkEdge(edge) {
1169
+ if (!this.isLiveEdge(edge)) {
1170
+ throw new GraphFormatError("E_INDEX_RANGE", `edge index ${edge} is out of range or removed`, {
1171
+ edge,
1172
+ bound: this.staging.edgeBound,
1173
+ });
1174
+ }
1175
+ }
1176
+ checkNodeLimit(adding) {
1177
+ if (this.staging.nodeBound + adding > MAX_COUNT) {
1178
+ throw new GraphFormatError("E_TOO_LARGE", `node count ${this.staging.nodeBound + adding} exceeds MAX_COUNT`, {
1179
+ count: this.staging.nodeBound + adding,
1180
+ max: MAX_COUNT,
1181
+ });
1182
+ }
1183
+ }
1184
+ /**
1185
+ * The E_TOO_LARGE check of invariant I3 at the add* call that would cross the limit: the edge
1186
+ * index space and the live arc count (doubled for an undirected graph, loops once) must both
1187
+ * stay at or below MAX_COUNT.
1188
+ * @param addingEdges - edges about to be added
1189
+ * @param addingArcs - arcs they contribute (1 per directed edge or loop, 2 per undirected non-loop)
1190
+ */
1191
+ checkEdgeLimit(addingEdges, addingArcs) {
1192
+ const { staging } = this;
1193
+ const edges = staging.edgeBound + addingEdges;
1194
+ const liveArcs = this.directedValue ? staging.liveEdgeCount : 2 * staging.liveEdgeCount - staging.selfLoopCount;
1195
+ const arcs = liveArcs + addingArcs;
1196
+ if (edges > MAX_COUNT || arcs > MAX_COUNT) {
1197
+ throw new GraphFormatError("E_TOO_LARGE", `edge count ${edges} (${arcs} arcs) exceeds MAX_COUNT`, {
1198
+ count: edges,
1199
+ arcs,
1200
+ max: MAX_COUNT,
1201
+ });
1202
+ }
1203
+ }
1204
+ /**
1205
+ * The arcs a batch of edges contributes.
1206
+ * @param src - sources
1207
+ * @param dst - targets
1208
+ * @param count - how many
1209
+ * @returns the arc count
1210
+ */
1211
+ arcsOf(src, dst, count) {
1212
+ if (this.directedValue) {
1213
+ return count;
1214
+ }
1215
+ let arcs = 0;
1216
+ for (let i = 0; i < count; i++) {
1217
+ arcs += src[i] === dst[i] ? 1 : 2;
1218
+ }
1219
+ return arcs;
1220
+ }
1221
+ /**
1222
+ * Add or revive a validated id.
1223
+ * @param id - a validated id
1224
+ * @returns the index
1225
+ */
1226
+ addValidatedNode(id) {
1227
+ const { staging } = this;
1228
+ staging.materialiseIds();
1229
+ const map = staging.idToIndex;
1230
+ const existing = map.get(id);
1231
+ if (existing !== undefined) {
1232
+ if (!staging.nodeAlive.get(existing)) {
1233
+ staging.nodeAlive.set(existing, true);
1234
+ staging.liveNodeCount++;
1235
+ this.mutated();
1236
+ }
1237
+ return existing;
1238
+ }
1239
+ this.checkNodeLimit(1);
1240
+ const index = staging.pushNode(typeof id === "string" ? detachString(id) : id);
1241
+ this.mutated();
1242
+ return index;
1243
+ }
1244
+ /**
1245
+ * Resolve an edge endpoint id: the live index, a revived or new node under addMissingNodes, or
1246
+ * E_UNKNOWN_NODE.
1247
+ * @param id - a validated id
1248
+ * @returns the index
1249
+ */
1250
+ resolveEndpoint(id) {
1251
+ const index = this.lookup(id);
1252
+ if (index !== INVALID_INDEX) {
1253
+ return index;
1254
+ }
1255
+ if (this.fixed.addMissingNodes) {
1256
+ return this.addValidatedNode(id);
1257
+ }
1258
+ throw new GraphFormatError("E_UNKNOWN_NODE", `unknown node id ${describeId(id)}`, { id });
1259
+ }
1260
+ requireLive(id) {
1261
+ if (this.lookup(id) === INVALID_INDEX) {
1262
+ throw new GraphFormatError("E_UNKNOWN_NODE", `unknown node id ${describeId(id)}`, { id });
1263
+ }
1264
+ }
1265
+ removeLiveNode(index) {
1266
+ const { staging } = this;
1267
+ const removed = [];
1268
+ for (let e = staging.firstOut.get(index); e !== INVALID_INDEX; e = staging.nextOut.get(e)) {
1269
+ if (staging.edgeAlive.get(e)) {
1270
+ staging.killEdge(e);
1271
+ removed.push(e);
1272
+ }
1273
+ }
1274
+ for (let e = staging.firstIn.get(index); e !== INVALID_INDEX; e = staging.nextIn.get(e)) {
1275
+ if (staging.edgeAlive.get(e)) {
1276
+ staging.killEdge(e);
1277
+ removed.push(e);
1278
+ }
1279
+ }
1280
+ staging.nodeAlive.set(index, false);
1281
+ staging.liveNodeCount--;
1282
+ this.mutated();
1283
+ return Uint32Array.from(removed).sort();
1284
+ }
1285
+ // ---------------------------------------------------------------- private: edges
1286
+ /**
1287
+ * Validate a weight argument: NaN is E_INVALID_WEIGHT; on an unweighted (`weighted: false`) builder
1288
+ * only 1 is accepted and it is treated as omitted.
1289
+ * @param weight - the argument
1290
+ * @param edge - the bulk position, for the error details
1291
+ * @returns the weight to store, or undefined when omitted
1292
+ */
1293
+ checkWeight(weight, edge) {
1294
+ if (weight === undefined) {
1295
+ return undefined;
1296
+ }
1297
+ if (typeof weight !== "number" || Number.isNaN(weight)) {
1298
+ throw new GraphFormatError("E_INVALID_WEIGHT", `weight ${String(weight)} is not a number`, {
1299
+ weight,
1300
+ ...(edge === undefined ? {} : { edge }),
1301
+ });
1302
+ }
1303
+ if (this.fixed.weighted === false) {
1304
+ if (weight !== 1) {
1305
+ throw new GraphFormatError("E_INVALID_WEIGHT", `weight ${weight} on a builder declared unweighted`, {
1306
+ weight,
1307
+ reason: "unweighted builder",
1308
+ ...(edge === undefined ? {} : { edge }),
1309
+ });
1310
+ }
1311
+ return undefined;
1312
+ }
1313
+ return weight;
1314
+ }
1315
+ pushEdge(u, v, weight) {
1316
+ this.checkEdgeLimit(1, this.directedValue || u === v ? 1 : 2);
1317
+ const e = this.staging.pushEdge(u, v, weight);
1318
+ this.mutated();
1319
+ return e;
1320
+ }
1321
+ /**
1322
+ * The in-place expansion of design section 6.6: every live edge u -> v gets a mirror v -> u at a
1323
+ * new index (loops are not mirrored), graphty.directed is 0 for both halves and graphty.pair links
1324
+ * them; every other column's mirror rows stay unset.
1325
+ */
1326
+ expandToDirected() {
1327
+ const { staging } = this;
1328
+ const live = staging.liveEdgeCount;
1329
+ this.checkEdgeLimit(live, live);
1330
+ const directedDecl = { name: DIRECTED_COLUMN, dtype: "bool", role: "directed" };
1331
+ const pairDecl = { name: PAIR_COLUMN, dtype: "u32", role: "pair", refersTo: "edge" };
1332
+ // both declarations are checked before either is applied (design section 11.1)
1333
+ this.checkDeclaration("edge", directedDecl);
1334
+ this.checkDeclaration("edge", pairDecl);
1335
+ const directedHandle = this.declare("edge", directedDecl);
1336
+ const pairHandle = this.declare("edge", pairDecl);
1337
+ const directedColumn = staging.edgeColumns[directedHandle];
1338
+ const pairColumn = staging.edgeColumns[pairHandle];
1339
+ const bound = staging.edgeBound;
1340
+ for (let e = 0; e < bound; e++) {
1341
+ if (!staging.edgeAlive.get(e)) {
1342
+ continue;
1343
+ }
1344
+ directedColumn.write(e, false);
1345
+ const u = staging.src.get(e);
1346
+ const v = staging.dst.get(e);
1347
+ if (u === v) {
1348
+ continue;
1349
+ }
1350
+ const explicit = staging.weightExplicit(e);
1351
+ const weight = explicit && staging.weight !== null ? staging.weight.get(e) : undefined;
1352
+ const mirror = staging.pushEdge(v, u, weight);
1353
+ directedColumn.write(mirror, false);
1354
+ pairColumn.write(e, mirror);
1355
+ pairColumn.write(mirror, e);
1356
+ }
1357
+ this.directedValue = true;
1358
+ this.optionsCache = null;
1359
+ this.mutated();
1360
+ }
1361
+ // ---------------------------------------------------------------- private: columns
1362
+ columnsOf(domain) {
1363
+ return domain === "node" ? this.staging.nodeColumns : this.staging.edgeColumns;
1364
+ }
1365
+ handleOf(columns, name) {
1366
+ const index = this.columnIndex(columns, name);
1367
+ return (index < 0 ? INVALID_INDEX : index);
1368
+ }
1369
+ /**
1370
+ * The index of a staging column by name in O(1): a Map beside each column array, rebuilt when
1371
+ * the array is replaced (a fresh staging, a compaction) and kept in step by pushColumn().
1372
+ * Column names never change, so the map is invalidated only by identity or length.
1373
+ * @param columns - the node or edge staging columns
1374
+ * @param name - the column name
1375
+ * @returns the index, or -1 when absent
1376
+ */
1377
+ columnIndex(columns, name) {
1378
+ const cached = columns === this.staging.nodeColumns ? this.nodeNames : this.edgeNames;
1379
+ let index = cached;
1380
+ if (index === null || index.columns !== columns || index.map.size !== columns.length) {
1381
+ const map = new Map();
1382
+ for (let i = 0; i < columns.length; i++) {
1383
+ map.set(columns[i].meta.name, i);
1384
+ }
1385
+ index = { columns, map };
1386
+ if (columns === this.staging.nodeColumns) {
1387
+ this.nodeNames = index;
1388
+ }
1389
+ else if (columns === this.staging.edgeColumns) {
1390
+ this.edgeNames = index;
1391
+ }
1392
+ }
1393
+ return index.map.get(name) ?? -1;
1394
+ }
1395
+ /**
1396
+ * Append a staging column and keep the name index in step.
1397
+ * @param columns - the node or edge staging columns
1398
+ * @param column - the column to append
1399
+ * @returns the new column's index
1400
+ */
1401
+ pushColumn(columns, column) {
1402
+ columns.push(column);
1403
+ const index = columns === this.staging.nodeColumns ? this.nodeNames : this.edgeNames;
1404
+ if (index !== null && index.columns === columns && index.map.size === columns.length - 1) {
1405
+ index.map.set(column.meta.name, columns.length - 1);
1406
+ }
1407
+ return columns.length - 1;
1408
+ }
1409
+ /**
1410
+ * Declare a column (design section 11.3): the same shape returns the existing handle, another
1411
+ * shape is E_COLUMN_EXISTS, a role held by another column E_DUPLICATE_ROLE.
1412
+ * @param domain - node or edge
1413
+ * @param decl - the declaration
1414
+ * @returns the handle
1415
+ */
1416
+ declare(domain, decl) {
1417
+ const { meta, existing } = this.checkDeclaration(domain, decl);
1418
+ if (existing !== INVALID_INDEX) {
1419
+ return existing;
1420
+ }
1421
+ const columns = this.columnsOf(domain);
1422
+ return this.pushColumn(columns, new StagingColumn(meta, false));
1423
+ }
1424
+ /**
1425
+ * The checks of declare() without applying anything: the resolved metadata and, when a column
1426
+ * of the same shape exists, its handle.
1427
+ * @param domain - node or edge
1428
+ * @param decl - the declaration
1429
+ * @returns the metadata and the existing handle (INVALID_INDEX when the column is new)
1430
+ */
1431
+ checkDeclaration(domain, decl) {
1432
+ if (typeof decl.name !== "string" || decl.name.length === 0) {
1433
+ throw new GraphFormatError("E_COLUMN_TYPE", "a column declaration needs a non-empty name", {
1434
+ field: "name",
1435
+ found: decl.name,
1436
+ });
1437
+ }
1438
+ const meta = resolveColumnMeta(decl.name, domain, decl);
1439
+ const columns = this.columnsOf(domain);
1440
+ const existing = this.columnIndex(columns, meta.name);
1441
+ if (existing >= 0) {
1442
+ if (sameShape(columns[existing].meta, meta)) {
1443
+ return { meta, existing };
1444
+ }
1445
+ throw new GraphFormatError("E_COLUMN_EXISTS", `${domain} column "${meta.name}" is already declared as ${columns[existing].meta.dtype}`, { column: meta.name, domain, found: columns[existing].meta.dtype, expected: meta.dtype });
1446
+ }
1447
+ checkRole(columns, meta, domain);
1448
+ return { meta, existing: INVALID_INDEX };
1449
+ }
1450
+ /**
1451
+ * The staging column a handle or name refers to, auto-declaring an inferred column for an unknown
1452
+ * name when the value is set.
1453
+ * @param domain - node or edge
1454
+ * @param column - the handle or name
1455
+ * @param value - the value about to be written (decides the inferred dtype)
1456
+ * @returns the column, or null when an unknown name receives an unset value
1457
+ */
1458
+ resolveColumn(domain, column, value) {
1459
+ const columns = this.columnsOf(domain);
1460
+ if (typeof column === "number") {
1461
+ if (!Number.isInteger(column) || column < 0 || column >= columns.length) {
1462
+ throw new GraphFormatError("E_UNKNOWN_COLUMN", `no ${domain} column with handle ${column}`, {
1463
+ domain,
1464
+ handle: column,
1465
+ });
1466
+ }
1467
+ return columns[column];
1468
+ }
1469
+ const index = this.columnIndex(columns, column);
1470
+ if (index >= 0) {
1471
+ return columns[index];
1472
+ }
1473
+ if (value === undefined || value === null) {
1474
+ return null;
1475
+ }
1476
+ const dtype = inferInitialDtype(value);
1477
+ const created = createStagingColumn(column, domain, { dtype, nullable: true }, true);
1478
+ this.pushColumn(columns, created);
1479
+ return created;
1480
+ }
1481
+ /**
1482
+ * Validate every value of a record against the columns it would be written to, without writing
1483
+ * (design section 11.1: a record whose value is refused is not applied at all). An existing
1484
+ * column checks the value as its `write` would; a new name must be inferrable (a JSON value or a
1485
+ * well-formed string) unless the value is unset.
1486
+ * @param domain - node or edge
1487
+ * @param row - the row the record is meant for (error details only)
1488
+ * @param keys - the record keys
1489
+ * @param attrs - the record
1490
+ */
1491
+ checkCells(domain, row, keys, attrs) {
1492
+ const columns = this.columnsOf(domain);
1493
+ for (const key of keys) {
1494
+ const value = attrs[key];
1495
+ const at = this.columnIndex(columns, key);
1496
+ const column = at < 0 ? undefined : columns[at];
1497
+ if (column !== undefined) {
1498
+ column.checkValue(row, this.checkReference(column.meta, value));
1499
+ }
1500
+ else if (value !== undefined && value !== null) {
1501
+ inferInitialDtype(value);
1502
+ if (typeof value === "string") {
1503
+ assertWellFormedString(value, { column: key, row });
1504
+ }
1505
+ else if (typeof value === "object") {
1506
+ assertJsonValue(value, `${key}[${row}]`);
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ /**
1512
+ * The refersTo range rule of invariant I12 at the write: an index-valued cell of a refersTo
1513
+ * column must be below the referenced space's current bound (a tombstoned target is a dangling
1514
+ * reference the next compaction resolves) or INVALID_INDEX; E_INDEX_RANGE otherwise. A scalar
1515
+ * INVALID_INDEX means "no reference" and is written as an unset row (E_COLUMN_TYPE on a
1516
+ * non-nullable column, which cannot hold it). Values of other shapes are left to the column's
1517
+ * own type check.
1518
+ * @param meta - the column
1519
+ * @param value - the value about to be written
1520
+ * @returns the value to write (undefined for a scalar INVALID_INDEX)
1521
+ */
1522
+ checkReference(meta, value) {
1523
+ if (meta.refersTo === null || value === undefined || value === null) {
1524
+ return value;
1525
+ }
1526
+ const bound = meta.refersTo === "node" ? this.staging.nodeBound : this.staging.edgeBound;
1527
+ const check = (index) => {
1528
+ if (typeof index === "number" && index !== INVALID_INDEX && !(index >= 0 && index < bound)) {
1529
+ throw new GraphFormatError("E_INDEX_RANGE", `column "${meta.name}" refers to ${String(meta.refersTo)} ${index}, which is not below ${bound}`, { column: meta.name, refersTo: meta.refersTo, found: index, bound });
1530
+ }
1531
+ };
1532
+ if (Array.isArray(value)) {
1533
+ for (const item of value) {
1534
+ check(item);
1535
+ }
1536
+ return value;
1537
+ }
1538
+ check(value);
1539
+ return value === INVALID_INDEX ? undefined : value;
1540
+ }
1541
+ /**
1542
+ * The checks copyColumn would fail on before it writes anything: a new column's role must be
1543
+ * free (E_DUPLICATE_ROLE).
1544
+ * @param domain - node or edge
1545
+ * @param column - the incoming column
1546
+ */
1547
+ checkIncomingColumn(domain, column) {
1548
+ const columns = this.columnsOf(domain);
1549
+ if (this.columnIndex(columns, column.meta.name) < 0) {
1550
+ checkRole(columns, column.meta, domain);
1551
+ }
1552
+ }
1553
+ /**
1554
+ * The checks appendExtension would fail on before it writes anything: an existing table of the
1555
+ * same name must hold every incoming column (E_UNKNOWN_COLUMN); a new table's declarations must
1556
+ * resolve (E_COLUMN_EXISTS / E_DUPLICATE_ROLE).
1557
+ * @param name - the table name
1558
+ * @param table - the incoming table
1559
+ */
1560
+ checkIncomingExtension(name, table) {
1561
+ const target = this.staging.extensions.find((t) => t.name === name);
1562
+ const columns = [...table];
1563
+ if (target === undefined) {
1564
+ const seen = [];
1565
+ for (const column of columns) {
1566
+ const decl = { ...metaToDecl(column.meta), name: column.meta.name };
1567
+ const created = createStagingColumn(decl.name, "extension", decl, false);
1568
+ if (seen.some((other) => other.meta.name === created.meta.name)) {
1569
+ throw new GraphFormatError("E_COLUMN_EXISTS", `column "${decl.name}" is declared twice in "${name}"`, {
1570
+ table: name,
1571
+ column: decl.name,
1572
+ });
1573
+ }
1574
+ checkRole(seen, created.meta, name);
1575
+ seen.push(created);
1576
+ }
1577
+ return;
1578
+ }
1579
+ for (const column of columns) {
1580
+ if (!target.columns.some((c) => c.meta.name === column.meta.name)) {
1581
+ throw new GraphFormatError("E_UNKNOWN_COLUMN", `extension table "${name}" has no column "${column.meta.name}"`, { table: name, column: column.meta.name });
1582
+ }
1583
+ }
1584
+ }
1585
+ /**
1586
+ * The widening of widenNodeColumn / widenEdgeColumn.
1587
+ * @param domain - node or edge
1588
+ * @param column - the handle or name
1589
+ * @param dtype - the dtype to widen to
1590
+ */
1591
+ widenColumn(domain, column, dtype) {
1592
+ const columns = this.columnsOf(domain);
1593
+ let target;
1594
+ if (typeof column === "number") {
1595
+ target = Number.isInteger(column) && column >= 0 && column < columns.length ? columns[column] : undefined;
1596
+ }
1597
+ else {
1598
+ const index = this.columnIndex(columns, column);
1599
+ target = index < 0 ? undefined : columns[index];
1600
+ }
1601
+ if (target === undefined) {
1602
+ throw new GraphFormatError("E_UNKNOWN_COLUMN", `no ${domain} column ${String(column)}`, { domain, column });
1603
+ }
1604
+ const current = target.inferredDtype;
1605
+ if (current === null) {
1606
+ throw new GraphFormatError("E_COLUMN_TYPE", `${domain} column "${target.meta.name}" is declared, not inferred`, {
1607
+ column: target.meta.name,
1608
+ domain,
1609
+ });
1610
+ }
1611
+ if (wideningRank(dtype) < 0) {
1612
+ throw new GraphFormatError("E_COLUMN_TYPE", `${dtype} is not a dtype inference widens to`, {
1613
+ column: target.meta.name,
1614
+ dtype,
1615
+ });
1616
+ }
1617
+ const next = dtype;
1618
+ if (wideningRank(next) < wideningRank(current)) {
1619
+ throw new GraphFormatError("E_COLUMN_TYPE", `${domain} column "${target.meta.name}" is ${current}; it cannot narrow to ${next}`, { column: target.meta.name, found: current, expected: next });
1620
+ }
1621
+ if (next === current) {
1622
+ return;
1623
+ }
1624
+ target.widenTo(next);
1625
+ this.widenings.push({ column: target.meta.name, domain, from: current, to: next });
1626
+ this.mutated();
1627
+ }
1628
+ writeCell(domain, column, row, value) {
1629
+ const target = this.resolveColumn(domain, column, value);
1630
+ if (target === null) {
1631
+ return;
1632
+ }
1633
+ const widened = target.write(row, this.checkReference(target.meta, value));
1634
+ if (widened !== null) {
1635
+ this.widenings.push({ column: target.meta.name, domain, from: widened.from, to: widened.to });
1636
+ }
1637
+ }
1638
+ /**
1639
+ * Replace or create a column from a typed array (setNodeColumn / setEdgeColumn).
1640
+ * @param domain - node or edge
1641
+ * @param rows - the table's current bound
1642
+ * @param name - the column name
1643
+ * @param data - the values
1644
+ * @param decl - the declaration patch
1645
+ */
1646
+ setBulkColumn(domain, rows, name, data, decl) {
1647
+ const column = stagingColumnFromTypedArray(domain, rows, name, data, decl, {
1648
+ node: this.staging.nodeBound,
1649
+ edge: this.staging.edgeBound,
1650
+ });
1651
+ const columns = this.columnsOf(domain);
1652
+ const existing = this.columnIndex(columns, name);
1653
+ checkRole(columns.filter((c) => c.meta.name !== name), column.meta, domain);
1654
+ if (existing >= 0) {
1655
+ columns[existing] = column;
1656
+ }
1657
+ else {
1658
+ this.pushColumn(columns, column);
1659
+ }
1660
+ }
1661
+ extensionOf(handle) {
1662
+ const { extensions } = this.staging;
1663
+ if (!Number.isInteger(handle) || handle < 0 || handle >= extensions.length) {
1664
+ throw new GraphFormatError("E_INDEX_RANGE", `no extension table with handle ${handle}`, { handle });
1665
+ }
1666
+ return extensions[handle];
1667
+ }
1668
+ /**
1669
+ * Copy a snapshot column into the builder (addGraph): declared under the same metadata when the
1670
+ * name is new, else widened to the union dtype (design section 6.6); every set row is written
1671
+ * through `rowMap`, with refersTo values rewritten through `refs`.
1672
+ * @param domain - node or edge
1673
+ * @param column - the source column
1674
+ * @param rowMap - source row -> builder row
1675
+ * @param refs - source node / edge index -> builder index
1676
+ */
1677
+ copyColumn(domain, column, rowMap, refs) {
1678
+ const columns = this.columnsOf(domain);
1679
+ const { name } = column.meta;
1680
+ let index = this.columnIndex(columns, name);
1681
+ if (index < 0) {
1682
+ const decl = { ...metaToDecl(column.meta), name };
1683
+ checkRole(columns, column.meta, domain);
1684
+ index = this.pushColumn(columns, createStagingColumn(name, domain, decl, false));
1685
+ }
1686
+ else {
1687
+ const target = columns[index];
1688
+ if (!sameShape(target.meta, column.meta)) {
1689
+ const union = wider(inferredEquivalent(target.meta.dtype), inferredEquivalent(column.meta.dtype));
1690
+ if (target.meta.dtype !== union) {
1691
+ const from = target.meta.dtype;
1692
+ target.widenTo(union);
1693
+ this.widenings.push({ column: name, domain, from, to: union });
1694
+ }
1695
+ }
1696
+ }
1697
+ const target = columns[index];
1698
+ const valueMap = referenceMap(column.meta, refs);
1699
+ for (let row = 0; row < column.length; row++) {
1700
+ if (!column.isSet(row)) {
1701
+ continue;
1702
+ }
1703
+ let value = column.value(row);
1704
+ if (valueMap !== null) {
1705
+ value = remapReference(value, valueMap);
1706
+ if (value === undefined) {
1707
+ continue;
1708
+ }
1709
+ }
1710
+ target.write(rowMap[row], value);
1711
+ }
1712
+ }
1713
+ /**
1714
+ * Append the rows of a snapshot extension table (addGraph): into the table of the same name when
1715
+ * the columns match, else a new table is declared from the source columns.
1716
+ * @param name - the table name
1717
+ * @param table - the source table
1718
+ * @param refs - source node / edge index -> builder index
1719
+ */
1720
+ appendExtension(name, table, refs) {
1721
+ const { staging } = this;
1722
+ const sourceColumns = [...table];
1723
+ let target = staging.extensions.find((t) => t.name === name);
1724
+ if (target === undefined) {
1725
+ const decls = sourceColumns.map((column) => ({ ...metaToDecl(column.meta), name: column.meta.name }));
1726
+ const handle = this.addExtensionTable(name, decls);
1727
+ target = staging.extensions[handle];
1728
+ }
1729
+ const targetColumns = target.columns;
1730
+ const first = target.rowCount;
1731
+ for (const column of sourceColumns) {
1732
+ const into = targetColumns.find((c) => c.meta.name === column.meta.name);
1733
+ if (into === undefined) {
1734
+ throw new GraphFormatError("E_UNKNOWN_COLUMN", `extension table "${name}" has no column "${column.meta.name}"`, { table: name, column: column.meta.name });
1735
+ }
1736
+ const valueMap = referenceMap(column.meta, refs);
1737
+ for (let row = 0; row < column.length; row++) {
1738
+ if (!column.isSet(row)) {
1739
+ into.ensureLength(first + row + 1);
1740
+ continue;
1741
+ }
1742
+ let value = column.value(row);
1743
+ if (valueMap !== null) {
1744
+ value = remapReference(value, valueMap);
1745
+ if (value === undefined) {
1746
+ into.ensureLength(first + row + 1);
1747
+ continue;
1748
+ }
1749
+ }
1750
+ into.write(first + row, value);
1751
+ }
1752
+ }
1753
+ for (const column of targetColumns) {
1754
+ column.ensureLength(first + table.rowCount);
1755
+ }
1756
+ target.rowCount = first + table.rowCount;
1757
+ }
1758
+ }
1759
+ // ============================================================ module helpers
1760
+ /**
1761
+ * Enforce "at most one column per role" among staging columns (design section 5.5).
1762
+ * @param columns - the existing columns
1763
+ * @param meta - the column being added
1764
+ * @param where - the table, for the message
1765
+ */
1766
+ function checkRole(columns, meta, where) {
1767
+ if (meta.role === null) {
1768
+ return;
1769
+ }
1770
+ const holder = columns.find((column) => column.meta.name !== meta.name && column.meta.role === meta.role);
1771
+ if (holder !== undefined) {
1772
+ throw new GraphFormatError("E_DUPLICATE_ROLE", `role "${meta.role}" is already held by column "${holder.meta.name}" in ${where}`, { role: meta.role, column: meta.name, holder: holder.meta.name });
1773
+ }
1774
+ }
1775
+ /**
1776
+ * The wider of two inferred dtypes in the order of design section 5.1.
1777
+ * @param a - one dtype
1778
+ * @param b - the other
1779
+ * @returns the wider
1780
+ */
1781
+ function wider(a, b) {
1782
+ return wideningRank(a) >= wideningRank(b) ? a : b;
1783
+ }
1784
+ /**
1785
+ * The index map a refersTo column's values are rewritten through, if any.
1786
+ * @param meta - the column
1787
+ * @param refs - the node and edge maps
1788
+ * @returns the map of the referenced space, or null
1789
+ */
1790
+ function referenceMap(meta, refs) {
1791
+ switch (meta.refersTo) {
1792
+ case "node":
1793
+ return refs.node;
1794
+ case "edge":
1795
+ return refs.edge;
1796
+ default:
1797
+ return null;
1798
+ }
1799
+ }
1800
+ /**
1801
+ * Rewrite an index-valued cell (a number, or an array of numbers for a list) through an index map;
1802
+ * dangling references are dropped, and a cell with nothing left is unset.
1803
+ * @param value - the cell
1804
+ * @param map - source index -> builder index
1805
+ * @returns the rewritten cell, or undefined to unset
1806
+ */
1807
+ function remapReference(value, map) {
1808
+ if (typeof value === "number") {
1809
+ return value < map.length ? map[value] : undefined;
1810
+ }
1811
+ if (typeof value === "object" && value !== null) {
1812
+ const items = Array.from(value);
1813
+ const kept = items.filter((item) => item < map.length).map((item) => map[item]);
1814
+ return kept.length === 0 && items.length > 0 ? undefined : kept;
1815
+ }
1816
+ throw new GraphFormatError("E_COLUMN_TYPE", `a refersTo cell holds a ${typeof value}, not an index or a list`, {
1817
+ found: typeof value,
1818
+ reason: "refersTo",
1819
+ });
1820
+ }
1821
+ /**
1822
+ * Build a one-row graph column now to validate a setGraphValue call.
1823
+ * @param name - the column name
1824
+ * @param value - the value
1825
+ * @param decl - the declaration patch
1826
+ */
1827
+ function buildGraphColumn(name, value, decl) {
1828
+ const column = createStagingColumn(name, "graph", decl.dtype === undefined ? { ...decl, dtype: inferInitialDtype(value) } : decl, decl.dtype === undefined);
1829
+ column.write(0, value);
1830
+ }
1831
+ /**
1832
+ * A staging column from a typed array (setNodeColumn / setEdgeColumn, design section 12.2): the
1833
+ * dtype comes from the array class unless the patch names one (bool and dict over a Uint32Array), the
1834
+ * length must be rows * components (bool: ceil(rows / 32) words), and every row is set unless the
1835
+ * patch declares the column nullable.
1836
+ * @param domain - node or edge
1837
+ * @param rows - the row count
1838
+ * @param name - the column name
1839
+ * @param data - the values
1840
+ * @param decl - the declaration patch
1841
+ * @param bounds - the builder's node and edge bounds, for refersTo range checks (E_INDEX_RANGE)
1842
+ * @returns the staging column
1843
+ */
1844
+ function stagingColumnFromTypedArray(domain, rows, name, data, decl, bounds) {
1845
+ const arrayDtype = dtypeOfArray(data);
1846
+ const dtype = decl.dtype ?? arrayDtype;
1847
+ const compatible = dtype === arrayDtype || (arrayDtype === "u32" && (dtype === "bool" || dtype === "dict"));
1848
+ if (!compatible) {
1849
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": a ${arrayDtype} array cannot back a ${dtype} column`, {
1850
+ column: name,
1851
+ field: "dtype",
1852
+ found: arrayDtype,
1853
+ expected: dtype,
1854
+ });
1855
+ }
1856
+ const column = createStagingColumn(name, domain, { nullable: false, ...decl, dtype }, false);
1857
+ const { components } = column.meta;
1858
+ let expected;
1859
+ switch (dtype) {
1860
+ case "bool":
1861
+ expected = Math.ceil(rows / 32);
1862
+ break;
1863
+ case "dict":
1864
+ expected = rows;
1865
+ break;
1866
+ case "f32":
1867
+ case "f64":
1868
+ case "i32":
1869
+ case "u32":
1870
+ case "u8":
1871
+ expected = rows * components;
1872
+ break;
1873
+ default:
1874
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": a ${String(dtype)} column cannot adopt a typed array`, {
1875
+ column: name,
1876
+ field: "dtype",
1877
+ });
1878
+ }
1879
+ if (data.length !== expected) {
1880
+ throw new GraphFormatError("E_COLUMN_LENGTH", `column "${name}": array length ${data.length}, expected ${expected}`, {
1881
+ column: name,
1882
+ expected,
1883
+ found: data.length,
1884
+ });
1885
+ }
1886
+ column.ensureLength(rows);
1887
+ switch (dtype) {
1888
+ case "bool": {
1889
+ const words = data;
1890
+ for (let row = 0; row < rows; row++) {
1891
+ column.write(row, ((words[row >>> 5] >>> (row & 31)) & 1) === 1);
1892
+ }
1893
+ break;
1894
+ }
1895
+ case "dict": {
1896
+ const { dictionary } = column;
1897
+ if (dictionary === null) {
1898
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}" has no dictionary`, { column: name });
1899
+ }
1900
+ for (let row = 0; row < rows; row++) {
1901
+ const code = data[row];
1902
+ if (code >= dictionary.size) {
1903
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": code ${code} at row ${row} is outside the ${dictionary.size} declared options`, {
1904
+ column: name,
1905
+ row,
1906
+ code,
1907
+ });
1908
+ }
1909
+ column.write(row, dictionary.values[code]);
1910
+ }
1911
+ break;
1912
+ }
1913
+ default: {
1914
+ const { typed } = column;
1915
+ if (typed === null) {
1916
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}" has no typed storage`, { column: name });
1917
+ }
1918
+ const { refersTo } = column.meta;
1919
+ const bound = refersTo === "node" ? bounds.node : bounds.edge;
1920
+ for (let i = 0; i < data.length; i++) {
1921
+ const value = data[i];
1922
+ if (refersTo !== null && value !== INVALID_INDEX && value >= bound) {
1923
+ throw new GraphFormatError("E_INDEX_RANGE", `column "${name}" refers to ${refersTo} ${value} at row ${i}, which is not below ${bound}`, { column: name, row: i, refersTo, found: value, bound });
1924
+ }
1925
+ if (refersTo !== null && value === INVALID_INDEX && column.validity === null) {
1926
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": row ${i} of a non-nullable refersTo column holds INVALID_INDEX`, { column: name, row: i, field: "nullable" });
1927
+ }
1928
+ typed.set(i, value);
1929
+ }
1930
+ if (column.validity !== null) {
1931
+ for (let row = 0; row < rows; row++) {
1932
+ column.validity.set(row, refersTo === null || data[row] !== INVALID_INDEX);
1933
+ }
1934
+ }
1935
+ break;
1936
+ }
1937
+ }
1938
+ return column;
1939
+ }
1940
+ //# sourceMappingURL=graph-builder.js.map