@graphty/graph-format 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -43
- package/dist/graph-format.d.ts +1 -0
- package/dist/graph-format.d.ts.map +1 -0
- package/dist/graph-format.js +15798 -0
- package/dist/graph-format.js.map +1 -0
- package/dist/src/builder/arena.d.ts +85 -0
- package/dist/src/builder/arena.d.ts.map +1 -0
- package/dist/src/builder/arena.js +154 -0
- package/dist/src/builder/arena.js.map +1 -0
- package/dist/src/builder/compact.d.ts +515 -0
- package/dist/src/builder/compact.d.ts.map +1 -0
- package/dist/src/builder/compact.js +1553 -0
- package/dist/src/builder/compact.js.map +1 -0
- package/dist/src/builder/counting-sort.d.ts +96 -0
- package/dist/src/builder/counting-sort.d.ts.map +1 -0
- package/dist/src/builder/counting-sort.js +430 -0
- package/dist/src/builder/counting-sort.js.map +1 -0
- package/dist/src/builder/freeze.d.ts +72 -0
- package/dist/src/builder/freeze.d.ts.map +1 -0
- package/dist/src/builder/freeze.js +548 -0
- package/dist/src/builder/freeze.js.map +1 -0
- package/dist/src/builder/graph-builder.d.ts +602 -0
- package/dist/src/builder/graph-builder.d.ts.map +1 -0
- package/dist/src/builder/graph-builder.js +1940 -0
- package/dist/src/builder/graph-builder.js.map +1 -0
- package/dist/src/builder/options.d.ts +16 -0
- package/dist/src/builder/options.d.ts.map +1 -0
- package/dist/src/builder/options.js +21 -0
- package/dist/src/builder/options.js.map +1 -0
- package/dist/src/columns/bitmap.d.ts +96 -0
- package/dist/src/columns/bitmap.d.ts.map +1 -0
- package/dist/src/columns/bitmap.js +184 -0
- package/dist/src/columns/bitmap.js.map +1 -0
- package/dist/src/columns/column.d.ts +220 -0
- package/dist/src/columns/column.d.ts.map +1 -0
- package/dist/src/columns/column.js +2109 -0
- package/dist/src/columns/column.js.map +1 -0
- package/dist/src/columns/dictionary.d.ts +60 -0
- package/dist/src/columns/dictionary.d.ts.map +1 -0
- package/dist/src/columns/dictionary.js +102 -0
- package/dist/src/columns/dictionary.js.map +1 -0
- package/dist/src/columns/growable.d.ts +198 -0
- package/dist/src/columns/growable.d.ts.map +1 -0
- package/dist/src/columns/growable.js +296 -0
- package/dist/src/columns/growable.js.map +1 -0
- package/dist/src/columns/infer.d.ts +121 -0
- package/dist/src/columns/infer.d.ts.map +1 -0
- package/dist/src/columns/infer.js +268 -0
- package/dist/src/columns/infer.js.map +1 -0
- package/dist/src/columns/remap.d.ts +143 -0
- package/dist/src/columns/remap.d.ts.map +1 -0
- package/dist/src/columns/remap.js +588 -0
- package/dist/src/columns/remap.js.map +1 -0
- package/dist/src/columns/table.d.ts +184 -0
- package/dist/src/columns/table.d.ts.map +1 -0
- package/dist/src/columns/table.js +485 -0
- package/dist/src/columns/table.js.map +1 -0
- package/dist/src/constants.d.ts +34 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +79 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/errors.d.ts +76 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +32 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/ids/edge-id-index.d.ts +77 -0
- package/dist/src/ids/edge-id-index.d.ts.map +1 -0
- package/dist/src/ids/edge-id-index.js +155 -0
- package/dist/src/ids/edge-id-index.js.map +1 -0
- package/dist/src/ids/node-id-map.d.ts +280 -0
- package/dist/src/ids/node-id-map.d.ts.map +1 -0
- package/dist/src/ids/node-id-map.js +1139 -0
- package/dist/src/ids/node-id-map.js.map +1 -0
- package/dist/src/ids/string-store.d.ts +219 -0
- package/dist/src/ids/string-store.d.ts.map +1 -0
- package/dist/src/ids/string-store.js +526 -0
- package/dist/src/ids/string-store.js.map +1 -0
- package/dist/src/index.d.ts +30 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +29 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/populate/from-csr.d.ts +61 -0
- package/dist/src/populate/from-csr.d.ts.map +1 -0
- package/dist/src/populate/from-csr.js +706 -0
- package/dist/src/populate/from-csr.js.map +1 -0
- package/dist/src/populate/from-edge-arrays.d.ts +51 -0
- package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
- package/dist/src/populate/from-edge-arrays.js +217 -0
- package/dist/src/populate/from-edge-arrays.js.map +1 -0
- package/dist/src/populate/from-records.d.ts +51 -0
- package/dist/src/populate/from-records.d.ts.map +1 -0
- package/dist/src/populate/from-records.js +447 -0
- package/dist/src/populate/from-records.js.map +1 -0
- package/dist/src/snapshot/derived.d.ts +211 -0
- package/dist/src/snapshot/derived.d.ts.map +1 -0
- package/dist/src/snapshot/derived.js +1254 -0
- package/dist/src/snapshot/derived.js.map +1 -0
- package/dist/src/snapshot/graph-meta.d.ts +23 -0
- package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
- package/dist/src/snapshot/graph-meta.js +159 -0
- package/dist/src/snapshot/graph-meta.js.map +1 -0
- package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
- package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
- package/dist/src/snapshot/graph-snapshot.js +1247 -0
- package/dist/src/snapshot/graph-snapshot.js.map +1 -0
- package/dist/src/snapshot/hash.d.ts +90 -0
- package/dist/src/snapshot/hash.d.ts.map +1 -0
- package/dist/src/snapshot/hash.js +228 -0
- package/dist/src/snapshot/hash.js.map +1 -0
- package/dist/src/snapshot/queries.d.ts +74 -0
- package/dist/src/snapshot/queries.d.ts.map +1 -0
- package/dist/src/snapshot/queries.js +131 -0
- package/dist/src/snapshot/queries.js.map +1 -0
- package/dist/src/snapshot/validate.d.ts +192 -0
- package/dist/src/snapshot/validate.d.ts.map +1 -0
- package/dist/src/snapshot/validate.js +990 -0
- package/dist/src/snapshot/validate.js.map +1 -0
- package/dist/src/snapshot/views.d.ts +274 -0
- package/dist/src/snapshot/views.d.ts.map +1 -0
- package/dist/src/snapshot/views.js +856 -0
- package/dist/src/snapshot/views.js.map +1 -0
- package/dist/src/types/builder.d.ts +456 -0
- package/dist/src/types/builder.d.ts.map +1 -0
- package/dist/src/types/builder.js +11 -0
- package/dist/src/types/builder.js.map +1 -0
- package/dist/src/types/columns.d.ts +615 -0
- package/dist/src/types/columns.d.ts.map +1 -0
- package/dist/src/types/columns.js +14 -0
- package/dist/src/types/columns.js.map +1 -0
- package/dist/src/types/index.d.ts +14 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +11 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/internal.d.ts +172 -0
- package/dist/src/types/internal.d.ts.map +1 -0
- package/dist/src/types/internal.js +13 -0
- package/dist/src/types/internal.js.map +1 -0
- package/dist/src/types/snapshot.d.ts +801 -0
- package/dist/src/types/snapshot.d.ts.map +1 -0
- package/dist/src/types/snapshot.js +14 -0
- package/dist/src/types/snapshot.js.map +1 -0
- package/dist/src/types/wire.d.ts +196 -0
- package/dist/src/types/wire.d.ts.map +1 -0
- package/dist/src/types/wire.js +8 -0
- package/dist/src/types/wire.js.map +1 -0
- package/dist/src/util/mask.d.ts +55 -0
- package/dist/src/util/mask.d.ts.map +1 -0
- package/dist/src/util/mask.js +71 -0
- package/dist/src/util/mask.js.map +1 -0
- package/dist/src/util/options.d.ts +22 -0
- package/dist/src/util/options.d.ts.map +1 -0
- package/dist/src/util/options.js +49 -0
- package/dist/src/util/options.js.map +1 -0
- package/dist/src/util/shared-buffers.d.ts +42 -0
- package/dist/src/util/shared-buffers.d.ts.map +1 -0
- package/dist/src/util/shared-buffers.js +52 -0
- package/dist/src/util/shared-buffers.js.map +1 -0
- package/dist/src/util/typed-array.d.ts +205 -0
- package/dist/src/util/typed-array.d.ts.map +1 -0
- package/dist/src/util/typed-array.js +289 -0
- package/dist/src/util/typed-array.js.map +1 -0
- package/dist/src/wire/bytes.d.ts +69 -0
- package/dist/src/wire/bytes.d.ts.map +1 -0
- package/dist/src/wire/bytes.js +361 -0
- package/dist/src/wire/bytes.js.map +1 -0
- package/dist/src/wire/carried-views.d.ts +111 -0
- package/dist/src/wire/carried-views.d.ts.map +1 -0
- package/dist/src/wire/carried-views.js +286 -0
- package/dist/src/wire/carried-views.js.map +1 -0
- package/dist/src/wire/from-wire.d.ts +144 -0
- package/dist/src/wire/from-wire.d.ts.map +1 -0
- package/dist/src/wire/from-wire.js +1425 -0
- package/dist/src/wire/from-wire.js.map +1 -0
- package/dist/src/wire/to-wire.d.ts +171 -0
- package/dist/src/wire/to-wire.d.ts.map +1 -0
- package/dist/src/wire/to-wire.js +652 -0
- package/dist/src/wire/to-wire.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +72 -7
- package/src/builder/arena.ts +192 -0
- package/src/builder/compact.ts +1811 -0
- package/src/builder/counting-sort.ts +506 -0
- package/src/builder/freeze.ts +689 -0
- package/src/builder/graph-builder.ts +2264 -0
- package/src/builder/options.ts +29 -0
- package/src/columns/bitmap.ts +196 -0
- package/src/columns/column.ts +2574 -0
- package/src/columns/dictionary.ts +114 -0
- package/src/columns/growable.ts +350 -0
- package/src/columns/infer.ts +285 -0
- package/src/columns/remap.ts +663 -0
- package/src/columns/table.ts +568 -0
- package/src/constants.ts +89 -0
- package/src/errors.ts +115 -0
- package/src/ids/edge-id-index.ts +179 -0
- package/src/ids/node-id-map.ts +1340 -0
- package/src/ids/string-store.ts +589 -0
- package/src/index.ts +139 -0
- package/src/lib-resizable-array-buffer.d.ts +34 -0
- package/src/populate/from-csr.ts +802 -0
- package/src/populate/from-edge-arrays.ts +257 -0
- package/src/populate/from-records.ts +536 -0
- package/src/snapshot/derived.ts +1486 -0
- package/src/snapshot/graph-meta.ts +167 -0
- package/src/snapshot/graph-snapshot.ts +1503 -0
- package/src/snapshot/hash.ts +241 -0
- package/src/snapshot/queries.ts +136 -0
- package/src/snapshot/validate.ts +1108 -0
- package/src/snapshot/views.ts +943 -0
- package/src/types/builder.ts +480 -0
- package/src/types/columns.ts +727 -0
- package/src/types/index.ts +14 -0
- package/src/types/internal.ts +195 -0
- package/src/types/snapshot.ts +886 -0
- package/src/types/wire.ts +207 -0
- package/src/util/mask.ts +89 -0
- package/src/util/options.ts +57 -0
- package/src/util/shared-buffers.ts +56 -0
- package/src/util/typed-array.ts +371 -0
- package/src/wire/bytes.ts +440 -0
- package/src/wire/carried-views.ts +327 -0
- package/src/wire/from-wire.ts +1690 -0
- package/src/wire/to-wire.ts +755 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fromRecords()` (design section 8.1, entry point 3): plain node-link records, the shape every JSON
|
|
3
|
+
* dialect parses into and what graphty-element's data sources emit. Every record is pushed into a
|
|
4
|
+
* GraphBuilder scalar by scalar -- the id, the endpoints and the weight through `addNode` / `addEdge`,
|
|
5
|
+
* every other key through `setNodeValue` / `setEdgeValue` -- so attribute columns are inferred and
|
|
6
|
+
* widened per column by the builder exactly as `addNodeRecord` / `addEdgeRecord` would (design
|
|
7
|
+
* section 5.1), and the FreezeReport carries the widenings. The reserved keys (the node id, the edge
|
|
8
|
+
* source / target and the weight) never become attribute columns: the id map and the arc array
|
|
9
|
+
* already hold them.
|
|
10
|
+
*
|
|
11
|
+
* Id coercion (design section 4.1) is applied here, before an id reaches the builder: "keep" (the
|
|
12
|
+
* default; JSON values are already typed), "canonical" (canonical integer text becomes a number,
|
|
13
|
+
* everything else stays a string), "string" and "number".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { GraphBuilder } from "../builder/graph-builder.js";
|
|
17
|
+
import { GraphFormatError } from "../errors.js";
|
|
18
|
+
import { type GraphSnapshot } from "../snapshot/graph-snapshot.js";
|
|
19
|
+
import {
|
|
20
|
+
type BuilderOptionsPatch,
|
|
21
|
+
type ColumnDecl,
|
|
22
|
+
type ColumnHandle,
|
|
23
|
+
type FreezeOptions,
|
|
24
|
+
type FreezeReport,
|
|
25
|
+
type GraphBuilderOptions,
|
|
26
|
+
type IdCoercion,
|
|
27
|
+
type NodeId,
|
|
28
|
+
type RecordsInput,
|
|
29
|
+
} from "../types/index.js";
|
|
30
|
+
import { assertOneOf } from "../util/options.js";
|
|
31
|
+
import { splitOptions } from "./from-edge-arrays.js";
|
|
32
|
+
|
|
33
|
+
/** A node or edge record. */
|
|
34
|
+
type RecordRow = Readonly<Record<string, unknown>>;
|
|
35
|
+
|
|
36
|
+
/** The default source keys, tried in order when `edgeSource` is not given. */
|
|
37
|
+
const SOURCE_KEYS: readonly string[] = ["source", "src", "from"];
|
|
38
|
+
|
|
39
|
+
/** The default target keys, tried in order when `edgeTarget` is not given. */
|
|
40
|
+
const TARGET_KEYS: readonly string[] = ["target", "dst", "to"];
|
|
41
|
+
|
|
42
|
+
/** The canonical integer text of design section 4.1. */
|
|
43
|
+
const CANONICAL_INTEGER = /^-?(0|[1-9][0-9]*)$/;
|
|
44
|
+
|
|
45
|
+
// ============================================================ ids
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a record has an own property (records may come from `JSON.parse`, so prototype lookups are
|
|
49
|
+
* never consulted).
|
|
50
|
+
* @param record - the record
|
|
51
|
+
* @param key - the key
|
|
52
|
+
* @returns true when the key is an own property
|
|
53
|
+
*/
|
|
54
|
+
function hasKey(record: RecordRow, key: string): boolean {
|
|
55
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The E_INVALID_ID error for an id cell the coercion rule cannot turn into a NodeId.
|
|
60
|
+
* @param field - which cell (id / source / target)
|
|
61
|
+
* @param value - the value
|
|
62
|
+
* @param rule - the coercion rule in force
|
|
63
|
+
* @returns the error
|
|
64
|
+
*/
|
|
65
|
+
function invalidId(field: string, value: unknown, rule: IdCoercion): GraphFormatError {
|
|
66
|
+
return new GraphFormatError(
|
|
67
|
+
"E_INVALID_ID",
|
|
68
|
+
`${field} value of type ${typeof value} is not a legal id under "${rule}"`,
|
|
69
|
+
{
|
|
70
|
+
field,
|
|
71
|
+
found: typeof value,
|
|
72
|
+
rule,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Apply an id coercion rule to one id cell (design section 4.1). The result still goes through the
|
|
79
|
+
* builder's own id validation (E_INVALID_ID for anything that is not a finite number or a well-formed
|
|
80
|
+
* string), so this function only converts; it rejects the shapes a rule cannot convert.
|
|
81
|
+
* @param value - the cell value
|
|
82
|
+
* @param rule - the coercion rule
|
|
83
|
+
* @param field - which cell, for the error message
|
|
84
|
+
* @returns the id to hand to the builder
|
|
85
|
+
*/
|
|
86
|
+
export function coerceId(value: unknown, rule: IdCoercion, field: string): NodeId {
|
|
87
|
+
switch (rule) {
|
|
88
|
+
case "keep":
|
|
89
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
throw invalidId(field, value, rule);
|
|
93
|
+
case "canonical":
|
|
94
|
+
if (typeof value === "number") {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === "string") {
|
|
98
|
+
// "-0" is excluded so the rule stays injective on text (-0 and 0 are one id)
|
|
99
|
+
if (CANONICAL_INTEGER.test(value) && value !== "-0") {
|
|
100
|
+
const n = Number(value);
|
|
101
|
+
return Number.isSafeInteger(n) ? n : value;
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
throw invalidId(field, value, rule);
|
|
106
|
+
case "string":
|
|
107
|
+
if (typeof value === "string") {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
typeof value === "number" ||
|
|
112
|
+
typeof value === "boolean" ||
|
|
113
|
+
typeof value === "bigint" ||
|
|
114
|
+
value === null
|
|
115
|
+
) {
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
throw invalidId(field, value, rule);
|
|
119
|
+
case "number":
|
|
120
|
+
if (typeof value === "number") {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "string") {
|
|
124
|
+
return Number(value);
|
|
125
|
+
}
|
|
126
|
+
throw invalidId(field, value, rule);
|
|
127
|
+
default: {
|
|
128
|
+
const name: string = rule;
|
|
129
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id coercion "${name}"`, {
|
|
130
|
+
field: "ids",
|
|
131
|
+
found: name,
|
|
132
|
+
reason: "unsupported option",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read an endpoint cell: the explicit key when one was given, else the first of the default keys the
|
|
140
|
+
* record has.
|
|
141
|
+
* @param record - the edge record
|
|
142
|
+
* @param explicit - the caller's key, or undefined
|
|
143
|
+
* @param defaults - the default keys in order
|
|
144
|
+
* @returns the key that was used and its value; the key is null when none is present
|
|
145
|
+
*/
|
|
146
|
+
function endpointOf(
|
|
147
|
+
record: RecordRow,
|
|
148
|
+
explicit: string | undefined,
|
|
149
|
+
defaults: readonly string[],
|
|
150
|
+
): { readonly key: string | null; readonly value: unknown } {
|
|
151
|
+
if (explicit !== undefined) {
|
|
152
|
+
return { key: hasKey(record, explicit) ? explicit : null, value: record[explicit] };
|
|
153
|
+
}
|
|
154
|
+
for (const key of defaults) {
|
|
155
|
+
if (hasKey(record, key)) {
|
|
156
|
+
return { key, value: record[key] };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { key: null, value: undefined };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The E_INVALID_ID error for an edge record without an endpoint.
|
|
164
|
+
* @param field - "source" or "target"
|
|
165
|
+
* @param keys - the keys that were tried
|
|
166
|
+
* @param edge - the record's position in the edge iteration
|
|
167
|
+
* @returns the error
|
|
168
|
+
*/
|
|
169
|
+
function missingEndpoint(field: string, keys: readonly string[], edge: number): GraphFormatError {
|
|
170
|
+
return new GraphFormatError(
|
|
171
|
+
"E_INVALID_ID",
|
|
172
|
+
`edge record ${edge} has no ${field} (keys tried: ${keys.join(", ")})`,
|
|
173
|
+
{
|
|
174
|
+
field,
|
|
175
|
+
keys: [...keys],
|
|
176
|
+
edge,
|
|
177
|
+
reason: "missing endpoint",
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================ attribute policy
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* How non-reserved record keys become columns: the builder's inference, one json column per key, no
|
|
186
|
+
* columns at all, or only the declared columns.
|
|
187
|
+
*/
|
|
188
|
+
class AttributePolicy {
|
|
189
|
+
private readonly mode: "infer" | "json" | "none" | "declared";
|
|
190
|
+
|
|
191
|
+
private readonly declared: ReadonlyMap<string, ColumnDecl>;
|
|
192
|
+
|
|
193
|
+
private readonly handles: { readonly node: Map<string, ColumnHandle>; readonly edge: Map<string, ColumnHandle> };
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolve the `columns` option.
|
|
197
|
+
* @param columns - the option value; default "infer"
|
|
198
|
+
*/
|
|
199
|
+
constructor(columns: RecordsInput["columns"]) {
|
|
200
|
+
this.handles = { node: new Map(), edge: new Map() };
|
|
201
|
+
if (columns === undefined || columns === "infer") {
|
|
202
|
+
this.mode = "infer";
|
|
203
|
+
this.declared = new Map();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (columns === "json" || columns === "none") {
|
|
207
|
+
this.mode = columns;
|
|
208
|
+
this.declared = new Map();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (typeof columns === "string") {
|
|
212
|
+
// unreachable for typed callers; an unknown mode string from untyped JS lands here
|
|
213
|
+
const found: string = columns;
|
|
214
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unsupported columns option "${found}"`, {
|
|
215
|
+
field: "columns",
|
|
216
|
+
found,
|
|
217
|
+
reason: "unsupported option",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const declared = new Map<string, ColumnDecl>();
|
|
221
|
+
const list: readonly ColumnDecl[] = columns;
|
|
222
|
+
for (const decl of list) {
|
|
223
|
+
if (typeof decl.name !== "string" || decl.name.length === 0) {
|
|
224
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "a column declaration needs a non-empty name", {
|
|
225
|
+
field: "name",
|
|
226
|
+
found: decl.name,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (declared.has(decl.name)) {
|
|
230
|
+
throw new GraphFormatError("E_COLUMN_EXISTS", `column "${decl.name}" is declared twice`, {
|
|
231
|
+
column: decl.name,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
declared.set(decl.name, decl);
|
|
235
|
+
}
|
|
236
|
+
this.mode = "declared";
|
|
237
|
+
this.declared = declared;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Whether any attribute key is read at all.
|
|
242
|
+
* @returns false under "none"
|
|
243
|
+
*/
|
|
244
|
+
get readsAttributes(): boolean {
|
|
245
|
+
return this.mode !== "none";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Write one attribute cell according to the policy.
|
|
250
|
+
* @param builder - the builder
|
|
251
|
+
* @param domain - "node" or "edge"
|
|
252
|
+
* @param row - the node index or logical edge index
|
|
253
|
+
* @param key - the record key
|
|
254
|
+
* @param value - the cell value
|
|
255
|
+
*/
|
|
256
|
+
write(builder: GraphBuilder, domain: "node" | "edge", row: number, key: string, value: unknown): void {
|
|
257
|
+
switch (this.mode) {
|
|
258
|
+
case "none":
|
|
259
|
+
return;
|
|
260
|
+
case "infer":
|
|
261
|
+
if (domain === "node") {
|
|
262
|
+
builder.setNodeValue(key, row, value);
|
|
263
|
+
} else {
|
|
264
|
+
builder.setEdgeValue(key, row, value);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
case "json":
|
|
268
|
+
case "declared": {
|
|
269
|
+
const handle = this.handleOf(builder, domain, key);
|
|
270
|
+
if (handle === null) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (domain === "node") {
|
|
274
|
+
builder.setNodeValue(handle, row, value);
|
|
275
|
+
} else {
|
|
276
|
+
builder.setEdgeValue(handle, row, value);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
default: {
|
|
281
|
+
const name: string = this.mode;
|
|
282
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown attribute mode ${name}`, { found: name });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The handle of a key's column in one table, declaring it on first use: a json column under
|
|
289
|
+
* "json", the caller's declaration under "declared" (an undeclared key has no column).
|
|
290
|
+
* @param builder - the builder
|
|
291
|
+
* @param domain - "node" or "edge"
|
|
292
|
+
* @param key - the record key
|
|
293
|
+
* @returns the handle, or null when the key is not stored
|
|
294
|
+
*/
|
|
295
|
+
private handleOf(builder: GraphBuilder, domain: "node" | "edge", key: string): ColumnHandle | null {
|
|
296
|
+
const cache = this.handles[domain];
|
|
297
|
+
const cached = cache.get(key);
|
|
298
|
+
if (cached !== undefined) {
|
|
299
|
+
return cached;
|
|
300
|
+
}
|
|
301
|
+
let decl: ColumnDecl;
|
|
302
|
+
if (this.mode === "json") {
|
|
303
|
+
decl = { name: key, dtype: "json" };
|
|
304
|
+
} else {
|
|
305
|
+
const declared = this.declared.get(key);
|
|
306
|
+
if (declared === undefined) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
decl = declared;
|
|
310
|
+
}
|
|
311
|
+
const handle = domain === "node" ? builder.declareNodeColumn(decl) : builder.declareEdgeColumn(decl);
|
|
312
|
+
cache.set(key, handle);
|
|
313
|
+
return handle;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ============================================================ the entry point
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The resolved record keys and rules of one fromRecords call.
|
|
321
|
+
*/
|
|
322
|
+
interface RecordRules {
|
|
323
|
+
/** The node id key, or null for positional nodes (d3 v3). */
|
|
324
|
+
readonly nodeId: string | null;
|
|
325
|
+
/** The explicit source key, or undefined for the default keys. */
|
|
326
|
+
readonly edgeSource: string | undefined;
|
|
327
|
+
/** The explicit target key, or undefined for the default keys. */
|
|
328
|
+
readonly edgeTarget: string | undefined;
|
|
329
|
+
/** The weight key, or null for unweighted. */
|
|
330
|
+
readonly edgeWeight: string | null;
|
|
331
|
+
/** The id coercion rule. */
|
|
332
|
+
readonly ids: IdCoercion;
|
|
333
|
+
/** The attribute policy. */
|
|
334
|
+
readonly attributes: AttributePolicy;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Resolve the keys and rules of a RecordsInput with their defaults.
|
|
339
|
+
* @param input - the input
|
|
340
|
+
* @returns the rules
|
|
341
|
+
*/
|
|
342
|
+
function rulesOf(input: RecordsInput): RecordRules {
|
|
343
|
+
return {
|
|
344
|
+
nodeId: input.nodeId === undefined ? "id" : input.nodeId,
|
|
345
|
+
edgeSource: input.edgeSource,
|
|
346
|
+
edgeTarget: input.edgeTarget,
|
|
347
|
+
edgeWeight: input.edgeWeight === undefined ? "weight" : input.edgeWeight,
|
|
348
|
+
ids: assertOneOf("ids", input.ids, ["keep", "canonical", "string", "number"] as const) ?? "keep",
|
|
349
|
+
attributes: new AttributePolicy(input.columns),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Resolve a positional endpoint (nodeId null: endpoints are node indices, design section 8.1): a
|
|
355
|
+
* non-negative integer below the builder's node bound, or -- under addMissingNodes -- one that
|
|
356
|
+
* extends the anonymous node range.
|
|
357
|
+
* @param builder - the builder
|
|
358
|
+
* @param value - the cell value
|
|
359
|
+
* @param field - "source" or "target"
|
|
360
|
+
* @param edge - the record's position, for the error
|
|
361
|
+
* @returns the node index
|
|
362
|
+
*/
|
|
363
|
+
function positionalEndpoint(builder: GraphBuilder, value: unknown, field: string, edge: number): number {
|
|
364
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
365
|
+
throw new GraphFormatError(
|
|
366
|
+
"E_INVALID_ID",
|
|
367
|
+
`edge record ${edge}: ${field} ${String(value)} is not a node index`,
|
|
368
|
+
{
|
|
369
|
+
field,
|
|
370
|
+
found: value,
|
|
371
|
+
edge,
|
|
372
|
+
reason: "not an index",
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const bound = builder.nodeBound;
|
|
377
|
+
if (value >= bound) {
|
|
378
|
+
if (!builder.options.addMissingNodes) {
|
|
379
|
+
throw new GraphFormatError("E_UNKNOWN_NODE", `edge record ${edge}: node index ${value} does not exist`, {
|
|
380
|
+
index: value,
|
|
381
|
+
field,
|
|
382
|
+
edge,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
builder.addAnonymousNodes(value - bound + 1);
|
|
386
|
+
}
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Read the weight cell of an edge record: absent, undefined and null mean "no weight" (1 in a
|
|
392
|
+
* weighted graph, design section 3.7); anything else must be a number.
|
|
393
|
+
* @param record - the edge record
|
|
394
|
+
* @param key - the weight key
|
|
395
|
+
* @param edge - the record's position, for the error
|
|
396
|
+
* @returns the weight, or undefined when omitted
|
|
397
|
+
*/
|
|
398
|
+
function weightOf(record: RecordRow, key: string, edge: number): number | undefined {
|
|
399
|
+
if (!hasKey(record, key)) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
const raw = record[key];
|
|
403
|
+
if (raw === undefined || raw === null) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
if (typeof raw !== "number") {
|
|
407
|
+
throw new GraphFormatError("E_INVALID_WEIGHT", `edge record ${edge}: weight "${key}" is not a number`, {
|
|
408
|
+
key,
|
|
409
|
+
found: typeof raw,
|
|
410
|
+
edge,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return raw;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Push the node records.
|
|
418
|
+
* @param builder - the builder
|
|
419
|
+
* @param rules - the resolved rules
|
|
420
|
+
* @param nodes - the node records
|
|
421
|
+
*/
|
|
422
|
+
function pushNodes(builder: GraphBuilder, rules: RecordRules, nodes: Iterable<RecordRow>): void {
|
|
423
|
+
const { nodeId, attributes } = rules;
|
|
424
|
+
let position = 0;
|
|
425
|
+
for (const record of nodes) {
|
|
426
|
+
let index: number;
|
|
427
|
+
if (nodeId === null) {
|
|
428
|
+
index = builder.addAnonymousNodes(1);
|
|
429
|
+
} else {
|
|
430
|
+
if (!hasKey(record, nodeId)) {
|
|
431
|
+
throw new GraphFormatError("E_INVALID_ID", `node record ${position} has no "${nodeId}" key`, {
|
|
432
|
+
field: nodeId,
|
|
433
|
+
node: position,
|
|
434
|
+
reason: "missing id",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
index = builder.addNode(coerceId(record[nodeId], rules.ids, nodeId));
|
|
438
|
+
}
|
|
439
|
+
if (attributes.readsAttributes) {
|
|
440
|
+
for (const key of Object.keys(record)) {
|
|
441
|
+
if (key !== nodeId) {
|
|
442
|
+
attributes.write(builder, "node", index, key, record[key]);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
position++;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Push the edge records.
|
|
452
|
+
* @param builder - the builder
|
|
453
|
+
* @param rules - the resolved rules
|
|
454
|
+
* @param edges - the edge records
|
|
455
|
+
*/
|
|
456
|
+
function pushEdges(builder: GraphBuilder, rules: RecordRules, edges: Iterable<RecordRow>): void {
|
|
457
|
+
const { nodeId, edgeWeight, attributes } = rules;
|
|
458
|
+
let position = 0;
|
|
459
|
+
for (const record of edges) {
|
|
460
|
+
const source = endpointOf(record, rules.edgeSource, SOURCE_KEYS);
|
|
461
|
+
const target = endpointOf(record, rules.edgeTarget, TARGET_KEYS);
|
|
462
|
+
if (source.key === null) {
|
|
463
|
+
throw missingEndpoint(
|
|
464
|
+
"source",
|
|
465
|
+
rules.edgeSource === undefined ? SOURCE_KEYS : [rules.edgeSource],
|
|
466
|
+
position,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (target.key === null) {
|
|
470
|
+
throw missingEndpoint(
|
|
471
|
+
"target",
|
|
472
|
+
rules.edgeTarget === undefined ? TARGET_KEYS : [rules.edgeTarget],
|
|
473
|
+
position,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const weight = edgeWeight === null ? undefined : weightOf(record, edgeWeight, position);
|
|
477
|
+
let edge: number;
|
|
478
|
+
if (nodeId === null) {
|
|
479
|
+
const u = positionalEndpoint(builder, source.value, "source", position);
|
|
480
|
+
const v = positionalEndpoint(builder, target.value, "target", position);
|
|
481
|
+
edge = builder.addEdgeByIndex(u, v, weight);
|
|
482
|
+
} else {
|
|
483
|
+
edge = builder.addEdge(
|
|
484
|
+
coerceId(source.value, rules.ids, "source"),
|
|
485
|
+
coerceId(target.value, rules.ids, "target"),
|
|
486
|
+
weight,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (attributes.readsAttributes) {
|
|
490
|
+
for (const key of Object.keys(record)) {
|
|
491
|
+
if (key !== source.key && key !== target.key && key !== edgeWeight) {
|
|
492
|
+
attributes.write(builder, "edge", edge, key, record[key]);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
position++;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Build a snapshot from node-link records (design section 8.1): node records (optional; every node
|
|
502
|
+
* an edge names is created under addMissingNodes, the default) and edge records, pushed into a
|
|
503
|
+
* GraphBuilder one scalar at a time and frozen. The node id comes from `nodeId` ("id" by default;
|
|
504
|
+
* null makes the node index the record position and the endpoints indices, the d3 v3 shape), the
|
|
505
|
+
* endpoints from `edgeSource` / `edgeTarget` ("source" / "target" by default, falling back to "src"
|
|
506
|
+
* / "from" and "dst" / "to"), the weight from `edgeWeight` ("weight" by default; null = unweighted).
|
|
507
|
+
* Every other key becomes an attribute column per `columns`: "infer" (default) lets the builder
|
|
508
|
+
* infer and widen per column (design section 5.1), "json" stores each key as a json column, "none"
|
|
509
|
+
* loads only the structure, and a ColumnDecl list stores only the declared keys. Ids are coerced per
|
|
510
|
+
* `ids` ("keep" by default). The staging weight precision defaults to f64 so a JSON weight such as
|
|
511
|
+
* 0.1 is never corrupted (the shadow column costs nothing when every value is f32-exact); pass
|
|
512
|
+
* `weightDtype: "f32"` to opt out.
|
|
513
|
+
* @param input - the records and keys; a record without its id or an endpoint, or with an id the
|
|
514
|
+
* coercion rule cannot convert, is E_INVALID_ID; a non-numeric weight is E_INVALID_WEIGHT; an
|
|
515
|
+
* unknown endpoint under addMissingNodes false is E_UNKNOWN_NODE
|
|
516
|
+
* @param options - builder policies and freeze options
|
|
517
|
+
* @returns the snapshot and the freeze report (its `widened` entries name the inferred columns that
|
|
518
|
+
* widened while the records were read)
|
|
519
|
+
*/
|
|
520
|
+
export function fromRecords(
|
|
521
|
+
input: RecordsInput,
|
|
522
|
+
options: BuilderOptionsPatch & FreezeOptions = {},
|
|
523
|
+
): { snapshot: GraphSnapshot; report: FreezeReport } {
|
|
524
|
+
const rules = rulesOf(input);
|
|
525
|
+
const split = splitOptions(input.directed, options);
|
|
526
|
+
const builderOptions: GraphBuilderOptions = {
|
|
527
|
+
...split.builder,
|
|
528
|
+
weightDtype: options.weightDtype ?? "f64",
|
|
529
|
+
};
|
|
530
|
+
const builder = new GraphBuilder(builderOptions);
|
|
531
|
+
if (input.nodes !== undefined) {
|
|
532
|
+
pushNodes(builder, rules, input.nodes);
|
|
533
|
+
}
|
|
534
|
+
pushEdges(builder, rules, input.edges);
|
|
535
|
+
return builder.freezeWithReport(split.freeze);
|
|
536
|
+
}
|