@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,568 @@
1
+ /**
2
+ * AttributeTable: a set of index-aligned columns with a fixed row count (design section 5.7). The
3
+ * column SET is a mutable side table of the snapshot (set / remove / rename at any time, by the
4
+ * snapshot owner); the row count is fixed for the life of the table (invariant I17) and column
5
+ * contents are frozen unless the column was declared mutable (design section 5.8). "Absent" is
6
+ * always null on this surface; undefined only ever means "unset row" from value(). Iteration yields
7
+ * columns in declaration order. At most one column per role (E_DUPLICATE_ROLE).
8
+ *
9
+ * The exported helper functions (tableWithColumns, verifyUniqueColumns, declareColumn) are for the
10
+ * snapshot, builder and freeze modules; they are not part of the public surface.
11
+ */
12
+
13
+ import { GraphFormatError } from "../errors.js";
14
+ import {
15
+ type AttributeTableContract,
16
+ type Column,
17
+ type ColumnDeclPatch,
18
+ type ColumnDomain,
19
+ type ColumnInput,
20
+ type ColumnOf,
21
+ type ColumnRole,
22
+ type Dtype,
23
+ type F32,
24
+ type I32,
25
+ type SetOptions,
26
+ type TypedArrayData,
27
+ type U32,
28
+ } from "../types/index.js";
29
+ import { type TableParts } from "../types/internal.js";
30
+ import { assertOneOf } from "../util/options.js";
31
+ import { claimHolder, holderCount, isShared, noteShared } from "../util/shared-buffers.js";
32
+ import { bitmapGet } from "./bitmap.js";
33
+ import {
34
+ columnBuffers,
35
+ columnFromTypedArray,
36
+ columnFromValues,
37
+ gpuViewOf,
38
+ isPackageColumn,
39
+ metaToDecl,
40
+ resolveColumnMeta,
41
+ rewrapColumn,
42
+ } from "./column.js";
43
+
44
+ /**
45
+ * Whether a set() payload is a typed array (as opposed to a Column object or a JS array).
46
+ * @param data - the payload
47
+ * @returns true for one of the five typed-array classes
48
+ */
49
+ function isTypedArrayData(data: unknown): data is TypedArrayData {
50
+ return (
51
+ data instanceof Uint32Array ||
52
+ data instanceof Int32Array ||
53
+ data instanceof Float32Array ||
54
+ data instanceof Float64Array ||
55
+ data instanceof Uint8Array
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Whether a set() payload is a Column object (duck-typed on the members every column has).
61
+ * @param data - the payload
62
+ * @returns true for a Column
63
+ */
64
+ function isColumn(data: unknown): data is Column {
65
+ if (typeof data !== "object" || data === null || Array.isArray(data) || isTypedArrayData(data)) {
66
+ return false;
67
+ }
68
+ const candidate = data as { dtype?: unknown; meta?: unknown; length?: unknown };
69
+ return (
70
+ typeof candidate.dtype === "string" &&
71
+ typeof candidate.meta === "object" &&
72
+ typeof candidate.length === "number"
73
+ );
74
+ }
75
+
76
+ /**
77
+ * The number of column-set changes a table has seen (set / remove / rename), so a cached role or name
78
+ * lookup can be reused while the set is unchanged.
79
+ * @param table - the table
80
+ * @returns the change count
81
+ */
82
+ export function columnSetVersion(table: AttributeTable): number {
83
+ return (table as unknown as { mutations: number }).mutations;
84
+ }
85
+
86
+ /**
87
+ * Record a table as a holder of a column and of every buffer the column views (design section 9.1).
88
+ * @param column - the column being attached
89
+ */
90
+ function claimColumn(column: Column): void {
91
+ claimHolder(column);
92
+ for (const buffer of columnBuffers(column)) {
93
+ claimHolder(buffer);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * A set of columns with a fixed row count (design sections 5.7 and 12.2): `nodes` (nodeCount rows),
99
+ * `edges` (edgeCount rows), `graph` (1 row) and extension tables. The column set is a mutable side
100
+ * table; the row count is fixed; iteration is in declaration order.
101
+ */
102
+ export class AttributeTable implements AttributeTableContract {
103
+ /** The table the columns belong to. */
104
+ readonly domain: ColumnDomain;
105
+
106
+ /** Number of rows of every column; fixed for the life of the table. */
107
+ readonly rowCount: number;
108
+
109
+ private readonly columns: Map<string, Column>;
110
+
111
+ /** Bumped on every change of the column set (set / remove / rename), for callers that cache a lookup. */
112
+ private mutations = 0;
113
+
114
+ /**
115
+ * Create a table from wrapped columns (the constructor the freeze pipeline, fromCsr and the wire
116
+ * reader use). Every column must have the table's row count and domain, names must be unique and
117
+ * at most one column may hold each role.
118
+ * @param parts - the domain, the row count and the columns in declaration order
119
+ * @internal
120
+ */
121
+ constructor(parts: TableParts) {
122
+ const { domain, rowCount } = parts;
123
+ if (!(Number.isInteger(rowCount) && rowCount >= 0)) {
124
+ throw new GraphFormatError("E_COLUMN_LENGTH", `invalid row count ${rowCount} for a ${domain} table`, {
125
+ domain,
126
+ found: rowCount,
127
+ });
128
+ }
129
+ this.domain = domain;
130
+ this.rowCount = rowCount;
131
+ this.columns = new Map();
132
+ for (const column of parts.columns) {
133
+ const { name } = column.meta;
134
+ if (this.columns.has(name)) {
135
+ throw new GraphFormatError("E_COLUMN_EXISTS", `column "${name}" is declared twice`, { column: name });
136
+ }
137
+ this.checkColumn(name, column);
138
+ this.checkRole(name, column.meta.role, false);
139
+ this.columns.set(name, column);
140
+ claimColumn(column);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Column names in declaration order.
146
+ * @returns a fresh array of the names
147
+ */
148
+ names(): readonly string[] {
149
+ return [...this.columns.keys()];
150
+ }
151
+
152
+ /**
153
+ * Whether a column exists.
154
+ * @param name - the column name
155
+ * @returns true when present
156
+ */
157
+ has(name: string): boolean {
158
+ return this.columns.has(name);
159
+ }
160
+
161
+ /**
162
+ * Total lookup by name.
163
+ * @param name - the column name
164
+ * @returns the column, or null when absent
165
+ */
166
+ get(name: string): Column | null {
167
+ return this.columns.get(name) ?? null;
168
+ }
169
+
170
+ /**
171
+ * Checked lookup by name.
172
+ * @param name - the column name
173
+ * @returns the column; E_UNKNOWN_COLUMN when absent
174
+ */
175
+ require(name: string): Column {
176
+ const column = this.columns.get(name);
177
+ if (column === undefined) {
178
+ throw new GraphFormatError("E_UNKNOWN_COLUMN", `no ${this.domain} column named "${name}"`, {
179
+ column: name,
180
+ domain: this.domain,
181
+ });
182
+ }
183
+ return column;
184
+ }
185
+
186
+ /**
187
+ * Total lookup by name and dtype.
188
+ * @param name - the column name
189
+ * @param dtype - the expected dtype
190
+ * @returns the column, or null when absent or of another dtype
191
+ */
192
+ typed<D extends Dtype>(name: string, dtype: D): ColumnOf<D> | null {
193
+ const column = this.columns.get(name);
194
+ if (column === undefined || column.dtype !== dtype) {
195
+ return null;
196
+ }
197
+ return column as ColumnOf<D>;
198
+ }
199
+
200
+ /**
201
+ * Checked lookup by name and dtype.
202
+ * @param name - the column name
203
+ * @param dtype - the expected dtype
204
+ * @returns the column; E_UNKNOWN_COLUMN when absent, E_COLUMN_TYPE when of another dtype
205
+ */
206
+ requireTyped<D extends Dtype>(name: string, dtype: D): ColumnOf<D> {
207
+ const column = this.require(name);
208
+ if (column.dtype !== dtype) {
209
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}" is ${column.dtype}, expected ${dtype}`, {
210
+ column: name,
211
+ expected: dtype,
212
+ found: column.dtype,
213
+ });
214
+ }
215
+ return column as ColumnOf<D>;
216
+ }
217
+
218
+ /**
219
+ * The column holding a role (at most one per table).
220
+ * @param role - the role
221
+ * @returns the column, or null when no column has the role
222
+ */
223
+ byRole(role: ColumnRole): Column | null {
224
+ for (const column of this.columns.values()) {
225
+ if (column.meta.role === role) {
226
+ return column;
227
+ }
228
+ }
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * Typed read of one cell honouring the column's declared default.
234
+ * @param name - the column name; E_UNKNOWN_COLUMN when absent
235
+ * @param row - the row index; E_INDEX_RANGE when out of range
236
+ * @returns the value, the default for an unset row, or undefined for an unset row without a default
237
+ */
238
+ value(name: string, row: number): unknown {
239
+ return this.require(name).value(row);
240
+ }
241
+
242
+ /**
243
+ * Whether one cell holds a value.
244
+ * @param name - the column name; E_UNKNOWN_COLUMN when absent
245
+ * @param row - the row index (false when out of range)
246
+ * @returns true when set
247
+ */
248
+ isSet(name: string, row: number): boolean {
249
+ return this.require(name).isSet(row);
250
+ }
251
+
252
+ /**
253
+ * Attach a column (design section 5.7). A typed array is adopted by reference after its length is
254
+ * checked (E_COLUMN_LENGTH); a u8 array from which no padded u32 view is constructible is copied
255
+ * unless opts.adopt is "strict" (E_COLUMN_ALIGNMENT); a view over a SharedArrayBuffer or a
256
+ * resizable buffer is E_UNSUPPORTED (decision D-SAB, invariant I10); a JS array builds a column of
257
+ * the declared dtype, or of the dtype inferred from the values (design section 5.1) when the
258
+ * patch names none; a Column object is moved in when its row count matches. Replaces a column of
259
+ * the same name. E_DUPLICATE_ROLE when the declared role is held by another column and
260
+ * opts.replaceRole is not set; with replaceRole the previous holder is removed.
261
+ * @param name - the column name
262
+ * @param data - a Column, a typed array, or a JS array of values
263
+ * @param decl - declaration fields to apply (dtype is inferred from a typed array when omitted)
264
+ * @param opts - role replacement and u8 adoption options
265
+ * @returns the attached column (`column.data !== data` only when a u8 array was copied)
266
+ */
267
+ set(
268
+ name: string,
269
+ data: Column | TypedArrayData | readonly unknown[],
270
+ decl?: ColumnDeclPatch,
271
+ opts?: SetOptions,
272
+ ): Column {
273
+ const patch = decl ?? {};
274
+ let column: Column;
275
+ if (isTypedArrayData(data)) {
276
+ const adopt = assertOneOf("adopt", opts?.adopt, ["copy", "strict"] as const) ?? "copy";
277
+ column = columnFromTypedArray(this.domain, this.rowCount, name, data, patch, adopt);
278
+ } else if (Array.isArray(data)) {
279
+ column = columnFromValues(this.domain, this.rowCount, name, data, patch);
280
+ } else if (isColumn(data)) {
281
+ if (!isPackageColumn(data)) {
282
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": not a Column of this package`, {
283
+ column: name,
284
+ reason: "foreign column",
285
+ });
286
+ }
287
+ column = this.adoptColumn(name, data, decl);
288
+ if (column !== data && holderCount(data) > 0) {
289
+ // a re-wrapped column shares every buffer with `data`, which another table still holds
290
+ noteShared(column);
291
+ }
292
+ } else {
293
+ throw new GraphFormatError("E_COLUMN_TYPE", `column "${name}": unsupported data (${typeof data})`, {
294
+ column: name,
295
+ found: typeof data,
296
+ });
297
+ }
298
+ this.checkRole(name, column.meta.role, opts?.replaceRole ?? false);
299
+ const previous = this.columns.get(name);
300
+ if (previous !== column) {
301
+ // a Column object moved in from another table gains a second holder, and so does every
302
+ // buffer it views (a zero-copy slice of another table's column, design section 9.1);
303
+ // re-wrapping this table's own column under a patch adds no holder of its buffers
304
+ claimHolder(column);
305
+ if (previous !== data) {
306
+ for (const buffer of columnBuffers(column)) {
307
+ claimHolder(buffer);
308
+ }
309
+ }
310
+ }
311
+ // Map.set on an existing key keeps its position, so a replacement keeps the declaration order
312
+ this.columns.set(name, column);
313
+ this.mutations++;
314
+ return column;
315
+ }
316
+
317
+ /**
318
+ * Remove a column.
319
+ * @param name - the column name
320
+ * @returns true when a column was removed
321
+ */
322
+ remove(name: string): boolean {
323
+ const removed = this.columns.delete(name);
324
+ if (removed) {
325
+ this.mutations++;
326
+ }
327
+ return removed;
328
+ }
329
+
330
+ /**
331
+ * Rename a column in place (declaration order kept). The Column object is re-wrapped under the new
332
+ * name; its storage is shared with the previous object.
333
+ * @param from - the current name; E_UNKNOWN_COLUMN when absent
334
+ * @param to - the new name; E_COLUMN_EXISTS when another column has it
335
+ */
336
+ rename(from: string, to: string): void {
337
+ const column = this.require(from);
338
+ if (from === to) {
339
+ return;
340
+ }
341
+ if (this.columns.has(to)) {
342
+ throw new GraphFormatError("E_COLUMN_EXISTS", `cannot rename "${from}": a column named "${to}" exists`, {
343
+ column: to,
344
+ });
345
+ }
346
+ const renamed = rewrapColumn(column, resolveColumnMeta(to, this.domain, metaToDecl(column.meta)));
347
+ // the same table keeps holding the same buffers: only the new Column object is claimed
348
+ claimHolder(renamed);
349
+ if (isShared(column)) {
350
+ noteShared(renamed);
351
+ }
352
+ const entries = [...this.columns.entries()];
353
+ this.columns.clear();
354
+ for (const [name, existing] of entries) {
355
+ if (name === from) {
356
+ this.columns.set(to, renamed);
357
+ } else {
358
+ this.columns.set(name, existing);
359
+ }
360
+ }
361
+ this.mutations++;
362
+ }
363
+
364
+ /**
365
+ * The array a GPU binds for a column (design section 10.4): its own data for the direct dtypes,
366
+ * the padded u32 view for u8, the packed words for bool, the codes for dict, and a cached f32 copy
367
+ * for f64.
368
+ * @param name - the column name; E_UNKNOWN_COLUMN when absent
369
+ * @returns the bindable array; E_GPU_INELIGIBLE for string / list / json
370
+ */
371
+ gpuView(name: string): U32 | I32 | F32 {
372
+ return gpuViewOf(this.require(name));
373
+ }
374
+
375
+ /**
376
+ * A new table with the same columns: the Column objects are shared, the set is independent.
377
+ * @returns the cloned table
378
+ */
379
+ clone(): AttributeTable {
380
+ return new AttributeTable({
381
+ domain: this.domain,
382
+ rowCount: this.rowCount,
383
+ columns: [...this.columns.values()],
384
+ });
385
+ }
386
+
387
+ /**
388
+ * Iterate the columns in declaration order.
389
+ * @returns an iterator over the columns
390
+ */
391
+ [Symbol.iterator](): IterableIterator<Column> {
392
+ return this.columns.values();
393
+ }
394
+
395
+ /**
396
+ * Move a Column object in: checked for row count and re-wrapped when its name, domain or
397
+ * declaration differ from the target slot.
398
+ * @param name - the target name
399
+ * @param column - the column
400
+ * @param decl - an optional declaration patch
401
+ * @returns the column to store (the same object when nothing changes)
402
+ */
403
+ private adoptColumn(name: string, column: Column, decl: ColumnDeclPatch | undefined): Column {
404
+ this.checkColumn(name, column);
405
+ const { meta } = column;
406
+ const unchanged = decl === undefined && meta.name === name && meta.domain === this.domain;
407
+ if (unchanged) {
408
+ return column;
409
+ }
410
+ const merged: ColumnDeclPatch = { ...metaToDecl(meta), ...decl };
411
+ if (decl?.fill === undefined) {
412
+ merged.fill = meta.fill;
413
+ }
414
+ return rewrapColumn(column, resolveColumnMeta(name, this.domain, merged));
415
+ }
416
+
417
+ private checkColumn(name: string, column: Column): void {
418
+ if (column.length !== this.rowCount) {
419
+ throw new GraphFormatError(
420
+ "E_COLUMN_LENGTH",
421
+ `column "${name}" has ${column.length} rows; the ${this.domain} table has ${this.rowCount}`,
422
+ { column: name, expected: this.rowCount, found: column.length },
423
+ );
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Enforce "at most one column per role": E_DUPLICATE_ROLE when another column holds the role,
429
+ * unless replaceRole, in which case that column is removed.
430
+ * @param name - the column being attached (a column may keep its own role)
431
+ * @param role - the role being attached, or null
432
+ * @param replaceRole - whether to evict the previous holder
433
+ */
434
+ private checkRole(name: string, role: ColumnRole | null, replaceRole: boolean): void {
435
+ if (role === null) {
436
+ return;
437
+ }
438
+ for (const [otherName, other] of this.columns) {
439
+ if (otherName !== name && other.meta.role === role) {
440
+ if (!replaceRole) {
441
+ throw new GraphFormatError(
442
+ "E_DUPLICATE_ROLE",
443
+ `role "${role}" is already held by column "${otherName}" in the ${this.domain} table`,
444
+ { role, column: name, holder: otherName },
445
+ );
446
+ }
447
+ this.columns.delete(otherName);
448
+ return;
449
+ }
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Create an empty table.
456
+ * @param domain - the table's domain
457
+ * @param rowCount - the fixed row count
458
+ * @returns a table with no columns
459
+ */
460
+ export function createTable(domain: ColumnDomain, rowCount: number): AttributeTable {
461
+ return new AttributeTable({ domain, rowCount, columns: [] });
462
+ }
463
+
464
+ /**
465
+ * A cloned table plus the given columns (the column half of snapshot.withColumns(), design section
466
+ * 7.3): the Column objects of the source are shared, the set is new, and each entry of `columns` is
467
+ * attached through set() (a typed array, or a ColumnInput carrying data and a declaration patch).
468
+ * @param table - the source table
469
+ * @param columns - the columns to add or replace, keyed by name
470
+ * @returns the new table
471
+ */
472
+ export function tableWithColumns(
473
+ table: AttributeTable,
474
+ columns: Readonly<Record<string, TypedArrayData | ColumnInput>>,
475
+ ): AttributeTable {
476
+ const out = table.clone();
477
+ for (const name of Object.keys(columns)) {
478
+ const input = columns[name];
479
+ if (isTypedArrayData(input)) {
480
+ out.set(name, input);
481
+ } else {
482
+ out.set(name, input.data, input.decl);
483
+ }
484
+ }
485
+ return out;
486
+ }
487
+
488
+ /**
489
+ * The comparable key of one set cell for uniqueness checks: the number, string, boolean or
490
+ * dictionary value; json values are compared by their JSON text.
491
+ * @param column - the column
492
+ * @param row - a set row
493
+ * @returns the key
494
+ */
495
+ function uniqueKey(column: Column, row: number): unknown {
496
+ const { dtype } = column;
497
+ switch (dtype) {
498
+ case "f32":
499
+ case "f64":
500
+ case "i32":
501
+ case "u32":
502
+ case "u8":
503
+ if (column.meta.components === 1) {
504
+ return column.data[row];
505
+ }
506
+ return Array.from(
507
+ column.data.subarray(row * column.meta.components, (row + 1) * column.meta.components),
508
+ ).join(",");
509
+ case "bool":
510
+ return bitmapGet(column.data, row);
511
+ case "dict":
512
+ return column.codes[row];
513
+ case "string":
514
+ return column.valueAt(row);
515
+ case "list":
516
+ return JSON.stringify(column.sliceOf(row));
517
+ case "json":
518
+ return JSON.stringify(column.values[row]);
519
+ default: {
520
+ const name: string = dtype;
521
+ throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
522
+ }
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Enforce a unique column over its set rows (design section 5.5; the freeze step 10 hook):
528
+ * E_DUPLICATE_EDGE_ID for an edge column and E_DUPLICATE_ID for every other domain, with the
529
+ * duplicated value and both rows in details.
530
+ * @param column - a column declared unique
531
+ */
532
+ export function verifyUniqueColumn(column: Column): void {
533
+ if (!column.meta.unique) {
534
+ return;
535
+ }
536
+ const seen = new Map<unknown, number>();
537
+ for (let row = 0; row < column.length; row++) {
538
+ if (!column.isSet(row)) {
539
+ continue;
540
+ }
541
+ const key = uniqueKey(column, row);
542
+ const first = seen.get(key);
543
+ if (first !== undefined) {
544
+ const code = column.meta.domain === "edge" ? "E_DUPLICATE_EDGE_ID" : "E_DUPLICATE_ID";
545
+ throw new GraphFormatError(
546
+ code,
547
+ `column "${column.meta.name}" is unique but rows ${first} and ${row} hold the same value`,
548
+ {
549
+ column: column.meta.name,
550
+ domain: column.meta.domain,
551
+ rows: [first, row],
552
+ value: column.value(row),
553
+ },
554
+ );
555
+ }
556
+ seen.set(key, row);
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Enforce every unique column of a table (freeze step 10).
562
+ * @param table - the table to check
563
+ */
564
+ export function verifyUniqueColumns(table: AttributeTable): void {
565
+ for (const column of table) {
566
+ verifyUniqueColumn(column);
567
+ }
568
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Package-wide constants of @graphty/graph-format (design sections 2, 3.2, 9.2, 10.3 and 12.2).
3
+ *
4
+ * The four constants listed in design section 12.2 (INVALID_INDEX, MAX_COUNT, FORMAT_VERSION and
5
+ * SNAPSHOT_BRAND) are public and re-exported by src/index.ts. The remaining constants are shared by
6
+ * the builder, arena and wire modules but are not part of the public surface.
7
+ */
8
+
9
+ /**
10
+ * The one and only "no index" sentinel: 0xFFFFFFFF. Used for node, edge and arc indices alike, both as
11
+ * a JS return value (indexOf, findArc, edgeIndexOf, nodeColumn, edgeColumn, codeOf) and inside
12
+ * Uint32Array vectors (remaps, parents, labels). It never appears in rowPtr, colIdx, arcToEdge,
13
+ * edgeToArc or any view array (invariant I2), and because every count is bounded by MAX_COUNT it can
14
+ * never collide with a valid index (invariant I3).
15
+ */
16
+ export const INVALID_INDEX = 0xffffffff;
17
+
18
+ /**
19
+ * Upper bound on nodeCount, edgeCount and arcCount: 0xFFFFFFFE (invariant I3). Crossing it at an add*
20
+ * call throws E_TOO_LARGE. One below INVALID_INDEX so the sentinel is always free.
21
+ */
22
+ export const MAX_COUNT = 0xfffffffe;
23
+
24
+ /**
25
+ * The data-model major version carried by every snapshot as `formatVersion` and compared by
26
+ * isGraphSnapshot(), fromWire() and fromBytes() (design section 13.5). A mismatch is
27
+ * E_UNSUPPORTED_VERSION with details.kind "format". It bumps only when the meaning of an invariant, a
28
+ * field, a flag or a sentinel changes.
29
+ */
30
+ export const FORMAT_VERSION = 1;
31
+
32
+ /**
33
+ * Brand property read by isGraphSnapshot() (design section 7.5). It is a Symbol.for() symbol rather
34
+ * than a private one so that two copies of the package inside one application still recognise each
35
+ * other's snapshots structurally, without instanceof.
36
+ */
37
+ export const SNAPSHOT_BRAND: unique symbol = Symbol.for("@graphty/graph-format/snapshot");
38
+
39
+ /**
40
+ * Alignment, in bytes, of every core-array segment inside the arena and of every segment inside the
41
+ * GSNP byte container (design sections 9.2 and 10.3). It is WebGPU's minStorageBufferOffsetAlignment
42
+ * default, so each segment can be bound as a storage buffer window without copying.
43
+ * @internal
44
+ */
45
+ export const ALIGNMENT = 256;
46
+
47
+ /**
48
+ * Whether this host stores multi-byte integers little-endian, computed once at module load. Every
49
+ * supported platform (x86-64, ARM64, WebAssembly) is little-endian; on a big-endian host toBytes() and
50
+ * toWire() throw E_UNSUPPORTED with details.reason "big-endian host" so that no file is ever written
51
+ * that other hosts would reject (design section 9.2).
52
+ * @internal
53
+ */
54
+ export const IS_LITTLE_ENDIAN: boolean = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
55
+
56
+ /**
57
+ * Wire major version written into the GSNP container header and the manifest (design section 9.2).
58
+ * A reader refuses any other major with E_UNSUPPORTED_VERSION (details.kind "wire").
59
+ * @internal
60
+ */
61
+ export const WIRE_MAJOR = 1;
62
+
63
+ /**
64
+ * Wire minor version written into the GSNP container header and the manifest (design section 9.2).
65
+ * Minors are additive: older readers ignore unknown manifest fields. Wire 1.0 tags non-finite
66
+ * numbers as `{ "$num": ... }` and wraps a JSON object whose only key is a tag name as
67
+ * `{ "$esc": ... }` (design section 5.9).
68
+ * @internal
69
+ */
70
+ export const WIRE_MINOR = 0;
71
+
72
+ /**
73
+ * The manifest `format` discriminator of a WireManifest (design section 9.1).
74
+ * @internal
75
+ */
76
+ export const WIRE_FORMAT = "graphty-snapshot";
77
+
78
+ /**
79
+ * The four magic bytes at the start of a GSNP byte container: "GSNP" in ASCII (design section 9.2).
80
+ * @internal
81
+ */
82
+ export const CONTAINER_MAGIC: readonly [number, number, number, number] = [0x47, 0x53, 0x4e, 0x50];
83
+
84
+ /**
85
+ * The u32 value written through a host-order Uint32Array at container offset 8 so a reader can detect
86
+ * an endianness mismatch (design section 9.2): a little-endian reader sees the bytes 04 03 02 01.
87
+ * @internal
88
+ */
89
+ export const ENDIAN_PROBE = 0x01020304;