@graphty/graph-io 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 +250 -28
- package/dist/chunks/children-CL3Cy0ez.js +238 -0
- package/dist/chunks/children-CL3Cy0ez.js.map +1 -0
- package/dist/chunks/escape-DyI8JofU.js +938 -0
- package/dist/chunks/escape-DyI8JofU.js.map +1 -0
- package/dist/chunks/importer-CQnJuWJw.js +2987 -0
- package/dist/chunks/importer-CQnJuWJw.js.map +1 -0
- package/dist/chunks/importer-CpCpfbxr.js +2015 -0
- package/dist/chunks/importer-CpCpfbxr.js.map +1 -0
- package/dist/chunks/importer-DbnGYr3_.js +2342 -0
- package/dist/chunks/importer-DbnGYr3_.js.map +1 -0
- package/dist/chunks/importer-GozH8DkN.js +3050 -0
- package/dist/chunks/importer-GozH8DkN.js.map +1 -0
- package/dist/chunks/records-CGpxszm1.js +605 -0
- package/dist/chunks/records-CGpxszm1.js.map +1 -0
- package/dist/chunks/text-CajMdVFy.js +189 -0
- package/dist/chunks/text-CajMdVFy.js.map +1 -0
- package/dist/chunks/writer-DxSKC7TL.js +2842 -0
- package/dist/chunks/writer-DxSKC7TL.js.map +1 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +1702 -0
- package/dist/csv.js.map +1 -0
- package/dist/dot.d.ts +1 -0
- package/dist/dot.js +8 -0
- package/dist/dot.js.map +1 -0
- package/dist/gexf.d.ts +1 -0
- package/dist/gexf.js +3466 -0
- package/dist/gexf.js.map +1 -0
- package/dist/gml.d.ts +1 -0
- package/dist/gml.js +2647 -0
- package/dist/gml.js.map +1 -0
- package/dist/graph-io.d.ts +1 -0
- package/dist/graph-io.js +790 -0
- package/dist/graph-io.js.map +1 -0
- package/dist/graphml.d.ts +1 -0
- package/dist/graphml.js +8 -0
- package/dist/graphml.js.map +1 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +11 -0
- package/dist/json.js.map +1 -0
- package/dist/neo4j.d.ts +1 -0
- package/dist/neo4j.js +2046 -0
- package/dist/neo4j.js.map +1 -0
- package/dist/pajek.d.ts +1 -0
- package/dist/pajek.js +8 -0
- package/dist/pajek.js.map +1 -0
- package/dist/src/children.d.ts +134 -0
- package/dist/src/children.d.ts.map +1 -0
- package/dist/src/children.js +274 -0
- package/dist/src/children.js.map +1 -0
- package/dist/src/common/attributes.d.ts +229 -0
- package/dist/src/common/attributes.d.ts.map +1 -0
- package/dist/src/common/attributes.js +368 -0
- package/dist/src/common/attributes.js.map +1 -0
- package/dist/src/common/codes.d.ts +105 -0
- package/dist/src/common/codes.d.ts.map +1 -0
- package/dist/src/common/codes.js +107 -0
- package/dist/src/common/codes.js.map +1 -0
- package/dist/src/common/declared-types.d.ts +84 -0
- package/dist/src/common/declared-types.d.ts.map +1 -0
- package/dist/src/common/declared-types.js +326 -0
- package/dist/src/common/declared-types.js.map +1 -0
- package/dist/src/common/direction.d.ts +206 -0
- package/dist/src/common/direction.d.ts.map +1 -0
- package/dist/src/common/direction.js +370 -0
- package/dist/src/common/direction.js.map +1 -0
- package/dist/src/common/escape.d.ts +92 -0
- package/dist/src/common/escape.d.ts.map +1 -0
- package/dist/src/common/escape.js +212 -0
- package/dist/src/common/escape.js.map +1 -0
- package/dist/src/common/export.d.ts +249 -0
- package/dist/src/common/export.d.ts.map +1 -0
- package/dist/src/common/export.js +594 -0
- package/dist/src/common/export.js.map +1 -0
- package/dist/src/common/format.d.ts +59 -0
- package/dist/src/common/format.d.ts.map +1 -0
- package/dist/src/common/format.js +106 -0
- package/dist/src/common/format.js.map +1 -0
- package/dist/src/common/ids.d.ts +83 -0
- package/dist/src/common/ids.d.ts.map +1 -0
- package/dist/src/common/ids.js +158 -0
- package/dist/src/common/ids.js.map +1 -0
- package/dist/src/common/input.d.ts +100 -0
- package/dist/src/common/input.d.ts.map +1 -0
- package/dist/src/common/input.js +335 -0
- package/dist/src/common/input.js.map +1 -0
- package/dist/src/common/lists.d.ts +34 -0
- package/dist/src/common/lists.d.ts.map +1 -0
- package/dist/src/common/lists.js +185 -0
- package/dist/src/common/lists.js.map +1 -0
- package/dist/src/common/options.d.ts +108 -0
- package/dist/src/common/options.d.ts.map +1 -0
- package/dist/src/common/options.js +265 -0
- package/dist/src/common/options.js.map +1 -0
- package/dist/src/common/report.d.ts +187 -0
- package/dist/src/common/report.d.ts.map +1 -0
- package/dist/src/common/report.js +274 -0
- package/dist/src/common/report.js.map +1 -0
- package/dist/src/common/temporal.d.ts +71 -0
- package/dist/src/common/temporal.d.ts.map +1 -0
- package/dist/src/common/temporal.js +266 -0
- package/dist/src/common/temporal.js.map +1 -0
- package/dist/src/common/text.d.ts +104 -0
- package/dist/src/common/text.d.ts.map +1 -0
- package/dist/src/common/text.js +255 -0
- package/dist/src/common/text.js.map +1 -0
- package/dist/src/common/weights.d.ts +77 -0
- package/dist/src/common/weights.d.ts.map +1 -0
- package/dist/src/common/weights.js +156 -0
- package/dist/src/common/weights.js.map +1 -0
- package/dist/src/common/writer.d.ts +51 -0
- package/dist/src/common/writer.d.ts.map +1 -0
- package/dist/src/common/writer.js +108 -0
- package/dist/src/common/writer.js.map +1 -0
- package/dist/src/common/xml.d.ts +245 -0
- package/dist/src/common/xml.d.ts.map +1 -0
- package/dist/src/common/xml.js +942 -0
- package/dist/src/common/xml.js.map +1 -0
- package/dist/src/formats/csv/exporter.d.ts +70 -0
- package/dist/src/formats/csv/exporter.d.ts.map +1 -0
- package/dist/src/formats/csv/exporter.js +682 -0
- package/dist/src/formats/csv/exporter.js.map +1 -0
- package/dist/src/formats/csv/header.d.ts +66 -0
- package/dist/src/formats/csv/header.d.ts.map +1 -0
- package/dist/src/formats/csv/header.js +152 -0
- package/dist/src/formats/csv/header.js.map +1 -0
- package/dist/src/formats/csv/importer.d.ts +82 -0
- package/dist/src/formats/csv/importer.d.ts.map +1 -0
- package/dist/src/formats/csv/importer.js +849 -0
- package/dist/src/formats/csv/importer.js.map +1 -0
- package/dist/src/formats/csv/index.d.ts +60 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -0
- package/dist/src/formats/csv/index.js +63 -0
- package/dist/src/formats/csv/index.js.map +1 -0
- package/dist/src/formats/csv/records.d.ts +188 -0
- package/dist/src/formats/csv/records.d.ts.map +1 -0
- package/dist/src/formats/csv/records.js +702 -0
- package/dist/src/formats/csv/records.js.map +1 -0
- package/dist/src/formats/csv/values.d.ts +105 -0
- package/dist/src/formats/csv/values.d.ts.map +1 -0
- package/dist/src/formats/csv/values.js +192 -0
- package/dist/src/formats/csv/values.js.map +1 -0
- package/dist/src/formats/dot/exporter.d.ts +52 -0
- package/dist/src/formats/dot/exporter.d.ts.map +1 -0
- package/dist/src/formats/dot/exporter.js +836 -0
- package/dist/src/formats/dot/exporter.js.map +1 -0
- package/dist/src/formats/dot/importer.d.ts +102 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -0
- package/dist/src/formats/dot/importer.js +1291 -0
- package/dist/src/formats/dot/importer.js.map +1 -0
- package/dist/src/formats/dot/index.d.ts +7 -0
- package/dist/src/formats/dot/index.d.ts.map +1 -0
- package/dist/src/formats/dot/index.js +7 -0
- package/dist/src/formats/dot/index.js.map +1 -0
- package/dist/src/formats/dot/names.d.ts +29 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -0
- package/dist/src/formats/dot/names.js +28 -0
- package/dist/src/formats/dot/names.js.map +1 -0
- package/dist/src/formats/dot/tokenizer.d.ts +114 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -0
- package/dist/src/formats/dot/tokenizer.js +341 -0
- package/dist/src/formats/dot/tokenizer.js.map +1 -0
- package/dist/src/formats/gexf/exporter.d.ts +56 -0
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -0
- package/dist/src/formats/gexf/exporter.js +1395 -0
- package/dist/src/formats/gexf/exporter.js.map +1 -0
- package/dist/src/formats/gexf/importer.d.ts +73 -0
- package/dist/src/formats/gexf/importer.d.ts.map +1 -0
- package/dist/src/formats/gexf/importer.js +1880 -0
- package/dist/src/formats/gexf/importer.js.map +1 -0
- package/dist/src/formats/gexf/index.d.ts +96 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -0
- package/dist/src/formats/gexf/index.js +97 -0
- package/dist/src/formats/gexf/index.js.map +1 -0
- package/dist/src/formats/gexf/schema.d.ts +135 -0
- package/dist/src/formats/gexf/schema.d.ts.map +1 -0
- package/dist/src/formats/gexf/schema.js +323 -0
- package/dist/src/formats/gexf/schema.js.map +1 -0
- package/dist/src/formats/gml/exporter.d.ts +69 -0
- package/dist/src/formats/gml/exporter.d.ts.map +1 -0
- package/dist/src/formats/gml/exporter.js +1093 -0
- package/dist/src/formats/gml/exporter.js.map +1 -0
- package/dist/src/formats/gml/importer.d.ts +66 -0
- package/dist/src/formats/gml/importer.d.ts.map +1 -0
- package/dist/src/formats/gml/importer.js +1331 -0
- package/dist/src/formats/gml/importer.js.map +1 -0
- package/dist/src/formats/gml/index.d.ts +85 -0
- package/dist/src/formats/gml/index.d.ts.map +1 -0
- package/dist/src/formats/gml/index.js +88 -0
- package/dist/src/formats/gml/index.js.map +1 -0
- package/dist/src/formats/gml/syntax.d.ts +186 -0
- package/dist/src/formats/gml/syntax.d.ts.map +1 -0
- package/dist/src/formats/gml/syntax.js +467 -0
- package/dist/src/formats/gml/syntax.js.map +1 -0
- package/dist/src/formats/graphml/constants.d.ts +169 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -0
- package/dist/src/formats/graphml/constants.js +165 -0
- package/dist/src/formats/graphml/constants.js.map +1 -0
- package/dist/src/formats/graphml/exporter.d.ts +34 -0
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -0
- package/dist/src/formats/graphml/exporter.js +1176 -0
- package/dist/src/formats/graphml/exporter.js.map +1 -0
- package/dist/src/formats/graphml/importer.d.ts +31 -0
- package/dist/src/formats/graphml/importer.d.ts.map +1 -0
- package/dist/src/formats/graphml/importer.js +1607 -0
- package/dist/src/formats/graphml/importer.js.map +1 -0
- package/dist/src/formats/graphml/index.d.ts +8 -0
- package/dist/src/formats/graphml/index.d.ts.map +1 -0
- package/dist/src/formats/graphml/index.js +8 -0
- package/dist/src/formats/graphml/index.js.map +1 -0
- package/dist/src/formats/graphml/tree.d.ts +72 -0
- package/dist/src/formats/graphml/tree.d.ts.map +1 -0
- package/dist/src/formats/graphml/tree.js +290 -0
- package/dist/src/formats/graphml/tree.js.map +1 -0
- package/dist/src/formats/json/dialect.d.ts +125 -0
- package/dist/src/formats/json/dialect.d.ts.map +1 -0
- package/dist/src/formats/json/dialect.js +262 -0
- package/dist/src/formats/json/dialect.js.map +1 -0
- package/dist/src/formats/json/exporter.d.ts +89 -0
- package/dist/src/formats/json/exporter.d.ts.map +1 -0
- package/dist/src/formats/json/exporter.js +1358 -0
- package/dist/src/formats/json/exporter.js.map +1 -0
- package/dist/src/formats/json/importer.d.ts +108 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -0
- package/dist/src/formats/json/importer.js +1838 -0
- package/dist/src/formats/json/importer.js.map +1 -0
- package/dist/src/formats/json/index.d.ts +8 -0
- package/dist/src/formats/json/index.d.ts.map +1 -0
- package/dist/src/formats/json/index.js +8 -0
- package/dist/src/formats/json/index.js.map +1 -0
- package/dist/src/formats/neo4j/exporter.d.ts +68 -0
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -0
- package/dist/src/formats/neo4j/exporter.js +1055 -0
- package/dist/src/formats/neo4j/exporter.js.map +1 -0
- package/dist/src/formats/neo4j/header.d.ts +52 -0
- package/dist/src/formats/neo4j/header.d.ts.map +1 -0
- package/dist/src/formats/neo4j/header.js +131 -0
- package/dist/src/formats/neo4j/header.js.map +1 -0
- package/dist/src/formats/neo4j/importer.d.ts +73 -0
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -0
- package/dist/src/formats/neo4j/importer.js +932 -0
- package/dist/src/formats/neo4j/importer.js.map +1 -0
- package/dist/src/formats/neo4j/index.d.ts +79 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -0
- package/dist/src/formats/neo4j/index.js +83 -0
- package/dist/src/formats/neo4j/index.js.map +1 -0
- package/dist/src/formats/pajek/exporter.d.ts +58 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -0
- package/dist/src/formats/pajek/exporter.js +825 -0
- package/dist/src/formats/pajek/exporter.js.map +1 -0
- package/dist/src/formats/pajek/importer.d.ts +88 -0
- package/dist/src/formats/pajek/importer.d.ts.map +1 -0
- package/dist/src/formats/pajek/importer.js +1047 -0
- package/dist/src/formats/pajek/importer.js.map +1 -0
- package/dist/src/formats/pajek/index.d.ts +7 -0
- package/dist/src/formats/pajek/index.d.ts.map +1 -0
- package/dist/src/formats/pajek/index.js +7 -0
- package/dist/src/formats/pajek/index.js.map +1 -0
- package/dist/src/formats/pajek/syntax.d.ts +112 -0
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -0
- package/dist/src/formats/pajek/syntax.js +269 -0
- package/dist/src/formats/pajek/syntax.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/registry.d.ts +207 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +481 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/sniff.d.ts +104 -0
- package/dist/src/sniff.d.ts.map +1 -0
- package/dist/src/sniff.js +357 -0
- package/dist/src/sniff.js.map +1 -0
- package/dist/src/types.d.ts +238 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +29 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +122 -7
- package/src/children.ts +335 -0
- package/src/common/attributes.ts +520 -0
- package/src/common/codes.ts +153 -0
- package/src/common/declared-types.ts +374 -0
- package/src/common/direction.ts +518 -0
- package/src/common/escape.ts +231 -0
- package/src/common/export.ts +817 -0
- package/src/common/format.ts +111 -0
- package/src/common/ids.ts +176 -0
- package/src/common/input.ts +378 -0
- package/src/common/lists.ts +196 -0
- package/src/common/options.ts +377 -0
- package/src/common/report.ts +352 -0
- package/src/common/temporal.ts +302 -0
- package/src/common/text.ts +294 -0
- package/src/common/weights.ts +202 -0
- package/src/common/writer.ts +123 -0
- package/src/common/xml.ts +1053 -0
- package/src/formats/csv/exporter.ts +894 -0
- package/src/formats/csv/header.ts +172 -0
- package/src/formats/csv/importer.ts +1104 -0
- package/src/formats/csv/index.ts +88 -0
- package/src/formats/csv/records.ts +813 -0
- package/src/formats/csv/values.ts +224 -0
- package/src/formats/dot/exporter.ts +1014 -0
- package/src/formats/dot/importer.ts +1549 -0
- package/src/formats/dot/index.ts +7 -0
- package/src/formats/dot/names.ts +40 -0
- package/src/formats/dot/tokenizer.ts +384 -0
- package/src/formats/gexf/exporter.ts +1696 -0
- package/src/formats/gexf/importer.ts +2333 -0
- package/src/formats/gexf/index.ts +142 -0
- package/src/formats/gexf/schema.ts +361 -0
- package/src/formats/gml/exporter.ts +1404 -0
- package/src/formats/gml/importer.ts +1591 -0
- package/src/formats/gml/index.ts +128 -0
- package/src/formats/gml/syntax.ts +545 -0
- package/src/formats/graphml/constants.ts +225 -0
- package/src/formats/graphml/exporter.ts +1458 -0
- package/src/formats/graphml/importer.ts +2027 -0
- package/src/formats/graphml/index.ts +8 -0
- package/src/formats/graphml/tree.ts +318 -0
- package/src/formats/json/dialect.ts +317 -0
- package/src/formats/json/exporter.ts +1616 -0
- package/src/formats/json/importer.ts +2271 -0
- package/src/formats/json/index.ts +8 -0
- package/src/formats/neo4j/exporter.ts +1287 -0
- package/src/formats/neo4j/header.ts +156 -0
- package/src/formats/neo4j/importer.ts +1220 -0
- package/src/formats/neo4j/index.ts +116 -0
- package/src/formats/pajek/exporter.ts +1000 -0
- package/src/formats/pajek/importer.ts +1311 -0
- package/src/formats/pajek/index.ts +7 -0
- package/src/formats/pajek/syntax.ts +307 -0
- package/src/index.ts +244 -0
- package/src/registry.ts +617 -0
- package/src/sniff.ts +397 -0
- package/src/types.ts +262 -0
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Neo4j exporter (design section 8.5, research note 07 section 9): writes a snapshot as
|
|
3
|
+
* neo4j-admin import CSV -- node sections with `:ID`, `:LABEL` and typed property columns,
|
|
4
|
+
* relationship sections with `:START_ID`, `:END_ID`, `:TYPE`, the weight and typed property
|
|
5
|
+
* columns -- as one document (node sections first) or as the node or relationship part alone.
|
|
6
|
+
*
|
|
7
|
+
* What a property graph cannot carry is reported by check() before anything is written: an
|
|
8
|
+
* undirected snapshot (every edge becomes a directed relationship), expanded mixed direction (per
|
|
9
|
+
* `onMixedDirection`), edge ids, hierarchy, element lifetimes, graph attributes, nested json (points
|
|
10
|
+
* excepted), multi-component columns (flattened to arrays), u32 / u8 columns (written as `long` /
|
|
11
|
+
* `int`), ids whose text would re-import as another type or collide, and list items that contain
|
|
12
|
+
* the array delimiter.
|
|
13
|
+
*
|
|
14
|
+
* Column headers restore the declared Neo4j type from `origin.type` when it is compatible with the
|
|
15
|
+
* column's dtype (`int`, `byte`, `short`, `long`, `char`, `duration`, the temporal types, `point`,
|
|
16
|
+
* `type[]`), and derive it from the dtype otherwise. Temporal columns write their `.text`
|
|
17
|
+
* companion when set and the canonical ISO form otherwise. Nodes are grouped into sections by
|
|
18
|
+
* (id space, stored-id column) in index order, so a re-import restores the same node order;
|
|
19
|
+
* relationships likewise by (start space, end space).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
type Column,
|
|
24
|
+
type ColumnMeta,
|
|
25
|
+
type Dtype,
|
|
26
|
+
GraphFormatError,
|
|
27
|
+
type GraphSnapshot,
|
|
28
|
+
type NodeId,
|
|
29
|
+
type ScalarDtype,
|
|
30
|
+
} from "@graphty/graph-format";
|
|
31
|
+
|
|
32
|
+
import { ID_TEXT_COLLISION_CODE, ID_TEXT_TYPE_CODE } from "../../common/codes.js";
|
|
33
|
+
import { type DeclaredTypeSpec, mapDeclaredType } from "../../common/declared-types.js";
|
|
34
|
+
import { type PairFolding, pairFolding } from "../../common/direction.js";
|
|
35
|
+
import { quoteCsvCell } from "../../common/escape.js";
|
|
36
|
+
import { capabilities, checkCapabilities, LOSS } from "../../common/export.js";
|
|
37
|
+
import { formatF32, formatF64, formatInteger } from "../../common/format.js";
|
|
38
|
+
import { isCanonicalIntegerText } from "../../common/ids.js";
|
|
39
|
+
import { type ResolvedExportOptions, resolveExportOptions } from "../../common/options.js";
|
|
40
|
+
import { formatTemporal, type TemporalKind } from "../../common/temporal.js";
|
|
41
|
+
import { type ExplicitWeights, explicitWeights } from "../../common/weights.js";
|
|
42
|
+
import { encodeChunks, joinText } from "../../common/writer.js";
|
|
43
|
+
import { type CommonExportOptions, type ExportCapabilities, type GraphExporter, type LossNote } from "../../types.js";
|
|
44
|
+
import { checkRecordSyntax, type RecordSyntax } from "../csv/records.js";
|
|
45
|
+
import { formatHeaderField } from "./header.js";
|
|
46
|
+
import { ID_SPACE_COLUMN, LABELS_COLUMN, TYPE_COLUMN } from "./importer.js";
|
|
47
|
+
|
|
48
|
+
/** The format-specific options of the Neo4j exporter. */
|
|
49
|
+
export interface Neo4jExportOptions {
|
|
50
|
+
/** Which tables to write: both (node sections first; default), the node sections or the relationship sections. */
|
|
51
|
+
part?: "all" | "nodes" | "relationships" | undefined;
|
|
52
|
+
/** The field delimiter; one character; "," by default. */
|
|
53
|
+
delimiter?: string | undefined;
|
|
54
|
+
/** The array delimiter of list values and `:LABEL` cells; ";" by default. */
|
|
55
|
+
arrayDelimiter?: ";" | "," | "|" | undefined;
|
|
56
|
+
/** The quote character; one character; a double quote by default. */
|
|
57
|
+
quote?: string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The property that receives explicit edge weights (`<name>:double`); "weight" by default (the
|
|
60
|
+
* importer's `weightFrom` default); null writes no weights.
|
|
61
|
+
*/
|
|
62
|
+
weightColumn?: string | null | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* The property name of the `:ID` column for nodes that have no stored-id column of their own
|
|
65
|
+
* (`<name>:ID`); null (default) writes a bare `:ID`.
|
|
66
|
+
*/
|
|
67
|
+
idColumn?: string | null | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Loss code: undirected edges (an undirected snapshot, or the folded pairs of a mixed one) written as directed relationships. */
|
|
71
|
+
export const UNDIRECTED_LOSS = "W_NEO4J_UNDIRECTED_AS_DIRECTED";
|
|
72
|
+
|
|
73
|
+
/** Loss code: node ids whose text re-imports as another type under the canonical id rule. */
|
|
74
|
+
export const ID_TEXT_TYPE_LOSS = ID_TEXT_TYPE_CODE;
|
|
75
|
+
|
|
76
|
+
/** Loss code: two node ids share one text; export() throws E_INVALID_ID. */
|
|
77
|
+
export const ID_TEXT_COLLISION_LOSS = ID_TEXT_COLLISION_CODE;
|
|
78
|
+
|
|
79
|
+
/** Loss code: an edge property column already uses the weight column name; export() throws E_COLUMN_EXISTS. */
|
|
80
|
+
export const WEIGHT_COLUMN_TAKEN_LOSS = "E_NEO4J_WEIGHT_COLUMN_TAKEN";
|
|
81
|
+
|
|
82
|
+
/** Loss code: a node property column already uses the idColumn name; export() throws E_COLUMN_EXISTS. */
|
|
83
|
+
export const ID_COLUMN_TAKEN_LOSS = "E_NEO4J_ID_COLUMN_TAKEN";
|
|
84
|
+
|
|
85
|
+
/** Loss code: a node has more than one stored-id column set; only the first is written. */
|
|
86
|
+
export const MULTIPLE_ID_PROPERTIES_LOSS = "W_NEO4J_MULTIPLE_ID_PROPERTIES";
|
|
87
|
+
|
|
88
|
+
/** Loss code: a declared integer type holds non-integral values and is written as double. */
|
|
89
|
+
export const DECLARED_TYPE_CHANGED_LOSS = "W_NEO4J_DECLARED_TYPE_CHANGED";
|
|
90
|
+
|
|
91
|
+
/** Loss code: a list item contains the array delimiter, which Neo4j cannot escape. */
|
|
92
|
+
export const ARRAY_DELIMITER_LOSS = "W_NEO4J_ARRAY_DELIMITER";
|
|
93
|
+
|
|
94
|
+
/** The roles Neo4j has a slot for: `:TYPE`, `:LABEL` and the id space of `:ID(Space)`. */
|
|
95
|
+
const SLOT_ROLES: ReadonlySet<string> = new Set(["kind", "labels", "idSpace"]);
|
|
96
|
+
|
|
97
|
+
const NEO4J = "neo4j";
|
|
98
|
+
const ID_TYPE = "ID";
|
|
99
|
+
|
|
100
|
+
/** The weight property the importer reads by default (its weightFrom default). */
|
|
101
|
+
const DEFAULT_WEIGHT_COLUMN = "weight";
|
|
102
|
+
|
|
103
|
+
/** The names the importer gives the slot columns, for the name-change notes. */
|
|
104
|
+
const ROLE_NAMES: Readonly<Record<string, string>> = Object.freeze({
|
|
105
|
+
kind: TYPE_COLUMN,
|
|
106
|
+
labels: LABELS_COLUMN,
|
|
107
|
+
idSpace: ID_SPACE_COLUMN,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The capabilities of neo4j-admin CSV (research note 07 section 9): declared scalar types and
|
|
112
|
+
* arrays; a dict column reads back as string (the header has no enumeration type); a position or
|
|
113
|
+
* visual column is written as a plain property (a point for a 2- or 3-component position) and
|
|
114
|
+
* reads back without its role.
|
|
115
|
+
*/
|
|
116
|
+
export const NEO4J_CAPABILITIES: ExportCapabilities = capabilities({
|
|
117
|
+
mixedDirection: false,
|
|
118
|
+
multiEdges: true,
|
|
119
|
+
selfLoops: true,
|
|
120
|
+
edgeIds: "none",
|
|
121
|
+
idCharset: "any",
|
|
122
|
+
dtypes: ["f32", "f64", "i32", "bool", "string"],
|
|
123
|
+
components: false,
|
|
124
|
+
lists: true,
|
|
125
|
+
json: false,
|
|
126
|
+
defaults: false,
|
|
127
|
+
options: false,
|
|
128
|
+
hierarchy: false,
|
|
129
|
+
temporal: "none",
|
|
130
|
+
graphAttributes: false,
|
|
131
|
+
positions: false,
|
|
132
|
+
viz: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/** Roles whose columns are never written as properties. */
|
|
136
|
+
const SKIPPED_ROLES: ReadonlySet<string> = new Set([
|
|
137
|
+
"directed",
|
|
138
|
+
"pair",
|
|
139
|
+
"mutual",
|
|
140
|
+
"weight",
|
|
141
|
+
"timeText",
|
|
142
|
+
"originalId",
|
|
143
|
+
"parent",
|
|
144
|
+
"parents",
|
|
145
|
+
"start",
|
|
146
|
+
"end",
|
|
147
|
+
"timestamp",
|
|
148
|
+
"timestamps",
|
|
149
|
+
"spells",
|
|
150
|
+
"open",
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
/** Notes about relationships only, dropped when only nodes are written. */
|
|
154
|
+
const EDGE_NOTE_CODES: ReadonlySet<string> = new Set([
|
|
155
|
+
LOSS.MIXED_DIRECTION,
|
|
156
|
+
LOSS.MIXED_DIRECTION_ERROR,
|
|
157
|
+
LOSS.MULTI_EDGES,
|
|
158
|
+
LOSS.SELF_LOOPS,
|
|
159
|
+
LOSS.EDGE_IDS_GENERATED,
|
|
160
|
+
LOSS.EDGE_IDS_DROPPED,
|
|
161
|
+
UNDIRECTED_LOSS,
|
|
162
|
+
WEIGHT_COLUMN_TAKEN_LOSS,
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
/** Notes about nodes only, dropped when only relationships are written. */
|
|
166
|
+
const NODE_NOTE_CODES: ReadonlySet<string> = new Set([
|
|
167
|
+
LOSS.ID_MANGLED,
|
|
168
|
+
LOSS.ID_CHARSET,
|
|
169
|
+
LOSS.ID_RENUMBERED,
|
|
170
|
+
ID_TEXT_TYPE_LOSS,
|
|
171
|
+
ID_COLUMN_TAKEN_LOSS,
|
|
172
|
+
MULTIPLE_ID_PROPERTIES_LOSS,
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const ARRAY_SYNTAXES: ReadonlySet<string> = new Set([";", ",", "|"]);
|
|
176
|
+
const PARTS: ReadonlySet<string> = new Set(["all", "nodes", "relationships"]);
|
|
177
|
+
|
|
178
|
+
/** The resolved format options. */
|
|
179
|
+
interface ResolvedNeo4jExportOptions {
|
|
180
|
+
readonly part: "all" | "nodes" | "relationships";
|
|
181
|
+
readonly syntax: RecordSyntax & { readonly delimiter: string };
|
|
182
|
+
readonly arrayDelimiter: string;
|
|
183
|
+
readonly weightColumn: string | null;
|
|
184
|
+
readonly idColumn: string | null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** How one cell's value is written. */
|
|
188
|
+
type CellKind = "integer" | "float" | "double" | "boolean" | "string" | "temporal" | "point" | "json";
|
|
189
|
+
|
|
190
|
+
/** The Neo4j type and cell kind of one scalar (a column or a list item). */
|
|
191
|
+
interface ScalarPlan {
|
|
192
|
+
readonly type: string;
|
|
193
|
+
readonly kind: CellKind;
|
|
194
|
+
readonly temporal: TemporalKind | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** One property column as it will be written. */
|
|
198
|
+
interface ColumnPlan {
|
|
199
|
+
readonly column: Column;
|
|
200
|
+
readonly header: string;
|
|
201
|
+
readonly scalar: ScalarPlan;
|
|
202
|
+
/** A list (or flattened vector) column: items are joined by the array delimiter. */
|
|
203
|
+
readonly list: boolean;
|
|
204
|
+
/** The companion text column of a temporal column, or null. */
|
|
205
|
+
readonly companion: Column | null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve the format options.
|
|
210
|
+
* @param options - the caller's options
|
|
211
|
+
* @returns the resolved options; E_UNSUPPORTED for an invalid value
|
|
212
|
+
*/
|
|
213
|
+
function resolveNeo4jExportOptions(options: Neo4jExportOptions | undefined): ResolvedNeo4jExportOptions {
|
|
214
|
+
const o: Neo4jExportOptions = options ?? {};
|
|
215
|
+
const part = o.part ?? "all";
|
|
216
|
+
if (!PARTS.has(part)) {
|
|
217
|
+
throw new GraphFormatError(
|
|
218
|
+
"E_UNSUPPORTED",
|
|
219
|
+
`option part: ${JSON.stringify(part)} is not "all", "nodes" or "relationships"`,
|
|
220
|
+
{
|
|
221
|
+
option: "part",
|
|
222
|
+
found: part,
|
|
223
|
+
},
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const arrayDelimiter = o.arrayDelimiter ?? ";";
|
|
227
|
+
if (!ARRAY_SYNTAXES.has(arrayDelimiter)) {
|
|
228
|
+
throw new GraphFormatError(
|
|
229
|
+
"E_UNSUPPORTED",
|
|
230
|
+
`option arrayDelimiter: ${JSON.stringify(arrayDelimiter)} is not one of ";", ",", "|"`,
|
|
231
|
+
{ option: "arrayDelimiter", found: arrayDelimiter },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const delimiter = o.delimiter ?? ",";
|
|
235
|
+
const syntax = { ...checkRecordSyntax({ delimiter, quote: o.quote ?? '"' }), delimiter };
|
|
236
|
+
if (syntax.delimiter === arrayDelimiter) {
|
|
237
|
+
throw new GraphFormatError("E_UNSUPPORTED", "options delimiter and arrayDelimiter must differ", {
|
|
238
|
+
option: "arrayDelimiter",
|
|
239
|
+
found: arrayDelimiter,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const weightColumn = o.weightColumn === undefined ? "weight" : o.weightColumn;
|
|
243
|
+
if (weightColumn !== null && (typeof weightColumn !== "string" || weightColumn.length === 0)) {
|
|
244
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option weightColumn: expected a non-empty name or null", {
|
|
245
|
+
option: "weightColumn",
|
|
246
|
+
found: weightColumn,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const idColumn = o.idColumn ?? null;
|
|
250
|
+
if (idColumn !== null && (typeof idColumn !== "string" || idColumn.length === 0)) {
|
|
251
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option idColumn: expected a non-empty name or null", {
|
|
252
|
+
option: "idColumn",
|
|
253
|
+
found: idColumn,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return { part, syntax, arrayDelimiter, weightColumn, idColumn };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Everything check() and export() share: the column plans, the section keys, the notes and the
|
|
261
|
+
* error export() throws (if any), computed once from the snapshot and the options.
|
|
262
|
+
*/
|
|
263
|
+
class ExportPlan {
|
|
264
|
+
readonly snapshot: GraphSnapshot;
|
|
265
|
+
|
|
266
|
+
readonly options: ResolvedNeo4jExportOptions;
|
|
267
|
+
|
|
268
|
+
readonly common: ResolvedExportOptions;
|
|
269
|
+
|
|
270
|
+
/** The notes: the generic capability notes first, then the format's own, in that order. */
|
|
271
|
+
readonly notes: LossNote[] = [];
|
|
272
|
+
|
|
273
|
+
/** The format's own notes, appended after the generic ones. */
|
|
274
|
+
private readonly ownNotes: LossNote[] = [];
|
|
275
|
+
|
|
276
|
+
/** The error export() throws because of the node sections, or null. */
|
|
277
|
+
private fatalNodes: GraphFormatError | null = null;
|
|
278
|
+
|
|
279
|
+
/** The error export() throws because of the relationship sections, or null. */
|
|
280
|
+
private fatalEdges: GraphFormatError | null = null;
|
|
281
|
+
|
|
282
|
+
/** The error export() throws whatever the part (an id text collision), or null. */
|
|
283
|
+
private fatalAll: GraphFormatError | null = null;
|
|
284
|
+
|
|
285
|
+
readonly nodeColumns: ColumnPlan[] = [];
|
|
286
|
+
|
|
287
|
+
readonly edgeColumns: ColumnPlan[] = [];
|
|
288
|
+
|
|
289
|
+
/** Stored-id node columns (`name:ID`), in declaration order. */
|
|
290
|
+
readonly idColumns: Column[] = [];
|
|
291
|
+
|
|
292
|
+
readonly idSpace: Column | null;
|
|
293
|
+
|
|
294
|
+
readonly labels: Column | null;
|
|
295
|
+
|
|
296
|
+
readonly kind: Column | null;
|
|
297
|
+
|
|
298
|
+
readonly weights: ExplicitWeights | null;
|
|
299
|
+
|
|
300
|
+
/** The pair folding (design section 3.6): the mirror halves of undirected pairs are not written. */
|
|
301
|
+
readonly folding: PairFolding;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Build the plan.
|
|
305
|
+
* @param snapshot - the snapshot
|
|
306
|
+
* @param options - the resolved format options
|
|
307
|
+
* @param common - the resolved common options
|
|
308
|
+
*/
|
|
309
|
+
constructor(snapshot: GraphSnapshot, options: ResolvedNeo4jExportOptions, common: ResolvedExportOptions) {
|
|
310
|
+
this.snapshot = snapshot;
|
|
311
|
+
this.options = options;
|
|
312
|
+
this.common = common;
|
|
313
|
+
this.idSpace = snapshot.nodes.byRole("idSpace");
|
|
314
|
+
this.labels = snapshot.nodes.byRole("labels");
|
|
315
|
+
this.kind = snapshot.edges.byRole("kind");
|
|
316
|
+
for (const column of snapshot.nodes) {
|
|
317
|
+
if (isStoredId(column.meta)) {
|
|
318
|
+
this.idColumns.push(column);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const generic = checkCapabilities(snapshot, NEO4J_CAPABILITIES, common, {
|
|
322
|
+
roles: SLOT_ROLES,
|
|
323
|
+
roleNames: ROLE_NAMES,
|
|
324
|
+
temporalText: true,
|
|
325
|
+
});
|
|
326
|
+
this.planColumns("node", snapshot.nodes, this.nodeColumns);
|
|
327
|
+
this.planColumns("edge", snapshot.edges, this.edgeColumns);
|
|
328
|
+
this.weights = this.planWeightSource();
|
|
329
|
+
this.folding = pairFolding(snapshot);
|
|
330
|
+
this.checkIds();
|
|
331
|
+
this.checkIdColumn();
|
|
332
|
+
this.checkStoredIds();
|
|
333
|
+
this.directionNotes();
|
|
334
|
+
const slots = new Set([this.kind, this.labels, this.idSpace].flatMap((c) => (c === null ? [] : [c.meta.name])));
|
|
335
|
+
for (const gen of generic) {
|
|
336
|
+
if (gen.code === LOSS.JSON && gen.column !== null && this.writtenAsPoint(gen.column)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (gen.code === LOSS.DTYPE && gen.column !== null && slots.has(gen.column)) {
|
|
340
|
+
// :TYPE, :LABEL and :ID(Space) read back as the dict / list-of-dict columns they were
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (gen.code === LOSS.MIXED_DIRECTION_ERROR && this.fatalEdges === null) {
|
|
344
|
+
this.fatalEdges = new GraphFormatError("E_DIRECTED", gen.message, { reason: "mixed direction" });
|
|
345
|
+
}
|
|
346
|
+
if ((gen.code === LOSS.POSITIONS || gen.code === LOSS.VIZ) && gen.column !== null) {
|
|
347
|
+
// written as a plain property (a point for a position); the role is what is lost
|
|
348
|
+
this.notes.push(
|
|
349
|
+
Object.freeze({
|
|
350
|
+
code: gen.code,
|
|
351
|
+
message: `node column "${gen.column}" is written as a plain property; its role is lost on re-import`,
|
|
352
|
+
column: gen.column,
|
|
353
|
+
count: gen.count,
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
this.notes.push(gen);
|
|
359
|
+
}
|
|
360
|
+
this.notes.push(...this.ownNotes);
|
|
361
|
+
this.filterNotesByPart();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The direction notes: an undirected snapshot writes every edge as a directed relationship;
|
|
366
|
+
* a mixed snapshot folds its undirected pairs to one directed relationship each under
|
|
367
|
+
* "directed" or "undirected" (Neo4j has no undirected relationship, so both policies write
|
|
368
|
+
* the same file and the generic W_MIXED_DIRECTION note names the policy); a mutual pair is
|
|
369
|
+
* written as two relationships without its mark.
|
|
370
|
+
*/
|
|
371
|
+
private directionNotes(): void {
|
|
372
|
+
const { snapshot, folding } = this;
|
|
373
|
+
if (!snapshot.directed) {
|
|
374
|
+
this.note(
|
|
375
|
+
UNDIRECTED_LOSS,
|
|
376
|
+
`the snapshot is undirected; every edge is written as a directed relationship (${snapshot.edgeCount} edge(s))`,
|
|
377
|
+
null,
|
|
378
|
+
snapshot.edgeCount,
|
|
379
|
+
);
|
|
380
|
+
} else if (this.common.onMixedDirection !== "error") {
|
|
381
|
+
let undirected = 0;
|
|
382
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
383
|
+
if (!folding.folded(e) && !folding.sourceDirected(e)) {
|
|
384
|
+
undirected++;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (undirected > 0) {
|
|
388
|
+
this.note(
|
|
389
|
+
UNDIRECTED_LOSS,
|
|
390
|
+
`${undirected} undirected edge(s) are written as one directed relationship each (source to target, a pair folded to its primary); Neo4j has no undirected relationship`,
|
|
391
|
+
null,
|
|
392
|
+
undirected,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (folding.mutualCount > 0) {
|
|
397
|
+
this.note(
|
|
398
|
+
LOSS.MUTUAL_EXPANDED,
|
|
399
|
+
`${folding.mutualCount} mutual pair(s) are written as two directed relationships; the mutual mark is lost`,
|
|
400
|
+
null,
|
|
401
|
+
folding.mutualCount,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Record a note.
|
|
408
|
+
* @param code - the code
|
|
409
|
+
* @param message - the message
|
|
410
|
+
* @param column - the column, or null
|
|
411
|
+
* @param count - the count, or null
|
|
412
|
+
*/
|
|
413
|
+
private note(code: string, message: string, column: string | null = null, count: number | null = null): void {
|
|
414
|
+
this.ownNotes.push(Object.freeze({ code, message, column, count }));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Plan the property columns of one table.
|
|
419
|
+
* @param domain - node or edge
|
|
420
|
+
* @param table - the table
|
|
421
|
+
* @param out - receives the plans in declaration order
|
|
422
|
+
*/
|
|
423
|
+
private planColumns(domain: "node" | "edge", table: Iterable<Column>, out: ColumnPlan[]): void {
|
|
424
|
+
const all = [...table];
|
|
425
|
+
const names = new Set(all.map((column) => column.meta.name));
|
|
426
|
+
const companionFor = new Map<string, Column>();
|
|
427
|
+
const companions = new Set<Column>();
|
|
428
|
+
for (const column of all) {
|
|
429
|
+
const target = column.meta.extra.for;
|
|
430
|
+
if (column.dtype === "string" && typeof target === "string" && names.has(target)) {
|
|
431
|
+
companionFor.set(target, column);
|
|
432
|
+
companions.add(column);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const column of all) {
|
|
436
|
+
const { meta } = column;
|
|
437
|
+
const { role } = meta;
|
|
438
|
+
if (companions.has(column) || (role !== null && SKIPPED_ROLES.has(role))) {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (domain === "node" && (isStoredId(meta) || column === this.idSpace || column === this.labels)) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (domain === "edge" && (role === "id" || column === this.kind)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
out.push(this.planColumn(column, companionFor.get(meta.name) ?? null));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Plan one property column: its header type, cell kind and notes.
|
|
453
|
+
* @param column - the column
|
|
454
|
+
* @param companion - its text companion, or null
|
|
455
|
+
* @returns the plan
|
|
456
|
+
*/
|
|
457
|
+
private planColumn(column: Column, companion: Column | null): ColumnPlan {
|
|
458
|
+
const { meta } = column;
|
|
459
|
+
const declared = meta.origin?.type ?? null;
|
|
460
|
+
const mapped = declared === null ? null : mapDeclaredType(NEO4J, declared, "f64");
|
|
461
|
+
const label = `${meta.domain} column "${meta.name}"`;
|
|
462
|
+
if (meta.dtype === "list") {
|
|
463
|
+
const itemDtype = meta.itemDtype ?? "string";
|
|
464
|
+
const compatible =
|
|
465
|
+
mapped !== null && mapped.list && mapped.itemDtype === itemDtype && mapped.kind !== "long";
|
|
466
|
+
let scalar = compatible
|
|
467
|
+
? { type: declaredScalar(declared as string), kind: kindOf(mapped), temporal: mapped.temporal }
|
|
468
|
+
: defaultScalar(itemDtype);
|
|
469
|
+
if (mapped !== null && mapped.list && mapped.kind === "long" && itemDtype === "f64") {
|
|
470
|
+
scalar = this.integralOrDouble(column, declaredScalar(declared as string), label);
|
|
471
|
+
}
|
|
472
|
+
const itemComponents = meta.itemComponents ?? 1;
|
|
473
|
+
if (itemComponents > 1) {
|
|
474
|
+
this.note(
|
|
475
|
+
LOSS.COMPONENTS,
|
|
476
|
+
`${label} holds items of ${itemComponents} components; they are flattened into one array`,
|
|
477
|
+
meta.name,
|
|
478
|
+
column.length - column.nullCount,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
this.checkArrayItems(column, scalar, label);
|
|
482
|
+
return {
|
|
483
|
+
column,
|
|
484
|
+
header: formatHeaderField(meta.name, `${scalar.type}[]`, null),
|
|
485
|
+
scalar,
|
|
486
|
+
list: true,
|
|
487
|
+
companion,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
let scalar: ScalarPlan;
|
|
491
|
+
const compatible = mapped !== null && !mapped.list && mapped.dtype === meta.dtype;
|
|
492
|
+
if (compatible && mapped.kind === "long") {
|
|
493
|
+
scalar = this.integralOrDouble(column, declared as string, label);
|
|
494
|
+
} else if (compatible) {
|
|
495
|
+
scalar = { type: declared as string, kind: kindOf(mapped), temporal: mapped.temporal };
|
|
496
|
+
} else {
|
|
497
|
+
scalar = defaultScalar(meta.dtype);
|
|
498
|
+
}
|
|
499
|
+
// the generic check treats position and visual columns by role only; their dtype and
|
|
500
|
+
// stride are checked here so the notes match those of any other column
|
|
501
|
+
const visual = meta.role === "position" || (meta.role !== null && isVizRole(meta.role));
|
|
502
|
+
if (visual && meta.dtype === "json" && scalar.kind !== "point") {
|
|
503
|
+
this.note(
|
|
504
|
+
LOSS.JSON,
|
|
505
|
+
`${label} holds nested values; the format has no nested values`,
|
|
506
|
+
meta.name,
|
|
507
|
+
column.length - column.nullCount,
|
|
508
|
+
);
|
|
509
|
+
} else if (visual && meta.dtype !== "json" && !NEO4J_CAPABILITIES.dtypes.includes(meta.dtype)) {
|
|
510
|
+
this.note(
|
|
511
|
+
LOSS.DTYPE,
|
|
512
|
+
`${label} is ${meta.dtype}; the format cannot keep that dtype`,
|
|
513
|
+
meta.name,
|
|
514
|
+
column.length - column.nullCount,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (meta.components > 1) {
|
|
518
|
+
if (visual) {
|
|
519
|
+
this.note(
|
|
520
|
+
LOSS.COMPONENTS,
|
|
521
|
+
`${label} has ${meta.components} components; the format has no strides`,
|
|
522
|
+
meta.name,
|
|
523
|
+
column.length - column.nullCount,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
column,
|
|
528
|
+
header: formatHeaderField(meta.name, `${scalar.type}[]`, null),
|
|
529
|
+
scalar,
|
|
530
|
+
list: true,
|
|
531
|
+
companion,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// an untyped Neo4j header (`name` alone) is a string property; restore it as written
|
|
535
|
+
const untyped = declared === null && meta.origin?.format === NEO4J && meta.dtype === "string";
|
|
536
|
+
const header = untyped ? meta.name : formatHeaderField(meta.name, scalar.type, null);
|
|
537
|
+
return { column, header, scalar, list: false, companion };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The plan of an f64 column declared with an integer type: kept when every set value is
|
|
542
|
+
* integral, written as double (with a note) otherwise.
|
|
543
|
+
* @param column - the column
|
|
544
|
+
* @param declared - the declared type text
|
|
545
|
+
* @param label - the column label for messages
|
|
546
|
+
* @returns the scalar plan
|
|
547
|
+
*/
|
|
548
|
+
private integralOrDouble(column: Column, declared: string, label: string): ScalarPlan {
|
|
549
|
+
let integral = true;
|
|
550
|
+
if (column.dtype === "f64") {
|
|
551
|
+
for (let r = 0; r < column.length && integral; r++) {
|
|
552
|
+
if (column.isSet(r) && !Number.isInteger(column.data[r])) {
|
|
553
|
+
integral = false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
} else if (column.dtype === "list" && column.child.dtype === "f64") {
|
|
557
|
+
const { data } = column.child;
|
|
558
|
+
for (let i = 0; i < data.length && integral; i++) {
|
|
559
|
+
if (!Number.isInteger(data[i])) {
|
|
560
|
+
integral = false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (integral) {
|
|
565
|
+
return { type: declared, kind: "integer", temporal: null };
|
|
566
|
+
}
|
|
567
|
+
this.note(
|
|
568
|
+
DECLARED_TYPE_CHANGED_LOSS,
|
|
569
|
+
`${label} is declared ${declared} but holds non-integral values; written as double`,
|
|
570
|
+
column.meta.name,
|
|
571
|
+
null,
|
|
572
|
+
);
|
|
573
|
+
return { type: "double", kind: "double", temporal: null };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Count the rows of a list column with an item containing the array delimiter.
|
|
578
|
+
* @param column - the list column
|
|
579
|
+
* @param scalar - the item plan
|
|
580
|
+
* @param label - the column label
|
|
581
|
+
*/
|
|
582
|
+
private checkArrayItems(column: Column, scalar: ScalarPlan, label: string): void {
|
|
583
|
+
if (
|
|
584
|
+
column.dtype !== "list" ||
|
|
585
|
+
(scalar.kind !== "string" && scalar.kind !== "json" && scalar.kind !== "point")
|
|
586
|
+
) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const { arrayDelimiter } = this.options;
|
|
590
|
+
let rows = 0;
|
|
591
|
+
for (let r = 0; r < column.length; r++) {
|
|
592
|
+
if (!column.isSet(r)) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
for (const item of column.sliceOf(r)) {
|
|
596
|
+
if (formatScalar(item, scalar, null).includes(arrayDelimiter)) {
|
|
597
|
+
rows++;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (rows > 0) {
|
|
603
|
+
this.note(
|
|
604
|
+
ARRAY_DELIMITER_LOSS,
|
|
605
|
+
`${label}: ${rows} row(s) hold an item containing the array delimiter "${arrayDelimiter}", which Neo4j cannot escape`,
|
|
606
|
+
column.meta.name,
|
|
607
|
+
rows,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* The explicit weights (design section 3.7), and the weight-column name check.
|
|
614
|
+
* @returns the weights, or null when no weights are written
|
|
615
|
+
*/
|
|
616
|
+
private planWeightSource(): ExplicitWeights | null {
|
|
617
|
+
const { snapshot } = this;
|
|
618
|
+
const name = this.options.weightColumn;
|
|
619
|
+
if (name === null) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
const weights = explicitWeights(snapshot);
|
|
623
|
+
const taken = this.edgeColumns.find((plan) => plan.column.meta.name === name);
|
|
624
|
+
if (taken !== undefined && weights.weighted) {
|
|
625
|
+
this.note(
|
|
626
|
+
WEIGHT_COLUMN_TAKEN_LOSS,
|
|
627
|
+
`edge column "${name}" already exists; explicit weights cannot be written under weightColumn "${name}"`,
|
|
628
|
+
name,
|
|
629
|
+
null,
|
|
630
|
+
);
|
|
631
|
+
if (this.fatalEdges === null) {
|
|
632
|
+
this.fatalEdges = new GraphFormatError(
|
|
633
|
+
"E_COLUMN_EXISTS",
|
|
634
|
+
`edge column "${name}" already exists; choose another weightColumn`,
|
|
635
|
+
{ column: name, domain: "edge" },
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
} else if (taken !== undefined && name === DEFAULT_WEIGHT_COLUMN) {
|
|
639
|
+
this.note(
|
|
640
|
+
LOSS.WEIGHT_KEY_CLASH,
|
|
641
|
+
`edge column "${name}" is written under the property the importer reads THE weight from (weightFrom "${name}"); it reads back as the weight, not as a column`,
|
|
642
|
+
name,
|
|
643
|
+
taken.column.length - taken.column.nullCount,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
return weights.weighted ? weights : null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Check the node ids' text forms: type changes under the canonical rule and collisions. */
|
|
650
|
+
private checkIds(): void {
|
|
651
|
+
const { ids } = this.snapshot;
|
|
652
|
+
let typeChanges = 0;
|
|
653
|
+
let collisions = 0;
|
|
654
|
+
switch (ids.kind) {
|
|
655
|
+
case "identity":
|
|
656
|
+
case "dense":
|
|
657
|
+
break;
|
|
658
|
+
case "numeric":
|
|
659
|
+
for (let i = 0; i < ids.size; i++) {
|
|
660
|
+
if (!Number.isSafeInteger(ids.idOf(i))) {
|
|
661
|
+
typeChanges++;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
case "string":
|
|
666
|
+
for (let i = 0; i < ids.size; i++) {
|
|
667
|
+
if (isCanonicalIntegerText(String(ids.idOf(i)))) {
|
|
668
|
+
typeChanges++;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
case "mixed": {
|
|
673
|
+
const seen = new Set<string>();
|
|
674
|
+
for (let i = 0; i < ids.size; i++) {
|
|
675
|
+
const id = ids.idOf(i);
|
|
676
|
+
const text = String(id);
|
|
677
|
+
if (typeof id === "number" ? !Number.isSafeInteger(id) : isCanonicalIntegerText(text)) {
|
|
678
|
+
typeChanges++;
|
|
679
|
+
}
|
|
680
|
+
if (seen.has(text)) {
|
|
681
|
+
collisions++;
|
|
682
|
+
} else {
|
|
683
|
+
seen.add(text);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
default: {
|
|
689
|
+
const name: string = ids.kind;
|
|
690
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${name}`, { kind: name });
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (typeChanges > 0) {
|
|
694
|
+
this.note(
|
|
695
|
+
ID_TEXT_TYPE_LOSS,
|
|
696
|
+
`${typeChanges} node id(s) re-import as another type under the canonical id rule (a string of canonical integer text, or a non-integer number)`,
|
|
697
|
+
null,
|
|
698
|
+
typeChanges,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
if (collisions > 0) {
|
|
702
|
+
this.note(
|
|
703
|
+
ID_TEXT_COLLISION_LOSS,
|
|
704
|
+
`${collisions} node id(s) share their text with another id (a number and a string); export() will throw`,
|
|
705
|
+
null,
|
|
706
|
+
collisions,
|
|
707
|
+
);
|
|
708
|
+
this.fatalAll = new GraphFormatError(
|
|
709
|
+
"E_INVALID_ID",
|
|
710
|
+
`${collisions} node id(s) share their text with another id; Neo4j ids are text`,
|
|
711
|
+
{ reason: "text collision", count: collisions },
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** Check that the idColumn option does not name an existing node property. */
|
|
717
|
+
private checkIdColumn(): void {
|
|
718
|
+
const name = this.options.idColumn;
|
|
719
|
+
if (name === null) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const taken =
|
|
723
|
+
this.nodeColumns.some((plan) => plan.column.meta.name === name) ||
|
|
724
|
+
this.idColumns.some((column) => column.meta.name === name);
|
|
725
|
+
if (!taken) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
this.note(
|
|
729
|
+
ID_COLUMN_TAKEN_LOSS,
|
|
730
|
+
`node column "${name}" already exists; the :ID column cannot be named idColumn "${name}"`,
|
|
731
|
+
name,
|
|
732
|
+
null,
|
|
733
|
+
);
|
|
734
|
+
if (this.fatalNodes === null) {
|
|
735
|
+
this.fatalNodes = new GraphFormatError(
|
|
736
|
+
"E_COLUMN_EXISTS",
|
|
737
|
+
`node column "${name}" already exists; choose another idColumn`,
|
|
738
|
+
{ column: name, domain: "node" },
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Count the nodes with more than one stored-id column set. */
|
|
744
|
+
private checkStoredIds(): void {
|
|
745
|
+
if (this.idColumns.length < 2) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
let count = 0;
|
|
749
|
+
for (let i = 0; i < this.snapshot.nodeCount; i++) {
|
|
750
|
+
let set = 0;
|
|
751
|
+
for (const column of this.idColumns) {
|
|
752
|
+
if (column.isSet(i)) {
|
|
753
|
+
set++;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (set > 1) {
|
|
757
|
+
count++;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (count > 0) {
|
|
761
|
+
this.note(
|
|
762
|
+
MULTIPLE_ID_PROPERTIES_LOSS,
|
|
763
|
+
`${count} node(s) have more than one stored-id column set; only the first in declaration order is written`,
|
|
764
|
+
null,
|
|
765
|
+
count,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Whether a json column is written as Neo4j points (so the generic nested-value note does not apply).
|
|
772
|
+
* @param name - the column name
|
|
773
|
+
* @returns true when its plan writes point literals
|
|
774
|
+
*/
|
|
775
|
+
private writtenAsPoint(name: string): boolean {
|
|
776
|
+
for (const plan of [...this.nodeColumns, ...this.edgeColumns]) {
|
|
777
|
+
if (plan.column.meta.name === name && plan.scalar.kind === "point") {
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Drop the notes about the part that is not written. */
|
|
785
|
+
private filterNotesByPart(): void {
|
|
786
|
+
const { part } = this.options;
|
|
787
|
+
if (part === "all") {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const { snapshot } = this;
|
|
791
|
+
const keep = this.notes.filter((n) => {
|
|
792
|
+
if (part === "nodes") {
|
|
793
|
+
if (EDGE_NOTE_CODES.has(n.code)) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
return n.column === null || !snapshot.edges.has(n.column) || snapshot.nodes.has(n.column);
|
|
797
|
+
}
|
|
798
|
+
if (NODE_NOTE_CODES.has(n.code)) {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
return n.column === null || !snapshot.nodes.has(n.column) || snapshot.edges.has(n.column);
|
|
802
|
+
});
|
|
803
|
+
this.notes.length = 0;
|
|
804
|
+
this.notes.push(...keep);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* The error export() throws for the requested part, or null when the export can proceed.
|
|
809
|
+
* @returns the error
|
|
810
|
+
*/
|
|
811
|
+
get fatal(): GraphFormatError | null {
|
|
812
|
+
if (this.fatalAll !== null) {
|
|
813
|
+
return this.fatalAll;
|
|
814
|
+
}
|
|
815
|
+
const { part } = this.options;
|
|
816
|
+
if (part !== "relationships" && this.fatalNodes !== null) {
|
|
817
|
+
return this.fatalNodes;
|
|
818
|
+
}
|
|
819
|
+
return part === "nodes" ? null : this.fatalEdges;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* The id space of a node.
|
|
824
|
+
* @param index - the node index
|
|
825
|
+
* @returns the space name, or null
|
|
826
|
+
*/
|
|
827
|
+
spaceOf(index: number): string | null {
|
|
828
|
+
const { idSpace } = this;
|
|
829
|
+
if (idSpace === null || !idSpace.isSet(index)) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
const value = idSpace.value(index);
|
|
833
|
+
return typeof value === "string" ? value : String(value);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* The stored-id column of a node: the first one set in declaration order.
|
|
838
|
+
* @param index - the node index
|
|
839
|
+
* @returns the column, or null
|
|
840
|
+
*/
|
|
841
|
+
storedIdOf(index: number): Column | null {
|
|
842
|
+
for (const column of this.idColumns) {
|
|
843
|
+
if (column.isSet(index)) {
|
|
844
|
+
return column;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* The node sections, then the relationship sections, as CSV lines.
|
|
852
|
+
* @yields one line at a time (with its line break)
|
|
853
|
+
* @returns nothing
|
|
854
|
+
*/
|
|
855
|
+
*lines(): Generator<string, void, undefined> {
|
|
856
|
+
if (this.fatal !== null) {
|
|
857
|
+
throw this.fatal;
|
|
858
|
+
}
|
|
859
|
+
const { part } = this.options;
|
|
860
|
+
if (part !== "relationships") {
|
|
861
|
+
yield* this.nodeLines();
|
|
862
|
+
}
|
|
863
|
+
if (part !== "nodes") {
|
|
864
|
+
yield* this.relationshipLines();
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* The node sections.
|
|
870
|
+
* @yields one line at a time
|
|
871
|
+
* @returns nothing
|
|
872
|
+
*/
|
|
873
|
+
private *nodeLines(): Generator<string, void, undefined> {
|
|
874
|
+
const { snapshot, labels, nodeColumns } = this;
|
|
875
|
+
const { ids } = snapshot;
|
|
876
|
+
const { delimiter } = this.options.syntax;
|
|
877
|
+
const propertyHeaders = nodeColumns.map((plan) => plan.header).join(delimiter);
|
|
878
|
+
let sections = 0;
|
|
879
|
+
let sectionSpace: string | null = null;
|
|
880
|
+
let sectionIdName: string | null = null;
|
|
881
|
+
const cells: string[] = [];
|
|
882
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
883
|
+
const space = this.spaceOf(i);
|
|
884
|
+
const stored = this.storedIdOf(i);
|
|
885
|
+
const idName = stored === null ? (this.options.idColumn ?? "") : stored.meta.name;
|
|
886
|
+
if (sections === 0 || space !== sectionSpace || idName !== sectionIdName) {
|
|
887
|
+
sections++;
|
|
888
|
+
sectionSpace = space;
|
|
889
|
+
sectionIdName = idName;
|
|
890
|
+
const header = [formatHeaderField(idName, ID_TYPE, space)];
|
|
891
|
+
if (labels !== null) {
|
|
892
|
+
header.push(":LABEL");
|
|
893
|
+
}
|
|
894
|
+
if (propertyHeaders.length > 0) {
|
|
895
|
+
header.push(propertyHeaders);
|
|
896
|
+
}
|
|
897
|
+
yield `${header.join(delimiter)}\n`;
|
|
898
|
+
}
|
|
899
|
+
cells.length = 0;
|
|
900
|
+
cells.push(this.cell(idText(ids.idOf(i))));
|
|
901
|
+
if (labels !== null) {
|
|
902
|
+
cells.push(this.cell(this.labelsText(i)));
|
|
903
|
+
}
|
|
904
|
+
for (const plan of nodeColumns) {
|
|
905
|
+
cells.push(this.cell(this.valueText(plan, i)));
|
|
906
|
+
}
|
|
907
|
+
yield `${cells.join(delimiter)}\n`;
|
|
908
|
+
}
|
|
909
|
+
if (sections === 0) {
|
|
910
|
+
// no nodes: one header so the section (and its columns) still exists
|
|
911
|
+
const header = [formatHeaderField(this.options.idColumn ?? "", ID_TYPE, null)];
|
|
912
|
+
if (labels !== null) {
|
|
913
|
+
header.push(":LABEL");
|
|
914
|
+
}
|
|
915
|
+
if (propertyHeaders.length > 0) {
|
|
916
|
+
header.push(propertyHeaders);
|
|
917
|
+
}
|
|
918
|
+
yield `${header.join(delimiter)}\n`;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* The relationship sections.
|
|
924
|
+
* @yields one line at a time
|
|
925
|
+
* @returns nothing
|
|
926
|
+
*/
|
|
927
|
+
private *relationshipLines(): Generator<string, void, undefined> {
|
|
928
|
+
const { snapshot, kind, weights, edgeColumns, folding } = this;
|
|
929
|
+
const { ids } = snapshot;
|
|
930
|
+
const { delimiter } = this.options.syntax;
|
|
931
|
+
const list = snapshot.edgeList();
|
|
932
|
+
const propertyHeaders = edgeColumns.map((plan) => plan.header).join(delimiter);
|
|
933
|
+
const weightHeader =
|
|
934
|
+
weights === null ? null : formatHeaderField(this.options.weightColumn ?? "weight", "double", null);
|
|
935
|
+
let sections = 0;
|
|
936
|
+
let sectionStart: string | null = null;
|
|
937
|
+
let sectionEnd: string | null = null;
|
|
938
|
+
const cells: string[] = [];
|
|
939
|
+
const headerOf = (startSpace: string | null, endSpace: string | null): string => {
|
|
940
|
+
const header = [formatHeaderField("", "START_ID", startSpace), formatHeaderField("", "END_ID", endSpace)];
|
|
941
|
+
if (kind !== null) {
|
|
942
|
+
header.push(":TYPE");
|
|
943
|
+
}
|
|
944
|
+
if (weightHeader !== null) {
|
|
945
|
+
header.push(weightHeader);
|
|
946
|
+
}
|
|
947
|
+
if (propertyHeaders.length > 0) {
|
|
948
|
+
header.push(propertyHeaders);
|
|
949
|
+
}
|
|
950
|
+
return `${header.join(delimiter)}\n`;
|
|
951
|
+
};
|
|
952
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
953
|
+
if (folding.folded(e)) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const u = list.src[e];
|
|
957
|
+
const v = list.dst[e];
|
|
958
|
+
const startSpace = this.spaceOf(u);
|
|
959
|
+
const endSpace = this.spaceOf(v);
|
|
960
|
+
if (sections === 0 || startSpace !== sectionStart || endSpace !== sectionEnd) {
|
|
961
|
+
sections++;
|
|
962
|
+
sectionStart = startSpace;
|
|
963
|
+
sectionEnd = endSpace;
|
|
964
|
+
yield headerOf(startSpace, endSpace);
|
|
965
|
+
}
|
|
966
|
+
cells.length = 0;
|
|
967
|
+
cells.push(this.cell(idText(ids.idOf(u))), this.cell(idText(ids.idOf(v))));
|
|
968
|
+
if (kind !== null) {
|
|
969
|
+
cells.push(this.cell(kind.isSet(e) ? textOf(kind, e) : null));
|
|
970
|
+
}
|
|
971
|
+
if (weights !== null) {
|
|
972
|
+
cells.push(this.cell(weights.text(e)));
|
|
973
|
+
}
|
|
974
|
+
for (const plan of edgeColumns) {
|
|
975
|
+
cells.push(this.cell(this.valueText(plan, e)));
|
|
976
|
+
}
|
|
977
|
+
yield `${cells.join(delimiter)}\n`;
|
|
978
|
+
}
|
|
979
|
+
if (sections === 0) {
|
|
980
|
+
yield headerOf(null, null);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* A CSV cell: empty for an unset value, a quoted empty string for a set empty string, the
|
|
986
|
+
* quoted text otherwise.
|
|
987
|
+
* @param text - the value text, or null for unset
|
|
988
|
+
* @returns the cell as written
|
|
989
|
+
*/
|
|
990
|
+
private cell(text: string | null): string {
|
|
991
|
+
if (text === null) {
|
|
992
|
+
return "";
|
|
993
|
+
}
|
|
994
|
+
const { quote } = this.options.syntax;
|
|
995
|
+
if (text.length === 0) {
|
|
996
|
+
return quote + quote;
|
|
997
|
+
}
|
|
998
|
+
return quoteCsvCell(text, this.options.syntax.delimiter);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* The `:LABEL` cell of a node.
|
|
1003
|
+
* @param index - the node index
|
|
1004
|
+
* @returns the labels joined by the array delimiter, or null when unset
|
|
1005
|
+
*/
|
|
1006
|
+
private labelsText(index: number): string | null {
|
|
1007
|
+
const { labels } = this;
|
|
1008
|
+
if (labels === null || !labels.isSet(index)) {
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
if (labels.dtype === "list") {
|
|
1012
|
+
return labels
|
|
1013
|
+
.sliceOf(index)
|
|
1014
|
+
.map((item) => String(item))
|
|
1015
|
+
.join(this.options.arrayDelimiter);
|
|
1016
|
+
}
|
|
1017
|
+
return textOf(labels, index);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* The text of one property cell.
|
|
1022
|
+
* @param plan - the column plan
|
|
1023
|
+
* @param row - the row
|
|
1024
|
+
* @returns the text, or null when unset
|
|
1025
|
+
*/
|
|
1026
|
+
private valueText(plan: ColumnPlan, row: number): string | null {
|
|
1027
|
+
const { column, scalar, companion } = plan;
|
|
1028
|
+
if (!column.isSet(row)) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
if (plan.list) {
|
|
1032
|
+
const items = column.dtype === "list" ? column.sliceOf(row) : (column.value(row) as ArrayLike<unknown>);
|
|
1033
|
+
const texts: string[] = [];
|
|
1034
|
+
for (const item of Array.from(items)) {
|
|
1035
|
+
// an item of several components (itemComponents > 1) is flattened into the array
|
|
1036
|
+
if (Array.isArray(item) || ArrayBuffer.isView(item)) {
|
|
1037
|
+
for (const component of Array.from(item as ArrayLike<unknown>)) {
|
|
1038
|
+
texts.push(formatScalar(component, scalar, null));
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
texts.push(formatScalar(item, scalar, null));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return texts.join(this.options.arrayDelimiter);
|
|
1045
|
+
}
|
|
1046
|
+
const text = companion !== null && companion.isSet(row) ? textOf(companion, row) : null;
|
|
1047
|
+
return formatScalar(column.dtype === "json" ? column.values[row] : column.value(row), scalar, text);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Whether a node column is a stored id (`name:ID` on import).
|
|
1053
|
+
* @param meta - the column metadata
|
|
1054
|
+
* @returns true for a string column declared by the Neo4j importer as the id property
|
|
1055
|
+
*/
|
|
1056
|
+
function isStoredId(meta: ColumnMeta): boolean {
|
|
1057
|
+
return (
|
|
1058
|
+
meta.role === null &&
|
|
1059
|
+
meta.dtype === "string" &&
|
|
1060
|
+
meta.origin !== null &&
|
|
1061
|
+
meta.origin.format === NEO4J &&
|
|
1062
|
+
meta.origin.type === ID_TYPE
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Whether a role is one of the visual roles.
|
|
1068
|
+
* @param role - the role
|
|
1069
|
+
* @returns true for color, size, shape and thickness
|
|
1070
|
+
*/
|
|
1071
|
+
function isVizRole(role: string): boolean {
|
|
1072
|
+
return role === "color" || role === "size" || role === "shape" || role === "thickness";
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* The declared scalar type text of a list declaration (`string[]` -> `string`).
|
|
1077
|
+
* @param declared - the declared type text
|
|
1078
|
+
* @returns the text without its `[]` suffix
|
|
1079
|
+
*/
|
|
1080
|
+
function declaredScalar(declared: string): string {
|
|
1081
|
+
const trimmed = declared.trim();
|
|
1082
|
+
return trimmed.endsWith("[]") ? trimmed.slice(0, -2) : trimmed;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* The cell kind of a mapped declared type.
|
|
1087
|
+
* @param spec - the mapped type
|
|
1088
|
+
* @returns the kind
|
|
1089
|
+
*/
|
|
1090
|
+
function kindOf(spec: DeclaredTypeSpec): CellKind {
|
|
1091
|
+
switch (spec.kind) {
|
|
1092
|
+
case "boolean":
|
|
1093
|
+
return "boolean";
|
|
1094
|
+
case "integer":
|
|
1095
|
+
case "long":
|
|
1096
|
+
return "integer";
|
|
1097
|
+
case "float":
|
|
1098
|
+
return "float";
|
|
1099
|
+
case "double":
|
|
1100
|
+
return "double";
|
|
1101
|
+
case "string":
|
|
1102
|
+
case "duration":
|
|
1103
|
+
return "string";
|
|
1104
|
+
case "temporal":
|
|
1105
|
+
return "temporal";
|
|
1106
|
+
case "point":
|
|
1107
|
+
return "point";
|
|
1108
|
+
case "json":
|
|
1109
|
+
return "json";
|
|
1110
|
+
default: {
|
|
1111
|
+
const name: string = spec.kind;
|
|
1112
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown value kind ${name}`, { kind: name });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* The Neo4j type of a dtype without a compatible declaration.
|
|
1119
|
+
* @param dtype - the column or item dtype
|
|
1120
|
+
* @returns the type and cell kind
|
|
1121
|
+
*/
|
|
1122
|
+
function defaultScalar(dtype: Dtype | ScalarDtype): ScalarPlan {
|
|
1123
|
+
switch (dtype) {
|
|
1124
|
+
case "f32":
|
|
1125
|
+
return { type: "float", kind: "float", temporal: null };
|
|
1126
|
+
case "f64":
|
|
1127
|
+
return { type: "double", kind: "double", temporal: null };
|
|
1128
|
+
case "i32":
|
|
1129
|
+
case "u8":
|
|
1130
|
+
return { type: "int", kind: "integer", temporal: null };
|
|
1131
|
+
case "u32":
|
|
1132
|
+
return { type: "long", kind: "integer", temporal: null };
|
|
1133
|
+
case "bool":
|
|
1134
|
+
return { type: "boolean", kind: "boolean", temporal: null };
|
|
1135
|
+
case "dict":
|
|
1136
|
+
case "string":
|
|
1137
|
+
return { type: "string", kind: "string", temporal: null };
|
|
1138
|
+
case "json":
|
|
1139
|
+
return { type: "string", kind: "json", temporal: null };
|
|
1140
|
+
case "list":
|
|
1141
|
+
return { type: "string", kind: "json", temporal: null };
|
|
1142
|
+
default: {
|
|
1143
|
+
const name: string = dtype;
|
|
1144
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${name}`, { dtype: name });
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Format one scalar value for a cell.
|
|
1151
|
+
* @param value - the value
|
|
1152
|
+
* @param scalar - the plan
|
|
1153
|
+
* @param text - the companion text of a temporal value, or null
|
|
1154
|
+
* @returns the text
|
|
1155
|
+
*/
|
|
1156
|
+
function formatScalar(value: unknown, scalar: ScalarPlan, text: string | null): string {
|
|
1157
|
+
switch (scalar.kind) {
|
|
1158
|
+
case "integer":
|
|
1159
|
+
return typeof value === "number" ? formatInteger(value) : String(value);
|
|
1160
|
+
case "float":
|
|
1161
|
+
return typeof value === "number" ? formatF32(value) : String(value);
|
|
1162
|
+
case "double":
|
|
1163
|
+
return typeof value === "number" ? formatF64(value) : String(value);
|
|
1164
|
+
case "boolean":
|
|
1165
|
+
return value === true ? "true" : "false";
|
|
1166
|
+
case "string":
|
|
1167
|
+
return typeof value === "string" ? value : String(value);
|
|
1168
|
+
case "temporal":
|
|
1169
|
+
if (text !== null) {
|
|
1170
|
+
return text;
|
|
1171
|
+
}
|
|
1172
|
+
return typeof value === "number" && scalar.temporal !== null
|
|
1173
|
+
? formatTemporal(value, scalar.temporal)
|
|
1174
|
+
: String(value);
|
|
1175
|
+
case "point":
|
|
1176
|
+
return formatPoint(value);
|
|
1177
|
+
case "json":
|
|
1178
|
+
return JSON.stringify(value) ?? "";
|
|
1179
|
+
default: {
|
|
1180
|
+
const name: string = scalar.kind;
|
|
1181
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown cell kind ${name}`, { kind: name });
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* A Neo4j point literal `{x:1.5, y:2, crs:'cartesian'}` from a point object; any other value is
|
|
1188
|
+
* written as JSON text.
|
|
1189
|
+
* @param value - the json value
|
|
1190
|
+
* @returns the literal
|
|
1191
|
+
*/
|
|
1192
|
+
function formatPoint(value: unknown): string {
|
|
1193
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1194
|
+
return JSON.stringify(value) ?? "";
|
|
1195
|
+
}
|
|
1196
|
+
const parts: string[] = [];
|
|
1197
|
+
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
1198
|
+
if (typeof item === "number") {
|
|
1199
|
+
parts.push(`${key}:${formatF64(item)}`);
|
|
1200
|
+
} else if (typeof item === "string") {
|
|
1201
|
+
parts.push(`${key}:'${item.replace(/'/g, "")}'`);
|
|
1202
|
+
} else {
|
|
1203
|
+
return JSON.stringify(value) ?? "";
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return `{${parts.join(", ")}}`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* The text of a node id.
|
|
1211
|
+
* @param id - the id
|
|
1212
|
+
* @returns String(id)
|
|
1213
|
+
*/
|
|
1214
|
+
function idText(id: NodeId): string {
|
|
1215
|
+
if (typeof id === "string") {
|
|
1216
|
+
return id;
|
|
1217
|
+
}
|
|
1218
|
+
return Number.isInteger(id) ? formatInteger(id) : String(id);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* The text of a set cell of a dict / string (or any scalar) column.
|
|
1223
|
+
* @param column - the column
|
|
1224
|
+
* @param row - the row
|
|
1225
|
+
* @returns the value as text
|
|
1226
|
+
*/
|
|
1227
|
+
function textOf(column: Column, row: number): string {
|
|
1228
|
+
switch (column.dtype) {
|
|
1229
|
+
case "string":
|
|
1230
|
+
return column.valueAt(row);
|
|
1231
|
+
case "dict":
|
|
1232
|
+
return String(column.value(row));
|
|
1233
|
+
case "list":
|
|
1234
|
+
return column.sliceOf(row).map(String).join(",");
|
|
1235
|
+
case "json":
|
|
1236
|
+
return JSON.stringify(column.values[row]) ?? "";
|
|
1237
|
+
case "bool":
|
|
1238
|
+
return column.value(row) === true ? "true" : "false";
|
|
1239
|
+
default: {
|
|
1240
|
+
const value = column.value(row);
|
|
1241
|
+
return typeof value === "number" ? String(value) : Array.from(value as ArrayLike<number>).join(",");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** The Neo4j exporter. */
|
|
1247
|
+
export const neo4jExporter: GraphExporter<Neo4jExportOptions> = Object.freeze({
|
|
1248
|
+
format: NEO4J,
|
|
1249
|
+
capabilities: NEO4J_CAPABILITIES,
|
|
1250
|
+
/**
|
|
1251
|
+
* Pre-flight: what export() would lose.
|
|
1252
|
+
* @param snapshot - the snapshot
|
|
1253
|
+
* @param options - format-specific and common options
|
|
1254
|
+
* @returns the notes
|
|
1255
|
+
*/
|
|
1256
|
+
check(snapshot: GraphSnapshot, options?: Neo4jExportOptions & CommonExportOptions): readonly LossNote[] {
|
|
1257
|
+
return Object.freeze([...plan(snapshot, options).notes]);
|
|
1258
|
+
},
|
|
1259
|
+
/**
|
|
1260
|
+
* Write the snapshot as UTF-8 chunks.
|
|
1261
|
+
* @param snapshot - the snapshot
|
|
1262
|
+
* @param options - format-specific and common options
|
|
1263
|
+
* @returns the chunks
|
|
1264
|
+
*/
|
|
1265
|
+
export(snapshot: GraphSnapshot, options?: Neo4jExportOptions & CommonExportOptions): AsyncIterable<Uint8Array> {
|
|
1266
|
+
return encodeChunks(plan(snapshot, options).lines());
|
|
1267
|
+
},
|
|
1268
|
+
/**
|
|
1269
|
+
* Write the snapshot as one string.
|
|
1270
|
+
* @param snapshot - the snapshot
|
|
1271
|
+
* @param options - format-specific and common options
|
|
1272
|
+
* @returns the document
|
|
1273
|
+
*/
|
|
1274
|
+
exportToString(snapshot: GraphSnapshot, options?: Neo4jExportOptions & CommonExportOptions): Promise<string> {
|
|
1275
|
+
return joinText(plan(snapshot, options).lines());
|
|
1276
|
+
},
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Build the export plan of a snapshot.
|
|
1281
|
+
* @param snapshot - the snapshot
|
|
1282
|
+
* @param options - the caller's options
|
|
1283
|
+
* @returns the plan
|
|
1284
|
+
*/
|
|
1285
|
+
function plan(snapshot: GraphSnapshot, options: (Neo4jExportOptions & CommonExportOptions) | undefined): ExportPlan {
|
|
1286
|
+
return new ExportPlan(snapshot, resolveNeo4jExportOptions(options), resolveExportOptions(options));
|
|
1287
|
+
}
|