@graphty/graph-io 0.0.0 → 0.2.1
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,932 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Neo4j importer (design sections 8.4 and 5.1, research note 07 sections 2.6 and 2.10): reads
|
|
3
|
+
* neo4j-admin import CSV -- node tables with `:ID`, `:LABEL` and typed property columns,
|
|
4
|
+
* relationship tables with `:START_ID`, `:END_ID`, `:TYPE` and typed property columns -- and pushes
|
|
5
|
+
* scalars into the sink one record at a time. Input is one or more files: the primary input plus the
|
|
6
|
+
* `nodes` and `relationships` option inputs, each read in order; every input holds one or more
|
|
7
|
+
* sections, each starting with its own header row (the single-file convention of graphty-element's
|
|
8
|
+
* CSVDataSource, where a node table and a relationship table follow each other in one file).
|
|
9
|
+
*
|
|
10
|
+
* Mapping (design section 5.1 and decision Q27):
|
|
11
|
+
* - Property columns are declared up front from the header types (`int` -> i32, `long` -> f64
|
|
12
|
+
* with a precision issue beyond 2^53, `float` -> f32, `double` -> f64, `boolean` -> bool,
|
|
13
|
+
* `string` / `char` / `duration` -> string, temporal types -> f64 milliseconds with a `.text`
|
|
14
|
+
* companion when the source text is not canonical, `point` -> json, `type[]` -> list); an
|
|
15
|
+
* untyped column is a string property. An unquoted empty cell is "property not set"; a quoted
|
|
16
|
+
* empty cell is an empty string (or an empty list), as neo4j-admin stores it by default.
|
|
17
|
+
* - `:LABEL` becomes the node list-of-dict column `labels` (role `labels`); `:TYPE` the edge dict
|
|
18
|
+
* column `type` (role `kind`); an id space `:ID(Space)` the node dict column `idSpace` (role
|
|
19
|
+
* `idSpace`), and a stored id `name:ID(Space)` also a string node column `name` whose
|
|
20
|
+
* `origin.namespace` is the space. A property whose name collides with one of those is renamed
|
|
21
|
+
* `<name>#<name>` (design section 5.6).
|
|
22
|
+
* - Ids are text cells coerced by the `ids` option ("canonical" by default, so `1` is the number 1
|
|
23
|
+
* and `007` stays a string; integers beyond 2^53 stay strings). Relationships are always directed
|
|
24
|
+
* ("In Neo4j, all relationships have a direction"), so the sink is set directed before the first
|
|
25
|
+
* edge and `onMixedDirection: "undirected"` is the way to read a file as undirected.
|
|
26
|
+
* - `:IGNORE` columns are skipped and counted in a loss note.
|
|
27
|
+
*/
|
|
28
|
+
import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
|
|
29
|
+
import { declareAttribute, declareCompanion, declareOn, losesPrecision, parseDeclaredTemporal, parseDeclaredValue, PRECISION_CODE, RENAMED_CODE, takenIn, uniqueColumnName, } from "../../common/attributes.js";
|
|
30
|
+
import { DUPLICATE_NODE_CODE as SHARED_DUPLICATE_NODE_CODE, MISSING_ENDPOINT_CODE as SHARED_MISSING_ENDPOINT_CODE, MISSING_ID_CODE as SHARED_MISSING_ID_CODE, ROLE_TAKEN_CODE as SHARED_ROLE_TAKEN_CODE, } from "../../common/codes.js";
|
|
31
|
+
import { DirectionResolver } from "../../common/direction.js";
|
|
32
|
+
import { ID_MERGED_CODE as SHARED_ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
|
|
33
|
+
import { inputLength, isImportInput, throwIfAborted } from "../../common/input.js";
|
|
34
|
+
import { splitListText } from "../../common/lists.js";
|
|
35
|
+
import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, } from "../../common/options.js";
|
|
36
|
+
import { ImportReportBuilder } from "../../common/report.js";
|
|
37
|
+
import { parseWeightText } from "../../common/weights.js";
|
|
38
|
+
import { ImportError, } from "../../types.js";
|
|
39
|
+
import { checkRecordSyntax, RecordReader } from "../csv/records.js";
|
|
40
|
+
import { isHeaderRecord, parseHeaderField } from "./header.js";
|
|
41
|
+
/** The name of the node list column holding `:LABEL` values. */
|
|
42
|
+
export const LABELS_COLUMN = "labels";
|
|
43
|
+
/** The name of the edge dict column holding `:TYPE` values. */
|
|
44
|
+
export const TYPE_COLUMN = "type";
|
|
45
|
+
/** The name of the node dict column holding the id space of `:ID(Space)`. */
|
|
46
|
+
export const ID_SPACE_COLUMN = "idSpace";
|
|
47
|
+
/** Issue code: a header row (or a whole section) is malformed; the import aborts. */
|
|
48
|
+
export const HEADER_CODE = "E_NEO4J_HEADER";
|
|
49
|
+
/** Issue code: a row has a different number of cells than its header. */
|
|
50
|
+
export const COLUMN_COUNT_CODE = "E_NEO4J_COLUMN_COUNT";
|
|
51
|
+
/** Issue code: a node row has an unquoted empty `:ID` cell (a quoted empty cell is the id ""). */
|
|
52
|
+
export const MISSING_ID_CODE = SHARED_MISSING_ID_CODE;
|
|
53
|
+
/** Issue code: a relationship row has an unquoted empty `:START_ID` or `:END_ID` cell. */
|
|
54
|
+
export const MISSING_ENDPOINT_CODE = SHARED_MISSING_ENDPOINT_CODE;
|
|
55
|
+
/** Issue code: a node id was declared twice (same id space); the later row's properties win. */
|
|
56
|
+
export const DUPLICATE_NODE_CODE = SHARED_DUPLICATE_NODE_CODE;
|
|
57
|
+
/** Issue code: a node id was declared in two id spaces; the core has one id space and the later row is skipped. */
|
|
58
|
+
export const ID_SPACE_COLLISION_CODE = "E_NEO4J_ID_SPACE_COLLISION";
|
|
59
|
+
/** Issue code: two different id cells became one id under `ids: "number"`. */
|
|
60
|
+
export const ID_MERGED_CODE = SHARED_ID_MERGED_CODE;
|
|
61
|
+
/** Issue code: a header brace option the importer does not act on. */
|
|
62
|
+
export const HEADER_OPTION_CODE = "W_NEO4J_HEADER_OPTION_IGNORED";
|
|
63
|
+
/** Issue code: a reserved column (labels / type / idSpace) lost its role because the sink already holds it. */
|
|
64
|
+
export const ROLE_TAKEN_CODE = SHARED_ROLE_TAKEN_CODE;
|
|
65
|
+
/** Loss code: `:IGNORE` columns were skipped. */
|
|
66
|
+
export const IGNORED_COLUMNS_LOSS = "W_NEO4J_IGNORED_COLUMNS";
|
|
67
|
+
/** The common options the Neo4j importer reads (the rest is reported by reportUnusedOptions). */
|
|
68
|
+
const USED_OPTIONS = new Set([
|
|
69
|
+
"ids",
|
|
70
|
+
"addMissingNodes",
|
|
71
|
+
"duplicateEdges",
|
|
72
|
+
"selfLoops",
|
|
73
|
+
"onMixedDirection",
|
|
74
|
+
"weightFrom",
|
|
75
|
+
"weightDtype",
|
|
76
|
+
"long",
|
|
77
|
+
"errorLimit",
|
|
78
|
+
"signal",
|
|
79
|
+
"onProgress",
|
|
80
|
+
]);
|
|
81
|
+
const NEO4J = "neo4j";
|
|
82
|
+
/** Rows between two checks of the cancellation signal (a whole string input is one chunk). */
|
|
83
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
84
|
+
const LABELS_DECL = {
|
|
85
|
+
name: LABELS_COLUMN,
|
|
86
|
+
dtype: "list",
|
|
87
|
+
itemDtype: "dict",
|
|
88
|
+
nullable: true,
|
|
89
|
+
role: "labels",
|
|
90
|
+
origin: { format: NEO4J, id: ":LABEL", title: null, type: "LABEL", namespace: null },
|
|
91
|
+
};
|
|
92
|
+
const TYPE_DECL = {
|
|
93
|
+
name: TYPE_COLUMN,
|
|
94
|
+
dtype: "dict",
|
|
95
|
+
nullable: true,
|
|
96
|
+
role: "kind",
|
|
97
|
+
origin: { format: NEO4J, id: ":TYPE", title: null, type: "TYPE", namespace: null },
|
|
98
|
+
};
|
|
99
|
+
const ID_SPACE_DECL = {
|
|
100
|
+
name: ID_SPACE_COLUMN,
|
|
101
|
+
dtype: "dict",
|
|
102
|
+
nullable: true,
|
|
103
|
+
role: "idSpace",
|
|
104
|
+
origin: { format: NEO4J, id: ":ID", title: null, type: "ID", namespace: null },
|
|
105
|
+
};
|
|
106
|
+
const ARRAY_DELIMITERS = { ";": "semicolon", ",": "comma", "|": "pipe" };
|
|
107
|
+
/** The delimiters sniffed between when none is given: neo4j-admin's default and the TSV tab. */
|
|
108
|
+
const NEO4J_DELIMITER_CANDIDATES = Object.freeze([",", "\t"]);
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the format-specific options.
|
|
111
|
+
* @param options - the caller's options
|
|
112
|
+
* @returns the resolved options; E_UNSUPPORTED for an invalid value
|
|
113
|
+
*/
|
|
114
|
+
function resolveNeo4jOptions(options) {
|
|
115
|
+
const o = options ?? {};
|
|
116
|
+
const arrayDelimiter = o.arrayDelimiter ?? ";";
|
|
117
|
+
const listSyntax = ARRAY_DELIMITERS[arrayDelimiter];
|
|
118
|
+
if (typeof arrayDelimiter !== "string" || listSyntax === undefined) {
|
|
119
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option arrayDelimiter: ${JSON.stringify(arrayDelimiter)} is not one of ";", ",", "|"`, { option: "arrayDelimiter", found: arrayDelimiter });
|
|
120
|
+
}
|
|
121
|
+
const syntax = checkRecordSyntax({
|
|
122
|
+
delimiter: o.delimiter ?? null,
|
|
123
|
+
quote: o.quote ?? '"',
|
|
124
|
+
candidates: NEO4J_DELIMITER_CANDIDATES.filter((d) => d !== arrayDelimiter),
|
|
125
|
+
});
|
|
126
|
+
if (syntax.delimiter === arrayDelimiter) {
|
|
127
|
+
throw new GraphFormatError("E_UNSUPPORTED", "options delimiter and arrayDelimiter must differ", {
|
|
128
|
+
option: "arrayDelimiter",
|
|
129
|
+
found: arrayDelimiter,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
nodes: inputList("nodes", o.nodes),
|
|
134
|
+
relationships: inputList("relationships", o.relationships),
|
|
135
|
+
syntax,
|
|
136
|
+
listSyntax,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Normalise an input-list option.
|
|
141
|
+
* @param name - the option name
|
|
142
|
+
* @param value - one input, a list of inputs, or undefined
|
|
143
|
+
* @returns the inputs; E_UNSUPPORTED for anything else
|
|
144
|
+
*/
|
|
145
|
+
function inputList(name, value) {
|
|
146
|
+
if (value === undefined || value === null) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const list = Array.isArray(value) ? value : [value];
|
|
150
|
+
for (const item of list) {
|
|
151
|
+
if (!isImportInput(item)) {
|
|
152
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an entry is not an ImportInput`, {
|
|
153
|
+
option: name,
|
|
154
|
+
found: typeof item,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return list;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Which nodes were declared by a node row and in which id space, by node index, so a repeated id
|
|
162
|
+
* is reported (a duplicate in one space, a collision across spaces).
|
|
163
|
+
*/
|
|
164
|
+
class NodeRegistry {
|
|
165
|
+
constructor() {
|
|
166
|
+
this.codes = new Uint32Array(1024);
|
|
167
|
+
this.spaces = new Map();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The code of an id space (1 for "no space").
|
|
171
|
+
* @param space - the space name or null
|
|
172
|
+
* @returns a code >= 1
|
|
173
|
+
*/
|
|
174
|
+
codeOf(space) {
|
|
175
|
+
let code = this.spaces.get(space);
|
|
176
|
+
if (code === undefined) {
|
|
177
|
+
code = this.spaces.size + 1;
|
|
178
|
+
this.spaces.set(space, code);
|
|
179
|
+
}
|
|
180
|
+
return code;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Record that a node row declared a node.
|
|
184
|
+
* @param index - the node index
|
|
185
|
+
* @param code - the space code
|
|
186
|
+
* @returns "new" for a first declaration, "duplicate" for a repeat in the same space, "collision" across spaces
|
|
187
|
+
*/
|
|
188
|
+
declare(index, code) {
|
|
189
|
+
if (index >= this.codes.length) {
|
|
190
|
+
let size = this.codes.length * 2;
|
|
191
|
+
while (size <= index) {
|
|
192
|
+
size *= 2;
|
|
193
|
+
}
|
|
194
|
+
const grown = new Uint32Array(size);
|
|
195
|
+
grown.set(this.codes);
|
|
196
|
+
this.codes = grown;
|
|
197
|
+
}
|
|
198
|
+
const previous = this.codes[index];
|
|
199
|
+
if (previous === 0) {
|
|
200
|
+
this.codes[index] = code;
|
|
201
|
+
return "new";
|
|
202
|
+
}
|
|
203
|
+
return previous === code ? "duplicate" : "collision";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Byte progress over several inputs as one sequence: each input's progress is offset by the bytes
|
|
208
|
+
* of the inputs before it, and the total is known only when every input is in memory.
|
|
209
|
+
*/
|
|
210
|
+
class ProgressTracker {
|
|
211
|
+
/**
|
|
212
|
+
* Create a tracker.
|
|
213
|
+
* @param callback - the caller's onProgress, or null
|
|
214
|
+
* @param inputs - every input in reading order
|
|
215
|
+
*/
|
|
216
|
+
constructor(callback, inputs) {
|
|
217
|
+
this.offset = 0;
|
|
218
|
+
this.lastDone = 0;
|
|
219
|
+
this.callback = callback;
|
|
220
|
+
let total = 0;
|
|
221
|
+
for (const input of inputs) {
|
|
222
|
+
const length = inputLength(input);
|
|
223
|
+
if (length === null) {
|
|
224
|
+
total = undefined;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
total += length;
|
|
228
|
+
}
|
|
229
|
+
this.total = total;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The read options for one input.
|
|
233
|
+
* @param signal - the cancellation signal
|
|
234
|
+
* @returns options whose onProgress reports cumulative bytes
|
|
235
|
+
*/
|
|
236
|
+
optionsFor(signal) {
|
|
237
|
+
const { callback } = this;
|
|
238
|
+
if (callback === null) {
|
|
239
|
+
return { signal };
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
signal,
|
|
243
|
+
onProgress: (done) => {
|
|
244
|
+
this.lastDone = done;
|
|
245
|
+
callback(this.offset + done, this.total);
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Move the offset past the input just finished. */
|
|
250
|
+
finishInput() {
|
|
251
|
+
this.offset += this.lastDone;
|
|
252
|
+
this.lastDone = 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The state of one import call: the sink, the report, the resolved options, the reserved column
|
|
257
|
+
* handles and the per-section row handlers.
|
|
258
|
+
*/
|
|
259
|
+
class Neo4jImportSession {
|
|
260
|
+
/**
|
|
261
|
+
* Create a session.
|
|
262
|
+
* @param sink - the sink
|
|
263
|
+
* @param report - the report
|
|
264
|
+
* @param common - the resolved common options
|
|
265
|
+
* @param options - the resolved format options
|
|
266
|
+
*/
|
|
267
|
+
constructor(sink, report, common, options) {
|
|
268
|
+
this.registry = new NodeRegistry();
|
|
269
|
+
this.labelsHandle = INVALID_INDEX;
|
|
270
|
+
this.typeHandle = INVALID_INDEX;
|
|
271
|
+
this.idSpaceHandle = INVALID_INDEX;
|
|
272
|
+
this.ignoredColumns = 0;
|
|
273
|
+
/** Whether the sink's direction was set (before the first relationship, design section 8.4 rule 1). */
|
|
274
|
+
this.headerSet = false;
|
|
275
|
+
/** Scratch: the parsed value of every cell of the current row. */
|
|
276
|
+
this.values = [];
|
|
277
|
+
/** Scratch: the companion text of every cell of the current row. */
|
|
278
|
+
this.texts = [];
|
|
279
|
+
/** Scratch: the property slots of the current row that lost precision. */
|
|
280
|
+
this.precisionSlots = [];
|
|
281
|
+
this.sink = sink;
|
|
282
|
+
this.report = report;
|
|
283
|
+
this.common = common;
|
|
284
|
+
this.options = options;
|
|
285
|
+
this.coercer = new IdCoercer(common.ids);
|
|
286
|
+
this.direction = new DirectionResolver(sink, report, common.onMixedDirection);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Read every input.
|
|
290
|
+
* @param inputs - the inputs in reading order
|
|
291
|
+
*/
|
|
292
|
+
async run(inputs) {
|
|
293
|
+
const progress = new ProgressTracker(this.common.onProgress, inputs);
|
|
294
|
+
for (const input of inputs) {
|
|
295
|
+
await this.readInput(input, progress.optionsFor(this.common.signal));
|
|
296
|
+
progress.finishInput();
|
|
297
|
+
}
|
|
298
|
+
if (this.ignoredColumns > 0) {
|
|
299
|
+
this.report.loss(IGNORED_COLUMNS_LOSS, `${this.ignoredColumns} :IGNORE column(s) were skipped as the header instructs`, null, this.ignoredColumns);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Read one input: a header row, then data rows until the next header row.
|
|
304
|
+
* @param input - the input
|
|
305
|
+
* @param readOptions - cancellation and progress
|
|
306
|
+
*/
|
|
307
|
+
async readInput(input, readOptions) {
|
|
308
|
+
const reader = new RecordReader(input, this.report, this.options.syntax, readOptions);
|
|
309
|
+
let section = null;
|
|
310
|
+
let sinceCheck = 0;
|
|
311
|
+
for await (const count of reader) {
|
|
312
|
+
if (section === null || isHeaderRecord(reader.cells, count)) {
|
|
313
|
+
section = this.declareSection(reader, count);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (section.kind === "node") {
|
|
317
|
+
this.nodeRow(section, reader, count);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
this.relationshipRow(section, reader, count);
|
|
321
|
+
}
|
|
322
|
+
if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
323
|
+
sinceCheck = 0;
|
|
324
|
+
throwIfAborted(readOptions.signal);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (section === null) {
|
|
328
|
+
this.report.fail(HEADER_CODE, "the input has no header row", { line: 1 });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Interpret a header row: parse every cell, check the section shape, declare its columns.
|
|
333
|
+
* @param reader - the reader positioned on the header
|
|
334
|
+
* @param count - the number of header cells
|
|
335
|
+
* @returns the section
|
|
336
|
+
*/
|
|
337
|
+
declareSection(reader, count) {
|
|
338
|
+
const { line } = reader;
|
|
339
|
+
const fields = [];
|
|
340
|
+
try {
|
|
341
|
+
for (let i = 0; i < count; i++) {
|
|
342
|
+
fields.push(parseHeaderField(reader.cells[i]));
|
|
343
|
+
}
|
|
344
|
+
const kinds = new Map();
|
|
345
|
+
fields.forEach((field, i) => {
|
|
346
|
+
const list = kinds.get(field.kind);
|
|
347
|
+
if (list === undefined) {
|
|
348
|
+
kinds.set(field.kind, [i]);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
list.push(i);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
const ids = kinds.get("ID") ?? [];
|
|
355
|
+
const starts = kinds.get("START_ID") ?? [];
|
|
356
|
+
const ends = kinds.get("END_ID") ?? [];
|
|
357
|
+
const isNode = ids.length > 0;
|
|
358
|
+
const isRelationship = starts.length > 0 || ends.length > 0;
|
|
359
|
+
if (isNode && isRelationship) {
|
|
360
|
+
throw headerError("a header mixes :ID with :START_ID / :END_ID");
|
|
361
|
+
}
|
|
362
|
+
if (!isNode && !isRelationship) {
|
|
363
|
+
throw headerError("a header needs an :ID column (nodes) or :START_ID and :END_ID columns (relationships)");
|
|
364
|
+
}
|
|
365
|
+
checkPropertyNames(fields);
|
|
366
|
+
this.ignoredColumns += (kinds.get("IGNORE") ?? []).length;
|
|
367
|
+
if (isNode) {
|
|
368
|
+
if (ids.length > 1) {
|
|
369
|
+
throw headerError("a node header has more than one :ID column");
|
|
370
|
+
}
|
|
371
|
+
if (kinds.has("TYPE")) {
|
|
372
|
+
throw headerError("a node header cannot have a :TYPE column");
|
|
373
|
+
}
|
|
374
|
+
return this.declareNodeSection(fields, ids[0], kinds.get("LABEL") ?? [], line);
|
|
375
|
+
}
|
|
376
|
+
if (starts.length !== 1 || ends.length !== 1) {
|
|
377
|
+
throw headerError("a relationship header needs exactly one :START_ID and one :END_ID column");
|
|
378
|
+
}
|
|
379
|
+
if (kinds.has("LABEL")) {
|
|
380
|
+
throw headerError("a relationship header cannot have a :LABEL column");
|
|
381
|
+
}
|
|
382
|
+
const types = kinds.get("TYPE") ?? [];
|
|
383
|
+
if (types.length > 1) {
|
|
384
|
+
throw headerError("a relationship header has more than one :TYPE column");
|
|
385
|
+
}
|
|
386
|
+
return this.declareRelationshipSection(fields, starts[0], ends[0], types.length === 1 ? types[0] : -1, line);
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
if (err instanceof GraphFormatError && !(err instanceof ImportError)) {
|
|
390
|
+
this.report.fail(HEADER_CODE, `line ${line}: ${err.message}`, { line }, { cause: err.code });
|
|
391
|
+
}
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Declare the columns of a node section.
|
|
397
|
+
* @param fields - the parsed header
|
|
398
|
+
* @param idCell - the `:ID` cell
|
|
399
|
+
* @param labelCells - the `:LABEL` cells
|
|
400
|
+
* @param line - the header line
|
|
401
|
+
* @returns the section
|
|
402
|
+
*/
|
|
403
|
+
declareNodeSection(fields, idCell, labelCells, line) {
|
|
404
|
+
const idField = fields[idCell];
|
|
405
|
+
const extraLabels = [];
|
|
406
|
+
for (const field of fields) {
|
|
407
|
+
for (const [key, value] of field.options) {
|
|
408
|
+
if (key === "label" && field.kind === "ID") {
|
|
409
|
+
extraLabels.push(value);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
this.report.warning("unsupported", HEADER_OPTION_CODE, `header option ${key}:${value} of "${field.text}" is ignored`, { line, element: field.text });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (labelCells.length > 0 || extraLabels.length > 0) {
|
|
417
|
+
this.ensureLabels();
|
|
418
|
+
}
|
|
419
|
+
if (idField.space !== null) {
|
|
420
|
+
this.ensureIdSpace();
|
|
421
|
+
}
|
|
422
|
+
let idHandle = INVALID_INDEX;
|
|
423
|
+
if (idField.name.length > 0) {
|
|
424
|
+
idHandle = this.declareProperty("node", { ...idField, type: null }, idCell, { origin: { format: NEO4J, id: idField.name, title: null, type: "ID", namespace: idField.space } }, line).handle;
|
|
425
|
+
}
|
|
426
|
+
const properties = this.declareProperties("node", fields, line, null);
|
|
427
|
+
return {
|
|
428
|
+
kind: "node",
|
|
429
|
+
width: fields.length,
|
|
430
|
+
idCell,
|
|
431
|
+
idHandle,
|
|
432
|
+
space: idField.space,
|
|
433
|
+
spaceCode: this.registry.codeOf(idField.space),
|
|
434
|
+
labelCells,
|
|
435
|
+
extraLabels,
|
|
436
|
+
properties,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Declare the columns of a relationship section.
|
|
441
|
+
* @param fields - the parsed header
|
|
442
|
+
* @param startCell - the `:START_ID` cell
|
|
443
|
+
* @param endCell - the `:END_ID` cell
|
|
444
|
+
* @param typeCell - the `:TYPE` cell, or -1
|
|
445
|
+
* @param line - the header line
|
|
446
|
+
* @returns the section
|
|
447
|
+
*/
|
|
448
|
+
declareRelationshipSection(fields, startCell, endCell, typeCell, line) {
|
|
449
|
+
for (const field of fields) {
|
|
450
|
+
for (const [key, value] of field.options) {
|
|
451
|
+
this.report.warning("unsupported", HEADER_OPTION_CODE, `header option ${key}:${value} of "${field.text}" is ignored`, { line, element: field.text });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (typeCell >= 0) {
|
|
455
|
+
this.ensureType();
|
|
456
|
+
}
|
|
457
|
+
const { weightFrom } = this.common;
|
|
458
|
+
let weightCell = -1;
|
|
459
|
+
if (weightFrom !== null) {
|
|
460
|
+
weightCell = fields.findIndex((field) => field.kind === "PROPERTY" && field.name === weightFrom);
|
|
461
|
+
}
|
|
462
|
+
const properties = this.declareProperties("edge", fields, line, weightCell);
|
|
463
|
+
return { kind: "relationship", width: fields.length, startCell, endCell, typeCell, weightCell, properties };
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Declare every PROPERTY field of a header.
|
|
467
|
+
* @param domain - node or edge
|
|
468
|
+
* @param fields - the parsed header
|
|
469
|
+
* @param line - the header line
|
|
470
|
+
* @param skipCell - a cell to leave undeclared (the weight), or null
|
|
471
|
+
* @returns the property slots
|
|
472
|
+
*/
|
|
473
|
+
declareProperties(domain, fields, line, skipCell) {
|
|
474
|
+
const slots = [];
|
|
475
|
+
fields.forEach((field, cell) => {
|
|
476
|
+
if (field.kind !== "PROPERTY" || cell === skipCell) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
slots.push(this.declareProperty(domain, field, cell, {}, line));
|
|
480
|
+
});
|
|
481
|
+
const width = fields.length;
|
|
482
|
+
if (this.values.length < width) {
|
|
483
|
+
this.values = new Array(width);
|
|
484
|
+
this.texts = new Array(width);
|
|
485
|
+
}
|
|
486
|
+
return slots;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Declare one property column on the sink: the same name and shape again shares the column;
|
|
490
|
+
* a different shape under the same name is renamed `<name>#<name>` (design section 5.6).
|
|
491
|
+
* @param domain - node or edge
|
|
492
|
+
* @param field - the header field
|
|
493
|
+
* @param cell - the field's cell index
|
|
494
|
+
* @param patch - declaration fields to override (the id property's origin)
|
|
495
|
+
* @param line - the header line
|
|
496
|
+
* @returns the slot
|
|
497
|
+
*/
|
|
498
|
+
declareProperty(domain, field, cell, patch, line) {
|
|
499
|
+
const { sink, report } = this;
|
|
500
|
+
const { listSyntax } = this.options;
|
|
501
|
+
const input = {
|
|
502
|
+
format: NEO4J,
|
|
503
|
+
id: field.name,
|
|
504
|
+
title: null,
|
|
505
|
+
type: field.type,
|
|
506
|
+
namespace: null,
|
|
507
|
+
listSyntax,
|
|
508
|
+
long: this.common.long,
|
|
509
|
+
};
|
|
510
|
+
let declared = declareAttribute(input);
|
|
511
|
+
let decl = { ...declared.decl, ...patch };
|
|
512
|
+
let handle;
|
|
513
|
+
try {
|
|
514
|
+
handle = declareOn(sink, domain, decl);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
if (!(err instanceof GraphFormatError) || err.code !== "E_COLUMN_EXISTS") {
|
|
518
|
+
throw err;
|
|
519
|
+
}
|
|
520
|
+
declared = declareAttribute({ ...input, taken: takenIn(sink, domain) });
|
|
521
|
+
decl = { ...declared.decl, ...patch, name: declared.decl.name };
|
|
522
|
+
handle = declareOn(sink, domain, decl);
|
|
523
|
+
}
|
|
524
|
+
for (const issue of declared.issues) {
|
|
525
|
+
report.warning(issue.category, issue.code, issue.message, { line, element: field.text });
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
cell,
|
|
529
|
+
name: decl.name,
|
|
530
|
+
handle,
|
|
531
|
+
spec: declared.spec,
|
|
532
|
+
companionDecl: declared.companion,
|
|
533
|
+
companion: INVALID_INDEX,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/** Declare (or adopt) the labels column. */
|
|
537
|
+
ensureLabels() {
|
|
538
|
+
if (this.labelsHandle === INVALID_INDEX) {
|
|
539
|
+
this.labelsHandle = this.declareReserved("node", LABELS_DECL);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** Declare (or adopt) the relationship type column. */
|
|
543
|
+
ensureType() {
|
|
544
|
+
if (this.typeHandle === INVALID_INDEX) {
|
|
545
|
+
this.typeHandle = this.declareReserved("edge", TYPE_DECL);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/** Declare (or adopt) the id space column. */
|
|
549
|
+
ensureIdSpace() {
|
|
550
|
+
if (this.idSpaceHandle === INVALID_INDEX) {
|
|
551
|
+
this.idSpaceHandle = this.declareReserved("node", ID_SPACE_DECL);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Declare a reserved column: the same shape again shares it; a name taken by another shape is
|
|
556
|
+
* renamed `<name>#<origin.id>` and reported; a role the sink already holds elsewhere is dropped
|
|
557
|
+
* and reported.
|
|
558
|
+
* @param domain - node or edge
|
|
559
|
+
* @param decl - the reserved declaration
|
|
560
|
+
* @returns the handle
|
|
561
|
+
*/
|
|
562
|
+
declareReserved(domain, decl) {
|
|
563
|
+
const { sink, report } = this;
|
|
564
|
+
let current = decl;
|
|
565
|
+
for (;;) {
|
|
566
|
+
try {
|
|
567
|
+
return declareOn(sink, domain, current);
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
if (!(err instanceof GraphFormatError)) {
|
|
571
|
+
throw err;
|
|
572
|
+
}
|
|
573
|
+
if (err.code === "E_COLUMN_EXISTS") {
|
|
574
|
+
const name = uniqueColumnName(current.name, current.origin?.id ?? null, takenIn(sink, domain));
|
|
575
|
+
report.warning("coercion", RENAMED_CODE, `column "${current.name}" renamed to "${name}": the name was taken`, { element: current.name });
|
|
576
|
+
current = { ...current, name };
|
|
577
|
+
}
|
|
578
|
+
else if (err.code === "E_DUPLICATE_ROLE") {
|
|
579
|
+
report.warning("coercion", ROLE_TAKEN_CODE, `column "${current.name}" declared without role "${String(current.role)}": the sink already holds that role`, { element: current.name });
|
|
580
|
+
const { role: _role, ...withoutRole } = current;
|
|
581
|
+
current = withoutRole;
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
throw err;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Push one node row.
|
|
591
|
+
* @param section - the section
|
|
592
|
+
* @param reader - the reader positioned on the row
|
|
593
|
+
* @param count - the row's cell count
|
|
594
|
+
*/
|
|
595
|
+
nodeRow(section, reader, count) {
|
|
596
|
+
const { sink, report } = this;
|
|
597
|
+
const { cells, quoted, line } = reader;
|
|
598
|
+
if (count !== section.width) {
|
|
599
|
+
report.error("validation-error", COLUMN_COUNT_CODE, `row has ${count} cell(s) but the header has ${section.width}`, { line });
|
|
600
|
+
report.counts.skippedNodes++;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const idText = cells[section.idCell];
|
|
604
|
+
if (idText.length === 0 && !quoted[section.idCell]) {
|
|
605
|
+
// a quoted empty cell is the id "" (neo4j-admin: an empty quoted field is an empty string)
|
|
606
|
+
report.error("missing-value", MISSING_ID_CODE, "empty :ID cell", { line });
|
|
607
|
+
report.counts.skippedNodes++;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const id = this.coerceId(idText, line);
|
|
611
|
+
if (id === null || !this.parseProperties(section.properties, cells, quoted, line, idText)) {
|
|
612
|
+
report.counts.skippedNodes++;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
let labels;
|
|
616
|
+
if (section.labelCells.length > 0 || section.extraLabels.length > 0) {
|
|
617
|
+
labels = this.labelsOf(section, cells, quoted);
|
|
618
|
+
}
|
|
619
|
+
const index = sink.addNode(id);
|
|
620
|
+
const status = this.registry.declare(index, section.spaceCode);
|
|
621
|
+
if (status === "collision") {
|
|
622
|
+
report.error("validation-error", ID_SPACE_COLLISION_CODE, `node ${idText} is declared in id space ${section.space ?? "(none)"} and in another id space; the core has one id space and this row is skipped`, { line, element: idText });
|
|
623
|
+
report.counts.skippedNodes++;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (status === "duplicate") {
|
|
627
|
+
report.warning("merged", DUPLICATE_NODE_CODE, `node ${idText} is declared twice; the later properties win`, {
|
|
628
|
+
line,
|
|
629
|
+
element: idText,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (section.idHandle !== INVALID_INDEX) {
|
|
633
|
+
sink.setNodeValue(section.idHandle, index, idText);
|
|
634
|
+
}
|
|
635
|
+
if (section.space !== null) {
|
|
636
|
+
sink.setNodeValue(this.idSpaceHandle, index, section.space);
|
|
637
|
+
}
|
|
638
|
+
if (labels !== undefined) {
|
|
639
|
+
sink.setNodeValue(this.labelsHandle, index, labels);
|
|
640
|
+
}
|
|
641
|
+
this.writeProperties("node", section.properties, index);
|
|
642
|
+
this.reportPrecision(line, idText);
|
|
643
|
+
report.counts.nodes++;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Push one relationship row.
|
|
647
|
+
* @param section - the section
|
|
648
|
+
* @param reader - the reader positioned on the row
|
|
649
|
+
* @param count - the row's cell count
|
|
650
|
+
*/
|
|
651
|
+
relationshipRow(section, reader, count) {
|
|
652
|
+
const { sink, report } = this;
|
|
653
|
+
const { cells, quoted, line } = reader;
|
|
654
|
+
if (!this.headerSet) {
|
|
655
|
+
// a Neo4j file is directed by definition ("all relationships have a direction"); the sink's
|
|
656
|
+
// direction is set once, before the first relationship, so a node-only file leaves it alone
|
|
657
|
+
this.headerSet = true;
|
|
658
|
+
this.direction.setHeader(true, { line });
|
|
659
|
+
}
|
|
660
|
+
if (count !== section.width) {
|
|
661
|
+
report.error("validation-error", COLUMN_COUNT_CODE, `row has ${count} cell(s) but the header has ${section.width}`, { line });
|
|
662
|
+
report.counts.skippedEdges++;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const startText = cells[section.startCell];
|
|
666
|
+
const endText = cells[section.endCell];
|
|
667
|
+
const startMissing = startText.length === 0 && !quoted[section.startCell];
|
|
668
|
+
if (startMissing || (endText.length === 0 && !quoted[section.endCell])) {
|
|
669
|
+
report.error("missing-value", MISSING_ENDPOINT_CODE, `empty ${startMissing ? ":START_ID" : ":END_ID"} cell`, {
|
|
670
|
+
line,
|
|
671
|
+
});
|
|
672
|
+
report.counts.skippedEdges++;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const element = `${startText}->${endText}`;
|
|
676
|
+
const source = this.coerceId(startText, line);
|
|
677
|
+
const target = source === null ? null : this.coerceId(endText, line);
|
|
678
|
+
if (source === null || target === null) {
|
|
679
|
+
report.counts.skippedEdges++;
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
let weight;
|
|
683
|
+
if (section.weightCell >= 0) {
|
|
684
|
+
try {
|
|
685
|
+
weight = parseWeightText(cells[section.weightCell]);
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
report.recordError(err, { line, element });
|
|
689
|
+
report.counts.skippedEdges++;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (!this.parseProperties(section.properties, cells, quoted, line, element)) {
|
|
694
|
+
report.counts.skippedEdges++;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
let edge;
|
|
698
|
+
try {
|
|
699
|
+
edge = this.direction.addEdge(source, target, "directed", weight, { line, element });
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
report.recordError(err, { line, element });
|
|
703
|
+
report.counts.skippedEdges++;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (section.typeCell >= 0) {
|
|
707
|
+
const type = cells[section.typeCell];
|
|
708
|
+
if (type.length > 0) {
|
|
709
|
+
sink.setEdgeValue(this.typeHandle, edge, type);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
this.writeProperties("edge", section.properties, edge);
|
|
713
|
+
this.reportPrecision(line, element);
|
|
714
|
+
report.counts.edges++;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Coerce an id cell, reporting a merge under `ids: "number"` and an invalid id as an error.
|
|
718
|
+
* @param text - the cell
|
|
719
|
+
* @param line - the row's line
|
|
720
|
+
* @returns the id, or null when the cell was rejected (the error is recorded)
|
|
721
|
+
*/
|
|
722
|
+
coerceId(text, line) {
|
|
723
|
+
const { coercer, report } = this;
|
|
724
|
+
let id;
|
|
725
|
+
try {
|
|
726
|
+
id = coercer.text(text);
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
report.recordError(err, { line, element: text });
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const merge = coercer.lastMerge;
|
|
733
|
+
if (merge !== null) {
|
|
734
|
+
report.warning("coercion", ID_MERGED_CODE, `id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`, { line, element: text });
|
|
735
|
+
}
|
|
736
|
+
return id;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Parse the property cells of a row into the scratch arrays.
|
|
740
|
+
* @param slots - the section's property slots
|
|
741
|
+
* @param cells - the row's cells
|
|
742
|
+
* @param quoted - whether each cell was quoted
|
|
743
|
+
* @param line - the row's line
|
|
744
|
+
* @param element - the row's element name for issues
|
|
745
|
+
* @returns true when every cell parsed; false after recording the first error
|
|
746
|
+
*/
|
|
747
|
+
parseProperties(slots, cells, quoted, line, element) {
|
|
748
|
+
const { values, texts, precisionSlots } = this;
|
|
749
|
+
precisionSlots.length = 0;
|
|
750
|
+
for (const slot of slots) {
|
|
751
|
+
const text = cells[slot.cell];
|
|
752
|
+
texts[slot.cell] = null;
|
|
753
|
+
if (text.length === 0) {
|
|
754
|
+
values[slot.cell] = quoted[slot.cell] ? emptyValue(slot.spec) : undefined;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const { spec } = slot;
|
|
758
|
+
try {
|
|
759
|
+
if (spec.temporal !== null && !spec.list) {
|
|
760
|
+
const parsed = parseDeclaredTemporal(text, spec);
|
|
761
|
+
values[slot.cell] = parsed.value;
|
|
762
|
+
texts[slot.cell] = parsed.text;
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
values[slot.cell] = parseDeclaredValue(text, spec, this.options.listSyntax);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
this.report.recordError(err, { line, element: `${element} ${slot.name}` });
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (spec.precision && losesPrecision(spec, text)) {
|
|
773
|
+
precisionSlots.push(slot);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Write the parsed property values of a row.
|
|
780
|
+
* @param domain - node or edge
|
|
781
|
+
* @param slots - the section's property slots
|
|
782
|
+
* @param row - the node or edge index
|
|
783
|
+
*/
|
|
784
|
+
writeProperties(domain, slots, row) {
|
|
785
|
+
const { sink, values, texts } = this;
|
|
786
|
+
for (const slot of slots) {
|
|
787
|
+
const value = values[slot.cell];
|
|
788
|
+
if (value === undefined) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const text = texts[slot.cell];
|
|
792
|
+
if (text !== null && slot.companion === INVALID_INDEX && slot.companionDecl !== null) {
|
|
793
|
+
slot.companion = declareCompanion(sink, domain, slot.companionDecl);
|
|
794
|
+
}
|
|
795
|
+
if (domain === "node") {
|
|
796
|
+
sink.setNodeValue(slot.handle, row, value);
|
|
797
|
+
if (text !== null) {
|
|
798
|
+
sink.setNodeValue(slot.companion, row, text);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
sink.setEdgeValue(slot.handle, row, value);
|
|
803
|
+
if (text !== null) {
|
|
804
|
+
sink.setEdgeValue(slot.companion, row, text);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Report the precision losses of the row just accepted.
|
|
811
|
+
* @param line - the row's line
|
|
812
|
+
* @param element - the row's element name
|
|
813
|
+
*/
|
|
814
|
+
reportPrecision(line, element) {
|
|
815
|
+
for (const slot of this.precisionSlots) {
|
|
816
|
+
this.report.warning("precision", PRECISION_CODE, `long value of "${slot.name}" is beyond 2^53 and was stored as the nearest f64`, { line, element });
|
|
817
|
+
}
|
|
818
|
+
this.precisionSlots.length = 0;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* The labels of a node row: every `:LABEL` cell split by the array delimiter (empty items
|
|
822
|
+
* dropped), plus the header's `{label:...}` options.
|
|
823
|
+
* @param section - the section
|
|
824
|
+
* @param cells - the row's cells
|
|
825
|
+
* @param quoted - whether each cell was quoted
|
|
826
|
+
* @returns the labels, or undefined when every label cell is unset and no extra label exists
|
|
827
|
+
*/
|
|
828
|
+
labelsOf(section, cells, quoted) {
|
|
829
|
+
let labels;
|
|
830
|
+
for (const cell of section.labelCells) {
|
|
831
|
+
const text = cells[cell];
|
|
832
|
+
if (text.length === 0 && !quoted[cell]) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (labels === undefined) {
|
|
836
|
+
labels = [];
|
|
837
|
+
}
|
|
838
|
+
for (const item of splitListText(text, this.options.listSyntax)) {
|
|
839
|
+
if (item.length > 0) {
|
|
840
|
+
labels.push(item);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (section.extraLabels.length > 0) {
|
|
845
|
+
if (labels === undefined) {
|
|
846
|
+
labels = [];
|
|
847
|
+
}
|
|
848
|
+
labels.push(...section.extraLabels);
|
|
849
|
+
}
|
|
850
|
+
return labels;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* The value of a quoted empty cell: an empty string for a string-kind property, an empty list for
|
|
855
|
+
* a list property, unset for anything else (neo4j-admin's default `--ignore-empty-strings=false`).
|
|
856
|
+
* @param spec - the property's declared type
|
|
857
|
+
* @returns the value, or undefined for unset
|
|
858
|
+
*/
|
|
859
|
+
function emptyValue(spec) {
|
|
860
|
+
if (spec.list) {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
if (spec.kind === "string" || spec.kind === "duration") {
|
|
864
|
+
return "";
|
|
865
|
+
}
|
|
866
|
+
return undefined;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Check that no two PROPERTY (or stored-id) fields of one header share a name.
|
|
870
|
+
* @param fields - the parsed header
|
|
871
|
+
*/
|
|
872
|
+
function checkPropertyNames(fields) {
|
|
873
|
+
const seen = new Set();
|
|
874
|
+
for (const field of fields) {
|
|
875
|
+
if (field.name.length === 0 || field.kind === "IGNORE") {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (seen.has(field.name)) {
|
|
879
|
+
throw headerError(`property "${field.name}" is declared twice`);
|
|
880
|
+
}
|
|
881
|
+
seen.add(field.name);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* The error of a malformed section header.
|
|
886
|
+
* @param reason - why
|
|
887
|
+
* @returns the error
|
|
888
|
+
*/
|
|
889
|
+
function headerError(reason) {
|
|
890
|
+
return new GraphFormatError("E_UNSUPPORTED", reason, { reason: "header" });
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Confidence that a head of bytes is a neo4j-admin CSV: the first line holds an `:ID`,
|
|
894
|
+
* `:START_ID` or `:END_ID` header cell.
|
|
895
|
+
* @param head - the first bytes of the input
|
|
896
|
+
* @returns 0.95 for a Neo4j header, 0 otherwise
|
|
897
|
+
*/
|
|
898
|
+
function sniffNeo4j(head) {
|
|
899
|
+
const text = new TextDecoder("utf-8", { fatal: false, ignoreBOM: false }).decode(head);
|
|
900
|
+
const end = text.search(/[\r\n]/);
|
|
901
|
+
const first = end < 0 ? text : text.slice(0, end);
|
|
902
|
+
const cells = first.split(/[,;|\t]/);
|
|
903
|
+
return isHeaderRecord(cells, cells.length) ? 0.95 : 0;
|
|
904
|
+
}
|
|
905
|
+
/** The Neo4j importer. */
|
|
906
|
+
export const neo4jImporter = Object.freeze({
|
|
907
|
+
format: NEO4J,
|
|
908
|
+
extensions: Object.freeze([".csv", ".tsv"]),
|
|
909
|
+
mimeTypes: Object.freeze(["text/csv", "text/tab-separated-values"]),
|
|
910
|
+
sniff: sniffNeo4j,
|
|
911
|
+
/**
|
|
912
|
+
* Read one or more neo4j-admin CSV inputs into the sink.
|
|
913
|
+
* @param input - the primary input (a node table, a relationship table, or sections of both)
|
|
914
|
+
* @param sink - the sink
|
|
915
|
+
* @param options - format-specific and common options
|
|
916
|
+
* @returns the report; ImportError beyond the error limit or on a malformed header
|
|
917
|
+
*/
|
|
918
|
+
async import(input, sink, options) {
|
|
919
|
+
const common = resolveImportOptions(options, { ids: "canonical", defaultDirected: true, weightFrom: "weight" });
|
|
920
|
+
const format = resolveNeo4jOptions(options);
|
|
921
|
+
const report = new ImportReportBuilder(NEO4J, common.errorLimit);
|
|
922
|
+
reportSinkOptions(sink, options, report);
|
|
923
|
+
// nodeIdFrom and defaultDirected are among the reported options: Neo4j ids are the :ID
|
|
924
|
+
// column and every relationship is directed
|
|
925
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
926
|
+
const session = new Neo4jImportSession(sink, report, common, format);
|
|
927
|
+
await session.run([input, ...format.nodes, ...format.relationships]);
|
|
928
|
+
throwIfAborted(common.signal);
|
|
929
|
+
return report.finish();
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
//# sourceMappingURL=importer.js.map
|