@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
package/dist/neo4j.js
ADDED
|
@@ -0,0 +1,2046 @@
|
|
|
1
|
+
import { V as ROLE_TAKEN_CODE$1, n as ID_MERGED_CODE$1, k as DUPLICATE_NODE_CODE$1, M as MISSING_ENDPOINT_CODE$1, w as MISSING_ID_CODE$1, aC as resolveImportOptions, I as ImportReportBuilder, az as reportSinkOptions, aA as reportUnusedOptions, a as throwIfAborted, s as IdCoercer, l as DirectionResolver, u as ImportError, aL as declareAttribute, a_ as declareOn, aI as uniqueColumnName, S as RENAMED_CODE, ax as parseWeightText, aM as parseDeclaredTemporal, aN as parseDeclaredValue, aO as losesPrecision, ae as declareCompanion, R as PRECISION_CODE, aQ as splitListText, ar as isImportInput, aZ as takenIn, ao as inputLength, a9 as capabilities, o as ID_TEXT_COLLISION_CODE, p as ID_TEXT_TYPE_CODE, au as joinText, ah as encodeChunks, aB as resolveExportOptions, aa as checkCapabilities, aw as pairFolding, L as LOSS, aU as mapDeclaredType, ai as explicitWeights, aq as isCanonicalIntegerText, aW as formatTemporal, al as formatF64, ak as formatF32, an as formatInteger, W as SINK_OPTION_CODE, J as OPTION_IGNORED_CODE, C as COLUMN_RENAMED_CODE, a1 as UNKNOWN_ATTR_TYPE_CODE, r as INVALID_UTF8_CODE, a5 as WEIGHT_KEY_CLASH_CODE, U as ROLE_DROPPED_CODE, F as MUTUAL_EXPANDED_CODE } from "./chunks/writer-DxSKC7TL.js";
|
|
2
|
+
import { c as checkRecordSyntax, R as RecordReader, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-CGpxszm1.js";
|
|
3
|
+
import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
|
|
4
|
+
import { m as quoteCsvCell } from "./chunks/escape-DyI8JofU.js";
|
|
5
|
+
const STRUCTURAL = /* @__PURE__ */ new Set(["ID", "LABEL", "START_ID", "END_ID", "TYPE", "IGNORE"]);
|
|
6
|
+
const SPACE_KINDS = /* @__PURE__ */ new Set(["ID", "START_ID", "END_ID"]);
|
|
7
|
+
const HEADER_MARKER = /^[^:{}()]*:\s*(ID|START_ID|END_ID)\s*(\(|\{|$)/i;
|
|
8
|
+
const CELL = /^([^:(){}]*)(?::([^:(){}]*))?(?:\(([^)]*)\))?(?:\{([^}]*)\})?$/;
|
|
9
|
+
function isHeaderRecord(cells, count) {
|
|
10
|
+
for (let i = 0; i < count; i++) {
|
|
11
|
+
const cell = cells[i];
|
|
12
|
+
if (cell.indexOf(":") >= 0 && HEADER_MARKER.test(cell.trim())) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function parseHeaderField(text) {
|
|
19
|
+
const trimmed = text.trim();
|
|
20
|
+
const m = CELL.exec(trimmed);
|
|
21
|
+
if (m === null) {
|
|
22
|
+
throw headerError$1(text, "not a neo4j header cell");
|
|
23
|
+
}
|
|
24
|
+
const name = m[1].trim();
|
|
25
|
+
const rawType = m[2] === void 0 ? null : m[2].trim();
|
|
26
|
+
const space = m[3] === void 0 ? null : m[3].trim();
|
|
27
|
+
const options = parseOptions(text, m[4]);
|
|
28
|
+
if (rawType === null) {
|
|
29
|
+
if (name.length === 0) {
|
|
30
|
+
throw headerError$1(text, "an empty header cell");
|
|
31
|
+
}
|
|
32
|
+
if (space !== null || options.size > 0) {
|
|
33
|
+
throw headerError$1(text, "an id space or options need a field type");
|
|
34
|
+
}
|
|
35
|
+
return { text, name, kind: "PROPERTY", type: null, space: null, options };
|
|
36
|
+
}
|
|
37
|
+
if (rawType.length === 0) {
|
|
38
|
+
throw headerError$1(text, "an empty field type");
|
|
39
|
+
}
|
|
40
|
+
const upper = rawType.toUpperCase();
|
|
41
|
+
if (STRUCTURAL.has(upper)) {
|
|
42
|
+
const kind = upper;
|
|
43
|
+
if (space !== null && !SPACE_KINDS.has(kind)) {
|
|
44
|
+
throw headerError$1(text, `an id space is only allowed on ID, START_ID and END_ID, not ${kind}`);
|
|
45
|
+
}
|
|
46
|
+
if (name.length > 0 && kind !== "ID" && kind !== "IGNORE") {
|
|
47
|
+
throw headerError$1(text, `a ${kind} field cannot be stored as a property`);
|
|
48
|
+
}
|
|
49
|
+
return { text, name, kind, type: null, space: space === "" ? null : space, options };
|
|
50
|
+
}
|
|
51
|
+
if (name.length === 0) {
|
|
52
|
+
throw headerError$1(text, "a property column needs a name");
|
|
53
|
+
}
|
|
54
|
+
if (space !== null) {
|
|
55
|
+
throw headerError$1(text, "an id space is only allowed on ID, START_ID and END_ID");
|
|
56
|
+
}
|
|
57
|
+
return { text, name, kind: "PROPERTY", type: rawType, space: null, options };
|
|
58
|
+
}
|
|
59
|
+
function parseOptions(text, body) {
|
|
60
|
+
const options = /* @__PURE__ */ new Map();
|
|
61
|
+
if (body === void 0 || body.trim().length === 0) {
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
for (const part of body.split(",")) {
|
|
65
|
+
const colon = part.indexOf(":");
|
|
66
|
+
if (colon < 0) {
|
|
67
|
+
throw headerError$1(text, `option "${part.trim()}" is not key:value`);
|
|
68
|
+
}
|
|
69
|
+
const key = part.slice(0, colon).trim().toLowerCase();
|
|
70
|
+
const value = part.slice(colon + 1).trim();
|
|
71
|
+
if (key.length === 0) {
|
|
72
|
+
throw headerError$1(text, "an option needs a key");
|
|
73
|
+
}
|
|
74
|
+
options.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
return options;
|
|
77
|
+
}
|
|
78
|
+
function headerError$1(text, reason) {
|
|
79
|
+
return new GraphFormatError("E_UNSUPPORTED", `header cell "${text}": ${reason}`, {
|
|
80
|
+
reason: "header",
|
|
81
|
+
cell: text,
|
|
82
|
+
detail: reason
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function formatHeaderField(name, type, space) {
|
|
86
|
+
const base = `${name}:${type}`;
|
|
87
|
+
return space === null ? base : `${base}(${space})`;
|
|
88
|
+
}
|
|
89
|
+
const LABELS_COLUMN = "labels";
|
|
90
|
+
const TYPE_COLUMN = "type";
|
|
91
|
+
const ID_SPACE_COLUMN = "idSpace";
|
|
92
|
+
const HEADER_CODE = "E_NEO4J_HEADER";
|
|
93
|
+
const COLUMN_COUNT_CODE = "E_NEO4J_COLUMN_COUNT";
|
|
94
|
+
const MISSING_ID_CODE = MISSING_ID_CODE$1;
|
|
95
|
+
const MISSING_ENDPOINT_CODE = MISSING_ENDPOINT_CODE$1;
|
|
96
|
+
const DUPLICATE_NODE_CODE = DUPLICATE_NODE_CODE$1;
|
|
97
|
+
const ID_SPACE_COLLISION_CODE = "E_NEO4J_ID_SPACE_COLLISION";
|
|
98
|
+
const ID_MERGED_CODE = ID_MERGED_CODE$1;
|
|
99
|
+
const HEADER_OPTION_CODE = "W_NEO4J_HEADER_OPTION_IGNORED";
|
|
100
|
+
const ROLE_TAKEN_CODE = ROLE_TAKEN_CODE$1;
|
|
101
|
+
const IGNORED_COLUMNS_LOSS = "W_NEO4J_IGNORED_COLUMNS";
|
|
102
|
+
const USED_OPTIONS = /* @__PURE__ */ new Set([
|
|
103
|
+
"ids",
|
|
104
|
+
"addMissingNodes",
|
|
105
|
+
"duplicateEdges",
|
|
106
|
+
"selfLoops",
|
|
107
|
+
"onMixedDirection",
|
|
108
|
+
"weightFrom",
|
|
109
|
+
"weightDtype",
|
|
110
|
+
"long",
|
|
111
|
+
"errorLimit",
|
|
112
|
+
"signal",
|
|
113
|
+
"onProgress"
|
|
114
|
+
]);
|
|
115
|
+
const NEO4J$1 = "neo4j";
|
|
116
|
+
const ABORT_CHECK_INTERVAL = 64;
|
|
117
|
+
const LABELS_DECL = {
|
|
118
|
+
name: LABELS_COLUMN,
|
|
119
|
+
dtype: "list",
|
|
120
|
+
itemDtype: "dict",
|
|
121
|
+
nullable: true,
|
|
122
|
+
role: "labels",
|
|
123
|
+
origin: { format: NEO4J$1, id: ":LABEL", title: null, type: "LABEL", namespace: null }
|
|
124
|
+
};
|
|
125
|
+
const TYPE_DECL = {
|
|
126
|
+
name: TYPE_COLUMN,
|
|
127
|
+
dtype: "dict",
|
|
128
|
+
nullable: true,
|
|
129
|
+
role: "kind",
|
|
130
|
+
origin: { format: NEO4J$1, id: ":TYPE", title: null, type: "TYPE", namespace: null }
|
|
131
|
+
};
|
|
132
|
+
const ID_SPACE_DECL = {
|
|
133
|
+
name: ID_SPACE_COLUMN,
|
|
134
|
+
dtype: "dict",
|
|
135
|
+
nullable: true,
|
|
136
|
+
role: "idSpace",
|
|
137
|
+
origin: { format: NEO4J$1, id: ":ID", title: null, type: "ID", namespace: null }
|
|
138
|
+
};
|
|
139
|
+
const ARRAY_DELIMITERS = { ";": "semicolon", ",": "comma", "|": "pipe" };
|
|
140
|
+
const NEO4J_DELIMITER_CANDIDATES = Object.freeze([",", " "]);
|
|
141
|
+
function resolveNeo4jOptions(options) {
|
|
142
|
+
const o = options ?? {};
|
|
143
|
+
const arrayDelimiter = o.arrayDelimiter ?? ";";
|
|
144
|
+
const listSyntax = ARRAY_DELIMITERS[arrayDelimiter];
|
|
145
|
+
if (typeof arrayDelimiter !== "string" || listSyntax === void 0) {
|
|
146
|
+
throw new GraphFormatError(
|
|
147
|
+
"E_UNSUPPORTED",
|
|
148
|
+
`option arrayDelimiter: ${JSON.stringify(arrayDelimiter)} is not one of ";", ",", "|"`,
|
|
149
|
+
{ option: "arrayDelimiter", found: arrayDelimiter }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const syntax = checkRecordSyntax({
|
|
153
|
+
delimiter: o.delimiter ?? null,
|
|
154
|
+
quote: o.quote ?? '"',
|
|
155
|
+
candidates: NEO4J_DELIMITER_CANDIDATES.filter((d) => d !== arrayDelimiter)
|
|
156
|
+
});
|
|
157
|
+
if (syntax.delimiter === arrayDelimiter) {
|
|
158
|
+
throw new GraphFormatError("E_UNSUPPORTED", "options delimiter and arrayDelimiter must differ", {
|
|
159
|
+
option: "arrayDelimiter",
|
|
160
|
+
found: arrayDelimiter
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
nodes: inputList("nodes", o.nodes),
|
|
165
|
+
relationships: inputList("relationships", o.relationships),
|
|
166
|
+
syntax,
|
|
167
|
+
listSyntax
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function inputList(name, value) {
|
|
171
|
+
if (value === void 0 || value === null) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const list = Array.isArray(value) ? value : [value];
|
|
175
|
+
for (const item of list) {
|
|
176
|
+
if (!isImportInput(item)) {
|
|
177
|
+
throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an entry is not an ImportInput`, {
|
|
178
|
+
option: name,
|
|
179
|
+
found: typeof item
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return list;
|
|
184
|
+
}
|
|
185
|
+
class NodeRegistry {
|
|
186
|
+
constructor() {
|
|
187
|
+
this.codes = new Uint32Array(1024);
|
|
188
|
+
this.spaces = /* @__PURE__ */ new Map();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The code of an id space (1 for "no space").
|
|
192
|
+
* @param space - the space name or null
|
|
193
|
+
* @returns a code >= 1
|
|
194
|
+
*/
|
|
195
|
+
codeOf(space) {
|
|
196
|
+
let code = this.spaces.get(space);
|
|
197
|
+
if (code === void 0) {
|
|
198
|
+
code = this.spaces.size + 1;
|
|
199
|
+
this.spaces.set(space, code);
|
|
200
|
+
}
|
|
201
|
+
return code;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Record that a node row declared a node.
|
|
205
|
+
* @param index - the node index
|
|
206
|
+
* @param code - the space code
|
|
207
|
+
* @returns "new" for a first declaration, "duplicate" for a repeat in the same space, "collision" across spaces
|
|
208
|
+
*/
|
|
209
|
+
declare(index, code) {
|
|
210
|
+
if (index >= this.codes.length) {
|
|
211
|
+
let size = this.codes.length * 2;
|
|
212
|
+
while (size <= index) {
|
|
213
|
+
size *= 2;
|
|
214
|
+
}
|
|
215
|
+
const grown = new Uint32Array(size);
|
|
216
|
+
grown.set(this.codes);
|
|
217
|
+
this.codes = grown;
|
|
218
|
+
}
|
|
219
|
+
const previous = this.codes[index];
|
|
220
|
+
if (previous === 0) {
|
|
221
|
+
this.codes[index] = code;
|
|
222
|
+
return "new";
|
|
223
|
+
}
|
|
224
|
+
return previous === code ? "duplicate" : "collision";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
class ProgressTracker {
|
|
228
|
+
/**
|
|
229
|
+
* Create a tracker.
|
|
230
|
+
* @param callback - the caller's onProgress, or null
|
|
231
|
+
* @param inputs - every input in reading order
|
|
232
|
+
*/
|
|
233
|
+
constructor(callback, inputs) {
|
|
234
|
+
this.offset = 0;
|
|
235
|
+
this.lastDone = 0;
|
|
236
|
+
this.callback = callback;
|
|
237
|
+
let total = 0;
|
|
238
|
+
for (const input of inputs) {
|
|
239
|
+
const length = inputLength(input);
|
|
240
|
+
if (length === null) {
|
|
241
|
+
total = void 0;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
total += length;
|
|
245
|
+
}
|
|
246
|
+
this.total = total;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The read options for one input.
|
|
250
|
+
* @param signal - the cancellation signal
|
|
251
|
+
* @returns options whose onProgress reports cumulative bytes
|
|
252
|
+
*/
|
|
253
|
+
optionsFor(signal) {
|
|
254
|
+
const { callback } = this;
|
|
255
|
+
if (callback === null) {
|
|
256
|
+
return { signal };
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
signal,
|
|
260
|
+
onProgress: (done) => {
|
|
261
|
+
this.lastDone = done;
|
|
262
|
+
callback(this.offset + done, this.total);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Move the offset past the input just finished. */
|
|
267
|
+
finishInput() {
|
|
268
|
+
this.offset += this.lastDone;
|
|
269
|
+
this.lastDone = 0;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
class Neo4jImportSession {
|
|
273
|
+
/**
|
|
274
|
+
* Create a session.
|
|
275
|
+
* @param sink - the sink
|
|
276
|
+
* @param report - the report
|
|
277
|
+
* @param common - the resolved common options
|
|
278
|
+
* @param options - the resolved format options
|
|
279
|
+
*/
|
|
280
|
+
constructor(sink, report, common, options) {
|
|
281
|
+
this.registry = new NodeRegistry();
|
|
282
|
+
this.labelsHandle = INVALID_INDEX;
|
|
283
|
+
this.typeHandle = INVALID_INDEX;
|
|
284
|
+
this.idSpaceHandle = INVALID_INDEX;
|
|
285
|
+
this.ignoredColumns = 0;
|
|
286
|
+
this.headerSet = false;
|
|
287
|
+
this.values = [];
|
|
288
|
+
this.texts = [];
|
|
289
|
+
this.precisionSlots = [];
|
|
290
|
+
this.sink = sink;
|
|
291
|
+
this.report = report;
|
|
292
|
+
this.common = common;
|
|
293
|
+
this.options = options;
|
|
294
|
+
this.coercer = new IdCoercer(common.ids);
|
|
295
|
+
this.direction = new DirectionResolver(sink, report, common.onMixedDirection);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Read every input.
|
|
299
|
+
* @param inputs - the inputs in reading order
|
|
300
|
+
*/
|
|
301
|
+
async run(inputs) {
|
|
302
|
+
const progress = new ProgressTracker(this.common.onProgress, inputs);
|
|
303
|
+
for (const input of inputs) {
|
|
304
|
+
await this.readInput(input, progress.optionsFor(this.common.signal));
|
|
305
|
+
progress.finishInput();
|
|
306
|
+
}
|
|
307
|
+
if (this.ignoredColumns > 0) {
|
|
308
|
+
this.report.loss(
|
|
309
|
+
IGNORED_COLUMNS_LOSS,
|
|
310
|
+
`${this.ignoredColumns} :IGNORE column(s) were skipped as the header instructs`,
|
|
311
|
+
null,
|
|
312
|
+
this.ignoredColumns
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Read one input: a header row, then data rows until the next header row.
|
|
318
|
+
* @param input - the input
|
|
319
|
+
* @param readOptions - cancellation and progress
|
|
320
|
+
*/
|
|
321
|
+
async readInput(input, readOptions) {
|
|
322
|
+
const reader = new RecordReader(input, this.report, this.options.syntax, readOptions);
|
|
323
|
+
let section = null;
|
|
324
|
+
let sinceCheck = 0;
|
|
325
|
+
for await (const count of reader) {
|
|
326
|
+
if (section === null || isHeaderRecord(reader.cells, count)) {
|
|
327
|
+
section = this.declareSection(reader, count);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (section.kind === "node") {
|
|
331
|
+
this.nodeRow(section, reader, count);
|
|
332
|
+
} else {
|
|
333
|
+
this.relationshipRow(section, reader, count);
|
|
334
|
+
}
|
|
335
|
+
if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
336
|
+
sinceCheck = 0;
|
|
337
|
+
throwIfAborted(readOptions.signal);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (section === null) {
|
|
341
|
+
this.report.fail(HEADER_CODE, "the input has no header row", { line: 1 });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Interpret a header row: parse every cell, check the section shape, declare its columns.
|
|
346
|
+
* @param reader - the reader positioned on the header
|
|
347
|
+
* @param count - the number of header cells
|
|
348
|
+
* @returns the section
|
|
349
|
+
*/
|
|
350
|
+
declareSection(reader, count) {
|
|
351
|
+
const { line } = reader;
|
|
352
|
+
const fields = [];
|
|
353
|
+
try {
|
|
354
|
+
for (let i = 0; i < count; i++) {
|
|
355
|
+
fields.push(parseHeaderField(reader.cells[i]));
|
|
356
|
+
}
|
|
357
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
358
|
+
fields.forEach((field, i) => {
|
|
359
|
+
const list = kinds.get(field.kind);
|
|
360
|
+
if (list === void 0) {
|
|
361
|
+
kinds.set(field.kind, [i]);
|
|
362
|
+
} else {
|
|
363
|
+
list.push(i);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
const ids = kinds.get("ID") ?? [];
|
|
367
|
+
const starts = kinds.get("START_ID") ?? [];
|
|
368
|
+
const ends = kinds.get("END_ID") ?? [];
|
|
369
|
+
const isNode = ids.length > 0;
|
|
370
|
+
const isRelationship = starts.length > 0 || ends.length > 0;
|
|
371
|
+
if (isNode && isRelationship) {
|
|
372
|
+
throw headerError("a header mixes :ID with :START_ID / :END_ID");
|
|
373
|
+
}
|
|
374
|
+
if (!isNode && !isRelationship) {
|
|
375
|
+
throw headerError(
|
|
376
|
+
"a header needs an :ID column (nodes) or :START_ID and :END_ID columns (relationships)"
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
checkPropertyNames(fields);
|
|
380
|
+
this.ignoredColumns += (kinds.get("IGNORE") ?? []).length;
|
|
381
|
+
if (isNode) {
|
|
382
|
+
if (ids.length > 1) {
|
|
383
|
+
throw headerError("a node header has more than one :ID column");
|
|
384
|
+
}
|
|
385
|
+
if (kinds.has("TYPE")) {
|
|
386
|
+
throw headerError("a node header cannot have a :TYPE column");
|
|
387
|
+
}
|
|
388
|
+
return this.declareNodeSection(fields, ids[0], kinds.get("LABEL") ?? [], line);
|
|
389
|
+
}
|
|
390
|
+
if (starts.length !== 1 || ends.length !== 1) {
|
|
391
|
+
throw headerError("a relationship header needs exactly one :START_ID and one :END_ID column");
|
|
392
|
+
}
|
|
393
|
+
if (kinds.has("LABEL")) {
|
|
394
|
+
throw headerError("a relationship header cannot have a :LABEL column");
|
|
395
|
+
}
|
|
396
|
+
const types = kinds.get("TYPE") ?? [];
|
|
397
|
+
if (types.length > 1) {
|
|
398
|
+
throw headerError("a relationship header has more than one :TYPE column");
|
|
399
|
+
}
|
|
400
|
+
return this.declareRelationshipSection(
|
|
401
|
+
fields,
|
|
402
|
+
starts[0],
|
|
403
|
+
ends[0],
|
|
404
|
+
types.length === 1 ? types[0] : -1,
|
|
405
|
+
line
|
|
406
|
+
);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
if (err instanceof GraphFormatError && !(err instanceof ImportError)) {
|
|
409
|
+
this.report.fail(HEADER_CODE, `line ${line}: ${err.message}`, { line }, { cause: err.code });
|
|
410
|
+
}
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Declare the columns of a node section.
|
|
416
|
+
* @param fields - the parsed header
|
|
417
|
+
* @param idCell - the `:ID` cell
|
|
418
|
+
* @param labelCells - the `:LABEL` cells
|
|
419
|
+
* @param line - the header line
|
|
420
|
+
* @returns the section
|
|
421
|
+
*/
|
|
422
|
+
declareNodeSection(fields, idCell, labelCells, line) {
|
|
423
|
+
const idField = fields[idCell];
|
|
424
|
+
const extraLabels = [];
|
|
425
|
+
for (const field of fields) {
|
|
426
|
+
for (const [key, value] of field.options) {
|
|
427
|
+
if (key === "label" && field.kind === "ID") {
|
|
428
|
+
extraLabels.push(value);
|
|
429
|
+
} else {
|
|
430
|
+
this.report.warning(
|
|
431
|
+
"unsupported",
|
|
432
|
+
HEADER_OPTION_CODE,
|
|
433
|
+
`header option ${key}:${value} of "${field.text}" is ignored`,
|
|
434
|
+
{ line, element: field.text }
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (labelCells.length > 0 || extraLabels.length > 0) {
|
|
440
|
+
this.ensureLabels();
|
|
441
|
+
}
|
|
442
|
+
if (idField.space !== null) {
|
|
443
|
+
this.ensureIdSpace();
|
|
444
|
+
}
|
|
445
|
+
let idHandle = INVALID_INDEX;
|
|
446
|
+
if (idField.name.length > 0) {
|
|
447
|
+
idHandle = this.declareProperty(
|
|
448
|
+
"node",
|
|
449
|
+
{ ...idField, type: null },
|
|
450
|
+
idCell,
|
|
451
|
+
{ origin: { format: NEO4J$1, id: idField.name, title: null, type: "ID", namespace: idField.space } },
|
|
452
|
+
line
|
|
453
|
+
).handle;
|
|
454
|
+
}
|
|
455
|
+
const properties = this.declareProperties("node", fields, line, null);
|
|
456
|
+
return {
|
|
457
|
+
kind: "node",
|
|
458
|
+
width: fields.length,
|
|
459
|
+
idCell,
|
|
460
|
+
idHandle,
|
|
461
|
+
space: idField.space,
|
|
462
|
+
spaceCode: this.registry.codeOf(idField.space),
|
|
463
|
+
labelCells,
|
|
464
|
+
extraLabels,
|
|
465
|
+
properties
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Declare the columns of a relationship section.
|
|
470
|
+
* @param fields - the parsed header
|
|
471
|
+
* @param startCell - the `:START_ID` cell
|
|
472
|
+
* @param endCell - the `:END_ID` cell
|
|
473
|
+
* @param typeCell - the `:TYPE` cell, or -1
|
|
474
|
+
* @param line - the header line
|
|
475
|
+
* @returns the section
|
|
476
|
+
*/
|
|
477
|
+
declareRelationshipSection(fields, startCell, endCell, typeCell, line) {
|
|
478
|
+
for (const field of fields) {
|
|
479
|
+
for (const [key, value] of field.options) {
|
|
480
|
+
this.report.warning(
|
|
481
|
+
"unsupported",
|
|
482
|
+
HEADER_OPTION_CODE,
|
|
483
|
+
`header option ${key}:${value} of "${field.text}" is ignored`,
|
|
484
|
+
{ line, element: field.text }
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (typeCell >= 0) {
|
|
489
|
+
this.ensureType();
|
|
490
|
+
}
|
|
491
|
+
const { weightFrom } = this.common;
|
|
492
|
+
let weightCell = -1;
|
|
493
|
+
if (weightFrom !== null) {
|
|
494
|
+
weightCell = fields.findIndex((field) => field.kind === "PROPERTY" && field.name === weightFrom);
|
|
495
|
+
}
|
|
496
|
+
const properties = this.declareProperties("edge", fields, line, weightCell);
|
|
497
|
+
return { kind: "relationship", width: fields.length, startCell, endCell, typeCell, weightCell, properties };
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Declare every PROPERTY field of a header.
|
|
501
|
+
* @param domain - node or edge
|
|
502
|
+
* @param fields - the parsed header
|
|
503
|
+
* @param line - the header line
|
|
504
|
+
* @param skipCell - a cell to leave undeclared (the weight), or null
|
|
505
|
+
* @returns the property slots
|
|
506
|
+
*/
|
|
507
|
+
declareProperties(domain, fields, line, skipCell) {
|
|
508
|
+
const slots = [];
|
|
509
|
+
fields.forEach((field, cell) => {
|
|
510
|
+
if (field.kind !== "PROPERTY" || cell === skipCell) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
slots.push(this.declareProperty(domain, field, cell, {}, line));
|
|
514
|
+
});
|
|
515
|
+
const width = fields.length;
|
|
516
|
+
if (this.values.length < width) {
|
|
517
|
+
this.values = new Array(width);
|
|
518
|
+
this.texts = new Array(width);
|
|
519
|
+
}
|
|
520
|
+
return slots;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Declare one property column on the sink: the same name and shape again shares the column;
|
|
524
|
+
* a different shape under the same name is renamed `<name>#<name>` (design section 5.6).
|
|
525
|
+
* @param domain - node or edge
|
|
526
|
+
* @param field - the header field
|
|
527
|
+
* @param cell - the field's cell index
|
|
528
|
+
* @param patch - declaration fields to override (the id property's origin)
|
|
529
|
+
* @param line - the header line
|
|
530
|
+
* @returns the slot
|
|
531
|
+
*/
|
|
532
|
+
declareProperty(domain, field, cell, patch, line) {
|
|
533
|
+
const { sink, report } = this;
|
|
534
|
+
const { listSyntax } = this.options;
|
|
535
|
+
const input = {
|
|
536
|
+
format: NEO4J$1,
|
|
537
|
+
id: field.name,
|
|
538
|
+
title: null,
|
|
539
|
+
type: field.type,
|
|
540
|
+
namespace: null,
|
|
541
|
+
listSyntax,
|
|
542
|
+
long: this.common.long
|
|
543
|
+
};
|
|
544
|
+
let declared = declareAttribute(input);
|
|
545
|
+
let decl = { ...declared.decl, ...patch };
|
|
546
|
+
let handle;
|
|
547
|
+
try {
|
|
548
|
+
handle = declareOn(sink, domain, decl);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (!(err instanceof GraphFormatError) || err.code !== "E_COLUMN_EXISTS") {
|
|
551
|
+
throw err;
|
|
552
|
+
}
|
|
553
|
+
declared = declareAttribute({ ...input, taken: takenIn(sink, domain) });
|
|
554
|
+
decl = { ...declared.decl, ...patch, name: declared.decl.name };
|
|
555
|
+
handle = declareOn(sink, domain, decl);
|
|
556
|
+
}
|
|
557
|
+
for (const issue of declared.issues) {
|
|
558
|
+
report.warning(issue.category, issue.code, issue.message, { line, element: field.text });
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
cell,
|
|
562
|
+
name: decl.name,
|
|
563
|
+
handle,
|
|
564
|
+
spec: declared.spec,
|
|
565
|
+
companionDecl: declared.companion,
|
|
566
|
+
companion: INVALID_INDEX
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/** Declare (or adopt) the labels column. */
|
|
570
|
+
ensureLabels() {
|
|
571
|
+
if (this.labelsHandle === INVALID_INDEX) {
|
|
572
|
+
this.labelsHandle = this.declareReserved("node", LABELS_DECL);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** Declare (or adopt) the relationship type column. */
|
|
576
|
+
ensureType() {
|
|
577
|
+
if (this.typeHandle === INVALID_INDEX) {
|
|
578
|
+
this.typeHandle = this.declareReserved("edge", TYPE_DECL);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/** Declare (or adopt) the id space column. */
|
|
582
|
+
ensureIdSpace() {
|
|
583
|
+
if (this.idSpaceHandle === INVALID_INDEX) {
|
|
584
|
+
this.idSpaceHandle = this.declareReserved("node", ID_SPACE_DECL);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Declare a reserved column: the same shape again shares it; a name taken by another shape is
|
|
589
|
+
* renamed `<name>#<origin.id>` and reported; a role the sink already holds elsewhere is dropped
|
|
590
|
+
* and reported.
|
|
591
|
+
* @param domain - node or edge
|
|
592
|
+
* @param decl - the reserved declaration
|
|
593
|
+
* @returns the handle
|
|
594
|
+
*/
|
|
595
|
+
declareReserved(domain, decl) {
|
|
596
|
+
const { sink, report } = this;
|
|
597
|
+
let current = decl;
|
|
598
|
+
for (; ; ) {
|
|
599
|
+
try {
|
|
600
|
+
return declareOn(sink, domain, current);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
if (!(err instanceof GraphFormatError)) {
|
|
603
|
+
throw err;
|
|
604
|
+
}
|
|
605
|
+
if (err.code === "E_COLUMN_EXISTS") {
|
|
606
|
+
const name = uniqueColumnName(current.name, current.origin?.id ?? null, takenIn(sink, domain));
|
|
607
|
+
report.warning(
|
|
608
|
+
"coercion",
|
|
609
|
+
RENAMED_CODE,
|
|
610
|
+
`column "${current.name}" renamed to "${name}": the name was taken`,
|
|
611
|
+
{ element: current.name }
|
|
612
|
+
);
|
|
613
|
+
current = { ...current, name };
|
|
614
|
+
} else if (err.code === "E_DUPLICATE_ROLE") {
|
|
615
|
+
report.warning(
|
|
616
|
+
"coercion",
|
|
617
|
+
ROLE_TAKEN_CODE,
|
|
618
|
+
`column "${current.name}" declared without role "${String(current.role)}": the sink already holds that role`,
|
|
619
|
+
{ element: current.name }
|
|
620
|
+
);
|
|
621
|
+
const { role: _role, ...withoutRole } = current;
|
|
622
|
+
current = withoutRole;
|
|
623
|
+
} else {
|
|
624
|
+
throw err;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Push one node row.
|
|
631
|
+
* @param section - the section
|
|
632
|
+
* @param reader - the reader positioned on the row
|
|
633
|
+
* @param count - the row's cell count
|
|
634
|
+
*/
|
|
635
|
+
nodeRow(section, reader, count) {
|
|
636
|
+
const { sink, report } = this;
|
|
637
|
+
const { cells, quoted, line } = reader;
|
|
638
|
+
if (count !== section.width) {
|
|
639
|
+
report.error(
|
|
640
|
+
"validation-error",
|
|
641
|
+
COLUMN_COUNT_CODE,
|
|
642
|
+
`row has ${count} cell(s) but the header has ${section.width}`,
|
|
643
|
+
{ line }
|
|
644
|
+
);
|
|
645
|
+
report.counts.skippedNodes++;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const idText2 = cells[section.idCell];
|
|
649
|
+
if (idText2.length === 0 && !quoted[section.idCell]) {
|
|
650
|
+
report.error("missing-value", MISSING_ID_CODE, "empty :ID cell", { line });
|
|
651
|
+
report.counts.skippedNodes++;
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const id = this.coerceId(idText2, line);
|
|
655
|
+
if (id === null || !this.parseProperties(section.properties, cells, quoted, line, idText2)) {
|
|
656
|
+
report.counts.skippedNodes++;
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
let labels;
|
|
660
|
+
if (section.labelCells.length > 0 || section.extraLabels.length > 0) {
|
|
661
|
+
labels = this.labelsOf(section, cells, quoted);
|
|
662
|
+
}
|
|
663
|
+
const index = sink.addNode(id);
|
|
664
|
+
const status = this.registry.declare(index, section.spaceCode);
|
|
665
|
+
if (status === "collision") {
|
|
666
|
+
report.error(
|
|
667
|
+
"validation-error",
|
|
668
|
+
ID_SPACE_COLLISION_CODE,
|
|
669
|
+
`node ${idText2} is declared in id space ${section.space ?? "(none)"} and in another id space; the core has one id space and this row is skipped`,
|
|
670
|
+
{ line, element: idText2 }
|
|
671
|
+
);
|
|
672
|
+
report.counts.skippedNodes++;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (status === "duplicate") {
|
|
676
|
+
report.warning(
|
|
677
|
+
"merged",
|
|
678
|
+
DUPLICATE_NODE_CODE,
|
|
679
|
+
`node ${idText2} is declared twice; the later properties win`,
|
|
680
|
+
{
|
|
681
|
+
line,
|
|
682
|
+
element: idText2
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (section.idHandle !== INVALID_INDEX) {
|
|
687
|
+
sink.setNodeValue(section.idHandle, index, idText2);
|
|
688
|
+
}
|
|
689
|
+
if (section.space !== null) {
|
|
690
|
+
sink.setNodeValue(this.idSpaceHandle, index, section.space);
|
|
691
|
+
}
|
|
692
|
+
if (labels !== void 0) {
|
|
693
|
+
sink.setNodeValue(this.labelsHandle, index, labels);
|
|
694
|
+
}
|
|
695
|
+
this.writeProperties("node", section.properties, index);
|
|
696
|
+
this.reportPrecision(line, idText2);
|
|
697
|
+
report.counts.nodes++;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Push one relationship row.
|
|
701
|
+
* @param section - the section
|
|
702
|
+
* @param reader - the reader positioned on the row
|
|
703
|
+
* @param count - the row's cell count
|
|
704
|
+
*/
|
|
705
|
+
relationshipRow(section, reader, count) {
|
|
706
|
+
const { sink, report } = this;
|
|
707
|
+
const { cells, quoted, line } = reader;
|
|
708
|
+
if (!this.headerSet) {
|
|
709
|
+
this.headerSet = true;
|
|
710
|
+
this.direction.setHeader(true, { line });
|
|
711
|
+
}
|
|
712
|
+
if (count !== section.width) {
|
|
713
|
+
report.error(
|
|
714
|
+
"validation-error",
|
|
715
|
+
COLUMN_COUNT_CODE,
|
|
716
|
+
`row has ${count} cell(s) but the header has ${section.width}`,
|
|
717
|
+
{ line }
|
|
718
|
+
);
|
|
719
|
+
report.counts.skippedEdges++;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const startText = cells[section.startCell];
|
|
723
|
+
const endText = cells[section.endCell];
|
|
724
|
+
const startMissing = startText.length === 0 && !quoted[section.startCell];
|
|
725
|
+
if (startMissing || endText.length === 0 && !quoted[section.endCell]) {
|
|
726
|
+
report.error(
|
|
727
|
+
"missing-value",
|
|
728
|
+
MISSING_ENDPOINT_CODE,
|
|
729
|
+
`empty ${startMissing ? ":START_ID" : ":END_ID"} cell`,
|
|
730
|
+
{
|
|
731
|
+
line
|
|
732
|
+
}
|
|
733
|
+
);
|
|
734
|
+
report.counts.skippedEdges++;
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const element = `${startText}->${endText}`;
|
|
738
|
+
const source = this.coerceId(startText, line);
|
|
739
|
+
const target = source === null ? null : this.coerceId(endText, line);
|
|
740
|
+
if (source === null || target === null) {
|
|
741
|
+
report.counts.skippedEdges++;
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let weight;
|
|
745
|
+
if (section.weightCell >= 0) {
|
|
746
|
+
try {
|
|
747
|
+
weight = parseWeightText(cells[section.weightCell]);
|
|
748
|
+
} catch (err) {
|
|
749
|
+
report.recordError(err, { line, element });
|
|
750
|
+
report.counts.skippedEdges++;
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (!this.parseProperties(section.properties, cells, quoted, line, element)) {
|
|
755
|
+
report.counts.skippedEdges++;
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
let edge;
|
|
759
|
+
try {
|
|
760
|
+
edge = this.direction.addEdge(source, target, "directed", weight, { line, element });
|
|
761
|
+
} catch (err) {
|
|
762
|
+
report.recordError(err, { line, element });
|
|
763
|
+
report.counts.skippedEdges++;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (section.typeCell >= 0) {
|
|
767
|
+
const type = cells[section.typeCell];
|
|
768
|
+
if (type.length > 0) {
|
|
769
|
+
sink.setEdgeValue(this.typeHandle, edge, type);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
this.writeProperties("edge", section.properties, edge);
|
|
773
|
+
this.reportPrecision(line, element);
|
|
774
|
+
report.counts.edges++;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Coerce an id cell, reporting a merge under `ids: "number"` and an invalid id as an error.
|
|
778
|
+
* @param text - the cell
|
|
779
|
+
* @param line - the row's line
|
|
780
|
+
* @returns the id, or null when the cell was rejected (the error is recorded)
|
|
781
|
+
*/
|
|
782
|
+
coerceId(text, line) {
|
|
783
|
+
const { coercer, report } = this;
|
|
784
|
+
let id;
|
|
785
|
+
try {
|
|
786
|
+
id = coercer.text(text);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
report.recordError(err, { line, element: text });
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
const merge = coercer.lastMerge;
|
|
792
|
+
if (merge !== null) {
|
|
793
|
+
report.warning(
|
|
794
|
+
"coercion",
|
|
795
|
+
ID_MERGED_CODE,
|
|
796
|
+
`id "${merge.text}" merged with "${merge.previousText}" as ${merge.id} under ids: "number"`,
|
|
797
|
+
{ line, element: text }
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return id;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Parse the property cells of a row into the scratch arrays.
|
|
804
|
+
* @param slots - the section's property slots
|
|
805
|
+
* @param cells - the row's cells
|
|
806
|
+
* @param quoted - whether each cell was quoted
|
|
807
|
+
* @param line - the row's line
|
|
808
|
+
* @param element - the row's element name for issues
|
|
809
|
+
* @returns true when every cell parsed; false after recording the first error
|
|
810
|
+
*/
|
|
811
|
+
parseProperties(slots, cells, quoted, line, element) {
|
|
812
|
+
const { values, texts, precisionSlots } = this;
|
|
813
|
+
precisionSlots.length = 0;
|
|
814
|
+
for (const slot of slots) {
|
|
815
|
+
const text = cells[slot.cell];
|
|
816
|
+
texts[slot.cell] = null;
|
|
817
|
+
if (text.length === 0) {
|
|
818
|
+
values[slot.cell] = quoted[slot.cell] ? emptyValue(slot.spec) : void 0;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const { spec } = slot;
|
|
822
|
+
try {
|
|
823
|
+
if (spec.temporal !== null && !spec.list) {
|
|
824
|
+
const parsed = parseDeclaredTemporal(text, spec);
|
|
825
|
+
values[slot.cell] = parsed.value;
|
|
826
|
+
texts[slot.cell] = parsed.text;
|
|
827
|
+
} else {
|
|
828
|
+
values[slot.cell] = parseDeclaredValue(text, spec, this.options.listSyntax);
|
|
829
|
+
}
|
|
830
|
+
} catch (err) {
|
|
831
|
+
this.report.recordError(err, { line, element: `${element} ${slot.name}` });
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
if (spec.precision && losesPrecision(spec, text)) {
|
|
835
|
+
precisionSlots.push(slot);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Write the parsed property values of a row.
|
|
842
|
+
* @param domain - node or edge
|
|
843
|
+
* @param slots - the section's property slots
|
|
844
|
+
* @param row - the node or edge index
|
|
845
|
+
*/
|
|
846
|
+
writeProperties(domain, slots, row) {
|
|
847
|
+
const { sink, values, texts } = this;
|
|
848
|
+
for (const slot of slots) {
|
|
849
|
+
const value = values[slot.cell];
|
|
850
|
+
if (value === void 0) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const text = texts[slot.cell];
|
|
854
|
+
if (text !== null && slot.companion === INVALID_INDEX && slot.companionDecl !== null) {
|
|
855
|
+
slot.companion = declareCompanion(sink, domain, slot.companionDecl);
|
|
856
|
+
}
|
|
857
|
+
if (domain === "node") {
|
|
858
|
+
sink.setNodeValue(slot.handle, row, value);
|
|
859
|
+
if (text !== null) {
|
|
860
|
+
sink.setNodeValue(slot.companion, row, text);
|
|
861
|
+
}
|
|
862
|
+
} else {
|
|
863
|
+
sink.setEdgeValue(slot.handle, row, value);
|
|
864
|
+
if (text !== null) {
|
|
865
|
+
sink.setEdgeValue(slot.companion, row, text);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Report the precision losses of the row just accepted.
|
|
872
|
+
* @param line - the row's line
|
|
873
|
+
* @param element - the row's element name
|
|
874
|
+
*/
|
|
875
|
+
reportPrecision(line, element) {
|
|
876
|
+
for (const slot of this.precisionSlots) {
|
|
877
|
+
this.report.warning(
|
|
878
|
+
"precision",
|
|
879
|
+
PRECISION_CODE,
|
|
880
|
+
`long value of "${slot.name}" is beyond 2^53 and was stored as the nearest f64`,
|
|
881
|
+
{ line, element }
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
this.precisionSlots.length = 0;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* The labels of a node row: every `:LABEL` cell split by the array delimiter (empty items
|
|
888
|
+
* dropped), plus the header's `{label:...}` options.
|
|
889
|
+
* @param section - the section
|
|
890
|
+
* @param cells - the row's cells
|
|
891
|
+
* @param quoted - whether each cell was quoted
|
|
892
|
+
* @returns the labels, or undefined when every label cell is unset and no extra label exists
|
|
893
|
+
*/
|
|
894
|
+
labelsOf(section, cells, quoted) {
|
|
895
|
+
let labels;
|
|
896
|
+
for (const cell of section.labelCells) {
|
|
897
|
+
const text = cells[cell];
|
|
898
|
+
if (text.length === 0 && !quoted[cell]) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (labels === void 0) {
|
|
902
|
+
labels = [];
|
|
903
|
+
}
|
|
904
|
+
for (const item of splitListText(text, this.options.listSyntax)) {
|
|
905
|
+
if (item.length > 0) {
|
|
906
|
+
labels.push(item);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (section.extraLabels.length > 0) {
|
|
911
|
+
if (labels === void 0) {
|
|
912
|
+
labels = [];
|
|
913
|
+
}
|
|
914
|
+
labels.push(...section.extraLabels);
|
|
915
|
+
}
|
|
916
|
+
return labels;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function emptyValue(spec) {
|
|
920
|
+
if (spec.list) {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
if (spec.kind === "string" || spec.kind === "duration") {
|
|
924
|
+
return "";
|
|
925
|
+
}
|
|
926
|
+
return void 0;
|
|
927
|
+
}
|
|
928
|
+
function checkPropertyNames(fields) {
|
|
929
|
+
const seen = /* @__PURE__ */ new Set();
|
|
930
|
+
for (const field of fields) {
|
|
931
|
+
if (field.name.length === 0 || field.kind === "IGNORE") {
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (seen.has(field.name)) {
|
|
935
|
+
throw headerError(`property "${field.name}" is declared twice`);
|
|
936
|
+
}
|
|
937
|
+
seen.add(field.name);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function headerError(reason) {
|
|
941
|
+
return new GraphFormatError("E_UNSUPPORTED", reason, { reason: "header" });
|
|
942
|
+
}
|
|
943
|
+
function sniffNeo4j(head) {
|
|
944
|
+
const text = new TextDecoder("utf-8", { fatal: false, ignoreBOM: false }).decode(head);
|
|
945
|
+
const end = text.search(/[\r\n]/);
|
|
946
|
+
const first = end < 0 ? text : text.slice(0, end);
|
|
947
|
+
const cells = first.split(/[,;|\t]/);
|
|
948
|
+
return isHeaderRecord(cells, cells.length) ? 0.95 : 0;
|
|
949
|
+
}
|
|
950
|
+
const neo4jImporter = Object.freeze({
|
|
951
|
+
format: NEO4J$1,
|
|
952
|
+
extensions: Object.freeze([".csv", ".tsv"]),
|
|
953
|
+
mimeTypes: Object.freeze(["text/csv", "text/tab-separated-values"]),
|
|
954
|
+
sniff: sniffNeo4j,
|
|
955
|
+
/**
|
|
956
|
+
* Read one or more neo4j-admin CSV inputs into the sink.
|
|
957
|
+
* @param input - the primary input (a node table, a relationship table, or sections of both)
|
|
958
|
+
* @param sink - the sink
|
|
959
|
+
* @param options - format-specific and common options
|
|
960
|
+
* @returns the report; ImportError beyond the error limit or on a malformed header
|
|
961
|
+
*/
|
|
962
|
+
async import(input, sink, options) {
|
|
963
|
+
const common = resolveImportOptions(options, { ids: "canonical", defaultDirected: true, weightFrom: "weight" });
|
|
964
|
+
const format = resolveNeo4jOptions(options);
|
|
965
|
+
const report = new ImportReportBuilder(NEO4J$1, common.errorLimit);
|
|
966
|
+
reportSinkOptions(sink, options, report);
|
|
967
|
+
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
968
|
+
const session = new Neo4jImportSession(sink, report, common, format);
|
|
969
|
+
await session.run([input, ...format.nodes, ...format.relationships]);
|
|
970
|
+
throwIfAborted(common.signal);
|
|
971
|
+
return report.finish();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
const UNDIRECTED_LOSS = "W_NEO4J_UNDIRECTED_AS_DIRECTED";
|
|
975
|
+
const ID_TEXT_TYPE_LOSS = ID_TEXT_TYPE_CODE;
|
|
976
|
+
const ID_TEXT_COLLISION_LOSS = ID_TEXT_COLLISION_CODE;
|
|
977
|
+
const WEIGHT_COLUMN_TAKEN_LOSS = "E_NEO4J_WEIGHT_COLUMN_TAKEN";
|
|
978
|
+
const ID_COLUMN_TAKEN_LOSS = "E_NEO4J_ID_COLUMN_TAKEN";
|
|
979
|
+
const MULTIPLE_ID_PROPERTIES_LOSS = "W_NEO4J_MULTIPLE_ID_PROPERTIES";
|
|
980
|
+
const DECLARED_TYPE_CHANGED_LOSS = "W_NEO4J_DECLARED_TYPE_CHANGED";
|
|
981
|
+
const ARRAY_DELIMITER_LOSS = "W_NEO4J_ARRAY_DELIMITER";
|
|
982
|
+
const SLOT_ROLES = /* @__PURE__ */ new Set(["kind", "labels", "idSpace"]);
|
|
983
|
+
const NEO4J = "neo4j";
|
|
984
|
+
const ID_TYPE = "ID";
|
|
985
|
+
const DEFAULT_WEIGHT_COLUMN = "weight";
|
|
986
|
+
const ROLE_NAMES = Object.freeze({
|
|
987
|
+
kind: TYPE_COLUMN,
|
|
988
|
+
labels: LABELS_COLUMN,
|
|
989
|
+
idSpace: ID_SPACE_COLUMN
|
|
990
|
+
});
|
|
991
|
+
const NEO4J_CAPABILITIES = capabilities({
|
|
992
|
+
mixedDirection: false,
|
|
993
|
+
multiEdges: true,
|
|
994
|
+
selfLoops: true,
|
|
995
|
+
edgeIds: "none",
|
|
996
|
+
idCharset: "any",
|
|
997
|
+
dtypes: ["f32", "f64", "i32", "bool", "string"],
|
|
998
|
+
components: false,
|
|
999
|
+
lists: true,
|
|
1000
|
+
json: false,
|
|
1001
|
+
defaults: false,
|
|
1002
|
+
options: false,
|
|
1003
|
+
hierarchy: false,
|
|
1004
|
+
temporal: "none",
|
|
1005
|
+
graphAttributes: false,
|
|
1006
|
+
positions: false,
|
|
1007
|
+
viz: false
|
|
1008
|
+
});
|
|
1009
|
+
const SKIPPED_ROLES = /* @__PURE__ */ new Set([
|
|
1010
|
+
"directed",
|
|
1011
|
+
"pair",
|
|
1012
|
+
"mutual",
|
|
1013
|
+
"weight",
|
|
1014
|
+
"timeText",
|
|
1015
|
+
"originalId",
|
|
1016
|
+
"parent",
|
|
1017
|
+
"parents",
|
|
1018
|
+
"start",
|
|
1019
|
+
"end",
|
|
1020
|
+
"timestamp",
|
|
1021
|
+
"timestamps",
|
|
1022
|
+
"spells",
|
|
1023
|
+
"open"
|
|
1024
|
+
]);
|
|
1025
|
+
const EDGE_NOTE_CODES = /* @__PURE__ */ new Set([
|
|
1026
|
+
LOSS.MIXED_DIRECTION,
|
|
1027
|
+
LOSS.MIXED_DIRECTION_ERROR,
|
|
1028
|
+
LOSS.MULTI_EDGES,
|
|
1029
|
+
LOSS.SELF_LOOPS,
|
|
1030
|
+
LOSS.EDGE_IDS_GENERATED,
|
|
1031
|
+
LOSS.EDGE_IDS_DROPPED,
|
|
1032
|
+
UNDIRECTED_LOSS,
|
|
1033
|
+
WEIGHT_COLUMN_TAKEN_LOSS
|
|
1034
|
+
]);
|
|
1035
|
+
const NODE_NOTE_CODES = /* @__PURE__ */ new Set([
|
|
1036
|
+
LOSS.ID_MANGLED,
|
|
1037
|
+
LOSS.ID_CHARSET,
|
|
1038
|
+
LOSS.ID_RENUMBERED,
|
|
1039
|
+
ID_TEXT_TYPE_LOSS,
|
|
1040
|
+
ID_COLUMN_TAKEN_LOSS,
|
|
1041
|
+
MULTIPLE_ID_PROPERTIES_LOSS
|
|
1042
|
+
]);
|
|
1043
|
+
const ARRAY_SYNTAXES = /* @__PURE__ */ new Set([";", ",", "|"]);
|
|
1044
|
+
const PARTS = /* @__PURE__ */ new Set(["all", "nodes", "relationships"]);
|
|
1045
|
+
function resolveNeo4jExportOptions(options) {
|
|
1046
|
+
const o = options ?? {};
|
|
1047
|
+
const part = o.part ?? "all";
|
|
1048
|
+
if (!PARTS.has(part)) {
|
|
1049
|
+
throw new GraphFormatError(
|
|
1050
|
+
"E_UNSUPPORTED",
|
|
1051
|
+
`option part: ${JSON.stringify(part)} is not "all", "nodes" or "relationships"`,
|
|
1052
|
+
{
|
|
1053
|
+
option: "part",
|
|
1054
|
+
found: part
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
const arrayDelimiter = o.arrayDelimiter ?? ";";
|
|
1059
|
+
if (!ARRAY_SYNTAXES.has(arrayDelimiter)) {
|
|
1060
|
+
throw new GraphFormatError(
|
|
1061
|
+
"E_UNSUPPORTED",
|
|
1062
|
+
`option arrayDelimiter: ${JSON.stringify(arrayDelimiter)} is not one of ";", ",", "|"`,
|
|
1063
|
+
{ option: "arrayDelimiter", found: arrayDelimiter }
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
const delimiter = o.delimiter ?? ",";
|
|
1067
|
+
const syntax = { ...checkRecordSyntax({ delimiter, quote: o.quote ?? '"' }), delimiter };
|
|
1068
|
+
if (syntax.delimiter === arrayDelimiter) {
|
|
1069
|
+
throw new GraphFormatError("E_UNSUPPORTED", "options delimiter and arrayDelimiter must differ", {
|
|
1070
|
+
option: "arrayDelimiter",
|
|
1071
|
+
found: arrayDelimiter
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const weightColumn = o.weightColumn === void 0 ? "weight" : o.weightColumn;
|
|
1075
|
+
if (weightColumn !== null && (typeof weightColumn !== "string" || weightColumn.length === 0)) {
|
|
1076
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option weightColumn: expected a non-empty name or null", {
|
|
1077
|
+
option: "weightColumn",
|
|
1078
|
+
found: weightColumn
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
const idColumn = o.idColumn ?? null;
|
|
1082
|
+
if (idColumn !== null && (typeof idColumn !== "string" || idColumn.length === 0)) {
|
|
1083
|
+
throw new GraphFormatError("E_UNSUPPORTED", "option idColumn: expected a non-empty name or null", {
|
|
1084
|
+
option: "idColumn",
|
|
1085
|
+
found: idColumn
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
return { part, syntax, arrayDelimiter, weightColumn, idColumn };
|
|
1089
|
+
}
|
|
1090
|
+
class ExportPlan {
|
|
1091
|
+
/**
|
|
1092
|
+
* Build the plan.
|
|
1093
|
+
* @param snapshot - the snapshot
|
|
1094
|
+
* @param options - the resolved format options
|
|
1095
|
+
* @param common - the resolved common options
|
|
1096
|
+
*/
|
|
1097
|
+
constructor(snapshot, options, common) {
|
|
1098
|
+
this.notes = [];
|
|
1099
|
+
this.ownNotes = [];
|
|
1100
|
+
this.fatalNodes = null;
|
|
1101
|
+
this.fatalEdges = null;
|
|
1102
|
+
this.fatalAll = null;
|
|
1103
|
+
this.nodeColumns = [];
|
|
1104
|
+
this.edgeColumns = [];
|
|
1105
|
+
this.idColumns = [];
|
|
1106
|
+
this.snapshot = snapshot;
|
|
1107
|
+
this.options = options;
|
|
1108
|
+
this.common = common;
|
|
1109
|
+
this.idSpace = snapshot.nodes.byRole("idSpace");
|
|
1110
|
+
this.labels = snapshot.nodes.byRole("labels");
|
|
1111
|
+
this.kind = snapshot.edges.byRole("kind");
|
|
1112
|
+
for (const column of snapshot.nodes) {
|
|
1113
|
+
if (isStoredId(column.meta)) {
|
|
1114
|
+
this.idColumns.push(column);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
const generic = checkCapabilities(snapshot, NEO4J_CAPABILITIES, common, {
|
|
1118
|
+
roles: SLOT_ROLES,
|
|
1119
|
+
roleNames: ROLE_NAMES,
|
|
1120
|
+
temporalText: true
|
|
1121
|
+
});
|
|
1122
|
+
this.planColumns("node", snapshot.nodes, this.nodeColumns);
|
|
1123
|
+
this.planColumns("edge", snapshot.edges, this.edgeColumns);
|
|
1124
|
+
this.weights = this.planWeightSource();
|
|
1125
|
+
this.folding = pairFolding(snapshot);
|
|
1126
|
+
this.checkIds();
|
|
1127
|
+
this.checkIdColumn();
|
|
1128
|
+
this.checkStoredIds();
|
|
1129
|
+
this.directionNotes();
|
|
1130
|
+
const slots = new Set([this.kind, this.labels, this.idSpace].flatMap((c) => c === null ? [] : [c.meta.name]));
|
|
1131
|
+
for (const gen of generic) {
|
|
1132
|
+
if (gen.code === LOSS.JSON && gen.column !== null && this.writtenAsPoint(gen.column)) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
if (gen.code === LOSS.DTYPE && gen.column !== null && slots.has(gen.column)) {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (gen.code === LOSS.MIXED_DIRECTION_ERROR && this.fatalEdges === null) {
|
|
1139
|
+
this.fatalEdges = new GraphFormatError("E_DIRECTED", gen.message, { reason: "mixed direction" });
|
|
1140
|
+
}
|
|
1141
|
+
if ((gen.code === LOSS.POSITIONS || gen.code === LOSS.VIZ) && gen.column !== null) {
|
|
1142
|
+
this.notes.push(
|
|
1143
|
+
Object.freeze({
|
|
1144
|
+
code: gen.code,
|
|
1145
|
+
message: `node column "${gen.column}" is written as a plain property; its role is lost on re-import`,
|
|
1146
|
+
column: gen.column,
|
|
1147
|
+
count: gen.count
|
|
1148
|
+
})
|
|
1149
|
+
);
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
this.notes.push(gen);
|
|
1153
|
+
}
|
|
1154
|
+
this.notes.push(...this.ownNotes);
|
|
1155
|
+
this.filterNotesByPart();
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* The direction notes: an undirected snapshot writes every edge as a directed relationship;
|
|
1159
|
+
* a mixed snapshot folds its undirected pairs to one directed relationship each under
|
|
1160
|
+
* "directed" or "undirected" (Neo4j has no undirected relationship, so both policies write
|
|
1161
|
+
* the same file and the generic W_MIXED_DIRECTION note names the policy); a mutual pair is
|
|
1162
|
+
* written as two relationships without its mark.
|
|
1163
|
+
*/
|
|
1164
|
+
directionNotes() {
|
|
1165
|
+
const { snapshot, folding } = this;
|
|
1166
|
+
if (!snapshot.directed) {
|
|
1167
|
+
this.note(
|
|
1168
|
+
UNDIRECTED_LOSS,
|
|
1169
|
+
`the snapshot is undirected; every edge is written as a directed relationship (${snapshot.edgeCount} edge(s))`,
|
|
1170
|
+
null,
|
|
1171
|
+
snapshot.edgeCount
|
|
1172
|
+
);
|
|
1173
|
+
} else if (this.common.onMixedDirection !== "error") {
|
|
1174
|
+
let undirected = 0;
|
|
1175
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1176
|
+
if (!folding.folded(e) && !folding.sourceDirected(e)) {
|
|
1177
|
+
undirected++;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (undirected > 0) {
|
|
1181
|
+
this.note(
|
|
1182
|
+
UNDIRECTED_LOSS,
|
|
1183
|
+
`${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`,
|
|
1184
|
+
null,
|
|
1185
|
+
undirected
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (folding.mutualCount > 0) {
|
|
1190
|
+
this.note(
|
|
1191
|
+
LOSS.MUTUAL_EXPANDED,
|
|
1192
|
+
`${folding.mutualCount} mutual pair(s) are written as two directed relationships; the mutual mark is lost`,
|
|
1193
|
+
null,
|
|
1194
|
+
folding.mutualCount
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Record a note.
|
|
1200
|
+
* @param code - the code
|
|
1201
|
+
* @param message - the message
|
|
1202
|
+
* @param column - the column, or null
|
|
1203
|
+
* @param count - the count, or null
|
|
1204
|
+
*/
|
|
1205
|
+
note(code, message, column = null, count = null) {
|
|
1206
|
+
this.ownNotes.push(Object.freeze({ code, message, column, count }));
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Plan the property columns of one table.
|
|
1210
|
+
* @param domain - node or edge
|
|
1211
|
+
* @param table - the table
|
|
1212
|
+
* @param out - receives the plans in declaration order
|
|
1213
|
+
*/
|
|
1214
|
+
planColumns(domain, table, out) {
|
|
1215
|
+
const all = [...table];
|
|
1216
|
+
const names = new Set(all.map((column) => column.meta.name));
|
|
1217
|
+
const companionFor = /* @__PURE__ */ new Map();
|
|
1218
|
+
const companions = /* @__PURE__ */ new Set();
|
|
1219
|
+
for (const column of all) {
|
|
1220
|
+
const target = column.meta.extra.for;
|
|
1221
|
+
if (column.dtype === "string" && typeof target === "string" && names.has(target)) {
|
|
1222
|
+
companionFor.set(target, column);
|
|
1223
|
+
companions.add(column);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
for (const column of all) {
|
|
1227
|
+
const { meta } = column;
|
|
1228
|
+
const { role } = meta;
|
|
1229
|
+
if (companions.has(column) || role !== null && SKIPPED_ROLES.has(role)) {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (domain === "node" && (isStoredId(meta) || column === this.idSpace || column === this.labels)) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (domain === "edge" && (role === "id" || column === this.kind)) {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
out.push(this.planColumn(column, companionFor.get(meta.name) ?? null));
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Plan one property column: its header type, cell kind and notes.
|
|
1243
|
+
* @param column - the column
|
|
1244
|
+
* @param companion - its text companion, or null
|
|
1245
|
+
* @returns the plan
|
|
1246
|
+
*/
|
|
1247
|
+
planColumn(column, companion) {
|
|
1248
|
+
const { meta } = column;
|
|
1249
|
+
const declared = meta.origin?.type ?? null;
|
|
1250
|
+
const mapped = declared === null ? null : mapDeclaredType(NEO4J, declared, "f64");
|
|
1251
|
+
const label = `${meta.domain} column "${meta.name}"`;
|
|
1252
|
+
if (meta.dtype === "list") {
|
|
1253
|
+
const itemDtype = meta.itemDtype ?? "string";
|
|
1254
|
+
const compatible2 = mapped !== null && mapped.list && mapped.itemDtype === itemDtype && mapped.kind !== "long";
|
|
1255
|
+
let scalar2 = compatible2 ? { type: declaredScalar(declared), kind: kindOf(mapped), temporal: mapped.temporal } : defaultScalar(itemDtype);
|
|
1256
|
+
if (mapped !== null && mapped.list && mapped.kind === "long" && itemDtype === "f64") {
|
|
1257
|
+
scalar2 = this.integralOrDouble(column, declaredScalar(declared), label);
|
|
1258
|
+
}
|
|
1259
|
+
const itemComponents = meta.itemComponents ?? 1;
|
|
1260
|
+
if (itemComponents > 1) {
|
|
1261
|
+
this.note(
|
|
1262
|
+
LOSS.COMPONENTS,
|
|
1263
|
+
`${label} holds items of ${itemComponents} components; they are flattened into one array`,
|
|
1264
|
+
meta.name,
|
|
1265
|
+
column.length - column.nullCount
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
this.checkArrayItems(column, scalar2, label);
|
|
1269
|
+
return {
|
|
1270
|
+
column,
|
|
1271
|
+
header: formatHeaderField(meta.name, `${scalar2.type}[]`, null),
|
|
1272
|
+
scalar: scalar2,
|
|
1273
|
+
list: true,
|
|
1274
|
+
companion
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
let scalar;
|
|
1278
|
+
const compatible = mapped !== null && !mapped.list && mapped.dtype === meta.dtype;
|
|
1279
|
+
if (compatible && mapped.kind === "long") {
|
|
1280
|
+
scalar = this.integralOrDouble(column, declared, label);
|
|
1281
|
+
} else if (compatible) {
|
|
1282
|
+
scalar = { type: declared, kind: kindOf(mapped), temporal: mapped.temporal };
|
|
1283
|
+
} else {
|
|
1284
|
+
scalar = defaultScalar(meta.dtype);
|
|
1285
|
+
}
|
|
1286
|
+
const visual = meta.role === "position" || meta.role !== null && isVizRole(meta.role);
|
|
1287
|
+
if (visual && meta.dtype === "json" && scalar.kind !== "point") {
|
|
1288
|
+
this.note(
|
|
1289
|
+
LOSS.JSON,
|
|
1290
|
+
`${label} holds nested values; the format has no nested values`,
|
|
1291
|
+
meta.name,
|
|
1292
|
+
column.length - column.nullCount
|
|
1293
|
+
);
|
|
1294
|
+
} else if (visual && meta.dtype !== "json" && !NEO4J_CAPABILITIES.dtypes.includes(meta.dtype)) {
|
|
1295
|
+
this.note(
|
|
1296
|
+
LOSS.DTYPE,
|
|
1297
|
+
`${label} is ${meta.dtype}; the format cannot keep that dtype`,
|
|
1298
|
+
meta.name,
|
|
1299
|
+
column.length - column.nullCount
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
if (meta.components > 1) {
|
|
1303
|
+
if (visual) {
|
|
1304
|
+
this.note(
|
|
1305
|
+
LOSS.COMPONENTS,
|
|
1306
|
+
`${label} has ${meta.components} components; the format has no strides`,
|
|
1307
|
+
meta.name,
|
|
1308
|
+
column.length - column.nullCount
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
column,
|
|
1313
|
+
header: formatHeaderField(meta.name, `${scalar.type}[]`, null),
|
|
1314
|
+
scalar,
|
|
1315
|
+
list: true,
|
|
1316
|
+
companion
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
const untyped = declared === null && meta.origin?.format === NEO4J && meta.dtype === "string";
|
|
1320
|
+
const header = untyped ? meta.name : formatHeaderField(meta.name, scalar.type, null);
|
|
1321
|
+
return { column, header, scalar, list: false, companion };
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* The plan of an f64 column declared with an integer type: kept when every set value is
|
|
1325
|
+
* integral, written as double (with a note) otherwise.
|
|
1326
|
+
* @param column - the column
|
|
1327
|
+
* @param declared - the declared type text
|
|
1328
|
+
* @param label - the column label for messages
|
|
1329
|
+
* @returns the scalar plan
|
|
1330
|
+
*/
|
|
1331
|
+
integralOrDouble(column, declared, label) {
|
|
1332
|
+
let integral = true;
|
|
1333
|
+
if (column.dtype === "f64") {
|
|
1334
|
+
for (let r = 0; r < column.length && integral; r++) {
|
|
1335
|
+
if (column.isSet(r) && !Number.isInteger(column.data[r])) {
|
|
1336
|
+
integral = false;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
} else if (column.dtype === "list" && column.child.dtype === "f64") {
|
|
1340
|
+
const { data } = column.child;
|
|
1341
|
+
for (let i = 0; i < data.length && integral; i++) {
|
|
1342
|
+
if (!Number.isInteger(data[i])) {
|
|
1343
|
+
integral = false;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (integral) {
|
|
1348
|
+
return { type: declared, kind: "integer", temporal: null };
|
|
1349
|
+
}
|
|
1350
|
+
this.note(
|
|
1351
|
+
DECLARED_TYPE_CHANGED_LOSS,
|
|
1352
|
+
`${label} is declared ${declared} but holds non-integral values; written as double`,
|
|
1353
|
+
column.meta.name,
|
|
1354
|
+
null
|
|
1355
|
+
);
|
|
1356
|
+
return { type: "double", kind: "double", temporal: null };
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Count the rows of a list column with an item containing the array delimiter.
|
|
1360
|
+
* @param column - the list column
|
|
1361
|
+
* @param scalar - the item plan
|
|
1362
|
+
* @param label - the column label
|
|
1363
|
+
*/
|
|
1364
|
+
checkArrayItems(column, scalar, label) {
|
|
1365
|
+
if (column.dtype !== "list" || scalar.kind !== "string" && scalar.kind !== "json" && scalar.kind !== "point") {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const { arrayDelimiter } = this.options;
|
|
1369
|
+
let rows = 0;
|
|
1370
|
+
for (let r = 0; r < column.length; r++) {
|
|
1371
|
+
if (!column.isSet(r)) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
for (const item of column.sliceOf(r)) {
|
|
1375
|
+
if (formatScalar(item, scalar, null).includes(arrayDelimiter)) {
|
|
1376
|
+
rows++;
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (rows > 0) {
|
|
1382
|
+
this.note(
|
|
1383
|
+
ARRAY_DELIMITER_LOSS,
|
|
1384
|
+
`${label}: ${rows} row(s) hold an item containing the array delimiter "${arrayDelimiter}", which Neo4j cannot escape`,
|
|
1385
|
+
column.meta.name,
|
|
1386
|
+
rows
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* The explicit weights (design section 3.7), and the weight-column name check.
|
|
1392
|
+
* @returns the weights, or null when no weights are written
|
|
1393
|
+
*/
|
|
1394
|
+
planWeightSource() {
|
|
1395
|
+
const { snapshot } = this;
|
|
1396
|
+
const name = this.options.weightColumn;
|
|
1397
|
+
if (name === null) {
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
const weights = explicitWeights(snapshot);
|
|
1401
|
+
const taken = this.edgeColumns.find((plan2) => plan2.column.meta.name === name);
|
|
1402
|
+
if (taken !== void 0 && weights.weighted) {
|
|
1403
|
+
this.note(
|
|
1404
|
+
WEIGHT_COLUMN_TAKEN_LOSS,
|
|
1405
|
+
`edge column "${name}" already exists; explicit weights cannot be written under weightColumn "${name}"`,
|
|
1406
|
+
name,
|
|
1407
|
+
null
|
|
1408
|
+
);
|
|
1409
|
+
if (this.fatalEdges === null) {
|
|
1410
|
+
this.fatalEdges = new GraphFormatError(
|
|
1411
|
+
"E_COLUMN_EXISTS",
|
|
1412
|
+
`edge column "${name}" already exists; choose another weightColumn`,
|
|
1413
|
+
{ column: name, domain: "edge" }
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
} else if (taken !== void 0 && name === DEFAULT_WEIGHT_COLUMN) {
|
|
1417
|
+
this.note(
|
|
1418
|
+
LOSS.WEIGHT_KEY_CLASH,
|
|
1419
|
+
`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`,
|
|
1420
|
+
name,
|
|
1421
|
+
taken.column.length - taken.column.nullCount
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return weights.weighted ? weights : null;
|
|
1425
|
+
}
|
|
1426
|
+
/** Check the node ids' text forms: type changes under the canonical rule and collisions. */
|
|
1427
|
+
checkIds() {
|
|
1428
|
+
const { ids } = this.snapshot;
|
|
1429
|
+
let typeChanges = 0;
|
|
1430
|
+
let collisions = 0;
|
|
1431
|
+
switch (ids.kind) {
|
|
1432
|
+
case "identity":
|
|
1433
|
+
case "dense":
|
|
1434
|
+
break;
|
|
1435
|
+
case "numeric":
|
|
1436
|
+
for (let i = 0; i < ids.size; i++) {
|
|
1437
|
+
if (!Number.isSafeInteger(ids.idOf(i))) {
|
|
1438
|
+
typeChanges++;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
break;
|
|
1442
|
+
case "string":
|
|
1443
|
+
for (let i = 0; i < ids.size; i++) {
|
|
1444
|
+
if (isCanonicalIntegerText(String(ids.idOf(i)))) {
|
|
1445
|
+
typeChanges++;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
break;
|
|
1449
|
+
case "mixed": {
|
|
1450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1451
|
+
for (let i = 0; i < ids.size; i++) {
|
|
1452
|
+
const id = ids.idOf(i);
|
|
1453
|
+
const text = String(id);
|
|
1454
|
+
if (typeof id === "number" ? !Number.isSafeInteger(id) : isCanonicalIntegerText(text)) {
|
|
1455
|
+
typeChanges++;
|
|
1456
|
+
}
|
|
1457
|
+
if (seen.has(text)) {
|
|
1458
|
+
collisions++;
|
|
1459
|
+
} else {
|
|
1460
|
+
seen.add(text);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
default: {
|
|
1466
|
+
const name = ids.kind;
|
|
1467
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown id map kind ${name}`, { kind: name });
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (typeChanges > 0) {
|
|
1471
|
+
this.note(
|
|
1472
|
+
ID_TEXT_TYPE_LOSS,
|
|
1473
|
+
`${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)`,
|
|
1474
|
+
null,
|
|
1475
|
+
typeChanges
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
if (collisions > 0) {
|
|
1479
|
+
this.note(
|
|
1480
|
+
ID_TEXT_COLLISION_LOSS,
|
|
1481
|
+
`${collisions} node id(s) share their text with another id (a number and a string); export() will throw`,
|
|
1482
|
+
null,
|
|
1483
|
+
collisions
|
|
1484
|
+
);
|
|
1485
|
+
this.fatalAll = new GraphFormatError(
|
|
1486
|
+
"E_INVALID_ID",
|
|
1487
|
+
`${collisions} node id(s) share their text with another id; Neo4j ids are text`,
|
|
1488
|
+
{ reason: "text collision", count: collisions }
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
/** Check that the idColumn option does not name an existing node property. */
|
|
1493
|
+
checkIdColumn() {
|
|
1494
|
+
const name = this.options.idColumn;
|
|
1495
|
+
if (name === null) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const taken = this.nodeColumns.some((plan2) => plan2.column.meta.name === name) || this.idColumns.some((column) => column.meta.name === name);
|
|
1499
|
+
if (!taken) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
this.note(
|
|
1503
|
+
ID_COLUMN_TAKEN_LOSS,
|
|
1504
|
+
`node column "${name}" already exists; the :ID column cannot be named idColumn "${name}"`,
|
|
1505
|
+
name,
|
|
1506
|
+
null
|
|
1507
|
+
);
|
|
1508
|
+
if (this.fatalNodes === null) {
|
|
1509
|
+
this.fatalNodes = new GraphFormatError(
|
|
1510
|
+
"E_COLUMN_EXISTS",
|
|
1511
|
+
`node column "${name}" already exists; choose another idColumn`,
|
|
1512
|
+
{ column: name, domain: "node" }
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/** Count the nodes with more than one stored-id column set. */
|
|
1517
|
+
checkStoredIds() {
|
|
1518
|
+
if (this.idColumns.length < 2) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
let count = 0;
|
|
1522
|
+
for (let i = 0; i < this.snapshot.nodeCount; i++) {
|
|
1523
|
+
let set = 0;
|
|
1524
|
+
for (const column of this.idColumns) {
|
|
1525
|
+
if (column.isSet(i)) {
|
|
1526
|
+
set++;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (set > 1) {
|
|
1530
|
+
count++;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (count > 0) {
|
|
1534
|
+
this.note(
|
|
1535
|
+
MULTIPLE_ID_PROPERTIES_LOSS,
|
|
1536
|
+
`${count} node(s) have more than one stored-id column set; only the first in declaration order is written`,
|
|
1537
|
+
null,
|
|
1538
|
+
count
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Whether a json column is written as Neo4j points (so the generic nested-value note does not apply).
|
|
1544
|
+
* @param name - the column name
|
|
1545
|
+
* @returns true when its plan writes point literals
|
|
1546
|
+
*/
|
|
1547
|
+
writtenAsPoint(name) {
|
|
1548
|
+
for (const plan2 of [...this.nodeColumns, ...this.edgeColumns]) {
|
|
1549
|
+
if (plan2.column.meta.name === name && plan2.scalar.kind === "point") {
|
|
1550
|
+
return true;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return false;
|
|
1554
|
+
}
|
|
1555
|
+
/** Drop the notes about the part that is not written. */
|
|
1556
|
+
filterNotesByPart() {
|
|
1557
|
+
const { part } = this.options;
|
|
1558
|
+
if (part === "all") {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const { snapshot } = this;
|
|
1562
|
+
const keep = this.notes.filter((n) => {
|
|
1563
|
+
if (part === "nodes") {
|
|
1564
|
+
if (EDGE_NOTE_CODES.has(n.code)) {
|
|
1565
|
+
return false;
|
|
1566
|
+
}
|
|
1567
|
+
return n.column === null || !snapshot.edges.has(n.column) || snapshot.nodes.has(n.column);
|
|
1568
|
+
}
|
|
1569
|
+
if (NODE_NOTE_CODES.has(n.code)) {
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
return n.column === null || !snapshot.nodes.has(n.column) || snapshot.edges.has(n.column);
|
|
1573
|
+
});
|
|
1574
|
+
this.notes.length = 0;
|
|
1575
|
+
this.notes.push(...keep);
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* The error export() throws for the requested part, or null when the export can proceed.
|
|
1579
|
+
* @returns the error
|
|
1580
|
+
*/
|
|
1581
|
+
get fatal() {
|
|
1582
|
+
if (this.fatalAll !== null) {
|
|
1583
|
+
return this.fatalAll;
|
|
1584
|
+
}
|
|
1585
|
+
const { part } = this.options;
|
|
1586
|
+
if (part !== "relationships" && this.fatalNodes !== null) {
|
|
1587
|
+
return this.fatalNodes;
|
|
1588
|
+
}
|
|
1589
|
+
return part === "nodes" ? null : this.fatalEdges;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* The id space of a node.
|
|
1593
|
+
* @param index - the node index
|
|
1594
|
+
* @returns the space name, or null
|
|
1595
|
+
*/
|
|
1596
|
+
spaceOf(index) {
|
|
1597
|
+
const { idSpace } = this;
|
|
1598
|
+
if (idSpace === null || !idSpace.isSet(index)) {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
const value = idSpace.value(index);
|
|
1602
|
+
return typeof value === "string" ? value : String(value);
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* The stored-id column of a node: the first one set in declaration order.
|
|
1606
|
+
* @param index - the node index
|
|
1607
|
+
* @returns the column, or null
|
|
1608
|
+
*/
|
|
1609
|
+
storedIdOf(index) {
|
|
1610
|
+
for (const column of this.idColumns) {
|
|
1611
|
+
if (column.isSet(index)) {
|
|
1612
|
+
return column;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* The node sections, then the relationship sections, as CSV lines.
|
|
1619
|
+
* @yields one line at a time (with its line break)
|
|
1620
|
+
* @returns nothing
|
|
1621
|
+
*/
|
|
1622
|
+
*lines() {
|
|
1623
|
+
if (this.fatal !== null) {
|
|
1624
|
+
throw this.fatal;
|
|
1625
|
+
}
|
|
1626
|
+
const { part } = this.options;
|
|
1627
|
+
if (part !== "relationships") {
|
|
1628
|
+
yield* this.nodeLines();
|
|
1629
|
+
}
|
|
1630
|
+
if (part !== "nodes") {
|
|
1631
|
+
yield* this.relationshipLines();
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* The node sections.
|
|
1636
|
+
* @yields one line at a time
|
|
1637
|
+
* @returns nothing
|
|
1638
|
+
*/
|
|
1639
|
+
*nodeLines() {
|
|
1640
|
+
const { snapshot, labels, nodeColumns } = this;
|
|
1641
|
+
const { ids } = snapshot;
|
|
1642
|
+
const { delimiter } = this.options.syntax;
|
|
1643
|
+
const propertyHeaders = nodeColumns.map((plan2) => plan2.header).join(delimiter);
|
|
1644
|
+
let sections = 0;
|
|
1645
|
+
let sectionSpace = null;
|
|
1646
|
+
let sectionIdName = null;
|
|
1647
|
+
const cells = [];
|
|
1648
|
+
for (let i = 0; i < snapshot.nodeCount; i++) {
|
|
1649
|
+
const space = this.spaceOf(i);
|
|
1650
|
+
const stored = this.storedIdOf(i);
|
|
1651
|
+
const idName = stored === null ? this.options.idColumn ?? "" : stored.meta.name;
|
|
1652
|
+
if (sections === 0 || space !== sectionSpace || idName !== sectionIdName) {
|
|
1653
|
+
sections++;
|
|
1654
|
+
sectionSpace = space;
|
|
1655
|
+
sectionIdName = idName;
|
|
1656
|
+
const header = [formatHeaderField(idName, ID_TYPE, space)];
|
|
1657
|
+
if (labels !== null) {
|
|
1658
|
+
header.push(":LABEL");
|
|
1659
|
+
}
|
|
1660
|
+
if (propertyHeaders.length > 0) {
|
|
1661
|
+
header.push(propertyHeaders);
|
|
1662
|
+
}
|
|
1663
|
+
yield `${header.join(delimiter)}
|
|
1664
|
+
`;
|
|
1665
|
+
}
|
|
1666
|
+
cells.length = 0;
|
|
1667
|
+
cells.push(this.cell(idText(ids.idOf(i))));
|
|
1668
|
+
if (labels !== null) {
|
|
1669
|
+
cells.push(this.cell(this.labelsText(i)));
|
|
1670
|
+
}
|
|
1671
|
+
for (const plan2 of nodeColumns) {
|
|
1672
|
+
cells.push(this.cell(this.valueText(plan2, i)));
|
|
1673
|
+
}
|
|
1674
|
+
yield `${cells.join(delimiter)}
|
|
1675
|
+
`;
|
|
1676
|
+
}
|
|
1677
|
+
if (sections === 0) {
|
|
1678
|
+
const header = [formatHeaderField(this.options.idColumn ?? "", ID_TYPE, null)];
|
|
1679
|
+
if (labels !== null) {
|
|
1680
|
+
header.push(":LABEL");
|
|
1681
|
+
}
|
|
1682
|
+
if (propertyHeaders.length > 0) {
|
|
1683
|
+
header.push(propertyHeaders);
|
|
1684
|
+
}
|
|
1685
|
+
yield `${header.join(delimiter)}
|
|
1686
|
+
`;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* The relationship sections.
|
|
1691
|
+
* @yields one line at a time
|
|
1692
|
+
* @returns nothing
|
|
1693
|
+
*/
|
|
1694
|
+
*relationshipLines() {
|
|
1695
|
+
const { snapshot, kind, weights, edgeColumns, folding } = this;
|
|
1696
|
+
const { ids } = snapshot;
|
|
1697
|
+
const { delimiter } = this.options.syntax;
|
|
1698
|
+
const list = snapshot.edgeList();
|
|
1699
|
+
const propertyHeaders = edgeColumns.map((plan2) => plan2.header).join(delimiter);
|
|
1700
|
+
const weightHeader = weights === null ? null : formatHeaderField(this.options.weightColumn ?? "weight", "double", null);
|
|
1701
|
+
let sections = 0;
|
|
1702
|
+
let sectionStart = null;
|
|
1703
|
+
let sectionEnd = null;
|
|
1704
|
+
const cells = [];
|
|
1705
|
+
const headerOf = (startSpace, endSpace) => {
|
|
1706
|
+
const header = [formatHeaderField("", "START_ID", startSpace), formatHeaderField("", "END_ID", endSpace)];
|
|
1707
|
+
if (kind !== null) {
|
|
1708
|
+
header.push(":TYPE");
|
|
1709
|
+
}
|
|
1710
|
+
if (weightHeader !== null) {
|
|
1711
|
+
header.push(weightHeader);
|
|
1712
|
+
}
|
|
1713
|
+
if (propertyHeaders.length > 0) {
|
|
1714
|
+
header.push(propertyHeaders);
|
|
1715
|
+
}
|
|
1716
|
+
return `${header.join(delimiter)}
|
|
1717
|
+
`;
|
|
1718
|
+
};
|
|
1719
|
+
for (let e = 0; e < snapshot.edgeCount; e++) {
|
|
1720
|
+
if (folding.folded(e)) {
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
const u = list.src[e];
|
|
1724
|
+
const v = list.dst[e];
|
|
1725
|
+
const startSpace = this.spaceOf(u);
|
|
1726
|
+
const endSpace = this.spaceOf(v);
|
|
1727
|
+
if (sections === 0 || startSpace !== sectionStart || endSpace !== sectionEnd) {
|
|
1728
|
+
sections++;
|
|
1729
|
+
sectionStart = startSpace;
|
|
1730
|
+
sectionEnd = endSpace;
|
|
1731
|
+
yield headerOf(startSpace, endSpace);
|
|
1732
|
+
}
|
|
1733
|
+
cells.length = 0;
|
|
1734
|
+
cells.push(this.cell(idText(ids.idOf(u))), this.cell(idText(ids.idOf(v))));
|
|
1735
|
+
if (kind !== null) {
|
|
1736
|
+
cells.push(this.cell(kind.isSet(e) ? textOf(kind, e) : null));
|
|
1737
|
+
}
|
|
1738
|
+
if (weights !== null) {
|
|
1739
|
+
cells.push(this.cell(weights.text(e)));
|
|
1740
|
+
}
|
|
1741
|
+
for (const plan2 of edgeColumns) {
|
|
1742
|
+
cells.push(this.cell(this.valueText(plan2, e)));
|
|
1743
|
+
}
|
|
1744
|
+
yield `${cells.join(delimiter)}
|
|
1745
|
+
`;
|
|
1746
|
+
}
|
|
1747
|
+
if (sections === 0) {
|
|
1748
|
+
yield headerOf(null, null);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* A CSV cell: empty for an unset value, a quoted empty string for a set empty string, the
|
|
1753
|
+
* quoted text otherwise.
|
|
1754
|
+
* @param text - the value text, or null for unset
|
|
1755
|
+
* @returns the cell as written
|
|
1756
|
+
*/
|
|
1757
|
+
cell(text) {
|
|
1758
|
+
if (text === null) {
|
|
1759
|
+
return "";
|
|
1760
|
+
}
|
|
1761
|
+
const { quote } = this.options.syntax;
|
|
1762
|
+
if (text.length === 0) {
|
|
1763
|
+
return quote + quote;
|
|
1764
|
+
}
|
|
1765
|
+
return quoteCsvCell(text, this.options.syntax.delimiter);
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* The `:LABEL` cell of a node.
|
|
1769
|
+
* @param index - the node index
|
|
1770
|
+
* @returns the labels joined by the array delimiter, or null when unset
|
|
1771
|
+
*/
|
|
1772
|
+
labelsText(index) {
|
|
1773
|
+
const { labels } = this;
|
|
1774
|
+
if (labels === null || !labels.isSet(index)) {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
if (labels.dtype === "list") {
|
|
1778
|
+
return labels.sliceOf(index).map((item) => String(item)).join(this.options.arrayDelimiter);
|
|
1779
|
+
}
|
|
1780
|
+
return textOf(labels, index);
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* The text of one property cell.
|
|
1784
|
+
* @param plan - the column plan
|
|
1785
|
+
* @param row - the row
|
|
1786
|
+
* @returns the text, or null when unset
|
|
1787
|
+
*/
|
|
1788
|
+
valueText(plan2, row) {
|
|
1789
|
+
const { column, scalar, companion } = plan2;
|
|
1790
|
+
if (!column.isSet(row)) {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
if (plan2.list) {
|
|
1794
|
+
const items = column.dtype === "list" ? column.sliceOf(row) : column.value(row);
|
|
1795
|
+
const texts = [];
|
|
1796
|
+
for (const item of Array.from(items)) {
|
|
1797
|
+
if (Array.isArray(item) || ArrayBuffer.isView(item)) {
|
|
1798
|
+
for (const component of Array.from(item)) {
|
|
1799
|
+
texts.push(formatScalar(component, scalar, null));
|
|
1800
|
+
}
|
|
1801
|
+
} else {
|
|
1802
|
+
texts.push(formatScalar(item, scalar, null));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
return texts.join(this.options.arrayDelimiter);
|
|
1806
|
+
}
|
|
1807
|
+
const text = companion !== null && companion.isSet(row) ? textOf(companion, row) : null;
|
|
1808
|
+
return formatScalar(column.dtype === "json" ? column.values[row] : column.value(row), scalar, text);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function isStoredId(meta) {
|
|
1812
|
+
return meta.role === null && meta.dtype === "string" && meta.origin !== null && meta.origin.format === NEO4J && meta.origin.type === ID_TYPE;
|
|
1813
|
+
}
|
|
1814
|
+
function isVizRole(role) {
|
|
1815
|
+
return role === "color" || role === "size" || role === "shape" || role === "thickness";
|
|
1816
|
+
}
|
|
1817
|
+
function declaredScalar(declared) {
|
|
1818
|
+
const trimmed = declared.trim();
|
|
1819
|
+
return trimmed.endsWith("[]") ? trimmed.slice(0, -2) : trimmed;
|
|
1820
|
+
}
|
|
1821
|
+
function kindOf(spec) {
|
|
1822
|
+
switch (spec.kind) {
|
|
1823
|
+
case "boolean":
|
|
1824
|
+
return "boolean";
|
|
1825
|
+
case "integer":
|
|
1826
|
+
case "long":
|
|
1827
|
+
return "integer";
|
|
1828
|
+
case "float":
|
|
1829
|
+
return "float";
|
|
1830
|
+
case "double":
|
|
1831
|
+
return "double";
|
|
1832
|
+
case "string":
|
|
1833
|
+
case "duration":
|
|
1834
|
+
return "string";
|
|
1835
|
+
case "temporal":
|
|
1836
|
+
return "temporal";
|
|
1837
|
+
case "point":
|
|
1838
|
+
return "point";
|
|
1839
|
+
case "json":
|
|
1840
|
+
return "json";
|
|
1841
|
+
default: {
|
|
1842
|
+
const name = spec.kind;
|
|
1843
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown value kind ${name}`, { kind: name });
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function defaultScalar(dtype) {
|
|
1848
|
+
switch (dtype) {
|
|
1849
|
+
case "f32":
|
|
1850
|
+
return { type: "float", kind: "float", temporal: null };
|
|
1851
|
+
case "f64":
|
|
1852
|
+
return { type: "double", kind: "double", temporal: null };
|
|
1853
|
+
case "i32":
|
|
1854
|
+
case "u8":
|
|
1855
|
+
return { type: "int", kind: "integer", temporal: null };
|
|
1856
|
+
case "u32":
|
|
1857
|
+
return { type: "long", kind: "integer", temporal: null };
|
|
1858
|
+
case "bool":
|
|
1859
|
+
return { type: "boolean", kind: "boolean", temporal: null };
|
|
1860
|
+
case "dict":
|
|
1861
|
+
case "string":
|
|
1862
|
+
return { type: "string", kind: "string", temporal: null };
|
|
1863
|
+
case "json":
|
|
1864
|
+
return { type: "string", kind: "json", temporal: null };
|
|
1865
|
+
case "list":
|
|
1866
|
+
return { type: "string", kind: "json", temporal: null };
|
|
1867
|
+
default: {
|
|
1868
|
+
const name = dtype;
|
|
1869
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown dtype ${name}`, { dtype: name });
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
function formatScalar(value, scalar, text) {
|
|
1874
|
+
switch (scalar.kind) {
|
|
1875
|
+
case "integer":
|
|
1876
|
+
return typeof value === "number" ? formatInteger(value) : String(value);
|
|
1877
|
+
case "float":
|
|
1878
|
+
return typeof value === "number" ? formatF32(value) : String(value);
|
|
1879
|
+
case "double":
|
|
1880
|
+
return typeof value === "number" ? formatF64(value) : String(value);
|
|
1881
|
+
case "boolean":
|
|
1882
|
+
return value === true ? "true" : "false";
|
|
1883
|
+
case "string":
|
|
1884
|
+
return typeof value === "string" ? value : String(value);
|
|
1885
|
+
case "temporal":
|
|
1886
|
+
if (text !== null) {
|
|
1887
|
+
return text;
|
|
1888
|
+
}
|
|
1889
|
+
return typeof value === "number" && scalar.temporal !== null ? formatTemporal(value, scalar.temporal) : String(value);
|
|
1890
|
+
case "point":
|
|
1891
|
+
return formatPoint(value);
|
|
1892
|
+
case "json":
|
|
1893
|
+
return JSON.stringify(value) ?? "";
|
|
1894
|
+
default: {
|
|
1895
|
+
const name = scalar.kind;
|
|
1896
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown cell kind ${name}`, { kind: name });
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
function formatPoint(value) {
|
|
1901
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1902
|
+
return JSON.stringify(value) ?? "";
|
|
1903
|
+
}
|
|
1904
|
+
const parts = [];
|
|
1905
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1906
|
+
if (typeof item === "number") {
|
|
1907
|
+
parts.push(`${key}:${formatF64(item)}`);
|
|
1908
|
+
} else if (typeof item === "string") {
|
|
1909
|
+
parts.push(`${key}:'${item.replace(/'/g, "")}'`);
|
|
1910
|
+
} else {
|
|
1911
|
+
return JSON.stringify(value) ?? "";
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return `{${parts.join(", ")}}`;
|
|
1915
|
+
}
|
|
1916
|
+
function idText(id) {
|
|
1917
|
+
if (typeof id === "string") {
|
|
1918
|
+
return id;
|
|
1919
|
+
}
|
|
1920
|
+
return Number.isInteger(id) ? formatInteger(id) : String(id);
|
|
1921
|
+
}
|
|
1922
|
+
function textOf(column, row) {
|
|
1923
|
+
switch (column.dtype) {
|
|
1924
|
+
case "string":
|
|
1925
|
+
return column.valueAt(row);
|
|
1926
|
+
case "dict":
|
|
1927
|
+
return String(column.value(row));
|
|
1928
|
+
case "list":
|
|
1929
|
+
return column.sliceOf(row).map(String).join(",");
|
|
1930
|
+
case "json":
|
|
1931
|
+
return JSON.stringify(column.values[row]) ?? "";
|
|
1932
|
+
case "bool":
|
|
1933
|
+
return column.value(row) === true ? "true" : "false";
|
|
1934
|
+
default: {
|
|
1935
|
+
const value = column.value(row);
|
|
1936
|
+
return typeof value === "number" ? String(value) : Array.from(value).join(",");
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
const neo4jExporter = Object.freeze({
|
|
1941
|
+
format: NEO4J,
|
|
1942
|
+
capabilities: NEO4J_CAPABILITIES,
|
|
1943
|
+
/**
|
|
1944
|
+
* Pre-flight: what export() would lose.
|
|
1945
|
+
* @param snapshot - the snapshot
|
|
1946
|
+
* @param options - format-specific and common options
|
|
1947
|
+
* @returns the notes
|
|
1948
|
+
*/
|
|
1949
|
+
check(snapshot, options) {
|
|
1950
|
+
return Object.freeze([...plan(snapshot, options).notes]);
|
|
1951
|
+
},
|
|
1952
|
+
/**
|
|
1953
|
+
* Write the snapshot as UTF-8 chunks.
|
|
1954
|
+
* @param snapshot - the snapshot
|
|
1955
|
+
* @param options - format-specific and common options
|
|
1956
|
+
* @returns the chunks
|
|
1957
|
+
*/
|
|
1958
|
+
export(snapshot, options) {
|
|
1959
|
+
return encodeChunks(plan(snapshot, options).lines());
|
|
1960
|
+
},
|
|
1961
|
+
/**
|
|
1962
|
+
* Write the snapshot as one string.
|
|
1963
|
+
* @param snapshot - the snapshot
|
|
1964
|
+
* @param options - format-specific and common options
|
|
1965
|
+
* @returns the document
|
|
1966
|
+
*/
|
|
1967
|
+
exportToString(snapshot, options) {
|
|
1968
|
+
return joinText(plan(snapshot, options).lines());
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
function plan(snapshot, options) {
|
|
1972
|
+
return new ExportPlan(snapshot, resolveNeo4jExportOptions(options), resolveExportOptions(options));
|
|
1973
|
+
}
|
|
1974
|
+
const NEO4J_ISSUE = Object.freeze({
|
|
1975
|
+
/** The input holds invalid UTF-8 (fatal). */
|
|
1976
|
+
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
1977
|
+
/** An unterminated quoted field (fatal). */
|
|
1978
|
+
CSV_UNCLOSED_QUOTE: UNCLOSED_QUOTE_CODE,
|
|
1979
|
+
/** Text after a closing quote (fatal). */
|
|
1980
|
+
CSV_QUOTE: BAD_QUOTE_CODE,
|
|
1981
|
+
/** No header or a malformed header (fatal). */
|
|
1982
|
+
HEADER: HEADER_CODE,
|
|
1983
|
+
/** A row with a different field count than the header. */
|
|
1984
|
+
COLUMN_COUNT: COLUMN_COUNT_CODE,
|
|
1985
|
+
/** A node row without an id. */
|
|
1986
|
+
MISSING_ID: MISSING_ID_CODE,
|
|
1987
|
+
/** A relationship row without a start or end id. */
|
|
1988
|
+
MISSING_ENDPOINT: MISSING_ENDPOINT_CODE,
|
|
1989
|
+
/** A node id repeated in one id space (last write wins). */
|
|
1990
|
+
DUPLICATE_NODE: DUPLICATE_NODE_CODE,
|
|
1991
|
+
/** A node id declared in two id spaces. */
|
|
1992
|
+
ID_SPACE_COLLISION: ID_SPACE_COLLISION_CODE,
|
|
1993
|
+
/** Two id texts merged into one number under ids "number". */
|
|
1994
|
+
ID_MERGED: ID_MERGED_CODE,
|
|
1995
|
+
/** A header brace option the importer does not apply. */
|
|
1996
|
+
HEADER_OPTION_IGNORED: HEADER_OPTION_CODE,
|
|
1997
|
+
/** A declared type the format does not define (kept as string). */
|
|
1998
|
+
UNKNOWN_ATTR_TYPE: UNKNOWN_ATTR_TYPE_CODE,
|
|
1999
|
+
/** A long value beyond 2^53 rounded. */
|
|
2000
|
+
PRECISION: PRECISION_CODE,
|
|
2001
|
+
/** A column renamed `<name>#<id>` because the name was taken (design section 5.6). */
|
|
2002
|
+
COLUMN_RENAMED: COLUMN_RENAMED_CODE,
|
|
2003
|
+
/** A role the caller's sink already holds. */
|
|
2004
|
+
ROLE_TAKEN: ROLE_TAKEN_CODE,
|
|
2005
|
+
/** A common option the importer has no use for (nodeIdFrom, defaultDirected, ...). */
|
|
2006
|
+
OPTION_IGNORED: OPTION_IGNORED_CODE,
|
|
2007
|
+
/** A builder-policy option the sink does not honour. */
|
|
2008
|
+
SINK_OPTION: SINK_OPTION_CODE
|
|
2009
|
+
});
|
|
2010
|
+
const NEO4J_LOSS = Object.freeze({
|
|
2011
|
+
/** `:IGNORE` columns skipped on import. */
|
|
2012
|
+
IGNORED_COLUMNS: IGNORED_COLUMNS_LOSS,
|
|
2013
|
+
/** Undirected edges (an undirected snapshot, the folded pairs of a mixed one) written as directed relationships. */
|
|
2014
|
+
UNDIRECTED_AS_DIRECTED: UNDIRECTED_LOSS,
|
|
2015
|
+
/** A mutual pair written as two directed relationships without its mark. */
|
|
2016
|
+
MUTUAL_EXPANDED: MUTUAL_EXPANDED_CODE,
|
|
2017
|
+
/** A role column Neo4j has no slot for (label, ...) written as a plain property. */
|
|
2018
|
+
ROLE_DROPPED: ROLE_DROPPED_CODE,
|
|
2019
|
+
/** A plain `weight` edge column reads back as THE weight (the importer's weightFrom default). */
|
|
2020
|
+
WEIGHT_KEY_CLASH: WEIGHT_KEY_CLASH_CODE,
|
|
2021
|
+
/** A node id whose text re-imports as another type under the canonical rule. */
|
|
2022
|
+
ID_TEXT_TYPE: ID_TEXT_TYPE_LOSS,
|
|
2023
|
+
/** A number and a string id with the same text; export() throws. */
|
|
2024
|
+
ID_TEXT_COLLISION: ID_TEXT_COLLISION_LOSS,
|
|
2025
|
+
/** The weight column name is taken by an edge column; export() throws. */
|
|
2026
|
+
WEIGHT_COLUMN_TAKEN: WEIGHT_COLUMN_TAKEN_LOSS,
|
|
2027
|
+
/** The id column name is taken by a node column; export() throws. */
|
|
2028
|
+
ID_COLUMN_TAKEN: ID_COLUMN_TAKEN_LOSS,
|
|
2029
|
+
/** Several stored-id properties; one becomes the `:ID` column. */
|
|
2030
|
+
MULTIPLE_ID_PROPERTIES: MULTIPLE_ID_PROPERTIES_LOSS,
|
|
2031
|
+
/** An integer-typed column with non-integral values written as double. */
|
|
2032
|
+
DECLARED_TYPE_CHANGED: DECLARED_TYPE_CHANGED_LOSS,
|
|
2033
|
+
/** A list item containing the array delimiter, which Neo4j cannot escape. */
|
|
2034
|
+
ARRAY_DELIMITER: ARRAY_DELIMITER_LOSS
|
|
2035
|
+
});
|
|
2036
|
+
export {
|
|
2037
|
+
ID_SPACE_COLUMN,
|
|
2038
|
+
LABELS_COLUMN,
|
|
2039
|
+
NEO4J_CAPABILITIES,
|
|
2040
|
+
NEO4J_ISSUE,
|
|
2041
|
+
NEO4J_LOSS,
|
|
2042
|
+
TYPE_COLUMN,
|
|
2043
|
+
neo4jExporter,
|
|
2044
|
+
neo4jImporter
|
|
2045
|
+
};
|
|
2046
|
+
//# sourceMappingURL=neo4j.js.map
|