@graphty/graph-io 0.0.0 → 0.2.1

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