@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,1811 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The builder's staging representation and its compaction (design sections 6.2 and 6.3 step 1).
|
|
3
|
+
*
|
|
4
|
+
* Staging is structure-of-arrays typed storage that grows by doubling: the node side (`ids`,
|
|
5
|
+
* `idToIndex`, `nodeAlive`, the incidence-list heads `firstOut` / `firstIn`), the edge side (`src`,
|
|
6
|
+
* `dst`, `weight`, `weightSet`, `edgeAlive`, the incidence-list links `nextOut` / `nextIn`), the live
|
|
7
|
+
* counts, and one `StagingColumn` per declared node / edge / extension column (a growable typed array,
|
|
8
|
+
* packed bitmap or JS array plus an optional validity bitmap and, for dict columns, an interning
|
|
9
|
+
* dictionary). No per-node or per-edge object is ever allocated.
|
|
10
|
+
*
|
|
11
|
+
* Compaction (`compactStaging`) gathers a NEW staging object through node and edge remaps assigned in
|
|
12
|
+
* index order (invariant I14): tombstoned nodes and edges disappear, `refersTo` column values are
|
|
13
|
+
* rewritten (dangling references become INVALID_INDEX with the row unset), fresh `ids` / `idToIndex`
|
|
14
|
+
* objects are built so the previous snapshot keeps the ones it shares (design section 4.2), and the
|
|
15
|
+
* incidence lists are rebuilt. The source staging is never modified, so a freeze that throws after
|
|
16
|
+
* compacting leaves the builder exactly as it was (design section 11.1); the builder commits the new
|
|
17
|
+
* staging only when the freeze succeeds.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { bitmapClear, bitmapGet, bitmapWordCount, makeBitmap } from "../columns/bitmap.js";
|
|
21
|
+
import {
|
|
22
|
+
allocNumeric,
|
|
23
|
+
assertJsonValue,
|
|
24
|
+
columnOfValues,
|
|
25
|
+
createColumn,
|
|
26
|
+
isArrayLikeNumbers,
|
|
27
|
+
metaToDecl,
|
|
28
|
+
representableNumber,
|
|
29
|
+
resolveColumnMeta,
|
|
30
|
+
seedDictionary,
|
|
31
|
+
} from "../columns/column.js";
|
|
32
|
+
import { assertWellFormedString, DictionaryBuilder } from "../columns/dictionary.js";
|
|
33
|
+
import { GrowableBitmap, GrowableTypedArray } from "../columns/growable.js";
|
|
34
|
+
import { coerceValue, DtypeInferrer, type InferredDtype, inferValueDtype } from "../columns/infer.js";
|
|
35
|
+
import { INVALID_INDEX } from "../constants.js";
|
|
36
|
+
import { GraphFormatError } from "../errors.js";
|
|
37
|
+
import { detachString } from "../ids/string-store.js";
|
|
38
|
+
import {
|
|
39
|
+
type Column,
|
|
40
|
+
type ColumnDeclPatch,
|
|
41
|
+
type ColumnDomain,
|
|
42
|
+
type ColumnMeta,
|
|
43
|
+
type Dtype,
|
|
44
|
+
type NodeId,
|
|
45
|
+
type ScalarDtype,
|
|
46
|
+
type TypedArrayData,
|
|
47
|
+
type U32,
|
|
48
|
+
} from "../types/index.js";
|
|
49
|
+
import { type MutableColumnParts } from "../types/internal.js";
|
|
50
|
+
|
|
51
|
+
// ============================================================ small helpers
|
|
52
|
+
|
|
53
|
+
/** The shared, frozen empty list every unset / unwritten list row points at. */
|
|
54
|
+
const EMPTY_LIST: readonly unknown[] = Object.freeze([]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The E_COLUMN_TYPE error for a value a column cannot hold.
|
|
58
|
+
* @param meta - the column
|
|
59
|
+
* @param row - the row
|
|
60
|
+
* @param message - what is wrong
|
|
61
|
+
* @param found - the offending value's type or shape
|
|
62
|
+
* @returns the error
|
|
63
|
+
*/
|
|
64
|
+
function cellError(meta: ColumnMeta, row: number, message: string, found: unknown): GraphFormatError {
|
|
65
|
+
return new GraphFormatError("E_COLUMN_TYPE", `row ${row} of ${meta.dtype} column "${meta.name}": ${message}`, {
|
|
66
|
+
column: meta.name,
|
|
67
|
+
domain: meta.domain,
|
|
68
|
+
row,
|
|
69
|
+
found,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The dtype an inferred (auto-declared) column starts with for a value (design section 5.1).
|
|
75
|
+
* @param value - the first set value
|
|
76
|
+
* @returns the inferred dtype; E_COLUMN_TYPE for a value no column can hold
|
|
77
|
+
*/
|
|
78
|
+
export function inferInitialDtype(value: unknown): InferredDtype {
|
|
79
|
+
const dtype = inferValueDtype(value);
|
|
80
|
+
if (dtype === null) {
|
|
81
|
+
throw new GraphFormatError("E_COLUMN_TYPE", "an unset value cannot start an inferred column", {
|
|
82
|
+
reason: "unset",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return dtype;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The position of a dtype in the widening order of design section 5.1, for the union rule of
|
|
90
|
+
* `addGraph()` (design section 6.6): the dtypes outside the inference order map to the narrowest
|
|
91
|
+
* inferred dtype that can hold every value of theirs.
|
|
92
|
+
* @param dtype - any dtype
|
|
93
|
+
* @returns the inferred dtype the column widens to when it must hold values of `dtype`
|
|
94
|
+
*/
|
|
95
|
+
export function inferredEquivalent(dtype: Dtype): InferredDtype {
|
|
96
|
+
switch (dtype) {
|
|
97
|
+
case "bool":
|
|
98
|
+
return "bool";
|
|
99
|
+
case "i32":
|
|
100
|
+
case "u8":
|
|
101
|
+
return "i32";
|
|
102
|
+
case "u32":
|
|
103
|
+
case "f32":
|
|
104
|
+
case "f64":
|
|
105
|
+
return "f64";
|
|
106
|
+
case "dict":
|
|
107
|
+
case "string":
|
|
108
|
+
return "string";
|
|
109
|
+
case "list":
|
|
110
|
+
case "json":
|
|
111
|
+
return "json";
|
|
112
|
+
default: {
|
|
113
|
+
const name: string = dtype;
|
|
114
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check one list item against the child dtype of a list column at write time, so a bad item fails
|
|
121
|
+
* at the call that supplied it rather than at freeze.
|
|
122
|
+
* @param meta - the list column
|
|
123
|
+
* @param row - the row
|
|
124
|
+
* @param item - the item
|
|
125
|
+
*/
|
|
126
|
+
function checkListItem(meta: ColumnMeta, row: number, item: unknown): void {
|
|
127
|
+
const itemDtype = meta.itemDtype as ScalarDtype;
|
|
128
|
+
const components = meta.itemComponents ?? 1;
|
|
129
|
+
switch (itemDtype) {
|
|
130
|
+
case "f32":
|
|
131
|
+
case "f64":
|
|
132
|
+
case "i32":
|
|
133
|
+
case "u32":
|
|
134
|
+
case "u8": {
|
|
135
|
+
if (components === 1) {
|
|
136
|
+
if (typeof item !== "number" || !representableNumber(itemDtype, item)) {
|
|
137
|
+
throw cellError(meta, row, `list item is not a representable ${itemDtype}`, typeof item);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(item) || item.length !== components) {
|
|
142
|
+
throw cellError(meta, row, `list item is not ${components} numbers`, typeof item);
|
|
143
|
+
}
|
|
144
|
+
for (const k of item as unknown[]) {
|
|
145
|
+
if (typeof k !== "number" || !representableNumber(itemDtype, k)) {
|
|
146
|
+
throw cellError(meta, row, `list item component is not a representable ${itemDtype}`, typeof k);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
case "bool":
|
|
152
|
+
if (typeof item !== "boolean") {
|
|
153
|
+
throw cellError(meta, row, "list item is not a boolean", typeof item);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
case "dict":
|
|
157
|
+
case "string":
|
|
158
|
+
if (typeof item !== "string") {
|
|
159
|
+
throw cellError(meta, row, "list item is not a string", typeof item);
|
|
160
|
+
}
|
|
161
|
+
assertWellFormedString(item, { column: meta.name, row });
|
|
162
|
+
return;
|
|
163
|
+
case "json":
|
|
164
|
+
assertJsonValue(item, meta.name);
|
|
165
|
+
return;
|
|
166
|
+
default: {
|
|
167
|
+
const name: string = itemDtype;
|
|
168
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================ staging columns
|
|
174
|
+
|
|
175
|
+
/** The widening a write caused on an inferred column. */
|
|
176
|
+
interface WideningStep {
|
|
177
|
+
/** The dtype before. */
|
|
178
|
+
readonly from: Dtype;
|
|
179
|
+
/** The dtype after. */
|
|
180
|
+
readonly to: Dtype;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One growable column of the builder (design section 6.2).
|
|
185
|
+
*
|
|
186
|
+
* A DECLARED column stores values in the storage of its dtype (a growable typed array for the numeric
|
|
187
|
+
* dtypes and dict codes, a growable bitmap for bool, a JS array for string / list / json) plus an
|
|
188
|
+
* optional validity bitmap and, for dict columns, the interning dictionary; rows are materialised
|
|
189
|
+
* lazily up to `length` and unset rows physically hold the fill (design section 5.3). It never widens
|
|
190
|
+
* and rejects values it cannot hold with E_COLUMN_TYPE at the write.
|
|
191
|
+
*
|
|
192
|
+
* An INFERRED column (auto-declared by a string column name in `setNodeValue` / `addNodeRecord`, or
|
|
193
|
+
* a declared column widened by the union rule of `addGraph`) keeps the values exactly as written in a
|
|
194
|
+
* JS array and widens its dtype monotonically per design section 5.1 as wider values arrive; the
|
|
195
|
+
* values are coerced ONCE, at freeze, to the final dtype (bool -> 1 / 0 in a number column, number or
|
|
196
|
+
* boolean -> its canonical text in a string column), exactly as `fromRecords` would.
|
|
197
|
+
*/
|
|
198
|
+
export class StagingColumn {
|
|
199
|
+
/** The resolved metadata; replaced when an inferred column widens. */
|
|
200
|
+
meta: ColumnMeta;
|
|
201
|
+
|
|
202
|
+
/** Rows materialised so far. */
|
|
203
|
+
length = 0;
|
|
204
|
+
|
|
205
|
+
/** Declared numeric data (rows * components) or dict codes. */
|
|
206
|
+
typed: GrowableTypedArray<TypedArrayData> | null = null;
|
|
207
|
+
|
|
208
|
+
/** Declared bool data. */
|
|
209
|
+
bits: GrowableBitmap | null = null;
|
|
210
|
+
|
|
211
|
+
/** Declared string / list / json rows, or the raw values of an inferred column. */
|
|
212
|
+
values: unknown[] | null = null;
|
|
213
|
+
|
|
214
|
+
/** dict only: the interning dictionary. */
|
|
215
|
+
dictionary: DictionaryBuilder | null = null;
|
|
216
|
+
|
|
217
|
+
/** The validity bitmap; null for a non-nullable column. */
|
|
218
|
+
validity: GrowableBitmap | null = null;
|
|
219
|
+
|
|
220
|
+
private inferrer: DtypeInferrer | null;
|
|
221
|
+
|
|
222
|
+
private readonly resizable: boolean | undefined;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create an empty column.
|
|
226
|
+
* @param meta - the resolved metadata
|
|
227
|
+
* @param inferred - whether the column stages raw values and may widen
|
|
228
|
+
* @param resizable - the staging buffer kind (tests pass false); undefined = engine default
|
|
229
|
+
* @param dictionary - an existing dictionary to share (compaction); a fresh one when omitted
|
|
230
|
+
*/
|
|
231
|
+
constructor(meta: ColumnMeta, inferred: boolean, resizable?: boolean, dictionary?: DictionaryBuilder) {
|
|
232
|
+
this.meta = meta;
|
|
233
|
+
this.resizable = resizable;
|
|
234
|
+
if (inferred) {
|
|
235
|
+
this.inferrer = new DtypeInferrer();
|
|
236
|
+
this.inferrer.widenTo(inferredEquivalent(meta.dtype));
|
|
237
|
+
} else {
|
|
238
|
+
this.inferrer = null;
|
|
239
|
+
}
|
|
240
|
+
this.allocate(dictionary);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Whether the column stages raw values and may widen.
|
|
245
|
+
* @returns true for an inferred column
|
|
246
|
+
*/
|
|
247
|
+
get inferred(): boolean {
|
|
248
|
+
return this.inferrer !== null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The inferred dtype of an inferred column, or null.
|
|
253
|
+
* @returns the current inferred dtype
|
|
254
|
+
*/
|
|
255
|
+
get inferredDtype(): InferredDtype | null {
|
|
256
|
+
return this.inferrer === null ? null : this.inferrer.dtype;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Make at least `rows` rows exist, unset (when nullable) and holding the fill.
|
|
261
|
+
* @param rows - the row count to reach
|
|
262
|
+
*/
|
|
263
|
+
ensureLength(rows: number): void {
|
|
264
|
+
if (rows <= this.length) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const { meta } = this;
|
|
268
|
+
if (this.typed !== null) {
|
|
269
|
+
this.typed.resize(rows * meta.components, this.fillNumber());
|
|
270
|
+
} else if (this.bits !== null) {
|
|
271
|
+
this.bits.resize(rows, meta.fill === true);
|
|
272
|
+
} else if (this.values !== null) {
|
|
273
|
+
const fill = this.fillValue();
|
|
274
|
+
for (let row = this.length; row < rows; row++) {
|
|
275
|
+
this.values.push(fill);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (this.validity !== null) {
|
|
279
|
+
this.validity.resize(rows, false);
|
|
280
|
+
}
|
|
281
|
+
this.length = rows;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Whether a row holds a value.
|
|
286
|
+
* @param row - the row
|
|
287
|
+
* @returns true when set (non-nullable columns: when materialised)
|
|
288
|
+
*/
|
|
289
|
+
isSet(row: number): boolean {
|
|
290
|
+
if (row >= this.length) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return this.validity === null || this.validity.get(row);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Set one cell (design sections 5.1 and 6.6). `undefined` unsets the row, and so does `null`
|
|
298
|
+
* except on a declared json column (JSON null imports as unset, design section 5.3). An inferred
|
|
299
|
+
* column widens when the value is wider than its dtype; the widening is returned so the builder
|
|
300
|
+
* can report it.
|
|
301
|
+
* @param row - the row; rows below it are materialised
|
|
302
|
+
* @param value - the value
|
|
303
|
+
* @returns the widening that took place, or null
|
|
304
|
+
*/
|
|
305
|
+
write(row: number, value: unknown): WideningStep | null {
|
|
306
|
+
this.ensureLength(row + 1);
|
|
307
|
+
if (value === undefined || (value === null && (this.inferrer !== null || this.meta.dtype !== "json"))) {
|
|
308
|
+
this.unset(row);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
let widened: WideningStep | null = null;
|
|
312
|
+
if (this.inferrer !== null) {
|
|
313
|
+
const before = this.meta.dtype;
|
|
314
|
+
if (this.inferrer.observeValue(value)) {
|
|
315
|
+
const next = this.inferrer.dtype as InferredDtype;
|
|
316
|
+
this.widenTo(next);
|
|
317
|
+
widened = { from: before, to: next };
|
|
318
|
+
}
|
|
319
|
+
this.storeRaw(row, value);
|
|
320
|
+
} else {
|
|
321
|
+
this.store(row, value);
|
|
322
|
+
}
|
|
323
|
+
if (this.validity !== null) {
|
|
324
|
+
this.validity.set(row, true);
|
|
325
|
+
}
|
|
326
|
+
return widened;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Validate a value exactly as `write` would, without writing anything: the same E_COLUMN_TYPE
|
|
331
|
+
* errors for a value the column cannot hold (a non-representable number, a lone surrogate, a
|
|
332
|
+
* non-JSON object, an unset value on a non-nullable column). The record methods of the builder
|
|
333
|
+
* validate every attribute before they apply anything (design section 11.1).
|
|
334
|
+
* @param row - the row the value is meant for (error details only)
|
|
335
|
+
* @param value - the value
|
|
336
|
+
*/
|
|
337
|
+
checkValue(row: number, value: unknown): void {
|
|
338
|
+
if (value === undefined || (value === null && (this.inferrer !== null || this.meta.dtype !== "json"))) {
|
|
339
|
+
if (this.validity === null) {
|
|
340
|
+
throw this.unsetError(row);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (this.inferrer !== null) {
|
|
345
|
+
inferValueDtype(value);
|
|
346
|
+
this.checkRaw(row, value);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.coerce(row, value);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Unset one cell: the validity bit is cleared and the fill is written back (design section 5.3).
|
|
354
|
+
* @param row - the row; E_COLUMN_TYPE on a non-nullable column
|
|
355
|
+
*/
|
|
356
|
+
unset(row: number): void {
|
|
357
|
+
this.ensureLength(row + 1);
|
|
358
|
+
if (this.validity === null) {
|
|
359
|
+
throw this.unsetError(row);
|
|
360
|
+
}
|
|
361
|
+
this.validity.set(row, false);
|
|
362
|
+
this.writeFill(row);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The E_COLUMN_TYPE error for unsetting a row of a non-nullable column.
|
|
367
|
+
* @param row - the row
|
|
368
|
+
* @returns the error
|
|
369
|
+
*/
|
|
370
|
+
private unsetError(row: number): GraphFormatError {
|
|
371
|
+
return new GraphFormatError(
|
|
372
|
+
"E_COLUMN_TYPE",
|
|
373
|
+
`row ${row} of non-nullable column "${this.meta.name}" cannot be unset`,
|
|
374
|
+
{ column: this.meta.name, row, field: "nullable" },
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The JS value of one cell as the frozen column would hold it: a number (or an array of
|
|
380
|
+
* `components` numbers), a boolean, a string, a list or a json value; undefined for an unset or
|
|
381
|
+
* unmaterialised row. An inferred column's raw value is coerced to its current dtype.
|
|
382
|
+
* @param row - the row
|
|
383
|
+
* @returns the value
|
|
384
|
+
*/
|
|
385
|
+
read(row: number): unknown {
|
|
386
|
+
if (!this.isSet(row)) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
if (this.inferrer !== null) {
|
|
390
|
+
return coerceValue((this.values as unknown[])[row], this.inferrer.dtype as InferredDtype);
|
|
391
|
+
}
|
|
392
|
+
const { meta } = this;
|
|
393
|
+
const { dtype, components } = meta;
|
|
394
|
+
switch (dtype) {
|
|
395
|
+
case "f32":
|
|
396
|
+
case "f64":
|
|
397
|
+
case "i32":
|
|
398
|
+
case "u32":
|
|
399
|
+
case "u8": {
|
|
400
|
+
const typed = this.typed as GrowableTypedArray<TypedArrayData>;
|
|
401
|
+
if (components === 1) {
|
|
402
|
+
return typed.get(row);
|
|
403
|
+
}
|
|
404
|
+
const out = new Array<number>(components);
|
|
405
|
+
for (let k = 0; k < components; k++) {
|
|
406
|
+
out[k] = typed.get(row * components + k);
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
case "bool":
|
|
411
|
+
return (this.bits as GrowableBitmap).get(row);
|
|
412
|
+
case "dict": {
|
|
413
|
+
const code = (this.typed as GrowableTypedArray<TypedArrayData>).get(row);
|
|
414
|
+
return (this.dictionary as DictionaryBuilder).values[code];
|
|
415
|
+
}
|
|
416
|
+
case "string":
|
|
417
|
+
case "list":
|
|
418
|
+
case "json":
|
|
419
|
+
return (this.values as unknown[])[row];
|
|
420
|
+
default: {
|
|
421
|
+
const name: string = dtype;
|
|
422
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Widen the column to a wider dtype of the design section 5.1 order. An inferred column only
|
|
429
|
+
* changes its dtype (its raw values are coerced at freeze); a declared column becomes inferred:
|
|
430
|
+
* its current values are read out and kept as raw values from then on.
|
|
431
|
+
* @param next - the dtype to widen to
|
|
432
|
+
*/
|
|
433
|
+
widenTo(next: InferredDtype): void {
|
|
434
|
+
const { meta } = this;
|
|
435
|
+
const nextMeta = resolveColumnMeta(meta.name, meta.domain, {
|
|
436
|
+
dtype: next,
|
|
437
|
+
nullable: true,
|
|
438
|
+
role: meta.role ?? undefined,
|
|
439
|
+
extra: meta.extra,
|
|
440
|
+
});
|
|
441
|
+
if (this.inferrer !== null) {
|
|
442
|
+
this.inferrer.widenTo(next);
|
|
443
|
+
this.meta = nextMeta;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const rows = this.length;
|
|
447
|
+
const raw = new Array<unknown>(rows);
|
|
448
|
+
const validity = new GrowableBitmap({ capacity: rows, resizable: this.resizable });
|
|
449
|
+
validity.resize(rows, false);
|
|
450
|
+
for (let row = 0; row < rows; row++) {
|
|
451
|
+
if (this.isSet(row)) {
|
|
452
|
+
raw[row] = this.read(row);
|
|
453
|
+
validity.set(row, true);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
this.meta = nextMeta;
|
|
457
|
+
this.inferrer = new DtypeInferrer();
|
|
458
|
+
this.inferrer.widenTo(next);
|
|
459
|
+
this.typed = null;
|
|
460
|
+
this.bits = null;
|
|
461
|
+
this.dictionary = null;
|
|
462
|
+
this.values = raw;
|
|
463
|
+
this.validity = validity;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* A new column of the same shape holding the rows `indexMap` selects (`out[i] = this[indexMap[i]]`),
|
|
468
|
+
* or every row in order when `indexMap` is null, with `refersTo` values rewritten through
|
|
469
|
+
* `valueRemap` when given: an in-range value maps to its new index and a dangling reference
|
|
470
|
+
* becomes INVALID_INDEX with the row unset (for a list, dangling items are dropped and a row whose
|
|
471
|
+
* every item dangled becomes unset and empty). The dictionary of a dict column is shared.
|
|
472
|
+
* @param indexMap - new row -> old row, or null to keep every row
|
|
473
|
+
* @param valueRemap - the remap of the space the values reference, or null
|
|
474
|
+
* @returns the gathered column
|
|
475
|
+
*/
|
|
476
|
+
gather(indexMap: U32 | null, valueRemap: U32 | null): StagingColumn {
|
|
477
|
+
const rows = indexMap === null ? this.length : indexMap.length;
|
|
478
|
+
const out = new StagingColumn(this.meta, this.inferred, this.resizable, this.dictionary ?? undefined);
|
|
479
|
+
out.ensureLength(rows);
|
|
480
|
+
const { components } = this.meta;
|
|
481
|
+
const rewrite = valueRemap !== null && this.meta.refersTo !== null;
|
|
482
|
+
for (let i = 0; i < rows; i++) {
|
|
483
|
+
const old = indexMap === null ? i : indexMap[i];
|
|
484
|
+
if (!this.isSet(old)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (out.typed !== null && this.typed !== null) {
|
|
488
|
+
if (rewrite) {
|
|
489
|
+
const mapped = remapIndex(this.typed.get(old), valueRemap);
|
|
490
|
+
if (mapped === INVALID_INDEX) {
|
|
491
|
+
// dangling reference: INVALID_INDEX (the fill of a refersTo column) with the row unset
|
|
492
|
+
out.unsetDangling(i);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
out.typed.set(i, mapped);
|
|
496
|
+
} else {
|
|
497
|
+
for (let k = 0; k < components; k++) {
|
|
498
|
+
out.typed.set(i * components + k, this.typed.get(old * components + k));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} else if (out.bits !== null && this.bits !== null) {
|
|
502
|
+
out.bits.set(i, this.bits.get(old));
|
|
503
|
+
} else if (out.values !== null && this.values !== null) {
|
|
504
|
+
let value = this.values[old];
|
|
505
|
+
if (rewrite) {
|
|
506
|
+
const items = value as readonly number[];
|
|
507
|
+
const kept: number[] = [];
|
|
508
|
+
for (const item of items) {
|
|
509
|
+
const mapped = remapIndex(item, valueRemap);
|
|
510
|
+
if (mapped !== INVALID_INDEX) {
|
|
511
|
+
kept.push(mapped);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (kept.length === 0 && items.length > 0) {
|
|
515
|
+
out.unsetDangling(i);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
value = kept;
|
|
519
|
+
}
|
|
520
|
+
out.values[i] = value;
|
|
521
|
+
}
|
|
522
|
+
if (out.validity !== null) {
|
|
523
|
+
out.validity.set(i, true);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return out;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Leave row `i` of a gathered column unset because its reference dangled (design section 5.11).
|
|
531
|
+
* A non-nullable column gains a validity bitmap and becomes nullable (invariant I12 forbids a set
|
|
532
|
+
* row holding INVALID_INDEX; `remapColumn` flips `nullable` the same way): every earlier row is
|
|
533
|
+
* marked set except a scalar reference row still holding the INVALID_INDEX fill.
|
|
534
|
+
* @param i - the row being gathered; rows below it are already gathered
|
|
535
|
+
*/
|
|
536
|
+
private unsetDangling(i: number): void {
|
|
537
|
+
if (this.validity !== null) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const { meta } = this;
|
|
541
|
+
this.meta = resolveColumnMeta(meta.name, meta.domain, { ...metaToDecl(meta), nullable: true });
|
|
542
|
+
const validity = new GrowableBitmap({ capacity: this.length, resizable: this.resizable });
|
|
543
|
+
validity.resize(this.length, false);
|
|
544
|
+
const { typed } = this;
|
|
545
|
+
for (let row = 0; row < i; row++) {
|
|
546
|
+
if (typed === null || typed.get(row) !== INVALID_INDEX) {
|
|
547
|
+
validity.set(row, true);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
this.validity = validity;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Freeze the column into `rows` rows (design section 6.3 step 10): typed data is copied out of
|
|
555
|
+
* staging into exact-length buffers (invariant I18), the validity bitmap is copied (and dropped when
|
|
556
|
+
* every row is set), a dict dictionary is copied so the snapshot's column never changes (I17), and
|
|
557
|
+
* string / list / json rows and the raw values of an inferred column go through the column factory,
|
|
558
|
+
* which coerces them to the final dtype.
|
|
559
|
+
* @param rows - the row count of the table
|
|
560
|
+
* @returns the frozen column
|
|
561
|
+
*/
|
|
562
|
+
toColumn(rows: number): Column {
|
|
563
|
+
this.ensureLength(rows);
|
|
564
|
+
const { meta } = this;
|
|
565
|
+
const { dtype, components } = meta;
|
|
566
|
+
if (this.inferrer !== null) {
|
|
567
|
+
const source = this.values as unknown[];
|
|
568
|
+
const entries = new Array<unknown>(rows);
|
|
569
|
+
for (let row = 0; row < rows; row++) {
|
|
570
|
+
entries[row] = this.isSet(row) ? source[row] : undefined;
|
|
571
|
+
}
|
|
572
|
+
return columnOfValues(meta, entries);
|
|
573
|
+
}
|
|
574
|
+
let validity: U32 | null = null;
|
|
575
|
+
if (this.validity !== null && this.validity.count() !== rows) {
|
|
576
|
+
validity = this.validity.trim();
|
|
577
|
+
}
|
|
578
|
+
const parts: MutableColumnParts = {
|
|
579
|
+
meta,
|
|
580
|
+
length: rows,
|
|
581
|
+
data: null,
|
|
582
|
+
validity,
|
|
583
|
+
nullCount: validity === null ? 0 : rows - (this.validity as GrowableBitmap).count(),
|
|
584
|
+
dictionary: null,
|
|
585
|
+
offsets: null,
|
|
586
|
+
utf8: null,
|
|
587
|
+
strings: null,
|
|
588
|
+
child: null,
|
|
589
|
+
values: null,
|
|
590
|
+
};
|
|
591
|
+
switch (dtype) {
|
|
592
|
+
case "f32":
|
|
593
|
+
case "f64":
|
|
594
|
+
case "i32":
|
|
595
|
+
case "u32":
|
|
596
|
+
case "u8": {
|
|
597
|
+
const data = allocNumeric(dtype, rows * components);
|
|
598
|
+
data.set((this.typed as GrowableTypedArray<TypedArrayData>).view().subarray(0, rows * components));
|
|
599
|
+
parts.data = data;
|
|
600
|
+
if (meta.refersTo !== null && this.validity === null) {
|
|
601
|
+
unsetFillReferences(parts, data as U32, rows);
|
|
602
|
+
}
|
|
603
|
+
return createColumn(parts);
|
|
604
|
+
}
|
|
605
|
+
case "bool":
|
|
606
|
+
parts.data = (this.bits as GrowableBitmap).trim();
|
|
607
|
+
return createColumn(parts);
|
|
608
|
+
case "dict": {
|
|
609
|
+
const codes = new Uint32Array(rows);
|
|
610
|
+
codes.set((this.typed as GrowableTypedArray<TypedArrayData>).view().subarray(0, rows));
|
|
611
|
+
parts.data = codes;
|
|
612
|
+
parts.dictionary = [...(this.dictionary as DictionaryBuilder).values];
|
|
613
|
+
return createColumn(parts);
|
|
614
|
+
}
|
|
615
|
+
case "string":
|
|
616
|
+
case "list":
|
|
617
|
+
case "json": {
|
|
618
|
+
const source = this.values as unknown[];
|
|
619
|
+
const entries = new Array<unknown>(rows);
|
|
620
|
+
for (let row = 0; row < rows; row++) {
|
|
621
|
+
const value = this.isSet(row) ? source[row] : undefined;
|
|
622
|
+
entries[row] = value === undefined ? this.unsetEntry() : value;
|
|
623
|
+
}
|
|
624
|
+
return columnOfValues(meta, entries);
|
|
625
|
+
}
|
|
626
|
+
default: {
|
|
627
|
+
const name: string = dtype;
|
|
628
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Bytes of typed staging held by the column (JS arrays are not counted).
|
|
635
|
+
* @returns the byte count
|
|
636
|
+
*/
|
|
637
|
+
byteLength(): number {
|
|
638
|
+
let bytes = 0;
|
|
639
|
+
if (this.typed !== null) {
|
|
640
|
+
bytes += this.typed.array.byteLength;
|
|
641
|
+
}
|
|
642
|
+
if (this.bits !== null) {
|
|
643
|
+
bytes += bitmapWordCount(this.bits.length) * 4;
|
|
644
|
+
}
|
|
645
|
+
if (this.validity !== null) {
|
|
646
|
+
bytes += bitmapWordCount(this.validity.length) * 4;
|
|
647
|
+
}
|
|
648
|
+
return bytes;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Allocate the storage of the current dtype (raw values for an inferred column).
|
|
653
|
+
* @param dictionary - a dictionary to share, or undefined for a fresh seeded one
|
|
654
|
+
*/
|
|
655
|
+
private allocate(dictionary: DictionaryBuilder | undefined): void {
|
|
656
|
+
const { meta } = this;
|
|
657
|
+
const { dtype } = meta;
|
|
658
|
+
const options = { resizable: this.resizable };
|
|
659
|
+
if (this.inferrer !== null) {
|
|
660
|
+
this.values = [];
|
|
661
|
+
this.validity = new GrowableBitmap(options);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
switch (dtype) {
|
|
665
|
+
case "f32":
|
|
666
|
+
this.typed = new GrowableTypedArray(Float32Array, options);
|
|
667
|
+
break;
|
|
668
|
+
case "f64":
|
|
669
|
+
this.typed = new GrowableTypedArray(Float64Array, options);
|
|
670
|
+
break;
|
|
671
|
+
case "i32":
|
|
672
|
+
this.typed = new GrowableTypedArray(Int32Array, options);
|
|
673
|
+
break;
|
|
674
|
+
case "u32":
|
|
675
|
+
this.typed = new GrowableTypedArray(Uint32Array, options);
|
|
676
|
+
break;
|
|
677
|
+
case "u8":
|
|
678
|
+
this.typed = new GrowableTypedArray(Uint8Array, options);
|
|
679
|
+
break;
|
|
680
|
+
case "bool":
|
|
681
|
+
this.bits = new GrowableBitmap(options);
|
|
682
|
+
break;
|
|
683
|
+
case "dict": {
|
|
684
|
+
this.typed = new GrowableTypedArray(Uint32Array, options);
|
|
685
|
+
this.dictionary = dictionary ?? seedDictionary(meta);
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
case "string":
|
|
689
|
+
case "list":
|
|
690
|
+
case "json":
|
|
691
|
+
this.values = [];
|
|
692
|
+
break;
|
|
693
|
+
default: {
|
|
694
|
+
const name: string = dtype;
|
|
695
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
this.validity = meta.nullable ? new GrowableBitmap(options) : null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* The number written into unset numeric rows (the dict fill code for dict columns).
|
|
703
|
+
* @returns the fill
|
|
704
|
+
*/
|
|
705
|
+
private fillNumber(): number {
|
|
706
|
+
const { meta } = this;
|
|
707
|
+
if (meta.dtype === "dict") {
|
|
708
|
+
const dictionary = this.dictionary as DictionaryBuilder;
|
|
709
|
+
const text = typeof meta.fill === "string" ? meta.fill : "";
|
|
710
|
+
if (this.validity === null) {
|
|
711
|
+
// a set row holding the fill needs a member's code: the nominal "" is interned on demand
|
|
712
|
+
return dictionary.intern(text);
|
|
713
|
+
}
|
|
714
|
+
const code = dictionary.codeOf(text);
|
|
715
|
+
return code === INVALID_INDEX ? 0 : code;
|
|
716
|
+
}
|
|
717
|
+
return typeof meta.fill === "number" ? meta.fill : 0;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* The JS value stored in unset rows of a JS-array column (undefined for an inferred column).
|
|
722
|
+
* @returns the fill
|
|
723
|
+
*/
|
|
724
|
+
private fillValue(): unknown {
|
|
725
|
+
if (this.inferrer !== null) {
|
|
726
|
+
return undefined;
|
|
727
|
+
}
|
|
728
|
+
switch (this.meta.dtype) {
|
|
729
|
+
case "string":
|
|
730
|
+
return typeof this.meta.fill === "string" ? this.meta.fill : "";
|
|
731
|
+
case "list":
|
|
732
|
+
return EMPTY_LIST;
|
|
733
|
+
default:
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* The entry handed to the column factory for an unset row of a declared JS-array column:
|
|
740
|
+
* undefined for a nullable column, the fill for a non-nullable one (json: null, the one JSON value
|
|
741
|
+
* that means "nothing").
|
|
742
|
+
* @returns the entry
|
|
743
|
+
*/
|
|
744
|
+
private unsetEntry(): unknown {
|
|
745
|
+
if (this.meta.nullable) {
|
|
746
|
+
return undefined;
|
|
747
|
+
}
|
|
748
|
+
return this.meta.dtype === "json" ? null : this.fillValue();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private writeFill(row: number): void {
|
|
752
|
+
const { components } = this.meta;
|
|
753
|
+
if (this.typed !== null) {
|
|
754
|
+
const fill = this.fillNumber();
|
|
755
|
+
for (let k = 0; k < components; k++) {
|
|
756
|
+
this.typed.set(row * components + k, fill);
|
|
757
|
+
}
|
|
758
|
+
} else if (this.bits !== null) {
|
|
759
|
+
this.bits.set(row, this.meta.fill === true);
|
|
760
|
+
} else if (this.values !== null) {
|
|
761
|
+
this.values[row] = this.fillValue();
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Keep a raw value on an inferred column after checking it is storable at all (a json value must
|
|
767
|
+
* be JSON; a string must be well-formed).
|
|
768
|
+
* @param row - the row
|
|
769
|
+
* @param value - the value as written
|
|
770
|
+
*/
|
|
771
|
+
private storeRaw(row: number, value: unknown): void {
|
|
772
|
+
this.checkRaw(row, value);
|
|
773
|
+
(this.values as unknown[])[row] = typeof value === "string" ? detachString(value) : value;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* The checks of storeRaw: a string must be well-formed, an object must be JSON.
|
|
778
|
+
* @param row - the row
|
|
779
|
+
* @param value - the value as written
|
|
780
|
+
*/
|
|
781
|
+
private checkRaw(row: number, value: unknown): void {
|
|
782
|
+
const { meta } = this;
|
|
783
|
+
if (typeof value === "string") {
|
|
784
|
+
assertWellFormedString(value, { column: meta.name, row });
|
|
785
|
+
} else if (typeof value === "object") {
|
|
786
|
+
assertJsonValue(value, `${meta.name}[${row}]`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Store a value on a declared column: validate and normalise it (E_COLUMN_TYPE when the column
|
|
792
|
+
* cannot hold it), then write the normalised form.
|
|
793
|
+
* @param row - the row
|
|
794
|
+
* @param value - the value
|
|
795
|
+
*/
|
|
796
|
+
private store(row: number, value: unknown): void {
|
|
797
|
+
this.put(row, this.coerce(row, value));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Validate a value against a declared column's dtype and return the form `put` writes: a number
|
|
802
|
+
* or an array of `components` numbers, a boolean, a string, a copied list, or a JSON value.
|
|
803
|
+
* @param row - the row (error details only)
|
|
804
|
+
* @param value - the value
|
|
805
|
+
* @returns the normalised value
|
|
806
|
+
*/
|
|
807
|
+
private coerce(row: number, value: unknown): unknown {
|
|
808
|
+
const { meta } = this;
|
|
809
|
+
const { dtype, components } = meta;
|
|
810
|
+
switch (dtype) {
|
|
811
|
+
case "f32":
|
|
812
|
+
case "f64":
|
|
813
|
+
case "i32":
|
|
814
|
+
case "u32":
|
|
815
|
+
case "u8": {
|
|
816
|
+
const scalar = typeof value === "boolean" ? Number(value) : value;
|
|
817
|
+
if (typeof scalar === "number") {
|
|
818
|
+
if (!representableNumber(dtype, scalar)) {
|
|
819
|
+
throw cellError(meta, row, `${scalar} is not representable`, scalar);
|
|
820
|
+
}
|
|
821
|
+
return scalar;
|
|
822
|
+
}
|
|
823
|
+
if (components === 1 || !isArrayLikeNumbers(scalar, components)) {
|
|
824
|
+
throw cellError(meta, row, `not ${components} number(s)`, typeof scalar);
|
|
825
|
+
}
|
|
826
|
+
for (let k = 0; k < components; k++) {
|
|
827
|
+
if (!representableNumber(dtype, scalar[k])) {
|
|
828
|
+
throw cellError(meta, row, `${scalar[k]} is not representable`, scalar[k]);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return scalar;
|
|
832
|
+
}
|
|
833
|
+
case "bool":
|
|
834
|
+
return coerceValue(value, "bool") === true;
|
|
835
|
+
case "dict":
|
|
836
|
+
case "string": {
|
|
837
|
+
const text = coerceValue(value, "string");
|
|
838
|
+
if (typeof text !== "string") {
|
|
839
|
+
throw cellError(meta, row, "not a string", typeof value);
|
|
840
|
+
}
|
|
841
|
+
assertWellFormedString(text, { column: meta.name, row });
|
|
842
|
+
return detachString(text);
|
|
843
|
+
}
|
|
844
|
+
case "list": {
|
|
845
|
+
if (!Array.isArray(value)) {
|
|
846
|
+
throw cellError(meta, row, "not an array", typeof value);
|
|
847
|
+
}
|
|
848
|
+
for (const item of value as unknown[]) {
|
|
849
|
+
checkListItem(meta, row, item);
|
|
850
|
+
}
|
|
851
|
+
return (value as unknown[]).map((item) => (typeof item === "string" ? detachString(item) : item));
|
|
852
|
+
}
|
|
853
|
+
case "json":
|
|
854
|
+
assertJsonValue(value, `${meta.name}[${row}]`);
|
|
855
|
+
return value;
|
|
856
|
+
default: {
|
|
857
|
+
const name: string = dtype;
|
|
858
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Write a value `coerce` returned.
|
|
865
|
+
* @param row - the row
|
|
866
|
+
* @param value - the normalised value
|
|
867
|
+
*/
|
|
868
|
+
private put(row: number, value: unknown): void {
|
|
869
|
+
const { dtype, components } = this.meta;
|
|
870
|
+
switch (dtype) {
|
|
871
|
+
case "f32":
|
|
872
|
+
case "f64":
|
|
873
|
+
case "i32":
|
|
874
|
+
case "u32":
|
|
875
|
+
case "u8": {
|
|
876
|
+
const typed = this.typed as GrowableTypedArray<TypedArrayData>;
|
|
877
|
+
if (typeof value === "number") {
|
|
878
|
+
if (components === 1) {
|
|
879
|
+
typed.set(row, value);
|
|
880
|
+
} else {
|
|
881
|
+
for (let k = 0; k < components; k++) {
|
|
882
|
+
typed.set(row * components + k, value);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const vector = value as ArrayLike<number>;
|
|
888
|
+
for (let k = 0; k < components; k++) {
|
|
889
|
+
typed.set(row * components + k, vector[k]);
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
case "bool":
|
|
894
|
+
(this.bits as GrowableBitmap).set(row, value === true);
|
|
895
|
+
return;
|
|
896
|
+
case "dict":
|
|
897
|
+
(this.typed as GrowableTypedArray<TypedArrayData>).set(
|
|
898
|
+
row,
|
|
899
|
+
(this.dictionary as DictionaryBuilder).intern(value as string),
|
|
900
|
+
);
|
|
901
|
+
return;
|
|
902
|
+
case "string":
|
|
903
|
+
case "list":
|
|
904
|
+
case "json":
|
|
905
|
+
(this.values as unknown[])[row] = value;
|
|
906
|
+
return;
|
|
907
|
+
default: {
|
|
908
|
+
const name: string = dtype;
|
|
909
|
+
throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${name}`, { dtype: name });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Map an index-valued cell through a remap: in range and alive -> the new index; otherwise INVALID_INDEX.
|
|
917
|
+
* @param value - the old index
|
|
918
|
+
* @param remap - old -> new or INVALID_INDEX
|
|
919
|
+
* @returns the new index or INVALID_INDEX
|
|
920
|
+
*/
|
|
921
|
+
function remapIndex(value: number, remap: U32): number {
|
|
922
|
+
return value < remap.length ? remap[value] : INVALID_INDEX;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* The frozen form of a non-nullable scalar `refersTo` column whose rows were never all written: the
|
|
927
|
+
* unwritten rows still hold the INVALID_INDEX fill, and a SET row holding INVALID_INDEX violates
|
|
928
|
+
* invariant I12, so the frozen column becomes nullable with exactly those rows unset (the rule of
|
|
929
|
+
* `gather` for a dangling reference and of `remapColumn`; design sections 5.11 and 6.3, the builder
|
|
930
|
+
* establishes I1-I13 by construction). The builder's own declaration is left alone. A column whose
|
|
931
|
+
* every row holds an index is returned as declared.
|
|
932
|
+
* @param parts - the parts being assembled; `meta`, `validity` and `nullCount` are replaced when needed
|
|
933
|
+
* @param data - the frozen u32 data (stride 1: refersTo requires a scalar u32)
|
|
934
|
+
* @param rows - the row count
|
|
935
|
+
*/
|
|
936
|
+
function unsetFillReferences(parts: MutableColumnParts, data: U32, rows: number): void {
|
|
937
|
+
let validity: U32 | null = null;
|
|
938
|
+
for (let row = 0; row < rows; row++) {
|
|
939
|
+
if (data[row] === INVALID_INDEX) {
|
|
940
|
+
validity ??= makeBitmap(rows, true);
|
|
941
|
+
bitmapClear(validity, row);
|
|
942
|
+
parts.nullCount++;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (validity !== null) {
|
|
946
|
+
const { meta } = parts;
|
|
947
|
+
parts.meta = resolveColumnMeta(meta.name, meta.domain, { ...metaToDecl(meta), nullable: true });
|
|
948
|
+
parts.validity = validity;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Create a staging column from a declaration patch.
|
|
954
|
+
* @param name - the column name
|
|
955
|
+
* @param domain - the table
|
|
956
|
+
* @param decl - the declaration; dtype required
|
|
957
|
+
* @param inferred - whether the column may widen
|
|
958
|
+
* @param resizable - the staging buffer kind
|
|
959
|
+
* @returns the column
|
|
960
|
+
*/
|
|
961
|
+
export function createStagingColumn(
|
|
962
|
+
name: string,
|
|
963
|
+
domain: ColumnDomain,
|
|
964
|
+
decl: ColumnDeclPatch,
|
|
965
|
+
inferred: boolean,
|
|
966
|
+
resizable?: boolean,
|
|
967
|
+
): StagingColumn {
|
|
968
|
+
return new StagingColumn(resolveColumnMeta(name, domain, decl), inferred, resizable);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// ============================================================ extension tables
|
|
972
|
+
|
|
973
|
+
/** One extension table under construction (design section 5.10). */
|
|
974
|
+
export interface ExtensionStaging {
|
|
975
|
+
/** The table name. */
|
|
976
|
+
readonly name: string;
|
|
977
|
+
/** Its columns in declaration order. */
|
|
978
|
+
readonly columns: StagingColumn[];
|
|
979
|
+
/** Rows appended so far. */
|
|
980
|
+
rowCount: number;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// ============================================================ staging
|
|
984
|
+
|
|
985
|
+
/** Construction options of a Staging. */
|
|
986
|
+
interface StagingOptions {
|
|
987
|
+
/** Staging weight precision (design section 3.7). */
|
|
988
|
+
readonly weightDtype: "f32" | "f64";
|
|
989
|
+
/** Allocate the weight array up front (`weighted: true`). */
|
|
990
|
+
readonly weighted: boolean;
|
|
991
|
+
/** Node capacity hint. */
|
|
992
|
+
readonly expectedNodes: number | null;
|
|
993
|
+
/** Edge capacity hint. */
|
|
994
|
+
readonly expectedEdges: number | null;
|
|
995
|
+
/** The staging buffer kind; undefined = engine default. */
|
|
996
|
+
readonly resizable: boolean | undefined;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Whether every edge so far agreed on supplying or omitting its weight (design section 3.7): the
|
|
1001
|
+
* `weightSet` bitmap is only allocated once they disagree ("mixed").
|
|
1002
|
+
*/
|
|
1003
|
+
type WeightMode = "none" | "explicit" | "omitted" | "mixed";
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* The staging of one builder (design section 6.2). Plain growable storage plus the push and link
|
|
1007
|
+
* primitives; every rule (id validation, policies, error codes) lives in the builder.
|
|
1008
|
+
*/
|
|
1009
|
+
export class Staging {
|
|
1010
|
+
/** The staging options. */
|
|
1011
|
+
readonly options: StagingOptions;
|
|
1012
|
+
|
|
1013
|
+
/** The ids in index order, or null while every node is anonymous (id === index). */
|
|
1014
|
+
ids: NodeId[] | null = null;
|
|
1015
|
+
|
|
1016
|
+
/** id -> index, shared by reference with frozen snapshots; null while every node is anonymous. */
|
|
1017
|
+
idToIndex: Map<NodeId, number> | null = null;
|
|
1018
|
+
|
|
1019
|
+
/** Bit i set: node i is live. */
|
|
1020
|
+
readonly nodeAlive: GrowableBitmap;
|
|
1021
|
+
|
|
1022
|
+
/** Head of the out-list of every node (an edge index), INVALID_INDEX = empty. */
|
|
1023
|
+
readonly firstOut: GrowableTypedArray<U32>;
|
|
1024
|
+
|
|
1025
|
+
/** Head of the in-list of every node. */
|
|
1026
|
+
readonly firstIn: GrowableTypedArray<U32>;
|
|
1027
|
+
|
|
1028
|
+
/** Declared source of every edge. */
|
|
1029
|
+
readonly src: GrowableTypedArray<U32>;
|
|
1030
|
+
|
|
1031
|
+
/** Declared target of every edge. */
|
|
1032
|
+
readonly dst: GrowableTypedArray<U32>;
|
|
1033
|
+
|
|
1034
|
+
/** Per-edge weights (f32 or f64 per weightDtype), or null while unweighted. */
|
|
1035
|
+
weight: GrowableTypedArray<TypedArrayData> | null = null;
|
|
1036
|
+
|
|
1037
|
+
/** Bit e set: edge e's weight was supplied explicitly; null while every edge agrees. */
|
|
1038
|
+
weightSet: GrowableBitmap | null = null;
|
|
1039
|
+
|
|
1040
|
+
/** Whether edges so far supplied, omitted, or disagreed about their weight. */
|
|
1041
|
+
weightMode: WeightMode = "none";
|
|
1042
|
+
|
|
1043
|
+
/** Bit e set: edge e is live. */
|
|
1044
|
+
readonly edgeAlive: GrowableBitmap;
|
|
1045
|
+
|
|
1046
|
+
/** Next edge in the out-list of the source of every edge. */
|
|
1047
|
+
readonly nextOut: GrowableTypedArray<U32>;
|
|
1048
|
+
|
|
1049
|
+
/** Next edge in the in-list of the target of every edge. */
|
|
1050
|
+
readonly nextIn: GrowableTypedArray<U32>;
|
|
1051
|
+
|
|
1052
|
+
/** Live node count. */
|
|
1053
|
+
liveNodeCount = 0;
|
|
1054
|
+
|
|
1055
|
+
/** Live edge count. */
|
|
1056
|
+
liveEdgeCount = 0;
|
|
1057
|
+
|
|
1058
|
+
/** Live self-loop count. */
|
|
1059
|
+
selfLoopCount = 0;
|
|
1060
|
+
|
|
1061
|
+
/** Node columns in handle order. */
|
|
1062
|
+
nodeColumns: StagingColumn[] = [];
|
|
1063
|
+
|
|
1064
|
+
/** Edge columns in handle order. */
|
|
1065
|
+
edgeColumns: StagingColumn[] = [];
|
|
1066
|
+
|
|
1067
|
+
/** Extension tables in handle order. */
|
|
1068
|
+
extensions: ExtensionStaging[] = [];
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Create empty staging.
|
|
1072
|
+
* @param options - precision, capacity hints and buffer kind
|
|
1073
|
+
*/
|
|
1074
|
+
constructor(options: StagingOptions) {
|
|
1075
|
+
this.options = options;
|
|
1076
|
+
const { resizable } = options;
|
|
1077
|
+
const nodes = options.expectedNodes ?? 0;
|
|
1078
|
+
const edges = options.expectedEdges ?? 0;
|
|
1079
|
+
this.nodeAlive = new GrowableBitmap({ capacity: nodes, resizable });
|
|
1080
|
+
this.firstOut = new GrowableTypedArray<U32>(Uint32Array, { capacity: nodes, resizable });
|
|
1081
|
+
this.firstIn = new GrowableTypedArray<U32>(Uint32Array, { capacity: nodes, resizable });
|
|
1082
|
+
this.src = new GrowableTypedArray<U32>(Uint32Array, { capacity: edges, resizable });
|
|
1083
|
+
this.dst = new GrowableTypedArray<U32>(Uint32Array, { capacity: edges, resizable });
|
|
1084
|
+
this.edgeAlive = new GrowableBitmap({ capacity: edges, resizable });
|
|
1085
|
+
this.nextOut = new GrowableTypedArray<U32>(Uint32Array, { capacity: edges, resizable });
|
|
1086
|
+
this.nextIn = new GrowableTypedArray<U32>(Uint32Array, { capacity: edges, resizable });
|
|
1087
|
+
if (options.weighted) {
|
|
1088
|
+
this.weight = this.allocateWeights(edges);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* The next node index to be assigned.
|
|
1094
|
+
* @returns the node bound
|
|
1095
|
+
*/
|
|
1096
|
+
get nodeBound(): number {
|
|
1097
|
+
return this.nodeAlive.length;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* The next edge index to be assigned.
|
|
1102
|
+
* @returns the edge bound
|
|
1103
|
+
*/
|
|
1104
|
+
get edgeBound(): number {
|
|
1105
|
+
return this.edgeAlive.length;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Whether tombstoned nodes or edges exist.
|
|
1110
|
+
* @returns true when compaction is needed
|
|
1111
|
+
*/
|
|
1112
|
+
get hasTombstones(): boolean {
|
|
1113
|
+
return this.liveNodeCount < this.nodeBound || this.liveEdgeCount < this.edgeBound;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Grow capacity ahead of a bulk push.
|
|
1118
|
+
* @param nodes - the node count to fit
|
|
1119
|
+
* @param edges - the edge count to fit
|
|
1120
|
+
*/
|
|
1121
|
+
reserve(nodes: number, edges: number): void {
|
|
1122
|
+
if (nodes > 0) {
|
|
1123
|
+
this.firstOut.ensureCapacity(nodes);
|
|
1124
|
+
this.firstIn.ensureCapacity(nodes);
|
|
1125
|
+
}
|
|
1126
|
+
if (edges > 0) {
|
|
1127
|
+
this.src.ensureCapacity(edges);
|
|
1128
|
+
this.dst.ensureCapacity(edges);
|
|
1129
|
+
this.nextOut.ensureCapacity(edges);
|
|
1130
|
+
this.nextIn.ensureCapacity(edges);
|
|
1131
|
+
if (this.weight !== null) {
|
|
1132
|
+
this.weight.ensureCapacity(edges);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Materialise `ids` and `idToIndex` from the anonymous prefix (every node so far has id === index).
|
|
1139
|
+
*/
|
|
1140
|
+
materialiseIds(): void {
|
|
1141
|
+
if (this.ids !== null && this.idToIndex !== null) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const bound = this.nodeBound;
|
|
1145
|
+
const ids = new Array<NodeId>(bound);
|
|
1146
|
+
const map = new Map<NodeId, number>();
|
|
1147
|
+
for (let i = 0; i < bound; i++) {
|
|
1148
|
+
ids[i] = i;
|
|
1149
|
+
map.set(i, i);
|
|
1150
|
+
}
|
|
1151
|
+
this.ids = ids;
|
|
1152
|
+
this.idToIndex = map;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Append a live node; the id is recorded only when the id structures exist.
|
|
1157
|
+
* @param id - the id, or null for an anonymous node (id === index)
|
|
1158
|
+
* @returns the new index
|
|
1159
|
+
*/
|
|
1160
|
+
pushNode(id: NodeId | null): number {
|
|
1161
|
+
const index = this.nodeAlive.push(true);
|
|
1162
|
+
this.firstOut.push(INVALID_INDEX);
|
|
1163
|
+
this.firstIn.push(INVALID_INDEX);
|
|
1164
|
+
if (this.ids !== null && this.idToIndex !== null) {
|
|
1165
|
+
const stored = id ?? index;
|
|
1166
|
+
this.ids.push(stored);
|
|
1167
|
+
this.idToIndex.set(stored, index);
|
|
1168
|
+
}
|
|
1169
|
+
this.liveNodeCount++;
|
|
1170
|
+
return index;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* The weight array, allocated on first use and back-filled with 1 for existing edges.
|
|
1175
|
+
* @returns the growable weight array
|
|
1176
|
+
*/
|
|
1177
|
+
ensureWeights(): GrowableTypedArray<TypedArrayData> {
|
|
1178
|
+
if (this.weight === null) {
|
|
1179
|
+
this.weight = this.allocateWeights(this.edgeBound);
|
|
1180
|
+
this.weight.resize(this.edgeBound, 1);
|
|
1181
|
+
}
|
|
1182
|
+
return this.weight;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Record whether an edge's weight was explicit (design section 3.7): the `weightSet` bitmap is
|
|
1187
|
+
* allocated on the first edge that disagrees with every earlier one.
|
|
1188
|
+
* @param edge - the edge index (=== edgeBound - 1 when appending)
|
|
1189
|
+
* @param explicit - whether the weight was supplied
|
|
1190
|
+
*/
|
|
1191
|
+
trackWeight(edge: number, explicit: boolean): void {
|
|
1192
|
+
if (this.weightSet !== null) {
|
|
1193
|
+
if (edge >= this.weightSet.length) {
|
|
1194
|
+
this.weightSet.resize(edge + 1, false);
|
|
1195
|
+
}
|
|
1196
|
+
this.weightSet.set(edge, explicit);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
switch (this.weightMode) {
|
|
1200
|
+
case "none":
|
|
1201
|
+
this.weightMode = explicit ? "explicit" : "omitted";
|
|
1202
|
+
return;
|
|
1203
|
+
case "explicit":
|
|
1204
|
+
if (!explicit) {
|
|
1205
|
+
this.allocateWeightSet(true, edge);
|
|
1206
|
+
}
|
|
1207
|
+
return;
|
|
1208
|
+
case "omitted":
|
|
1209
|
+
if (explicit) {
|
|
1210
|
+
this.allocateWeightSet(false, edge);
|
|
1211
|
+
}
|
|
1212
|
+
return;
|
|
1213
|
+
case "mixed":
|
|
1214
|
+
return;
|
|
1215
|
+
default: {
|
|
1216
|
+
const mode: string = this.weightMode;
|
|
1217
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown weight mode ${mode}`, { mode });
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Whether an edge's weight was supplied explicitly (design section 3.7).
|
|
1224
|
+
* @param e - the edge index
|
|
1225
|
+
* @returns true when explicit
|
|
1226
|
+
*/
|
|
1227
|
+
weightExplicit(e: number): boolean {
|
|
1228
|
+
if (this.weightSet === null) {
|
|
1229
|
+
return this.weightMode === "explicit";
|
|
1230
|
+
}
|
|
1231
|
+
return e < this.weightSet.length && this.weightSet.get(e);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Append a live edge and link it into both incidence lists. The caller has validated the
|
|
1236
|
+
* endpoints and the weight.
|
|
1237
|
+
* @param u - source index
|
|
1238
|
+
* @param v - target index
|
|
1239
|
+
* @param weight - the weight, or undefined when omitted (stored as 1 when the array exists)
|
|
1240
|
+
* @returns the new edge index
|
|
1241
|
+
*/
|
|
1242
|
+
pushEdge(u: number, v: number, weight: number | undefined): number {
|
|
1243
|
+
const e = this.src.push(u);
|
|
1244
|
+
this.dst.push(v);
|
|
1245
|
+
if (weight !== undefined) {
|
|
1246
|
+
this.ensureWeights().push(weight);
|
|
1247
|
+
} else if (this.weight !== null) {
|
|
1248
|
+
this.weight.push(1);
|
|
1249
|
+
}
|
|
1250
|
+
this.trackWeight(e, weight !== undefined);
|
|
1251
|
+
this.edgeAlive.push(true);
|
|
1252
|
+
this.nextOut.push(this.firstOut.get(u));
|
|
1253
|
+
this.firstOut.set(u, e);
|
|
1254
|
+
this.nextIn.push(this.firstIn.get(v));
|
|
1255
|
+
this.firstIn.set(v, e);
|
|
1256
|
+
this.liveEdgeCount++;
|
|
1257
|
+
if (u === v) {
|
|
1258
|
+
this.selfLoopCount++;
|
|
1259
|
+
}
|
|
1260
|
+
return e;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Append `src.length` live edges at once (the bulk `addEdges` path): the same storage, weight
|
|
1265
|
+
* tracking and incidence-list linking as one pushEdge per edge in order, done as typed-array
|
|
1266
|
+
* passes. The caller has validated the endpoints and the weights.
|
|
1267
|
+
* @param src - source indices
|
|
1268
|
+
* @param dst - target indices, as many as `src`
|
|
1269
|
+
* @param weights - the explicit weight of every edge, or null when every weight is omitted
|
|
1270
|
+
* @returns the first new edge index
|
|
1271
|
+
*/
|
|
1272
|
+
pushEdges(src: U32, dst: U32, weights: TypedArrayData | null): number {
|
|
1273
|
+
const count = src.length;
|
|
1274
|
+
const first = this.edgeBound;
|
|
1275
|
+
const end = first + count;
|
|
1276
|
+
if (count === 0) {
|
|
1277
|
+
return first;
|
|
1278
|
+
}
|
|
1279
|
+
this.src.pushAll(src);
|
|
1280
|
+
this.dst.pushAll(dst);
|
|
1281
|
+
if (weights !== null) {
|
|
1282
|
+
this.ensureWeights().pushAll(weights);
|
|
1283
|
+
} else if (this.weight !== null) {
|
|
1284
|
+
this.weight.resize(end, 1);
|
|
1285
|
+
}
|
|
1286
|
+
this.trackWeights(first, count, weights !== null);
|
|
1287
|
+
this.edgeAlive.resize(end, true);
|
|
1288
|
+
this.nextOut.resize(end);
|
|
1289
|
+
this.nextIn.resize(end);
|
|
1290
|
+
this.selfLoopCount += linkEdgeRange(
|
|
1291
|
+
this.src.array,
|
|
1292
|
+
this.dst.array,
|
|
1293
|
+
first,
|
|
1294
|
+
end,
|
|
1295
|
+
this.firstOut.array,
|
|
1296
|
+
this.firstIn.array,
|
|
1297
|
+
this.nextOut.array,
|
|
1298
|
+
this.nextIn.array,
|
|
1299
|
+
);
|
|
1300
|
+
this.liveEdgeCount += count;
|
|
1301
|
+
return first;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* trackWeight for a batch of `count` edges from `first` that all supplied or all omitted their
|
|
1306
|
+
* weight: the same mode transitions and bitmap contents as `count` calls in order.
|
|
1307
|
+
* @param first - the first edge of the batch
|
|
1308
|
+
* @param count - the batch size, at least 1
|
|
1309
|
+
* @param explicit - whether the batch supplied its weights
|
|
1310
|
+
*/
|
|
1311
|
+
private trackWeights(first: number, count: number, explicit: boolean): void {
|
|
1312
|
+
const end = first + count;
|
|
1313
|
+
let bitmap = this.weightSet;
|
|
1314
|
+
if (bitmap === null) {
|
|
1315
|
+
switch (this.weightMode) {
|
|
1316
|
+
case "none":
|
|
1317
|
+
this.weightMode = explicit ? "explicit" : "omitted";
|
|
1318
|
+
return;
|
|
1319
|
+
case "explicit":
|
|
1320
|
+
if (explicit) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
bitmap = this.allocateWeightSet(true, first);
|
|
1324
|
+
break;
|
|
1325
|
+
case "omitted":
|
|
1326
|
+
if (!explicit) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
bitmap = this.allocateWeightSet(false, first);
|
|
1330
|
+
break;
|
|
1331
|
+
case "mixed":
|
|
1332
|
+
return;
|
|
1333
|
+
default: {
|
|
1334
|
+
const mode: string = this.weightMode;
|
|
1335
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown weight mode ${mode}`, { mode });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (bitmap.length < first) {
|
|
1340
|
+
bitmap.resize(first, false);
|
|
1341
|
+
}
|
|
1342
|
+
bitmap.resize(end, explicit);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Tombstone a live edge (O(1)).
|
|
1347
|
+
* @param e - a live edge index
|
|
1348
|
+
*/
|
|
1349
|
+
killEdge(e: number): void {
|
|
1350
|
+
this.edgeAlive.set(e, false);
|
|
1351
|
+
this.liveEdgeCount--;
|
|
1352
|
+
if (this.src.get(e) === this.dst.get(e)) {
|
|
1353
|
+
this.selfLoopCount--;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Live edges leaving a node, walked from the out-list (O(degree)).
|
|
1359
|
+
* @param u - the node index
|
|
1360
|
+
* @returns the live edge indices in ascending order
|
|
1361
|
+
*/
|
|
1362
|
+
outEdges(u: number): U32 {
|
|
1363
|
+
const out: number[] = [];
|
|
1364
|
+
for (let e = this.firstOut.get(u); e !== INVALID_INDEX; e = this.nextOut.get(e)) {
|
|
1365
|
+
if (this.edgeAlive.get(e)) {
|
|
1366
|
+
out.push(e);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return Uint32Array.from(out).sort();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* Live edges entering a node, walked from the in-list (O(degree)).
|
|
1374
|
+
* @param v - the node index
|
|
1375
|
+
* @returns the live edge indices in ascending order
|
|
1376
|
+
*/
|
|
1377
|
+
inEdges(v: number): U32 {
|
|
1378
|
+
const out: number[] = [];
|
|
1379
|
+
for (let e = this.firstIn.get(v); e !== INVALID_INDEX; e = this.nextIn.get(e)) {
|
|
1380
|
+
if (this.edgeAlive.get(e)) {
|
|
1381
|
+
out.push(e);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return Uint32Array.from(out).sort();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Live edges incident to a node in either orientation (the undirected outEdgesOf / inEdgesOf,
|
|
1389
|
+
* design section 6.6 and invariant I7): both lists walked, a self-loop listed once (O(degree)).
|
|
1390
|
+
* @param u - the node index
|
|
1391
|
+
* @returns the live edge indices in ascending order
|
|
1392
|
+
*/
|
|
1393
|
+
incidentEdges(u: number): U32 {
|
|
1394
|
+
const out: number[] = [];
|
|
1395
|
+
for (let e = this.firstOut.get(u); e !== INVALID_INDEX; e = this.nextOut.get(e)) {
|
|
1396
|
+
if (this.edgeAlive.get(e)) {
|
|
1397
|
+
out.push(e);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
for (let e = this.firstIn.get(u); e !== INVALID_INDEX; e = this.nextIn.get(e)) {
|
|
1401
|
+
if (this.edgeAlive.get(e) && this.src.get(e) !== u) {
|
|
1402
|
+
out.push(e);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return Uint32Array.from(out).sort();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Bytes of typed staging held (capacity, not length), for `byteLength()`.
|
|
1410
|
+
* @returns the byte count
|
|
1411
|
+
*/
|
|
1412
|
+
byteLength(): number {
|
|
1413
|
+
let bytes =
|
|
1414
|
+
this.firstOut.array.byteLength +
|
|
1415
|
+
this.firstIn.array.byteLength +
|
|
1416
|
+
this.src.array.byteLength +
|
|
1417
|
+
this.dst.array.byteLength +
|
|
1418
|
+
this.nextOut.array.byteLength +
|
|
1419
|
+
this.nextIn.array.byteLength +
|
|
1420
|
+
bitmapWordCount(this.nodeAlive.length) * 4 +
|
|
1421
|
+
bitmapWordCount(this.edgeAlive.length) * 4;
|
|
1422
|
+
if (this.weight !== null) {
|
|
1423
|
+
bytes += this.weight.array.byteLength;
|
|
1424
|
+
}
|
|
1425
|
+
if (this.weightSet !== null) {
|
|
1426
|
+
bytes += bitmapWordCount(this.weightSet.length) * 4;
|
|
1427
|
+
}
|
|
1428
|
+
for (const column of this.nodeColumns) {
|
|
1429
|
+
bytes += column.byteLength();
|
|
1430
|
+
}
|
|
1431
|
+
for (const column of this.edgeColumns) {
|
|
1432
|
+
bytes += column.byteLength();
|
|
1433
|
+
}
|
|
1434
|
+
for (const table of this.extensions) {
|
|
1435
|
+
for (const column of table.columns) {
|
|
1436
|
+
bytes += column.byteLength();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return bytes;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private allocateWeights(capacity: number): GrowableTypedArray<TypedArrayData> {
|
|
1443
|
+
const { resizable } = this.options;
|
|
1444
|
+
return this.options.weightDtype === "f64"
|
|
1445
|
+
? new GrowableTypedArray<TypedArrayData>(Float64Array, { capacity, resizable })
|
|
1446
|
+
: new GrowableTypedArray<TypedArrayData>(Float32Array, { capacity, resizable });
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Allocate the weightSet bitmap over `edge + 1` bits: every earlier edge gets `earlier`, `edge`
|
|
1451
|
+
* itself the opposite.
|
|
1452
|
+
* @param earlier - the value of every edge before `edge`
|
|
1453
|
+
* @param edge - the first disagreeing edge
|
|
1454
|
+
* @returns the new bitmap
|
|
1455
|
+
*/
|
|
1456
|
+
private allocateWeightSet(earlier: boolean, edge: number): GrowableBitmap {
|
|
1457
|
+
const bitmap = new GrowableBitmap({ capacity: edge + 1, resizable: this.options.resizable });
|
|
1458
|
+
bitmap.resize(edge, earlier);
|
|
1459
|
+
bitmap.push(!earlier);
|
|
1460
|
+
this.weightSet = bitmap;
|
|
1461
|
+
this.weightMode = "mixed";
|
|
1462
|
+
return bitmap;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// ============================================================ compaction
|
|
1467
|
+
|
|
1468
|
+
/** The maps of the two index spaces a `refersTo` column's values may live in (old -> new, or source -> builder). */
|
|
1469
|
+
export interface IndexMaps {
|
|
1470
|
+
/** Node index map. */
|
|
1471
|
+
readonly node: U32;
|
|
1472
|
+
/** Edge index map. */
|
|
1473
|
+
readonly edge: U32;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/** An old -> new index remap with its inverse and the new count. */
|
|
1477
|
+
export interface IndexRemap {
|
|
1478
|
+
/** old index -> new index or INVALID_INDEX. */
|
|
1479
|
+
readonly remap: U32;
|
|
1480
|
+
/** new index -> old index. */
|
|
1481
|
+
readonly origin: U32;
|
|
1482
|
+
/** The new count. */
|
|
1483
|
+
readonly count: number;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* The remap that drops every index whose `keep(i)` is false, assigning new indices in index order
|
|
1488
|
+
* (invariant I14).
|
|
1489
|
+
* @param bound - the old index space size
|
|
1490
|
+
* @param keep - whether an old index survives
|
|
1491
|
+
* @returns the remap, its inverse and the new count
|
|
1492
|
+
*/
|
|
1493
|
+
export function remapDropping(bound: number, keep: (i: number) => boolean): IndexRemap {
|
|
1494
|
+
const remap = new Uint32Array(bound);
|
|
1495
|
+
let count = 0;
|
|
1496
|
+
for (let i = 0; i < bound; i++) {
|
|
1497
|
+
remap[i] = keep(i) ? count++ : INVALID_INDEX;
|
|
1498
|
+
}
|
|
1499
|
+
const origin = new Uint32Array(count);
|
|
1500
|
+
for (let i = 0; i < bound; i++) {
|
|
1501
|
+
if (remap[i] !== INVALID_INDEX) {
|
|
1502
|
+
origin[remap[i]] = i;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
return { remap, origin, count };
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* The remap that keeps exactly the indices whose bit is set in `keep` (a bitmap over `bound` bits),
|
|
1510
|
+
* assigning new indices in index order (invariant I14): `remapDropping` over the builder's alive
|
|
1511
|
+
* bitmaps without a call per index.
|
|
1512
|
+
* @param bound - the old index space size
|
|
1513
|
+
* @param keep - a bitmap of at least `bound` bits; bit i set = old index i survives
|
|
1514
|
+
* @returns the remap, its inverse and the new count
|
|
1515
|
+
*/
|
|
1516
|
+
export function remapKeepingBits(bound: number, keep: U32): IndexRemap {
|
|
1517
|
+
const remap = new Uint32Array(bound);
|
|
1518
|
+
let count = 0;
|
|
1519
|
+
for (let i = 0; i < bound; i++) {
|
|
1520
|
+
if (((keep[i >>> 5] >>> (i & 31)) & 1) === 1) {
|
|
1521
|
+
remap[i] = count++;
|
|
1522
|
+
} else {
|
|
1523
|
+
remap[i] = INVALID_INDEX;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const origin = new Uint32Array(count);
|
|
1527
|
+
for (let i = 0; i < bound; i++) {
|
|
1528
|
+
const mapped = remap[i];
|
|
1529
|
+
if (mapped !== INVALID_INDEX) {
|
|
1530
|
+
origin[mapped] = i;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return { remap, origin, count };
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* The remap that keeps exactly the survivors of a merge walk (the indices with `survivorOf[e] === e`),
|
|
1538
|
+
* assigning new indices in index order (invariant I14): `remapDropping` for design section 6.5's
|
|
1539
|
+
* step-7 repeat without a call per edge.
|
|
1540
|
+
* @param survivorOf - old edge -> its survivor (itself for a survivor)
|
|
1541
|
+
* @returns the remap, its inverse and the new count
|
|
1542
|
+
*/
|
|
1543
|
+
export function remapSurvivors(survivorOf: U32): IndexRemap {
|
|
1544
|
+
const bound = survivorOf.length;
|
|
1545
|
+
const remap = new Uint32Array(bound);
|
|
1546
|
+
let count = 0;
|
|
1547
|
+
for (let e = 0; e < bound; e++) {
|
|
1548
|
+
if (survivorOf[e] === e) {
|
|
1549
|
+
remap[e] = count++;
|
|
1550
|
+
} else {
|
|
1551
|
+
remap[e] = INVALID_INDEX;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const origin = new Uint32Array(count);
|
|
1555
|
+
for (let e = 0; e < bound; e++) {
|
|
1556
|
+
const mapped = remap[e];
|
|
1557
|
+
if (mapped !== INVALID_INDEX) {
|
|
1558
|
+
origin[mapped] = e;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return { remap, origin, count };
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/** What a compaction gathers: the node and edge remaps plus optional merged-weight overrides. */
|
|
1565
|
+
interface CompactionPlan {
|
|
1566
|
+
/** The node remap (old -> new). */
|
|
1567
|
+
readonly nodes: IndexRemap;
|
|
1568
|
+
/** The edge remap (old -> new) of the ROWS; merged edges map to INVALID_INDEX here (their survivor carries them). */
|
|
1569
|
+
readonly edges: IndexRemap;
|
|
1570
|
+
/**
|
|
1571
|
+
* The remap `refersTo: "edge"` VALUES are rewritten through when it differs from the row remap: a
|
|
1572
|
+
* merged edge maps to its survivor's new index (design sections 5.11 and 7.3), never to
|
|
1573
|
+
* INVALID_INDEX. Null when the row remap applies to values too.
|
|
1574
|
+
*/
|
|
1575
|
+
readonly edgeValues: U32 | null;
|
|
1576
|
+
/** Per NEW edge: the reduced weight to store where `mergedWeightStored` is set; null when no merge happened. */
|
|
1577
|
+
readonly mergedWeights: Float64Array | null;
|
|
1578
|
+
/** Per NEW edge: bit set when `mergedWeights` holds a value to store; null when no merge happened. */
|
|
1579
|
+
readonly mergedWeightStored: U32 | null;
|
|
1580
|
+
/** Per NEW edge: bit set when the merged group's weight is explicit after the merge; null when no merge happened. */
|
|
1581
|
+
readonly mergedWeightSet: U32 | null;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Link the edges [first, end) into both incidence lists in index order, exactly as pushEdge does one
|
|
1586
|
+
* at a time (every list head becomes the highest linked index).
|
|
1587
|
+
* @param src - sources (backing view)
|
|
1588
|
+
* @param dst - targets (backing view)
|
|
1589
|
+
* @param first - the first edge to link
|
|
1590
|
+
* @param end - one past the last edge to link
|
|
1591
|
+
* @param firstOut - out-list heads
|
|
1592
|
+
* @param firstIn - in-list heads
|
|
1593
|
+
* @param nextOut - receives the out-list links
|
|
1594
|
+
* @param nextIn - receives the in-list links
|
|
1595
|
+
* @returns the number of self-loops among the linked edges
|
|
1596
|
+
*/
|
|
1597
|
+
function linkEdgeRange(
|
|
1598
|
+
src: U32,
|
|
1599
|
+
dst: U32,
|
|
1600
|
+
first: number,
|
|
1601
|
+
end: number,
|
|
1602
|
+
firstOut: U32,
|
|
1603
|
+
firstIn: U32,
|
|
1604
|
+
nextOut: U32,
|
|
1605
|
+
nextIn: U32,
|
|
1606
|
+
): number {
|
|
1607
|
+
let loops = 0;
|
|
1608
|
+
for (let e = first; e < end; e++) {
|
|
1609
|
+
const u = src[e];
|
|
1610
|
+
const v = dst[e];
|
|
1611
|
+
nextOut[e] = firstOut[u];
|
|
1612
|
+
firstOut[u] = e;
|
|
1613
|
+
nextIn[e] = firstIn[v];
|
|
1614
|
+
firstIn[v] = e;
|
|
1615
|
+
if (u === v) {
|
|
1616
|
+
loops++;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return loops;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* The edge gather of a compaction: endpoints through the node remap, in new-index order, with both
|
|
1624
|
+
* incidence lists rebuilt (every list head is the highest edge index, as after pushEdge in order).
|
|
1625
|
+
* @param src - the source staging's sources (backing view)
|
|
1626
|
+
* @param dst - the source staging's targets (backing view)
|
|
1627
|
+
* @param origin - new edge index -> old edge index
|
|
1628
|
+
* @param nodeRemap - old node index -> new node index
|
|
1629
|
+
* @param outSrc - receives the remapped sources
|
|
1630
|
+
* @param outDst - receives the remapped targets
|
|
1631
|
+
* @param firstOut - out-list heads, INVALID_INDEX on entry
|
|
1632
|
+
* @param firstIn - in-list heads, INVALID_INDEX on entry
|
|
1633
|
+
* @param nextOut - receives the out-list links
|
|
1634
|
+
* @param nextIn - receives the in-list links
|
|
1635
|
+
* @returns the self-loop count
|
|
1636
|
+
*/
|
|
1637
|
+
function gatherEdges(
|
|
1638
|
+
src: U32,
|
|
1639
|
+
dst: U32,
|
|
1640
|
+
origin: U32,
|
|
1641
|
+
nodeRemap: U32,
|
|
1642
|
+
outSrc: U32,
|
|
1643
|
+
outDst: U32,
|
|
1644
|
+
firstOut: U32,
|
|
1645
|
+
firstIn: U32,
|
|
1646
|
+
nextOut: U32,
|
|
1647
|
+
nextIn: U32,
|
|
1648
|
+
): number {
|
|
1649
|
+
const edgeCount = origin.length;
|
|
1650
|
+
let loops = 0;
|
|
1651
|
+
for (let e = 0; e < edgeCount; e++) {
|
|
1652
|
+
const old = origin[e];
|
|
1653
|
+
const u = nodeRemap[src[old]];
|
|
1654
|
+
const v = nodeRemap[dst[old]];
|
|
1655
|
+
outSrc[e] = u;
|
|
1656
|
+
outDst[e] = v;
|
|
1657
|
+
nextOut[e] = firstOut[u];
|
|
1658
|
+
firstOut[u] = e;
|
|
1659
|
+
nextIn[e] = firstIn[v];
|
|
1660
|
+
firstIn[v] = e;
|
|
1661
|
+
if (u === v) {
|
|
1662
|
+
loops++;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return loops;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* `out[i] = values[origin[i]]` for every new index; the element conversion is the target array's
|
|
1670
|
+
* (an f64 source into f32 staging rounds exactly as a push would).
|
|
1671
|
+
* @param values - the old values (backing view)
|
|
1672
|
+
* @param origin - new index -> old index
|
|
1673
|
+
* @param out - receives the gathered values
|
|
1674
|
+
*/
|
|
1675
|
+
function gatherValues(values: TypedArrayData, origin: U32, out: TypedArrayData): void {
|
|
1676
|
+
const count = origin.length;
|
|
1677
|
+
for (let i = 0; i < count; i++) {
|
|
1678
|
+
out[i] = values[origin[i]];
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Store the reduced weight of every merged group on its survivor (design section 6.5).
|
|
1684
|
+
* @param merged - per new edge, the reduced weight where `stored` is set
|
|
1685
|
+
* @param stored - per new edge, bit set when a reduced weight is to be stored
|
|
1686
|
+
* @param weights - the new staging weights
|
|
1687
|
+
* @param edgeCount - the new edge count
|
|
1688
|
+
*/
|
|
1689
|
+
function applyMergedWeights(merged: Float64Array, stored: U32, weights: TypedArrayData, edgeCount: number): void {
|
|
1690
|
+
for (let e = 0; e < edgeCount; e++) {
|
|
1691
|
+
if (bitmapGet(stored, e)) {
|
|
1692
|
+
weights[e] = merged[e];
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Gather a new staging through a compaction plan (design section 6.3 step 1). The source is left
|
|
1699
|
+
* untouched; the result has no tombstones, fresh `ids` / `idToIndex` objects (or none when every node
|
|
1700
|
+
* is anonymous), rebuilt incidence lists, gathered columns with `refersTo` values rewritten, and
|
|
1701
|
+
* extension tables with their `refersTo` values rewritten.
|
|
1702
|
+
* @param source - the staging to compact
|
|
1703
|
+
* @param plan - the remaps and merge overrides
|
|
1704
|
+
* @returns the compacted staging
|
|
1705
|
+
*/
|
|
1706
|
+
export function compactStaging(source: Staging, plan: CompactionPlan): Staging {
|
|
1707
|
+
const { nodes, edges } = plan;
|
|
1708
|
+
const out = new Staging({
|
|
1709
|
+
...source.options,
|
|
1710
|
+
weighted: source.weight !== null || plan.mergedWeights !== null,
|
|
1711
|
+
expectedNodes: nodes.count,
|
|
1712
|
+
expectedEdges: edges.count,
|
|
1713
|
+
});
|
|
1714
|
+
// nodes: every new node is live with empty incidence lists (what pushNode(null) does, in bulk); ids
|
|
1715
|
+
// and map are fresh objects; anonymous nodes stay anonymous (their ids are their new indices)
|
|
1716
|
+
out.nodeAlive.resize(nodes.count, true);
|
|
1717
|
+
out.firstOut.resize(nodes.count, INVALID_INDEX);
|
|
1718
|
+
out.firstIn.resize(nodes.count, INVALID_INDEX);
|
|
1719
|
+
out.liveNodeCount = nodes.count;
|
|
1720
|
+
if (source.ids !== null) {
|
|
1721
|
+
const ids = new Array<NodeId>(nodes.count);
|
|
1722
|
+
const map = new Map<NodeId, number>();
|
|
1723
|
+
for (let i = 0; i < nodes.count; i++) {
|
|
1724
|
+
const id = source.ids[nodes.origin[i]];
|
|
1725
|
+
ids[i] = id;
|
|
1726
|
+
map.set(id, i);
|
|
1727
|
+
}
|
|
1728
|
+
out.ids = ids;
|
|
1729
|
+
out.idToIndex = map;
|
|
1730
|
+
}
|
|
1731
|
+
// edges: gathered in new-index order through the backing views (one typed-array pass, design
|
|
1732
|
+
// section 6.3 step 1), weights and explicit-weight bits carried, lists rebuilt by pushing every
|
|
1733
|
+
// edge in index order exactly as pushEdge does (head of each list = highest index)
|
|
1734
|
+
const { remap: nodeRemap } = nodes;
|
|
1735
|
+
const edgeCount = edges.count;
|
|
1736
|
+
const { origin } = edges;
|
|
1737
|
+
out.weightMode = source.weightMode;
|
|
1738
|
+
if (source.weightSet !== null || plan.mergedWeightSet !== null) {
|
|
1739
|
+
out.weightSet = new GrowableBitmap({ capacity: edgeCount, resizable: source.options.resizable });
|
|
1740
|
+
out.weightMode = "mixed";
|
|
1741
|
+
}
|
|
1742
|
+
out.src.resize(edgeCount);
|
|
1743
|
+
out.dst.resize(edgeCount);
|
|
1744
|
+
out.nextOut.resize(edgeCount);
|
|
1745
|
+
out.nextIn.resize(edgeCount);
|
|
1746
|
+
out.edgeAlive.resize(edgeCount, true);
|
|
1747
|
+
out.liveEdgeCount = edgeCount;
|
|
1748
|
+
out.selfLoopCount = gatherEdges(
|
|
1749
|
+
source.src.array,
|
|
1750
|
+
source.dst.array,
|
|
1751
|
+
origin,
|
|
1752
|
+
nodeRemap,
|
|
1753
|
+
out.src.array,
|
|
1754
|
+
out.dst.array,
|
|
1755
|
+
out.firstOut.array,
|
|
1756
|
+
out.firstIn.array,
|
|
1757
|
+
out.nextOut.array,
|
|
1758
|
+
out.nextIn.array,
|
|
1759
|
+
);
|
|
1760
|
+
if (out.weight !== null) {
|
|
1761
|
+
out.weight.resize(edgeCount);
|
|
1762
|
+
const weights = out.weight.array;
|
|
1763
|
+
if (source.weight === null) {
|
|
1764
|
+
weights.fill(1, 0, edgeCount);
|
|
1765
|
+
} else {
|
|
1766
|
+
gatherValues(source.weight.array, origin, weights);
|
|
1767
|
+
}
|
|
1768
|
+
if (plan.mergedWeights !== null && plan.mergedWeightStored !== null) {
|
|
1769
|
+
applyMergedWeights(plan.mergedWeights, plan.mergedWeightStored, weights, edgeCount);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
if (out.weightSet !== null) {
|
|
1773
|
+
out.weightSet.resize(edgeCount, false);
|
|
1774
|
+
for (let e = 0; e < edgeCount; e++) {
|
|
1775
|
+
let explicit = source.weightExplicit(origin[e]);
|
|
1776
|
+
if (plan.mergedWeightSet !== null && bitmapGet(plan.mergedWeightSet, e)) {
|
|
1777
|
+
explicit = true;
|
|
1778
|
+
}
|
|
1779
|
+
if (explicit) {
|
|
1780
|
+
out.weightSet.set(e, true);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
// columns
|
|
1785
|
+
const refs: IndexMaps = { node: nodeRemap, edge: plan.edgeValues ?? edges.remap };
|
|
1786
|
+
out.nodeColumns = source.nodeColumns.map((column) => column.gather(nodes.origin, valueRemapFor(column, refs)));
|
|
1787
|
+
out.edgeColumns = source.edgeColumns.map((column) => column.gather(edges.origin, valueRemapFor(column, refs)));
|
|
1788
|
+
out.extensions = source.extensions.map((table) => ({
|
|
1789
|
+
name: table.name,
|
|
1790
|
+
rowCount: table.rowCount,
|
|
1791
|
+
columns: table.columns.map((column) => column.gather(null, valueRemapFor(column, refs))),
|
|
1792
|
+
}));
|
|
1793
|
+
return out;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* The value remap that applies to a column's `refersTo`, if any.
|
|
1798
|
+
* @param column - the column
|
|
1799
|
+
* @param refs - the node and edge remaps
|
|
1800
|
+
* @returns the remap of the referenced space, or null
|
|
1801
|
+
*/
|
|
1802
|
+
function valueRemapFor(column: StagingColumn, refs: IndexMaps): U32 | null {
|
|
1803
|
+
switch (column.meta.refersTo) {
|
|
1804
|
+
case "node":
|
|
1805
|
+
return refs.node;
|
|
1806
|
+
case "edge":
|
|
1807
|
+
return refs.edge;
|
|
1808
|
+
default:
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
}
|